launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24816
[Merge] ~cjwatson/launchpad:oci-recipe-schedule-registry-upload into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:oci-recipe-schedule-registry-upload into launchpad:master with ~cjwatson/launchpad:oci-detailed-registry-errors as a prerequisite.
Commit message:
Add OCIRecipeBuild.scheduleRegistryUpload
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/384966
This allows an upload to be scheduled after configuring the recipe appropriately, without needing to dispatch a new build.
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:oci-recipe-schedule-registry-upload into launchpad:master.
diff --git a/lib/lp/oci/interfaces/ocirecipebuild.py b/lib/lp/oci/interfaces/ocirecipebuild.py
index aa6e291..5fe56a9 100644
--- a/lib/lp/oci/interfaces/ocirecipebuild.py
+++ b/lib/lp/oci/interfaces/ocirecipebuild.py
@@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function, unicode_literals
__metaclass__ = type
__all__ = [
+ 'CannotScheduleRegistryUpload',
'IOCIFile',
'IOCIFileSet',
'IOCIRecipeBuild',
@@ -19,13 +20,17 @@ from lazr.enum import (
Item,
)
from lazr.restful.declarations import (
+ error_status,
export_as_webservice_entry,
+ export_write_operation,
exported,
+ operation_for_version,
)
from lazr.restful.fields import (
CollectionField,
Reference,
)
+from six.moves import http_client
from zope.interface import (
Attribute,
Interface,
@@ -53,6 +58,11 @@ from lp.services.librarian.interfaces import ILibraryFileAlias
from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
+@error_status(http_client.BAD_REQUEST)
+class CannotScheduleRegistryUpload(Exception):
+ """This build cannot be uploaded to registries."""
+
+
class OCIRecipeBuildRegistryUploadStatus(EnumeratedType):
"""OCI build registry upload status type
@@ -216,6 +226,15 @@ class IOCIRecipeBuildEdit(Interface):
:return: An `IOCILayerFile`.
"""
+ @export_write_operation()
+ @operation_for_version("devel")
+ def scheduleRegistryUpload():
+ """Schedule an upload of this build to each configured registry.
+
+ :raises CannotScheduleRegistryUpload: if the build is not in a state
+ where an upload can be scheduled.
+ """
+
def cancel():
"""Cancel the build if it is either pending or in progress.
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 8562778..26c780b 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -163,6 +163,11 @@ class OCIRecipe(Storm, WebhookTargetMixin):
self.date_last_modified = date_created
self.git_ref = git_ref
+ def __repr__(self):
+ return "<OCIRecipe ~%s/%s/+oci/%s/+recipe/%s>" % (
+ self.owner.name, self.oci_project.pillar.name,
+ self.oci_project.name, self.name)
+
@property
def valid_webhook_event_types(self):
return ["oci-recipe:build:0.1"]
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index 9613404..fc84708 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -42,12 +42,14 @@ from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
from lp.buildmaster.model.packagebuild import PackageBuildMixin
from lp.oci.interfaces.ocirecipe import IOCIRecipeSet
from lp.oci.interfaces.ocirecipebuild import (
+ CannotScheduleRegistryUpload,
IOCIFile,
IOCIFileSet,
IOCIRecipeBuild,
IOCIRecipeBuildSet,
OCIRecipeBuildRegistryUploadStatus,
)
+from lp.oci.interfaces.ocirecipebuildjob import IOCIRegistryUploadJobSource
from lp.oci.model.ocirecipebuildjob import (
OCIRecipeBuildJob,
OCIRecipeBuildJobType,
@@ -446,6 +448,28 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
job = self.last_registry_upload_job
return (job and job.errors) or []
+ def scheduleRegistryUpload(self):
+ """See `IOCIRecipeBuild`."""
+ if not self.recipe.can_upload_to_registry:
+ raise CannotScheduleRegistryUpload(
+ "Cannot upload this build to registries because the recipe is "
+ "not properly configured.")
+ if not self.was_built or self.getFiles().is_empty():
+ raise CannotScheduleRegistryUpload(
+ "Cannot upload this build because it has no files.")
+ if (self.registry_upload_status ==
+ OCIRecipeBuildRegistryUploadStatus.PENDING):
+ raise CannotScheduleRegistryUpload(
+ "An upload of this build is already in progress.")
+ elif (self.registry_upload_status ==
+ OCIRecipeBuildRegistryUploadStatus.UPLOADED):
+ # XXX cjwatson 2020-04-22: This won't be quite right in the case
+ # where a recipe has multiple push rules.
+ raise CannotScheduleRegistryUpload(
+ "Cannot upload this build because it has already been "
+ "uploaded.")
+ getUtility(IOCIRegistryUploadJobSource).create(self)
+
@implementer(IOCIRecipeBuildSet)
class OCIRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
diff --git a/lib/lp/oci/subscribers/ocirecipebuild.py b/lib/lp/oci/subscribers/ocirecipebuild.py
index 242d761..606157f 100644
--- a/lib/lp/oci/subscribers/ocirecipebuild.py
+++ b/lib/lp/oci/subscribers/ocirecipebuild.py
@@ -14,6 +14,7 @@ from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
from lp.oci.interfaces.ocirecipebuildjob import IOCIRegistryUploadJobSource
from lp.services.features import getFeatureFlag
+from lp.services.scripts import log
from lp.services.webapp.publisher import canonical_url
from lp.services.webhooks.interfaces import IWebhookSet
from lp.services.webhooks.payload import compose_webhook_payload
@@ -47,4 +48,9 @@ def oci_recipe_build_modified(build, event):
if status_changed:
if (build.recipe.can_upload_to_registry and
build.status == BuildStatus.FULLYBUILT):
+ log.info("Scheduling upload of %r to registries." % build)
getUtility(IOCIRegistryUploadJobSource).create(build)
+ else:
+ log.info(
+ "%r is not configured for upload to registries." %
+ build.recipe)
diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py
index 80e94d5..1dbd170 100644
--- a/lib/lp/oci/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/tests/test_ocirecipebuild.py
@@ -29,6 +29,7 @@ from lp.oci.interfaces.ocirecipe import (
OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
)
from lp.oci.interfaces.ocirecipebuild import (
+ CannotScheduleRegistryUpload,
IOCIFileSet,
IOCIRecipeBuild,
IOCIRecipeBuildSet,
@@ -36,6 +37,7 @@ from lp.oci.interfaces.ocirecipebuild import (
)
from lp.oci.interfaces.ocirecipebuildjob import IOCIRegistryUploadJobSource
from lp.oci.model.ocirecipebuild import OCIRecipeBuildSet
+from lp.oci.tests.helpers import OCIConfigHelperMixin
from lp.registry.interfaces.series import SeriesStatus
from lp.services.config import config
from lp.services.features.testing import FeatureFixture
@@ -82,7 +84,7 @@ class TestOCIFileSet(TestCaseWithFactory):
self.assertIsNone(result)
-class TestOCIRecipeBuild(TestCaseWithFactory):
+class TestOCIRecipeBuild(OCIConfigHelperMixin, TestCaseWithFactory):
layer = LaunchpadZopelessLayer
@@ -257,6 +259,38 @@ class TestOCIRecipeBuild(TestCaseWithFactory):
self.assertEqual(1, hook.deliveries.count())
self.assertThat(logger.output, LogsScheduledWebhooks(expected_logs))
+ def test_updateStatus_failure_does_not_trigger_registry_uploads(self):
+ # A failed OCIRecipeBuild does not trigger registry uploads.
+ self.setConfig()
+ self.factory.makeOCIPushRule(self.build.recipe)
+ with dbuser(config.builddmaster.dbuser):
+ self.build.updateStatus(BuildStatus.FAILEDTOBUILD)
+ self.assertContentEqual([], self.build.registry_upload_jobs)
+
+ def test_updateStatus_fullybuilt_not_configured(self):
+ # A completed OCIRecipeBuild does not trigger registry uploads if
+ # the recipe is not properly configured for that.
+ logger = self.useFixture(FakeLogger())
+ with dbuser(config.builddmaster.dbuser):
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.assertEqual(0, len(list(self.build.registry_upload_jobs)))
+ self.assertIn(
+ "%r is not configured for upload to registries." % (
+ self.build.recipe),
+ logger.output.splitlines())
+
+ def test_updateStatus_fullybuilt_triggers_registry_uploads(self):
+ # A completed OCIRecipeBuild triggers registry uploads.
+ self.setConfig()
+ logger = self.useFixture(FakeLogger())
+ self.factory.makeOCIPushRule(self.build.recipe)
+ with dbuser(config.builddmaster.dbuser):
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.assertEqual(1, len(list(self.build.registry_upload_jobs)))
+ self.assertIn(
+ "Scheduling upload of %r to registries." % self.build,
+ logger.output.splitlines())
+
def test_eta(self):
# OCIRecipeBuild.eta returns a non-None value when it should, or
# None when there's no start time.
@@ -349,6 +383,119 @@ class TestOCIRecipeBuild(TestCaseWithFactory):
[{"code": "BOOM", "message": "Boom", "detail": "It went boom"}],
build.registry_upload_errors)
+ def test_scheduleRegistryUpload(self):
+ # A build not previously uploaded to a registry can be uploaded
+ # manually.
+ self.setConfig()
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.factory.makeOCIPushRule(recipe=self.build.recipe)
+ self.factory.makeOCIFile(
+ build=self.build,
+ library_file=self.factory.makeLibraryFileAlias(db_only=True))
+ self.build.scheduleRegistryUpload()
+ [job] = getUtility(IOCIRegistryUploadJobSource).iterReady()
+ self.assertEqual(JobStatus.WAITING, job.job.status)
+ self.assertEqual(self.build, job.build)
+
+ def test_scheduleRegistryUpload_not_configured(self):
+ # A build that is not properly configured cannot be uploaded to
+ # registries.
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.assertRaisesWithContent(
+ CannotScheduleRegistryUpload,
+ "Cannot upload this build to registries because the recipe is not "
+ "properly configured.",
+ self.build.scheduleRegistryUpload)
+ self.assertEqual(
+ [], list(getUtility(IOCIRegistryUploadJobSource).iterReady()))
+
+ def test_scheduleRegistryUpload_no_files(self):
+ # A build with no files cannot be uploaded to registries.
+ self.setConfig()
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.factory.makeOCIPushRule(recipe=self.build.recipe)
+ self.assertRaisesWithContent(
+ CannotScheduleRegistryUpload,
+ "Cannot upload this build because it has no files.",
+ self.build.scheduleRegistryUpload)
+ self.assertEqual(
+ [], list(getUtility(IOCIRegistryUploadJobSource).iterReady()))
+
+ def test_scheduleRegistryUpload_already_in_progress(self):
+ # A build with an upload already in progress will not have another
+ # one created.
+ self.setConfig()
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.factory.makeOCIPushRule(recipe=self.build.recipe)
+ self.factory.makeOCIFile(
+ build=self.build,
+ library_file=self.factory.makeLibraryFileAlias(db_only=True))
+ old_job = getUtility(IOCIRegistryUploadJobSource).create(self.build)
+ self.assertRaisesWithContent(
+ CannotScheduleRegistryUpload,
+ "An upload of this build is already in progress.",
+ self.build.scheduleRegistryUpload)
+ self.assertEqual(
+ [old_job],
+ list(getUtility(IOCIRegistryUploadJobSource).iterReady()))
+
+ def test_scheduleRegistryUpload_already_uploaded(self):
+ # A build with an upload that has already completed will not have
+ # another one created.
+ self.setConfig()
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.factory.makeOCIPushRule(recipe=self.build.recipe)
+ self.factory.makeOCIFile(
+ build=self.build,
+ library_file=self.factory.makeLibraryFileAlias(db_only=True))
+ old_job = getUtility(IOCIRegistryUploadJobSource).create(self.build)
+ removeSecurityProxy(old_job).job._status = JobStatus.COMPLETED
+ self.assertRaisesWithContent(
+ CannotScheduleRegistryUpload,
+ "Cannot upload this build because it has already been uploaded.",
+ self.build.scheduleRegistryUpload)
+ self.assertEqual(
+ [], list(getUtility(IOCIRegistryUploadJobSource).iterReady()))
+
+ def test_scheduleRegistryUpload_triggers_webhooks(self):
+ # Scheduling a registry upload triggers webhooks on the
+ # corresponding recipe.
+ self.setConfig()
+ logger = self.useFixture(FakeLogger())
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.factory.makeOCIFile(
+ build=self.build,
+ library_file=self.factory.makeLibraryFileAlias(db_only=True))
+ self.factory.makeOCIPushRule(recipe=self.build.recipe)
+ hook = self.factory.makeWebhook(
+ target=self.build.recipe, event_types=["oci-recipe:build:0.1"])
+ with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):
+ self.build.scheduleRegistryUpload()
+ expected_payload = {
+ "recipe_build": Equals(
+ canonical_url(self.build, force_local_path=True)),
+ "action": Equals("status-changed"),
+ "recipe": Equals(
+ canonical_url(self.build.recipe, force_local_path=True)),
+ "build_request": Is(None),
+ "status": Equals("Successfully built"),
+ "registry_upload_status": Equals("Pending"),
+ }
+ delivery = hook.deliveries.one()
+ self.assertThat(
+ delivery, MatchesStructure(
+ event_type=Equals("oci-recipe:build:0.1"),
+ payload=MatchesDict(expected_payload)))
+ with dbuser(config.IWebhookDeliveryJobSource.dbuser):
+ self.assertEqual(
+ "<WebhookDeliveryJob for webhook %d on %r>" % (
+ hook.id, hook.target),
+ repr(delivery))
+ self.assertThat(
+ logger.output, LogsScheduledWebhooks([
+ (hook, "oci-recipe:build:0.1",
+ MatchesDict(expected_payload))]))
+
class TestOCIRecipeBuildSet(TestCaseWithFactory):