launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #24446
[Merge] ~cjwatson/launchpad:oci-webhooks into launchpad:master
Colin Watson has proposed merging ~cjwatson/launchpad:oci-webhooks into launchpad:master.
Commit message:
Add webhooks for OCI recipe builds
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/380351
DB patch: https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/380350
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:oci-webhooks into launchpad:master.
diff --git a/database/schema/security.cfg b/database/schema/security.cfg
index d0832c1..4c1b396 100644
--- a/database/schema/security.cfg
+++ b/database/schema/security.cfg
@@ -2620,6 +2620,7 @@ public.livefs = SELECT
public.ociproject = SELECT
public.ociprojectname = SELECT
public.ociprojectseries = SELECT
+public.ocirecipe = SELECT
public.person = SELECT
public.product = SELECT
public.snap = SELECT
diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py
index 483eb85..7d044f5 100644
--- a/lib/lp/oci/browser/ocirecipe.py
+++ b/lib/lp/oci/browser/ocirecipe.py
@@ -35,8 +35,10 @@ from lp.oci.interfaces.ocirecipe import (
IOCIRecipe,
IOCIRecipeSet,
NoSuchOCIRecipe,
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
)
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
+from lp.services.features import getFeatureFlag
from lp.services.propertycache import cachedproperty
from lp.services.webapp import (
canonical_url,
@@ -48,10 +50,11 @@ from lp.services.webapp import (
stepthrough,
)
from lp.services.webapp.breadcrumb import NameBreadcrumb
+from lp.services.webhooks.browser import WebhookTargetNavigationMixin
from lp.soyuz.browser.build import get_build_by_id_str
-class OCIRecipeNavigation(Navigation):
+class OCIRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
usedfor = IOCIRecipe
@@ -77,7 +80,7 @@ class OCIRecipeNavigationMenu(NavigationMenu):
facet = "overview"
- links = ("admin", "edit", "delete")
+ links = ("admin", "edit", "webhooks", "delete")
@enabled_with_permission("launchpad.Admin")
def admin(self):
@@ -87,6 +90,12 @@ class OCIRecipeNavigationMenu(NavigationMenu):
def edit(self):
return Link("+edit", "Edit OCI recipe", icon="edit")
+ @enabled_with_permission('launchpad.Edit')
+ def webhooks(self):
+ return Link(
+ '+webhooks', 'Manage webhooks', icon='edit',
+ enabled=bool(getFeatureFlag(OCI_RECIPE_WEBHOOKS_FEATURE_FLAG)))
+
@enabled_with_permission("launchpad.Edit")
def delete(self):
return Link("+delete", "Delete OCI recipe", icon="trash-icon")
diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py
index 7933074..540649f 100644
--- a/lib/lp/oci/browser/tests/test_ocirecipe.py
+++ b/lib/lp/oci/browser/tests/test_ocirecipe.py
@@ -327,10 +327,13 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
processor=processor)
self.factory.makeBuilder(virtualized=True)
- def makeOCIRecipe(self, **kwargs):
+ def makeOCIRecipe(self, oci_project=None, **kwargs):
+ if oci_project is None:
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
return self.factory.makeOCIRecipe(
registrant=self.person, owner=self.person, name="recipe-name",
- **kwargs)
+ oci_project=oci_project, **kwargs)
def makeBuild(self, recipe=None, date_created=None, **kwargs):
if recipe is None:
@@ -343,7 +346,8 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
date_created=date_created, **kwargs)
def test_breadcrumb(self):
- oci_project = self.factory.makeOCIProject()
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
oci_project_name = oci_project.name
oci_project_url = canonical_url(oci_project)
recipe = self.makeOCIRecipe(oci_project=oci_project)
@@ -369,7 +373,8 @@ class TestOCIRecipeView(BaseTestOCIRecipeView):
text=re.compile(r"\srecipe-name\s")))))
def test_index(self):
- oci_project = self.factory.makeOCIProject()
+ oci_project = self.factory.makeOCIProject(
+ pillar=self.distroseries.distribution)
oci_project_name = oci_project.name
oci_project_display = oci_project.display_name
[ref] = self.factory.makeGitRefs(
diff --git a/lib/lp/oci/configure.zcml b/lib/lp/oci/configure.zcml
index a01178a..da8d695 100644
--- a/lib/lp/oci/configure.zcml
+++ b/lib/lp/oci/configure.zcml
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2020 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
<configure
@@ -49,6 +49,14 @@
permission="launchpad.Admin"
interface="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuildAdmin" />
</class>
+ <subscriber
+ for="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuild
+ lazr.lifecycle.interfaces.IObjectCreatedEvent"
+ handler="lp.oci.subscribers.ocirecipebuild.oci_recipe_build_created" />
+ <subscriber
+ for="lp.oci.interfaces.ocirecipebuild.IOCIRecipeBuild
+ lazr.lifecycle.interfaces.IObjectModifiedEvent"
+ handler="lp.oci.subscribers.ocirecipebuild.oci_recipe_build_status_changed" />
<!-- OCIRecipeBuildSet -->
<securedutility
diff --git a/lib/lp/oci/interfaces/ocirecipe.py b/lib/lp/oci/interfaces/ocirecipe.py
index e665c1e..d5bef5e 100644
--- a/lib/lp/oci/interfaces/ocirecipe.py
+++ b/lib/lp/oci/interfaces/ocirecipe.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Canonical Ltd. This software is licensed under the
+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Interfaces related to recipes for OCI Images."""
@@ -15,6 +15,7 @@ __all__ = [
'IOCIRecipeView',
'NoSourceForOCIRecipe',
'NoSuchOCIRecipe',
+ 'OCI_RECIPE_WEBHOOKS_FEATURE_FLAG',
'OCIRecipeBuildAlreadyPending',
'OCIRecipeNotOwner',
]
@@ -49,6 +50,10 @@ from lp.services.fields import (
PersonChoice,
PublicPersonChoice,
)
+from lp.services.webhooks.interfaces import IWebhookTarget
+
+
+OCI_RECIPE_WEBHOOKS_FEATURE_FLAG = "oci.recipe.webhooks.enabled"
@error_status(http_client.UNAUTHORIZED)
@@ -132,7 +137,7 @@ class IOCIRecipeView(Interface):
"""
-class IOCIRecipeEdit(Interface):
+class IOCIRecipeEdit(IWebhookTarget):
"""`IOCIRecipe` methods that require launchpad.Edit permission."""
def destroySelf():
diff --git a/lib/lp/oci/model/ocirecipe.py b/lib/lp/oci/model/ocirecipe.py
index 99593a3..aa3c532 100644
--- a/lib/lp/oci/model/ocirecipe.py
+++ b/lib/lp/oci/model/ocirecipe.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Canonical Ltd. This software is licensed under the
+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""A recipe for building Open Container Initiative images."""
@@ -61,6 +61,8 @@ from lp.services.database.stormexpr import (
Greatest,
NullsLast,
)
+from lp.services.webhooks.interfaces import IWebhookSet
+from lp.services.webhooks.model import WebhookTargetMixin
def oci_recipe_modified(recipe, event):
@@ -73,7 +75,7 @@ def oci_recipe_modified(recipe, event):
@implementer(IOCIRecipe)
-class OCIRecipe(Storm):
+class OCIRecipe(Storm, WebhookTargetMixin):
__storm_table__ = 'OCIRecipe'
@@ -123,6 +125,10 @@ class OCIRecipe(Storm):
self.date_last_modified = date_created
self.git_ref = git_ref
+ @property
+ def valid_webhook_event_types(self):
+ return ["oci-recipe:build:0.1"]
+
def destroySelf(self):
"""See `IOCIRecipe`."""
# XXX twom 2019-11-26 This needs to expand as more build artifacts
@@ -137,6 +143,7 @@ class OCIRecipe(Storm):
build_farm_job_ids = list(store.find(
OCIRecipeBuild.build_farm_job_id, OCIRecipeBuild.recipe == self))
store.find(OCIRecipeBuild, OCIRecipeBuild.recipe == self).remove()
+ getUtility(IWebhookSet).delete(self.webhooks)
store.remove(self)
store.find(
BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)).remove()
diff --git a/lib/lp/oci/model/ocirecipebuild.py b/lib/lp/oci/model/ocirecipebuild.py
index 12688cd..89b7215 100644
--- a/lib/lp/oci/model/ocirecipebuild.py
+++ b/lib/lp/oci/model/ocirecipebuild.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Canonical Ltd. This software is licensed under the
+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""A build record for OCI Recipes."""
@@ -59,7 +59,11 @@ from lp.services.librarian.model import (
LibraryFileAlias,
LibraryFileContent,
)
-from lp.services.propertycache import cachedproperty
+from lp.services.propertycache import (
+ cachedproperty,
+ get_property_cache,
+ )
+from lp.services.webapp.snapshot import notify_modified
@implementer(IOCIFile)
@@ -270,6 +274,22 @@ class OCIRecipeBuild(PackageBuildMixin, Storm):
return self.distribution.currentseries.getDistroArchSeriesByProcessor(
self.processor)
+ def updateStatus(self, status, builder=None, slave_status=None,
+ date_started=None, date_finished=None,
+ force_invalid_transition=False):
+ """See `IBuildFarmJob`."""
+ edited_fields = set()
+ with notify_modified(self, edited_fields) as previous_obj:
+ super(OCIRecipeBuild, self).updateStatus(
+ status, builder=builder, slave_status=slave_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")
+ # 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:
diff --git a/lib/lp/oci/subscribers/__init__.py b/lib/lp/oci/subscribers/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/lp/oci/subscribers/__init__.py
diff --git a/lib/lp/oci/subscribers/ocirecipebuild.py b/lib/lp/oci/subscribers/ocirecipebuild.py
new file mode 100644
index 0000000..3e7f80d
--- /dev/null
+++ b/lib/lp/oci/subscribers/ocirecipebuild.py
@@ -0,0 +1,42 @@
+# Copyright 2016-2020 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Event subscribers for OCI recipe builds."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from zope.component import getUtility
+
+from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG
+from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuild
+from lp.services.features import getFeatureFlag
+from lp.services.webapp.publisher import canonical_url
+from lp.services.webhooks.interfaces import IWebhookSet
+from lp.services.webhooks.payload import compose_webhook_payload
+
+
+def _trigger_oci_recipe_build_webhook(build, action):
+ if getFeatureFlag(OCI_RECIPE_WEBHOOKS_FEATURE_FLAG):
+ payload = {
+ "recipe_build": canonical_url(build, force_local_path=True),
+ "action": action,
+ }
+ payload.update(compose_webhook_payload(
+ IOCIRecipeBuild, build,
+ ["recipe", "status"]))
+ getUtility(IWebhookSet).trigger(
+ build.recipe, "oci-recipe:build:0.1", payload)
+
+
+def oci_recipe_build_created(build, event):
+ """Trigger events when a new OCI recipe build is created."""
+ _trigger_oci_recipe_build_webhook(build, "created")
+
+
+def oci_recipe_build_status_changed(build, event):
+ """Trigger events when OCI recipe build statuses change."""
+ if event.edited_fields is not None:
+ if "status" in event.edited_fields:
+ _trigger_oci_recipe_build_webhook(build, "status-changed")
diff --git a/lib/lp/oci/tests/test_ocirecipe.py b/lib/lp/oci/tests/test_ocirecipe.py
index 5b614f0..e930ccc 100644
--- a/lib/lp/oci/tests/test_ocirecipe.py
+++ b/lib/lp/oci/tests/test_ocirecipe.py
@@ -1,10 +1,18 @@
-# Copyright 2019 Canonical Ltd. This software is licensed under the
+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for OCI image building recipe functionality."""
from __future__ import absolute_import, print_function, unicode_literals
+from fixtures import FakeLogger
+from storm.exceptions import LostObjectError
+from testtools.matchers import (
+ Equals,
+ MatchesDict,
+ MatchesStructure,
+ )
+import transaction
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -15,21 +23,27 @@ from lp.oci.interfaces.ocirecipe import (
IOCIRecipeSet,
NoSourceForOCIRecipe,
NoSuchOCIRecipe,
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG,
OCIRecipeBuildAlreadyPending,
OCIRecipeNotOwner,
)
from lp.oci.interfaces.ocirecipebuild import IOCIRecipeBuildSet
+from lp.services.config import config
from lp.services.database.constants import (
ONE_DAY_AGO,
UTC_NOW,
)
from lp.services.database.sqlbase import flush_database_caches
+from lp.services.features.testing import FeatureFixture
+from lp.services.webapp.publisher import canonical_url
from lp.services.webapp.snapshot import notify_modified
+from lp.services.webhooks.testing import LogsScheduledWebhooks
from lp.testing import (
admin_logged_in,
person_logged_in,
TestCaseWithFactory,
)
+from lp.testing.dbuser import dbuser
from lp.testing.layers import DatabaseFunctionalLayer
@@ -80,6 +94,40 @@ class TestOCIRecipe(TestCaseWithFactory):
ocirecipe.requestBuild,
ocirecipe.owner, oci_arch)
+ def test_requestBuild_triggers_webhooks(self):
+ # Requesting a build triggers webhooks.
+ logger = self.useFixture(FakeLogger())
+ with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):
+ recipe = self.factory.makeOCIRecipe()
+ oci_arch = self.factory.makeOCIRecipeArch(recipe=recipe)
+ hook = self.factory.makeWebhook(
+ target=recipe, event_types=["oci-recipe:build:0.1"])
+ build = recipe.requestBuild(recipe.owner, oci_arch)
+
+ expected_payload = {
+ "recipe_build": Equals(
+ canonical_url(build, force_local_path=True)),
+ "action": Equals("created"),
+ "recipe": Equals(canonical_url(recipe, force_local_path=True)),
+ "status": Equals("Needs building"),
+ }
+ with person_logged_in(recipe.owner):
+ 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))]))
+
def test_destroySelf(self):
oci_recipe = self.factory.makeOCIRecipe()
build_ids = []
@@ -94,6 +142,17 @@ class TestOCIRecipe(TestCaseWithFactory):
for build_id in build_ids:
self.assertIsNone(getUtility(IOCIRecipeBuildSet).getByID(build_id))
+ def test_related_webhooks_deleted(self):
+ owner = self.factory.makePerson()
+ with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):
+ recipe = self.factory.makeOCIRecipe(registrant=owner, owner=owner)
+ webhook = self.factory.makeWebhook(target=recipe)
+ with person_logged_in(recipe.owner):
+ webhook.ping()
+ recipe.destroySelf()
+ transaction.commit()
+ self.assertRaises(LostObjectError, getattr, webhook, "target")
+
def test_getBuilds(self):
# Test the various getBuilds methods.
oci_recipe = self.factory.makeOCIRecipe()
diff --git a/lib/lp/oci/tests/test_ocirecipebuild.py b/lib/lp/oci/tests/test_ocirecipebuild.py
index 60aaf93..fcce007 100644
--- a/lib/lp/oci/tests/test_ocirecipebuild.py
+++ b/lib/lp/oci/tests/test_ocirecipebuild.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Canonical Ltd. This software is licensed under the
+# Copyright 2019-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for OCI image building recipe functionality."""
@@ -7,8 +7,14 @@ from __future__ import absolute_import, print_function, unicode_literals
from datetime import timedelta
+from fixtures import FakeLogger
import six
-from testtools.matchers import Equals
+from testtools.matchers import (
+ ContainsDict,
+ Equals,
+ MatchesDict,
+ MatchesStructure,
+ )
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -17,18 +23,24 @@ 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.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG
from lp.oci.interfaces.ocirecipebuild import (
IOCIRecipeBuild,
IOCIRecipeBuildSet,
)
from lp.oci.model.ocirecipebuild import OCIRecipeBuildSet
from lp.registry.interfaces.series import SeriesStatus
+from lp.services.config import config
+from lp.services.features.testing import FeatureFixture
from lp.services.propertycache import clear_property_cache
+from lp.services.webapp.publisher import canonical_url
+from lp.services.webhooks.testing import LogsScheduledWebhooks
from lp.testing import (
admin_logged_in,
StormStatementRecorder,
TestCaseWithFactory,
)
+from lp.testing.dbuser import dbuser
from lp.testing.layers import (
DatabaseFunctionalLayer,
LaunchpadZopelessLayer,
@@ -111,6 +123,63 @@ class TestOCIRecipeBuild(TestCaseWithFactory):
self.assertIsNotNone(bq.processor)
self.assertEqual(bq, self.build.buildqueue_record)
+ def test_updateStatus_triggers_webhooks(self):
+ # Updating the status of an OCIRecipeBuild triggers webhooks on the
+ # corresponding OCIRecipe.
+ logger = self.useFixture(FakeLogger())
+ 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.updateStatus(BuildStatus.FULLYBUILT)
+ 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)),
+ "status": Equals("Successfully built"),
+ }
+ self.assertThat(
+ logger.output, LogsScheduledWebhooks([
+ (hook, "oci-recipe:build:0.1",
+ MatchesDict(expected_payload))]))
+
+ 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))
+
+ def test_updateStatus_no_change_does_not_trigger_webhooks(self):
+ # An updateStatus call that doesn't change the build's status
+ # attribute does not trigger webhooks.
+ logger = self.useFixture(FakeLogger())
+ 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.updateStatus(BuildStatus.BUILDING)
+ expected_logs = [
+ (hook, "oci-recipe:build:0.1", ContainsDict({
+ "action": Equals("status-changed"),
+ "status": Equals("Currently building"),
+ }))]
+ self.assertEqual(1, hook.deliveries.count())
+ self.assertThat(logger.output, LogsScheduledWebhooks(expected_logs))
+
+ self.build.updateStatus(BuildStatus.BUILDING)
+ expected_logs = [
+ (hook, "oci-recipe:build:0.1", ContainsDict({
+ "action": Equals("status-changed"),
+ "status": Equals("Currently building"),
+ }))]
+ self.assertEqual(1, hook.deliveries.count())
+ self.assertThat(logger.output, LogsScheduledWebhooks(expected_logs))
+
def test_eta(self):
# OCIRecipeBuild.eta returns a non-None value when it should, or
# None when there's no start time.
@@ -193,16 +262,20 @@ class TestOCIRecipeBuildSet(TestCaseWithFactory):
self.assertTrue(target.virtualized)
def test_virtualized_processor_requires(self):
- distro_arch_series = self.factory.makeDistroArchSeries()
- distro_arch_series.processor.supports_nonvirtualized = False
recipe = self.factory.makeOCIRecipe(require_virtualized=False)
+ distro_arch_series = self.factory.makeDistroArchSeries(
+ distroseries=self.factory.makeDistroSeries(
+ distribution=recipe.oci_project.distribution))
+ distro_arch_series.processor.supports_nonvirtualized = False
target = self.factory.makeOCIRecipeBuild(
distro_arch_series=distro_arch_series, recipe=recipe)
self.assertTrue(target.virtualized)
def test_virtualized_no_support(self):
recipe = self.factory.makeOCIRecipe(require_virtualized=False)
- distro_arch_series = self.factory.makeDistroArchSeries()
+ distro_arch_series = self.factory.makeDistroArchSeries(
+ distroseries=self.factory.makeDistroSeries(
+ distribution=recipe.oci_project.distribution))
distro_arch_series.processor.supports_nonvirtualized = True
target = self.factory.makeOCIRecipeBuild(
recipe=recipe, distro_arch_series=distro_arch_series)
diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py
index 1a0250e..d4d20e3 100644
--- a/lib/lp/services/webhooks/interfaces.py
+++ b/lib/lp/services/webhooks/interfaces.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Webhook interfaces."""
@@ -76,6 +76,7 @@ WEBHOOK_EVENT_TYPES = {
"git:push:0.1": "Git push",
"livefs:build:0.1": "Live filesystem build",
"merge-proposal:0.1": "Merge proposal",
+ "oci-recipe:build:0.1": "OCI recipe build",
"snap:build:0.1": "Snap build",
}
diff --git a/lib/lp/services/webhooks/model.py b/lib/lp/services/webhooks/model.py
index 440ce26..cb42ce9 100644
--- a/lib/lp/services/webhooks/model.py
+++ b/lib/lp/services/webhooks/model.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -107,6 +107,9 @@ class Webhook(StormBase):
livefs_id = Int(name='livefs')
livefs = Reference(livefs_id, 'LiveFS.id')
+ oci_recipe_id = Int(name='oci_recipe')
+ oci_recipe = Reference(oci_recipe_id, 'OCIRecipe.id')
+
registrant_id = Int(name='registrant', allow_none=False)
registrant = Reference(registrant_id, 'Person.id')
date_created = DateTime(tzinfo=utc, allow_none=False)
@@ -128,6 +131,8 @@ class Webhook(StormBase):
return self.snap
elif self.livefs is not None:
return self.livefs
+ elif self.oci_recipe is not None:
+ return self.oci_recipe
else:
raise AssertionError("No target.")
@@ -181,6 +186,7 @@ class WebhookSet:
secret):
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
+ from lp.oci.interfaces.ocirecipe import IOCIRecipe
from lp.snappy.interfaces.snap import ISnap
from lp.soyuz.interfaces.livefs import ILiveFS
@@ -193,6 +199,8 @@ class WebhookSet:
hook.snap = target
elif ILiveFS.providedBy(target):
hook.livefs = target
+ elif IOCIRecipe.providedBy(target):
+ hook.oci_recipe = target
else:
raise AssertionError("Unsupported target: %r" % (target,))
hook.registrant = registrant
@@ -216,6 +224,7 @@ class WebhookSet:
def findByTarget(self, target):
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
+ from lp.oci.interfaces.ocirecipe import IOCIRecipe
from lp.snappy.interfaces.snap import ISnap
from lp.soyuz.interfaces.livefs import ILiveFS
@@ -227,6 +236,8 @@ class WebhookSet:
target_filter = Webhook.snap == target
elif ILiveFS.providedBy(target):
target_filter = Webhook.livefs == target
+ elif IOCIRecipe.providedBy(target):
+ target_filter = Webhook.oci_recipe == target
else:
raise AssertionError("Unsupported target: %r" % (target,))
return IStore(Webhook).find(Webhook, target_filter).order_by(
diff --git a/lib/lp/services/webhooks/tests/test_browser.py b/lib/lp/services/webhooks/tests/test_browser.py
index 39021bd..95ff526 100644
--- a/lib/lp/services/webhooks/tests/test_browser.py
+++ b/lib/lp/services/webhooks/tests/test_browser.py
@@ -16,6 +16,7 @@ from testtools.matchers import (
import transaction
from zope.component import getUtility
+from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.interfaces import IPlacelessAuthUtility
from lp.services.webapp.publisher import canonical_url
@@ -132,6 +133,27 @@ class LiveFSTestHelpers:
return [obj]
+class OCIRecipeTestHelpers:
+ event_type = "oci-recipe:build:0.1"
+ expected_event_types = [
+ ("oci-recipe:build:0.1", "OCI recipe build"),
+ ]
+
+ def setUp(self):
+ super(OCIRecipeTestHelpers, self).setUp()
+
+ def makeTarget(self):
+ self.useFixture(FeatureFixture({
+ 'webhooks.new.enabled': 'true',
+ OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: 'on',
+ }))
+ owner = self.factory.makePerson()
+ return self.factory.makeOCIRecipe(registrant=owner, owner=owner)
+
+ def getTraversalStack(self, obj):
+ return [obj]
+
+
class WebhookTargetViewTestHelpers:
def setUp(self):
@@ -257,6 +279,12 @@ class TestWebhooksViewLiveFS(
pass
+class TestWebhooksViewOCIRecipe(
+ TestWebhooksViewBase, OCIRecipeTestHelpers, TestCaseWithFactory):
+
+ pass
+
+
class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
layer = DatabaseFunctionalLayer
@@ -361,6 +389,12 @@ class TestWebhookAddViewLiveFS(
pass
+class TestWebhookAddViewOCIRecipe(
+ TestWebhookAddViewBase, OCIRecipeTestHelpers, TestCaseWithFactory):
+
+ pass
+
+
class WebhookViewTestHelpers:
def setUp(self):
@@ -469,6 +503,12 @@ class TestWebhookViewLiveFS(
pass
+class TestWebhookViewOCIRecipe(
+ TestWebhookViewBase, OCIRecipeTestHelpers, TestCaseWithFactory):
+
+ pass
+
+
class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
layer = DatabaseFunctionalLayer
@@ -525,3 +565,9 @@ class TestWebhookDeleteViewLiveFS(
TestWebhookDeleteViewBase, LiveFSTestHelpers, TestCaseWithFactory):
pass
+
+
+class TestWebhookDeleteViewOCIRecipe(
+ TestWebhookDeleteViewBase, OCIRecipeTestHelpers, TestCaseWithFactory):
+
+ pass
diff --git a/lib/lp/services/webhooks/tests/test_job.py b/lib/lp/services/webhooks/tests/test_job.py
index 762f7ff..caf81ce 100644
--- a/lib/lp/services/webhooks/tests/test_job.py
+++ b/lib/lp/services/webhooks/tests/test_job.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2019 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for `WebhookJob`s."""
@@ -37,6 +37,7 @@ from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
from lp.app import versioninfo
+from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG
from lp.services.database.interfaces import IStore
from lp.services.features.testing import FeatureFixture
from lp.services.job.interfaces.job import JobStatus
@@ -354,6 +355,17 @@ class TestWebhookDeliveryJob(TestCaseWithFactory):
"<WebhookDeliveryJob for webhook %d on %r>" % (hook.id, livefs),
repr(job))
+ def test_oci_recipe__repr__(self):
+ # `WebhookDeliveryJob` objects for OCI recipes have an informative
+ # __repr__.
+ with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):
+ recipe = self.factory.makeOCIRecipe()
+ hook = self.factory.makeWebhook(target=recipe)
+ job = WebhookDeliveryJob.create(hook, 'test', payload={'foo': 'bar'})
+ self.assertEqual(
+ "<WebhookDeliveryJob for webhook %d on %r>" % (hook.id, recipe),
+ repr(job))
+
def test_short_lease_and_timeout(self):
# Webhook jobs have a request timeout of 30 seconds, a celery
# timeout of 45 seconds, and a lease of 60 seconds, to give
diff --git a/lib/lp/services/webhooks/tests/test_model.py b/lib/lp/services/webhooks/tests/test_model.py
index cd43758..e8810f2 100644
--- a/lib/lp/services/webhooks/tests/test_model.py
+++ b/lib/lp/services/webhooks/tests/test_model.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
from storm.store import Store
@@ -13,6 +13,7 @@ from zope.security.checker import getChecker
from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
+from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG
from lp.registry.enums import BranchSharingPolicy
from lp.services.database.interfaces import IStore
from lp.services.features.testing import FeatureFixture
@@ -414,3 +415,16 @@ class TestWebhookSetLiveFS(TestWebhookSetBase, TestCaseWithFactory):
LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
return self.factory.makeLiveFS(registrant=owner,
owner=owner, **kwargs)
+
+
+class TestWebhookSetOCIRecipe(TestWebhookSetBase, TestCaseWithFactory):
+
+ event_type = 'oci-recipe:build:0.1'
+
+ def makeTarget(self, owner=None, **kwargs):
+ if owner is None:
+ owner = self.factory.makePerson()
+
+ with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):
+ return self.factory.makeOCIRecipe(
+ registrant=owner, owner=owner, **kwargs)
diff --git a/lib/lp/services/webhooks/tests/test_webservice.py b/lib/lp/services/webhooks/tests/test_webservice.py
index 19af1bc..dafc6d0 100644
--- a/lib/lp/services/webhooks/tests/test_webservice.py
+++ b/lib/lp/services/webhooks/tests/test_webservice.py
@@ -1,4 +1,4 @@
-# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2020 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for the webhook webservice objects."""
@@ -21,6 +21,7 @@ from testtools.matchers import (
)
from zope.security.proxy import removeSecurityProxy
+from lp.oci.interfaces.ocirecipe import OCI_RECIPE_WEBHOOKS_FEATURE_FLAG
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.interfaces import OAuthPermission
from lp.soyuz.interfaces.livefs import (
@@ -394,3 +395,13 @@ class TestWebhookTargetLiveFS(TestWebhookTargetBase, TestCaseWithFactory):
with FeatureFixture({LIVEFS_FEATURE_FLAG: "on",
LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
return self.factory.makeLiveFS(registrant=owner, owner=owner)
+
+
+class TestWebhookTargetOCIRecipe(TestWebhookTargetBase, TestCaseWithFactory):
+
+ event_type = 'oci-recipe:build:0.1'
+
+ def makeTarget(self):
+ owner = self.factory.makePerson()
+ with FeatureFixture({OCI_RECIPE_WEBHOOKS_FEATURE_FLAG: "on"}):
+ return self.factory.makeOCIRecipe(registrant=owner, owner=owner)
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 6a65466..075eb87 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -4992,7 +4992,12 @@ class BareLaunchpadObjectFactory(ObjectFactory):
if requester is None:
requester = self.makePerson()
if distro_arch_series is None:
- distroseries = self.makeDistroSeries(status=SeriesStatus.CURRENT)
+ if recipe is not None:
+ distribution = recipe.oci_project.distribution
+ else:
+ distribution = None
+ distroseries = self.makeDistroSeries(
+ distribution=distribution, status=SeriesStatus.CURRENT)
processor = getUtility(IProcessorSet).getByName("386")
distro_arch_series = self.makeDistroArchSeries(
distroseries=distroseries, architecturetag="i386",