Merge lp:~cjwatson/launchpad/snap-webhooks into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 17929
Proposed branch: lp:~cjwatson/launchpad/snap-webhooks
Merge into: lp:launchpad
Diff against target: 810 lines (+269/-62)
16 files modified
database/schema/security.cfg (+5/-1)
lib/lp/services/webhooks/interfaces.py (+2/-1)
lib/lp/services/webhooks/model.py (+12/-1)
lib/lp/services/webhooks/templates/webhooktarget-webhooks.pt (+2/-2)
lib/lp/services/webhooks/tests/test_browser.py (+55/-5)
lib/lp/services/webhooks/tests/test_model.py (+57/-39)
lib/lp/services/webhooks/tests/test_webservice.py (+12/-1)
lib/lp/snappy/browser/snap.py (+10/-3)
lib/lp/snappy/configure.zcml (+5/-1)
lib/lp/snappy/interfaces/snap.py (+7/-3)
lib/lp/snappy/interfaces/snapbuild.py (+7/-1)
lib/lp/snappy/model/snap.py (+9/-2)
lib/lp/snappy/model/snapbuild.py (+19/-1)
lib/lp/snappy/subscribers/snapbuild.py (+30/-0)
lib/lp/snappy/tests/test_snap.py (+12/-1)
lib/lp/snappy/tests/test_snapbuild.py (+25/-0)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-webhooks
Reviewer Review Type Date Requested Status
Celso Providelo (community) Approve
Launchpad code reviewers Pending
Review via email: mp+283193@code.launchpad.net

Commit message

Add webhook support for snap packages.

Description of the change

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.

To post a comment you must log in.
Revision history for this message
Celso Providelo (cprov) wrote :

Thanks, Colin.

Regarding the DB security changes, do we need to deploy them specially or are they applied for every devel revision on-the-fly ?

The new webhook payload looks complete (action, snap_url, snap_build_url, status) and useful for API operations.

Obviously I'm missing tons of important details, but the drive-by refactoring make sense and testing looks sufficient.

[]

review: Approve
Revision history for this message
Colin Watson (cjwatson) wrote :

DB security changes are applied automatically, indeed.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2015-11-21 11:11:50 +0000
+++ database/schema/security.cfg 2016-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2009-2015 Canonical Ltd. This software is licensed under the1# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3#3#
4# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE4# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
@@ -1003,6 +1003,8 @@
1003public.teamparticipation = SELECT1003public.teamparticipation = SELECT
1004public.translationimportqueueentry = SELECT, INSERT, UPDATE1004public.translationimportqueueentry = SELECT, INSERT, UPDATE
1005public.translationtemplatesbuild = SELECT, INSERT, UPDATE1005public.translationtemplatesbuild = SELECT, INSERT, UPDATE
1006public.webhook = SELECT
1007public.webhookjob = SELECT, INSERT
1006type=user1008type=user
10071009
1008[buildd_manager]1010[buildd_manager]
@@ -1429,6 +1431,8 @@
1429public.teamparticipation = SELECT, INSERT1431public.teamparticipation = SELECT, INSERT
1430public.validpersoncache = SELECT1432public.validpersoncache = SELECT
1431public.validpersonorteamcache = SELECT1433public.validpersonorteamcache = SELECT
1434public.webhook = SELECT
1435public.webhookjob = SELECT, INSERT
1432public.wikiname = SELECT, INSERT1436public.wikiname = SELECT, INSERT
1433public.xref = SELECT, INSERT1437public.xref = SELECT, INSERT
1434type=group1438type=group
14351439
=== 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-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Webhook interfaces."""4"""Webhook interfaces."""
@@ -76,6 +76,7 @@
76 "bzr:push:0.1": "Bazaar push",76 "bzr:push:0.1": "Bazaar push",
77 "git:push:0.1": "Git push",77 "git:push:0.1": "Git push",
78 "merge-proposal:0.1": "Merge proposal",78 "merge-proposal:0.1": "Merge proposal",
79 "snap:build:0.1": "Snap build",
79 }80 }
8081
8182
8283
=== 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-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -102,6 +102,9 @@
102 branch_id = Int(name='branch')102 branch_id = Int(name='branch')
103 branch = Reference(branch_id, 'Branch.id')103 branch = Reference(branch_id, 'Branch.id')
104104
105 snap_id = Int(name='snap')
106 snap = Reference(snap_id, 'Snap.id')
107
105 registrant_id = Int(name='registrant', allow_none=False)108 registrant_id = Int(name='registrant', allow_none=False)
106 registrant = Reference(registrant_id, 'Person.id')109 registrant = Reference(registrant_id, 'Person.id')
107 date_created = DateTime(tzinfo=utc, allow_none=False)110 date_created = DateTime(tzinfo=utc, allow_none=False)
@@ -119,6 +122,8 @@
119 return self.git_repository122 return self.git_repository
120 elif self.branch is not None:123 elif self.branch is not None:
121 return self.branch124 return self.branch
125 elif self.snap is not None:
126 return self.snap
122 else:127 else:
123 raise AssertionError("No target.")128 raise AssertionError("No target.")
124129
@@ -172,11 +177,14 @@
172 secret):177 secret):
173 from lp.code.interfaces.branch import IBranch178 from lp.code.interfaces.branch import IBranch
174 from lp.code.interfaces.gitrepository import IGitRepository179 from lp.code.interfaces.gitrepository import IGitRepository
180 from lp.snappy.interfaces.snap import ISnap
175 hook = Webhook()181 hook = Webhook()
176 if IGitRepository.providedBy(target):182 if IGitRepository.providedBy(target):
177 hook.git_repository = target183 hook.git_repository = target
178 elif IBranch.providedBy(target):184 elif IBranch.providedBy(target):
179 hook.branch = target185 hook.branch = target
186 elif ISnap.providedBy(target):
187 hook.snap = target
180 else:188 else:
181 raise AssertionError("Unsupported target: %r" % (target,))189 raise AssertionError("Unsupported target: %r" % (target,))
182 hook.registrant = registrant190 hook.registrant = registrant
@@ -200,10 +208,13 @@
200 def findByTarget(self, target):208 def findByTarget(self, target):
201 from lp.code.interfaces.branch import IBranch209 from lp.code.interfaces.branch import IBranch
202 from lp.code.interfaces.gitrepository import IGitRepository210 from lp.code.interfaces.gitrepository import IGitRepository
211 from lp.snappy.interfaces.snap import ISnap
203 if IGitRepository.providedBy(target):212 if IGitRepository.providedBy(target):
204 target_filter = Webhook.git_repository == target213 target_filter = Webhook.git_repository == target
205 elif IBranch.providedBy(target):214 elif IBranch.providedBy(target):
206 target_filter = Webhook.branch == target215 target_filter = Webhook.branch == target
216 elif ISnap.providedBy(target):
217 target_filter = Webhook.snap == target
207 else:218 else:
208 raise AssertionError("Unsupported target: %r" % (target,))219 raise AssertionError("Unsupported target: %r" % (target,))
209 return IStore(Webhook).find(Webhook, target_filter).order_by(220 return IStore(Webhook).find(Webhook, target_filter).order_by(
210221
=== 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-02-19 14:20:42 +0000
@@ -18,8 +18,8 @@
18 <div>18 <div>
19 <div class="beta" style="display: inline">19 <div class="beta" style="display: inline">
20 <img class="beta" alt="[BETA]" src="/@@/beta" /></div>20 <img class="beta" alt="[BETA]" src="/@@/beta" /></div>
21 The only currently supported events are Git and Bazaar pushes. We'll21 The only currently supported events are Git and Bazaar pushes and
22 be rolling out webhooks for more soon.22 snap package builds. We'll be rolling out webhooks for more soon.
23 </div>23 </div>
24 <ul class="horizontal">24 <ul class="horizontal">
25 <li>25 <li>
2626
=== 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-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Unit tests for Webhook views."""4"""Unit tests for Webhook views."""
@@ -17,6 +17,7 @@
1717
18from lp.services.features.testing import FeatureFixture18from lp.services.features.testing import FeatureFixture
19from lp.services.webapp.publisher import canonical_url19from lp.services.webapp.publisher import canonical_url
20from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
20from lp.testing import (21from lp.testing import (
21 login_person,22 login_person,
22 record_two_runs,23 record_two_runs,
@@ -59,6 +60,9 @@
59 def makeTarget(self):60 def makeTarget(self):
60 return self.factory.makeGitRepository()61 return self.factory.makeGitRepository()
6162
63 def getTraversalStack(self, obj):
64 return [obj.target, obj]
65
6266
63class BranchTestHelpers:67class BranchTestHelpers:
6468
@@ -71,6 +75,28 @@
71 def makeTarget(self):75 def makeTarget(self):
72 return self.factory.makeBranch()76 return self.factory.makeBranch()
7377
78 def getTraversalStack(self, obj):
79 return [obj.target, obj]
80
81
82class SnapTestHelpers:
83
84 event_type = "snap:build:0.1"
85 expected_event_types = [
86 ("snap:build:0.1", "Snap build"),
87 ]
88
89 def makeTarget(self):
90 self.useFixture(FeatureFixture({
91 SNAP_FEATURE_FLAG: 'true',
92 'webhooks.new.enabled': 'true',
93 }))
94 owner = self.factory.makePerson()
95 return self.factory.makeSnap(registrant=owner, owner=owner)
96
97 def getTraversalStack(self, obj):
98 return [obj]
99
74100
75class WebhookTargetViewTestHelpers:101class WebhookTargetViewTestHelpers:
76102
@@ -84,8 +110,8 @@
84 def makeView(self, name, **kwargs):110 def makeView(self, name, **kwargs):
85 view = create_view(self.target, name, principal=self.owner, **kwargs)111 view = create_view(self.target, name, principal=self.owner, **kwargs)
86 # To test the breadcrumbs we need a correct traversal stack.112 # To test the breadcrumbs we need a correct traversal stack.
87 view.request.traversed_objects = [113 view.request.traversed_objects = (
88 self.target.target, self.target, view]114 self.getTraversalStack(self.target) + [view])
89 view.initialize()115 view.initialize()
90 return view116 return view
91117
@@ -162,6 +188,12 @@
162 pass188 pass
163189
164190
191class TestWebhooksViewSnap(
192 TestWebhooksViewBase, SnapTestHelpers, TestCaseWithFactory):
193
194 pass
195
196
165class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):197class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
166198
167 layer = DatabaseFunctionalLayer199 layer = DatabaseFunctionalLayer
@@ -254,6 +286,12 @@
254 pass286 pass
255287
256288
289class TestWebhookAddViewSnap(
290 TestWebhookAddViewBase, SnapTestHelpers, TestCaseWithFactory):
291
292 pass
293
294
257class WebhookViewTestHelpers:295class WebhookViewTestHelpers:
258296
259 def setUp(self):297 def setUp(self):
@@ -268,8 +306,8 @@
268 def makeView(self, name, **kwargs):306 def makeView(self, name, **kwargs):
269 view = create_view(self.webhook, name, principal=self.owner, **kwargs)307 view = create_view(self.webhook, name, principal=self.owner, **kwargs)
270 # To test the breadcrumbs we need a correct traversal stack.308 # To test the breadcrumbs we need a correct traversal stack.
271 view.request.traversed_objects = [309 view.request.traversed_objects = (
272 self.target.target, self.target, self.webhook, view]310 self.getTraversalStack(self.target) + [self.webhook, view])
273 view.initialize()311 view.initialize()
274 return view312 return view
275313
@@ -350,6 +388,12 @@
350 pass388 pass
351389
352390
391class TestWebhookViewSnap(
392 TestWebhookViewBase, SnapTestHelpers, TestCaseWithFactory):
393
394 pass
395
396
353class TestWebhookDeleteViewBase(WebhookViewTestHelpers):397class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
354398
355 layer = DatabaseFunctionalLayer399 layer = DatabaseFunctionalLayer
@@ -394,3 +438,9 @@
394 TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory):438 TestWebhookDeleteViewBase, BranchTestHelpers, TestCaseWithFactory):
395439
396 pass440 pass
441
442
443class TestWebhookDeleteViewSnap(
444 TestWebhookDeleteViewBase, SnapTestHelpers, TestCaseWithFactory):
445
446 pass
397447
=== 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-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4from lazr.lifecycle.event import ObjectModifiedEvent4from lazr.lifecycle.event import ObjectModifiedEvent
@@ -17,6 +17,7 @@
17from lp.app.enums import InformationType17from lp.app.enums import InformationType
18from lp.registry.enums import BranchSharingPolicy18from lp.registry.enums import BranchSharingPolicy
19from lp.services.database.interfaces import IStore19from lp.services.database.interfaces import IStore
20from lp.services.features.testing import FeatureFixture
20from lp.services.webapp.authorization import check_permission21from lp.services.webapp.authorization import check_permission
21from lp.services.webhooks.interfaces import (22from lp.services.webhooks.interfaces import (
22 IWebhook,23 IWebhook,
@@ -26,6 +27,7 @@
26 WebhookJob,27 WebhookJob,
27 WebhookSet,28 WebhookSet,
28 )29 )
30from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
29from lp.testing import (31from lp.testing import (
30 admin_logged_in,32 admin_logged_in,
31 anonymous_logged_in,33 anonymous_logged_in,
@@ -204,6 +206,45 @@
204 login_person(target.owner)206 login_person(target.owner)
205 self.assertTrue(WebhookSet._checkVisibility(target, target.owner))207 self.assertTrue(WebhookSet._checkVisibility(target, target.owner))
206208
209 def test_trigger(self):
210 owner = self.factory.makePerson()
211 target1 = self.makeTarget(owner=owner)
212 target2 = self.makeTarget(owner=owner)
213 hook1a = self.factory.makeWebhook(
214 target=target1, event_types=[])
215 hook1b = self.factory.makeWebhook(
216 target=target1, event_types=[self.event_type])
217 hook2a = self.factory.makeWebhook(
218 target=target2, event_types=[self.event_type])
219 hook2b = self.factory.makeWebhook(
220 target=target2, event_types=[self.event_type], active=False)
221
222 # Only webhooks subscribed to the relevant target and event type
223 # are triggered.
224 getUtility(IWebhookSet).trigger(
225 target1, self.event_type, {'some': 'payload'})
226 with admin_logged_in():
227 self.assertThat(list(hook1a.deliveries), HasLength(0))
228 self.assertThat(list(hook1b.deliveries), HasLength(1))
229 self.assertThat(list(hook2a.deliveries), HasLength(0))
230 self.assertThat(list(hook2b.deliveries), HasLength(0))
231 delivery = hook1b.deliveries.one()
232 self.assertEqual(delivery.payload, {'some': 'payload'})
233
234 # Disabled webhooks aren't triggered.
235 getUtility(IWebhookSet).trigger(
236 target2, self.event_type, {'other': 'payload'})
237 with admin_logged_in():
238 self.assertThat(list(hook1a.deliveries), HasLength(0))
239 self.assertThat(list(hook1b.deliveries), HasLength(1))
240 self.assertThat(list(hook2a.deliveries), HasLength(1))
241 self.assertThat(list(hook2b.deliveries), HasLength(0))
242 delivery = hook2a.deliveries.one()
243 self.assertEqual(delivery.payload, {'other': 'payload'})
244
245
246class TestWebhookSetMergeProposalBase(TestWebhookSetBase):
247
207 def test__checkVisibility_private_artifact(self):248 def test__checkVisibility_private_artifact(self):
208 owner = self.factory.makePerson()249 owner = self.factory.makePerson()
209 target = self.makeTarget(250 target = self.makeTarget(
@@ -248,42 +289,6 @@
248 self.assertFalse(289 self.assertFalse(
249 WebhookSet._checkVisibility(mp2, mp2.merge_target.owner))290 WebhookSet._checkVisibility(mp2, mp2.merge_target.owner))
250291
251 def test_trigger(self):
252 owner = self.factory.makePerson()
253 target1 = self.makeTarget(owner=owner)
254 target2 = self.makeTarget(owner=owner)
255 hook1a = self.factory.makeWebhook(
256 target=target1, event_types=[])
257 hook1b = self.factory.makeWebhook(
258 target=target1, event_types=[self.event_type])
259 hook2a = self.factory.makeWebhook(
260 target=target2, event_types=[self.event_type])
261 hook2b = self.factory.makeWebhook(
262 target=target2, event_types=[self.event_type], active=False)
263
264 # Only webhooks subscribed to the relevant target and event type
265 # are triggered.
266 getUtility(IWebhookSet).trigger(
267 target1, self.event_type, {'some': 'payload'})
268 with admin_logged_in():
269 self.assertThat(list(hook1a.deliveries), HasLength(0))
270 self.assertThat(list(hook1b.deliveries), HasLength(1))
271 self.assertThat(list(hook2a.deliveries), HasLength(0))
272 self.assertThat(list(hook2b.deliveries), HasLength(0))
273 delivery = hook1b.deliveries.one()
274 self.assertEqual(delivery.payload, {'some': 'payload'})
275
276 # Disabled webhooks aren't triggered.
277 getUtility(IWebhookSet).trigger(
278 target2, self.event_type, {'other': 'payload'})
279 with admin_logged_in():
280 self.assertThat(list(hook1a.deliveries), HasLength(0))
281 self.assertThat(list(hook1b.deliveries), HasLength(1))
282 self.assertThat(list(hook2a.deliveries), HasLength(1))
283 self.assertThat(list(hook2b.deliveries), HasLength(0))
284 delivery = hook2a.deliveries.one()
285 self.assertEqual(delivery.payload, {'other': 'payload'})
286
287 def test_trigger_skips_invisible(self):292 def test_trigger_skips_invisible(self):
288 # No webhooks are dispatched if the visibility check fails.293 # No webhooks are dispatched if the visibility check fails.
289 project = self.factory.makeProduct(294 project = self.factory.makeProduct(
@@ -344,7 +349,8 @@
344 self.assertEqual(delivery.payload, {'some': 'payload'})349 self.assertEqual(delivery.payload, {'some': 'payload'})
345350
346351
347class TestWebhookSetGitRepository(TestWebhookSetBase, TestCaseWithFactory):352class TestWebhookSetGitRepository(
353 TestWebhookSetMergeProposalBase, TestCaseWithFactory):
348354
349 event_type = 'git:push:0.1'355 event_type = 'git:push:0.1'
350356
@@ -366,7 +372,8 @@
366 reviewer=reviewer)372 reviewer=reviewer)
367373
368374
369class TestWebhookSetBranch(TestWebhookSetBase, TestCaseWithFactory):375class TestWebhookSetBranch(
376 TestWebhookSetMergeProposalBase, TestCaseWithFactory):
370377
371 event_type = 'bzr:push:0.1'378 event_type = 'bzr:push:0.1'
372379
@@ -384,3 +391,14 @@
384 return self.factory.makeBranchMergeProposal(391 return self.factory.makeBranchMergeProposal(
385 registrant=owner, target_branch=target, source_branch=source,392 registrant=owner, target_branch=target, source_branch=source,
386 reviewer=reviewer)393 reviewer=reviewer)
394
395
396class TestWebhookSetSnap(TestWebhookSetBase, TestCaseWithFactory):
397
398 event_type = 'snap:build:0.1'
399
400 def makeTarget(self, owner=None, **kwargs):
401 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: 'true'}))
402 if owner is None:
403 owner = self.factory.makePerson()
404 return self.factory.makeSnap(registrant=owner, owner=owner, **kwargs)
387405
=== 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-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Tests for the webhook webservice objects."""4"""Tests for the webhook webservice objects."""
@@ -23,6 +23,7 @@
2323
24from lp.services.features.testing import FeatureFixture24from lp.services.features.testing import FeatureFixture
25from lp.services.webapp.interfaces import OAuthPermission25from lp.services.webapp.interfaces import OAuthPermission
26from lp.snappy.interfaces.snap import SNAP_FEATURE_FLAG
26from lp.testing import (27from lp.testing import (
27 api_url,28 api_url,
28 person_logged_in,29 person_logged_in,
@@ -370,3 +371,13 @@
370371
371 def makeTarget(self):372 def makeTarget(self):
372 return self.factory.makeBranch()373 return self.factory.makeBranch()
374
375
376class TestWebhookTargetSnap(TestWebhookTargetBase, TestCaseWithFactory):
377
378 event_type = 'snap:build:0.1'
379
380 def makeTarget(self):
381 self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: 'true'}))
382 owner = self.factory.makePerson()
383 return self.factory.makeSnap(registrant=owner, owner=owner)
373384
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2016-02-05 06:05:50 +0000
+++ lib/lp/snappy/browser/snap.py 2016-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Snap views."""4"""Snap views."""
@@ -65,6 +65,7 @@
65 Breadcrumb,65 Breadcrumb,
66 NameBreadcrumb,66 NameBreadcrumb,
67 )67 )
68from lp.services.webhooks.browser import WebhookTargetNavigationMixin
68from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget69from lp.snappy.browser.widgets.snaparchive import SnapArchiveWidget
69from lp.snappy.interfaces.snap import (70from lp.snappy.interfaces.snap import (
70 ISnap,71 ISnap,
@@ -82,7 +83,7 @@
82from lp.soyuz.interfaces.archive import IArchive83from lp.soyuz.interfaces.archive import IArchive
8384
8485
85class SnapNavigation(Navigation):86class SnapNavigation(WebhookTargetNavigationMixin, Navigation):
86 usedfor = ISnap87 usedfor = ISnap
8788
88 @stepthrough('+build')89 @stepthrough('+build')
@@ -110,7 +111,7 @@
110111
111 facet = 'overview'112 facet = 'overview'
112113
113 links = ('edit', 'delete', 'admin')114 links = ('admin', 'edit', 'webhooks', 'delete')
114115
115 @enabled_with_permission('launchpad.Admin')116 @enabled_with_permission('launchpad.Admin')
116 def admin(self):117 def admin(self):
@@ -121,6 +122,12 @@
121 return Link('+edit', 'Edit snap package', icon='edit')122 return Link('+edit', 'Edit snap package', icon='edit')
122123
123 @enabled_with_permission('launchpad.Edit')124 @enabled_with_permission('launchpad.Edit')
125 def webhooks(self):
126 return Link(
127 '+webhooks', 'Manage webhooks', icon='edit',
128 enabled=bool(getFeatureFlag('webhooks.new.enabled')))
129
130 @enabled_with_permission('launchpad.Edit')
124 def delete(self):131 def delete(self):
125 return Link('+delete', 'Delete snap package', icon='trash-icon')132 return Link('+delete', 'Delete snap package', icon='trash-icon')
126133
127134
=== 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-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1<!-- Copyright 2015 Canonical Ltd. This software is licensed under the1<!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2 GNU Affero General Public License version 3 (see the file LICENSE).2 GNU Affero General Public License version 3 (see the file LICENSE).
3-->3-->
44
@@ -52,6 +52,10 @@
52 permission="launchpad.Admin"52 permission="launchpad.Admin"
53 interface="lp.snappy.interfaces.snapbuild.ISnapBuildAdmin" />53 interface="lp.snappy.interfaces.snapbuild.ISnapBuildAdmin" />
54 </class>54 </class>
55 <subscriber
56 for="lp.snappy.interfaces.snapbuild.ISnapBuild
57 lp.snappy.interfaces.snapbuild.ISnapBuildStatusChangedEvent"
58 handler="lp.snappy.subscribers.snapbuild.snap_build_status_changed" />
5559
56 <!-- SnapBuildSet -->60 <!-- SnapBuildSet -->
57 <securedutility61 <securedutility
5862
=== modified file 'lib/lp/snappy/interfaces/snap.py'
--- lib/lp/snappy/interfaces/snap.py 2016-02-05 06:05:50 +0000
+++ lib/lp/snappy/interfaces/snap.py 2016-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Snap package interfaces."""4"""Snap package interfaces."""
@@ -18,6 +18,7 @@
18 'SNAP_FEATURE_FLAG',18 'SNAP_FEATURE_FLAG',
19 'SNAP_PRIVATE_FEATURE_FLAG',19 'SNAP_PRIVATE_FEATURE_FLAG',
20 'SNAP_TESTING_FLAGS',20 'SNAP_TESTING_FLAGS',
21 'SNAP_WEBHOOKS_FEATURE_FLAG',
21 'SnapBuildAlreadyPending',22 'SnapBuildAlreadyPending',
22 'SnapBuildArchiveOwnerMismatch',23 'SnapBuildArchiveOwnerMismatch',
23 'SnapBuildDisallowedArchitecture',24 'SnapBuildDisallowedArchitecture',
@@ -85,18 +86,21 @@
85 PersonChoice,86 PersonChoice,
86 PublicPersonChoice,87 PublicPersonChoice,
87 )88 )
89from lp.services.webhooks.interfaces import IWebhookTarget
88from lp.soyuz.interfaces.archive import IArchive90from lp.soyuz.interfaces.archive import IArchive
89from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries91from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
9092
9193
92SNAP_FEATURE_FLAG = u"snap.allow_new"94SNAP_FEATURE_FLAG = u"snap.allow_new"
93SNAP_PRIVATE_FEATURE_FLAG = u"snap.allow_private"95SNAP_PRIVATE_FEATURE_FLAG = u"snap.allow_private"
96SNAP_WEBHOOKS_FEATURE_FLAG = u"snap.webhooks.enabled"
9497
9598
96SNAP_TESTING_FLAGS = {99SNAP_TESTING_FLAGS = {
97 SNAP_FEATURE_FLAG: u"on",100 SNAP_FEATURE_FLAG: u"on",
98 SNAP_PRIVATE_FEATURE_FLAG: u"on",101 SNAP_PRIVATE_FEATURE_FLAG: u"on",
99}102 SNAP_WEBHOOKS_FEATURE_FLAG: u"on",
103 }
100104
101105
102@error_status(httplib.BAD_REQUEST)106@error_status(httplib.BAD_REQUEST)
@@ -293,7 +297,7 @@
293 value_type=Reference(schema=Interface), readonly=True)))297 value_type=Reference(schema=Interface), readonly=True)))
294298
295299
296class ISnapEdit(Interface):300class ISnapEdit(IWebhookTarget):
297 """`ISnap` methods that require launchpad.Edit permission."""301 """`ISnap` methods that require launchpad.Edit permission."""
298302
299 @export_destructor_operation()303 @export_destructor_operation()
300304
=== 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-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Snap package build interfaces."""4"""Snap package build interfaces."""
@@ -8,6 +8,7 @@
8__all__ = [8__all__ = [
9 'ISnapBuild',9 'ISnapBuild',
10 'ISnapBuildSet',10 'ISnapBuildSet',
11 'ISnapBuildStatusChangedEvent',
11 'ISnapFile',12 'ISnapFile',
12 ]13 ]
1314
@@ -20,6 +21,7 @@
20 operation_parameters,21 operation_parameters,
21 )22 )
22from lazr.restful.fields import Reference23from lazr.restful.fields import Reference
24from zope.component.interfaces import IObjectEvent
23from zope.interface import Interface25from zope.interface import Interface
24from zope.schema import (26from zope.schema import (
25 Bool,27 Bool,
@@ -39,6 +41,10 @@
39from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries41from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
4042
4143
44class ISnapBuildStatusChangedEvent(IObjectEvent):
45 """The status of a snap package build changed."""
46
47
42class ISnapFile(Interface):48class ISnapFile(Interface):
43 """A file produced by a snap package build."""49 """A file produced by a snap package build."""
4450
4551
=== modified file 'lib/lp/snappy/model/snap.py'
--- lib/lp/snappy/model/snap.py 2016-02-05 06:05:50 +0000
+++ lib/lp/snappy/model/snap.py 2016-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -71,6 +71,8 @@
71 )71 )
72from lp.services.features import getFeatureFlag72from lp.services.features import getFeatureFlag
73from lp.services.webapp.interfaces import ILaunchBag73from lp.services.webapp.interfaces import ILaunchBag
74from lp.services.webhooks.interfaces import IWebhookSet
75from lp.services.webhooks.model import WebhookTargetMixin
74from lp.snappy.interfaces.snap import (76from lp.snappy.interfaces.snap import (
75 BadSnapSearchContext,77 BadSnapSearchContext,
76 CannotDeleteSnap,78 CannotDeleteSnap,
@@ -110,7 +112,7 @@
110112
111113
112@implementer(ISnap, IHasOwner)114@implementer(ISnap, IHasOwner)
113class Snap(Storm):115class Snap(Storm, WebhookTargetMixin):
114 """See `ISnap`."""116 """See `ISnap`."""
115117
116 __storm_table__ = 'Snap'118 __storm_table__ = 'Snap'
@@ -169,6 +171,10 @@
169 self.private = private171 self.private = private
170172
171 @property173 @property
174 def valid_webhook_event_types(self):
175 return ["snap:build:0.1"]
176
177 @property
172 def git_ref(self):178 def git_ref(self):
173 """See `ISnap`."""179 """See `ISnap`."""
174 if self.git_repository is not None:180 if self.git_repository is not None:
@@ -353,6 +359,7 @@
353 raise CannotDeleteSnap("Cannot delete a snap package with builds.")359 raise CannotDeleteSnap("Cannot delete a snap package with builds.")
354 store = IStore(Snap)360 store = IStore(Snap)
355 store.find(SnapArch, SnapArch.snap == self).remove()361 store.find(SnapArch, SnapArch.snap == self).remove()
362 getUtility(IWebhookSet).delete(self.webhooks)
356 store.remove(self)363 store.remove(self)
357364
358365
359366
=== modified file 'lib/lp/snappy/model/snapbuild.py'
--- lib/lp/snappy/model/snapbuild.py 2016-02-04 00:45:12 +0000
+++ lib/lp/snappy/model/snapbuild.py 2016-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4__metaclass__ = type4__metaclass__ = type
@@ -22,6 +22,8 @@
22 )22 )
23from storm.store import EmptyResultSet23from storm.store import EmptyResultSet
24from zope.component import getUtility24from zope.component import getUtility
25from zope.component.interfaces import ObjectEvent
26from zope.event import notify
25from zope.interface import implementer27from zope.interface import implementer
2628
27from lp.app.errors import NotFoundError29from lp.app.errors import NotFoundError
@@ -59,6 +61,7 @@
59from lp.snappy.interfaces.snapbuild import (61from lp.snappy.interfaces.snapbuild import (
60 ISnapBuild,62 ISnapBuild,
61 ISnapBuildSet,63 ISnapBuildSet,
64 ISnapBuildStatusChangedEvent,
62 ISnapFile,65 ISnapFile,
63 )66 )
64from lp.snappy.mail.snapbuild import SnapBuildMailer67from lp.snappy.mail.snapbuild import SnapBuildMailer
@@ -67,6 +70,11 @@
67from lp.soyuz.model.distroarchseries import DistroArchSeries70from lp.soyuz.model.distroarchseries import DistroArchSeries
6871
6972
73@implementer(ISnapBuildStatusChangedEvent)
74class SnapBuildStatusChangedEvent(ObjectEvent):
75 """See `ISnapBuildStatusChangedEvent`."""
76
77
70@implementer(ISnapFile)78@implementer(ISnapFile)
71class SnapFile(Storm):79class SnapFile(Storm):
72 """See `ISnap`."""80 """See `ISnap`."""
@@ -300,6 +308,16 @@
300 """See `IPackageBuild`."""308 """See `IPackageBuild`."""
301 return not self.getFiles().is_empty()309 return not self.getFiles().is_empty()
302310
311 def updateStatus(self, status, builder=None, slave_status=None,
312 date_started=None, date_finished=None,
313 force_invalid_transition=False):
314 """See `IBuildFarmJob`."""
315 super(SnapBuild, self).updateStatus(
316 status, builder=builder, slave_status=slave_status,
317 date_started=date_started, date_finished=date_finished,
318 force_invalid_transition=force_invalid_transition)
319 notify(SnapBuildStatusChangedEvent(self))
320
303 def notify(self, extra_info=None):321 def notify(self, extra_info=None):
304 """See `IPackageBuild`."""322 """See `IPackageBuild`."""
305 if not config.builddmaster.send_build_notification:323 if not config.builddmaster.send_build_notification:
306324
=== 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-02-19 14:20:42 +0000
@@ -0,0 +1,30 @@
1# Copyright 2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Event subscribers for snap builds."""
5
6from __future__ import absolute_import, print_function, unicode_literals
7
8__metaclass__ = type
9
10from zope.component import getUtility
11
12from lp.services.features import getFeatureFlag
13from lp.services.webapp.publisher import canonical_url
14from lp.services.webhooks.interfaces import IWebhookSet
15from lp.services.webhooks.payload import compose_webhook_payload
16from lp.snappy.interfaces.snap import SNAP_WEBHOOKS_FEATURE_FLAG
17from lp.snappy.interfaces.snapbuild import ISnapBuild
18
19
20def snap_build_status_changed(snapbuild, event):
21 """Trigger webhooks when snap package build statuses change."""
22 if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG):
23 payload = {
24 "snap_build": canonical_url(snapbuild, force_local_path=True),
25 "action": "status-changed",
26 }
27 payload.update(compose_webhook_payload(
28 ISnapBuild, snapbuild, ["snap", "status"]))
29 getUtility(IWebhookSet).trigger(
30 snapbuild.snap, "snap:build:0.1", payload)
031
=== modified file 'lib/lp/snappy/tests/test_snap.py'
--- lib/lp/snappy/tests/test_snap.py 2016-02-05 06:05:50 +0000
+++ lib/lp/snappy/tests/test_snap.py 2016-02-19 14:20:42 +0000
@@ -1,4 +1,4 @@
1# Copyright 2015 Canonical Ltd. This software is licensed under the1# Copyright 2015-2016 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test snap packages."""4"""Test snap packages."""
@@ -8,6 +8,7 @@
8from datetime import timedelta8from datetime import timedelta
99
10from lazr.lifecycle.event import ObjectModifiedEvent10from lazr.lifecycle.event import ObjectModifiedEvent
11from storm.exceptions import LostObjectError
11from storm.locals import Store12from storm.locals import Store
12from testtools.matchers import Equals13from testtools.matchers import Equals
13import transaction14import transaction
@@ -371,6 +372,16 @@
371 self.assertRaises(CannotDeleteSnap, snap.destroySelf)372 self.assertRaises(CannotDeleteSnap, snap.destroySelf)
372 self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))373 self.assertTrue(getUtility(ISnapSet).exists(owner, u"condemned"))
373374
375 def test_related_webhooks_deleted(self):
376 owner = self.factory.makePerson()
377 snap = self.factory.makeSnap(registrant=owner, owner=owner)
378 webhook = self.factory.makeWebhook(target=snap)
379 with person_logged_in(snap.owner):
380 webhook.ping()
381 snap.destroySelf()
382 transaction.commit()
383 self.assertRaises(LostObjectError, getattr, webhook, "target")
384
374385
375class TestSnapSet(TestCaseWithFactory):386class TestSnapSet(TestCaseWithFactory):
376387
377388
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2016-02-06 00:12:30 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2016-02-19 14:20:42 +0000
@@ -15,6 +15,11 @@
15 )15 )
1616
17import pytz17import pytz
18from testtools.matchers import (
19 Equals,
20 MatchesDict,
21 MatchesStructure,
22 )
18from zope.component import getUtility23from zope.component import getUtility
19from zope.security.proxy import removeSecurityProxy24from zope.security.proxy import removeSecurityProxy
2025
@@ -29,6 +34,7 @@
29from lp.services.features.testing import FeatureFixture34from lp.services.features.testing import FeatureFixture
30from lp.services.librarian.browser import ProxiedLibraryFileAlias35from lp.services.librarian.browser import ProxiedLibraryFileAlias
31from lp.services.webapp.interfaces import OAuthPermission36from lp.services.webapp.interfaces import OAuthPermission
37from lp.services.webapp.publisher import canonical_url
32from lp.snappy.interfaces.snap import (38from lp.snappy.interfaces.snap import (
33 SNAP_TESTING_FLAGS,39 SNAP_TESTING_FLAGS,
34 SnapFeatureDisabled,40 SnapFeatureDisabled,
@@ -218,6 +224,25 @@
218 self.factory.makeSnapFile(snapbuild=self.build)224 self.factory.makeSnapFile(snapbuild=self.build)
219 self.assertTrue(self.build.verifySuccessfulUpload())225 self.assertTrue(self.build.verifySuccessfulUpload())
220226
227 def test_updateStatus_triggers_webhooks(self):
228 # Updating the status of a SnapBuild triggers webhooks on the
229 # corresponding Snap.
230 hook = self.factory.makeWebhook(
231 target=self.build.snap, event_types=["snap:build:0.1"])
232 self.build.updateStatus(BuildStatus.FULLYBUILT)
233 expected_payload = {
234 "snap_build": Equals(
235 canonical_url(self.build, force_local_path=True)),
236 "action": Equals("status-changed"),
237 "snap": Equals(
238 canonical_url(self.build.snap, force_local_path=True)),
239 "status": Equals("Successfully built"),
240 }
241 self.assertThat(
242 hook.deliveries.one(), MatchesStructure(
243 event_type=Equals("snap:build:0.1"),
244 payload=MatchesDict(expected_payload)))
245
221 def test_notify_fullybuilt(self):246 def test_notify_fullybuilt(self):
222 # notify does not send mail when a SnapBuild completes normally.247 # notify does not send mail when a SnapBuild completes normally.
223 person = self.factory.makePerson(name="person")248 person = self.factory.makePerson(name="person")