launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19901
[Merge] lp:~cjwatson/launchpad/snap-webhooks into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-webhooks into lp:launchpad.
Commit message:
Add webhook support for snap packages.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1535826 in Launchpad itself: "Add webhook support for snaps"
https://bugs.launchpad.net/launchpad/+bug/1535826
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-webhooks/+merge/283193
Add webhook support for snap packages.
This currently only supports notifying about build status changes, using a webhook called "snap:build:0.1". I thought about notifying only terminal statuses, but on reflection a third-party service making snap build requests might well want to show an indication of a build being in progress, so this just notifies everything and the far end can sort out what it wants.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-webhooks into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2015-11-21 11:11:50 +0000
+++ database/schema/security.cfg 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
#
# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
@@ -1003,6 +1003,8 @@
public.teamparticipation = SELECT
public.translationimportqueueentry = SELECT, INSERT, UPDATE
public.translationtemplatesbuild = SELECT, INSERT, UPDATE
+public.webhook = SELECT
+public.webhookjob = SELECT, INSERT
type=user
[buildd_manager]
@@ -1429,6 +1431,8 @@
public.teamparticipation = SELECT, INSERT
public.validpersoncache = SELECT
public.validpersonorteamcache = SELECT
+public.webhook = SELECT
+public.webhookjob = SELECT, INSERT
public.wikiname = SELECT, INSERT
public.xref = SELECT, INSERT
type=group
=== modified file 'lib/lp/services/webhooks/interfaces.py'
--- lib/lp/services/webhooks/interfaces.py 2015-10-14 16:03:01 +0000
+++ lib/lp/services/webhooks/interfaces.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 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 @@
"bzr:push:0.1": "Bazaar push",
"git:push:0.1": "Git push",
"merge-proposal:0.1": "Merge proposal",
+ "snap:build:0.1": "Snap build",
}
=== modified file 'lib/lp/services/webhooks/model.py'
--- lib/lp/services/webhooks/model.py 2015-11-11 18:12:24 +0000
+++ lib/lp/services/webhooks/model.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -102,6 +102,9 @@
branch_id = Int(name='branch')
branch = Reference(branch_id, 'Branch.id')
+ snap_id = Int(name='snap')
+ snap = Reference(snap_id, 'Snap.id')
+
registrant_id = Int(name='registrant', allow_none=False)
registrant = Reference(registrant_id, 'Person.id')
date_created = DateTime(tzinfo=utc, allow_none=False)
@@ -119,6 +122,8 @@
return self.git_repository
elif self.branch is not None:
return self.branch
+ elif self.snap is not None:
+ return self.snap
else:
raise AssertionError("No target.")
@@ -172,11 +177,14 @@
secret):
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
+ from lp.snappy.interfaces.snap import ISnap
hook = Webhook()
if IGitRepository.providedBy(target):
hook.git_repository = target
elif IBranch.providedBy(target):
hook.branch = target
+ elif ISnap.providedBy(target):
+ hook.snap = target
else:
raise AssertionError("Unsupported target: %r" % (target,))
hook.registrant = registrant
@@ -200,10 +208,13 @@
def findByTarget(self, target):
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.gitrepository import IGitRepository
+ from lp.snappy.interfaces.snap import ISnap
if IGitRepository.providedBy(target):
target_filter = Webhook.git_repository == target
elif IBranch.providedBy(target):
target_filter = Webhook.branch == target
+ elif ISnap.providedBy(target):
+ target_filter = Webhook.snap == target
else:
raise AssertionError("Unsupported target: %r" % (target,))
return IStore(Webhook).find(Webhook, target_filter).order_by(
=== modified file 'lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt'
--- lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2015-09-24 13:44:02 +0000
+++ lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt 2016-01-19 17:45:04 +0000
@@ -18,8 +18,8 @@
<div>
<div class="beta" style="display: inline">
<img class="beta" alt="[BETA]" src="/@@/beta" /></div>
- The only currently supported events are Git and Bazaar pushes. We'll
- be rolling out webhooks for more soon.
+ The only currently supported events are Git and Bazaar pushes and
+ snap package builds. We'll be rolling out webhooks for more soon.
</div>
<ul class="horizontal">
<li>
=== modified file 'lib/lp/services/webhooks/tests/test_browser.py'
--- lib/lp/services/webhooks/tests/test_browser.py 2015-10-26 11:47:38 +0000
+++ lib/lp/services/webhooks/tests/test_browser.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Unit tests for Webhook views."""
@@ -17,6 +17,7 @@
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.publisher import canonical_url
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
from lp.testing import (
login_person,
record_two_runs,
@@ -59,6 +60,9 @@
def makeTarget(self):
return self.factory.makeGitRepository()
+ def getTraversalStack(self, obj):
+ return [obj.target, obj]
+
class BranchTestHelpers:
@@ -71,6 +75,28 @@
def makeTarget(self):
return self.factory.makeBranch()
+ def getTraversalStack(self, obj):
+ return [obj.target, obj]
+
+
+class SnapTestHelpers:
+
+ event_type = "snap:build:0.1"
+ expected_event_types = [
+ ("snap:build:0.1", "Snap build"),
+ ]
+
+ def makeTarget(self):
+ self.useFixture(FeatureFixture({
+ SNAP_FEATURE_FLAG: 'true',
+ 'webhooks.new.enabled': 'true',
+ }))
+ owner = self.factory.makePerson()
+ return self.factory.makeSnap(registrant=owner, owner=owner)
+
+ def getTraversalStack(self, obj):
+ return [obj]
+
class WebhookTargetViewTestHelpers:
@@ -84,8 +110,8 @@
def makeView(self, name, **kwargs):
view = create_view(self.target, name, principal=self.owner, **kwargs)
# To test the breadcrumbs we need a correct traversal stack.
- view.request.traversed_objects = [
- self.target.target, self.target, view]
+ view.request.traversed_objects = (
+ self.getTraversalStack(self.target) + [view])
view.initialize()
return view
@@ -162,6 +188,12 @@
pass
+class TestWebhooksViewSnap(
+ TestWebhooksViewBase, SnapTestHelpers, TestCaseWithFactory):
+
+ pass
+
+
class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
layer = DatabaseFunctionalLayer
@@ -254,6 +286,12 @@
pass
+class TestWebhookAddViewSnap(
+ TestWebhookAddViewBase, SnapTestHelpers, TestCaseWithFactory):
+
+ pass
+
+
class WebhookViewTestHelpers:
def setUp(self):
@@ -268,8 +306,8 @@
def makeView(self, name, **kwargs):
view = create_view(self.webhook, name, principal=self.owner, **kwargs)
# To test the breadcrumbs we need a correct traversal stack.
- view.request.traversed_objects = [
- self.target.target, self.target, self.webhook, view]
+ view.request.traversed_objects = (
+ self.getTraversalStack(self.target) + [self.webhook, view])
view.initialize()
return view
@@ -350,6 +388,12 @@
pass
+class TestWebhookViewSnap(
+ TestWebhookViewBase, SnapTestHelpers, TestCaseWithFactory):
+
+ pass
+
+
class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
layer = DatabaseFunctionalLayer
@@ -394,3 +438,9 @@
TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory):
pass
+
+
+class TestWebhookDeleteViewSnap(
+ TestWebhookDeleteViewBase, SnapTestHelpers, TestCaseWithFactory):
+
+ pass
=== modified file 'lib/lp/services/webhooks/tests/test_model.py'
--- lib/lp/services/webhooks/tests/test_model.py 2015-10-14 16:03:01 +0000
+++ lib/lp/services/webhooks/tests/test_model.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
from lazr.lifecycle.event import ObjectModifiedEvent
@@ -17,6 +17,7 @@
from lp.app.enums import InformationType
from lp.registry.enums import BranchSharingPolicy
from lp.services.database.interfaces import IStore
+from lp.services.features.testing import FeatureFixture
from lp.services.webapp.authorization import check_permission
from lp.services.webhooks.interfaces import (
IWebhook,
@@ -26,6 +27,7 @@
WebhookJob,
WebhookSet,
)
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
from lp.testing import (
admin_logged_in,
anonymous_logged_in,
@@ -204,6 +206,45 @@
login_person(target.owner)
self.assertTrue(WebhookSet._checkVisibility(target, target.owner))
+ def test_trigger(self):
+ owner = self.factory.makePerson()
+ target1 = self.makeTarget(owner=owner)
+ target2 = self.makeTarget(owner=owner)
+ hook1a = self.factory.makeWebhook(
+ target=target1, event_types=[])
+ hook1b = self.factory.makeWebhook(
+ target=target1, event_types=[self.event_type])
+ hook2a = self.factory.makeWebhook(
+ target=target2, event_types=[self.event_type])
+ hook2b = self.factory.makeWebhook(
+ target=target2, event_types=[self.event_type], active=False)
+
+ # Only webhooks subscribed to the relevant target and event type
+ # are triggered.
+ getUtility(IWebhookSet).trigger(
+ target1, self.event_type, {'some': 'payload'})
+ with admin_logged_in():
+ self.assertThat(list(hook1a.deliveries), HasLength(0))
+ self.assertThat(list(hook1b.deliveries), HasLength(1))
+ self.assertThat(list(hook2a.deliveries), HasLength(0))
+ self.assertThat(list(hook2b.deliveries), HasLength(0))
+ delivery = hook1b.deliveries.one()
+ self.assertEqual(delivery.payload, {'some': 'payload'})
+
+ # Disabled webhooks aren't triggered.
+ getUtility(IWebhookSet).trigger(
+ target2, self.event_type, {'other': 'payload'})
+ with admin_logged_in():
+ self.assertThat(list(hook1a.deliveries), HasLength(0))
+ self.assertThat(list(hook1b.deliveries), HasLength(1))
+ self.assertThat(list(hook2a.deliveries), HasLength(1))
+ self.assertThat(list(hook2b.deliveries), HasLength(0))
+ delivery = hook2a.deliveries.one()
+ self.assertEqual(delivery.payload, {'other': 'payload'})
+
+
+class TestWebhookSetMergeProposalBase(TestWebhookSetBase):
+
def test__checkVisibility_private_artifact(self):
owner = self.factory.makePerson()
target = self.makeTarget(
@@ -248,42 +289,6 @@
self.assertFalse(
WebhookSet._checkVisibility(mp2, mp2.merge_target.owner))
- def test_trigger(self):
- owner = self.factory.makePerson()
- target1 = self.makeTarget(owner=owner)
- target2 = self.makeTarget(owner=owner)
- hook1a = self.factory.makeWebhook(
- target=target1, event_types=[])
- hook1b = self.factory.makeWebhook(
- target=target1, event_types=[self.event_type])
- hook2a = self.factory.makeWebhook(
- target=target2, event_types=[self.event_type])
- hook2b = self.factory.makeWebhook(
- target=target2, event_types=[self.event_type], active=False)
-
- # Only webhooks subscribed to the relevant target and event type
- # are triggered.
- getUtility(IWebhookSet).trigger(
- target1, self.event_type, {'some': 'payload'})
- with admin_logged_in():
- self.assertThat(list(hook1a.deliveries), HasLength(0))
- self.assertThat(list(hook1b.deliveries), HasLength(1))
- self.assertThat(list(hook2a.deliveries), HasLength(0))
- self.assertThat(list(hook2b.deliveries), HasLength(0))
- delivery = hook1b.deliveries.one()
- self.assertEqual(delivery.payload, {'some': 'payload'})
-
- # Disabled webhooks aren't triggered.
- getUtility(IWebhookSet).trigger(
- target2, self.event_type, {'other': 'payload'})
- with admin_logged_in():
- self.assertThat(list(hook1a.deliveries), HasLength(0))
- self.assertThat(list(hook1b.deliveries), HasLength(1))
- self.assertThat(list(hook2a.deliveries), HasLength(1))
- self.assertThat(list(hook2b.deliveries), HasLength(0))
- delivery = hook2a.deliveries.one()
- self.assertEqual(delivery.payload, {'other': 'payload'})
-
def test_trigger_skips_invisible(self):
# No webhooks are dispatched if the visibility check fails.
project = self.factory.makeProduct(
@@ -344,7 +349,8 @@
self.assertEqual(delivery.payload, {'some': 'payload'})
-class TestWebhookSetGitRepository(TestWebhookSetBase, TestCaseWithFactory):
+class TestWebhookSetGitRepository(
+ TestWebhookSetMergeProposalBase, TestCaseWithFactory):
event_type = 'git:push:0.1'
@@ -366,7 +372,8 @@
reviewer=reviewer)
-class TestWebhookSetBranch(TestWebhookSetBase, TestCaseWithFactory):
+class TestWebhookSetBranch(
+ TestWebhookSetMergeProposalBase, TestCaseWithFactory):
event_type = 'bzr:push:0.1'
@@ -384,3 +391,14 @@
return self.factory.makeBranchMergeProposal(
registrant=owner, target_branch=target, source_branch=source,
reviewer=reviewer)
+
+
+class TestWebhookSetSnap(TestWebhookSetBase, TestCaseWithFactory):
+
+ event_type = 'snap:build:0.1'
+
+ def makeTarget(self, owner=None, **kwargs):
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: 'true'}))
+ if owner is None:
+ owner = self.factory.makePerson()
+ return self.factory.makeSnap(registrant=owner, owner=owner, **kwargs)
=== modified file 'lib/lp/services/webhooks/tests/test_webservice.py'
--- lib/lp/services/webhooks/tests/test_webservice.py 2015-10-13 16:58:20 +0000
+++ lib/lp/services/webhooks/tests/test_webservice.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 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."""
@@ -23,6 +23,7 @@
from lp.services.features.testing import FeatureFixture
from lp.services.webapp.interfaces import OAuthPermission
+from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
from lp.testing import (
api_url,
person_logged_in,
@@ -370,3 +371,13 @@
def makeTarget(self):
return self.factory.makeBranch()
+
+
+class TestWebhookTargetSnap(TestWebhookTargetBase, TestCaseWithFactory):
+
+ event_type = 'snap:build:0.1'
+
+ def makeTarget(self):
+ self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: 'true'}))
+ owner = self.factory.makePerson()
+ return self.factory.makeSnap(registrant=owner, owner=owner)
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2015-10-08 13:58:57 +0000
+++ lib/lp/snappy/browser/snap.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Snap views."""
@@ -63,6 +63,7 @@
Breadcrumb,
NameBreadcrumb,
)
+from lp.services.webhooks.browser import WebhookTargetNavigationMixin
from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget
from lp.snappy.interfaces.snap import (
ISnap,
@@ -78,7 +79,7 @@
from lp.soyuz.interfaces.archive import IArchive
-class SnapNavigation(Navigation):
+class SnapNavigation(WebhookTargetNavigationMixin, Navigation):
usedfor = ISnap
@stepthrough('+build')
@@ -106,7 +107,7 @@
facet = 'overview'
- links = ('edit', 'delete', 'admin')
+ links = ('admin', 'edit', 'webhooks', 'delete')
@enabled_with_permission('launchpad.Admin')
def admin(self):
@@ -117,6 +118,12 @@
return Link('+edit', 'Edit snap package', icon='edit')
@enabled_with_permission('launchpad.Edit')
+ def webhooks(self):
+ return Link(
+ '+webhooks', 'Manage webhooks', icon='edit',
+ enabled=bool(getFeatureFlag('webhooks.new.enabled')))
+
+ @enabled_with_permission('launchpad.Edit')
def delete(self):
return Link('+delete', 'Delete snap package', icon='trash-icon')
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2015-09-25 17:26:03 +0000
+++ lib/lp/snappy/configure.zcml 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2015 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -52,6 +52,10 @@
permission="launchpad.Admin"
interface="lp.snappy.interfaces.snapbuild.ISnapBuildAdmin" />
</class>
+ <subscriber
+ for="lp.snappy.interfaces.snapbuild.ISnapBuild
+ lp.snappy.interfaces.snapbuild.ISnapBuildStatusChangedEvent"
+ handler="lp.snappy.subscribers.snapbuild.snap_build_status_changed" />
<!-- SnapBuildSet -->
<securedutility
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2015-09-25 17:26:03 +0000
+++ lib/lp/snappy/interfaces/snap.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Snap package interfaces."""
@@ -16,6 +16,7 @@
'NoSourceForSnap',
'NoSuchSnap',
'SNAP_FEATURE_FLAG',
+ 'SNAP_WEBHOOKS_FEATURE_FLAG',
'SnapBuildAlreadyPending',
'SnapBuildArchiveOwnerMismatch',
'SnapBuildDisallowedArchitecture',
@@ -80,11 +81,13 @@
PersonChoice,
PublicPersonChoice,
)
+from lp.services.webhooks.interfaces import IWebhookTarget
from lp.soyuz.interfaces.archive import IArchive
from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
SNAP_FEATURE_FLAG = u"snap.allow_new"
+SNAP_WEBHOOKS_FEATURE_FLAG = u"snap.webhooks.enabled"
@error_status(httplib.BAD_REQUEST)
@@ -263,7 +266,7 @@
value_type=Reference(schema=Interface), readonly=True)))
-class ISnapEdit(Interface):
+class ISnapEdit(IWebhookTarget):
"""`ISnap` methods that require launchpad.Edit permission."""
@export_destructor_operation()
=== modified file 'lib/lp/snappy/interfaces/snapbuild.py'
--- lib/lp/snappy/interfaces/snapbuild.py 2015-07-23 16:41:12 +0000
+++ lib/lp/snappy/interfaces/snapbuild.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Snap package build interfaces."""
@@ -8,6 +8,7 @@
__all__ = [
'ISnapBuild',
'ISnapBuildSet',
+ 'ISnapBuildStatusChangedEvent',
'ISnapFile',
]
@@ -20,6 +21,7 @@
operation_parameters,
)
from lazr.restful.fields import Reference
+from zope.component.interfaces import IObjectEvent
from zope.interface import Interface
from zope.schema import (
Bool,
@@ -39,6 +41,10 @@
from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
+class ISnapBuildStatusChangedEvent(IObjectEvent):
+ """The status of a snap package build changed."""
+
+
class ISnapFile(Interface):
"""A file produced by a snap package build."""
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2015-10-08 14:18:29 +0000
+++ lib/lp/snappy/model/snap.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -69,6 +69,8 @@
)
from lp.services.features import getFeatureFlag
from lp.services.webapp.interfaces import ILaunchBag
+from lp.services.webhooks.interfaces import IWebhookSet
+from lp.services.webhooks.model import WebhookTargetMixin
from lp.snappy.interfaces.snap import (
BadSnapSearchContext,
CannotDeleteSnap,
@@ -105,7 +107,7 @@
@implementer(ISnap, IHasOwner)
-class Snap(Storm):
+class Snap(Storm, WebhookTargetMixin):
"""See `ISnap`."""
__storm_table__ = 'Snap'
@@ -160,6 +162,10 @@
self.date_last_modified = date_created
@property
+ def valid_webhook_event_types(self):
+ return ["snap:build:0.1"]
+
+ @property
def git_ref(self):
"""See `ISnap`."""
if self.git_repository is not None:
@@ -344,6 +350,7 @@
raise CannotDeleteSnap("Cannot delete a snap package with builds.")
store = IStore(Snap)
store.find(SnapArch, SnapArch.snap == self).remove()
+ getUtility(IWebhookSet).delete(self.webhooks)
store.remove(self)
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2015-09-16 13:30:33 +0000
+++ lib/lp/snappy/model/snapbuild.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
__metaclass__ = type
@@ -22,6 +22,8 @@
)
from storm.store import EmptyResultSet
from zope.component import getUtility
+from zope.component.interfaces import ObjectEvent
+from zope.event import notify
from zope.interface import implementer
from lp.app.errors import NotFoundError
@@ -59,6 +61,7 @@
from lp.snappy.interfaces.snapbuild import (
ISnapBuild,
ISnapBuildSet,
+ ISnapBuildStatusChangedEvent,
ISnapFile,
)
from lp.snappy.mail.snapbuild import SnapBuildMailer
@@ -67,6 +70,11 @@
from lp.soyuz.model.distroarchseries import DistroArchSeries
+@implementer(ISnapBuildStatusChangedEvent)
+class SnapBuildStatusChangedEvent(ObjectEvent):
+ """See `ISnapBuildStatusChangedEvent`."""
+
+
@implementer(ISnapFile)
class SnapFile(Storm):
"""See `ISnap`."""
@@ -296,6 +304,16 @@
"""See `IPackageBuild`."""
return not self.getFiles().is_empty()
+ def updateStatus(self, status, builder=None, slave_status=None,
+ date_started=None, date_finished=None,
+ force_invalid_transition=False):
+ """See `IBuildFarmJob`."""
+ super(SnapBuild, self).updateStatus(
+ status, builder=builder, slave_status=slave_status,
+ date_started=date_started, date_finished=date_finished,
+ force_invalid_transition=force_invalid_transition)
+ notify(SnapBuildStatusChangedEvent(self))
+
def notify(self, extra_info=None):
"""See `IPackageBuild`."""
if not config.builddmaster.send_build_notification:
=== added directory 'lib/lp/snappy/subscribers'
=== added file 'lib/lp/snappy/subscribers/__init__.py'
=== added file 'lib/lp/snappy/subscribers/snapbuild.py'
--- lib/lp/snappy/subscribers/snapbuild.py 1970-01-01 00:00:00 +0000
+++ lib/lp/snappy/subscribers/snapbuild.py 2016-01-19 17:45:04 +0000
@@ -0,0 +1,30 @@
+# Copyright 2016 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Event subscribers for snap builds."""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+__metaclass__ = type
+
+from zope.component import getUtility
+
+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
+from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG
+from lp.snappy.interfaces.snapbuild import ISnapBuild
+
+
+def snap_build_status_changed(snapbuild, event):
+ """Trigger webhooks when snap package build statuses change."""
+ if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG):
+ payload = {
+ "snap_build": canonical_url(snapbuild, force_local_path=True),
+ "action": "status-changed",
+ }
+ payload.update(compose_webhook_payload(
+ ISnapBuild, snapbuild, ["snap", "status"]))
+ getUtility(IWebhookSet).trigger(
+ snapbuild.snap, "snap:build:0.1", payload)
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2015-11-26 15:46:38 +0000
+++ lib/lp/snappy/tests/test_snap.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Test snap packages."""
@@ -8,6 +8,7 @@
from datetime import timedelta
from lazr.lifecycle.event import ObjectModifiedEvent
+from storm.exceptions import LostObjectError
from storm.locals import Store
from testtools.matchers import Equals
import transaction
@@ -354,6 +355,16 @@
self.assertRaises(CannotDeleteSnap, snap.destroySelf)
self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
+ def test_related_webhooks_deleted(self):
+ owner = self.factory.makePerson()
+ snap = self.factory.makeSnap(registrant=owner, owner=owner)
+ webhook = self.factory.makeWebhook(target=snap)
+ with person_logged_in(snap.owner):
+ webhook.ping()
+ snap.destroySelf()
+ transaction.commit()
+ self.assertRaises(LostObjectError, getattr, webhook, "target")
+
class TestSnapSet(TestCaseWithFactory):
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2015-09-11 12:20:23 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2016-01-19 17:45:04 +0000
@@ -1,4 +1,4 @@
-# Copyright 2015 Canonical Ltd. This software is licensed under the
+# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Test snap package build features."""
@@ -15,6 +15,11 @@
)
import pytz
+from testtools.matchers import (
+ Equals,
+ MatchesDict,
+ MatchesStructure,
+ )
from zope.component import getUtility
from zope.security.proxy import removeSecurityProxy
@@ -29,8 +34,10 @@
from lp.services.features.testing import FeatureFixture
from lp.services.librarian.browser import ProxiedLibraryFileAlias
from lp.services.webapp.interfaces import OAuthPermission
+from lp.services.webapp.publisher import canonical_url
from lp.snappy.interfaces.snap import (
SNAP_FEATURE_FLAG,
+ SNAP_WEBHOOKS_FEATURE_FLAG,
SnapFeatureDisabled,
)
from lp.snappy.interfaces.snapbuild import (
@@ -217,6 +224,26 @@
self.factory.makeSnapFile(snapbuild=self.build)
self.assertTrue(self.build.verifySuccessfulUpload())
+ def test_updateStatus_triggers_webhooks(self):
+ # Updating the status of a SnapBuild triggers webhooks on the
+ # corresponding Snap.
+ self.useFixture(FeatureFixture({SNAP_WEBHOOKS_FEATURE_FLAG: u"on"}))
+ hook = self.factory.makeWebhook(
+ target=self.build.snap, event_types=["snap:build:0.1"])
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ expected_payload = {
+ "snap_build": Equals(
+ canonical_url(self.build, force_local_path=True)),
+ "action": Equals("status-changed"),
+ "snap": Equals(
+ canonical_url(self.build.snap, force_local_path=True)),
+ "status": Equals("Successfully built"),
+ }
+ self.assertThat(
+ hook.deliveries.one(), MatchesStructure(
+ event_type=Equals("snap:build:0.1"),
+ payload=MatchesDict(expected_payload)))
+
def test_notify_fullybuilt(self):
# notify does not send mail when a SnapBuild completes normally.
person = self.factory.makePerson(name="person")
Follow ups