← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Tom Wardill has proposed merging lp:~twom/launchpad/snap-release-intents into lp:launchpad.

Commit message:
Use release intents to release snapbuilds to channels after building

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~twom/launchpad/snap-release-intents/+merge/369028

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.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~twom/launchpad/snap-release-intents into lp:launchpad.
=== modified file 'lib/lp/snappy/model/snapbuildjob.py'
--- lib/lp/snappy/model/snapbuildjob.py	2018-12-18 18:10:39 +0000
+++ lib/lp/snappy/model/snapbuildjob.py	2019-06-19 10:53:29 +0000
@@ -350,7 +350,6 @@
                     raise ManualReview(
                         "Package held for manual review on the store; "
                         "cannot release it automatically.")
-                client.release(self.snapbuild, self.store_revision)
             self.error_message = None
         except self.retry_error_types:
             raise

=== modified file 'lib/lp/snappy/model/snapstoreclient.py'
--- lib/lp/snappy/model/snapstoreclient.py	2019-06-14 14:46:15 +0000
+++ lib/lp/snappy/model/snapstoreclient.py	2019-06-19 10:53:29 +0000
@@ -266,6 +266,14 @@
             "series": snap.store_series.name,
             "built_at": snapbuild.date_started.isoformat(),
             }
+        # The security proxy is useless and breaks JSON serialisation.
+        channels = removeSecurityProxy(snap.store_channels)
+        if channels:
+            # This will cause a release
+            data.update({
+                "channels": channels,
+                "only_if_newer": True,
+            })
         # XXX cjwatson 2016-05-09: This should add timeline information, but
         # that's currently difficult in jobs.
         try:
@@ -394,41 +402,3 @@
         if channels is None:
             channels = _default_store_channels
         return channels
-
-    @classmethod
-    def _release(cls, snap, revision):
-        """Release a snap revision to specified channels."""
-        release_url = urlappend(
-            config.snappy.store_url, "dev/api/snap-release/")
-        data = {
-            "name": snap.store_name,
-            "revision": revision,
-            # The security proxy is useless and breaks JSON serialisation.
-            "channels": removeSecurityProxy(snap.store_channels),
-            "series": snap.store_series.name,
-            }
-        # XXX cjwatson 2016-06-28: This should add timeline information, but
-        # that's currently difficult in jobs.
-        try:
-            assert snap.store_secrets is not None
-            urlfetch(
-                release_url, method="POST", json=data,
-                auth=MacaroonAuth(
-                    snap.store_secrets["root"],
-                    snap.store_secrets.get("discharge")))
-        except requests.HTTPError as e:
-            if e.response.status_code == 401:
-                if (e.response.headers.get("WWW-Authenticate") ==
-                        "Macaroon needs_refresh=1"):
-                    raise NeedsRefreshResponse()
-            raise cls._makeSnapStoreError(ReleaseFailedResponse, e)
-
-    @classmethod
-    def release(cls, snapbuild, revision):
-        """See `ISnapStoreClient`."""
-        assert config.snappy.store_url is not None
-        snap = snapbuild.snap
-        assert snap.store_name is not None
-        assert snap.store_series is not None
-        assert snap.store_channels
-        cls.refreshIfNecessary(snap, cls._release, snap, revision)

=== modified file 'lib/lp/snappy/tests/test_snapbuildjob.py'
--- lib/lp/snappy/tests/test_snapbuildjob.py	2019-05-22 14:57:45 +0000
+++ lib/lp/snappy/tests/test_snapbuildjob.py	2019-06-19 10:53:29 +0000
@@ -65,7 +65,6 @@
         self.upload = FakeMethod()
         self.checkStatus = FakeMethod()
         self.listChannels = FakeMethod(result=[])
-        self.release = FakeMethod()
 
 
 class TestSnapBuildJob(TestCaseWithFactory):
@@ -160,7 +159,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertEqual(self.store_url, job.store_url)
         self.assertEqual(1, job.store_revision)
@@ -180,7 +178,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.store_url)
         self.assertIsNone(job.store_revision)
@@ -206,7 +203,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.store_url)
         self.assertIsNone(job.store_revision)
@@ -253,7 +249,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.store_url)
         self.assertIsNone(job.store_revision)
@@ -272,7 +267,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertEqual(self.store_url, job.store_url)
         self.assertEqual(1, job.store_revision)
@@ -298,7 +292,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.store_url)
         self.assertIsNone(job.store_revision)
@@ -349,7 +342,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.store_url)
         self.assertIsNone(job.store_revision)
@@ -399,7 +391,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.store_url)
         self.assertIsNone(job.store_revision)
@@ -418,7 +409,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([], client.upload.calls)
         self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertEqual(self.store_url, job.store_url)
         self.assertEqual(1, job.store_revision)
@@ -448,7 +438,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertIsNone(job.store_url)
         self.assertIsNone(job.store_revision)
@@ -497,7 +486,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([((snapbuild, 1), {})], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertEqual(self.store_url, job.store_url)
         self.assertEqual(1, job.store_revision)
@@ -525,7 +513,6 @@
             JobRunner([job]).runAll()
         self.assertEqual([((snapbuild,), {})], client.upload.calls)
         self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([], client.release.calls)
         self.assertContentEqual([job], snapbuild.store_upload_jobs)
         self.assertEqual(self.store_url, job.store_url)
         self.assertIsNone(job.store_revision)
@@ -558,57 +545,6 @@
         self.assertWebhookDeliveries(
             snapbuild, ["Pending", "Failed to release to channels"])
 
-    def test_run_release_failure_notifies(self):
-        # A run configured to automatically release the package to certain
-        # channels but that fails to do so sends mail.
-        requester = self.factory.makePerson(name="requester")
-        requester_team = self.factory.makeTeam(
-            owner=requester, name="requester-team", members=[requester])
-        snapbuild = self.makeSnapBuild(
-            requester=requester_team, name="test-snap", owner=requester_team,
-            store_channels=["stable", "edge"])
-        self.assertContentEqual([], snapbuild.store_upload_jobs)
-        job = SnapStoreUploadJob.create(snapbuild)
-        client = FakeSnapStoreClient()
-        client.upload.result = self.status_url
-        client.checkStatus.result = (self.store_url, 1)
-        client.release.failure = ReleaseFailedResponse("Failed to publish")
-        self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
-        with dbuser(config.ISnapStoreUploadJobSource.dbuser):
-            JobRunner([job]).runAll()
-        self.assertEqual([((snapbuild,), {})], client.upload.calls)
-        self.assertEqual([((self.status_url,), {})], client.checkStatus.calls)
-        self.assertEqual([((snapbuild, 1), {})], client.release.calls)
-        self.assertContentEqual([job], snapbuild.store_upload_jobs)
-        self.assertEqual(self.store_url, job.store_url)
-        self.assertEqual(1, job.store_revision)
-        self.assertEqual("Failed to publish", job.error_message)
-        [notification] = pop_notifications()
-        self.assertEqual(
-            config.canonical.noreply_from_address, notification["From"])
-        self.assertEqual(
-            "Requester <%s>" % requester.preferredemail.email,
-            notification["To"])
-        subject = notification["Subject"].replace("\n ", " ")
-        self.assertEqual("Store release failed for test-snap", subject)
-        self.assertEqual(
-            "Requester @requester-team",
-            notification["X-Launchpad-Message-Rationale"])
-        self.assertEqual(
-            requester_team.name, notification["X-Launchpad-Message-For"])
-        self.assertEqual(
-            "snap-build-release-failed",
-            notification["X-Launchpad-Notification-Type"])
-        body, footer = notification.get_payload(decode=True).split("\n-- \n")
-        self.assertIn("Failed to publish", body)
-        self.assertIn(self.store_url, body)
-        self.assertEqual(
-            "http://launchpad.test/~requester-team/+snap/test-snap/+build/%d\n";
-            "Your team Requester Team is the requester of the build.\n" %
-            snapbuild.id, footer)
-        self.assertWebhookDeliveries(
-            snapbuild, ["Pending", "Failed to release to channels"])
-
     def test_retry_delay(self):
         # The job is retried every minute, unless it just made one of its
         # first four attempts to poll the status endpoint, in which case the
@@ -655,15 +591,12 @@
         client = FakeSnapStoreClient()
         client.upload.result = self.status_url
         client.checkStatus.result = (self.store_url, 1)
-        client.release.failure = UploadFailedResponse(
-            "Proxy error", can_retry=True)
         self.useFixture(ZopeUtilityFixture(client, ISnapStoreClient))
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
             JobRunner([job]).runAll()
 
         previous_upload = client.upload.calls
         previous_checkStatus = client.checkStatus.calls
-        len_previous_release = len(client.release.calls)
 
         # Check we uploaded as expected
         self.assertEqual(self.store_url, job.store_url)
@@ -676,10 +609,10 @@
         with dbuser(config.ISnapStoreUploadJobSource.dbuser):
             JobRunner([job]).runAll()
 
-        # We should not have called `upload`, but moved straight to `release`
+        # Release is not called due to release intent in upload
+        # but ensure that we have not called upload twice
         self.assertEqual(previous_upload, client.upload.calls)
         self.assertEqual(previous_checkStatus, client.checkStatus.calls)
-        self.assertEqual(len_previous_release + 1, len(client.release.calls))
         self.assertIsNone(job.error_message)
 
     def test_with_snapbuild_metadata_as_none(self):

=== modified file 'lib/lp/snappy/tests/test_snapstoreclient.py'
--- lib/lp/snappy/tests/test_snapstoreclient.py	2019-06-14 14:49:25 +0000
+++ lib/lp/snappy/tests/test_snapstoreclient.py	2019-06-19 10:53:29 +0000
@@ -402,6 +402,41 @@
             ]))
 
     @responses.activate
+    def test_upload_with_release_intent(self):
+        snapbuild = self.makeUploadableSnapBuild()
+        snapbuild.snap.store_channels = ['beta', 'edge']
+        transaction.commit()
+        self._addUnscannedUploadResponse()
+        self._addSnapPushResponse()
+        with dbuser(config.ISnapStoreUploadJobSource.dbuser):
+            self.assertEqual(
+                "http://sca.example/dev/api/snaps/1/builds/1/status";,
+                self.client.upload(snapbuild))
+        requests = [call.request for call in responses.calls]
+        self.assertThat(requests, MatchesListwise([
+            RequestMatches(
+                url=Equals("http://updown.example/unscanned-upload/";),
+                method=Equals("POST"),
+                form_data={
+                    "binary": MatchesStructure.byEquality(
+                        name="binary", filename="test-snap.snap",
+                        value="dummy snap content",
+                        type="application/octet-stream",
+                        )}),
+            RequestMatches(
+                url=Equals("http://sca.example/dev/api/snap-push/";),
+                method=Equals("POST"),
+                headers=ContainsDict(
+                    {"Content-Type": Equals("application/json")}),
+                auth=("Macaroon", MacaroonsVerify(self.root_key)),
+                json_data={
+                    "name": "test-snap", "updown_id": 1, "series": "rolling",
+                    "built_at": snapbuild.date_started.isoformat(),
+                    "only_if_newer": True, "channels": ['beta', 'edge'],
+                    }),
+            ]))
+
+    @responses.activate
     def test_upload_no_discharge(self):
         root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
         root_macaroon = Macaroon(key=root_key)
@@ -638,100 +673,3 @@
         self.assertContentEqual([], responses.calls)
         self.assertIsNone(
             getUtility(IMemcacheClient).get(self.channels_memcache_key))
-
-    @responses.activate
-    def test_release(self):
-        snap = self.factory.makeSnap(
-            store_upload=True,
-            store_series=self.factory.makeSnappySeries(name="rolling"),
-            store_name="test-snap",
-            store_secrets=self._make_store_secrets(),
-            store_channels=["stable", "edge"])
-        snapbuild = self.factory.makeSnapBuild(snap=snap)
-        self._addSnapReleaseResponse()
-        self.client.release(snapbuild, 1)
-        self.assertThat(responses.calls[-1].request, RequestMatches(
-            url=Equals("http://sca.example/dev/api/snap-release/";),
-            method=Equals("POST"),
-            headers=ContainsDict({"Content-Type": Equals("application/json")}),
-            auth=("Macaroon", MacaroonsVerify(self.root_key)),
-            json_data={
-                "name": "test-snap", "revision": 1,
-                "channels": ["stable", "edge"], "series": "rolling",
-                }))
-
-    @responses.activate
-    def test_release_no_discharge(self):
-        root_key = hashlib.sha256(self.factory.getUniqueString()).hexdigest()
-        root_macaroon = Macaroon(key=root_key)
-        snap = self.factory.makeSnap(
-            store_upload=True,
-            store_series=self.factory.makeSnappySeries(name="rolling"),
-            store_name="test-snap",
-            store_secrets={"root": root_macaroon.serialize()},
-            store_channels=["stable", "edge"])
-        snapbuild = self.factory.makeSnapBuild(snap=snap)
-        self._addSnapReleaseResponse()
-        self.client.release(snapbuild, 1)
-        self.assertThat(responses.calls[-1].request, RequestMatches(
-            url=Equals("http://sca.example/dev/api/snap-release/";),
-            method=Equals("POST"),
-            headers=ContainsDict({"Content-Type": Equals("application/json")}),
-            auth=("Macaroon", MacaroonsVerify(root_key)),
-            json_data={
-                "name": "test-snap", "revision": 1,
-                "channels": ["stable", "edge"], "series": "rolling",
-                }))
-
-    @responses.activate
-    def test_release_needs_discharge_macaroon_refresh(self):
-        store_secrets = self._make_store_secrets()
-        snap = self.factory.makeSnap(
-            store_upload=True,
-            store_series=self.factory.makeSnappySeries(name="rolling"),
-            store_name="test-snap", store_secrets=store_secrets,
-            store_channels=["stable", "edge"])
-        snapbuild = self.factory.makeSnapBuild(snap=snap)
-        responses.add(
-            "POST", "http://sca.example/dev/api/snap-release/";, status=401,
-            headers={"WWW-Authenticate": "Macaroon needs_refresh=1"})
-        self._addMacaroonRefreshResponse()
-        self._addSnapReleaseResponse()
-        self.client.release(snapbuild, 1)
-        requests = [call.request for call in responses.calls]
-        self.assertThat(requests, MatchesListwise([
-            MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
-            MatchesStructure.byEquality(path_url="/api/v2/tokens/refresh"),
-            MatchesStructure.byEquality(path_url="/dev/api/snap-release/"),
-            ]))
-        self.assertNotEqual(
-            store_secrets["discharge"], snap.store_secrets["discharge"])
-
-    @responses.activate
-    def test_release_error(self):
-        snap = self.factory.makeSnap(
-            store_upload=True,
-            store_series=self.factory.makeSnappySeries(name="rolling"),
-            store_name="test-snap", store_secrets=self._make_store_secrets(),
-            store_channels=["stable", "edge"])
-        snapbuild = self.factory.makeSnapBuild(snap=snap)
-        responses.add(
-            "POST", "http://sca.example/dev/api/snap-release/";, status=503,
-            json={"error_list": [{"message": "Failed to publish"}]})
-        self.assertRaisesWithContent(
-            ReleaseFailedResponse, "Failed to publish",
-            self.client.release, snapbuild, 1)
-
-    @responses.activate
-    def test_release_404(self):
-        snap = self.factory.makeSnap(
-            store_upload=True,
-            store_series=self.factory.makeSnappySeries(name="rolling"),
-            store_name="test-snap", store_secrets=self._make_store_secrets(),
-            store_channels=["stable", "edge"])
-        snapbuild = self.factory.makeSnapBuild(snap=snap)
-        responses.add(
-            "POST", "http://sca.example/dev/api/snap-release/";, status=404)
-        self.assertRaisesWithContent(
-            ReleaseFailedResponse, b"404 Client Error: Not Found",
-            self.client.release, snapbuild, 1)


Follow ups