launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #31451
[Merge] ~ruinedyourlife/launchpad:rock-build-parser into launchpad:master
Jürgen Gmach has proposed merging ~ruinedyourlife/launchpad:rock-build-parser into launchpad:master with ~jugmac00/launchpad:send-proxy-arguments-when-building-rocks as a prerequisite.
Commit message:
Add rockcraft parser
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad/+git/launchpad/+merge/473188
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad:rock-build-parser into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index 8358e68..5407957 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -2895,34 +2895,3 @@ public.teammembership = SELECT
public.teamparticipation = SELECT
public.webhook = SELECT
public.webhookjob = SELECT, INSERT
-
-[rock-build-job]
-type=user
-groups=script
-public.account = SELECT
-public.builder = SELECT
-public.buildfarmjob = SELECT, INSERT
-public.buildqueue = SELECT, INSERT, UPDATE
-public.rockfile = SELECT
-public.rockrecipe = SELECT, UPDATE
-public.rockrecipebuild = SELECT, INSERT, UPDATE
-public.rockrecipebuildjob = SELECT, UPDATE
-public.rockrecipejob = SELECT, UPDATE
-public.distribution = SELECT
-public.distroarchseries = SELECT
-public.distroseries = SELECT
-public.emailaddress = SELECT
-public.gitref = SELECT
-public.gitrepository = SELECT
-public.job = SELECT, INSERT, UPDATE
-public.libraryfilealias = SELECT
-public.libraryfilecontent = SELECT
-public.person = SELECT
-public.personsettings = SELECT
-public.pocketchroot = SELECT
-public.processor = SELECT
-public.product = SELECT
-public.teammembership = SELECT
-public.teamparticipation = SELECT
-public.webhook = SELECT
-public.webhookjob = SELECT, INSERT
diff --git a/lib/lp/archiveuploader/rockrecipeupload.py b/lib/lp/archiveuploader/rockrecipeupload.py
deleted file mode 100644
index d7b9152..0000000
--- a/lib/lp/archiveuploader/rockrecipeupload.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# 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
deleted file mode 100644
index 28a12cb..0000000
--- a/lib/lp/archiveuploader/tests/test_rockrecipeupload.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# 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 1289241..baa903b 100644
--- a/lib/lp/archiveuploader/uploadprocessor.py
+++ b/lib/lp/archiveuploader/uploadprocessor.py
@@ -44,6 +44,7 @@ worst of the results from the various changes files found (in the order
above, failed being worst).
"""
+
import os
import shutil
import sys
@@ -60,7 +61,6 @@ 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,7 +77,6 @@ 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 (
@@ -776,32 +775,6 @@ 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.
@@ -857,8 +830,6 @@ 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:
diff --git a/lib/lp/buildmaster/enums.py b/lib/lp/buildmaster/enums.py
index be0a30b..672bc80 100644
--- a/lib/lp/buildmaster/enums.py
+++ b/lib/lp/buildmaster/enums.py
@@ -249,15 +249,6 @@ class BuildFarmJobType(DBEnumeratedType):
""",
)
- ROCKRECIPEBUILD = DBItem(
- 10,
- """
- Rock recipe build
-
- Build a rock from a recipe.
- """,
- )
-
class BuildQueueStatus(DBEnumeratedType):
"""Build queue status.
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 28afbf3..a1669be 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -126,7 +126,6 @@ from lp.registry.model.accesspolicy import (
)
from lp.registry.model.person import Person
from lp.registry.model.teammembership import TeamParticipation
-from lp.rocks.interfaces.rockrecipe import IRockRecipeSet
from lp.services.auth.model import AccessTokenTargetMixin
from lp.services.config import config
from lp.services.database import bulk
@@ -2005,15 +2004,6 @@ class GitRepository(
self,
)
)
- if not getUtility(IRockRecipeSet).findByGitRepository(self).is_empty():
- alteration_operations.append(
- DeletionCallable(
- None,
- msg("Some rock recipes build from this repository."),
- getUtility(IRockRecipeSet).detachFromGitRepository,
- self,
- )
- )
return (alteration_operations, deletion_operations)
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 5f8f255..7b70558 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -148,7 +148,6 @@ from lp.registry.interfaces.persondistributionsourcepackage import (
from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
from lp.registry.interfaces.personproduct import IPersonProductFactory
from lp.registry.tests.test_accesspolicy import get_policies_for_artifact
-from lp.rocks.interfaces.rockrecipe import ROCK_RECIPE_ALLOW_CREATE
from lp.services.auth.enums import AccessTokenScope
from lp.services.authserver.xmlrpc import AuthServerAPIView
from lp.services.config import config
@@ -1707,39 +1706,6 @@ class TestGitRepositoryDeletionConsequences(TestCaseWithFactory):
self.assertIsNone(recipe2.git_repository)
self.assertIsNone(recipe2.git_path)
- def test_rock_recipe_requirements(self):
- # If a repository is used by a rock recipe, the deletion
- # requirements indicate this.
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- [ref] = self.factory.makeGitRefs()
- self.factory.makeRockRecipe(git_ref=ref)
- self.assertEqual(
- {
- None: (
- "alter",
- _("Some rock recipes build from this repository."),
- )
- },
- ref.repository.getDeletionRequirements(),
- )
-
- def test_rock_recipe_deletion(self):
- # break_references allows deleting a repository used by a rock
- # recipe.
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- repository = self.factory.makeGitRepository()
- [ref1, ref2] = self.factory.makeGitRefs(
- repository=repository, paths=["refs/heads/1", "refs/heads/2"]
- )
- recipe1 = self.factory.makeRockRecipe(git_ref=ref1)
- recipe2 = self.factory.makeRockRecipe(git_ref=ref2)
- repository.destroySelf(break_references=True)
- transaction.commit()
- self.assertIsNone(recipe1.git_repository)
- self.assertIsNone(recipe1.git_path)
- self.assertIsNone(recipe2.git_repository)
- self.assertIsNone(recipe2.git_path)
-
def test_ClearPrerequisiteRepository(self):
# ClearPrerequisiteRepository.__call__ must clear the prerequisite
# repository.
diff --git a/lib/lp/configure.zcml b/lib/lp/configure.zcml
index 8592b8f..ab2b4b0 100644
--- a/lib/lp/configure.zcml
+++ b/lib/lp/configure.zcml
@@ -35,7 +35,6 @@
<include package="lp.testing" />
<include package="lp.testopenid" />
<include package="lp.registry" />
- <include package="lp.rocks" />
<include package="lp.xmlrpc" />
<include file="permissions.zcml" />
diff --git a/lib/lp/registry/browser/personproduct.py b/lib/lp/registry/browser/personproduct.py
index b778a73..86c8f7d 100644
--- a/lib/lp/registry/browser/personproduct.py
+++ b/lib/lp/registry/browser/personproduct.py
@@ -19,7 +19,6 @@ from lp.code.browser.vcslisting import PersonTargetDefaultVCSNavigationMixin
from lp.code.interfaces.branchnamespace import get_branch_namespace
from lp.registry.interfaces.personociproject import IPersonOCIProjectFactory
from lp.registry.interfaces.personproduct import IPersonProduct
-from lp.rocks.interfaces.rockrecipe import IRockRecipeSet
from lp.services.webapp import (
Navigation,
StandardLaunchpadFacets,
@@ -68,12 +67,6 @@ class PersonProductNavigation(
owner=self.context.person, project=self.context.product, name=name
)
- @stepthrough("+rock")
- def traverse_rock(self, name):
- return getUtility(IRockRecipeSet).getByName(
- owner=self.context.person, project=self.context.product, name=name
- )
-
@implementer(IMultiFacetedBreadcrumb)
class PersonProductBreadcrumb(Breadcrumb):
diff --git a/lib/lp/registry/personmerge.py b/lib/lp/registry/personmerge.py
index 0f535c3..151ff30 100644
--- a/lib/lp/registry/personmerge.py
+++ b/lib/lp/registry/personmerge.py
@@ -24,7 +24,6 @@ from lp.registry.interfaces.teammembership import (
ITeamMembershipSet,
TeamMembershipStatus,
)
-from lp.rocks.interfaces.rockrecipe import IRockRecipeSet
from lp.services.database import postgresql
from lp.services.database.interfaces import IStore
from lp.services.database.sqlbase import cursor, sqlvalues
@@ -937,25 +936,6 @@ def _mergeCharmRecipe(cur, from_person, to_person):
IStore(recipes[0]).flush()
-def _mergeRockRecipe(cur, from_person, to_person):
- # This shouldn't use removeSecurityProxy.
- recipes = getUtility(IRockRecipeSet).findByOwner(from_person)
- existing_names = [
- r.name for r in getUtility(IRockRecipeSet).findByOwner(to_person)
- ]
- for recipe in recipes:
- naked_recipe = removeSecurityProxy(recipe)
- new_name = naked_recipe.name
- count = 1
- while new_name in existing_names:
- new_name = "%s-%s" % (recipe.name, count)
- count += 1
- naked_recipe.owner = to_person
- naked_recipe.name = new_name
- if not recipes.is_empty():
- IStore(recipes[0]).flush()
-
-
def _purgeUnmergableTeamArtifacts(from_team, to_team, reviewer):
"""Purge team artifacts that cannot be merged, but can be removed."""
# A team cannot have more than one mailing list.
@@ -1206,9 +1186,6 @@ def merge_people(from_person, to_person, reviewer, delete=False):
_mergeCharmRecipe(cur, from_id, to_id)
skip.append(("charmrecipe", "owner"))
- _mergeRockRecipe(cur, from_id, to_id)
- skip.append(("rockrecipe", "owner"))
-
_mergeVulnerabilitySubscription(cur, from_id, to_id)
skip.append(("vulnerabilitysubscription", "person"))
diff --git a/lib/lp/registry/tests/test_personmerge.py b/lib/lp/registry/tests/test_personmerge.py
index dd6a8dd..fcc5245 100644
--- a/lib/lp/registry/tests/test_personmerge.py
+++ b/lib/lp/registry/tests/test_personmerge.py
@@ -41,10 +41,6 @@ from lp.registry.personmerge import (
merge_people,
)
from lp.registry.tests.test_person import KarmaTestMixin
-from lp.rocks.interfaces.rockrecipe import (
- ROCK_RECIPE_ALLOW_CREATE,
- IRockRecipeSet,
-)
from lp.services.config import config
from lp.services.database.sqlbase import cursor
from lp.services.features.testing import FeatureFixture
@@ -918,60 +914,6 @@ class TestMergePeople(TestCaseWithFactory, KarmaTestMixin):
),
)
- def test_merge_moves_rock_recipes(self):
- # When person/teams are merged, rock recipes owned by the from
- # person are moved.
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- duplicate = self.factory.makePerson()
- mergee = self.factory.makePerson()
- self.factory.makeRockRecipe(registrant=duplicate, owner=duplicate)
- self._do_premerge(duplicate, mergee)
- login_person(mergee)
- duplicate, mergee = self._do_merge(duplicate, mergee)
- self.assertEqual(
- 1, getUtility(IRockRecipeSet).findByOwner(mergee).count()
- )
-
- def test_merge_with_duplicated_rock_recipes(self):
- # If both the from and to people have rock recipes with the same
- # name, merging renames the duplicate from the from person's side.
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- duplicate = self.factory.makePerson()
- mergee = self.factory.makePerson()
- [ref] = self.factory.makeGitRefs()
- [ref2] = self.factory.makeGitRefs()
- self.factory.makeRockRecipe(
- registrant=duplicate, owner=duplicate, name="foo", git_ref=ref
- )
- self.factory.makeRockRecipe(
- registrant=mergee, owner=mergee, name="foo", git_ref=ref2
- )
- self._do_premerge(duplicate, mergee)
- login_person(mergee)
- duplicate, mergee = self._do_merge(duplicate, mergee)
- recipes = sorted(
- getUtility(IRockRecipeSet).findByOwner(mergee),
- key=attrgetter("name"),
- )
- self.assertEqual(2, len(recipes))
- self.assertThat(
- recipes,
- MatchesListwise(
- [
- MatchesStructure.byEquality(
- git_repository=ref2.repository,
- git_path=ref2.path,
- name="foo",
- ),
- MatchesStructure.byEquality(
- git_repository=ref.repository,
- git_path=ref.path,
- name="foo-1",
- ),
- ]
- ),
- )
-
class TestMergeMailingListSubscriptions(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
diff --git a/lib/lp/rocks/__init__.py b/lib/lp/rocks/adapters/__init__.py
similarity index 100%
rename from lib/lp/rocks/__init__.py
rename to lib/lp/rocks/adapters/__init__.py
diff --git a/lib/lp/rocks/adapters/buildarch.py b/lib/lp/rocks/adapters/buildarch.py
new file mode 100644
index 0000000..1c1bfec
--- /dev/null
+++ b/lib/lp/rocks/adapters/buildarch.py
@@ -0,0 +1,109 @@
+# Copyright 2024 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__all__ = [
+ "determine_instances_to_build",
+]
+
+from collections import OrderedDict
+
+
+class RockBasesParserError(Exception):
+ """Base class for all exceptions in this module."""
+
+
+class MissingPropertyError(RockBasesParserError):
+ """Error for when an expected property is not present in the YAML."""
+
+ def __init__(self, prop, msg=None):
+ if msg is None:
+ msg = f"Rock specification is missing the {prop!r} property"
+ super().__init__(msg)
+ self.property = prop
+
+
+class BadPropertyError(RockBasesParserError):
+ """Error for when a YAML property is malformed in some way."""
+
+
+class RockBase:
+ """A single base in rockcraft.yaml."""
+
+ def __init__(self, name, channel):
+ self.name = name
+ if not isinstance(channel, str):
+ raise BadPropertyError(
+ f"Channel {channel!r} is not a string (missing quotes?)"
+ )
+ self.channel = channel
+
+ @classmethod
+ def from_string(cls, base_string):
+ """Create a new base from a string like 'ubuntu@22.04'."""
+ name, channel = base_string.split("@")
+ return cls(name, channel)
+
+ def __eq__(self, other):
+ if not isinstance(other, RockBase):
+ return NotImplemented
+ return self.name == other.name and self.channel == other.channel
+
+ def __hash__(self):
+ return hash((self.name, self.channel))
+
+ def __str__(self):
+ return f"{self.name}@{self.channel}"
+
+
+def determine_instances_to_build(
+ rockcraft_data, supported_arches, default_distro_series
+):
+ """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 default_distro_series: The default `DistroSeries` to use if
+ rockcraft.yaml does not explicitly declare a base.
+ :return: A list of `DistroArchSeries`.
+ """
+ base = rockcraft_data.get("base")
+ if not base:
+ raise MissingPropertyError("base")
+
+ if base == "bare":
+ base = rockcraft_data.get("build-base")
+ if not base:
+ raise MissingPropertyError(
+ "build-base",
+ "When 'base' is 'bare', 'build-base' must be specified",
+ )
+
+ rock_base = RockBase.from_string(base)
+
+ platforms = rockcraft_data.get("platforms")
+ if not platforms:
+ raise MissingPropertyError("platforms")
+
+ instances = OrderedDict()
+ for platform, config in platforms.items():
+ build_on = config.get("build-on", [platform])
+ build_for = config.get("build-for", [platform])
+
+ if isinstance(build_on, str):
+ build_on = [build_on]
+ if isinstance(build_for, str):
+ build_for = [build_for]
+
+ for arch in build_on:
+ for das in supported_arches:
+ if (
+ das.distroseries.distribution.name == rock_base.name
+ and das.distroseries.version == rock_base.channel
+ and das.architecturetag == arch
+ ):
+ instances[das] = None
+ break
+
+ return list(instances)
diff --git a/lib/lp/rocks/mail/__init__.py b/lib/lp/rocks/adapters/tests/__init__.py
similarity index 100%
rename from lib/lp/rocks/mail/__init__.py
rename to lib/lp/rocks/adapters/tests/__init__.py
diff --git a/lib/lp/rocks/adapters/tests/test_buildarch.py b/lib/lp/rocks/adapters/tests/test_buildarch.py
new file mode 100644
index 0000000..0ff0b45
--- /dev/null
+++ b/lib/lp/rocks/adapters/tests/test_buildarch.py
@@ -0,0 +1,211 @@
+# Copyright 2024 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from functools import partial
+
+from testscenarios import WithScenarios, load_tests_apply_scenarios
+from testtools.matchers import (
+ Equals,
+ MatchesException,
+ MatchesListwise,
+ MatchesStructure,
+ Raises,
+)
+from zope.component import getUtility
+
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.buildmaster.interfaces.processor import (
+ IProcessorSet,
+ ProcessorNotFound,
+)
+from lp.rocks.adapters.buildarch import (
+ MissingPropertyError,
+ RockBase,
+ determine_instances_to_build,
+)
+from lp.testing import TestCase, TestCaseWithFactory
+from lp.testing.layers import LaunchpadZopelessLayer
+
+
+class TestRockBase(TestCase):
+ def test_from_string(self):
+ base = RockBase.from_string("ubuntu@22.04")
+ self.assertEqual("ubuntu", base.name)
+ self.assertEqual("22.04", base.channel)
+
+ def test_str(self):
+ base = RockBase("ubuntu", "22.04")
+ self.assertEqual("ubuntu@22.04", str(base))
+
+ def test_equality(self):
+ base1 = RockBase("ubuntu", "22.04")
+ base2 = RockBase("ubuntu", "22.04")
+ base3 = RockBase("ubuntu", "20.04")
+ self.assertEqual(base1, base2)
+ self.assertNotEqual(base1, base3)
+
+
+class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
+ layer = LaunchpadZopelessLayer
+
+ scenarios = [
+ (
+ "single platform",
+ {
+ "rockcraft_data": {
+ "base": "ubuntu@22.04",
+ "platforms": {
+ "amd64": {},
+ },
+ },
+ "expected": [("22.04", "amd64")],
+ },
+ ),
+ (
+ "multiple platforms",
+ {
+ "rockcraft_data": {
+ "base": "ubuntu@22.04",
+ "platforms": {
+ "amd64": {},
+ "arm64": {},
+ },
+ },
+ "expected": [("22.04", "amd64"), ("22.04", "arm64")],
+ },
+ ),
+ (
+ "build-on specified",
+ {
+ "rockcraft_data": {
+ "base": "ubuntu@22.04",
+ "platforms": {
+ "amd64": {"build-on": ["amd64", "arm64"]},
+ },
+ },
+ "expected": [("22.04", "amd64"), ("22.04", "arm64")],
+ },
+ ),
+ (
+ "build-for specified",
+ {
+ "rockcraft_data": {
+ "base": "ubuntu@22.04",
+ "platforms": {
+ "amd64": {"build-for": "amd64"},
+ "arm64": {"build-for": "arm64"},
+ },
+ },
+ "expected": [("22.04", "amd64"), ("22.04", "arm64")],
+ },
+ ),
+ (
+ "bare base",
+ {
+ "rockcraft_data": {
+ "base": "bare",
+ "build-base": "ubuntu@20.04",
+ "platforms": {
+ "amd64": {},
+ },
+ },
+ "expected": [("20.04", "amd64")],
+ },
+ ),
+ (
+ "missing base",
+ {
+ "rockcraft_data": {
+ "platforms": {
+ "amd64": {},
+ },
+ },
+ "expected_exception": MatchesException(
+ MissingPropertyError,
+ "Rock specification is missing the 'base' property",
+ ),
+ },
+ ),
+ (
+ "missing build-base for bare",
+ {
+ "rockcraft_data": {
+ "base": "bare",
+ "platforms": {
+ "amd64": {},
+ },
+ },
+ "expected_exception": MatchesException(
+ MissingPropertyError,
+ "When 'base' is 'bare', 'build-base' must be specified",
+ ),
+ },
+ ),
+ (
+ "missing platforms",
+ {
+ "rockcraft_data": {
+ "base": "ubuntu@22.04",
+ },
+ "expected_exception": MatchesException(
+ MissingPropertyError,
+ "Rock specification is missing the 'platforms' property",
+ ),
+ },
+ ),
+ ]
+
+ def test_determine_instances_to_build(self):
+ distro_serieses = [
+ self.factory.makeDistroSeries(
+ distribution=getUtility(ILaunchpadCelebrities).ubuntu,
+ version=version,
+ )
+ for version in ("22.04", "20.04")
+ ]
+ dases = []
+ for arch_tag in ("amd64", "arm64"):
+ try:
+ processor = getUtility(IProcessorSet).getByName(arch_tag)
+ except ProcessorNotFound:
+ processor = self.factory.makeProcessor(
+ name=arch_tag, supports_virtualized=True
+ )
+ for distro_series in distro_serieses:
+ dases.append(
+ self.factory.makeDistroArchSeries(
+ distroseries=distro_series,
+ architecturetag=arch_tag,
+ processor=processor,
+ )
+ )
+
+ build_instances_factory = partial(
+ determine_instances_to_build,
+ self.rockcraft_data,
+ dases,
+ distro_serieses[0],
+ )
+
+ if hasattr(self, "expected_exception"):
+ self.assertThat(
+ build_instances_factory, Raises(self.expected_exception)
+ )
+ else:
+ self.assertThat(
+ build_instances_factory(),
+ MatchesListwise(
+ [
+ MatchesStructure(
+ distroseries=MatchesStructure.byEquality(
+ version=version
+ ),
+ architecturetag=Equals(arch_tag),
+ )
+ for version, arch_tag in self.expected
+ ]
+ ),
+ )
+
+
+load_tests = load_tests_apply_scenarios
diff --git a/lib/lp/rocks/browser/configure.zcml b/lib/lp/rocks/browser/configure.zcml
deleted file mode 100644
index b70835f..0000000
--- a/lib/lp/rocks/browser/configure.zcml
+++ /dev/null
@@ -1,27 +0,0 @@
-<!-- Copyright 2024 Canonical Ltd. This software is licensed under the
- GNU Affero General Public License version 3 (see the file LICENSE).
--->
-
-<configure
- xmlns="http://namespaces.zope.org/zope"
- xmlns:browser="http://namespaces.zope.org/browser"
- xmlns:i18n="http://namespaces.zope.org/i18n"
- xmlns:lp="http://namespaces.canonical.com/lp"
- i18n_domain="launchpad">
- <lp:facet facet="overview">
- <lp:url
- for="lp.rocks.interfaces.rockrecipe.IRockRecipe"
- urldata="lp.rocks.browser.rockrecipe.RockRecipeURL" />
- <lp:navigation
- module="lp.rocks.browser.rockrecipe"
- classes="RockRecipeNavigation" />
- <lp:url
- for="lp.rocks.interfaces.rockrecipe.IRockRecipeBuildRequest"
- path_expression="string:+build-request/${id}"
- attribute_to_parent="recipe" />
- <lp:url
- for="lp.rocks.interfaces.rockrecipebuild.IRockRecipeBuild"
- path_expression="string:+build/${id}"
- attribute_to_parent="recipe" />
- </lp:facet>
-</configure>
diff --git a/lib/lp/rocks/browser/rockrecipe.py b/lib/lp/rocks/browser/rockrecipe.py
deleted file mode 100644
index 0e13dbe..0000000
--- a/lib/lp/rocks/browser/rockrecipe.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Rock recipe views."""
-
-__all__ = [
- "RockRecipeNavigation",
- "RockRecipeURL",
-]
-
-from zope.component import getUtility
-from zope.interface import implementer
-
-from lp.registry.interfaces.personproduct import IPersonProductFactory
-from lp.rocks.interfaces.rockrecipe import IRockRecipe
-from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuildSet
-from lp.services.webapp import Navigation, stepthrough
-from lp.services.webapp.interfaces import ICanonicalUrlData
-from lp.soyuz.browser.build import get_build_by_id_str
-
-
-@implementer(ICanonicalUrlData)
-class RockRecipeURL:
- """Rock recipe URL creation rules."""
-
- rootsite = "mainsite"
-
- def __init__(self, recipe):
- self.recipe = recipe
-
- @property
- def inside(self):
- owner = self.recipe.owner
- project = self.recipe.project
- return getUtility(IPersonProductFactory).create(owner, project)
-
- @property
- def path(self):
- return "+rock/%s" % self.recipe.name
-
-
-class RockRecipeNavigation(Navigation):
- usedfor = IRockRecipe
-
- @stepthrough("+build-request")
- def traverse_build_request(self, name):
- try:
- job_id = int(name)
- except ValueError:
- return None
- return self.context.getBuildRequest(job_id)
-
- @stepthrough("+build")
- def traverse_build(self, name):
- build = get_build_by_id_str(IRockRecipeBuildSet, name)
- if build is None or build.recipe != self.context:
- return None
- return build
diff --git a/lib/lp/rocks/browser/tests/test_rockrecipe.py b/lib/lp/rocks/browser/tests/test_rockrecipe.py
deleted file mode 100644
index 229824d..0000000
--- a/lib/lp/rocks/browser/tests/test_rockrecipe.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Test rock recipe views."""
-
-from lp.rocks.interfaces.rockrecipe import ROCK_RECIPE_ALLOW_CREATE
-from lp.services.features.testing import FeatureFixture
-from lp.services.webapp import canonical_url
-from lp.testing import TestCaseWithFactory
-from lp.testing.layers import DatabaseFunctionalLayer
-
-
-class TestRockRecipeNavigation(TestCaseWithFactory):
-
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- super().setUp()
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
-
- def test_canonical_url(self):
- owner = self.factory.makePerson(name="person")
- project = self.factory.makeProduct(name="project")
- recipe = self.factory.makeRockRecipe(
- registrant=owner, owner=owner, project=project, name="rock"
- )
- self.assertEqual(
- "http://launchpad.test/~person/project/+rock/rock",
- canonical_url(recipe),
- )
diff --git a/lib/lp/rocks/configure.zcml b/lib/lp/rocks/configure.zcml
deleted file mode 100644
index ade5ed8..0000000
--- a/lib/lp/rocks/configure.zcml
+++ /dev/null
@@ -1,102 +0,0 @@
-<!-- Copyright 2024 Canonical Ltd. This software is licensed under the
- GNU Affero General Public License version 3 (see the file LICENSE).
--->
-
-<configure
- xmlns="http://namespaces.zope.org/zope"
- xmlns:browser="http://namespaces.zope.org/browser"
- xmlns:i18n="http://namespaces.zope.org/i18n"
- xmlns:lp="http://namespaces.canonical.com/lp"
- xmlns:xmlrpc="http://namespaces.zope.org/xmlrpc"
- i18n_domain="launchpad">
-
- <lp:authorizations module=".security" />
- <include package=".browser" />
-
- <!-- RockRecipe -->
- <class class="lp.rocks.model.rockrecipe.RockRecipe">
- <require
- permission="launchpad.View"
- interface="lp.rocks.interfaces.rockrecipe.IRockRecipeView
- lp.rocks.interfaces.rockrecipe.IRockRecipeEditableAttributes
- lp.rocks.interfaces.rockrecipe.IRockRecipeAdminAttributes" />
- <require
- permission="launchpad.Edit"
- interface="lp.rocks.interfaces.rockrecipe.IRockRecipeEdit"
- set_schema="lp.rocks.interfaces.rockrecipe.IRockRecipeEditableAttributes" />
- <require
- permission="launchpad.Admin"
- set_schema="lp.rocks.interfaces.rockrecipe.IRockRecipeAdminAttributes" />
- </class>
- <subscriber
- for="lp.rocks.interfaces.rockrecipe.IRockRecipe
- zope.lifecycleevent.interfaces.IObjectModifiedEvent"
- handler="lp.rocks.model.rockrecipe.rock_recipe_modified" />
-
- <!-- RockRecipeSet -->
- <lp:securedutility
- class="lp.rocks.model.rockrecipe.RockRecipeSet"
- provides="lp.rocks.interfaces.rockrecipe.IRockRecipeSet">
- <allow interface="lp.rocks.interfaces.rockrecipe.IRockRecipeSet" />
- </lp:securedutility>
-
- <!-- RockRecipeBuildRequest -->
- <class class="lp.rocks.model.rockrecipe.RockRecipeBuildRequest">
- <require
- permission="launchpad.View"
- interface="lp.rocks.interfaces.rockrecipe.IRockRecipeBuildRequest" />
- </class>
-
- <!-- RockRecipeBuild -->
- <class class="lp.rocks.model.rockrecipebuild.RockRecipeBuild">
- <require
- permission="launchpad.View"
- interface="lp.rocks.interfaces.rockrecipebuild.IRockRecipeBuildView" />
- <require
- permission="launchpad.Edit"
- interface="lp.rocks.interfaces.rockrecipebuild.IRockRecipeBuildEdit" />
- <require
- permission="launchpad.Admin"
- interface="lp.rocks.interfaces.rockrecipebuild.IRockRecipeBuildAdmin" />
- </class>
-
- <!-- RockRecipeBuildSet -->
- <lp:securedutility
- class="lp.rocks.model.rockrecipebuild.RockRecipeBuildSet"
- provides="lp.rocks.interfaces.rockrecipebuild.IRockRecipeBuildSet">
- <allow interface="lp.rocks.interfaces.rockrecipebuild.IRockRecipeBuildSet" />
- </lp:securedutility>
- <lp:securedutility
- class="lp.rocks.model.rockrecipebuild.RockRecipeBuildSet"
- provides="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource"
- name="ROCKRECIPEBUILD">
- <allow interface="lp.buildmaster.interfaces.buildfarmjob.ISpecificBuildFarmJobSource" />
- </lp:securedutility>
-
- <!-- RockFile -->
- <class class="lp.rocks.model.rockrecipebuild.RockFile">
- <allow interface="lp.rocks.interfaces.rockrecipebuild.IRockFile" />
- </class>
-
- <!-- RockRecipeBuildBehaviour -->
- <adapter
- for="lp.rocks.interfaces.rockrecipebuild.IRockRecipeBuild"
- provides="lp.buildmaster.interfaces.buildfarmjobbehaviour.IBuildFarmJobBehaviour"
- factory="lp.rocks.model.rockrecipebuildbehaviour.RockRecipeBuildBehaviour"
- permission="zope.Public" />
-
- <!-- rock-related jobs -->
- <class class="lp.rocks.model.rockrecipejob.RockRecipeJob">
- <allow interface="lp.rocks.interfaces.rockrecipejob.IRockRecipeJob" />
- </class>
- <lp:securedutility
- component="lp.rocks.model.rockrecipejob.RockRecipeRequestBuildsJob"
- provides="lp.rocks.interfaces.rockrecipejob.IRockRecipeRequestBuildsJobSource">
- <allow interface="lp.rocks.interfaces.rockrecipejob.IRockRecipeRequestBuildsJobSource" />
- </lp:securedutility>
- <class class="lp.rocks.model.rockrecipejob.RockRecipeRequestBuildsJob">
- <allow interface="lp.rocks.interfaces.rockrecipejob.IRockRecipeJob" />
- <allow interface="lp.rocks.interfaces.rockrecipejob.IRockRecipeRequestBuildsJob" />
- </class>
-
-</configure>
diff --git a/lib/lp/rocks/emailtemplates/rockrecipebuild-notification.txt b/lib/lp/rocks/emailtemplates/rockrecipebuild-notification.txt
deleted file mode 100644
index af69ceb..0000000
--- a/lib/lp/rocks/emailtemplates/rockrecipebuild-notification.txt
+++ /dev/null
@@ -1,9 +0,0 @@
- * Rock Recipe: %(recipe_name)s
- * Project: %(project_name)s
- * Distroseries: %(distroseries)s
- * Architecture: %(architecturetag)s
- * State: %(build_state)s
- * Duration: %(build_duration)s
- * Build Log: %(log_url)s
- * Upload Log: %(upload_log_url)s
- * Builder: %(builder_url)s
diff --git a/lib/lp/rocks/interfaces/rockrecipe.py b/lib/lp/rocks/interfaces/rockrecipe.py
deleted file mode 100644
index 49fe8cd..0000000
--- a/lib/lp/rocks/interfaces/rockrecipe.py
+++ /dev/null
@@ -1,559 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Rock recipe interfaces."""
-
-__all__ = [
- "BadRockRecipeSource",
- "BadRockRecipeSearchContext",
- "ROCK_RECIPE_ALLOW_CREATE",
- "ROCK_RECIPE_PRIVATE_FEATURE_FLAG",
- "RockRecipeBuildAlreadyPending",
- "RockRecipeBuildDisallowedArchitecture",
- "RockRecipeBuildRequestStatus",
- "RockRecipeFeatureDisabled",
- "RockRecipeNotOwner",
- "RockRecipePrivacyMismatch",
- "RockRecipePrivateFeatureDisabled",
- "DuplicateRockRecipeName",
- "IRockRecipe",
- "IRockRecipeBuildRequest",
- "IRockRecipeSet",
- "NoSourceForRockRecipe",
- "NoSuchRockRecipe",
-]
-
-import http.client
-
-from lazr.enum import EnumeratedType, Item
-from lazr.restful.declarations import error_status, exported
-from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
-from zope.interface import Interface
-from zope.schema import (
- Bool,
- Choice,
- Datetime,
- Dict,
- Int,
- List,
- Set,
- Text,
- TextLine,
-)
-from zope.security.interfaces import Unauthorized
-
-from lp import _
-from lp.app.enums import InformationType
-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.code.interfaces.gitref import IGitRef
-from lp.code.interfaces.gitrepository import IGitRepository
-from lp.registry.interfaces.person import IPerson
-from lp.registry.interfaces.product import IProduct
-from lp.services.fields import PersonChoice, PublicPersonChoice
-from lp.snappy.validators.channels import channels_validator
-
-ROCK_RECIPE_ALLOW_CREATE = "rock.recipe.create.enabled"
-ROCK_RECIPE_PRIVATE_FEATURE_FLAG = "rock.recipe.allow_private"
-
-
-@error_status(http.client.UNAUTHORIZED)
-class RockRecipeFeatureDisabled(Unauthorized):
- """Only certain users can create new rock recipes."""
-
- def __init__(self):
- super().__init__(
- "You do not have permission to create new rock recipes."
- )
-
-
-@error_status(http.client.UNAUTHORIZED)
-class RockRecipePrivateFeatureDisabled(Unauthorized):
- """Only certain users can create private rock recipes."""
-
- def __init__(self):
- super().__init__(
- "You do not have permission to create private rock recipes."
- )
-
-
-@error_status(http.client.BAD_REQUEST)
-class DuplicateRockRecipeName(Exception):
- """Raised for rock recipes with duplicate project/owner/name."""
-
- def __init__(self):
- super().__init__(
- "There is already a rock recipe with the same project, owner, "
- "and name."
- )
-
-
-@error_status(http.client.UNAUTHORIZED)
-class RockRecipeNotOwner(Unauthorized):
- """The registrant/requester is not the owner or a member of its team."""
-
-
-class NoSuchRockRecipe(NameLookupFailed):
- """The requested rock recipe does not exist."""
-
- _message_prefix = "No such rock recipe with this owner and project"
-
-
-@error_status(http.client.BAD_REQUEST)
-class NoSourceForRockRecipe(Exception):
- """Rock recipes must have a source (Git branch)."""
-
- def __init__(self):
- super().__init__("New rock recipes must have a Git branch.")
-
-
-@error_status(http.client.BAD_REQUEST)
-class BadRockRecipeSource(Exception):
- """The elements of the source for a rock recipe are inconsistent."""
-
-
-@error_status(http.client.BAD_REQUEST)
-class RockRecipePrivacyMismatch(Exception):
- """Rock recipe privacy does not match its content."""
-
- def __init__(self, message=None):
- super().__init__(
- message
- or "Rock recipe contains private information and cannot be public."
- )
-
-
-class BadRockRecipeSearchContext(Exception):
- """The context is not valid for a rock recipe search."""
-
-
-@error_status(http.client.BAD_REQUEST)
-class RockRecipeBuildAlreadyPending(Exception):
- """A build was requested when an identical build was already pending."""
-
- def __init__(self):
- super().__init__(
- "An identical build of this rock recipe is already pending."
- )
-
-
-@error_status(http.client.BAD_REQUEST)
-class RockRecipeBuildDisallowedArchitecture(Exception):
- """A build was requested for a disallowed architecture."""
-
- def __init__(self, das):
- super().__init__(
- "This rock recipe is not allowed to build for %s/%s."
- % (das.distroseries.name, das.architecturetag)
- )
-
-
-class RockRecipeBuildRequestStatus(EnumeratedType):
- """The status of a request to build a rock recipe."""
-
- PENDING = Item(
- """
- Pending
-
- This rock recipe build request is pending.
- """
- )
-
- FAILED = Item(
- """
- Failed
-
- This rock recipe build request failed.
- """
- )
-
- COMPLETED = Item(
- """
- Completed
-
- This rock recipe build request completed successfully.
- """
- )
-
-
-class IRockRecipeBuildRequest(Interface):
- """A request to build a rock recipe."""
-
- id = Int(title=_("ID"), required=True, readonly=True)
-
- date_requested = Datetime(
- title=_("The time when this request was made"),
- required=True,
- readonly=True,
- )
-
- date_finished = Datetime(
- title=_("The time when this request finished"),
- required=False,
- readonly=True,
- )
-
- recipe = Reference(
- # Really IRockRecipe.
- Interface,
- title=_("Rock recipe"),
- required=True,
- readonly=True,
- )
-
- status = Choice(
- title=_("Status"),
- vocabulary=RockRecipeBuildRequestStatus,
- required=True,
- readonly=True,
- )
-
- error_message = TextLine(
- title=_("Error message"), required=True, readonly=True
- )
-
- builds = CollectionField(
- title=_("Builds produced by this request"),
- # Really IRockRecipeBuild.
- value_type=Reference(schema=Interface),
- required=True,
- readonly=True,
- )
-
- requester = Reference(
- title=_("The person requesting the builds."),
- schema=IPerson,
- required=True,
- readonly=True,
- )
-
- channels = Dict(
- title=_("Source snap channels for builds produced by this request"),
- key_type=TextLine(),
- required=False,
- readonly=True,
- )
-
- architectures = Set(
- title=_("If set, this request is limited to these architecture tags"),
- value_type=TextLine(),
- required=False,
- readonly=True,
- )
-
-
-class IRockRecipeView(Interface):
- """`IRockRecipe` attributes that require launchpad.View permission."""
-
- id = Int(title=_("ID"), required=True, readonly=True)
-
- date_created = Datetime(
- title=_("Date created"), required=True, readonly=True
- )
- date_last_modified = Datetime(
- title=_("Date last modified"), required=True, readonly=True
- )
-
- registrant = PublicPersonChoice(
- title=_("Registrant"),
- required=True,
- readonly=True,
- vocabulary="ValidPersonOrTeam",
- description=_("The person who registered this rock recipe."),
- )
-
- private = Bool(
- title=_("Private"),
- required=False,
- readonly=False,
- description=_("Whether this rock recipe is private."),
- )
-
- def getAllowedInformationTypes(user):
- """Get a list of acceptable `InformationType`s for this rock recipe.
-
- If the user is a Launchpad admin, any type is acceptable.
- """
-
- def visibleByUser(user):
- """Can the specified user see this rock recipe?"""
-
- def requestBuild(build_request, distro_arch_series, channels=None):
- """Request a single build of this rock recipe.
-
- This method is for internal use; external callers should use
- `requestBuilds` instead.
-
- :param build_request: The `IRockRecipeBuildRequest` job being
- processed.
- :param distro_arch_series: The architecture to build for.
- :param channels: A dictionary mapping snap names to channels to use
- for this build.
- :return: `IRockRecipeBuild`.
- """
-
- def requestBuilds(requester, channels=None, architectures=None):
- """Request that the rock recipe be built.
-
- This is an asynchronous operation; once the operation has finished,
- the resulting build request's C{status} will be "Completed" and its
- C{builds} collection will return the resulting builds.
-
- :param requester: The person requesting the builds.
- :param channels: A dictionary mapping snap names to channels to use
- for these builds.
- :param architectures: If not None, limit builds to architectures
- with these architecture tags (in addition to any other
- applicable constraints).
- :return: An `IRockRecipeBuildRequest`.
- """
-
- def getBuildRequest(job_id):
- """Get an asynchronous build request by ID.
-
- :param job_id: The ID of the build request.
- :return: `IRockRecipeBuildRequest`.
- """
-
-
-class IRockRecipeEdit(Interface):
- """`IRockRecipe` methods that require launchpad.Edit permission."""
-
- def destroySelf():
- """Delete this rock recipe, provided that it has no builds."""
-
-
-class IRockRecipeEditableAttributes(Interface):
- """`IRockRecipe` attributes that can be edited.
-
- These attributes need launchpad.View to see, and launchpad.Edit to change.
- """
-
- owner = exported(
- PersonChoice(
- title=_("Owner"),
- required=True,
- readonly=False,
- vocabulary="AllUserTeamsParticipationPlusSelf",
- description=_("The owner of this rock recipe."),
- )
- )
-
- project = ReferenceChoice(
- title=_("The project that this rock recipe is associated with"),
- schema=IProduct,
- vocabulary="Product",
- required=True,
- readonly=False,
- )
-
- name = TextLine(
- title=_("Rock recipe name"),
- required=True,
- readonly=False,
- constraint=name_validator,
- description=_("The name of the rock recipe."),
- )
-
- description = Text(
- title=_("Description"),
- required=False,
- readonly=False,
- description=_("A description of the rock recipe."),
- )
-
- git_repository = ReferenceChoice(
- title=_("Git repository"),
- schema=IGitRepository,
- vocabulary="GitRepository",
- required=False,
- readonly=True,
- description=_(
- "A Git repository with a branch containing a rockcraft.yaml "
- "recipe."
- ),
- )
-
- git_path = TextLine(
- title=_("Git branch path"),
- required=False,
- readonly=False,
- description=_(
- "The path of the Git branch containing a rockcraft.yaml recipe."
- ),
- )
-
- git_ref = Reference(
- IGitRef,
- title=_("Git branch"),
- required=False,
- readonly=False,
- description=_("The Git branch containing a rockcraft.yaml recipe."),
- )
-
- build_path = TextLine(
- title=_("Build path"),
- description=_(
- "Subdirectory within the branch containing rockcraft.yaml."
- ),
- constraint=path_does_not_escape,
- required=False,
- readonly=False,
- )
- information_type = Choice(
- title=_("Information type"),
- vocabulary=InformationType,
- required=True,
- readonly=False,
- default=InformationType.PUBLIC,
- description=_(
- "The type of information contained in this rock recipe."
- ),
- )
-
- auto_build = Bool(
- title=_("Automatically build when branch changes"),
- required=True,
- readonly=False,
- description=_(
- "Whether this rock recipe is built automatically when the branch "
- "containing its rockcraft.yaml recipe changes."
- ),
- )
-
- auto_build_channels = Dict(
- title=_("Source snap channels for automatic builds"),
- key_type=TextLine(),
- required=False,
- readonly=False,
- description=_(
- "A dictionary mapping snap names to channels to use when building "
- "this rock recipe. Currently only 'core', 'core18', 'core20', "
- "and 'rockcraft' keys are supported."
- ),
- )
-
- is_stale = Bool(
- title=_("Rock recipe is stale and is due to be rebuilt."),
- required=True,
- readonly=True,
- )
-
- store_upload = Bool(
- title=_("Automatically upload to store"),
- required=True,
- readonly=False,
- description=_(
- "Whether builds of this rock recipe are automatically uploaded "
- "to the store."
- ),
- )
-
- store_name = TextLine(
- title=_("Registered store name"),
- required=False,
- readonly=False,
- description=_("The registered name of this rock in the store."),
- )
-
- store_secrets = List(
- value_type=TextLine(),
- title=_("Store upload tokens"),
- required=False,
- readonly=False,
- description=_(
- "Serialized secrets issued by the store and the login service to "
- "authorize uploads of this rock recipe."
- ),
- )
-
- store_channels = List(
- title=_("Store channels"),
- required=False,
- readonly=False,
- constraint=channels_validator,
- description=_(
- "Channels to release this rock to after uploading it to the "
- "store. A channel is defined by a combination of an optional "
- "track, a risk, and an optional branch, e.g. "
- "'2.1/stable/fix-123', '2.1/stable', 'stable/fix-123', or "
- "'stable'."
- ),
- )
-
-
-class IRockRecipeAdminAttributes(Interface):
- """`IRockRecipe` attributes that can be edited by admins.
-
- These attributes need launchpad.View to see, and launchpad.Admin to change.
- """
-
- require_virtualized = Bool(
- title=_("Require virtualized builders"),
- required=True,
- readonly=False,
- description=_("Only build this rock recipe on virtual builders."),
- )
-
-
-class IRockRecipe(
- IRockRecipeView,
- IRockRecipeEdit,
- IRockRecipeEditableAttributes,
- IRockRecipeAdminAttributes,
- IInformationType,
-):
- """A buildable rock recipe."""
-
-
-class IRockRecipeSet(Interface):
- """A utility to create and access rock recipes."""
-
- def new(
- registrant,
- owner,
- project,
- name,
- description=None,
- git_ref=None,
- build_path=None,
- require_virtualized=True,
- information_type=InformationType.PUBLIC,
- auto_build=False,
- auto_build_channels=None,
- store_upload=False,
- store_name=None,
- store_secrets=None,
- store_channels=None,
- date_created=None,
- ):
- """Create an `IRockRecipe`."""
-
- def getByName(owner, project, name):
- """Returns the appropriate `IRockRecipe` for the given objects."""
-
- def isValidInformationType(information_type, owner, git_ref=None):
- """Whether the information type context is valid."""
-
- def preloadDataForRecipes(recipes, user):
- """Load the data related to a list of rock recipes."""
-
- def findByGitRepository(repository, paths=None):
- """Return all rock recipes for the given Git repository.
-
- :param repository: An `IGitRepository`.
- :param paths: If not None, only return rock recipes for one of
- these Git reference paths.
- """
-
- def findByOwner(owner):
- """Return all rock recipes with the given `owner`."""
-
- def detachFromGitRepository(repository):
- """Detach all rock recipes from the given Git repository.
-
- After this, any rock recipes that previously used this repository
- will have no source and so cannot dispatch new builds.
- """
diff --git a/lib/lp/rocks/interfaces/rockrecipebuild.py b/lib/lp/rocks/interfaces/rockrecipebuild.py
deleted file mode 100644
index f2f5eb7..0000000
--- a/lib/lp/rocks/interfaces/rockrecipebuild.py
+++ /dev/null
@@ -1,196 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Rock recipe build interfaces."""
-
-__all__ = [
- "IRockFile",
- "IRockRecipeBuild",
- "IRockRecipeBuildSet",
-]
-
-from lazr.restful.fields import Reference
-from zope.interface import Attribute, Interface
-from zope.schema import Bool, Datetime, Dict, Int, TextLine
-
-from lp import _
-from lp.buildmaster.interfaces.buildfarmjob import (
- IBuildFarmJobEdit,
- ISpecificBuildFarmJobSource,
-)
-from lp.buildmaster.interfaces.packagebuild import (
- IPackageBuild,
- IPackageBuildView,
-)
-from lp.registry.interfaces.person import IPerson
-from lp.rocks.interfaces.rockrecipe import IRockRecipe, IRockRecipeBuildRequest
-from lp.services.database.constants import DEFAULT
-from lp.services.librarian.interfaces import ILibraryFileAlias
-from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
-
-
-class IRockRecipeBuildView(IPackageBuildView):
- """`IRockRecipeBuild` attributes that require launchpad.View."""
-
- build_request = Reference(
- IRockRecipeBuildRequest,
- title=_("The build request that caused this build to be created."),
- required=True,
- readonly=True,
- )
-
- requester = Reference(
- IPerson,
- title=_("The person who requested this build."),
- required=True,
- readonly=True,
- )
-
- recipe = Reference(
- IRockRecipe,
- title=_("The rock recipe to build."),
- required=True,
- readonly=True,
- )
-
- distro_arch_series = Reference(
- IDistroArchSeries,
- title=_("The series and architecture for which to build."),
- required=True,
- readonly=True,
- )
-
- channels = Dict(
- title=_("Source snap channels to use for this build."),
- description=_(
- "A dictionary mapping snap names to channels to use for this "
- "build. Currently only 'core', 'core18', 'core20', "
- "and 'rockcraft' keys are supported."
- ),
- key_type=TextLine(),
- )
-
- virtualized = Bool(
- title=_("If True, this build is virtualized."), readonly=True
- )
-
- score = Int(
- title=_("Score of the related build farm job (if any)."),
- required=False,
- readonly=True,
- )
-
- eta = Datetime(
- title=_("The datetime when the build job is estimated to complete."),
- readonly=True,
- )
-
- estimate = Bool(
- title=_("If true, the date value is an estimate."), readonly=True
- )
-
- date = Datetime(
- title=_(
- "The date when the build completed or is estimated to complete."
- ),
- readonly=True,
- )
-
- revision_id = TextLine(
- title=_("Revision ID"),
- required=False,
- readonly=True,
- description=_(
- "The revision ID of the branch used for this build, if "
- "available."
- ),
- )
-
- store_upload_metadata = Attribute(
- _("A dict of data about store upload progress.")
- )
-
- def getFiles():
- """Retrieve the build's `IRockFile` records.
-
- :return: A result set of (`IRockFile`, `ILibraryFileAlias`,
- `ILibraryFileContent`).
- """
-
- def getFileByName(filename):
- """Return the corresponding `ILibraryFileAlias` in this context.
-
- The following file types (and extension) can be looked up:
-
- * Build log: '.txt.gz'
- * Upload log: '_log.txt'
-
- Any filename not matching one of these extensions is looked up as a
- rock recipe output file.
-
- :param filename: The filename to look up.
- :raises NotFoundError: if no file exists with the given name.
- :return: The corresponding `ILibraryFileAlias`.
- """
-
-
-class IRockRecipeBuildEdit(IBuildFarmJobEdit):
- """`IRockRecipeBuild` methods that require launchpad.Edit."""
-
- def addFile(lfa):
- """Add a file to this build.
-
- :param lfa: An `ILibraryFileAlias`.
- :return: An `IRockFile`.
- """
-
-
-class IRockRecipeBuildAdmin(Interface):
- """`IRockRecipeBuild` methods that require launchpad.Admin."""
-
- def rescore(score):
- """Change the build's score."""
-
-
-class IRockRecipeBuild(
- IRockRecipeBuildView,
- IRockRecipeBuildEdit,
- IRockRecipeBuildAdmin,
- IPackageBuild,
-):
- """A build record for a rock recipe."""
-
-
-class IRockRecipeBuildSet(ISpecificBuildFarmJobSource):
- """Utility to create and access `IRockRecipeBuild`s."""
-
- def new(
- build_request,
- recipe,
- distro_arch_series,
- channels=None,
- store_upload_metadata=None,
- date_created=DEFAULT,
- ):
- """Create an `IRockRecipeBuild`."""
-
- def preloadBuildsData(builds):
- """Load the data related to a list of rock recipe builds."""
-
-
-class IRockFile(Interface):
- """A file produced by a rock recipe build."""
-
- build = Reference(
- IRockRecipeBuild,
- title=_("The rock recipe build producing this file."),
- required=True,
- readonly=True,
- )
-
- library_file = Reference(
- ILibraryFileAlias,
- title=_("The library file alias for this file."),
- required=True,
- readonly=True,
- )
diff --git a/lib/lp/rocks/interfaces/rockrecipejob.py b/lib/lp/rocks/interfaces/rockrecipejob.py
deleted file mode 100644
index f3f79a8..0000000
--- a/lib/lp/rocks/interfaces/rockrecipejob.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Rock recipe job interfaces."""
-
-__all__ = [
- "IRockRecipeJob",
- "IRockRecipeRequestBuildsJob",
- "IRockRecipeRequestBuildsJobSource",
-]
-
-from lazr.restful.fields import Reference
-from zope.interface import Attribute, Interface
-from zope.schema import Datetime, Dict, List, Set, TextLine
-
-from lp import _
-from lp.registry.interfaces.person import IPerson
-from lp.rocks.interfaces.rockrecipe import IRockRecipe, IRockRecipeBuildRequest
-from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuild
-from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
-
-
-class IRockRecipeJob(Interface):
- """A job related to a rock recipe."""
-
- job = Reference(
- title=_("The common Job attributes."),
- schema=IJob,
- required=True,
- readonly=True,
- )
-
- recipe = Reference(
- title=_("The rock recipe to use for this job."),
- schema=IRockRecipe,
- required=True,
- readonly=True,
- )
-
- metadata = Attribute(_("A dict of data about the job."))
-
-
-class IRockRecipeRequestBuildsJob(IRunnableJob):
- """A Job that processes a request for builds of a rock recipe."""
-
- requester = Reference(
- title=_("The person requesting the builds."),
- schema=IPerson,
- required=True,
- readonly=True,
- )
-
- channels = Dict(
- title=_("Source snap channels to use for these builds."),
- description=_(
- "A dictionary mapping snap names to channels to use for these "
- "builds. Currently only 'core', 'core18', 'core20', and "
- "'rockcraft' keys are supported."
- ),
- key_type=TextLine(),
- required=False,
- readonly=True,
- )
-
- architectures = Set(
- title=_("If set, limit builds to these architecture tags."),
- value_type=TextLine(),
- required=False,
- readonly=True,
- )
-
- date_created = Datetime(
- title=_("Time when this job was created."),
- required=True,
- readonly=True,
- )
-
- date_finished = Datetime(
- title=_("Time when this job finished."), required=True, readonly=True
- )
-
- error_message = TextLine(
- title=_("Error message resulting from running this job."),
- required=False,
- readonly=True,
- )
-
- build_request = Reference(
- title=_("The build request corresponding to this job."),
- schema=IRockRecipeBuildRequest,
- required=True,
- readonly=True,
- )
-
- builds = List(
- title=_("The builds created by this request."),
- value_type=Reference(schema=IRockRecipeBuild),
- required=True,
- readonly=True,
- )
-
-
-class IRockRecipeRequestBuildsJobSource(IJobSource):
-
- def create(recipe, requester, channels=None, architectures=None):
- """Request builds of a rock recipe.
-
- :param recipe: The rock recipe to build.
- :param requester: The person requesting the builds.
- :param channels: A dictionary mapping snap names to channels to use
- for these builds.
- :param architectures: If not None, limit builds to architectures
- with these architecture tags (in addition to any other
- applicable constraints).
- """
-
- def findByRecipe(recipe, statuses=None, job_ids=None):
- """Find jobs for a rock recipe.
-
- :param recipe: A rock recipe to search for.
- :param statuses: An optional iterable of `JobStatus`es to search for.
- :param job_ids: An optional iterable of job IDs to search for.
- :return: A sequence of `RockRecipeRequestBuildsJob`s with the
- specified recipe.
- """
-
- def getByRecipeAndID(recipe, job_id):
- """Get a job by rock recipe and job ID.
-
- :return: The `RockRecipeRequestBuildsJob` with the specified recipe
- and ID.
- :raises: `NotFoundError` if there is no job with the specified
- recipe and ID, or its `job_type` is not
- `RockRecipeJobType.REQUEST_BUILDS`.
- """
diff --git a/lib/lp/rocks/mail/rockrecipebuild.py b/lib/lp/rocks/mail/rockrecipebuild.py
deleted file mode 100644
index 5146f98..0000000
--- a/lib/lp/rocks/mail/rockrecipebuild.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__all__ = [
- "RockRecipeBuildMailer",
-]
-
-from lp.app.browser.tales import DurationFormatterAPI
-from lp.services.config import config
-from lp.services.mail.basemailer import BaseMailer, RecipientReason
-from lp.services.webapp import canonical_url
-
-
-class RockRecipeBuildMailer(BaseMailer):
-
- app = "rocks"
-
- @classmethod
- def forStatus(cls, build):
- """Create a mailer for notifying about rock recipe build status.
-
- :param build: The relevant build.
- """
- requester = build.requester
- recipients = {requester: RecipientReason.forBuildRequester(requester)}
- return cls(
- "[Rock recipe build #%(build_id)d] %(build_title)s",
- "rockrecipebuild-notification.txt",
- recipients,
- config.canonical.noreply_from_address,
- "rock-recipe-build-status",
- build,
- )
-
- def __init__(
- self,
- subject,
- template_name,
- recipients,
- from_address,
- notification_type,
- build,
- ):
- super().__init__(
- subject,
- template_name,
- recipients,
- from_address,
- notification_type=notification_type,
- )
- self.build = build
-
- def _getHeaders(self, email, recipient):
- """See `BaseMailer`."""
- headers = super()._getHeaders(email, recipient)
- headers["X-Launchpad-Build-State"] = self.build.status.name
- return headers
-
- def _getTemplateParams(self, email, recipient):
- """See `BaseMailer`."""
- build = self.build
- params = super()._getTemplateParams(email, recipient)
- params.update(
- {
- "architecturetag": build.distro_arch_series.architecturetag,
- "build_duration": "",
- "build_id": build.id,
- "build_state": build.status.title,
- "build_title": build.title,
- "build_url": canonical_url(build),
- "builder_url": "",
- "distroseries": build.distro_series,
- "log_url": "",
- "project_name": build.recipe.project.name,
- "recipe_name": build.recipe.name,
- "upload_log_url": "",
- }
- )
- if build.duration is not None:
- duration_formatter = DurationFormatterAPI(build.duration)
- params["build_duration"] = duration_formatter.approximateduration()
- if build.log is not None:
- params["log_url"] = build.log_url
- if build.upload_log is not None:
- params["upload_log_url"] = build.upload_log_url
- if build.builder is not None:
- params["builder_url"] = canonical_url(build.builder)
- return params
-
- def _getFooter(self, email, recipient, params):
- """See `BaseMailer`."""
- return "%(build_url)s\n" "%(reason)s\n" % params
diff --git a/lib/lp/rocks/model/rockrecipe.py b/lib/lp/rocks/model/rockrecipe.py
deleted file mode 100644
index f227ce7..0000000
--- a/lib/lp/rocks/model/rockrecipe.py
+++ /dev/null
@@ -1,595 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Rock recipes."""
-
-__all__ = [
- "RockRecipe",
-]
-
-from datetime import timezone
-from operator import itemgetter
-
-from lazr.lifecycle.event import ObjectCreatedEvent
-from storm.databases.postgres import JSON
-from storm.locals import (
- Bool,
- DateTime,
- Int,
- Join,
- Or,
- Reference,
- Store,
- Unicode,
-)
-from zope.component import getUtility
-from zope.event import notify
-from zope.interface import implementer
-from zope.security.proxy import removeSecurityProxy
-
-from lp.app.enums import (
- FREE_INFORMATION_TYPES,
- PUBLIC_INFORMATION_TYPES,
- InformationType,
-)
-from lp.buildmaster.enums import BuildStatus
-from lp.code.model.gitcollection import GenericGitCollection
-from lp.code.model.gitrepository import GitRepository
-from lp.registry.errors import PrivatePersonLinkageError
-from lp.registry.interfaces.person import IPersonSet, validate_public_person
-from lp.registry.model.distribution import Distribution
-from lp.registry.model.distroseries import DistroSeries
-from lp.registry.model.series import ACTIVE_STATUSES
-from lp.rocks.interfaces.rockrecipe import (
- ROCK_RECIPE_ALLOW_CREATE,
- ROCK_RECIPE_PRIVATE_FEATURE_FLAG,
- DuplicateRockRecipeName,
- IRockRecipe,
- IRockRecipeBuildRequest,
- IRockRecipeSet,
- NoSourceForRockRecipe,
- RockRecipeBuildAlreadyPending,
- RockRecipeBuildDisallowedArchitecture,
- RockRecipeBuildRequestStatus,
- RockRecipeFeatureDisabled,
- RockRecipeNotOwner,
- RockRecipePrivacyMismatch,
- RockRecipePrivateFeatureDisabled,
-)
-from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuildSet
-from lp.rocks.interfaces.rockrecipejob import IRockRecipeRequestBuildsJobSource
-from lp.rocks.model.rockrecipebuild import RockRecipeBuild
-from lp.services.database.bulk import load_related
-from lp.services.database.constants import DEFAULT, UTC_NOW
-from lp.services.database.decoratedresultset import DecoratedResultSet
-from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import IPrimaryStore, IStore
-from lp.services.database.stormbase import StormBase
-from lp.services.features import getFeatureFlag
-from lp.services.job.interfaces.job import JobStatus
-from lp.services.librarian.model import LibraryFileAlias
-from lp.services.propertycache import cachedproperty, get_property_cache
-from lp.soyuz.model.distroarchseries import DistroArchSeries, PocketChroot
-
-
-def rock_recipe_modified(recipe, event):
- """Update the date_last_modified property when a rock recipe is modified.
-
- This method is registered as a subscriber to `IObjectModifiedEvent`
- events on rock recipes.
- """
- removeSecurityProxy(recipe).date_last_modified = UTC_NOW
-
-
-@implementer(IRockRecipeBuildRequest)
-class RockRecipeBuildRequest:
- """See `IRockRecipeBuildRequest`.
-
- This is not directly backed by a database table; instead, it is a
- webservice-friendly view of an asynchronous build request.
- """
-
- def __init__(self, recipe, id):
- self.recipe = recipe
- self.id = id
-
- @classmethod
- def fromJob(cls, job):
- """See `IRockRecipeBuildRequest`."""
- request = cls(job.recipe, job.job_id)
- get_property_cache(request)._job = job
- return request
-
- @cachedproperty
- def _job(self):
- job_source = getUtility(IRockRecipeRequestBuildsJobSource)
- return job_source.getByRecipeAndID(self.recipe, self.id)
-
- @property
- def date_requested(self):
- """See `IRockRecipeBuildRequest`."""
- return self._job.date_created
-
- @property
- def date_finished(self):
- """See `IRockRecipeBuildRequest`."""
- return self._job.date_finished
-
- @property
- def status(self):
- """See `IRockRecipeBuildRequest`."""
- status_map = {
- JobStatus.WAITING: RockRecipeBuildRequestStatus.PENDING,
- JobStatus.RUNNING: RockRecipeBuildRequestStatus.PENDING,
- JobStatus.COMPLETED: RockRecipeBuildRequestStatus.COMPLETED,
- JobStatus.FAILED: RockRecipeBuildRequestStatus.FAILED,
- JobStatus.SUSPENDED: RockRecipeBuildRequestStatus.PENDING,
- }
- return status_map[self._job.job.status]
-
- @property
- def error_message(self):
- """See `IRockRecipeBuildRequest`."""
- return self._job.error_message
-
- @property
- def builds(self):
- """See `IRockRecipeBuildRequest`."""
- return self._job.builds
-
- @property
- def requester(self):
- """See `IRockRecipeBuildRequest`."""
- return self._job.requester
-
- @property
- def channels(self):
- """See `IRockRecipeBuildRequest`."""
- return self._job.channels
-
- @property
- def architectures(self):
- """See `IRockRecipeBuildRequest`."""
- return self._job.architectures
-
-
-@implementer(IRockRecipe)
-class RockRecipe(StormBase):
- """See `IRockRecipe`."""
-
- __storm_table__ = "RockRecipe"
-
- id = Int(primary=True)
-
- date_created = DateTime(
- name="date_created", tzinfo=timezone.utc, allow_none=False
- )
- date_last_modified = DateTime(
- name="date_last_modified", tzinfo=timezone.utc, allow_none=False
- )
-
- registrant_id = Int(name="registrant", allow_none=False)
- registrant = Reference(registrant_id, "Person.id")
-
- def _validate_owner(self, attr, value):
- if not self.private:
- try:
- validate_public_person(self, attr, value)
- except PrivatePersonLinkageError:
- raise RockRecipePrivacyMismatch(
- "A public rock recipe cannot have a private owner."
- )
- return value
-
- owner_id = Int(name="owner", allow_none=False, validator=_validate_owner)
- owner = Reference(owner_id, "Person.id")
-
- project_id = Int(name="project", allow_none=False)
- project = Reference(project_id, "Product.id")
-
- name = Unicode(name="name", allow_none=False)
-
- description = Unicode(name="description", allow_none=True)
-
- def _validate_git_repository(self, attr, value):
- if not self.private and value is not None:
- if IStore(GitRepository).get(GitRepository, value).private:
- raise RockRecipePrivacyMismatch(
- "A public rock recipe cannot have a private repository."
- )
- return value
-
- git_repository_id = Int(
- name="git_repository",
- allow_none=True,
- validator=_validate_git_repository,
- )
- git_repository = Reference(git_repository_id, "GitRepository.id")
-
- git_path = Unicode(name="git_path", allow_none=True)
-
- build_path = Unicode(name="build_path", allow_none=True)
-
- require_virtualized = Bool(name="require_virtualized")
-
- def _valid_information_type(self, attr, value):
- if not getUtility(IRockRecipeSet).isValidInformationType(
- value, self.owner, self.git_ref
- ):
- raise RockRecipePrivacyMismatch
- return value
-
- information_type = DBEnum(
- enum=InformationType,
- default=InformationType.PUBLIC,
- name="information_type",
- validator=_valid_information_type,
- allow_none=False,
- )
-
- auto_build = Bool(name="auto_build", allow_none=False)
-
- auto_build_channels = JSON("auto_build_channels", allow_none=True)
-
- is_stale = Bool(name="is_stale", allow_none=False)
-
- store_upload = Bool(name="store_upload", allow_none=False)
-
- store_name = Unicode(name="store_name", allow_none=True)
-
- store_secrets = JSON("store_secrets", allow_none=True)
-
- _store_channels = JSON("store_channels", allow_none=True)
-
- def __init__(
- self,
- registrant,
- owner,
- project,
- name,
- description=None,
- git_ref=None,
- build_path=None,
- require_virtualized=True,
- information_type=InformationType.PUBLIC,
- auto_build=False,
- auto_build_channels=None,
- store_upload=False,
- store_name=None,
- store_secrets=None,
- store_channels=None,
- date_created=DEFAULT,
- ):
- """Construct a `RockRecipe`."""
- if not getFeatureFlag(ROCK_RECIPE_ALLOW_CREATE):
- raise RockRecipeFeatureDisabled()
- super().__init__()
-
- # Set this first for use by other validators.
- self.information_type = information_type
-
- self.date_created = date_created
- self.date_last_modified = date_created
- self.registrant = registrant
- self.owner = owner
- self.project = project
- self.name = name
- self.description = description
- self.git_ref = git_ref
- self.build_path = build_path
- self.require_virtualized = require_virtualized
- self.auto_build = auto_build
- self.auto_build_channels = auto_build_channels
- self.store_upload = store_upload
- self.store_name = store_name
- self.store_secrets = store_secrets
- self.store_channels = store_channels
-
- def __repr__(self):
- return "<RockRecipe ~%s/%s/+rock/%s>" % (
- self.owner.name,
- self.project.name,
- self.name,
- )
-
- @property
- def private(self):
- """See `IRockRecipe`."""
- return self.information_type not in PUBLIC_INFORMATION_TYPES
-
- @property
- def git_ref(self):
- """See `IRockRecipe`."""
- if self.git_repository is not None:
- return self.git_repository.getRefByPath(self.git_path)
- else:
- return None
-
- @git_ref.setter
- def git_ref(self, value):
- """See `IRockRecipe`."""
- if value is not None:
- self.git_repository = value.repository
- self.git_path = value.path
- else:
- self.git_repository = None
- self.git_path = None
-
- @property
- def store_channels(self):
- """See `IRockRecipe`."""
- return self._store_channels or []
-
- @store_channels.setter
- def store_channels(self, value):
- """See `IRockRecipe`."""
- self._store_channels = value or None
-
- def getAllowedInformationTypes(self, user):
- """See `IRockRecipe`."""
- # XXX jugmac00 2024-08-29: Only allow free information types until
- # we have more privacy infrastructure in place.
- return FREE_INFORMATION_TYPES
-
- def visibleByUser(self, user):
- """See `IRockRecipe`."""
- if self.information_type in PUBLIC_INFORMATION_TYPES:
- return True
- # XXX jugmac00 2024-08-29: Finish implementing this once we have
- # more privacy infrastructure.
- return False
-
- def _isBuildableArchitectureAllowed(self, das):
- """Check whether we may build for a buildable `DistroArchSeries`.
-
- The caller is assumed to have already checked that a suitable chroot
- is available (either directly or via
- `DistroSeries.buildable_architectures`).
- """
- return das.enabled and (
- das.processor.supports_virtualized or not self.require_virtualized
- )
-
- def _isArchitectureAllowed(self, das):
- """Check whether we may build for a `DistroArchSeries`."""
- return (
- das.getChroot() is not None
- and self._isBuildableArchitectureAllowed(das)
- )
-
- def getAllowedArchitectures(self):
- """See `ICharmRecipe`."""
- store = Store.of(self)
- origin = [
- DistroArchSeries,
- Join(
- DistroSeries, DistroArchSeries.distroseries == DistroSeries.id
- ),
- Join(Distribution, DistroSeries.distribution == Distribution.id),
- Join(
- PocketChroot,
- PocketChroot.distroarchseries == DistroArchSeries.id,
- ),
- Join(LibraryFileAlias, PocketChroot.chroot == LibraryFileAlias.id),
- ]
- # Preload DistroSeries and Distribution, since we'll need those in
- # determine_architectures_to_build.
- results = store.using(*origin).find(
- (DistroArchSeries, DistroSeries, Distribution),
- DistroSeries.status.is_in(ACTIVE_STATUSES),
- )
- all_buildable_dases = DecoratedResultSet(results, itemgetter(0))
- return [
- das
- for das in all_buildable_dases
- if self._isBuildableArchitectureAllowed(das)
- ]
-
- def _checkRequestBuild(self, requester):
- """May `requester` request builds of this rock recipe?"""
- if not requester.inTeam(self.owner):
- raise RockRecipeNotOwner(
- "%s cannot create rock recipe builds owned by %s."
- % (requester.display_name, self.owner.display_name)
- )
-
- def requestBuild(self, build_request, distro_arch_series, channels=None):
- """Request a single build of this rock recipe.
-
- This method is for internal use; external callers should use
- `requestBuilds` instead.
-
- :param build_request: The `IRockRecipeBuildRequest` job being
- processed.
- :param distro_arch_series: The architecture to build for.
- :param channels: A dictionary mapping snap names to channels to use
- for this build.
- :return: `IRockRecipeBuild`.
- """
- self._checkRequestBuild(build_request.requester)
- if not self._isArchitectureAllowed(distro_arch_series):
- raise RockRecipeBuildDisallowedArchitecture(distro_arch_series)
-
- if not channels:
- channels_clause = Or(
- RockRecipeBuild.channels == None,
- RockRecipeBuild.channels == {},
- )
- else:
- channels_clause = RockRecipeBuild.channels == channels
- pending = IStore(self).find(
- RockRecipeBuild,
- RockRecipeBuild.recipe == self,
- RockRecipeBuild.processor == distro_arch_series.processor,
- channels_clause,
- RockRecipeBuild.status == BuildStatus.NEEDSBUILD,
- )
- if pending.any() is not None:
- raise RockRecipeBuildAlreadyPending
-
- build = getUtility(IRockRecipeBuildSet).new(
- build_request, self, distro_arch_series, channels=channels
- )
- build.queueBuild()
- notify(ObjectCreatedEvent(build, user=build_request.requester))
- return build
-
- def requestBuilds(self, requester, channels=None, architectures=None):
- """See `IRockRecipe`."""
- self._checkRequestBuild(requester)
- job = getUtility(IRockRecipeRequestBuildsJobSource).create(
- self, requester, channels=channels, architectures=architectures
- )
- return self.getBuildRequest(job.job_id)
-
- def getBuildRequest(self, job_id):
- """See `IRockRecipe`."""
- return RockRecipeBuildRequest(self, job_id)
-
- def destroySelf(self):
- """See `IRockRecipe`."""
- IStore(RockRecipe).remove(self)
-
-
-@implementer(IRockRecipeSet)
-class RockRecipeSet:
- """See `IRockRecipeSet`."""
-
- def new(
- self,
- registrant,
- owner,
- project,
- name,
- description=None,
- git_ref=None,
- build_path=None,
- require_virtualized=True,
- information_type=InformationType.PUBLIC,
- auto_build=False,
- auto_build_channels=None,
- store_upload=False,
- store_name=None,
- store_secrets=None,
- store_channels=None,
- date_created=DEFAULT,
- ):
- """See `IRockRecipeSet`."""
- if not registrant.inTeam(owner):
- if owner.is_team:
- raise RockRecipeNotOwner(
- "%s is not a member of %s."
- % (registrant.displayname, owner.displayname)
- )
- else:
- raise RockRecipeNotOwner(
- "%s cannot create rock recipes owned by %s."
- % (registrant.displayname, owner.displayname)
- )
-
- if git_ref is None:
- raise NoSourceForRockRecipe
- if self.getByName(owner, project, name) is not None:
- raise DuplicateRockRecipeName
-
- # The relevant validators will do their own checks as well, but we
- # do a single up-front check here in order to avoid an
- # IntegrityError due to exceptions being raised during object
- # creation and to ensure that everything relevant is in the Storm
- # cache.
- if not self.isValidInformationType(information_type, owner, git_ref):
- raise RockRecipePrivacyMismatch
- store = IPrimaryStore(RockRecipe)
- recipe = RockRecipe(
- registrant,
- owner,
- project,
- name,
- description=description,
- git_ref=git_ref,
- build_path=build_path,
- require_virtualized=require_virtualized,
- information_type=information_type,
- auto_build=auto_build,
- auto_build_channels=auto_build_channels,
- store_upload=store_upload,
- store_name=store_name,
- store_secrets=store_secrets,
- store_channels=store_channels,
- date_created=date_created,
- )
- store.add(recipe)
-
- return recipe
-
- def getByName(self, owner, project, name):
- """See `IRockRecipeSet`."""
- return (
- IStore(RockRecipe)
- .find(RockRecipe, owner=owner, project=project, name=name)
- .one()
- )
-
- def isValidInformationType(self, information_type, owner, git_ref=None):
- """See `IRockRecipeSet`."""
- private = information_type not in PUBLIC_INFORMATION_TYPES
- if private:
- # If appropriately enabled via feature flag.
- if not getFeatureFlag(ROCK_RECIPE_PRIVATE_FEATURE_FLAG):
- raise RockRecipePrivateFeatureDisabled
- return True
-
- # Public rock recipes with private sources are not allowed.
- if git_ref is not None and git_ref.private:
- return False
-
- # Public rock recipes owned by private teams are not allowed.
- if owner is not None and owner.private:
- return False
-
- return True
-
- def preloadDataForRecipes(self, recipes, user=None):
- """See `IRockRecipeSet`."""
- recipes = [removeSecurityProxy(recipe) for recipe in recipes]
-
- person_ids = set()
- for recipe in recipes:
- person_ids.add(recipe.registrant_id)
- person_ids.add(recipe.owner_id)
-
- repositories = load_related(
- GitRepository, recipes, ["git_repository_id"]
- )
- if repositories:
- GenericGitCollection.preloadDataForRepositories(repositories)
-
- # Add repository owners to the list of pre-loaded persons. We need
- # the target repository owner as well, since repository unique names
- # aren't trigger-maintained.
- person_ids.update(repository.owner_id for repository in repositories)
-
- list(
- getUtility(IPersonSet).getPrecachedPersonsFromIDs(
- person_ids, need_validity=True
- )
- )
-
- def findByGitRepository(self, repository, paths=None):
- """See `IRockRecipeSet`."""
- clauses = [RockRecipe.git_repository == repository]
- if paths is not None:
- clauses.append(RockRecipe.git_path.is_in(paths))
- # XXX jugmac00 2024-08-29: Check permissions once we have some
- # privacy infrastructure.
- return IStore(RockRecipe).find(RockRecipe, *clauses)
-
- def findByOwner(self, owner):
- """See `ICharmRecipeSet`."""
- return IStore(RockRecipe).find(RockRecipe, owner=owner)
-
- def detachFromGitRepository(self, repository):
- """See `ICharmRecipeSet`."""
- self.findByGitRepository(repository).set(
- git_repository_id=None, git_path=None, date_last_modified=UTC_NOW
- )
diff --git a/lib/lp/rocks/model/rockrecipebuild.py b/lib/lp/rocks/model/rockrecipebuild.py
deleted file mode 100644
index e6025e5..0000000
--- a/lib/lp/rocks/model/rockrecipebuild.py
+++ /dev/null
@@ -1,445 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Rock recipe builds."""
-
-__all__ = [
- "RockFile",
- "RockRecipeBuild",
-]
-
-from datetime import timedelta, timezone
-
-import six
-from storm.databases.postgres import JSON
-from storm.locals import Bool, DateTime, Desc, Int, Reference, Store, Unicode
-from storm.store import EmptyResultSet
-from zope.component import getUtility
-from zope.interface import implementer
-
-from lp.app.errors import NotFoundError
-from lp.buildmaster.enums import (
- BuildFarmJobType,
- BuildQueueStatus,
- BuildStatus,
-)
-from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
-from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
-from lp.buildmaster.model.packagebuild import PackageBuildMixin
-from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.registry.interfaces.series import SeriesStatus
-from lp.registry.model.person import Person
-from lp.rocks.interfaces.rockrecipe import IRockRecipeSet
-from lp.rocks.interfaces.rockrecipebuild import (
- IRockFile,
- IRockRecipeBuild,
- IRockRecipeBuildSet,
-)
-from lp.rocks.mail.rockrecipebuild import RockRecipeBuildMailer
-from lp.services.config import config
-from lp.services.database.bulk import load_related
-from lp.services.database.constants import DEFAULT
-from lp.services.database.decoratedresultset import DecoratedResultSet
-from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import IPrimaryStore, IStore
-from lp.services.database.stormbase import StormBase
-from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
-from lp.services.propertycache import cachedproperty, get_property_cache
-from lp.services.webapp.snapshot import notify_modified
-
-
-@implementer(IRockRecipeBuild)
-class RockRecipeBuild(PackageBuildMixin, StormBase):
- """See `IRockRecipeBuild`."""
-
- __storm_table__ = "RockRecipeBuild"
-
- job_type = BuildFarmJobType.ROCKRECIPEBUILD
-
- id = Int(name="id", primary=True)
-
- build_request_id = Int(name="build_request", allow_none=False)
-
- requester_id = Int(name="requester", allow_none=False)
- requester = Reference(requester_id, "Person.id")
-
- recipe_id = Int(name="recipe", allow_none=False)
- recipe = Reference(recipe_id, "RockRecipe.id")
-
- distro_arch_series_id = Int(name="distro_arch_series", allow_none=False)
- distro_arch_series = Reference(
- distro_arch_series_id, "DistroArchSeries.id"
- )
-
- channels = JSON("channels", allow_none=True)
-
- processor_id = Int(name="processor", allow_none=False)
- processor = Reference(processor_id, "Processor.id")
-
- virtualized = Bool(name="virtualized", allow_none=False)
-
- date_created = DateTime(
- name="date_created", tzinfo=timezone.utc, allow_none=False
- )
- date_started = DateTime(
- name="date_started", tzinfo=timezone.utc, allow_none=True
- )
- date_finished = DateTime(
- name="date_finished", tzinfo=timezone.utc, allow_none=True
- )
- date_first_dispatched = DateTime(
- name="date_first_dispatched", tzinfo=timezone.utc, allow_none=True
- )
-
- builder_id = Int(name="builder", allow_none=True)
- builder = Reference(builder_id, "Builder.id")
-
- status = DBEnum(name="status", enum=BuildStatus, allow_none=False)
-
- log_id = Int(name="log", allow_none=True)
- log = Reference(log_id, "LibraryFileAlias.id")
-
- upload_log_id = Int(name="upload_log", allow_none=True)
- upload_log = Reference(upload_log_id, "LibraryFileAlias.id")
-
- dependencies = Unicode(name="dependencies", allow_none=True)
-
- failure_count = Int(name="failure_count", allow_none=False)
-
- build_farm_job_id = Int(name="build_farm_job", allow_none=False)
- build_farm_job = Reference(build_farm_job_id, "BuildFarmJob.id")
-
- revision_id = Unicode(name="revision_id", allow_none=True)
-
- store_upload_metadata = JSON("store_upload_json_data", allow_none=True)
-
- def __init__(
- self,
- build_farm_job,
- build_request,
- recipe,
- distro_arch_series,
- processor,
- virtualized,
- channels=None,
- store_upload_metadata=None,
- date_created=DEFAULT,
- ):
- """Construct a `RockRecipeBuild`."""
- requester = build_request.requester
- super().__init__()
- self.build_farm_job = build_farm_job
- self.build_request_id = build_request.id
- self.requester = requester
- self.recipe = recipe
- self.distro_arch_series = distro_arch_series
- self.processor = processor
- self.virtualized = virtualized
- self.channels = channels
- self.store_upload_metadata = store_upload_metadata
- self.date_created = date_created
- self.status = BuildStatus.NEEDSBUILD
-
- @property
- def build_request(self):
- return self.recipe.getBuildRequest(self.build_request_id)
-
- @property
- def is_private(self):
- """See `IBuildFarmJob`."""
- return self.recipe.private or self.recipe.owner.private
-
- def __repr__(self):
- return "<RockRecipeBuild ~%s/%s/+rock/%s/+build/%d>" % (
- self.recipe.owner.name,
- self.recipe.project.name,
- self.recipe.name,
- self.id,
- )
-
- @property
- def title(self):
- return "%s build of /~%s/%s/+rock/%s" % (
- self.distro_arch_series.architecturetag,
- self.recipe.owner.name,
- self.recipe.project.name,
- self.recipe.name,
- )
-
- @property
- def distribution(self):
- """See `IPackageBuild`."""
- return self.distro_arch_series.distroseries.distribution
-
- @property
- def distro_series(self):
- """See `IPackageBuild`."""
- return self.distro_arch_series.distroseries
-
- @property
- def archive(self):
- """See `IPackageBuild`."""
- return self.distribution.main_archive
-
- @property
- def pocket(self):
- """See `IPackageBuild`."""
- return PackagePublishingPocket.UPDATES
-
- @property
- def score(self):
- """See `IRockRecipeBuild`."""
- if self.buildqueue_record is None:
- return None
- else:
- return self.buildqueue_record.lastscore
-
- @property
- def can_be_retried(self):
- """See `IBuildFarmJob`."""
- # First check that the behaviour would accept the build if it
- # succeeded.
- if self.distro_series.status == SeriesStatus.OBSOLETE:
- return False
- return super().can_be_retried
-
- def calculateScore(self):
- """See `IBuildFarmJob`."""
- # XXX jugmac00 2024-09-08: We'll probably need something like
- # RockRecipe.relative_build_score at some point.
- return 2510
-
- def getMedianBuildDuration(self):
- """Return the median duration of our successful builds."""
- store = IStore(self)
- result = store.find(
- (RockRecipeBuild.date_started, RockRecipeBuild.date_finished),
- RockRecipeBuild.recipe == self.recipe,
- RockRecipeBuild.processor == self.processor,
- RockRecipeBuild.status == BuildStatus.FULLYBUILT,
- )
- result.order_by(Desc(RockRecipeBuild.date_finished))
- durations = [row[1] - row[0] for row in result[:9]]
- if len(durations) == 0:
- return None
- durations.sort()
- return durations[len(durations) // 2]
-
- def estimateDuration(self):
- """See `IBuildFarmJob`."""
- median = self.getMedianBuildDuration()
- if median is not None:
- return median
- return timedelta(minutes=10)
-
- @cachedproperty
- def eta(self):
- """The datetime when the build job is estimated to complete.
-
- This is the BuildQueue.estimated_duration plus the
- Job.date_started or BuildQueue.getEstimatedJobStartTime.
- """
- if self.buildqueue_record is None:
- return None
- queue_record = self.buildqueue_record
- if queue_record.status == BuildQueueStatus.WAITING:
- start_time = queue_record.getEstimatedJobStartTime()
- else:
- start_time = queue_record.date_started
- if start_time is None:
- return None
- duration = queue_record.estimated_duration
- return start_time + duration
-
- @property
- def estimate(self):
- """If true, the date value is an estimate."""
- if self.date_finished is not None:
- return False
- return self.eta is not None
-
- @property
- def date(self):
- """The date when the build completed or is estimated to complete."""
- if self.estimate:
- return self.eta
- return self.date_finished
-
- def getFiles(self):
- """See `IRockRecipeBuild`."""
- result = Store.of(self).find(
- (RockFile, LibraryFileAlias, LibraryFileContent),
- RockFile.build == self.id,
- LibraryFileAlias.id == RockFile.library_file_id,
- LibraryFileContent.id == LibraryFileAlias.content_id,
- )
- return result.order_by([LibraryFileAlias.filename, RockFile.id])
-
- def getFileByName(self, filename):
- """See `IRockRecipeBuild`."""
- if filename.endswith(".txt.gz"):
- file_object = self.log
- elif filename.endswith("_log.txt"):
- file_object = self.upload_log
- else:
- file_object = (
- Store.of(self)
- .find(
- LibraryFileAlias,
- RockFile.build == self.id,
- LibraryFileAlias.id == RockFile.library_file_id,
- LibraryFileAlias.filename == filename,
- )
- .one()
- )
-
- if file_object is not None and file_object.filename == filename:
- return file_object
-
- raise NotFoundError(filename)
-
- def addFile(self, lfa):
- """See `IRockRecipeBuild`."""
- rock_file = RockFile(build=self, library_file=lfa)
- IPrimaryStore(RockFile).add(rock_file)
- return rock_file
-
- def verifySuccessfulUpload(self):
- """See `IPackageBuild`."""
- return not self.getFiles().is_empty()
-
- def updateStatus(
- self,
- status,
- builder=None,
- worker_status=None,
- date_started=None,
- date_finished=None,
- force_invalid_transition=False,
- ):
- """See `IBuildFarmJob`."""
- edited_fields = set()
- with notify_modified(
- self, edited_fields, snapshot_names=("status", "revision_id")
- ) as previous_obj:
- super().updateStatus(
- status,
- builder=builder,
- worker_status=worker_status,
- date_started=date_started,
- date_finished=date_finished,
- force_invalid_transition=force_invalid_transition,
- )
- if self.status != previous_obj.status:
- edited_fields.add("status")
- if worker_status is not None:
- revision_id = worker_status.get("revision_id")
- if revision_id is not None:
- self.revision_id = six.ensure_text(revision_id)
- if revision_id != previous_obj.revision_id:
- edited_fields.add("revision_id")
- # notify_modified evaluates all attributes mentioned in the
- # interface, but we may then make changes that affect self.eta.
- del get_property_cache(self).eta
-
- def notify(self, extra_info=None):
- """See `IPackageBuild`."""
- if not config.builddmaster.send_build_notification:
- return
- if self.status == BuildStatus.FULLYBUILT:
- return
- mailer = RockRecipeBuildMailer.forStatus(self)
- mailer.sendAll()
-
-
-@implementer(IRockRecipeBuildSet)
-class RockRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
- """See `IRockRecipeBuildSet`."""
-
- def new(
- self,
- build_request,
- recipe,
- distro_arch_series,
- channels=None,
- store_upload_metadata=None,
- date_created=DEFAULT,
- ):
- """See `IRockRecipeBuildSet`."""
- store = IPrimaryStore(RockRecipeBuild)
- build_farm_job = getUtility(IBuildFarmJobSource).new(
- RockRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created
- )
- virtualized = (
- not distro_arch_series.processor.supports_nonvirtualized
- or recipe.require_virtualized
- )
- build = RockRecipeBuild(
- build_farm_job,
- build_request,
- recipe,
- distro_arch_series,
- distro_arch_series.processor,
- virtualized,
- channels=channels,
- store_upload_metadata=store_upload_metadata,
- date_created=date_created,
- )
- store.add(build)
- return build
-
- def getByID(self, build_id):
- """See `ISpecificBuildFarmJobSource`."""
- store = IPrimaryStore(RockRecipeBuild)
- return store.get(RockRecipeBuild, build_id)
-
- def getByBuildFarmJob(self, build_farm_job):
- """See `ISpecificBuildFarmJobSource`."""
- return (
- Store.of(build_farm_job)
- .find(RockRecipeBuild, build_farm_job_id=build_farm_job.id)
- .one()
- )
-
- def preloadBuildsData(self, builds):
- # Circular import.
- from lp.rocks.model.rockrecipe import RockRecipe
-
- load_related(Person, builds, ["requester_id"])
- lfas = load_related(LibraryFileAlias, builds, ["log_id"])
- load_related(LibraryFileContent, lfas, ["contentID"])
- recipes = load_related(RockRecipe, builds, ["recipe_id"])
- getUtility(IRockRecipeSet).preloadDataForRecipes(recipes)
-
- def getByBuildFarmJobs(self, build_farm_jobs):
- """See `ISpecificBuildFarmJobSource`."""
- if len(build_farm_jobs) == 0:
- return EmptyResultSet()
- rows = Store.of(build_farm_jobs[0]).find(
- RockRecipeBuild,
- RockRecipeBuild.build_farm_job_id.is_in(
- bfj.id for bfj in build_farm_jobs
- ),
- )
- return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
-
-
-@implementer(IRockFile)
-class RockFile(StormBase):
- """See `IRockFile`."""
-
- __storm_table__ = "RockFile"
-
- id = Int(name="id", primary=True)
-
- build_id = Int(name="build", allow_none=False)
- build = Reference(build_id, "RockRecipeBuild.id")
-
- library_file_id = Int(name="library_file", allow_none=False)
- library_file = Reference(library_file_id, "LibraryFileAlias.id")
-
- def __init__(self, build, library_file):
- """Construct a `RockFile`."""
- super().__init__()
- self.build = build
- self.library_file = library_file
diff --git a/lib/lp/rocks/model/rockrecipebuildbehaviour.py b/lib/lp/rocks/model/rockrecipebuildbehaviour.py
deleted file mode 100644
index f1f9d04..0000000
--- a/lib/lp/rocks/model/rockrecipebuildbehaviour.py
+++ /dev/null
@@ -1,124 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""An `IBuildFarmJobBehaviour` for `RockRecipeBuild`.
-
-Dispatches rock recipe build jobs to build-farm workers.
-"""
-
-__all__ = [
- "RockRecipeBuildBehaviour",
-]
-
-from typing import Any, Generator
-
-from twisted.internet import defer
-from zope.component import adapter
-from zope.interface import implementer
-from zope.security.proxy import removeSecurityProxy
-
-from lp.buildmaster.builderproxy import BuilderProxyMixin
-from lp.buildmaster.enums import BuildBaseImageType
-from lp.buildmaster.interfaces.builder import CannotBuild
-from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
- BuildArgs,
- IBuildFarmJobBehaviour,
-)
-from lp.buildmaster.model.buildfarmjobbehaviour import (
- BuildFarmJobBehaviourBase,
-)
-from lp.registry.interfaces.series import SeriesStatus
-from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuild
-from lp.soyuz.adapters.archivedependencies import get_sources_list_for_building
-
-
-@adapter(IRockRecipeBuild)
-@implementer(IBuildFarmJobBehaviour)
-class RockRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
- """Dispatches `RockRecipeBuild` jobs to workers."""
-
- builder_type = "rock"
- image_types = [BuildBaseImageType.LXD, BuildBaseImageType.CHROOT]
-
- def getLogFileName(self):
- das = self.build.distro_arch_series
-
- # Examples:
- # buildlog_rock_ubuntu_wily_amd64_name_FULLYBUILT.txt
- return "buildlog_rock_%s_%s_%s_%s_%s.txt" % (
- das.distroseries.distribution.name,
- das.distroseries.name,
- das.architecturetag,
- self.build.recipe.name,
- self.build.status.name,
- )
-
- def verifyBuildRequest(self, logger):
- """Assert some pre-build checks.
-
- The build request is checked:
- * Virtualized builds can't build on a non-virtual builder
- * Ensure that we have a chroot
- """
- build = self.build
- if build.virtualized and not self._builder.virtualized:
- raise AssertionError(
- "Attempt to build virtual item on a non-virtual builder."
- )
-
- chroot = build.distro_arch_series.getChroot()
- if chroot is None:
- raise CannotBuild(
- "Missing chroot for %s" % build.distro_arch_series.displayname
- )
-
- @defer.inlineCallbacks
- def extraBuildArgs(self, logger=None) -> Generator[Any, Any, BuildArgs]:
- """
- Return the extra arguments required by the worker for the given build.
- """
- build = self.build
- args: BuildArgs = yield super().extraBuildArgs(logger=logger)
- yield self.startProxySession(args)
- args["name"] = build.recipe.store_name or build.recipe.name
- channels = build.channels or {}
- # We have to remove the security proxy that Zope applies to this
- # dict, since otherwise we'll be unable to serialise it to XML-RPC.
- args["channels"] = removeSecurityProxy(channels)
- (
- args["archives"],
- args["trusted_keys"],
- ) = yield get_sources_list_for_building(
- self, build.distro_arch_series, None, logger=logger
- )
- if build.recipe.build_path is not None:
- args["build_path"] = build.recipe.build_path
- if build.recipe.build_path is not None:
- args["build_path"] = build.recipe.build_path
- if build.recipe.git_ref is not None:
- args["git_repository"] = build.recipe.git_repository.git_https_url
- # "git clone -b" doesn't accept full ref names. If this becomes
- # a problem then we could change launchpad-buildd to do "git
- # clone" followed by "git checkout" instead.
- if build.recipe.git_path != "HEAD":
- args["git_path"] = build.recipe.git_ref.name
- else:
- raise CannotBuild(
- "Source repository for ~%s/%s/+rock/%s has been deleted."
- % (
- build.recipe.owner.name,
- build.recipe.project.name,
- build.recipe.name,
- )
- )
- args["private"] = build.is_private
- return args
-
- def verifySuccessfulBuild(self):
- """See `IBuildFarmJobBehaviour`."""
- # The implementation in BuildFarmJobBehaviourBase checks whether the
- # target suite is modifiable in the target archive. However, a
- # `RockRecipeBuild`'s archive is a source rather than a target, so
- # that check does not make sense. We do, however, refuse to build
- # for obsolete series.
- assert self.build.distro_series.status != SeriesStatus.OBSOLETE
diff --git a/lib/lp/rocks/model/rockrecipejob.py b/lib/lp/rocks/model/rockrecipejob.py
deleted file mode 100644
index b0211af..0000000
--- a/lib/lp/rocks/model/rockrecipejob.py
+++ /dev/null
@@ -1,316 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Rock recipe jobs."""
-
-__all__ = [
- "RockRecipeJob",
- "RockRecipeJobType",
- "RockRecipeRequestBuildsJob",
-]
-
-import transaction
-from lazr.delegates import delegate_to
-from lazr.enum import DBEnumeratedType, DBItem
-from storm.databases.postgres import JSON
-from storm.locals import Desc, Int, Reference
-from storm.store import EmptyResultSet
-from zope.component import getUtility
-from zope.interface import implementer, provider
-
-from lp.app.errors import NotFoundError
-from lp.registry.interfaces.person import IPersonSet
-from lp.rocks.interfaces.rockrecipejob import (
- IRockRecipeJob,
- IRockRecipeRequestBuildsJob,
- IRockRecipeRequestBuildsJobSource,
-)
-from lp.rocks.model.rockrecipebuild import RockRecipeBuild
-from lp.services.config import config
-from lp.services.database.bulk import load_related
-from lp.services.database.decoratedresultset import DecoratedResultSet
-from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import IPrimaryStore, IStore
-from lp.services.database.stormbase import StormBase
-from lp.services.job.model.job import EnumeratedSubclass, Job
-from lp.services.job.runner import BaseRunnableJob
-from lp.services.mail.sendmail import format_address_for_person
-from lp.services.propertycache import cachedproperty
-from lp.services.scripts import log
-
-
-class RockRecipeJobType(DBEnumeratedType):
- """Values that `IRockRecipeJob.job_type` can take."""
-
- REQUEST_BUILDS = DBItem(
- 0,
- """
- Request builds
-
- This job requests builds of a rock recipe.
- """,
- )
-
-
-@implementer(IRockRecipeJob)
-class RockRecipeJob(StormBase):
- """See `IRockRecipeJob`."""
-
- __storm_table__ = "RockRecipeJob"
-
- job_id = Int(name="job", primary=True, allow_none=False)
- job = Reference(job_id, "Job.id")
-
- recipe_id = Int(name="recipe", allow_none=False)
- recipe = Reference(recipe_id, "RockRecipe.id")
-
- job_type = DBEnum(
- name="job_type", enum=RockRecipeJobType, allow_none=False
- )
-
- metadata = JSON("json_data", allow_none=False)
-
- def __init__(self, recipe, job_type, metadata, **job_args):
- """Constructor.
-
- Extra keyword arguments are used to construct the underlying Job
- object.
-
- :param recipe: The `IRockRecipe` this job relates to.
- :param job_type: The `RockRecipeJobType` of this job.
- :param metadata: The type-specific variables, as a JSON-compatible
- dict.
- """
- super().__init__()
- self.job = Job(**job_args)
- self.recipe = recipe
- self.job_type = job_type
- self.metadata = metadata
-
- def makeDerived(self):
- return RockRecipeJobDerived.makeSubclass(self)
-
-
-@delegate_to(IRockRecipeJob)
-class RockRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
-
- def __init__(self, recipe_job):
- self.context = recipe_job
-
- def __repr__(self):
- """An informative representation of the job."""
- return "<%s for ~%s/%s/+rock/%s>" % (
- self.__class__.__name__,
- self.recipe.owner.name,
- self.recipe.project.name,
- self.recipe.name,
- )
-
- @classmethod
- def get(cls, job_id):
- """Get a job by id.
-
- :return: The `RockRecipeJob` with the specified id, as the current
- `RockRecipeJobDerived` subclass.
- :raises: `NotFoundError` if there is no job with the specified id,
- or its `job_type` does not match the desired subclass.
- """
- recipe_job = IStore(RockRecipeJob).get(RockRecipeJob, job_id)
- if recipe_job.job_type != cls.class_job_type:
- raise NotFoundError(
- "No object found with id %d and type %s"
- % (job_id, cls.class_job_type.title)
- )
- return cls(recipe_job)
-
- @classmethod
- def iterReady(cls):
- """See `IJobSource`."""
- jobs = IPrimaryStore(RockRecipeJob).find(
- RockRecipeJob,
- RockRecipeJob.job_type == cls.class_job_type,
- RockRecipeJob.job == Job.id,
- Job.id.is_in(Job.ready_jobs),
- )
- return (cls(job) for job in jobs)
-
- def getOopsVars(self):
- """See `IRunnableJob`."""
- oops_vars = super().getOopsVars()
- oops_vars.extend(
- [
- ("job_id", self.context.job.id),
- ("job_type", self.context.job_type.title),
- ("recipe_owner_name", self.context.recipe.owner.name),
- ("recipe_project_name", self.context.recipe.project.name),
- ("recipe_name", self.context.recipe.name),
- ]
- )
- return oops_vars
-
-
-@implementer(IRockRecipeRequestBuildsJob)
-@provider(IRockRecipeRequestBuildsJobSource)
-class RockRecipeRequestBuildsJob(RockRecipeJobDerived):
- """A Job that processes a request for builds of a rock recipe."""
-
- class_job_type = RockRecipeJobType.REQUEST_BUILDS
-
- max_retries = 5
-
- config = config.IRockRecipeRequestBuildsJobSource
-
- @classmethod
- def create(cls, recipe, requester, channels=None, architectures=None):
- """See `IRockRecipeRequestBuildsJobSource`."""
- metadata = {
- "requester": requester.id,
- "channels": channels,
- # Really a set or None, but sets aren't directly
- # JSON-serialisable.
- "architectures": (
- list(architectures) if architectures is not None else None
- ),
- }
- recipe_job = RockRecipeJob(recipe, cls.class_job_type, metadata)
- job = cls(recipe_job)
- job.celeryRunOnCommit()
- IStore(RockRecipeJob).flush()
- return job
-
- @classmethod
- def findByRecipe(cls, recipe, statuses=None, job_ids=None):
- """See `IRockRecipeRequestBuildsJobSource`."""
- clauses = [
- RockRecipeJob.recipe == recipe,
- RockRecipeJob.job_type == cls.class_job_type,
- ]
- if statuses is not None:
- clauses.extend(
- [
- RockRecipeJob.job == Job.id,
- Job._status.is_in(statuses),
- ]
- )
- if job_ids is not None:
- clauses.append(RockRecipeJob.job_id.is_in(job_ids))
- recipe_jobs = (
- IStore(RockRecipeJob)
- .find(RockRecipeJob, *clauses)
- .order_by(Desc(RockRecipeJob.job_id))
- )
-
- def preload_jobs(rows):
- load_related(Job, rows, ["job_id"])
-
- return DecoratedResultSet(
- recipe_jobs,
- lambda recipe_job: cls(recipe_job),
- pre_iter_hook=preload_jobs,
- )
-
- @classmethod
- def getByRecipeAndID(cls, recipe, job_id):
- """See `IRockRecipeRequestBuildsJobSource`."""
- recipe_job = (
- IStore(RockRecipeJob)
- .find(
- RockRecipeJob,
- RockRecipeJob.job_id == job_id,
- RockRecipeJob.recipe == recipe,
- RockRecipeJob.job_type == cls.class_job_type,
- )
- .one()
- )
- if recipe_job is None:
- raise NotFoundError(
- "No REQUEST_BUILDS job with ID %d found for %r"
- % (job_id, recipe)
- )
- return cls(recipe_job)
-
- def getOperationDescription(self):
- return "requesting builds of %s" % self.recipe.name
-
- def getErrorRecipients(self):
- if self.requester is None or self.requester.preferredemail is None:
- return []
- return [format_address_for_person(self.requester)]
-
- @cachedproperty
- def requester(self):
- """See `IRockRecipeRequestBuildsJob`."""
- requester_id = self.metadata["requester"]
- return getUtility(IPersonSet).get(requester_id)
-
- @property
- def channels(self):
- """See `IRockRecipeRequestBuildsJob`."""
- return self.metadata["channels"]
-
- @property
- def architectures(self):
- """See `IRockRecipeRequestBuildsJob`."""
- architectures = self.metadata["architectures"]
- return set(architectures) if architectures is not None else None
-
- @property
- def date_created(self):
- """See `IRockRecipeRequestBuildsJob`."""
- return self.context.job.date_created
-
- @property
- def date_finished(self):
- """See `IRockRecipeRequestBuildsJob`."""
- return self.context.job.date_finished
-
- @property
- def error_message(self):
- """See `IRockRecipeRequestBuildsJob`."""
- return self.metadata.get("error_message")
-
- @error_message.setter
- def error_message(self, message):
- """See `IRockRecipeRequestBuildsJob`."""
- self.metadata["error_message"] = message
-
- @property
- def build_request(self):
- """See `IRockRecipeRequestBuildsJob`."""
- return self.recipe.getBuildRequest(self.job.id)
-
- @property
- def builds(self):
- """See `IRockRecipeRequestBuildsJob`."""
- build_ids = self.metadata.get("builds")
- if build_ids:
- return IStore(RockRecipeBuild).find(
- RockRecipeBuild, RockRecipeBuild.id.is_in(build_ids)
- )
- else:
- return EmptyResultSet()
-
- @builds.setter
- def builds(self, builds):
- """See `IRockRecipeRequestBuildsJob`."""
- self.metadata["builds"] = [build.id for build in builds]
-
- def run(self):
- """See `IRunnableJob`."""
- requester = self.requester
- if requester is None:
- log.info(
- "Skipping %r because the requester has been deleted." % self
- )
- return
- try:
- # XXX jugmac00 2024-09-06: Implement this once we have a
- # RockRecipeBuild model.
- raise NotImplementedError
- except Exception as e:
- self.error_message = str(e)
- # The normal job infrastructure will abort the transaction, but
- # we want to commit instead: the only database changes we make
- # are to this job's metadata and should be preserved.
- transaction.commit()
- raise
diff --git a/lib/lp/rocks/security.py b/lib/lp/rocks/security.py
deleted file mode 100644
index e45cec4..0000000
--- a/lib/lp/rocks/security.py
+++ /dev/null
@@ -1,90 +0,0 @@
-# Copyright 2009-2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Security adapters for the rocks package."""
-
-__all__ = []
-
-from lp.app.security import AuthorizationBase, DelegatedAuthorization
-from lp.rocks.interfaces.rockrecipe import IRockRecipe, IRockRecipeBuildRequest
-from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuild
-from lp.security import AdminByBuilddAdmin
-
-
-class ViewRockRecipe(AuthorizationBase):
- """Private rock recipes are only visible to their owners and admins."""
-
- permission = "launchpad.View"
- usedfor = IRockRecipe
-
- def checkAuthenticated(self, user):
- return self.obj.visibleByUser(user.person)
-
- def checkUnauthenticated(self):
- return self.obj.visibleByUser(None)
-
-
-class EditRockRecipe(AuthorizationBase):
- permission = "launchpad.Edit"
- usedfor = IRockRecipe
-
- def checkAuthenticated(self, user):
- return (
- user.isOwner(self.obj) or user.in_commercial_admin or user.in_admin
- )
-
-
-class AdminRockRecipe(AuthorizationBase):
- """Restrict changing build settings on rock recipes.
-
- The security of the non-virtualised build farm depends on these
- settings, so they can only be changed by "PPA"/commercial admins, or by
- "PPA" self admins on rock recipes that they can already edit.
- """
-
- permission = "launchpad.Admin"
- usedfor = IRockRecipe
-
- def checkAuthenticated(self, user):
- if user.in_ppa_admin or user.in_commercial_admin or user.in_admin:
- return True
- return user.in_ppa_self_admins and EditRockRecipe(
- self.obj
- ).checkAuthenticated(user)
-
-
-class ViewCharmRecipeBuildRequest(DelegatedAuthorization):
- permission = "launchpad.View"
- usedfor = IRockRecipeBuildRequest
-
- def __init__(self, obj):
- super().__init__(obj, obj.recipe, "launchpad.View")
-
-
-class ViewRockRecipeBuild(DelegatedAuthorization):
- permission = "launchpad.View"
- usedfor = IRockRecipeBuild
-
- def iter_objects(self):
- yield self.obj.recipe
-
-
-class EditRockRecipeBuild(AdminByBuilddAdmin):
- permission = "launchpad.Edit"
- usedfor = IRockRecipeBuild
-
- def checkAuthenticated(self, user):
- """Check edit access for rock recipe builds.
-
- Allow admins, buildd admins, and the owner of the rock recipe.
- (Note that the requester of the build is required to be in the team
- that owns the rock recipe.)
- """
- auth_recipe = EditRockRecipe(self.obj.recipe)
- if auth_recipe.checkAuthenticated(user):
- return True
- return super().checkAuthenticated(user)
-
-
-class AdminRockRecipeBuild(AdminByBuilddAdmin):
- usedfor = IRockRecipeBuild
diff --git a/lib/lp/rocks/tests/__init__.py b/lib/lp/rocks/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/lib/lp/rocks/tests/__init__.py
+++ /dev/null
diff --git a/lib/lp/rocks/tests/test_rockrecipe.py b/lib/lp/rocks/tests/test_rockrecipe.py
deleted file mode 100644
index 1f9c807..0000000
--- a/lib/lp/rocks/tests/test_rockrecipe.py
+++ /dev/null
@@ -1,566 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Test rock recipes."""
-
-from storm.locals import Store
-from testtools.matchers import (
- Equals,
- Is,
- MatchesDict,
- MatchesSetwise,
- MatchesStructure,
-)
-from zope.component import getUtility
-from zope.security.proxy import removeSecurityProxy
-
-from lp.app.enums import InformationType
-from lp.buildmaster.enums import BuildQueueStatus, BuildStatus
-from lp.buildmaster.interfaces.buildqueue import IBuildQueue
-from lp.buildmaster.interfaces.processor import (
- IProcessorSet,
- ProcessorNotFound,
-)
-from lp.buildmaster.model.buildqueue import BuildQueue
-from lp.rocks.interfaces.rockrecipe import (
- ROCK_RECIPE_ALLOW_CREATE,
- IRockRecipe,
- IRockRecipeSet,
- NoSourceForRockRecipe,
- RockRecipeBuildAlreadyPending,
- RockRecipeBuildDisallowedArchitecture,
- RockRecipeBuildRequestStatus,
- RockRecipeFeatureDisabled,
- RockRecipePrivateFeatureDisabled,
-)
-from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuild
-from lp.rocks.interfaces.rockrecipejob import IRockRecipeRequestBuildsJobSource
-from lp.services.database.constants import ONE_DAY_AGO, UTC_NOW
-from lp.services.database.interfaces import IStore
-from lp.services.database.sqlbase import get_transaction_timestamp
-from lp.services.features.testing import FeatureFixture
-from lp.services.job.interfaces.job import JobStatus
-from lp.services.webapp.snapshot import notify_modified
-from lp.testing import TestCaseWithFactory, admin_logged_in, person_logged_in
-from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadZopelessLayer
-
-
-class TestRockRecipeFeatureFlags(TestCaseWithFactory):
-
- layer = LaunchpadZopelessLayer
-
- def test_feature_flag_disabled(self):
- # Without a feature flag, we wil not create any rock recipes.
- self.assertRaises(
- RockRecipeFeatureDisabled, self.factory.makeRockRecipe
- )
-
- def test_private_feature_flag_disabled(self):
- # Without a private feature flag, we will not create new private
- # rock recipes.
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- self.assertRaises(
- RockRecipePrivateFeatureDisabled,
- self.factory.makeRockRecipe,
- information_type=InformationType.PROPRIETARY,
- )
-
-
-class TestRockRecipe(TestCaseWithFactory):
-
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- super().setUp()
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
-
- def test_implements_interfaces(self):
- # RockRecipe implements IRockRecipe.
- recipe = self.factory.makeRockRecipe()
- with admin_logged_in():
- self.assertProvides(recipe, IRockRecipe)
-
- def test___repr__(self):
- # RockRecipe objects have an informative __repr__.
- recipe = self.factory.makeRockRecipe()
- self.assertEqual(
- "<RockRecipe ~%s/%s/+rock/%s>"
- % (recipe.owner.name, recipe.project.name, recipe.name),
- repr(recipe),
- )
-
- def test_initial_date_last_modified(self):
- # The initial value of date_last_modified is date_created.
- recipe = self.factory.makeRockRecipe(date_created=ONE_DAY_AGO)
- self.assertEqual(recipe.date_created, recipe.date_last_modified)
-
- def test_modifiedevent_sets_date_last_modified(self):
- # When a RockRecipe receives an object modified event, the last
- # modified date is set to UTC_NOW.
- recipe = self.factory.makeRockRecipe(date_created=ONE_DAY_AGO)
- with notify_modified(removeSecurityProxy(recipe), ["name"]):
- pass
- self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW
- )
-
- def test_requestBuilds(self):
- # requestBuilds schedules a job and returns a corresponding
- # RockRecipeBuildRequest.
- recipe = self.factory.makeRockRecipe()
- now = get_transaction_timestamp(IStore(recipe))
- with person_logged_in(recipe.owner.teamowner):
- request = recipe.requestBuilds(recipe.owner.teamowner)
- self.assertThat(
- request,
- MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(RockRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=Is(None),
- architectures=Is(None),
- ),
- )
- [job] = getUtility(IRockRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(
- job,
- MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner.teamowner),
- channels=Is(None),
- architectures=Is(None),
- ),
- )
-
- def test_requestBuilds_with_channels(self):
- # If asked to build using particular snap channels, requestBuilds
- # passes those through to the job.
- recipe = self.factory.makeRockRecipe()
- now = get_transaction_timestamp(IStore(recipe))
- with person_logged_in(recipe.owner.teamowner):
- request = recipe.requestBuilds(
- recipe.owner.teamowner, channels={"rockcraft": "edge"}
- )
- self.assertThat(
- request,
- MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(RockRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=MatchesDict({"rockcraft": Equals("edge")}),
- architectures=Is(None),
- ),
- )
- [job] = getUtility(IRockRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(
- job,
- MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner.teamowner),
- channels=Equals({"rockcraft": "edge"}),
- architectures=Is(None),
- ),
- )
-
- def test_requestBuilds_with_architectures(self):
- # If asked to build for particular architectures, requestBuilds
- # passes those through to the job.
- recipe = self.factory.makeRockRecipe()
- now = get_transaction_timestamp(IStore(recipe))
- with person_logged_in(recipe.owner.teamowner):
- request = recipe.requestBuilds(
- recipe.owner.teamowner, architectures={"amd64", "i386"}
- )
- self.assertThat(
- request,
- MatchesStructure(
- date_requested=Equals(now),
- date_finished=Is(None),
- recipe=Equals(recipe),
- status=Equals(RockRecipeBuildRequestStatus.PENDING),
- error_message=Is(None),
- channels=Is(None),
- architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
- ),
- )
- [job] = getUtility(IRockRecipeRequestBuildsJobSource).iterReady()
- self.assertThat(
- job,
- MatchesStructure(
- job_id=Equals(request.id),
- job=MatchesStructure.byEquality(status=JobStatus.WAITING),
- recipe=Equals(recipe),
- requester=Equals(recipe.owner.teamowner),
- channels=Is(None),
- architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
- ),
- )
-
- def test_delete_without_builds(self):
- # A rock recipe with no builds can be deleted.
- owner = self.factory.makePerson()
- project = self.factory.makeProduct()
- recipe = self.factory.makeRockRecipe(
- registrant=owner, owner=owner, project=project, name="condemned"
- )
- self.assertIsNotNone(
- getUtility(IRockRecipeSet).getByName(owner, project, "condemned")
- )
- with person_logged_in(recipe.owner):
- recipe.destroySelf()
- self.assertIsNone(
- getUtility(IRockRecipeSet).getByName(owner, project, "condemned")
- )
-
- def makeBuildableDistroArchSeries(
- self,
- architecturetag=None,
- processor=None,
- supports_virtualized=True,
- supports_nonvirtualized=True,
- **kwargs,
- ):
- if architecturetag is None:
- architecturetag = self.factory.getUniqueUnicode("arch")
- if processor is None:
- try:
- processor = getUtility(IProcessorSet).getByName(
- architecturetag
- )
- except ProcessorNotFound:
- processor = self.factory.makeProcessor(
- name=architecturetag,
- supports_virtualized=supports_virtualized,
- supports_nonvirtualized=supports_nonvirtualized,
- )
- das = self.factory.makeDistroArchSeries(
- architecturetag=architecturetag, processor=processor, **kwargs
- )
- fake_chroot = self.factory.makeLibraryFileAlias(
- filename="fake_chroot.tar.gz", db_only=True
- )
- das.addOrUpdateChroot(fake_chroot)
- return das
-
- def test_requestBuild(self):
- # requestBuild creates a new RockRecipeBuild.
- recipe = self.factory.makeRockRecipe()
- das = self.makeBuildableDistroArchSeries()
- build_request = self.factory.makeRockRecipeBuildRequest(recipe=recipe)
- build = recipe.requestBuild(build_request, das)
- self.assertTrue(IRockRecipeBuild.providedBy(build))
- self.assertThat(
- build,
- MatchesStructure(
- requester=Equals(recipe.owner.teamowner),
- distro_arch_series=Equals(das),
- channels=Is(None),
- status=Equals(BuildStatus.NEEDSBUILD),
- ),
- )
- store = Store.of(build)
- store.flush()
- build_queue = store.find(
- BuildQueue,
- BuildQueue._build_farm_job_id
- == removeSecurityProxy(build).build_farm_job_id,
- ).one()
- self.assertProvides(build_queue, IBuildQueue)
- self.assertEqual(recipe.require_virtualized, build_queue.virtualized)
- self.assertEqual(BuildQueueStatus.WAITING, build_queue.status)
-
- def test_requestBuild_score(self):
- # Build requests have a relatively low queue score (2510).
- recipe = self.factory.makeRockRecipe()
- das = self.makeBuildableDistroArchSeries()
- build_request = self.factory.makeRockRecipeBuildRequest(recipe=recipe)
- build = recipe.requestBuild(build_request, das)
- queue_record = build.buildqueue_record
- queue_record.score()
- self.assertEqual(2510, queue_record.lastscore)
-
- def test_requestBuild_channels(self):
- # requestBuild can select non-default channels.
- recipe = self.factory.makeRockRecipe()
- das = self.makeBuildableDistroArchSeries()
- build_request = self.factory.makeRockRecipeBuildRequest(recipe=recipe)
- build = recipe.requestBuild(
- build_request, das, channels={"rockcraft": "edge"}
- )
- self.assertEqual({"rockcraft": "edge"}, build.channels)
-
- def test_requestBuild_rejects_repeats(self):
- # requestBuild refuses if there is already a pending build.
- recipe = self.factory.makeRockRecipe()
- distro_series = self.factory.makeDistroSeries()
- arches = [
- self.makeBuildableDistroArchSeries(distroseries=distro_series)
- for _ in range(2)
- ]
- build_request = self.factory.makeRockRecipeBuildRequest(recipe=recipe)
- old_build = recipe.requestBuild(build_request, arches[0])
- self.assertRaises(
- RockRecipeBuildAlreadyPending,
- recipe.requestBuild,
- build_request,
- arches[0],
- )
- # We can build for a different distroarchseries.
- recipe.requestBuild(build_request, arches[1])
- # channels=None and channels={} are treated as equivalent, but
- # anything else allows a new build.
- self.assertRaises(
- RockRecipeBuildAlreadyPending,
- recipe.requestBuild,
- build_request,
- arches[0],
- channels={},
- )
- recipe.requestBuild(
- build_request, arches[0], channels={"core": "edge"}
- )
- self.assertRaises(
- RockRecipeBuildAlreadyPending,
- recipe.requestBuild,
- build_request,
- arches[0],
- channels={"core": "edge"},
- )
- # Changing the status of the old build allows a new build.
- old_build.updateStatus(BuildStatus.BUILDING)
- old_build.updateStatus(BuildStatus.FULLYBUILT)
- recipe.requestBuild(build_request, arches[0])
-
- def test_requestBuild_virtualization(self):
- # New builds are virtualized if any of the processor or recipe
- # require it.
- recipe = self.factory.makeRockRecipe()
- distro_series = self.factory.makeDistroSeries()
- dases = {}
- for proc_nonvirt in True, False:
- das = self.makeBuildableDistroArchSeries(
- distroseries=distro_series,
- supports_virtualized=True,
- supports_nonvirtualized=proc_nonvirt,
- )
- dases[proc_nonvirt] = das
- for proc_nonvirt, recipe_virt, build_virt in (
- (True, False, False),
- (True, True, True),
- (False, False, True),
- (False, True, True),
- ):
- das = dases[proc_nonvirt]
- recipe = self.factory.makeRockRecipe(
- require_virtualized=recipe_virt
- )
- build_request = self.factory.makeRockRecipeBuildRequest(
- recipe=recipe
- )
- build = recipe.requestBuild(build_request, das)
- self.assertEqual(build_virt, build.virtualized)
-
- def test_requestBuild_nonvirtualized(self):
- # A non-virtualized processor can build a rock recipe iff the
- # recipe has require_virtualized set to False.
- recipe = self.factory.makeRockRecipe()
- distro_series = self.factory.makeDistroSeries()
- das = self.makeBuildableDistroArchSeries(
- distroseries=distro_series,
- supports_virtualized=False,
- supports_nonvirtualized=True,
- )
- build_request = self.factory.makeRockRecipeBuildRequest(recipe=recipe)
- self.assertRaises(
- RockRecipeBuildDisallowedArchitecture,
- recipe.requestBuild,
- build_request,
- das,
- )
- with admin_logged_in():
- recipe.require_virtualized = False
- recipe.requestBuild(build_request, das)
-
-
-class TestRockRecipeSet(TestCaseWithFactory):
-
- layer = DatabaseFunctionalLayer
-
- def setUp(self):
- super().setUp()
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
-
- def test_class_implements_interfaces(self):
- # The RockRecipeSet class implements IRockRecipeSet.
- self.assertProvides(getUtility(IRockRecipeSet), IRockRecipeSet)
-
- def makeRockRecipeComponents(self, git_ref=None):
- """Return a dict of values that can be used to make a rock recipe.
-
- Suggested use: provide as kwargs to IRockRecipeSet.new.
-
- :param git_ref: An `IGitRef`, or None.
- """
- registrant = self.factory.makePerson()
- components = {
- "registrant": registrant,
- "owner": self.factory.makeTeam(owner=registrant),
- "project": self.factory.makeProduct(),
- "name": self.factory.getUniqueUnicode("rock-name"),
- }
- if git_ref is None:
- git_ref = self.factory.makeGitRefs()[0]
- components["git_ref"] = git_ref
- return components
-
- def test_creation_git(self):
- # The metadata entries supplied when a rock recipe is created for a
- # Git branch are present on the new object.
- [ref] = self.factory.makeGitRefs()
- components = self.makeRockRecipeComponents(git_ref=ref)
- recipe = getUtility(IRockRecipeSet).new(**components)
- self.assertEqual(components["registrant"], recipe.registrant)
- self.assertEqual(components["owner"], recipe.owner)
- self.assertEqual(components["project"], recipe.project)
- self.assertEqual(components["name"], recipe.name)
- self.assertEqual(ref.repository, recipe.git_repository)
- self.assertEqual(ref.path, recipe.git_path)
- self.assertEqual(ref, recipe.git_ref)
- self.assertIsNone(recipe.build_path)
- self.assertFalse(recipe.auto_build)
- self.assertIsNone(recipe.auto_build_channels)
- self.assertTrue(recipe.require_virtualized)
- self.assertFalse(recipe.private)
- self.assertFalse(recipe.store_upload)
- self.assertIsNone(recipe.store_name)
- self.assertIsNone(recipe.store_secrets)
- self.assertEqual([], recipe.store_channels)
-
- def test_creation_no_source(self):
- # Attempting to create a rock recipe without a Git repository
- # fails.
- registrant = self.factory.makePerson()
- self.assertRaises(
- NoSourceForRockRecipe,
- getUtility(IRockRecipeSet).new,
- registrant,
- registrant,
- self.factory.makeProduct(),
- self.factory.getUniqueUnicode("rock-name"),
- )
-
- def test_getByName(self):
- owner = self.factory.makePerson()
- project = self.factory.makeProduct()
- project_recipe = self.factory.makeRockRecipe(
- registrant=owner, owner=owner, project=project, name="proj-rock"
- )
- self.factory.makeRockRecipe(
- registrant=owner, owner=owner, name="proj-rock"
- )
-
- self.assertEqual(
- project_recipe,
- getUtility(IRockRecipeSet).getByName(owner, project, "proj-rock"),
- )
-
- def test_findByGitRepository(self):
- # IRockRecipeSet.findByGitRepository returns all rock recipes with
- # the given Git repository.
- repositories = [self.factory.makeGitRepository() for i in range(2)]
- recipes = []
- for repository in repositories:
- for _ in range(2):
- [ref] = self.factory.makeGitRefs(repository=repository)
- recipes.append(self.factory.makeRockRecipe(git_ref=ref))
- recipe_set = getUtility(IRockRecipeSet)
- self.assertContentEqual(
- recipes[:2], recipe_set.findByGitRepository(repositories[0])
- )
- self.assertContentEqual(
- recipes[2:], recipe_set.findByGitRepository(repositories[1])
- )
-
- def test_findByGitRepository_paths(self):
- # IRockRecipeSet.findByGitRepository can restrict by reference
- # paths.
- repositories = [self.factory.makeGitRepository() for i in range(2)]
- recipes = []
- for repository in repositories:
- for _ in range(3):
- [ref] = self.factory.makeGitRefs(repository=repository)
- recipes.append(self.factory.makeRockRecipe(git_ref=ref))
- recipe_set = getUtility(IRockRecipeSet)
- self.assertContentEqual(
- [], recipe_set.findByGitRepository(repositories[0], paths=[])
- )
- self.assertContentEqual(
- [recipes[0]],
- recipe_set.findByGitRepository(
- repositories[0], paths=[recipes[0].git_ref.path]
- ),
- )
- self.assertContentEqual(
- recipes[:2],
- recipe_set.findByGitRepository(
- repositories[0],
- paths=[recipes[0].git_ref.path, recipes[1].git_ref.path],
- ),
- )
-
- def test_findByOwner(self):
- # IRockRecipeSet.findByOwner returns all rock recipes with the
- # given owner.
- owners = [self.factory.makePerson() for i in range(2)]
- recipes = []
- for owner in owners:
- for _ in range(2):
- recipes.append(
- self.factory.makeRockRecipe(registrant=owner, owner=owner)
- )
- recipe_set = getUtility(IRockRecipeSet)
- self.assertContentEqual(recipes[:2], recipe_set.findByOwner(owners[0]))
- self.assertContentEqual(recipes[2:], recipe_set.findByOwner(owners[1]))
-
- def test_detachFromGitRepository(self):
- # IRockRecipeSet.detachFromGitRepository clears the given Git
- # repository from all rock recipes.
- repositories = [self.factory.makeGitRepository() for i in range(2)]
- recipes = []
- paths = []
- refs = []
- for repository in repositories:
- for _ in range(2):
- [ref] = self.factory.makeGitRefs(repository=repository)
- paths.append(ref.path)
- refs.append(ref)
- recipes.append(
- self.factory.makeRockRecipe(
- git_ref=ref, date_created=ONE_DAY_AGO
- )
- )
- getUtility(IRockRecipeSet).detachFromGitRepository(repositories[0])
- self.assertEqual(
- [None, None, repositories[1], repositories[1]],
- [recipe.git_repository for recipe in recipes],
- )
- self.assertEqual(
- [None, None, paths[2], paths[3]],
- [recipe.git_path for recipe in recipes],
- )
- self.assertEqual(
- [None, None, refs[2], refs[3]],
- [recipe.git_ref for recipe in recipes],
- )
- for recipe in recipes[:2]:
- self.assertSqlAttributeEqualsDate(
- recipe, "date_last_modified", UTC_NOW
- )
diff --git a/lib/lp/rocks/tests/test_rockrecipebuild.py b/lib/lp/rocks/tests/test_rockrecipebuild.py
deleted file mode 100644
index a9d6ffe..0000000
--- a/lib/lp/rocks/tests/test_rockrecipebuild.py
+++ /dev/null
@@ -1,449 +0,0 @@
-# Copyright 2015-2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Test rock package build features."""
-from datetime import datetime, timedelta, timezone
-
-import six
-from testtools.matchers import Equals
-from zope.component import getUtility
-from zope.security.proxy import removeSecurityProxy
-
-from lp.app.enums import InformationType
-from lp.app.errors import NotFoundError
-from lp.buildmaster.enums import BuildStatus
-from lp.buildmaster.interfaces.buildqueue import IBuildQueue
-from lp.buildmaster.interfaces.packagebuild import IPackageBuild
-from lp.buildmaster.interfaces.processor import IProcessorSet
-from lp.registry.enums import PersonVisibility, TeamMembershipPolicy
-from lp.registry.interfaces.series import SeriesStatus
-from lp.rocks.interfaces.rockrecipe import (
- ROCK_RECIPE_ALLOW_CREATE,
- ROCK_RECIPE_PRIVATE_FEATURE_FLAG,
-)
-from lp.rocks.interfaces.rockrecipebuild import (
- IRockRecipeBuild,
- IRockRecipeBuildSet,
-)
-from lp.services.config import config
-from lp.services.features.testing import FeatureFixture
-from lp.services.propertycache import clear_property_cache
-from lp.testing import (
- StormStatementRecorder,
- TestCaseWithFactory,
- person_logged_in,
-)
-from lp.testing.layers import LaunchpadZopelessLayer
-from lp.testing.mail_helpers import pop_notifications
-from lp.testing.matchers import HasQueryCount
-
-expected_body = """\
- * Rock Recipe: rock-1
- * Project: rock-project
- * Distroseries: distro unstable
- * Architecture: i386
- * State: Failed to build
- * Duration: 10 minutes
- * Build Log: %s
- * Upload Log: %s
- * Builder: http://launchpad.test/builders/bob
-"""
-
-
-class TestRockRecipeBuild(TestCaseWithFactory):
-
- layer = LaunchpadZopelessLayer
-
- def setUp(self):
- super().setUp()
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- self.build = self.factory.makeRockRecipeBuild()
-
- def test_implements_interfaces(self):
- # RockRecipeBuild implements IPackageBuild and IRockRecipeBuild.
- self.assertProvides(self.build, IPackageBuild)
- self.assertProvides(self.build, IRockRecipeBuild)
-
- def test___repr__(self):
- # RockRecipeBuild has an informative __repr__.
- self.assertEqual(
- "<RockRecipeBuild ~%s/%s/+rock/%s/+build/%s>"
- % (
- self.build.recipe.owner.name,
- self.build.recipe.project.name,
- self.build.recipe.name,
- self.build.id,
- ),
- repr(self.build),
- )
-
- def test_title(self):
- # RockRecipeBuild has an informative title.
- das = self.build.distro_arch_series
- self.assertEqual(
- "%s build of /~%s/%s/+rock/%s"
- % (
- das.architecturetag,
- self.build.recipe.owner.name,
- self.build.recipe.project.name,
- self.build.recipe.name,
- ),
- self.build.title,
- )
-
- def test_queueBuild(self):
- # RockRecipeBuild can create the queue entry for itself.
- bq = self.build.queueBuild()
- self.assertProvides(bq, IBuildQueue)
- self.assertEqual(
- self.build.build_farm_job, removeSecurityProxy(bq)._build_farm_job
- )
- self.assertEqual(self.build, bq.specific_build)
- self.assertEqual(self.build.virtualized, bq.virtualized)
- self.assertIsNotNone(bq.processor)
- self.assertEqual(bq, self.build.buildqueue_record)
-
- def test_is_private(self):
- # A RockRecipeBuild is private iff its recipe or owner are.
- self.assertFalse(self.build.is_private)
- self.useFixture(
- FeatureFixture(
- {
- ROCK_RECIPE_ALLOW_CREATE: "on",
- ROCK_RECIPE_PRIVATE_FEATURE_FLAG: "on",
- }
- )
- )
- private_team = self.factory.makeTeam(
- membership_policy=TeamMembershipPolicy.MODERATED,
- visibility=PersonVisibility.PRIVATE,
- )
- with person_logged_in(private_team.teamowner):
- build = self.factory.makeRockRecipeBuild(
- requester=private_team.teamowner,
- owner=private_team,
- information_type=InformationType.PROPRIETARY,
- )
- self.assertTrue(build.is_private)
-
- def test_can_be_retried(self):
- ok_cases = [
- BuildStatus.FAILEDTOBUILD,
- BuildStatus.MANUALDEPWAIT,
- BuildStatus.CHROOTWAIT,
- BuildStatus.FAILEDTOUPLOAD,
- BuildStatus.CANCELLED,
- BuildStatus.SUPERSEDED,
- ]
- for status in BuildStatus.items:
- build = self.factory.makeRockRecipeBuild(status=status)
- if status in ok_cases:
- self.assertTrue(build.can_be_retried)
- else:
- self.assertFalse(build.can_be_retried)
-
- def test_can_be_retried_obsolete_series(self):
- # Builds for obsolete series cannot be retried.
- distroseries = self.factory.makeDistroSeries(
- status=SeriesStatus.OBSOLETE
- )
- das = self.factory.makeDistroArchSeries(distroseries=distroseries)
- build = self.factory.makeRockRecipeBuild(distro_arch_series=das)
- self.assertFalse(build.can_be_retried)
-
- def test_can_be_cancelled(self):
- # For all states that can be cancelled, can_be_cancelled returns True.
- ok_cases = [
- BuildStatus.BUILDING,
- BuildStatus.NEEDSBUILD,
- ]
- for status in BuildStatus.items:
- build = self.factory.makeRockRecipeBuild()
- build.queueBuild()
- build.updateStatus(status)
- if status in ok_cases:
- self.assertTrue(build.can_be_cancelled)
- else:
- self.assertFalse(build.can_be_cancelled)
-
- def test_retry_resets_state(self):
- # Retrying a build resets most of the state attributes, but does
- # not modify the first dispatch time.
- now = datetime.now(timezone.utc)
- build = self.factory.makeRockRecipeBuild()
- build.updateStatus(BuildStatus.BUILDING, date_started=now)
- build.updateStatus(BuildStatus.FAILEDTOBUILD)
- build.gotFailure()
- with person_logged_in(build.recipe.owner):
- build.retry()
- self.assertEqual(BuildStatus.NEEDSBUILD, build.status)
- self.assertEqual(now, build.date_first_dispatched)
- self.assertIsNone(build.log)
- self.assertIsNone(build.upload_log)
- self.assertEqual(0, build.failure_count)
-
- def test_cancel_not_in_progress(self):
- # The cancel() method for a pending build leaves it in the CANCELLED
- # state.
- self.build.queueBuild()
- self.build.cancel()
- self.assertEqual(BuildStatus.CANCELLED, self.build.status)
- self.assertIsNone(self.build.buildqueue_record)
-
- def test_cancel_in_progress(self):
- # The cancel() method for a building build leaves it in the
- # CANCELLING state.
- bq = self.build.queueBuild()
- bq.markAsBuilding(self.factory.makeBuilder())
- self.build.cancel()
- self.assertEqual(BuildStatus.CANCELLING, self.build.status)
- self.assertEqual(bq, self.build.buildqueue_record)
-
- def test_estimateDuration(self):
- # Without previous builds, the default time estimate is 10m.
- self.assertEqual(600, self.build.estimateDuration().seconds)
-
- def test_estimateDuration_with_history(self):
- # Previous successful builds of the same recipe are used for
- # estimates.
- self.factory.makeRockRecipeBuild(
- requester=self.build.requester,
- recipe=self.build.recipe,
- distro_arch_series=self.build.distro_arch_series,
- status=BuildStatus.FULLYBUILT,
- duration=timedelta(seconds=335),
- )
- for _ in range(3):
- self.factory.makeRockRecipeBuild(
- requester=self.build.requester,
- recipe=self.build.recipe,
- distro_arch_series=self.build.distro_arch_series,
- status=BuildStatus.FAILEDTOBUILD,
- duration=timedelta(seconds=20),
- )
- self.assertEqual(335, self.build.estimateDuration().seconds)
-
- def test_build_cookie(self):
- build = self.factory.makeRockRecipeBuild()
- self.assertEqual("ROCKRECIPEBUILD-%d" % build.id, build.build_cookie)
-
- def test_getFileByName_logs(self):
- # getFileByName returns the logs when requested by name.
- self.build.setLog(
- self.factory.makeLibraryFileAlias(filename="buildlog.txt.gz")
- )
- self.assertEqual(
- self.build.log, self.build.getFileByName("buildlog.txt.gz")
- )
- self.assertRaises(NotFoundError, self.build.getFileByName, "foo")
- self.build.storeUploadLog("uploaded")
- self.assertEqual(
- self.build.upload_log,
- self.build.getFileByName(self.build.upload_log.filename),
- )
-
- def test_getFileByName_uploaded_files(self):
- # getFileByName returns uploaded files when requested by name.
- filenames = ("ubuntu.squashfs", "ubuntu.manifest")
- lfas = []
- for filename in filenames:
- lfa = self.factory.makeLibraryFileAlias(filename=filename)
- lfas.append(lfa)
- self.build.addFile(lfa)
- self.assertContentEqual(
- lfas, [row[1] for row in self.build.getFiles()]
- )
- for filename, lfa in zip(filenames, lfas):
- self.assertEqual(lfa, self.build.getFileByName(filename))
- self.assertRaises(NotFoundError, self.build.getFileByName, "missing")
-
- def test_verifySuccessfulUpload(self):
- self.assertFalse(self.build.verifySuccessfulUpload())
- self.factory.makeRockFile(build=self.build)
- self.assertTrue(self.build.verifySuccessfulUpload())
-
- def test_updateStatus_stores_revision_id(self):
- # If the builder reports a revision_id, updateStatus saves it.
- self.assertIsNone(self.build.revision_id)
- self.build.updateStatus(BuildStatus.BUILDING, worker_status={})
- self.assertIsNone(self.build.revision_id)
- self.build.updateStatus(
- BuildStatus.BUILDING, worker_status={"revision_id": "dummy"}
- )
- self.assertEqual("dummy", self.build.revision_id)
-
- def test_notify_fullybuilt(self):
- # notify does not send mail when a recipe build completes normally.
- build = self.factory.makeRockRecipeBuild(status=BuildStatus.FULLYBUILT)
- build.notify()
- self.assertEqual(0, len(pop_notifications()))
-
- def test_notify_packagefail(self):
- # notify sends mail when a recipe build fails.
- person = self.factory.makePerson(name="person")
- project = self.factory.makeProduct(name="rock-project")
- distribution = self.factory.makeDistribution(name="distro")
- distroseries = self.factory.makeDistroSeries(
- distribution=distribution, name="unstable"
- )
- processor = getUtility(IProcessorSet).getByName("386")
- das = self.factory.makeDistroArchSeries(
- distroseries=distroseries,
- architecturetag="i386",
- processor=processor,
- )
- build = self.factory.makeRockRecipeBuild(
- name="rock-1",
- requester=person,
- owner=person,
- project=project,
- distro_arch_series=das,
- status=BuildStatus.FAILEDTOBUILD,
- builder=self.factory.makeBuilder(name="bob"),
- duration=timedelta(minutes=10),
- )
- build.setLog(self.factory.makeLibraryFileAlias())
- build.notify()
- [notification] = pop_notifications()
- self.assertEqual(
- config.canonical.noreply_from_address, notification["From"]
- )
- self.assertEqual(
- "Person <%s>" % person.preferredemail.email, notification["To"]
- )
- subject = notification["Subject"].replace("\n ", " ")
- self.assertEqual(
- "[Rock recipe build #%d] i386 build of "
- "/~person/rock-project/+rock/rock-1" % build.id,
- subject,
- )
- self.assertEqual(
- "Requester", notification["X-Launchpad-Message-Rationale"]
- )
- self.assertEqual(person.name, notification["X-Launchpad-Message-For"])
- self.assertEqual(
- "rock-recipe-build-status",
- notification["X-Launchpad-Notification-Type"],
- )
- self.assertEqual(
- "FAILEDTOBUILD", notification["X-Launchpad-Build-State"]
- )
- body, footer = six.ensure_text(
- notification.get_payload(decode=True)
- ).split("\n-- \n")
- self.assertEqual(expected_body % (build.log_url, ""), body)
- self.assertEqual(
- "http://launchpad.test/~person/rock-project/+rock/rock-1/"
- "+build/%d\n"
- "You are the requester of the build.\n" % build.id,
- footer,
- )
-
- def addFakeBuildLog(self, build):
- build.setLog(self.factory.makeLibraryFileAlias("mybuildlog.txt"))
-
- def test_log_url_123(self):
- # The log URL for a rock recipe build will use the recipe context.
- self.addFakeBuildLog(self.build)
- self.build.log_url
- self.assertEqual(
- "http://launchpad.test/~%s/%s/+rock/%s/+build/%d/+files/"
- "mybuildlog.txt"
- % (
- self.build.recipe.owner.name,
- self.build.recipe.project.name,
- self.build.recipe.name,
- self.build.id,
- ),
- self.build.log_url,
- )
-
- def test_eta(self):
- # RockRecipeBuild.eta returns a non-None value when it should, or
- # None when there's no start time.
- self.build.queueBuild()
- self.assertIsNone(self.build.eta)
- self.factory.makeBuilder(processors=[self.build.processor])
- clear_property_cache(self.build)
- self.assertIsNotNone(self.build.eta)
-
- def test_eta_cached(self):
- # The expensive completion time estimate is cached.
- self.build.queueBuild()
- self.build.eta
- with StormStatementRecorder() as recorder:
- self.build.eta
- self.assertThat(recorder, HasQueryCount(Equals(0)))
-
- def test_estimate(self):
- # RockRecipeBuild.estimate returns True until the job is completed.
- self.build.queueBuild()
- self.factory.makeBuilder(processors=[self.build.processor])
- self.build.updateStatus(BuildStatus.BUILDING)
- self.assertTrue(self.build.estimate)
- self.build.updateStatus(BuildStatus.FULLYBUILT)
- clear_property_cache(self.build)
- self.assertFalse(self.build.estimate)
-
-
-class TestRockRecipeBuildSet(TestCaseWithFactory):
-
- layer = LaunchpadZopelessLayer
-
- def setUp(self):
- super().setUp()
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
-
- def test_getByBuildFarmJob_works(self):
- build = self.factory.makeRockRecipeBuild()
- self.assertEqual(
- build,
- getUtility(IRockRecipeBuildSet).getByBuildFarmJob(
- build.build_farm_job
- ),
- )
-
- def test_getByBuildFarmJob_returns_None_when_missing(self):
- bpb = self.factory.makeBinaryPackageBuild()
- self.assertIsNone(
- getUtility(IRockRecipeBuildSet).getByBuildFarmJob(
- bpb.build_farm_job
- )
- )
-
- def test_getByBuildFarmJobs_works(self):
- builds = [self.factory.makeRockRecipeBuild() for i in range(10)]
- self.assertContentEqual(
- builds,
- getUtility(IRockRecipeBuildSet).getByBuildFarmJobs(
- [build.build_farm_job for build in builds]
- ),
- )
-
- def test_getByBuildFarmJobs_works_empty(self):
- self.assertContentEqual(
- [], getUtility(IRockRecipeBuildSet).getByBuildFarmJobs([])
- )
-
- def test_virtualized_recipe_requires(self):
- recipe = self.factory.makeRockRecipe(require_virtualized=True)
- target = self.factory.makeRockRecipeBuild(recipe=recipe)
- self.assertTrue(target.virtualized)
-
- def test_virtualized_processor_requires(self):
- recipe = self.factory.makeRockRecipe(require_virtualized=False)
- distro_arch_series = self.factory.makeDistroArchSeries()
- distro_arch_series.processor.supports_nonvirtualized = False
- target = self.factory.makeRockRecipeBuild(
- distro_arch_series=distro_arch_series, recipe=recipe
- )
- self.assertTrue(target.virtualized)
-
- def test_virtualized_no_support(self):
- recipe = self.factory.makeRockRecipe(require_virtualized=False)
- distro_arch_series = self.factory.makeDistroArchSeries()
- distro_arch_series.processor.supports_nonvirtualized = True
- target = self.factory.makeRockRecipeBuild(
- recipe=recipe, distro_arch_series=distro_arch_series
- )
- self.assertFalse(target.virtualized)
diff --git a/lib/lp/rocks/tests/test_rockrecipebuildbehaviour.py b/lib/lp/rocks/tests/test_rockrecipebuildbehaviour.py
deleted file mode 100644
index ede523b..0000000
--- a/lib/lp/rocks/tests/test_rockrecipebuildbehaviour.py
+++ /dev/null
@@ -1,588 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Test rock recipe build behaviour."""
-
-import base64
-import os.path
-import time
-import uuid
-from datetime import datetime
-from urllib.parse import urlsplit
-
-from fixtures import MockPatch
-from pymacaroons import Macaroon
-from testtools import ExpectedException
-from testtools.matchers import (
- ContainsDict,
- Equals,
- Is,
- IsInstance,
- MatchesDict,
- MatchesListwise,
- StartsWith,
-)
-from testtools.twistedsupport import (
- AsynchronousDeferredRunTestForBrokenTwisted,
-)
-from twisted.internet import defer
-from zope.component import getUtility
-from zope.proxy import isProxy
-from zope.security.proxy import removeSecurityProxy
-
-from lp.app.enums import InformationType
-from lp.archivepublisher.interfaces.archivegpgsigningkey import (
- IArchiveGPGSigningKey,
-)
-from lp.buildmaster.enums import BuildBaseImageType, BuildStatus
-from lp.buildmaster.interactor import shut_down_default_process_pool
-from lp.buildmaster.interfaces.builder import CannotBuild
-from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
- IBuildFarmJobBehaviour,
-)
-from lp.buildmaster.interfaces.processor import IProcessorSet
-from lp.buildmaster.tests.builderproxy import (
- InProcessProxyAuthAPIFixture,
- ProxyURLMatcher,
- RevocationEndpointMatcher,
-)
-from lp.buildmaster.tests.mock_workers import (
- MockBuilder,
- OkWorker,
- WorkerTestHelpers,
-)
-from lp.buildmaster.tests.test_buildfarmjobbehaviour import (
- TestGetUploadMethodsMixin,
- TestHandleStatusMixin,
- TestVerifySuccessfulBuildMixin,
-)
-from lp.registry.interfaces.series import SeriesStatus
-from lp.rocks.interfaces.rockrecipe import (
- ROCK_RECIPE_ALLOW_CREATE,
- ROCK_RECIPE_PRIVATE_FEATURE_FLAG,
-)
-from lp.rocks.model.rockrecipebuildbehaviour import RockRecipeBuildBehaviour
-from lp.services.config import config
-from lp.services.features.testing import FeatureFixture
-from lp.services.log.logger import BufferLogger, DevNullLogger
-from lp.services.statsd.tests import StatsMixin
-from lp.services.webapp import canonical_url
-from lp.soyuz.adapters.archivedependencies import get_sources_list_for_building
-from lp.soyuz.enums import PackagePublishingStatus
-from lp.soyuz.tests.soyuz import Base64KeyMatches
-from lp.testing import TestCaseWithFactory
-from lp.testing.dbuser import dbuser
-from lp.testing.gpgkeys import gpgkeysdir
-from lp.testing.keyserver import InProcessKeyServerFixture
-from lp.testing.layers import LaunchpadZopelessLayer
-
-
-class TestRockRecipeBuildBehaviourBase(TestCaseWithFactory):
- layer = LaunchpadZopelessLayer
-
- def setUp(self):
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- super().setUp()
-
- def makeJob(self, distribution=None, with_builder=False, **kwargs):
- """Create a sample `IRockRecipeBuildBehaviour`."""
- if distribution is None:
- distribution = self.factory.makeDistribution(name="distro")
- distroseries = self.factory.makeDistroSeries(
- distribution=distribution, name="unstable"
- )
- processor = getUtility(IProcessorSet).getByName("386")
- distroarchseries = self.factory.makeDistroArchSeries(
- distroseries=distroseries,
- architecturetag="i386",
- processor=processor,
- )
-
- # Taken from test_archivedependencies.py
- for component_name in ("main", "universe"):
- self.factory.makeComponentSelection(distroseries, component_name)
-
- build = self.factory.makeRockRecipeBuild(
- distro_arch_series=distroarchseries, name="test-rock", **kwargs
- )
- return IBuildFarmJobBehaviour(build)
-
-
-class TestRockRecipeBuildBehaviour(TestRockRecipeBuildBehaviourBase):
- layer = LaunchpadZopelessLayer
-
- def test_provides_interface(self):
- # RockRecipeBuildBehaviour provides IBuildFarmJobBehaviour.
- job = RockRecipeBuildBehaviour(None)
- self.assertProvides(job, IBuildFarmJobBehaviour)
-
- def test_adapts_IRockRecipeBuild(self):
- # IBuildFarmJobBehaviour adapts an IRockRecipeBuild.
- build = self.factory.makeRockRecipeBuild()
- job = IBuildFarmJobBehaviour(build)
- self.assertProvides(job, IBuildFarmJobBehaviour)
-
- def test_verifyBuildRequest_valid(self):
- # verifyBuildRequest doesn't raise any exceptions when called with a
- # valid builder set.
- job = self.makeJob()
- lfa = self.factory.makeLibraryFileAlias()
- job.build.distro_arch_series.addOrUpdateChroot(lfa)
- builder = MockBuilder()
- job.setBuilder(builder, OkWorker())
- logger = BufferLogger()
- job.verifyBuildRequest(logger)
- self.assertEqual("", logger.getLogBuffer())
-
- def test_verifyBuildRequest_virtual_mismatch(self):
- # verifyBuildRequest raises on an attempt to build a virtualized
- # build on a non-virtual builder.
- job = self.makeJob()
- lfa = self.factory.makeLibraryFileAlias()
- job.build.distro_arch_series.addOrUpdateChroot(lfa)
- builder = MockBuilder(virtualized=False)
- job.setBuilder(builder, OkWorker())
- logger = BufferLogger()
- e = self.assertRaises(AssertionError, job.verifyBuildRequest, logger)
- self.assertEqual(
- "Attempt to build virtual item on a non-virtual builder.", str(e)
- )
-
- def test_verifyBuildRequest_no_chroot(self):
- # verifyBuildRequest raises when the DAS has no chroot.
- job = self.makeJob()
- builder = MockBuilder()
- job.setBuilder(builder, OkWorker())
- logger = BufferLogger()
- e = self.assertRaises(CannotBuild, job.verifyBuildRequest, logger)
- self.assertIn("Missing chroot", str(e))
-
-
-class TestAsyncRockRecipeBuildBehaviour(
- StatsMixin, TestRockRecipeBuildBehaviourBase
-):
-
- run_tests_with = AsynchronousDeferredRunTestForBrokenTwisted.make_factory(
- timeout=30
- )
-
- @defer.inlineCallbacks
- def setUp(self):
- super().setUp()
- build_username = "OCIBUILD-1"
- self.token = {
- "secret": uuid.uuid4().hex,
- "username": build_username,
- "timestamp": datetime.utcnow().isoformat(),
- }
- self.proxy_url = (
- "http://{username}:{password}"
- "@{host}:{port}".format(
- username=self.token["username"],
- password=self.token["secret"],
- host=config.builddmaster.builder_proxy_host,
- port=config.builddmaster.builder_proxy_port,
- )
- )
- self.proxy_api = self.useFixture(InProcessProxyAuthAPIFixture())
- yield self.proxy_api.start()
- self.now = time.time()
- self.useFixture(MockPatch("time.time", return_value=self.now))
- self.addCleanup(shut_down_default_process_pool)
- self.setUpStats()
-
- def makeJob(self, **kwargs):
- # We need a builder in these tests, in order that requesting a proxy
- # token can piggyback on its reactor and pool.
- job = super().makeJob(**kwargs)
- builder = MockBuilder()
- builder.processor = job.build.processor
- worker = self.useFixture(WorkerTestHelpers()).getClientWorker()
- job.setBuilder(builder, worker)
- self.addCleanup(worker.pool.closeCachedConnections)
- return job
-
- @defer.inlineCallbacks
- def test_composeBuildRequest(self):
- job = self.makeJob()
- lfa = self.factory.makeLibraryFileAlias(db_only=True)
- job.build.distro_arch_series.addOrUpdateChroot(lfa)
- build_request = yield job.composeBuildRequest(None)
- self.assertThat(
- build_request,
- MatchesListwise(
- [
- Equals("rock"),
- Equals(job.build.distro_arch_series),
- Equals(job.build.pocket),
- Equals({}),
- IsInstance(dict),
- ]
- ),
- )
-
- @defer.inlineCallbacks
- def test_requestProxyToken_unconfigured(self):
- self.pushConfig("builddmaster", builder_proxy_host=None)
- job = self.makeJob()
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertEqual([], self.proxy_api.tokens.requests)
- self.assertNotIn("proxy_url", args)
- self.assertNotIn("revocation_endpoint", args)
-
- @defer.inlineCallbacks
- def test_requestProxyToken_no_secret(self):
- self.pushConfig(
- "builddmaster", builder_proxy_auth_api_admin_secret=None
- )
- job = self.makeJob()
- expected_exception_msg = (
- "builder_proxy_auth_api_admin_secret is not configured."
- )
- with ExpectedException(CannotBuild, expected_exception_msg):
- yield job.extraBuildArgs()
-
- @defer.inlineCallbacks
- def test_requestProxyToken(self):
- job = self.makeJob()
- yield job.extraBuildArgs()
- expected_uri = urlsplit(
- config.builddmaster.builder_proxy_auth_api_endpoint
- ).path.encode("UTF-8")
- request_matcher = MatchesDict(
- {
- "method": Equals(b"POST"),
- "uri": Equals(expected_uri),
- "headers": ContainsDict(
- {
- b"Authorization": MatchesListwise(
- [
- Equals(
- b"Basic "
- + base64.b64encode(
- b"admin-launchpad.test:admin-secret"
- )
- )
- ]
- ),
- b"Content-Type": MatchesListwise(
- [Equals(b"application/json")]
- ),
- }
- ),
- "json": MatchesDict(
- {"username": StartsWith(job.build.build_cookie + "-")}
- ),
- }
- )
- self.assertThat(
- self.proxy_api.tokens.requests,
- MatchesListwise([request_matcher]),
- )
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_git(self):
- # extraBuildArgs returns appropriate arguments if asked to build a
- # job for a Git branch.
- [ref] = self.factory.makeGitRefs()
- job = self.makeJob(git_ref=ref, with_builder=True)
- expected_archives, expected_trusted_keys = (
- yield get_sources_list_for_building(
- job, job.build.distro_arch_series, None
- )
- )
- for archive_line in expected_archives:
- self.assertIn("universe", archive_line)
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertThat(
- args,
- MatchesDict(
- {
- "archive_private": Is(False),
- "archives": Equals(expected_archives),
- "arch_tag": Equals("i386"),
- "build_url": Equals(canonical_url(job.build)),
- "builder_constraints": Equals([]),
- "channels": Equals({}),
- "fast_cleanup": Is(True),
- "git_repository": Equals(ref.repository.git_https_url),
- "git_path": Equals(ref.name),
- "name": Equals("test-rock"),
- "private": Is(False),
- "proxy_url": ProxyURLMatcher(job, self.now),
- "revocation_endpoint": RevocationEndpointMatcher(
- job, self.now
- ),
- "series": Equals("unstable"),
- "trusted_keys": Equals(expected_trusted_keys),
- "use_fetch_service": Is(False),
- }
- ),
- )
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_git_HEAD(self):
- # extraBuildArgs returns appropriate arguments if asked to build a
- # job for the default branch in a Launchpad-hosted Git repository.
- [ref] = self.factory.makeGitRefs()
- removeSecurityProxy(ref.repository)._default_branch = ref.path
- job = self.makeJob(
- git_ref=ref.repository.getRefByPath("HEAD"), with_builder=True
- )
- expected_archives, expected_trusted_keys = (
- yield get_sources_list_for_building(
- job, job.build.distro_arch_series, None
- )
- )
- for archive_line in expected_archives:
- self.assertIn("universe", archive_line)
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertThat(
- args,
- MatchesDict(
- {
- "archive_private": Is(False),
- "archives": Equals(expected_archives),
- "arch_tag": Equals("i386"),
- "build_url": Equals(canonical_url(job.build)),
- "builder_constraints": Equals([]),
- "channels": Equals({}),
- "fast_cleanup": Is(True),
- "git_repository": Equals(ref.repository.git_https_url),
- "name": Equals("test-rock"),
- "private": Is(False),
- "proxy_url": ProxyURLMatcher(job, self.now),
- "revocation_endpoint": RevocationEndpointMatcher(
- job, self.now
- ),
- "series": Equals("unstable"),
- "trusted_keys": Equals(expected_trusted_keys),
- "use_fetch_service": Is(False),
- }
- ),
- )
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_prefers_store_name(self):
- # For the "name" argument, extraBuildArgs prefers
- # RockRecipe.store_name over RockRecipe.name if the former is set.
- job = self.makeJob(store_name="something-else")
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertEqual("something-else", args["name"])
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_archive_trusted_keys(self):
- # If the archive has a signing key, extraBuildArgs sends it.
- yield self.useFixture(InProcessKeyServerFixture()).start()
- distribution = self.factory.makeDistribution()
- key_path = os.path.join(gpgkeysdir, "ppa-sample@xxxxxxxxxxxxxxxxx")
- yield IArchiveGPGSigningKey(distribution.main_archive).setSigningKey(
- key_path, async_keyserver=True
- )
- job = self.makeJob(distribution=distribution)
- self.factory.makeBinaryPackagePublishingHistory(
- distroarchseries=job.build.distro_arch_series,
- pocket=job.build.pocket,
- archive=distribution.main_archive,
- status=PackagePublishingStatus.PUBLISHED,
- )
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertThat(
- args["trusted_keys"],
- MatchesListwise(
- [
- Base64KeyMatches(
- "0D57E99656BEFB0897606EE9A022DD1F5001B46D"
- ),
- ]
- ),
- )
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_build_path(self):
- # If the recipe specifies a build path, extraBuildArgs sends it.
- job = self.makeJob(build_path="src", with_builder=True)
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertEqual("src", args["build_path"])
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_channels(self):
- # If the build needs particular channels, extraBuildArgs sends them.
- job = self.makeJob(channels={"rockcraft": "edge"}, with_builder=True)
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertFalse(isProxy(args["channels"]))
- self.assertEqual({"rockcraft": "edge"}, args["channels"])
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_archives_primary(self):
- # The build uses the release, security, and updates pockets from the
- # primary archive.
- job = self.makeJob(with_builder=True)
- expected_archives = [
- "deb %s %s main universe"
- % (job.archive.archive_url, job.build.distro_series.name),
- "deb %s %s-security main universe"
- % (job.archive.archive_url, job.build.distro_series.name),
- "deb %s %s-updates main universe"
- % (job.archive.archive_url, job.build.distro_series.name),
- ]
- with dbuser(config.builddmaster.dbuser):
- extra_args = yield job.extraBuildArgs()
- self.assertEqual(expected_archives, extra_args["archives"])
-
- @defer.inlineCallbacks
- def test_extraBuildArgs_private(self):
- # If the recipe is private, extraBuildArgs sends the appropriate
- # arguments.
- self.useFixture(
- FeatureFixture(
- {
- ROCK_RECIPE_ALLOW_CREATE: "on",
- ROCK_RECIPE_PRIVATE_FEATURE_FLAG: "on",
- }
- )
- )
- job = self.makeJob(information_type=InformationType.PROPRIETARY)
- with dbuser(config.builddmaster.dbuser):
- args = yield job.extraBuildArgs()
- self.assertTrue(args["private"])
-
- @defer.inlineCallbacks
- def test_composeBuildRequest_proxy_url_set(self):
- job = self.makeJob()
- build_request = yield job.composeBuildRequest(None)
- self.assertThat(
- build_request[4]["proxy_url"], ProxyURLMatcher(job, self.now)
- )
- self.assertFalse(build_request[4]["use_fetch_service"])
-
- @defer.inlineCallbacks
- def test_composeBuildRequest_git_ref_deleted(self):
- # If the source Git reference has been deleted, composeBuildRequest
- # raises CannotBuild.
- repository = self.factory.makeGitRepository()
- [ref] = self.factory.makeGitRefs(repository=repository)
- owner = self.factory.makePerson(name="rock-owner")
- project = self.factory.makeProduct(name="rock-project")
- job = self.makeJob(
- registrant=owner,
- owner=owner,
- project=project,
- git_ref=ref,
- )
- repository.removeRefs([ref.path])
- self.assertIsNone(job.build.recipe.git_ref)
- expected_exception_msg = (
- r"Source repository for "
- r"~rock-owner/rock-project/\+rock/test-rock has been deleted."
- )
- with ExpectedException(CannotBuild, expected_exception_msg):
- yield job.composeBuildRequest(None)
-
- @defer.inlineCallbacks
- def test_dispatchBuildToWorker_prefers_lxd(self):
- self.pushConfig("builddmaster", builder_proxy_host=None)
- job = self.makeJob()
- builder = MockBuilder()
- builder.processor = job.build.processor
- worker = OkWorker()
- job.setBuilder(builder, worker)
- chroot_lfa = self.factory.makeLibraryFileAlias(db_only=True)
- job.build.distro_arch_series.addOrUpdateChroot(
- chroot_lfa, image_type=BuildBaseImageType.CHROOT
- )
- lxd_lfa = self.factory.makeLibraryFileAlias(db_only=True)
- job.build.distro_arch_series.addOrUpdateChroot(
- lxd_lfa, image_type=BuildBaseImageType.LXD
- )
- yield job.dispatchBuildToWorker(DevNullLogger())
- self.assertEqual(
- ("ensurepresent", lxd_lfa.http_url, "", ""), worker.call_log[0]
- )
- self.assertEqual(1, self.stats_client.incr.call_count)
- self.assertEqual(
- self.stats_client.incr.call_args_list[0][0],
- (
- "build.count,builder_name={},env=test,"
- "job_type=ROCKRECIPEBUILD,region={}".format(
- builder.name, builder.region
- ),
- ),
- )
-
- @defer.inlineCallbacks
- def test_dispatchBuildToWorker_falls_back_to_chroot(self):
- self.pushConfig("builddmaster", builder_proxy_host=None)
- job = self.makeJob()
- builder = MockBuilder()
- builder.processor = job.build.processor
- worker = OkWorker()
- job.setBuilder(builder, worker)
- chroot_lfa = self.factory.makeLibraryFileAlias(db_only=True)
- job.build.distro_arch_series.addOrUpdateChroot(
- chroot_lfa, image_type=BuildBaseImageType.CHROOT
- )
- yield job.dispatchBuildToWorker(DevNullLogger())
- self.assertEqual(
- ("ensurepresent", chroot_lfa.http_url, "", ""), worker.call_log[0]
- )
-
-
-class MakeRockRecipeBuildMixin:
- """Provide the common makeBuild method returning a queued build."""
-
- def makeRockRecipe(self):
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
- return self.factory.makeRockRecipe(
- store_upload=True,
- store_name=self.factory.getUniqueUnicode(),
- store_secrets={"root": Macaroon().serialize()},
- )
-
- def makeBuild(self):
- recipe = self.makeRockRecipe()
- build = self.factory.makeRockRecipeBuild(
- requester=recipe.registrant,
- recipe=recipe,
- status=BuildStatus.BUILDING,
- )
- build.queueBuild()
- return build
-
- def makeUnmodifiableBuild(self):
- recipe = self.makeRockRecipe()
- build = self.factory.makeRockRecipeBuild(
- requester=recipe.registrant,
- recipe=recipe,
- status=BuildStatus.BUILDING,
- )
- build.distro_series.status = SeriesStatus.OBSOLETE
- build.queueBuild()
- return build
-
-
-class TestGetUploadMethodsForRockRecipeBuild(
- MakeRockRecipeBuildMixin, TestGetUploadMethodsMixin, TestCaseWithFactory
-):
- """IPackageBuild.getUpload* methods work with rock recipe builds."""
-
-
-class TestVerifySuccessfulBuildForRockRecipeBuild(
- MakeRockRecipeBuildMixin,
- TestVerifySuccessfulBuildMixin,
- TestCaseWithFactory,
-):
- """IBuildFarmJobBehaviour.verifySuccessfulBuild works."""
-
-
-class TestHandleStatusForRockRecipeBuild(
- MakeRockRecipeBuildMixin, TestHandleStatusMixin, TestCaseWithFactory
-):
- """IPackageBuild.handleStatus works with rock recipe builds."""
diff --git a/lib/lp/rocks/tests/test_rockrecipejob.py b/lib/lp/rocks/tests/test_rockrecipejob.py
deleted file mode 100644
index f5da198..0000000
--- a/lib/lp/rocks/tests/test_rockrecipejob.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright 2024 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Tests for rock recipe jobs."""
-
-from lp.rocks.interfaces.rockrecipe import ROCK_RECIPE_ALLOW_CREATE
-from lp.rocks.interfaces.rockrecipejob import (
- IRockRecipeJob,
- IRockRecipeRequestBuildsJob,
-)
-from lp.rocks.model.rockrecipejob import (
- RockRecipeJob,
- RockRecipeJobType,
- RockRecipeRequestBuildsJob,
-)
-from lp.services.features.testing import FeatureFixture
-from lp.testing import TestCaseWithFactory
-from lp.testing.layers import ZopelessDatabaseLayer
-
-
-class TestRockRecipeJob(TestCaseWithFactory):
-
- layer = ZopelessDatabaseLayer
-
- def setUp(self):
- super().setUp()
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
-
- def test_provides_interface(self):
- # `RockRecipeJob` objects provide `IRockRecipeJob`.
- recipe = self.factory.makeRockRecipe()
- self.assertProvides(
- RockRecipeJob(recipe, RockRecipeJobType.REQUEST_BUILDS, {}),
- IRockRecipeJob,
- )
-
-
-class TestRockRecipeRequestBuildsJob(TestCaseWithFactory):
-
- layer = ZopelessDatabaseLayer
-
- def setUp(self):
- super().setUp()
- self.useFixture(FeatureFixture({ROCK_RECIPE_ALLOW_CREATE: "on"}))
-
- def test_provides_interface(self):
- # `RockRecipeRequestBuildsJob` objects provide
- # `IRockRecipeRequestBuildsJob`."""
- recipe = self.factory.makeRockRecipe()
- job = RockRecipeRequestBuildsJob.create(recipe, recipe.registrant)
- self.assertProvides(job, IRockRecipeRequestBuildsJob)
-
- def test___repr__(self):
- # `RockRecipeRequestBuildsJob` objects have an informative __repr__.
- recipe = self.factory.makeRockRecipe()
- job = RockRecipeRequestBuildsJob.create(recipe, recipe.registrant)
- self.assertEqual(
- "<RockRecipeRequestBuildsJob for ~%s/%s/+rock/%s>"
- % (recipe.owner.name, recipe.project.name, recipe.name),
- repr(job),
- )
diff --git a/lib/lp/services/config/schema-lazr.conf b/lib/lp/services/config/schema-lazr.conf
index c285996..07f04c6 100644
--- a/lib/lp/services/config/schema-lazr.conf
+++ b/lib/lp/services/config/schema-lazr.conf
@@ -1995,11 +1995,6 @@ module: lp.charms.interfaces.charmrecipejob
dbuser: charm-build-job
crontab_group: MAIN
-[IRockRecipeRequestBuildsJobSource]
-module: lp.rocks.interfaces.rockrecipejob
-dbuser: rock-build-job
-crontab_group: MAIN
-
[ICIBuildUploadJobSource]
module: lp.soyuz.interfaces.archivejob
dbuser: uploader
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 609fc08..6cdf9b4 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -208,9 +208,6 @@ from lp.registry.model.karma import KarmaTotalCache
from lp.registry.model.milestone import Milestone
from lp.registry.model.packaging import Packaging
from lp.registry.model.suitesourcepackage import SuiteSourcePackage
-from lp.rocks.interfaces.rockrecipe import IRockRecipeSet
-from lp.rocks.interfaces.rockrecipebuild import IRockRecipeBuildSet
-from lp.rocks.model.rockrecipebuild import RockFile
from lp.services.auth.interfaces import IAccessTokenSet
from lp.services.auth.utils import create_access_token_secret
from lp.services.compat import message_as_bytes
@@ -6890,154 +6887,6 @@ class LaunchpadObjectFactory(ObjectFactory):
date_created=date_created,
)
- def makeRockRecipe(
- self,
- registrant=None,
- owner=None,
- project=None,
- name=None,
- description=None,
- git_ref=None,
- build_path=None,
- require_virtualized=True,
- information_type=InformationType.PUBLIC,
- auto_build=False,
- auto_build_channels=None,
- is_stale=None,
- store_upload=False,
- store_name=None,
- store_secrets=None,
- store_channels=None,
- date_created=DEFAULT,
- ):
- """Make a new rock recipe."""
- if registrant is None:
- registrant = self.makePerson()
- private = information_type not in PUBLIC_INFORMATION_TYPES
- if owner is None:
- # Private rock recipes cannot be owned by non-moderated teams.
- membership_policy = (
- TeamMembershipPolicy.OPEN
- if private
- else TeamMembershipPolicy.MODERATED
- )
- owner = self.makeTeam(
- registrant, membership_policy=membership_policy
- )
- if project is None:
- branch_sharing_policy = (
- BranchSharingPolicy.PUBLIC
- if not private
- else BranchSharingPolicy.PROPRIETARY
- )
- project = self.makeProduct(
- owner=registrant,
- registrant=registrant,
- information_type=information_type,
- branch_sharing_policy=branch_sharing_policy,
- )
- if name is None:
- name = self.getUniqueUnicode("rock-name")
- if git_ref is None:
- git_ref = self.makeGitRefs()[0]
- recipe = getUtility(IRockRecipeSet).new(
- registrant=registrant,
- owner=owner,
- project=project,
- name=name,
- description=description,
- git_ref=git_ref,
- build_path=build_path,
- require_virtualized=require_virtualized,
- information_type=information_type,
- auto_build=auto_build,
- auto_build_channels=auto_build_channels,
- store_upload=store_upload,
- store_name=store_name,
- store_secrets=store_secrets,
- store_channels=store_channels,
- date_created=date_created,
- )
- if is_stale is not None:
- removeSecurityProxy(recipe).is_stale = is_stale
- IStore(recipe).flush()
- return recipe
-
- def makeRockRecipeBuildRequest(
- self, recipe=None, requester=None, channels=None, architectures=None
- ):
- """Make a new RockRecipeBuildRequest."""
- if recipe is None:
- recipe = self.makeRockRecipe()
- if requester is None:
- requester = recipe.owner.teamowner
- if recipe.owner.is_team:
- requester = recipe.owner.teamowner
- else:
- requester = recipe.owner
- return recipe.requestBuilds(
- requester, channels=channels, architectures=architectures
- )
-
- def makeRockRecipeBuild(
- self,
- registrant=None,
- recipe=None,
- build_request=None,
- requester=None,
- distro_arch_series=None,
- channels=None,
- store_upload_metadata=None,
- date_created=DEFAULT,
- status=BuildStatus.NEEDSBUILD,
- builder=None,
- duration=None,
- **kwargs,
- ):
- if recipe is None:
- if registrant is None:
- if build_request is not None:
- registrant = build_request.requester
- else:
- registrant = requester
- recipe = self.makeRockRecipe(registrant=registrant, **kwargs)
- if distro_arch_series is None:
- distro_arch_series = self.makeDistroArchSeries()
- if build_request is None:
- build_request = self.makeRockRecipeBuildRequest(
- recipe=recipe, requester=requester, channels=channels
- )
- build = getUtility(IRockRecipeBuildSet).new(
- build_request,
- recipe,
- distro_arch_series,
- channels=channels,
- store_upload_metadata=store_upload_metadata,
- date_created=date_created,
- )
- if duration is not None:
- removeSecurityProxy(build).updateStatus(
- BuildStatus.BUILDING,
- builder=builder,
- date_started=build.date_created,
- )
- removeSecurityProxy(build).updateStatus(
- status,
- builder=builder,
- date_finished=build.date_started + duration,
- )
- else:
- removeSecurityProxy(build).updateStatus(status, builder=builder)
- IStore(build).flush()
- return build
-
- def makeRockFile(self, build=None, library_file=None):
- if build is None:
- build = self.makeRockRecipeBuild()
- if library_file is None:
- library_file = self.makeLibraryFileAlias()
- return ProxyFactory(RockFile(build=build, library_file=library_file))
-
def makeCIBuild(
self,
git_repository=None,
Follow ups