launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #20700
[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