Merge lp:~twom/launchpad/snap-release-intents into lp:launchpad

Proposed by Tom Wardill
Status: Merged
Merged at revision: 18988
Proposed branch: lp:~twom/launchpad/snap-release-intents
Merge into: lp:launchpad
Diff against target: 679 lines (+52/-351)
9 files modified
lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt (+0/-5)
lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt (+0/-7)
lib/lp/snappy/interfaces/snapbuild.py (+3/-0)
lib/lp/snappy/interfaces/snapstoreclient.py (+0/-15)
lib/lp/snappy/mail/snapbuild.py (+0/-28)
lib/lp/snappy/model/snapbuildjob.py (+4/-21)
lib/lp/snappy/model/snapstoreclient.py (+8/-41)
lib/lp/snappy/tests/test_snapbuildjob.py (+2/-122)
lib/lp/snappy/tests/test_snapstoreclient.py (+35/-112)
To merge this branch: bzr merge lp:~twom/launchpad/snap-release-intents
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+369028@code.launchpad.net

Commit message

Use release intents to release snapbuilds to channels after building

Description of the change

Use the SCA support for 'release_intents' and pass the required parameters to produce a release to a channel after review stage.
Pass `channels` and `only_if_newer` to ensure that the build is only released if it is newer that the current existing build in the specified channels.

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

I think in general this looks OK, but I want to see a bit more effort to go through and root out vestiges of the old workflow.

review: Needs Fixing
Revision history for this message
Tom Wardill (twom) :
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
1=== removed file 'lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt'
2--- lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt 2016-06-30 16:44:14 +0000
3+++ lib/lp/snappy/emailtemplates/snapbuild-manualreview.txt 1970-01-01 00:00:00 +0000
4@@ -1,5 +0,0 @@
5-This snap package could not be released automatically because it was held
6-for manual review. Once it has been approved, you will need to release it
7-manually from here:
8-
9- %(store_url)s
10
11=== removed file 'lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt'
12--- lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt 2016-06-30 16:44:14 +0000
13+++ lib/lp/snappy/emailtemplates/snapbuild-releasefailed.txt 1970-01-01 00:00:00 +0000
14@@ -1,7 +0,0 @@
15-Launchpad asked the store to release this snap package, but it failed:
16-
17- %(store_error_message)s
18-
19-You can try to release it manually here:
20-
21- %(store_url)s
22
23=== modified file 'lib/lp/snappy/interfaces/snapbuild.py'
24--- lib/lp/snappy/interfaces/snapbuild.py 2019-05-22 18:33:10 +0000
25+++ lib/lp/snappy/interfaces/snapbuild.py 2019-06-19 16:07:26 +0000
26@@ -111,6 +111,9 @@
27 The last attempt to upload this snap build to the store failed.
28 """)
29
30+ # This is an impossible state for new releases (2019-06-19), due
31+ # to the store handling releases for us, however historical tasks
32+ # can have this status, so it is maintained here.
33 FAILEDTORELEASE = Item("""
34 Failed to release to channels
35
36
37=== modified file 'lib/lp/snappy/interfaces/snapstoreclient.py'
38--- lib/lp/snappy/interfaces/snapstoreclient.py 2018-05-02 23:55:51 +0000
39+++ lib/lp/snappy/interfaces/snapstoreclient.py 2019-06-19 16:07:26 +0000
40@@ -13,7 +13,6 @@
41 'BadSearchResponse',
42 'ISnapStoreClient',
43 'NeedsRefreshResponse',
44- 'ReleaseFailedResponse',
45 'ScanFailedResponse',
46 'SnapStoreError',
47 'UnauthorizedUploadResponse',
48@@ -75,10 +74,6 @@
49 pass
50
51
52-class ReleaseFailedResponse(SnapStoreError):
53- pass
54-
55-
56 class ISnapStoreClient(Interface):
57 """Interface for the API provided by the snap store."""
58
59@@ -150,13 +145,3 @@
60 :return: A list of dictionaries, one per channel, each of which
61 contains at least "name" and "display_name" keys.
62 """
63-
64- def release(snapbuild, revision):
65- """Tell the store to release a snap build to specified channels.
66-
67- :param snapbuild: The `ISnapBuild` to release.
68- :param revision: The revision returned by the store when uploading
69- the build.
70- :raises ReleaseFailedResponse: if the store failed to release the
71- build.
72- """
73
74=== modified file 'lib/lp/snappy/mail/snapbuild.py'
75--- lib/lp/snappy/mail/snapbuild.py 2017-06-12 16:37:40 +0000
76+++ lib/lp/snappy/mail/snapbuild.py 2019-06-19 16:07:26 +0000
77@@ -88,34 +88,6 @@
78 config.canonical.noreply_from_address,
79 "snap-build-upload-scan-failed", build)
80
81- @classmethod
82- def forManualReview(cls, build):
83- """Create a mailer for notifying about manual review.
84-
85- :param build: The relevant build.
86- """
87- requester = build.requester
88- recipients = {requester: RecipientReason.forBuildRequester(requester)}
89- return cls(
90- "%(snap_name)s held for manual review",
91- "snapbuild-manualreview.txt", recipients,
92- config.canonical.noreply_from_address,
93- "snap-build-release-manual-review", build)
94-
95- @classmethod
96- def forReleaseFailure(cls, build):
97- """Create a mailer for notifying about store release failures.
98-
99- :param build: The relevant build.
100- """
101- requester = build.requester
102- recipients = {requester: RecipientReason.forBuildRequester(requester)}
103- return cls(
104- "Store release failed for %(snap_name)s",
105- "snapbuild-releasefailed.txt", recipients,
106- config.canonical.noreply_from_address,
107- "snap-build-release-failed", build)
108-
109 def __init__(self, subject, template_name, recipients, from_address,
110 notification_type, build):
111 super(SnapBuildMailer, self).__init__(
112
113=== modified file 'lib/lp/snappy/model/snapbuildjob.py'
114--- lib/lp/snappy/model/snapbuildjob.py 2018-12-18 18:10:39 +0000
115+++ lib/lp/snappy/model/snapbuildjob.py 2019-06-19 16:07:26 +0000
116@@ -58,7 +58,6 @@
117 BadRefreshResponse,
118 BadScanStatusResponse,
119 ISnapStoreClient,
120- ReleaseFailedResponse,
121 ScanFailedResponse,
122 SnapStoreError,
123 UnauthorizedUploadResponse,
124@@ -169,10 +168,6 @@
125 return oops_vars
126
127
128-class ManualReview(SnapStoreError):
129- pass
130-
131-
132 class RetryableSnapStoreError(SnapStoreError):
133 pass
134
135@@ -192,8 +187,6 @@
136 user_error_types = (
137 UnauthorizedUploadResponse,
138 ScanFailedResponse,
139- ManualReview,
140- ReleaseFailedResponse,
141 )
142
143 retry_error_types = (UploadNotScannedYetResponse, RetryableSnapStoreError)
144@@ -341,16 +334,12 @@
145 # We made progress, so reset attempt_count.
146 self.attempt_count = 1
147 if self.store_url is None:
148+ # This is no longer strictly necessary as the store is handling
149+ # releases via the release intent, but we export various fields
150+ # via the api, so once this is called, we're done with
151+ # this task
152 self.store_url, self.store_revision = (
153 client.checkStatus(self.status_url))
154- # We made progress, so reset attempt_count.
155- self.attempt_count = 1
156- if self.snapbuild.snap.store_channels:
157- if self.store_revision is None:
158- raise ManualReview(
159- "Package held for manual review on the store; "
160- "cannot release it automatically.")
161- client.release(self.snapbuild, self.store_revision)
162 self.error_message = None
163 except self.retry_error_types:
164 raise
165@@ -373,12 +362,6 @@
166 elif isinstance(e, (BadScanStatusResponse, ScanFailedResponse)):
167 mailer = SnapBuildMailer.forUploadScanFailure(self.snapbuild)
168 mailer.sendAll()
169- elif isinstance(e, ManualReview):
170- mailer = SnapBuildMailer.forManualReview(self.snapbuild)
171- mailer.sendAll()
172- elif isinstance(e, ReleaseFailedResponse):
173- mailer = SnapBuildMailer.forReleaseFailure(self.snapbuild)
174- mailer.sendAll()
175 # The normal job infrastructure will abort the transaction, but
176 # we want to commit instead: the only database changes we make
177 # are to this job's metadata and should be preserved.
178
179=== modified file 'lib/lp/snappy/model/snapstoreclient.py'
180--- lib/lp/snappy/model/snapstoreclient.py 2019-06-14 14:46:15 +0000
181+++ lib/lp/snappy/model/snapstoreclient.py 2019-06-19 16:07:26 +0000
182@@ -43,7 +43,6 @@
183 BadSearchResponse,
184 ISnapStoreClient,
185 NeedsRefreshResponse,
186- ReleaseFailedResponse,
187 ScanFailedResponse,
188 UnauthorizedUploadResponse,
189 UploadFailedResponse,
190@@ -266,6 +265,14 @@
191 "series": snap.store_series.name,
192 "built_at": snapbuild.date_started.isoformat(),
193 }
194+ # The security proxy is useless and breaks JSON serialisation.
195+ channels = removeSecurityProxy(snap.store_channels)
196+ if channels:
197+ # This will cause a release
198+ data.update({
199+ "channels": channels,
200+ "only_if_newer": True,
201+ })
202 # XXX cjwatson 2016-05-09: This should add timeline information, but
203 # that's currently difficult in jobs.
204 try:
205@@ -346,8 +353,6 @@
206 error_messages.append(error_detail)
207 raise ScanFailedResponse(
208 error_message, messages=error_messages)
209- elif not response_data["can_release"]:
210- return response_data["url"], None
211 else:
212 return response_data["url"], response_data["revision"]
213 except requests.HTTPError as e:
214@@ -394,41 +399,3 @@
215 if channels is None:
216 channels = _default_store_channels
217 return channels
218-
219- @classmethod
220- def _release(cls, snap, revision):
221- """Release a snap revision to specified channels."""
222- release_url = urlappend(
223- config.snappy.store_url, "dev/api/snap-release/")
224- data = {
225- "name": snap.store_name,
226- "revision": revision,
227- # The security proxy is useless and breaks JSON serialisation.
228- "channels": removeSecurityProxy(snap.store_channels),
229- "series": snap.store_series.name,
230- }
231- # XXX cjwatson 2016-06-28: This should add timeline information, but
232- # that's currently difficult in jobs.
233- try:
234- assert snap.store_secrets is not None
235- urlfetch(
236- release_url, method="POST", json=data,
237- auth=MacaroonAuth(
238- snap.store_secrets["root"],
239- snap.store_secrets.get("discharge")))
240- except requests.HTTPError as e:
241- if e.response.status_code == 401:
242- if (e.response.headers.get("WWW-Authenticate") ==
243- "Macaroon needs_refresh=1"):
244- raise NeedsRefreshResponse()
245- raise cls._makeSnapStoreError(ReleaseFailedResponse, e)
246-
247- @classmethod
248- def release(cls, snapbuild, revision):
249- """See `ISnapStoreClient`."""
250- assert config.snappy.store_url is not None
251- snap = snapbuild.snap
252- assert snap.store_name is not None
253- assert snap.store_series is not None
254- assert snap.store_channels
255- cls.refreshIfNecessary(snap, cls._release, snap, revision)
256
257=== modified file 'lib/lp/snappy/tests/test_snapbuildjob.py'
258--- lib/lp/snappy/tests/test_snapbuildjob.py 2019-05-22 14:57:45 +0000
259+++ lib/lp/snappy/tests/test_snapbuildjob.py 2019-06-19 16:07:26 +0000
260@@ -35,7 +35,6 @@
261 from lp.snappy.interfaces.snapstoreclient import (
262 BadRefreshResponse,
263 ISnapStoreClient,
264- ReleaseFailedResponse,
265 ScanFailedResponse,
266 UnauthorizedUploadResponse,
267 UploadFailedResponse,
268@@ -65,7 +64,6 @@
269 self.upload = FakeMethod()
270 self.checkStatus = FakeMethod()
271 self.listChannels = FakeMethod(result=[])
272- self.release = FakeMethod()
273
274
275 class TestSnapBuildJob(TestCaseWithFactory):
276@@ -160,7 +158,6 @@
277 JobRunner([job]).runAll()
278 self.assertEqual([((snapbuild,), {})], client.upload.calls)
279 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
280- self.assertEqual([], client.release.calls)
281 self.assertContentEqual([job], snapbuild.store_upload_jobs)
282 self.assertEqual(self.store_url, job.store_url)
283 self.assertEqual(1, job.store_revision)
284@@ -180,7 +177,6 @@
285 JobRunner([job]).runAll()
286 self.assertEqual([((snapbuild,), {})], client.upload.calls)
287 self.assertEqual([], client.checkStatus.calls)
288- self.assertEqual([], client.release.calls)
289 self.assertContentEqual([job], snapbuild.store_upload_jobs)
290 self.assertIsNone(job.store_url)
291 self.assertIsNone(job.store_revision)
292@@ -206,7 +202,6 @@
293 JobRunner([job]).runAll()
294 self.assertEqual([((snapbuild,), {})], client.upload.calls)
295 self.assertEqual([], client.checkStatus.calls)
296- self.assertEqual([], client.release.calls)
297 self.assertContentEqual([job], snapbuild.store_upload_jobs)
298 self.assertIsNone(job.store_url)
299 self.assertIsNone(job.store_revision)
300@@ -253,7 +248,6 @@
301 JobRunner([job]).runAll()
302 self.assertEqual([((snapbuild,), {})], client.upload.calls)
303 self.assertEqual([], client.checkStatus.calls)
304- self.assertEqual([], client.release.calls)
305 self.assertContentEqual([job], snapbuild.store_upload_jobs)
306 self.assertIsNone(job.store_url)
307 self.assertIsNone(job.store_revision)
308@@ -272,7 +266,6 @@
309 JobRunner([job]).runAll()
310 self.assertEqual([((snapbuild,), {})], client.upload.calls)
311 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
312- self.assertEqual([], client.release.calls)
313 self.assertContentEqual([job], snapbuild.store_upload_jobs)
314 self.assertEqual(self.store_url, job.store_url)
315 self.assertEqual(1, job.store_revision)
316@@ -298,7 +291,6 @@
317 JobRunner([job]).runAll()
318 self.assertEqual([((snapbuild,), {})], client.upload.calls)
319 self.assertEqual([], client.checkStatus.calls)
320- self.assertEqual([], client.release.calls)
321 self.assertContentEqual([job], snapbuild.store_upload_jobs)
322 self.assertIsNone(job.store_url)
323 self.assertIsNone(job.store_revision)
324@@ -349,7 +341,6 @@
325 JobRunner([job]).runAll()
326 self.assertEqual([((snapbuild,), {})], client.upload.calls)
327 self.assertEqual([], client.checkStatus.calls)
328- self.assertEqual([], client.release.calls)
329 self.assertContentEqual([job], snapbuild.store_upload_jobs)
330 self.assertIsNone(job.store_url)
331 self.assertIsNone(job.store_revision)
332@@ -399,7 +390,6 @@
333 JobRunner([job]).runAll()
334 self.assertEqual([((snapbuild,), {})], client.upload.calls)
335 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
336- self.assertEqual([], client.release.calls)
337 self.assertContentEqual([job], snapbuild.store_upload_jobs)
338 self.assertIsNone(job.store_url)
339 self.assertIsNone(job.store_revision)
340@@ -418,7 +408,6 @@
341 JobRunner([job]).runAll()
342 self.assertEqual([], client.upload.calls)
343 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
344- self.assertEqual([], client.release.calls)
345 self.assertContentEqual([job], snapbuild.store_upload_jobs)
346 self.assertEqual(self.store_url, job.store_url)
347 self.assertEqual(1, job.store_revision)
348@@ -448,7 +437,6 @@
349 JobRunner([job]).runAll()
350 self.assertEqual([((snapbuild,), {})], client.upload.calls)
351 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
352- self.assertEqual([], client.release.calls)
353 self.assertContentEqual([job], snapbuild.store_upload_jobs)
354 self.assertIsNone(job.store_url)
355 self.assertIsNone(job.store_revision)
356@@ -497,7 +485,6 @@
357 JobRunner([job]).runAll()
358 self.assertEqual([((snapbuild,), {})], client.upload.calls)
359 self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
360- self.assertEqual([((snapbuild, 1), {})], client.release.calls)
361 self.assertContentEqual([job], snapbuild.store_upload_jobs)
362 self.assertEqual(self.store_url, job.store_url)
363 self.assertEqual(1, job.store_revision)
364@@ -505,110 +492,6 @@
365 self.assertEqual([], pop_notifications())
366 self.assertWebhookDeliveries(snapbuild, ["Pending", "Uploaded"])
367
368- def test_run_release_manual_review_notifies(self):
369- # A run configured to automatically release the package to certain
370- # channels but that encounters the manual review state on upload
371- # sends mail.
372- requester = self.factory.makePerson(name="requester")
373- requester_team = self.factory.makeTeam(
374- owner=requester, name="requester-team", members=[requester])
375- snapbuild = self.makeSnapBuild(
376- requester=requester_team, name="test-snap", owner=requester_team,
377- store_channels=["stable", "edge"])
378- self.assertContentEqual([], snapbuild.store_upload_jobs)
379- job = SnapStoreUploadJob.create(snapbuild)
380- client = FakeSnapStoreClient()
381- client.upload.result = self.status_url
382- client.checkStatus.result = (self.store_url, None)
383- self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
384- with dbuser(config.ISnapStoreUploadJobSource.dbuser):
385- JobRunner([job]).runAll()
386- self.assertEqual([((snapbuild,), {})], client.upload.calls)
387- self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
388- self.assertEqual([], client.release.calls)
389- self.assertContentEqual([job], snapbuild.store_upload_jobs)
390- self.assertEqual(self.store_url, job.store_url)
391- self.assertIsNone(job.store_revision)
392- self.assertEqual(
393- "Package held for manual review on the store; "
394- "cannot release it automatically.",
395- job.error_message)
396- [notification] = pop_notifications()
397- self.assertEqual(
398- config.canonical.noreply_from_address, notification["From"])
399- self.assertEqual(
400- "Requester <%s>" % requester.preferredemail.email,
401- notification["To"])
402- subject = notification["Subject"].replace("\n ", " ")
403- self.assertEqual("test-snap held for manual review", subject)
404- self.assertEqual(
405- "Requester @requester-team",
406- notification["X-Launchpad-Message-Rationale"])
407- self.assertEqual(
408- requester_team.name, notification["X-Launchpad-Message-For"])
409- self.assertEqual(
410- "snap-build-release-manual-review",
411- notification["X-Launchpad-Notification-Type"])
412- body, footer = notification.get_payload(decode=True).split("\n-- \n")
413- self.assertIn(self.store_url, body)
414- self.assertEqual(
415- "http://launchpad.test/~requester-team/+snap/test-snap/+build/%d\n"
416- "Your team Requester Team is the requester of the build.\n" %
417- snapbuild.id, footer)
418- self.assertWebhookDeliveries(
419- snapbuild, ["Pending", "Failed to release to channels"])
420-
421- def test_run_release_failure_notifies(self):
422- # A run configured to automatically release the package to certain
423- # channels but that fails to do so sends mail.
424- requester = self.factory.makePerson(name="requester")
425- requester_team = self.factory.makeTeam(
426- owner=requester, name="requester-team", members=[requester])
427- snapbuild = self.makeSnapBuild(
428- requester=requester_team, name="test-snap", owner=requester_team,
429- store_channels=["stable", "edge"])
430- self.assertContentEqual([], snapbuild.store_upload_jobs)
431- job = SnapStoreUploadJob.create(snapbuild)
432- client = FakeSnapStoreClient()
433- client.upload.result = self.status_url
434- client.checkStatus.result = (self.store_url, 1)
435- client.release.failure = ReleaseFailedResponse("Failed to publish")
436- self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
437- with dbuser(config.ISnapStoreUploadJobSource.dbuser):
438- JobRunner([job]).runAll()
439- self.assertEqual([((snapbuild,), {})], client.upload.calls)
440- self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
441- self.assertEqual([((snapbuild, 1), {})], client.release.calls)
442- self.assertContentEqual([job], snapbuild.store_upload_jobs)
443- self.assertEqual(self.store_url, job.store_url)
444- self.assertEqual(1, job.store_revision)
445- self.assertEqual("Failed to publish", job.error_message)
446- [notification] = pop_notifications()
447- self.assertEqual(
448- config.canonical.noreply_from_address, notification["From"])
449- self.assertEqual(
450- "Requester <%s>" % requester.preferredemail.email,
451- notification["To"])
452- subject = notification["Subject"].replace("\n ", " ")
453- self.assertEqual("Store release failed for test-snap", subject)
454- self.assertEqual(
455- "Requester @requester-team",
456- notification["X-Launchpad-Message-Rationale"])
457- self.assertEqual(
458- requester_team.name, notification["X-Launchpad-Message-For"])
459- self.assertEqual(
460- "snap-build-release-failed",
461- notification["X-Launchpad-Notification-Type"])
462- body, footer = notification.get_payload(decode=True).split("\n-- \n")
463- self.assertIn("Failed to publish", body)
464- self.assertIn(self.store_url, body)
465- self.assertEqual(
466- "http://launchpad.test/~requester-team/+snap/test-snap/+build/%d\n"
467- "Your team Requester Team is the requester of the build.\n" %
468- snapbuild.id, footer)
469- self.assertWebhookDeliveries(
470- snapbuild, ["Pending", "Failed to release to channels"])
471-
472 def test_retry_delay(self):
473 # The job is retried every minute, unless it just made one of its
474 # first four attempts to poll the status endpoint, in which case the
475@@ -655,15 +538,12 @@
476 client = FakeSnapStoreClient()
477 client.upload.result = self.status_url
478 client.checkStatus.result = (self.store_url, 1)
479- client.release.failure = UploadFailedResponse(
480- "Proxy error", can_retry=True)
481 self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
482 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
483 JobRunner([job]).runAll()
484
485 previous_upload = client.upload.calls
486 previous_checkStatus = client.checkStatus.calls
487- len_previous_release = len(client.release.calls)
488
489 # Check we uploaded as expected
490 self.assertEqual(self.store_url, job.store_url)
491@@ -676,10 +556,10 @@
492 with dbuser(config.ISnapStoreUploadJobSource.dbuser):
493 JobRunner([job]).runAll()
494
495- # We should not have called `upload`, but moved straight to `release`
496+ # Release is not called due to release intent in upload
497+ # but ensure that we have not called upload twice
498 self.assertEqual(previous_upload, client.upload.calls)
499 self.assertEqual(previous_checkStatus, client.checkStatus.calls)
500- self.assertEqual(len_previous_release + 1, len(client.release.calls))
501 self.assertIsNone(job.error_message)
502
503 def test_with_snapbuild_metadata_as_none(self):
504
505=== modified file 'lib/lp/snappy/tests/test_snapstoreclient.py'
506--- lib/lp/snappy/tests/test_snapstoreclient.py 2019-06-14 14:49:25 +0000
507+++ lib/lp/snappy/tests/test_snapstoreclient.py 2019-06-19 16:07:26 +0000
508@@ -47,7 +47,6 @@
509 BadScanStatusResponse,
510 BadSearchResponse,
511 ISnapStoreClient,
512- ReleaseFailedResponse,
513 ScanFailedResponse,
514 UnauthorizedUploadResponse,
515 UploadFailedResponse,
516@@ -282,20 +281,6 @@
517 self.addCleanup(
518 getUtility(IMemcacheClient).delete, self.channels_memcache_key)
519
520- def _addSnapReleaseResponse(self):
521- responses.add(
522- "POST", "http://sca.example/dev/api/snap-release/",
523- json={
524- "success": True,
525- "channel_map": [
526- {"channel": "stable", "info": "specific",
527- "version": "1.0", "revision": 1},
528- {"channel": "edge", "info": "specific",
529- "version": "1.0", "revision": 1},
530- ],
531- "opened_channels": ["stable", "edge"],
532- })
533-
534 @responses.activate
535 def test_requestPackageUploadPermission(self):
536 snappy_series = self.factory.makeSnappySeries(name="rolling")
537@@ -402,6 +387,41 @@
538 ]))
539
540 @responses.activate
541+ def test_upload_with_release_intent(self):
542+ snapbuild = self.makeUploadableSnapBuild()
543+ snapbuild.snap.store_channels = ['beta', 'edge']
544+ transaction.commit()
545+ self._addUnscannedUploadResponse()
546+ self._addSnapPushResponse()
547+ with dbuser(config.ISnapStoreUploadJobSource.dbuser):
548+ self.assertEqual(
549+ "http://sca.example/dev/api/snaps/1/builds/1/status",
550+ self.client.upload(snapbuild))
551+ requests = [call.request for call in responses.calls]
552+ self.assertThat(requests, MatchesListwise([
553+ RequestMatches(
554+ url=Equals("http://updown.example/unscanned-upload/"),
555+ method=Equals("POST"),
556+ form_data={
557+ "binary": MatchesStructure.byEquality(
558+ name="binary", filename="test-snap.snap",
559+ value="dummy snap content",
560+ type="application/octet-stream",
561+ )}),
562+ RequestMatches(
563+ url=Equals("http://sca.example/dev/api/snap-push/"),
564+ method=Equals("POST"),
565+ headers=ContainsDict(
566+ {"Content-Type": Equals("application/json")}),
567+ auth=("Macaroon", MacaroonsVerify(self.root_key)),
568+ json_data={
569+ "name": "test-snap", "updown_id": 1, "series": "rolling",
570+ "built_at": snapbuild.date_started.isoformat(),
571+ "only_if_newer": True, "channels": ['beta', 'edge'],
572+ }),
573+ ]))
574+
575+ @responses.activate
576 def test_upload_no_discharge(self):
577 root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
578 root_macaroon = Macaroon(key=root_key)
579@@ -638,100 +658,3 @@
580 self.assertContentEqual([], responses.calls)
581 self.assertIsNone(
582 getUtility(IMemcacheClient).get(self.channels_memcache_key))
583-
584- @responses.activate
585- def test_release(self):
586- snap = self.factory.makeSnap(
587- store_upload=True,
588- store_series=self.factory.makeSnappySeries(name="rolling"),
589- store_name="test-snap",
590- store_secrets=self._make_store_secrets(),
591- store_channels=["stable", "edge"])
592- snapbuild = self.factory.makeSnapBuild(snap=snap)
593- self._addSnapReleaseResponse()
594- self.client.release(snapbuild, 1)
595- self.assertThat(responses.calls[-1].request, RequestMatches(
596- url=Equals("http://sca.example/dev/api/snap-release/"),
597- method=Equals("POST"),
598- headers=ContainsDict({"Content-Type": Equals("application/json")}),
599- auth=("Macaroon", MacaroonsVerify(self.root_key)),
600- json_data={
601- "name": "test-snap", "revision": 1,
602- "channels": ["stable", "edge"], "series": "rolling",
603- }))
604-
605- @responses.activate
606- def test_release_no_discharge(self):
607- root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
608- root_macaroon = Macaroon(key=root_key)
609- snap = self.factory.makeSnap(
610- store_upload=True,
611- store_series=self.factory.makeSnappySeries(name="rolling"),
612- store_name="test-snap",
613- store_secrets={"root": root_macaroon.serialize()},
614- store_channels=["stable", "edge"])
615- snapbuild = self.factory.makeSnapBuild(snap=snap)
616- self._addSnapReleaseResponse()
617- self.client.release(snapbuild, 1)
618- self.assertThat(responses.calls[-1].request, RequestMatches(
619- url=Equals("http://sca.example/dev/api/snap-release/"),
620- method=Equals("POST"),
621- headers=ContainsDict({"Content-Type": Equals("application/json")}),
622- auth=("Macaroon", MacaroonsVerify(root_key)),
623- json_data={
624- "name": "test-snap", "revision": 1,
625- "channels": ["stable", "edge"], "series": "rolling",
626- }))
627-
628- @responses.activate
629- def test_release_needs_discharge_macaroon_refresh(self):
630- store_secrets = self._make_store_secrets()
631- snap = self.factory.makeSnap(
632- store_upload=True,
633- store_series=self.factory.makeSnappySeries(name="rolling"),
634- store_name="test-snap", store_secrets=store_secrets,
635- store_channels=["stable", "edge"])
636- snapbuild = self.factory.makeSnapBuild(snap=snap)
637- responses.add(
638- "POST", "http://sca.example/dev/api/snap-release/", status=401,
639- headers={"WWW-Authenticate": "Macaroon needs_refresh=1"})
640- self._addMacaroonRefreshResponse()
641- self._addSnapReleaseResponse()
642- self.client.release(snapbuild, 1)
643- requests = [call.request for call in responses.calls]
644- self.assertThat(requests, MatchesListwise([
645- MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
646- MatchesStructure.byEquality(path_url="/api/v2/tokens/refresh"),
647- MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
648- ]))
649- self.assertNotEqual(
650- store_secrets["discharge"], snap.store_secrets["discharge"])
651-
652- @responses.activate
653- def test_release_error(self):
654- snap = self.factory.makeSnap(
655- store_upload=True,
656- store_series=self.factory.makeSnappySeries(name="rolling"),
657- store_name="test-snap", store_secrets=self._make_store_secrets(),
658- store_channels=["stable", "edge"])
659- snapbuild = self.factory.makeSnapBuild(snap=snap)
660- responses.add(
661- "POST", "http://sca.example/dev/api/snap-release/", status=503,
662- json={"error_list": [{"message": "Failed to publish"}]})
663- self.assertRaisesWithContent(
664- ReleaseFailedResponse, "Failed to publish",
665- self.client.release, snapbuild, 1)
666-
667- @responses.activate
668- def test_release_404(self):
669- snap = self.factory.makeSnap(
670- store_upload=True,
671- store_series=self.factory.makeSnappySeries(name="rolling"),
672- store_name="test-snap", store_secrets=self._make_store_secrets(),
673- store_channels=["stable", "edge"])
674- snapbuild = self.factory.makeSnapBuild(snap=snap)
675- responses.add(
676- "POST", "http://sca.example/dev/api/snap-release/", status=404)
677- self.assertRaisesWithContent(
678- ReleaseFailedResponse, b"404 Client Error: Not Found",
679- self.client.release, snapbuild, 1)