← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~jugmac00/launchpad:enable-filtering-when-requesting-rock-builds into launchpad:master

 

Jürgen Gmach has proposed merging ~jugmac00/launchpad:enable-filtering-when-requesting-rock-builds into launchpad:master.

Commit message:
Enable requesting a subset of architectures for rock builds

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/473987
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:enable-filtering-when-requesting-rock-builds into launchpad:master.
diff --git a/lib/lp/rocks/adapters/buildarch.py b/lib/lp/rocks/adapters/buildarch.py
index 57ce120..d78c7f0 100644
--- a/lib/lp/rocks/adapters/buildarch.py
+++ b/lib/lp/rocks/adapters/buildarch.py
@@ -131,7 +131,9 @@ class UnifiedRockBaseConfiguration:
         self.run_on = list(build_on) if run_on is None else run_on
 
     @classmethod
-    def from_dict(cls, rockcraft_data, supported_arches):
+    def from_dict(
+        cls, rockcraft_data, supported_arches, requested_architectures
+    ):
         base = rockcraft_data["base"]
         if isinstance(base, str):
             if base == "bare" and "build-base" not in rockcraft_data:
@@ -155,9 +157,6 @@ class UnifiedRockBaseConfiguration:
             # interpreted as a float. So we convert it to a string.
             base_channel = str(base["channel"])
 
-        # XXX jugmac00 2024-09-18: Find out if we need 'build-base' or not.
-        # There is no existing code that is using that.
-
         platforms = rockcraft_data.get("platforms")
         if not platforms:
             raise MissingPropertyError(
@@ -165,6 +164,13 @@ class UnifiedRockBaseConfiguration:
             )
         configs = []
         for platform, configuration in platforms.items():
+            # when we request specific architectures, we need to check
+            # whether they are defined in the rockcraft.yaml configuration
+            # when we do not request specific arches, we build for all
+            # platforms defined in the configuration files
+            if requested_architectures:
+                if platform not in requested_architectures:
+                    continue
             # The 'platforms' property and its values look like
             # platforms:
             #   ubuntu-amd64:
@@ -209,17 +215,22 @@ class UnifiedRockBaseConfiguration:
         return configs
 
 
-def determine_instances_to_build(rockcraft_data, supported_arches):
+def determine_instances_to_build(
+    rockcraft_data, supported_arches, requested_architectures=None
+):
     """Return a list of instances to build based on rockcraft.yaml.
 
     :param rockcraft_data: A parsed rockcraft.yaml.
     :param supported_arches: An ordered list of all `DistroArchSeries` that
         we can create builds for.  Note that these may span multiple
         `DistroSeries`.
+    :param requested_architectures: A list of requested architectures, or None;
+        for the latter case we build all architectures specified in the
+        rockcraft.yaml configuration file.
     :return: A list of `DistroArchSeries`.
     """
     configs = UnifiedRockBaseConfiguration.from_dict(
-        rockcraft_data, supported_arches
+        rockcraft_data, supported_arches, requested_architectures
     )
     # Ensure that multiple `run-on` items don't overlap; this is ambiguous
     # and forbidden by rockcraft.
diff --git a/lib/lp/rocks/interfaces/rockrecipe.py b/lib/lp/rocks/interfaces/rockrecipe.py
index 4f1f677..ca87032 100644
--- a/lib/lp/rocks/interfaces/rockrecipe.py
+++ b/lib/lp/rocks/interfaces/rockrecipe.py
@@ -69,6 +69,7 @@ from lp.app.errors import NameLookupFailed
 from lp.app.interfaces.informationtype import IInformationType
 from lp.app.validators.name import name_validator
 from lp.app.validators.path import path_does_not_escape
+from lp.buildmaster.interfaces.processor import IProcessor
 from lp.code.interfaces.gitref import IGitRef
 from lp.code.interfaces.gitrepository import IGitRepository
 from lp.registry.interfaces.person import IPerson
@@ -377,7 +378,12 @@ class IRockRecipeView(Interface):
             ),
             key_type=TextLine(),
             required=False,
-        )
+        ),
+        architectures=List(
+            title=_("The list of architectures to build for this recipe."),
+            value_type=Reference(schema=IProcessor),
+            required=False,
+        ),
     )
     @export_factory_operation(IRockRecipeBuildRequest, [])
     @operation_for_version("devel")
diff --git a/lib/lp/rocks/model/rockrecipe.py b/lib/lp/rocks/model/rockrecipe.py
index b4057e7..0a94562 100644
--- a/lib/lp/rocks/model/rockrecipe.py
+++ b/lib/lp/rocks/model/rockrecipe.py
@@ -532,27 +532,18 @@ class RockRecipe(StormBase):
             rockcraft_data = removeSecurityProxy(
                 getUtility(IRockRecipeSet).getRockcraftYaml(self)
             )
-
-            # Sort by (Distribution.id, DistroSeries.id, Processor.id) for
-            # determinism.  This is chosen to be a similar order as in
-            # BinaryPackageBuildSet.createForSource, to minimize confusion.
-            supported_arches = [
-                das
-                for das in sorted(
-                    self.getAllowedArchitectures(),
-                    key=attrgetter(
-                        "distroseries.distribution.id",
-                        "distroseries.id",
-                        "processor.id",
-                    ),
-                )
-                if (
-                    architectures is None
-                    or das.architecturetag in architectures
-                )
-            ]
+            supported_arches = sorted(
+                self.getAllowedArchitectures(),
+                key=attrgetter(
+                    "distroseries.distribution.id",
+                    "distroseries.id",
+                    "processor.id",
+                ),
+            )
             instances_to_build = determine_instances_to_build(
-                rockcraft_data, supported_arches
+                rockcraft_data,
+                supported_arches=supported_arches,
+                requested_architectures=architectures,
             )
         except Exception as e:
             if not allow_failures:
diff --git a/lib/lp/rocks/model/rockrecipejob.py b/lib/lp/rocks/model/rockrecipejob.py
index f21df96..7587e63 100644
--- a/lib/lp/rocks/model/rockrecipejob.py
+++ b/lib/lp/rocks/model/rockrecipejob.py
@@ -174,6 +174,14 @@ class RockRecipeRequestBuildsJob(RockRecipeJobDerived):
     @classmethod
     def create(cls, recipe, requester, channels=None, architectures=None):
         """See `IRockRecipeRequestBuildsJobSource`."""
+        # architectures can be a iterable of strings or Processors
+        # in the latter case, we need to convert them to strings
+        if architectures and all(
+            not (isinstance(arch, str)) for arch in architectures
+        ):
+            architectures = [
+                architecture.name for architecture in architectures
+            ]
         metadata = {
             "requester": requester.id,
             "channels": channels,
diff --git a/lib/lp/rocks/tests/test_rockrecipe.py b/lib/lp/rocks/tests/test_rockrecipe.py
index ae56494..dbda557 100644
--- a/lib/lp/rocks/tests/test_rockrecipe.py
+++ b/lib/lp/rocks/tests/test_rockrecipe.py
@@ -1860,6 +1860,121 @@ class TestRockRecipeWebservice(TestCaseWithFactory):
                 ),
             )
 
+    def test_requestBuilds_with_architectures(self):
+        # Requests for builds for all relevant architectures can be
+        # performed over the webservice, and the returned entry indicates
+        # the status of the asynchronous job.
+        distroseries = self.factory.makeDistroSeries(
+            distribution=getUtility(ILaunchpadCelebrities).ubuntu,
+            registrant=self.person,
+        )
+        amd640 = self.factory.makeProcessor(
+            name="amd640", supports_virtualized=True
+        )
+        risc500 = self.factory.makeProcessor(
+            name="risc500", supports_virtualized=True
+        )
+        s400x = self.factory.makeProcessor(
+            name="s400x", supports_virtualized=True
+        )
+        processors = [amd640, risc500, s400x]
+        for processor in processors:
+            self.makeBuildableDistroArchSeries(
+                distroseries=distroseries,
+                architecturetag=processor.name,
+                processor=processor,
+                owner=self.person,
+            )
+        [git_ref] = self.factory.makeGitRefs()
+        recipe = self.makeRockRecipe(git_ref=git_ref)
+        now = get_transaction_timestamp(IStore(distroseries))
+        # api_base = "http://api.launchpad.test/devel";
+        # amd640_api_url = api_base + api_url(amd640)
+        response = self.webservice.named_post(
+            recipe["self_link"],
+            "requestBuilds",
+            channels={"rockcraft": "edge"},
+            architectures=[api_url(amd640), api_url(risc500)],
+        )
+        self.assertEqual(201, response.status)
+        build_request_url = response.getHeader("Location")
+        build_request = self.webservice.get(build_request_url).jsonBody()
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, GreaterThan(now)
+                    ),
+                    "date_finished": Is(None),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Pending"),
+                    "error_message": Is(None),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+            ),
+        )
+        self.assertEqual([], self.getCollectionLinks(build_request, "builds"))
+        with person_logged_in(self.person):
+            rockcraft_yaml = (
+                "base: ubuntu@%s\nplatforms:\n" % distroseries.version
+            )
+            for processor in processors:
+                rockcraft_yaml += "    %s:\n" % processor.name
+            self.useFixture(GitHostingFixture(blob=rockcraft_yaml))
+            [job] = getUtility(IRockRecipeRequestBuildsJobSource).iterReady()
+            with dbuser(config.IRockRecipeRequestBuildsJobSource.dbuser):
+                JobRunner([job]).runAll()
+        date_requested = iso8601.parse_date(build_request["date_requested"])
+        now = get_transaction_timestamp(IStore(distroseries))
+        build_request = self.webservice.get(
+            build_request["self_link"]
+        ).jsonBody()
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, Equals(date_requested)
+                    ),
+                    "date_finished": AfterPreprocessing(
+                        iso8601.parse_date,
+                        MatchesAll(GreaterThan(date_requested), LessThan(now)),
+                    ),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Completed"),
+                    "error_message": Is(None),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+            ),
+        )
+        builds = self.webservice.get(
+            build_request["builds_collection_link"]
+        ).jsonBody()["entries"]
+        with person_logged_in(self.person):
+            self.assertThat(
+                builds,
+                MatchesSetwise(
+                    *(
+                        ContainsDict(
+                            {
+                                "recipe_link": Equals(recipe["self_link"]),
+                                "archive_link": Equals(
+                                    self.getURL(distroseries.main_archive)
+                                ),
+                                "arch_tag": Equals(processor.name),
+                                "channels": Equals({"rockcraft": "edge"}),
+                            }
+                        )
+                        for processor in (amd640, risc500)  # requested arches
+                    )
+                ),
+            )
+
     def test_requestBuilds_failure(self):
         # If the asynchronous build request job fails, this is reflected in
         # the build request entry.