← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~cjwatson/launchpad/snap-channels-ui into lp:launchpad

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/snap-channels-ui into lp:launchpad with lp:~cjwatson/launchpad/snap-channels-job as a prerequisite.

Commit message:
Add UI for automatically releasing snap packages.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1597819 in Launchpad itself: "Add option to automatically release snap packages to channels after upload"
  https://bugs.launchpad.net/launchpad/+bug/1597819

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/snap-channels-ui/+merge/298811

Add UI for automatically releasing snap packages.

This will require firewall changes first to let the appservers talk to search.apps.ubuntu.com (although there's a feature flag in the snap-channels-store-client branch so that can be disabled if something goes wrong).
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/snap-channels-ui into lp:launchpad.
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py	2016-06-30 17:23:30 +0000
+++ lib/lp/snappy/browser/snap.py	2016-06-30 17:23:30 +0000
@@ -332,6 +332,9 @@
     # This is only required if store_upload is True.  Later validation takes
     # care of adjusting the required attribute.
     store_name = copy_field(ISnap['store_name'], required=True)
+    store_channels = copy_field(
+        ISnap['store_channels'],
+        value_type=Choice(vocabulary='SnapStoreChannel'), required=True)
 
 
 def log_oops(error, request):
@@ -368,9 +371,11 @@
         'auto_build_pocket',
         'store_upload',
         'store_name',
+        'store_channels',
         ]
     custom_widget('store_distro_series', LaunchpadRadioWidget)
     custom_widget('auto_build_archive', SnapArchiveWidget)
+    custom_widget('store_channels', LabeledMultiCheckBoxWidget)
 
     def initialize(self):
         """See `LaunchpadView`."""
@@ -443,6 +448,7 @@
             super(SnapAddView, self).validate_widgets(data, ['store_upload'])
             store_upload = data.get('store_upload', False)
             self.widgets['store_name'].context.required = store_upload
+            self.widgets['store_channels'].context.required = store_upload
         super(SnapAddView, self).validate_widgets(data, names=names)
 
     @action('Create snap package', name='create')
@@ -464,7 +470,8 @@
             auto_build_pocket=data['auto_build_pocket'],
             private=private, store_upload=data['store_upload'],
             store_series=data['store_distro_series'].snappy_series,
-            store_name=data['store_name'], **kwargs)
+            store_name=data['store_name'],
+            store_channels=data.get('store_channels'), **kwargs)
         if data['store_upload']:
             self.requestAuthorization(snap)
         else:
@@ -534,6 +541,7 @@
                 data, ['store_upload'])
             store_upload = data.get('store_upload', False)
             self.widgets['store_name'].context.required = store_upload
+            self.widgets['store_channels'].context.required = store_upload
         super(BaseSnapEditView, self).validate_widgets(data, names=names)
 
     def _needStoreReauth(self, data):
@@ -572,6 +580,8 @@
         if not store_upload:
             if 'store_name' in data:
                 del data['store_name']
+            if 'store_channels' in data:
+                del data['store_channels']
         need_store_reauth = self._needStoreReauth(data)
         self.updateContextFromData(data)
         if need_store_reauth:
@@ -631,8 +641,10 @@
         'auto_build_pocket',
         'store_upload',
         'store_name',
+        'store_channels',
         ]
     custom_widget('store_distro_series', LaunchpadRadioWidget)
+    custom_widget('store_channels', LabeledMultiCheckBoxWidget)
     custom_widget('vcs', LaunchpadRadioWidget)
     custom_widget('git_ref', GitRefWidget)
     custom_widget('auto_build_archive', SnapArchiveWidget)

=== modified file 'lib/lp/snappy/browser/snapbuild.py'
--- lib/lp/snappy/browser/snapbuild.py	2016-06-21 14:51:06 +0000
+++ lib/lp/snappy/browser/snapbuild.py	2016-06-30 17:23:30 +0000
@@ -95,6 +95,11 @@
             return structured(
                 '<a href="%s">Manage this package in the store</a>',
                 job.store_url)
+        elif job.store_url:
+            return structured(
+                '<a href="%s">Manage this package in the store</a><br />'
+                'Releasing package to channels failed: %s',
+                job.store_url, job.error_message)
         else:
             return structured("Store upload failed: %s", job.error_message)
 

=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py	2016-06-30 17:23:30 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py	2016-06-30 17:23:30 +0000
@@ -65,6 +65,7 @@
     SnapFeatureDisabled,
     SnapPrivateFeatureDisabled,
     )
+from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
 from lp.testing import (
     admin_logged_in,
     BrowserTestCase,
@@ -135,6 +136,10 @@
     def test_private_feature_flag_disabled(self):
         # Without a private_snap feature flag, we will not create Snaps for
         # private contexts.
+        self.snap_store_client = FakeMethod()
+        self.snap_store_client.listChannels = FakeMethod(result=[])
+        self.useFixture(
+            ZopeUtilityFixture(self.snap_store_client, ISnapStoreClient))
         owner = self.factory.makePerson()
         branch = self.factory.makeAnyBranch(
             owner=owner, information_type=InformationType.USERDATA)
@@ -161,6 +166,15 @@
         with admin_logged_in():
             self.snappyseries = self.factory.makeSnappySeries(
                 usable_distro_series=[self.distroseries])
+        self.snap_store_client = FakeMethod()
+        self.snap_store_client.listChannels = FakeMethod(result=[
+            {"name": "stable", "display_name": "Stable"},
+            {"name": "edge", "display_name": "Edge"},
+            ])
+        self.snap_store_client.requestPackageUploadPermission = (
+            getUtility(ISnapStoreClient).requestPackageUploadPermission)
+        self.useFixture(
+            ZopeUtilityFixture(self.snap_store_client, ISnapStoreClient))
 
     def test_initial_distroseries(self):
         # The initial distroseries is the newest that is current or in
@@ -365,6 +379,8 @@
         browser.getControl("Automatically upload to store").selected = True
         browser.getControl("Registered store package name").value = (
             "store-name")
+        self.assertFalse(browser.getControl("Stable").selected)
+        browser.getControl("Edge").selected = True
         root_macaroon = Macaroon()
         root_macaroon.add_third_party_caveat(
             urlsplit(config.launchpad.openid_provider_root).netloc, "",
@@ -389,7 +405,8 @@
             owner=self.person, distro_series=self.distroseries,
             name=u"snap-name", source=branch, store_upload=True,
             store_series=self.snappyseries, store_name=u"store-name",
-            store_secrets={"root": root_macaroon_raw}))
+            store_secrets={"root": root_macaroon_raw},
+            store_channels=["edge"]))
         self.assertThat(self.request, MatchesStructure.byEquality(
             url="http://sca.example/dev/api/acl/";, method="POST"))
         expected_body = {
@@ -543,6 +560,15 @@
         with admin_logged_in():
             self.snappyseries = self.factory.makeSnappySeries(
                 usable_distro_series=[self.distroseries])
+        self.snap_store_client = FakeMethod()
+        self.snap_store_client.listChannels = FakeMethod(result=[
+            {"name": "stable", "display_name": "Stable"},
+            {"name": "edge", "display_name": "Edge"},
+            ])
+        self.snap_store_client.requestPackageUploadPermission = (
+            getUtility(ISnapStoreClient).requestPackageUploadPermission)
+        self.useFixture(
+            ZopeUtilityFixture(self.snap_store_client, ISnapStoreClient))
 
     def test_initial_store_series(self):
         # The initial store_series is the newest that is usable for the
@@ -792,10 +818,13 @@
         snap = self.factory.makeSnap(
             registrant=self.person, owner=self.person,
             distroseries=self.distroseries, store_upload=True,
-            store_series=self.snappyseries, store_name=u"one")
+            store_series=self.snappyseries, store_name=u"one",
+            store_channels=["edge"])
         view_url = canonical_url(snap, view_name="+edit")
         browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
         browser.getControl("Registered store package name").value = "two"
+        browser.getControl("Stable").selected = True
+        self.assertTrue(browser.getControl("Edge").selected)
         root_macaroon = Macaroon()
         root_macaroon.add_third_party_caveat(
             urlsplit(config.launchpad.openid_provider_root).netloc, "",
@@ -816,7 +845,8 @@
                 HTTPError, browser.getControl("Update snap package").click)
         login_person(self.person)
         self.assertThat(snap, MatchesStructure.byEquality(
-            store_name=u"two", store_secrets={"root": root_macaroon_raw}))
+            store_name=u"two", store_secrets={"root": root_macaroon_raw},
+            store_channels=["stable", "edge"]))
         self.assertThat(self.request, MatchesStructure.byEquality(
             url="http://sca.example/dev/api/acl/";, method="POST"))
         expected_body = {

=== modified file 'lib/lp/snappy/browser/tests/test_snapbuild.py'
--- lib/lp/snappy/browser/tests/test_snapbuild.py	2016-06-21 14:51:06 +0000
+++ lib/lp/snappy/browser/tests/test_snapbuild.py	2016-06-30 17:23:30 +0000
@@ -113,6 +113,20 @@
             "Store upload failed: Scan failed.",
             build_view.store_upload_status.escapedtext)
 
+    def test_store_upload_status_release_failed(self):
+        build = self.factory.makeSnapBuild(status=BuildStatus.FULLYBUILT)
+        job = getUtility(ISnapStoreUploadJobSource).create(build)
+        naked_job = removeSecurityProxy(job)
+        naked_job.job._status = JobStatus.FAILED
+        naked_job.store_url = "http://sca.example/dev/click-apps/1/rev/1/";
+        naked_job.error_message = "Failed to publish"
+        build_view = create_initialized_view(build, "+index")
+        self.assertEqual(
+            '<a href="%s">Manage this package in the store</a><br />'
+            "Releasing package to channels failed: Failed to publish" % (
+                job.store_url),
+            build_view.store_upload_status.escapedtext)
+
 
 class TestSnapBuildOperations(BrowserTestCase):
 

=== modified file 'lib/lp/snappy/templates/snap-edit.pt'
--- lib/lp/snappy/templates/snap-edit.pt	2016-06-30 17:23:30 +0000
+++ lib/lp/snappy/templates/snap-edit.pt	2016-06-30 17:23:30 +0000
@@ -87,6 +87,10 @@
                   <metal:block use-macro="context/@@launchpad_form/widget_row" />
                 </tal:widget>
               </table>
+              <tal:widget define="widget nocall:view/widgets/store_channels"
+                          condition="widget/context/value_type/vocabulary">
+                <metal:block use-macro="context/@@launchpad_form/widget_row" />
+              </tal:widget>
               <p class="formHelp">
                 If you change any settings related to automatically uploading
                 builds of this snap to the store, then the login service will

=== modified file 'lib/lp/snappy/templates/snap-index.pt'
--- lib/lp/snappy/templates/snap-index.pt	2016-06-30 17:23:30 +0000
+++ lib/lp/snappy/templates/snap-index.pt	2016-06-30 17:23:30 +0000
@@ -104,6 +104,16 @@
           <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
         </dd>
       </dl>
+      <dl id="store_channels" tal:condition="context/store_channels">
+        <dt>Store channels:</dt>
+        <dd>
+          <span tal:content="context/store_channels"/>
+          <a tal:replace="structure view/menu:overview/edit/fmt:icon"/>
+        </dd>
+      </dl>
+      <p id="store_channels" tal:condition="not: context/store_channels">
+        This snap package will not be released to any channels on the store.
+      </p>
     </div>
     <p id="store_upload" tal:condition="not: context/store_upload">
       Builds of this snap package are not automatically uploaded to the store.

=== modified file 'lib/lp/snappy/templates/snap-new.pt'
--- lib/lp/snappy/templates/snap-new.pt	2016-06-30 17:23:30 +0000
+++ lib/lp/snappy/templates/snap-new.pt	2016-06-30 17:23:30 +0000
@@ -58,6 +58,10 @@
                 <tal:widget define="widget nocall:view/widgets/store_name">
                   <metal:block use-macro="context/@@launchpad_form/widget_row" />
                 </tal:widget>
+                <tal:widget define="widget nocall:view/widgets/store_channels"
+                            condition="widget/context/value_type/vocabulary">
+                  <metal:block use-macro="context/@@launchpad_form/widget_row" />
+                </tal:widget>
               </table>
               <p class="formHelp">
                 If you ask Launchpad to automatically upload builds of this

=== modified file 'lib/lp/snappy/vocabularies.py'
--- lib/lp/snappy/vocabularies.py	2016-05-09 09:28:58 +0000
+++ lib/lp/snappy/vocabularies.py	2016-06-30 17:23:30 +0000
@@ -11,13 +11,18 @@
     ]
 
 from storm.locals import Desc
-from zope.schema.vocabulary import SimpleTerm
+from zope.component import getUtility
+from zope.schema.vocabulary import (
+    SimpleTerm,
+    SimpleVocabulary,
+    )
 
 from lp.registry.model.distribution import Distribution
 from lp.registry.model.distroseries import DistroSeries
 from lp.registry.model.series import ACTIVE_STATUSES
 from lp.services.database.interfaces import IStore
 from lp.services.webapp.vocabulary import StormVocabularyBase
+from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
 from lp.snappy.model.snappyseries import (
     SnappyDistroSeries,
     SnappySeries,
@@ -109,3 +114,15 @@
     _clauses = SnappyDistroSeriesVocabulary._clauses + [
         SnappySeries.status.is_in(ACTIVE_STATUSES),
         ]
+
+
+class SnapStoreChannelVocabulary(SimpleVocabulary):
+    """A vocabulary for searching store channels."""
+
+    def __init__(self, context=None):
+        channels = getUtility(ISnapStoreClient).listChannels()
+        terms = [
+            self.createTerm(
+                channel["name"], channel["name"], channel["display_name"])
+            for channel in channels]
+        super(SnapStoreChannelVocabulary, self).__init__(terms)

=== modified file 'lib/lp/snappy/vocabularies.zcml'
--- lib/lp/snappy/vocabularies.zcml	2016-05-05 20:02:01 +0000
+++ lib/lp/snappy/vocabularies.zcml	2016-06-30 17:23:30 +0000
@@ -48,4 +48,15 @@
         <allow interface="lp.services.webapp.vocabulary.IHugeVocabulary" />
     </class>
 
+    <securedutility
+        name="SnapStoreChannel"
+        component="lp.snappy.vocabularies.SnapStoreChannelVocabulary"
+        provides="zope.schema.interfaces.IVocabularyFactory">
+        <allow interface="zope.schema.interfaces.IVocabularyFactory" />
+    </securedutility>
+
+    <class class="lp.snappy.vocabularies.SnapStoreChannelVocabulary">
+        <allow interface="zope.schema.interfaces.IVocabularyTokenized" />
+    </class>
+
 </configure>


Follow ups