launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #21410
[Merge] lp:~cjwatson/launchpad/snap-webhook-store-status into lp:launchpad
Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-webhook-store-status into lp:launchpad.
Commit message:
Extend snap webhooks to include store_upload_status.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-webhook-store-status/+merge/320295
This is needed for build.snapcraft.io, so that it can increment metrics when snaps are released to edge; but in general it seems reasonable to include store upload status as well as build status in the webhook payload and trigger deliveries when it changes.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-webhook-store-status into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg 2016-11-23 03:42:51 +0000
+++ database/schema/security.cfg 2017-03-20 00:43:16 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2016 Canonical Ltd. This software is licensed under the
+# Copyright 2009-2017 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
#
# Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
@@ -2565,7 +2565,7 @@
public.distroarchseries = SELECT
public.distroseries = SELECT
public.emailaddress = SELECT
-public.job = SELECT, UPDATE
+public.job = SELECT, INSERT, UPDATE
public.libraryfilealias = SELECT
public.libraryfilecontent = SELECT
public.person = SELECT
@@ -2576,3 +2576,5 @@
public.snapfile = SELECT
public.snappyseries = SELECT
public.teammembership = SELECT
+public.webhook = SELECT
+public.webhookjob = SELECT, INSERT
=== modified file 'lib/lp/snappy/configure.zcml'
--- lib/lp/snappy/configure.zcml 2016-06-20 21:17:58 +0000
+++ lib/lp/snappy/configure.zcml 2017-03-20 00:43:16 +0000
@@ -1,4 +1,4 @@
-<!-- Copyright 2015-2016 Canonical Ltd. This software is licensed under the
+<!-- Copyright 2015-2017 Canonical Ltd. This software is licensed under the
GNU Affero General Public License version 3 (see the file LICENSE).
-->
@@ -139,6 +139,10 @@
<allow interface="lp.snappy.interfaces.snapbuildjob.ISnapBuildJob" />
<allow interface="lp.snappy.interfaces.snapbuildjob.ISnapStoreUploadJob" />
</class>
+ <subscriber
+ for="lp.snappy.interfaces.snapbuild.ISnapBuild
+ lp.snappy.interfaces.snapbuildjob.ISnapBuildStoreUploadStatusChangedEvent"
+ handler="lp.snappy.subscribers.snapbuild.snap_build_store_upload_status_changed" />
<webservice:register module="lp.snappy.interfaces.webservice" />
=== modified file 'lib/lp/snappy/interfaces/snapbuildjob.py'
--- lib/lp/snappy/interfaces/snapbuildjob.py 2016-06-21 14:51:06 +0000
+++ lib/lp/snappy/interfaces/snapbuildjob.py 2017-03-20 00:43:16 +0000
@@ -1,4 +1,4 @@
-# Copyright 2016 Canonical Ltd. This software is licensed under the
+# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Snap build job interfaces."""
@@ -8,11 +8,13 @@
__metaclass__ = type
__all__ = [
'ISnapBuildJob',
+ 'ISnapBuildStoreUploadStatusChangedEvent',
'ISnapStoreUploadJob',
'ISnapStoreUploadJobSource',
]
from lazr.restful.fields import Reference
+from zope.component.interfaces import IObjectEvent
from zope.interface import (
Attribute,
Interface,
@@ -42,6 +44,10 @@
metadata = Attribute(_("A dict of data about the job."))
+class ISnapBuildStoreUploadStatusChangedEvent(IObjectEvent):
+ """The store upload status of a snap package build changed."""
+
+
class ISnapStoreUploadJob(IRunnableJob):
"""A Job that uploads a snap build to the store."""
=== modified file 'lib/lp/snappy/model/snapbuildjob.py'
--- lib/lp/snappy/model/snapbuildjob.py 2016-10-16 22:04:39 +0000
+++ lib/lp/snappy/model/snapbuildjob.py 2017-03-20 00:43:16 +0000
@@ -1,4 +1,4 @@
-# Copyright 2016 Canonical Ltd. This software is licensed under the
+# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Snap build jobs."""
@@ -9,6 +9,7 @@
__all__ = [
'SnapBuildJob',
'SnapBuildJobType',
+ 'SnapBuildStoreUploadStatusChangedEvent',
'SnapStoreUploadJob',
]
@@ -26,6 +27,8 @@
)
import transaction
from zope.component import getUtility
+from zope.component.interfaces import ObjectEvent
+from zope.event import notify
from zope.interface import (
implementer,
provider,
@@ -44,8 +47,10 @@
Job,
)
from lp.services.job.runner import BaseRunnableJob
+from lp.services.propertycache import get_property_cache
from lp.snappy.interfaces.snapbuildjob import (
ISnapBuildJob,
+ ISnapBuildStoreUploadStatusChangedEvent,
ISnapStoreUploadJob,
ISnapStoreUploadJobSource,
)
@@ -164,6 +169,11 @@
pass
+@implementer(ISnapBuildStoreUploadStatusChangedEvent)
+class SnapBuildStoreUploadStatusChangedEvent(ObjectEvent):
+ """See `ISnapBuildStoreUploadStatusChangedEvent`."""
+
+
@implementer(ISnapStoreUploadJob)
@provider(ISnapStoreUploadJobSource)
class SnapStoreUploadJob(SnapBuildJobDerived):
@@ -191,6 +201,8 @@
snap_build_job = SnapBuildJob(snapbuild, cls.class_job_type, {})
job = cls(snap_build_job)
job.celeryRunOnCommit()
+ del get_property_cache(snapbuild).last_store_upload_job
+ notify(SnapBuildStoreUploadStatusChangedEvent(snapbuild))
return job
@property
@@ -213,6 +225,33 @@
"""See `ISnapStoreUploadJob`."""
self.metadata["store_url"] = url
+ # Ideally we'd just override Job._set_status or similar, but
+ # lazr.delegates makes that difficult, so we use this to override all
+ # the individual Job lifecycle methods instead.
+ def _do_lifecycle(self, method, *args, **kwargs):
+ old_store_upload_status = self.snapbuild.store_upload_status
+ method(*args, **kwargs)
+ if self.snapbuild.store_upload_status != old_store_upload_status:
+ notify(SnapBuildStoreUploadStatusChangedEvent(self.snapbuild))
+
+ def start(self, *args, **kwargs):
+ self._do_lifecycle(self.job.start, *args, **kwargs)
+
+ def complete(self, *args, **kwargs):
+ self._do_lifecycle(self.job.complete, *args, **kwargs)
+
+ def fail(self, *args, **kwargs):
+ self._do_lifecycle(self.job.fail, *args, **kwargs)
+
+ def queue(self, *args, **kwargs):
+ self._do_lifecycle(self.job.queue, *args, **kwargs)
+
+ def suspend(self, *args, **kwargs):
+ self._do_lifecycle(self.job.suspend, *args, **kwargs)
+
+ def resume(self, *args, **kwargs):
+ self._do_lifecycle(self.job.resume, *args, **kwargs)
+
def run(self):
"""See `IRunnableJob`."""
client = getUtility(ISnapStoreClient)
=== modified file 'lib/lp/snappy/subscribers/snapbuild.py'
--- lib/lp/snappy/subscribers/snapbuild.py 2016-07-19 16:32:46 +0000
+++ lib/lp/snappy/subscribers/snapbuild.py 2017-03-20 00:43:16 +0000
@@ -1,4 +1,4 @@
-# Copyright 2016 Canonical Ltd. This software is licensed under the
+# Copyright 2016-2017 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Event subscribers for snap builds."""
@@ -19,18 +19,27 @@
from lp.snappy.interfaces.snapbuildjob import ISnapStoreUploadJobSource
-def snap_build_status_changed(snapbuild, event):
- """Trigger events when snap package build statuses change."""
+def _trigger_snap_build_webhook(snapbuild):
if getFeatureFlag(SNAP_WEBHOOKS_FEATURE_FLAG):
payload = {
"snap_build": canonical_url(snapbuild, force_local_path=True),
"action": "status-changed",
}
payload.update(compose_webhook_payload(
- ISnapBuild, snapbuild, ["snap", "status"]))
+ ISnapBuild, snapbuild, ["snap", "status", "store_upload_status"]))
getUtility(IWebhookSet).trigger(
snapbuild.snap, "snap:build:0.1", payload)
+
+def snap_build_status_changed(snapbuild, event):
+ """Trigger events when snap package build statuses change."""
+ _trigger_snap_build_webhook(snapbuild)
+
if (snapbuild.snap.can_upload_to_store and snapbuild.snap.store_upload and
snapbuild.status == BuildStatus.FULLYBUILT):
getUtility(ISnapStoreUploadJobSource).create(snapbuild)
+
+
+def snap_build_store_upload_status_changed(snapbuild, event):
+ """Trigger events when snap package build store upload statuses change."""
+ _trigger_snap_build_webhook(snapbuild)
=== modified file 'lib/lp/snappy/tests/test_snapbuild.py'
--- lib/lp/snappy/tests/test_snapbuild.py 2017-02-27 18:46:38 +0000
+++ lib/lp/snappy/tests/test_snapbuild.py 2017-03-20 00:43:16 +0000
@@ -231,6 +231,7 @@
"snap": Equals(
canonical_url(self.build.snap, force_local_path=True)),
"status": Equals("Successfully built"),
+ "store_upload_status": Equals("Unscheduled"),
}
delivery = hook.deliveries.one()
self.assertThat(
@@ -471,6 +472,37 @@
self.assertEqual(
[], list(getUtility(ISnapStoreUploadJobSource).iterReady()))
+ def test_scheduleStoreUpload_triggers_webhooks(self):
+ # Scheduling a store upload triggers webhooks on the corresponding
+ # snap.
+ self.setUpStoreUpload()
+ self.build.updateStatus(BuildStatus.FULLYBUILT)
+ self.factory.makeSnapFile(
+ snapbuild=self.build,
+ libraryfile=self.factory.makeLibraryFileAlias(db_only=True))
+ hook = self.factory.makeWebhook(
+ target=self.build.snap, event_types=["snap:build:0.1"])
+ self.build.scheduleStoreUpload()
+ expected_payload = {
+ "snap_build": Equals(
+ canonical_url(self.build, force_local_path=True)),
+ "action": Equals("status-changed"),
+ "snap": Equals(
+ canonical_url(self.build.snap, force_local_path=True)),
+ "status": Equals("Successfully built"),
+ "store_upload_status": Equals("Pending"),
+ }
+ delivery = hook.deliveries.one()
+ self.assertThat(
+ delivery, MatchesStructure(
+ event_type=Equals("snap:build:0.1"),
+ payload=MatchesDict(expected_payload)))
+ with dbuser(config.IWebhookDeliveryJobSource.dbuser):
+ self.assertEqual(
+ "<WebhookDeliveryJob for webhook %d on %r>" % (
+ hook.id, hook.target),
+ repr(delivery))
+
class TestSnapBuildSet(TestCaseWithFactory):
=== modified file 'lib/lp/snappy/tests/test_snapbuildjob.py'
--- lib/lp/snappy/tests/test_snapbuildjob.py 2017-01-04 20:52:12 +0000
+++ lib/lp/snappy/tests/test_snapbuildjob.py 2017-03-20 00:43:16 +0000
@@ -8,12 +8,20 @@
__metaclass__ = type
from fixtures import FakeLogger
+from testtools.matchers import (
+ Equals,
+ MatchesDict,
+ MatchesListwise,
+ MatchesStructure,
+ )
from zope.interface import implementer
+from lp.buildmaster.enums import BuildStatus
from lp.services.config import config
from lp.services.features.testing import FeatureFixture
from lp.services.job.interfaces.job import JobStatus
from lp.services.job.runner import JobRunner
+from lp.services.webapp.publisher import canonical_url
from lp.snappy.interfaces.snap import SNAP_TESTING_FLAGS
from lp.snappy.interfaces.snapbuildjob import (
ISnapBuildJob,
@@ -92,10 +100,45 @@
self.assertEqual(
"<SnapStoreUploadJob for %s>" % snapbuild.title, repr(job))
+ def makeSnapBuild(self, **kwargs):
+ # Make a build with a builder and a webhook.
+ snapbuild = self.factory.makeSnapBuild(
+ builder=self.factory.makeBuilder(), **kwargs)
+ snapbuild.updateStatus(BuildStatus.FULLYBUILT)
+ self.factory.makeWebhook(
+ target=snapbuild.snap, event_types=["snap:build:0.1"])
+ return snapbuild
+
+ def assertWebhookDeliveries(self, snapbuild,
+ expected_store_upload_statuses):
+ hook = snapbuild.snap.webhooks.one()
+ deliveries = list(hook.deliveries)
+ deliveries.reverse()
+ expected_payloads = [{
+ "snap_build": Equals(
+ canonical_url(snapbuild, force_local_path=True)),
+ "action": Equals("status-changed"),
+ "snap": Equals(
+ canonical_url(snapbuild.snap, force_local_path=True)),
+ "status": Equals("Successfully built"),
+ "store_upload_status": Equals(expected),
+ } for expected in expected_store_upload_statuses]
+ matchers = [
+ MatchesStructure(
+ event_type=Equals("snap:build:0.1"),
+ payload=MatchesDict(expected_payload))
+ for expected_payload in expected_payloads]
+ self.assertThat(deliveries, MatchesListwise(matchers))
+ with dbuser(config.IWebhookDeliveryJobSource.dbuser):
+ for delivery in deliveries:
+ self.assertEqual(
+ "<WebhookDeliveryJob for webhook %d on %r>" % (
+ hook.id, hook.target),
+ repr(delivery))
+
def test_run(self):
# The job uploads the build to the store and records the store URL.
- snapbuild = self.factory.makeSnapBuild(
- builder=self.factory.makeBuilder())
+ snapbuild = self.makeSnapBuild()
self.assertContentEqual([], snapbuild.store_upload_jobs)
job = SnapStoreUploadJob.create(snapbuild)
client = FakeSnapStoreClient()
@@ -111,11 +154,11 @@
self.assertEqual(self.store_url, job.store_url)
self.assertIsNone(job.error_message)
self.assertEqual([], pop_notifications())
+ self.assertWebhookDeliveries(snapbuild, ["Pending", "Uploaded"])
def test_run_failed(self):
# A failed run sets the store upload status to FAILED.
- snapbuild = self.factory.makeSnapBuild(
- builder=self.factory.makeBuilder())
+ snapbuild = self.makeSnapBuild()
self.assertContentEqual([], snapbuild.store_upload_jobs)
job = SnapStoreUploadJob.create(snapbuild)
client = FakeSnapStoreClient()
@@ -130,15 +173,16 @@
self.assertIsNone(job.store_url)
self.assertEqual("An upload failure", job.error_message)
self.assertEqual([], pop_notifications())
+ self.assertWebhookDeliveries(
+ snapbuild, ["Pending", "Failed to upload"])
def test_run_unauthorized_notifies(self):
# A run that gets 401 from the store sends mail.
requester = self.factory.makePerson(name="requester")
requester_team = self.factory.makeTeam(
owner=requester, name="requester-team", members=[requester])
- snapbuild = self.factory.makeSnapBuild(
- requester=requester_team, name="test-snap", owner=requester_team,
- builder=self.factory.makeBuilder())
+ snapbuild = self.makeSnapBuild(
+ requester=requester_team, name="test-snap", owner=requester_team)
self.assertContentEqual([], snapbuild.store_upload_jobs)
job = SnapStoreUploadJob.create(snapbuild)
client = FakeSnapStoreClient()
@@ -177,6 +221,8 @@
"http://launchpad.dev/~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 upload"])
def test_run_upload_failure_notifies(self):
# A run that gets some other upload failure from the store sends
@@ -184,9 +230,8 @@
requester = self.factory.makePerson(name="requester")
requester_team = self.factory.makeTeam(
owner=requester, name="requester-team", members=[requester])
- snapbuild = self.factory.makeSnapBuild(
- requester=requester_team, name="test-snap", owner=requester_team,
- builder=self.factory.makeBuilder())
+ snapbuild = self.makeSnapBuild(
+ requester=requester_team, name="test-snap", owner=requester_team)
self.assertContentEqual([], snapbuild.store_upload_jobs)
job = SnapStoreUploadJob.create(snapbuild)
client = FakeSnapStoreClient()
@@ -225,13 +270,14 @@
self.assertEqual(
"%s\nYour team Requester Team is the requester of the build.\n" %
build_url, footer)
+ self.assertWebhookDeliveries(
+ snapbuild, ["Pending", "Failed to upload"])
def test_run_scan_pending_retries(self):
# A run that finds that the store has not yet finished scanning the
# package schedules itself to be retried.
self.useFixture(FakeLogger())
- snapbuild = self.factory.makeSnapBuild(
- builder=self.factory.makeBuilder())
+ snapbuild = self.makeSnapBuild()
self.assertContentEqual([], snapbuild.store_upload_jobs)
job = SnapStoreUploadJob.create(snapbuild)
client = FakeSnapStoreClient()
@@ -248,6 +294,7 @@
self.assertIsNone(job.error_message)
self.assertEqual([], pop_notifications())
self.assertEqual(JobStatus.WAITING, job.job.status)
+ self.assertWebhookDeliveries(snapbuild, ["Pending"])
# Try again. The upload part of the job is not retried, and this
# time the scan completes.
job.lease_expires = None
@@ -266,15 +313,15 @@
self.assertIsNone(job.error_message)
self.assertEqual([], pop_notifications())
self.assertEqual(JobStatus.COMPLETED, job.job.status)
+ self.assertWebhookDeliveries(snapbuild, ["Pending", "Uploaded"])
def test_run_scan_failure_notifies(self):
# A run that gets a scan failure from the store sends mail.
requester = self.factory.makePerson(name="requester")
requester_team = self.factory.makeTeam(
owner=requester, name="requester-team", members=[requester])
- snapbuild = self.factory.makeSnapBuild(
- requester=requester_team, name="test-snap", owner=requester_team,
- builder=self.factory.makeBuilder())
+ snapbuild = self.makeSnapBuild(
+ requester=requester_team, name="test-snap", owner=requester_team)
self.assertContentEqual([], snapbuild.store_upload_jobs)
job = SnapStoreUploadJob.create(snapbuild)
client = FakeSnapStoreClient()
@@ -311,12 +358,13 @@
"http://launchpad.dev/~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 upload"])
def test_run_release(self):
# A run configured to automatically release the package to certain
# channels does so.
- snapbuild = self.factory.makeSnapBuild(
- store_channels=["stable", "edge"])
+ snapbuild = self.makeSnapBuild(store_channels=["stable", "edge"])
self.assertContentEqual([], snapbuild.store_upload_jobs)
job = SnapStoreUploadJob.create(snapbuild)
client = FakeSnapStoreClient()
@@ -332,6 +380,7 @@
self.assertEqual(self.store_url, job.store_url)
self.assertIsNone(job.error_message)
self.assertEqual([], pop_notifications())
+ self.assertWebhookDeliveries(snapbuild, ["Pending", "Uploaded"])
def test_run_release_manual_review_notifies(self):
# A run configured to automatically release the package to certain
@@ -340,7 +389,7 @@
requester = self.factory.makePerson(name="requester")
requester_team = self.factory.makeTeam(
owner=requester, name="requester-team", members=[requester])
- snapbuild = self.factory.makeSnapBuild(
+ snapbuild = self.makeSnapBuild(
requester=requester_team, name="test-snap", owner=requester_team,
store_channels=["stable", "edge"])
self.assertContentEqual([], snapbuild.store_upload_jobs)
@@ -382,6 +431,8 @@
"http://launchpad.dev/~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_run_release_failure_notifies(self):
# A run configured to automatically release the package to certain
@@ -389,7 +440,7 @@
requester = self.factory.makePerson(name="requester")
requester_team = self.factory.makeTeam(
owner=requester, name="requester-team", members=[requester])
- snapbuild = self.factory.makeSnapBuild(
+ snapbuild = self.makeSnapBuild(
requester=requester_team, name="test-snap", owner=requester_team,
store_channels=["stable", "edge"])
self.assertContentEqual([], snapbuild.store_upload_jobs)
@@ -430,3 +481,5 @@
"http://launchpad.dev/~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"])
Follow ups