← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/launchpad:add-build-upload-processing-for-rock-recipes into launchpad:master

 

Jürgen Gmach has proposed merging ~jugmac00/launchpad:add-build-upload-processing-for-rock-recipes into launchpad:master with ~jugmac00/launchpad:add-build-behaviour-for-rock-recipes as a prerequisite.

Commit message:
Add build upload processing for rock recipes

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/472981

similar to https://git.launchpad.net/launchpad/commit/?id=06441a35383386d9d15f682de64b0d7ad306d4fb
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:add-build-upload-processing-for-rock-recipes into launchpad:master.
diff --git a/lib/lp/archiveuploader/rockrecipeupload.py b/lib/lp/archiveuploader/rockrecipeupload.py
new file mode 100644
index 0000000..d7b9152
--- /dev/null
+++ b/lib/lp/archiveuploader/rockrecipeupload.py
@@ -0,0 +1,66 @@
+# Copyright 2024 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Process a rock recipe upload."""
+
+__all__ = [
+    "RockRecipeUpload",
+]
+
+import os
+
+from zope.component import getUtility
+
+from lp.archiveuploader.utils import UploadError
+from lp.buildmaster.enums import BuildStatus
+from lp.services.helpers import filenameToContentType
+from lp.services.librarian.interfaces import ILibraryFileAliasSet
+
+
+class RockRecipeUpload:
+    """A rock recipe upload."""
+
+    def __init__(self, upload_path, logger):
+        """Create a `RockRecipeUpload`.
+
+        :param upload_path: A directory containing files to upload.
+        :param logger: The logger to be used.
+        """
+        self.upload_path = upload_path
+        self.logger = logger
+
+        self.librarian = getUtility(ILibraryFileAliasSet)
+
+    def process(self, build):
+        """Process this upload, loading it into the database."""
+        self.logger.debug("Beginning processing.")
+
+        found_rock = False
+        rock_paths = []
+        for dirpath, _, filenames in os.walk(self.upload_path):
+            if dirpath == self.upload_path:
+                # All relevant files will be in a subdirectory.
+                continue
+            for rock_file in sorted(filenames):
+                if rock_file.endswith(".rock"):
+                    found_rock = True
+                rock_paths.append(os.path.join(dirpath, rock_file))
+
+        if not found_rock:
+            raise UploadError("Build did not produce any rocks.")
+
+        for rock_path in rock_paths:
+            libraryfile = self.librarian.create(
+                os.path.basename(rock_path),
+                os.stat(rock_path).st_size,
+                open(rock_path, "rb"),
+                filenameToContentType(rock_path),
+                restricted=build.is_private,
+            )
+            build.addFile(libraryfile)
+
+        # The master verifies the status to confirm successful upload.
+        self.logger.debug("Updating %s" % build.title)
+        build.updateStatus(BuildStatus.FULLYBUILT)
+
+        self.logger.debug("Finished upload.")
diff --git a/lib/lp/archiveuploader/tests/test_rockrecipeupload.py b/lib/lp/archiveuploader/tests/test_rockrecipeupload.py
new file mode 100644
index 0000000..28a12cb
--- /dev/null
+++ b/lib/lp/archiveuploader/tests/test_rockrecipeupload.py
@@ -0,0 +1,78 @@
+# Copyright 2024 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for `RockRecipeUpload`."""
+
+import os
+
+from storm.store import Store
+
+from lp.archiveuploader.tests.test_uploadprocessor import (
+    TestUploadProcessorBase,
+)
+from lp.archiveuploader.uploadprocessor import UploadHandler, UploadStatusEnum
+from lp.buildmaster.enums import BuildStatus
+from lp.rocks.interfaces.rockrecipe import ROCK_RECIPE_ALLOW_CREATE
+from lp.services.features.testing import FeatureFixture
+from lp.services.osutils import write_file
+
+
+class TestRockRecipeUploads(TestUploadProcessorBase):
+    """End-to-end tests of rock recipe uploads."""
+
+    def setUp(self):
+        super().setUp()
+        self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
+
+        self.setupBreezy()
+
+        self.switchToAdmin()
+        self.build = self.factory.makeRockRecipeBuild(
+            distro_arch_series=self.breezy["i386"]
+        )
+        self.build.updateStatus(BuildStatus.UPLOADING)
+        Store.of(self.build).flush()
+        self.switchToUploader()
+        self.options.context = "buildd"
+
+        self.uploadprocessor = self.getUploadProcessor(
+            self.layer.txn, builds=True
+        )
+
+    def test_sets_build_and_state_123(self):
+        # The upload processor uploads files and sets the correct status.
+        self.assertFalse(self.build.verifySuccessfulUpload())
+        upload_dir = os.path.join(
+            self.incoming_folder, "test", str(self.build.id), "ubuntu"
+        )
+        write_file(os.path.join(upload_dir, "foo_0_all.rock"), b"rock")
+        write_file(os.path.join(upload_dir, "foo_0_all.manifest"), b"manifest")
+        handler = UploadHandler.forProcessor(
+            self.uploadprocessor, self.incoming_folder, "test", self.build
+        )
+        result = handler.processRockRecipe(self.log)
+        self.assertEqual(
+            UploadStatusEnum.ACCEPTED,
+            result,
+            "Rock upload failed\nGot: %s" % self.log.getLogBuffer(),
+        )
+        self.assertEqual(BuildStatus.FULLYBUILT, self.build.status)
+        self.assertTrue(self.build.verifySuccessfulUpload())
+
+    def test_requires_rock(self):
+        # The upload processor fails if the upload does not contain any
+        # .rock files.
+        self.assertFalse(self.build.verifySuccessfulUpload())
+        upload_dir = os.path.join(
+            self.incoming_folder, "test", str(self.build.id), "ubuntu"
+        )
+        write_file(os.path.join(upload_dir, "foo_0_all.manifest"), b"manifest")
+        handler = UploadHandler.forProcessor(
+            self.uploadprocessor, self.incoming_folder, "test", self.build
+        )
+        result = handler.processRockRecipe(self.log)
+        self.assertEqual(UploadStatusEnum.REJECTED, result)
+        self.assertIn(
+            "ERROR Build did not produce any rocks.", self.log.getLogBuffer()
+        )
+        self.assertFalse(self.build.verifySuccessfulUpload())
diff --git a/lib/lp/archiveuploader/uploadprocessor.py b/lib/lp/archiveuploader/uploadprocessor.py
index baa903b..1289241 100644
--- a/lib/lp/archiveuploader/uploadprocessor.py
+++ b/lib/lp/archiveuploader/uploadprocessor.py
@@ -44,7 +44,6 @@ worst of the results from the various changes files found (in the order
 above, failed being worst).
 
 """
-
 import os
 import shutil
 import sys
@@ -61,6 +60,7 @@ from lp.archiveuploader.nascentupload import (
     NascentUpload,
 )
 from lp.archiveuploader.ocirecipeupload import OCIRecipeUpload
+from lp.archiveuploader.rockrecipeupload import RockRecipeUpload
 from lp.archiveuploader.snapupload import SnapUpload
 from lp.archiveuploader.uploadpolicy import (
     BuildDaemonUploadPolicy,
@@ -77,6 +77,7 @@ from lp.code.interfaces.sourcepackagerecipebuild import (
 from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.registry.interfaces.person import IPersonSet
+from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuild
 from lp.services.log.logger import BufferLogger
 from lp.services.statsd.interfaces.statsd_client import IStatsdClient
 from lp.services.webapp.adapter import (
@@ -775,6 +776,32 @@ class BuildUploadHandler(UploadHandler):
             self.processor.ztm.abort()
             raise
 
+    def processRockRecipe(self, logger=None):
+        """Process a rock recipe upload."""
+        assert IRockRecipeBuild.providedBy(self.build)
+        if logger is None:
+            logger = self.processor.log
+        try:
+            logger.info("Processing rock upload %s" % self.upload_path)
+            RockRecipeUpload(self.upload_path, logger).process(self.build)
+
+            if self.processor.dry_run:
+                logger.info("Dry run, aborting transaction.")
+                self.processor.ztm.abort()
+            else:
+                logger.info(
+                    "Committing the transaction and any mails associated "
+                    "with this upload."
+                )
+                self.processor.ztm.commit()
+            return UploadStatusEnum.ACCEPTED
+        except UploadError as e:
+            logger.error(str(e))
+            return UploadStatusEnum.REJECTED
+        except BaseException:
+            self.processor.ztm.abort()
+            raise
+
     def process(self):
         """Process an upload that is the result of a build.
 
@@ -830,6 +857,8 @@ class BuildUploadHandler(UploadHandler):
                 result = self.processOCIRecipe(logger)
             elif ICharmRecipeBuild.providedBy(self.build):
                 result = self.processCharmRecipe(logger)
+            elif IRockRecipeBuild.providedBy(self.build):
+                result = self.processRockRecipe(logger)
             elif ICIBuild.providedBy(self.build):
                 result = self.processCIResult(logger)
             else: