Merge ~ilasc/launchpad:webhook-livefs into launchpad:master
- Git
- lp:~ilasc/launchpad
- webhook-livefs
- Merge into master
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) |
||||
Related bugs: |
|
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.
Will investigate in the morning - wanted to make sure I get the tidying up and the MP open today.
Ioana Lasc (ilasc) wrote : | # |
Thanks Colin! This is now ready for review.
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.
* Deleting a livefs needs to delete its associated webhooks; and we need a test for that too. See lp.snappy.
(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.)
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!
Colin Watson (cjwatson) : | # |
Preview Diff
1 | diff --git a/database/schema/security.cfg b/database/schema/security.cfg |
2 | index 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 |
13 | diff --git a/lib/lp/services/webhooks/interfaces.py b/lib/lp/services/webhooks/interfaces.py |
14 | index 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 | } |
25 | diff --git a/lib/lp/services/webhooks/model.py b/lib/lp/services/webhooks/model.py |
26 | index 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( |
83 | diff --git a/lib/lp/services/webhooks/tests/test_browser.py b/lib/lp/services/webhooks/tests/test_browser.py |
84 | index 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 |
197 | diff --git a/lib/lp/services/webhooks/tests/test_job.py b/lib/lp/services/webhooks/tests/test_job.py |
198 | index 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 |
230 | diff --git a/lib/lp/services/webhooks/tests/test_model.py b/lib/lp/services/webhooks/tests/test_model.py |
231 | index 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) |
271 | diff --git a/lib/lp/services/webhooks/tests/test_webservice.py b/lib/lp/services/webhooks/tests/test_webservice.py |
272 | index 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) |
301 | diff --git a/lib/lp/soyuz/browser/livefs.py b/lib/lp/soyuz/browser/livefs.py |
302 | index 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') |
322 | diff --git a/lib/lp/soyuz/configure.zcml b/lib/lp/soyuz/configure.zcml |
323 | index 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 |
341 | diff --git a/lib/lp/soyuz/interfaces/livefs.py b/lib/lp/soyuz/interfaces/livefs.py |
342 | index 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() |
376 | diff --git a/lib/lp/soyuz/model/livefs.py b/lib/lp/soyuz/model/livefs.py |
377 | index 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 | |
441 | diff --git a/lib/lp/soyuz/model/livefsbuild.py b/lib/lp/soyuz/model/livefsbuild.py |
442 | index 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: |
474 | diff --git a/lib/lp/soyuz/subscribers/__init__.py b/lib/lp/soyuz/subscribers/__init__.py |
475 | new file mode 100644 |
476 | index 0000000..e69de29 |
477 | --- /dev/null |
478 | +++ b/lib/lp/soyuz/subscribers/__init__.py |
479 | diff --git a/lib/lp/soyuz/subscribers/livefsbuild.py b/lib/lp/soyuz/subscribers/livefsbuild.py |
480 | new file mode 100644 |
481 | index 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") |
528 | diff --git a/lib/lp/soyuz/tests/test_livefs.py b/lib/lp/soyuz/tests/test_livefs.py |
529 | index 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 | |
636 | diff --git a/lib/lp/soyuz/tests/test_livefsbuild.py b/lib/lp/soyuz/tests/test_livefsbuild.py |
637 | index 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. |
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.)