Merge ~ilasc/launchpad:webhook-livefs into launchpad:master

Proposed by Ioana Lasc
Status: Merged
Approved by: Ioana Lasc
Approved revision: b1143ccbfbd6665a258a47629dce27f8563ceed5
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~ilasc/launchpad:webhook-livefs
Merge into: launchpad:master
Prerequisite: ~cjwatson/launchpad:fix-webhook-visibility-check
Diff against target: 738 lines (+328/-11)
16 files modified
database/schema/security.cfg (+1/-0)
lib/lp/services/webhooks/interfaces.py (+1/-0)
lib/lp/services/webhooks/model.py (+13/-0)
lib/lp/services/webhooks/tests/test_browser.py (+51/-7)
lib/lp/services/webhooks/tests/test_job.py (+15/-0)
lib/lp/services/webhooks/tests/test_model.py (+19/-0)
lib/lp/services/webhooks/tests/test_webservice.py (+15/-0)
lib/lp/soyuz/browser/livefs.py (+2/-1)
lib/lp/soyuz/configure.zcml (+8/-0)
lib/lp/soyuz/interfaces/livefs.py (+4/-1)
lib/lp/soyuz/model/livefs.py (+11/-1)
lib/lp/soyuz/model/livefsbuild.py (+15/-0)
lib/lp/soyuz/subscribers/__init__.py (+0/-0)
lib/lp/soyuz/subscribers/livefsbuild.py (+43/-0)
lib/lp/soyuz/tests/test_livefs.py (+63/-1)
lib/lp/soyuz/tests/test_livefsbuild.py (+67/-0)
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+377875@code.launchpad.net

Commit message

Add Webhook for LiveFS builds

Description of the change

Added webhooks for LiveFS builds.

While open for review, I did notice a test failure at the end of my day in lp.code.scripts.tests.test_request_daily_builds.TestRequestDailyBuilds.test_request_daily_builds after running full test suite with the message: "ERROR - team-name-100107/snap-name-100108/arch-100021: permission denied for relation account"

Will investigate in the morning - wanted to make sure I get the tidying up and the MP open today.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) wrote :

This mostly LGTM, thanks! I had a few minor comments from a line-by-line review, but the "needs fixing" is mainly for correcting the feature flag.

(As discussed on IRC, the test failure was in fact from my fix-webhook-visibility-check branch that's a prerequisite of this one. Sorry about that.)

review: Needs Fixing
Revision history for this message
Ioana Lasc (ilasc) wrote :

Thanks Colin! This is now ready for review.

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

This looks good so far. There are just a couple of missing things:

 * We need a test that requesting a new build of a livefs triggers a webhook delivery. This should go in lp.soyuz.tests.test_livefs; you can borrow from lp.snappy.tests.test_snap.TestSnap.test_requestBuild_triggers_webhooks.

 * Deleting a livefs needs to delete its associated webhooks; and we need a test for that too. See lp.snappy.model.snap.Snap.destroySelf and lp.snappy.tests.test_snap.TestSnapDeleteWithBuilds.test_related_webhooks_deleted (the equivalent should end up in lp.soyuz.tests.test_livefs.TestLiveFS.test_related_webhooks_deleted, I think).

(The second of these isn't such a major issue because, unlike snaps, it isn't currently possible to delete a livefs that's ever had builds; but it would be theoretically possible to create a livefs, set up a webhook on it, manually tell LP to ping the webhook target, and then delete the livefs. Supporting this is only one extra line in LiveFS.destroySelf and maybe a dozen lines of tests, so we might as well.)

review: Approve
Revision history for this message
Ioana Lasc (ilasc) wrote :

Thank you Colin, comments were of great help as usual.

Addressed the 2 shortcomings. Ready for a final glance. Thank you!

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/database/schema/security.cfg b/database/schema/security.cfg
2index b5d6071..a2071e8 100644
3--- a/database/schema/security.cfg
4+++ b/database/schema/security.cfg
5@@ -2597,6 +2597,7 @@ public.branch = SELECT
6 public.distribution = SELECT
7 public.gitrepository = SELECT
8 public.job = SELECT, UPDATE
9+public.livefs = SELECT
10 public.ociproject = SELECT
11 public.ociprojectname = SELECT
12 public.ociprojectseries = SELECT
13diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py
14index e4533b7..8fe724e 100644
15--- a/lib/lp/services/webhooks/interfaces.py
16+++ b/lib/lp/services/webhooks/interfaces.py
17@@ -75,6 +75,7 @@ from lp.services.webservice.apihelpers import (
18 WEBHOOK_EVENT_TYPES = {
19 "bzr:push:0.1": "Bazaar push",
20 "git:push:0.1": "Git push",
21+ "livefs:build:0.1": "Live filesystem build",
22 "merge-proposal:0.1": "Merge proposal",
23 "snap:build:0.1": "Snap build",
24 }
25diff --git a/lib/lp/services/webhooks/model.py b/lib/lp/services/webhooks/model.py
26index 0c38e8e..440ce26 100644
27--- a/lib/lp/services/webhooks/model.py
28+++ b/lib/lp/services/webhooks/model.py
29@@ -104,6 +104,9 @@ class Webhook(StormBase):
30 snap_id = Int(name='snap')
31 snap = Reference(snap_id, 'Snap.id')
32
33+ livefs_id = Int(name='livefs')
34+ livefs = Reference(livefs_id, 'LiveFS.id')
35+
36 registrant_id = Int(name='registrant', allow_none=False)
37 registrant = Reference(registrant_id, 'Person.id')
38 date_created = DateTime(tzinfo=utc, allow_none=False)
39@@ -123,6 +126,8 @@ class Webhook(StormBase):
40 return self.branch
41 elif self.snap is not None:
42 return self.snap
43+ elif self.livefs is not None:
44+ return self.livefs
45 else:
46 raise AssertionError("No target.")
47
48@@ -177,6 +182,8 @@ class WebhookSet:
49 from lp.code.interfaces.branch import IBranch
50 from lp.code.interfaces.gitrepository import IGitRepository
51 from lp.snappy.interfaces.snap import ISnap
52+ from lp.soyuz.interfaces.livefs import ILiveFS
53+
54 hook = Webhook()
55 if IGitRepository.providedBy(target):
56 hook.git_repository = target
57@@ -184,6 +191,8 @@ class WebhookSet:
58 hook.branch = target
59 elif ISnap.providedBy(target):
60 hook.snap = target
61+ elif ILiveFS.providedBy(target):
62+ hook.livefs = target
63 else:
64 raise AssertionError("Unsupported target: %r" % (target,))
65 hook.registrant = registrant
66@@ -208,12 +217,16 @@ class WebhookSet:
67 from lp.code.interfaces.branch import IBranch
68 from lp.code.interfaces.gitrepository import IGitRepository
69 from lp.snappy.interfaces.snap import ISnap
70+ from lp.soyuz.interfaces.livefs import ILiveFS
71+
72 if IGitRepository.providedBy(target):
73 target_filter = Webhook.git_repository == target
74 elif IBranch.providedBy(target):
75 target_filter = Webhook.branch == target
76 elif ISnap.providedBy(target):
77 target_filter = Webhook.snap == target
78+ elif ILiveFS.providedBy(target):
79+ target_filter = Webhook.livefs == target
80 else:
81 raise AssertionError("Unsupported target: %r" % (target,))
82 return IStore(Webhook).find(Webhook, target_filter).order_by(
83diff --git a/lib/lp/services/webhooks/tests/test_browser.py b/lib/lp/services/webhooks/tests/test_browser.py
84index 22a4be7..13adc13 100644
85--- a/lib/lp/services/webhooks/tests/test_browser.py
86+++ b/lib/lp/services/webhooks/tests/test_browser.py
87@@ -18,6 +18,10 @@ import transaction
88 from lp.services.features.testing import FeatureFixture
89 from lp.services.webapp.publisher import canonical_url
90 from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
91+from lp.soyuz.interfaces.livefs import (
92+ LIVEFS_FEATURE_FLAG,
93+ LIVEFS_WEBHOOKS_FEATURE_FLAG,
94+ )
95 from lp.testing import (
96 login_person,
97 record_two_runs,
98@@ -25,10 +29,7 @@ from lp.testing import (
99 )
100 from lp.testing.fakemethod import FakeMethod
101 from lp.testing.fixture import ZopeUtilityFixture
102-from lp.testing.layers import (
103- DatabaseFunctionalLayer,
104- LaunchpadFunctionalLayer,
105- )
106+from lp.testing.layers import DatabaseFunctionalLayer
107 from lp.testing.matchers import HasQueryCount
108 from lp.testing.pages import extract_text
109 from lp.testing.views import create_view
110@@ -109,6 +110,26 @@ class SnapTestHelpers:
111 return [obj]
112
113
114+class LiveFSTestHelpers:
115+ event_type = "livefs:build:0.1"
116+ expected_event_types = [
117+ ("livefs:build:0.1", "Live filesystem build"),
118+ ]
119+
120+ def setUp(self):
121+ super(LiveFSTestHelpers, self).setUp()
122+
123+ def makeTarget(self):
124+ self.useFixture(FeatureFixture({'webhooks.new.enabled': 'true',
125+ LIVEFS_FEATURE_FLAG: "on",
126+ LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}))
127+ owner = self.factory.makePerson()
128+ return self.factory.makeLiveFS(registrant=owner, owner=owner)
129+
130+ def getTraversalStack(self, obj):
131+ return [obj]
132+
133+
134 class WebhookTargetViewTestHelpers:
135
136 def setUp(self):
137@@ -195,14 +216,19 @@ class TestWebhooksViewGitRepository(
138
139 class TestWebhooksViewBranch(
140 TestWebhooksViewBase, BranchTestHelpers, TestCaseWithFactory):
141-
142 pass
143
144
145 class TestWebhooksViewSnap(
146 TestWebhooksViewBase, SnapTestHelpers, TestCaseWithFactory):
147
148- layer = LaunchpadFunctionalLayer
149+ pass
150+
151+
152+class TestWebhooksViewLiveFS(
153+ TestWebhooksViewBase, LiveFSTestHelpers, TestCaseWithFactory):
154+
155+ pass
156
157
158 class TestWebhookAddViewBase(WebhookTargetViewTestHelpers):
159@@ -300,7 +326,13 @@ class TestWebhookAddViewBranch(
160 class TestWebhookAddViewSnap(
161 TestWebhookAddViewBase, SnapTestHelpers, TestCaseWithFactory):
162
163- layer = LaunchpadFunctionalLayer
164+ pass
165+
166+
167+class TestWebhookAddViewLiveFS(
168+ TestWebhookAddViewBase, LiveFSTestHelpers, TestCaseWithFactory):
169+
170+ pass
171
172
173 class WebhookViewTestHelpers:
174@@ -405,6 +437,12 @@ class TestWebhookViewSnap(
175 pass
176
177
178+class TestWebhookViewLiveFS(
179+ TestWebhookViewBase, LiveFSTestHelpers, TestCaseWithFactory):
180+
181+ pass
182+
183+
184 class TestWebhookDeleteViewBase(WebhookViewTestHelpers):
185
186 layer = DatabaseFunctionalLayer
187@@ -455,3 +493,9 @@ class TestWebhookDeleteViewSnap(
188 TestWebhookDeleteViewBase, SnapTestHelpers, TestCaseWithFactory):
189
190 pass
191+
192+
193+class TestWebhookDeleteViewLiveFS(
194+ TestWebhookDeleteViewBase, LiveFSTestHelpers, TestCaseWithFactory):
195+
196+ pass
197diff --git a/lib/lp/services/webhooks/tests/test_job.py b/lib/lp/services/webhooks/tests/test_job.py
198index 260698c..762f7ff 100644
199--- a/lib/lp/services/webhooks/tests/test_job.py
200+++ b/lib/lp/services/webhooks/tests/test_job.py
201@@ -60,6 +60,10 @@ from lp.services.webhooks.model import (
202 WebhookJobType,
203 )
204 from lp.services.webhooks.testing import LogsScheduledWebhooks
205+from lp.soyuz.interfaces.livefs import (
206+ LIVEFS_FEATURE_FLAG,
207+ LIVEFS_WEBHOOKS_FEATURE_FLAG,
208+ )
209 from lp.testing import (
210 login_person,
211 TestCaseWithFactory,
212@@ -339,6 +343,17 @@ class TestWebhookDeliveryJob(TestCaseWithFactory):
213 "<WebhookDeliveryJob for webhook %d on %r>" % (hook.id, snap),
214 repr(job))
215
216+ def test_livefs__repr__(self):
217+ # `WebhookDeliveryJob` objects for livefs have an informative __repr__.
218+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on",
219+ LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
220+ livefs = self.factory.makeLiveFS()
221+ hook = self.factory.makeWebhook(target=livefs)
222+ job = WebhookDeliveryJob.create(hook, 'test', payload={'foo': 'bar'})
223+ self.assertEqual(
224+ "<WebhookDeliveryJob for webhook %d on %r>" % (hook.id, livefs),
225+ repr(job))
226+
227 def test_short_lease_and_timeout(self):
228 # Webhook jobs have a request timeout of 30 seconds, a celery
229 # timeout of 45 seconds, and a lease of 60 seconds, to give
230diff --git a/lib/lp/services/webhooks/tests/test_model.py b/lib/lp/services/webhooks/tests/test_model.py
231index 41189e3..cd43758 100644
232--- a/lib/lp/services/webhooks/tests/test_model.py
233+++ b/lib/lp/services/webhooks/tests/test_model.py
234@@ -15,6 +15,7 @@ from zope.security.proxy import removeSecurityProxy
235 from lp.app.enums import InformationType
236 from lp.registry.enums import BranchSharingPolicy
237 from lp.services.database.interfaces import IStore
238+from lp.services.features.testing import FeatureFixture
239 from lp.services.webapp.authorization import check_permission
240 from lp.services.webapp.snapshot import notify_modified
241 from lp.services.webhooks.interfaces import IWebhookSet
242@@ -22,6 +23,10 @@ from lp.services.webhooks.model import (
243 WebhookJob,
244 WebhookSet,
245 )
246+from lp.soyuz.interfaces.livefs import (
247+ LIVEFS_FEATURE_FLAG,
248+ LIVEFS_WEBHOOKS_FEATURE_FLAG,
249+ )
250 from lp.testing import (
251 admin_logged_in,
252 anonymous_logged_in,
253@@ -395,3 +400,17 @@ class TestWebhookSetSnap(TestWebhookSetBase, TestCaseWithFactory):
254 if owner is None:
255 owner = self.factory.makePerson()
256 return self.factory.makeSnap(registrant=owner, owner=owner, **kwargs)
257+
258+
259+class TestWebhookSetLiveFS(TestWebhookSetBase, TestCaseWithFactory):
260+
261+ event_type = 'livefs:build:0.1'
262+
263+ def makeTarget(self, owner=None, **kwargs):
264+ if owner is None:
265+ owner = self.factory.makePerson()
266+
267+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on",
268+ LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
269+ return self.factory.makeLiveFS(registrant=owner,
270+ owner=owner, **kwargs)
271diff --git a/lib/lp/services/webhooks/tests/test_webservice.py b/lib/lp/services/webhooks/tests/test_webservice.py
272index cdf92b5..19af1bc 100644
273--- a/lib/lp/services/webhooks/tests/test_webservice.py
274+++ b/lib/lp/services/webhooks/tests/test_webservice.py
275@@ -23,6 +23,10 @@ from zope.security.proxy import removeSecurityProxy
276
277 from lp.services.features.testing import FeatureFixture
278 from lp.services.webapp.interfaces import OAuthPermission
279+from lp.soyuz.interfaces.livefs import (
280+ LIVEFS_FEATURE_FLAG,
281+ LIVEFS_WEBHOOKS_FEATURE_FLAG,
282+ )
283 from lp.testing import (
284 api_url,
285 person_logged_in,
286@@ -379,3 +383,14 @@ class TestWebhookTargetSnap(TestWebhookTargetBase, TestCaseWithFactory):
287 def makeTarget(self):
288 owner = self.factory.makePerson()
289 return self.factory.makeSnap(registrant=owner, owner=owner)
290+
291+
292+class TestWebhookTargetLiveFS(TestWebhookTargetBase, TestCaseWithFactory):
293+
294+ event_type = 'livefs:build:0.1'
295+
296+ def makeTarget(self):
297+ owner = self.factory.makePerson()
298+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on",
299+ LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
300+ return self.factory.makeLiveFS(registrant=owner, owner=owner)
301diff --git a/lib/lp/soyuz/browser/livefs.py b/lib/lp/soyuz/browser/livefs.py
302index 5f1bfb9..e2659ca 100644
303--- a/lib/lp/soyuz/browser/livefs.py
304+++ b/lib/lp/soyuz/browser/livefs.py
305@@ -55,6 +55,7 @@ from lp.services.webapp.breadcrumb import (
306 Breadcrumb,
307 NameBreadcrumb,
308 )
309+from lp.services.webhooks.browser import WebhookTargetNavigationMixin
310 from lp.soyuz.browser.build import get_build_by_id_str
311 from lp.soyuz.interfaces.livefs import (
312 ILiveFS,
313@@ -66,7 +67,7 @@ from lp.soyuz.interfaces.livefs import (
314 from lp.soyuz.interfaces.livefsbuild import ILiveFSBuildSet
315
316
317-class LiveFSNavigation(Navigation):
318+class LiveFSNavigation(WebhookTargetNavigationMixin, Navigation):
319 usedfor = ILiveFS
320
321 @stepthrough('+build')
322diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml
323index 0e35ccb..da011cc 100644
324--- a/lib/lp/soyuz/configure.zcml
325+++ b/lib/lp/soyuz/configure.zcml
326@@ -1022,6 +1022,14 @@
327 permission="launchpad.Admin"
328 interface=".interfaces.livefsbuild.ILiveFSBuildAdmin"/>
329 </class>
330+ <subscriber
331+ for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild
332+ lazr.lifecycle.interfaces.IObjectCreatedEvent"
333+ handler="lp.soyuz.subscribers.livefsbuild.livefs_build_created" />
334+ <subscriber
335+ for="lp.soyuz.interfaces.livefsbuild.ILiveFSBuild
336+ lazr.lifecycle.interfaces.IObjectModifiedEvent"
337+ handler="lp.soyuz.subscribers.livefsbuild.livefs_build_status_changed" />
338
339 <!-- LiveFSBuildSet -->
340 <securedutility
341diff --git a/lib/lp/soyuz/interfaces/livefs.py b/lib/lp/soyuz/interfaces/livefs.py
342index 140f874..c530dc8 100644
343--- a/lib/lp/soyuz/interfaces/livefs.py
344+++ b/lib/lp/soyuz/interfaces/livefs.py
345@@ -14,6 +14,7 @@ __all__ = [
346 'ILiveFSSet',
347 'ILiveFSView',
348 'LIVEFS_FEATURE_FLAG',
349+ 'LIVEFS_WEBHOOKS_FEATURE_FLAG',
350 'LiveFSBuildAlreadyPending',
351 'LiveFSBuildArchiveOwnerMismatch',
352 'LiveFSFeatureDisabled',
353@@ -69,11 +70,13 @@ from lp.services.fields import (
354 PersonChoice,
355 PublicPersonChoice,
356 )
357+from lp.services.webhooks.interfaces import IWebhookTarget
358 from lp.soyuz.interfaces.archive import IArchive
359 from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries
360
361
362 LIVEFS_FEATURE_FLAG = u"soyuz.livefs.allow_new"
363+LIVEFS_WEBHOOKS_FEATURE_FLAG = u"soyuz.livefs.webhooks.enabled"
364
365
366 @error_status(httplib.BAD_REQUEST)
367@@ -213,7 +216,7 @@ class ILiveFSView(IPrivacy):
368 value_type=Reference(schema=Interface), readonly=True)))
369
370
371-class ILiveFSEdit(Interface):
372+class ILiveFSEdit(IWebhookTarget):
373 """`ILiveFS` methods that require launchpad.Edit permission."""
374
375 @export_destructor_operation()
376diff --git a/lib/lp/soyuz/model/livefs.py b/lib/lp/soyuz/model/livefs.py
377index b79c1ca..0ec68b4 100644
378--- a/lib/lp/soyuz/model/livefs.py
379+++ b/lib/lp/soyuz/model/livefs.py
380@@ -9,6 +9,7 @@ __all__ = [
381 from datetime import timedelta
382 import math
383
384+from lazr.lifecycle.event import ObjectCreatedEvent
385 import pytz
386 from storm.locals import (
387 Bool,
388@@ -24,6 +25,7 @@ from storm.locals import (
389 Unicode,
390 )
391 from zope.component import getUtility
392+from zope.event import notify
393 from zope.interface import implementer
394 from zope.security.proxy import removeSecurityProxy
395
396@@ -57,6 +59,8 @@ from lp.services.database.stormexpr import (
397 )
398 from lp.services.features import getFeatureFlag
399 from lp.services.webapp.interfaces import ILaunchBag
400+from lp.services.webhooks.interfaces import IWebhookSet
401+from lp.services.webhooks.model import WebhookTargetMixin
402 from lp.soyuz.interfaces.archive import ArchiveDisabled
403 from lp.soyuz.interfaces.livefs import (
404 CannotDeleteLiveFS,
405@@ -88,7 +92,7 @@ def livefs_modified(livefs, event):
406
407
408 @implementer(ILiveFS, IHasOwner)
409-class LiveFS(Storm):
410+class LiveFS(Storm, WebhookTargetMixin):
411 """See `ILiveFS`."""
412
413 __storm_table__ = 'LiveFS'
414@@ -139,6 +143,10 @@ class LiveFS(Storm):
415 self.keep_binary_files_days = keep_binary_files_days
416
417 @property
418+ def valid_webhook_event_types(self):
419+ return ["livefs:build:0.1"]
420+
421+ @property
422 def private(self):
423 """See `IPrivacy`."""
424 # A LiveFS has no privacy support of its own, but it is private if
425@@ -191,6 +199,7 @@ class LiveFS(Storm):
426 unique_key=unique_key, metadata_override=metadata_override,
427 version=version)
428 build.queueBuild()
429+ notify(ObjectCreatedEvent(build, user=requester))
430 return build
431
432 def _getBuilds(self, filter_term, order_by):
433@@ -255,6 +264,7 @@ class LiveFS(Storm):
434 if not self.builds.is_empty():
435 raise CannotDeleteLiveFS(
436 "Cannot delete a live filesystem with builds.")
437+ getUtility(IWebhookSet).delete(self.webhooks)
438 IStore(LiveFS).remove(self)
439
440
441diff --git a/lib/lp/soyuz/model/livefsbuild.py b/lib/lp/soyuz/model/livefsbuild.py
442index 310ab5c..2bf500e 100644
443--- a/lib/lp/soyuz/model/livefsbuild.py
444+++ b/lib/lp/soyuz/model/livefsbuild.py
445@@ -50,6 +50,7 @@ from lp.services.librarian.model import (
446 LibraryFileAlias,
447 LibraryFileContent,
448 )
449+from lp.services.webapp.snapshot import notify_modified
450 from lp.soyuz.interfaces.component import IComponentSet
451 from lp.soyuz.interfaces.livefs import (
452 LIVEFS_FEATURE_FLAG,
453@@ -315,6 +316,20 @@ class LiveFSBuild(PackageBuildMixin, Storm):
454 """See `IPackageBuild`."""
455 return not self.getFiles().is_empty()
456
457+ def updateStatus(self, status, builder=None, slave_status=None,
458+ date_started=None, date_finished=None,
459+ force_invalid_transition=False):
460+ """See `IBuildFarmJob`."""
461+
462+ edited_fields = set()
463+ with notify_modified(self, edited_fields) as previous_obj:
464+ super(LiveFSBuild, self).updateStatus(
465+ status, builder=builder, slave_status=slave_status,
466+ date_started=date_started, date_finished=date_finished,
467+ force_invalid_transition=force_invalid_transition)
468+ if self.status != previous_obj.status:
469+ edited_fields.add("status")
470+
471 def notify(self, extra_info=None):
472 """See `IPackageBuild`."""
473 if not config.builddmaster.send_build_notification:
474diff --git a/lib/lp/soyuz/subscribers/__init__.py b/lib/lp/soyuz/subscribers/__init__.py
475new file mode 100644
476index 0000000..e69de29
477--- /dev/null
478+++ b/lib/lp/soyuz/subscribers/__init__.py
479diff --git a/lib/lp/soyuz/subscribers/livefsbuild.py b/lib/lp/soyuz/subscribers/livefsbuild.py
480new file mode 100644
481index 0000000..852f9d3
482--- /dev/null
483+++ b/lib/lp/soyuz/subscribers/livefsbuild.py
484@@ -0,0 +1,43 @@
485+# Copyright 2016-2020 Canonical Ltd. This software is licensed under the
486+# GNU Affero General Public License version 3 (see the file LICENSE).
487+
488+"""Event subscribers for livefs builds."""
489+from __future__ import absolute_import, print_function, unicode_literals
490+
491+
492+__metaclass__ = type
493+
494+
495+from zope.component import getUtility
496+
497+from lp.services.features import getFeatureFlag
498+from lp.services.webapp.publisher import canonical_url
499+from lp.services.webhooks.interfaces import IWebhookSet
500+from lp.services.webhooks.payload import compose_webhook_payload
501+from lp.soyuz.interfaces.livefs import LIVEFS_WEBHOOKS_FEATURE_FLAG
502+from lp.soyuz.interfaces.livefsbuild import ILiveFSBuild
503+
504+
505+def _trigger_livefs_build_webhook(livefsbuild, action):
506+ if getFeatureFlag(LIVEFS_WEBHOOKS_FEATURE_FLAG):
507+ payload = {
508+ "livefs_build": canonical_url(livefsbuild, force_local_path=True),
509+ "action": action,
510+ }
511+ payload.update(compose_webhook_payload(
512+ ILiveFSBuild, livefsbuild,
513+ ["livefs", "status"]))
514+ getUtility(IWebhookSet).trigger(
515+ livefsbuild.livefs, "livefs:build:0.1", payload)
516+
517+
518+def livefs_build_created(livefsbuild, event):
519+ """Trigger events when a new livefs build is created."""
520+ _trigger_livefs_build_webhook(livefsbuild, "created")
521+
522+
523+def livefs_build_status_changed(livefsbuild, event):
524+ """Trigger events when livefs package build statuses change."""
525+ if event.edited_fields is not None:
526+ if "status" in event.edited_fields:
527+ _trigger_livefs_build_webhook(livefsbuild, "status-changed")
528diff --git a/lib/lp/soyuz/tests/test_livefs.py b/lib/lp/soyuz/tests/test_livefs.py
529index 29170b4..315b96a 100644
530--- a/lib/lp/soyuz/tests/test_livefs.py
531+++ b/lib/lp/soyuz/tests/test_livefs.py
532@@ -12,9 +12,15 @@ from datetime import (
533 timedelta,
534 )
535
536+from fixtures import FakeLogger
537 import pytz
538+from storm.exceptions import LostObjectError
539 from storm.locals import Store
540-from testtools.matchers import Equals
541+from testtools.matchers import (
542+ Equals,
543+ MatchesDict,
544+ MatchesStructure,
545+ )
546 import transaction
547 from zope.component import getUtility
548 from zope.security.interfaces import Unauthorized
549@@ -30,16 +36,20 @@ from lp.buildmaster.model.buildqueue import BuildQueue
550 from lp.registry.enums import PersonVisibility
551 from lp.registry.interfaces.distribution import IDistributionSet
552 from lp.registry.interfaces.pocket import PackagePublishingPocket
553+from lp.services.config import config
554 from lp.services.database.constants import UTC_NOW
555 from lp.services.features.testing import FeatureFixture
556+from lp.services.webapp import canonical_url
557 from lp.services.webapp.interfaces import OAuthPermission
558 from lp.services.webapp.snapshot import notify_modified
559+from lp.services.webhooks.testing import LogsScheduledWebhooks
560 from lp.soyuz.interfaces.livefs import (
561 CannotDeleteLiveFS,
562 ILiveFS,
563 ILiveFSSet,
564 ILiveFSView,
565 LIVEFS_FEATURE_FLAG,
566+ LIVEFS_WEBHOOKS_FEATURE_FLAG,
567 LiveFSBuildAlreadyPending,
568 LiveFSFeatureDisabled,
569 )
570@@ -54,6 +64,7 @@ from lp.testing import (
571 StormStatementRecorder,
572 TestCaseWithFactory,
573 )
574+from lp.testing.dbuser import dbuser
575 from lp.testing.layers import (
576 DatabaseFunctionalLayer,
577 LaunchpadZopelessLayer,
578@@ -362,6 +373,57 @@ class TestLiveFS(TestCaseWithFactory):
579 self.assertTrue(
580 getUtility(ILiveFSSet).exists(owner, distroseries, "condemned"))
581
582+ def test_requestBuild_triggers_webhooks(self):
583+ # Requesting a build triggers webhooks.
584+ logger = self.useFixture(FakeLogger())
585+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on",
586+ LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
587+ livefs = self.factory.makeLiveFS()
588+ distroarchseries = self.factory.makeDistroArchSeries(
589+ distroseries=livefs.distro_series)
590+ hook = self.factory.makeWebhook(
591+ target=livefs, event_types=["livefs:build:0.1"])
592+ build = livefs.requestBuild(
593+ livefs.owner, livefs.distro_series.main_archive,
594+ distroarchseries,
595+ PackagePublishingPocket.RELEASE)
596+
597+ expected_payload = {
598+ "livefs_build": Equals(canonical_url(build,
599+ force_local_path=True)),
600+ "action": Equals("created"),
601+ "livefs": Equals(canonical_url(livefs, force_local_path=True)),
602+ "status": Equals("Needs building"),
603+ }
604+ with person_logged_in(livefs.owner):
605+ delivery = hook.deliveries.one()
606+ self.assertThat(
607+ delivery, MatchesStructure(
608+ event_type=Equals("livefs:build:0.1"),
609+ payload=MatchesDict(expected_payload)))
610+ with dbuser(config.IWebhookDeliveryJobSource.dbuser):
611+ self.assertEqual(
612+ "<WebhookDeliveryJob for webhook %d on %r>" % (
613+ hook.id, hook.target),
614+ repr(delivery))
615+ self.assertThat(
616+ logger.output,
617+ LogsScheduledWebhooks([
618+ (hook, "livefs:build:0.1",
619+ MatchesDict(expected_payload))]))
620+
621+ def test_related_webhooks_deleted(self):
622+ owner = self.factory.makePerson()
623+ with FeatureFixture({LIVEFS_FEATURE_FLAG: "on",
624+ LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
625+ livefs = self.factory.makeLiveFS(registrant=owner, owner=owner)
626+ webhook = self.factory.makeWebhook(target=livefs)
627+ with person_logged_in(livefs.owner):
628+ webhook.ping()
629+ livefs.destroySelf()
630+ transaction.commit()
631+ self.assertRaises(LostObjectError, getattr, webhook, "target")
632+
633
634 class TestLiveFSSet(TestCaseWithFactory):
635
636diff --git a/lib/lp/soyuz/tests/test_livefsbuild.py b/lib/lp/soyuz/tests/test_livefsbuild.py
637index 987c6b1..99ceccc 100644
638--- a/lib/lp/soyuz/tests/test_livefsbuild.py
639+++ b/lib/lp/soyuz/tests/test_livefsbuild.py
640@@ -13,7 +13,14 @@ from datetime import (
641 )
642 from urllib2 import urlopen
643
644+from fixtures import FakeLogger
645 import pytz
646+from testtools.matchers import (
647+ ContainsDict,
648+ Equals,
649+ MatchesDict,
650+ MatchesStructure,
651+ )
652 from zope.component import getUtility
653 from zope.security.proxy import removeSecurityProxy
654
655@@ -31,9 +38,12 @@ from lp.services.config import config
656 from lp.services.features.testing import FeatureFixture
657 from lp.services.librarian.browser import ProxiedLibraryFileAlias
658 from lp.services.webapp.interfaces import OAuthPermission
659+from lp.services.webapp.publisher import canonical_url
660+from lp.services.webhooks.testing import LogsScheduledWebhooks
661 from lp.soyuz.enums import ArchivePurpose
662 from lp.soyuz.interfaces.livefs import (
663 LIVEFS_FEATURE_FLAG,
664+ LIVEFS_WEBHOOKS_FEATURE_FLAG,
665 LiveFSFeatureDisabled,
666 )
667 from lp.soyuz.interfaces.livefsbuild import (
668@@ -48,6 +58,7 @@ from lp.testing import (
669 person_logged_in,
670 TestCaseWithFactory,
671 )
672+from lp.testing.dbuser import dbuser
673 from lp.testing.layers import (
674 LaunchpadFunctionalLayer,
675 LaunchpadZopelessLayer,
676@@ -162,6 +173,62 @@ class TestLiveFSBuild(TestCaseWithFactory):
677 self.assertEqual(BuildStatus.CANCELLED, self.build.status)
678 self.assertIsNone(self.build.buildqueue_record)
679
680+ def test_updateStatus_triggers_webhooks(self):
681+ # Updating the status of a SnapBuild triggers webhooks on the
682+ # corresponding Snap.
683+ logger = self.useFixture(FakeLogger())
684+ hook = self.factory.makeWebhook(
685+ target=self.build.livefs, event_types=["livefs:build:0.1"])
686+ with FeatureFixture({LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
687+ self.build.updateStatus(BuildStatus.FULLYBUILT)
688+ expected_payload = {
689+ "livefs_build": Equals(
690+ canonical_url(self.build, force_local_path=True)),
691+ "action": Equals("status-changed"),
692+ "livefs": Equals(
693+ canonical_url(self.build.livefs, force_local_path=True)),
694+ "status": Equals("Successfully built"),
695+ }
696+ self.assertThat(
697+ logger.output, LogsScheduledWebhooks([
698+ (hook, "livefs:build:0.1", MatchesDict(expected_payload))]))
699+
700+ delivery = hook.deliveries.one()
701+ self.assertThat(
702+ delivery, MatchesStructure(
703+ event_type=Equals("livefs:build:0.1"),
704+ payload=MatchesDict(expected_payload)))
705+ with dbuser(config.IWebhookDeliveryJobSource.dbuser):
706+ self.assertEqual(
707+ "<WebhookDeliveryJob for webhook %d on %r>" % (
708+ hook.id, hook.target),
709+ repr(delivery))
710+
711+ def test_updateStatus_no_change_does_not_trigger_webhooks(self):
712+ # An updateStatus call that doesn't change the build's status
713+ # attribute does not trigger webhooks.
714+ logger = self.useFixture(FakeLogger())
715+ hook = self.factory.makeWebhook(
716+ target=self.build.livefs, event_types=["livefs:build:0.1"])
717+ with FeatureFixture({LIVEFS_WEBHOOKS_FEATURE_FLAG: "on"}):
718+ self.build.updateStatus(BuildStatus.BUILDING)
719+ expected_logs = [
720+ (hook, "livefs:build:0.1", ContainsDict({
721+ "action": Equals("status-changed"),
722+ "status": Equals("Currently building"),
723+ }))]
724+ self.assertEqual(1, hook.deliveries.count())
725+ self.assertThat(logger.output, LogsScheduledWebhooks(expected_logs))
726+
727+ self.build.updateStatus(BuildStatus.BUILDING)
728+ expected_logs = [
729+ (hook, "livefs:build:0.1", ContainsDict({
730+ "action": Equals("status-changed"),
731+ "status": Equals("Currently building"),
732+ }))]
733+ self.assertEqual(1, hook.deliveries.count())
734+ self.assertThat(logger.output, LogsScheduledWebhooks(expected_logs))
735+
736 def test_cancel_in_progress(self):
737 # The cancel() method for a building build leaves it in the
738 # CANCELLING state.

Subscribers

People subscribed via source and target branches

to status/vote changes: