Merge lp:~maxiberta/launchpad/snap-name-extraction into lp:launchpad

Proposed by Maximiliano Bertacchini
Status: Merged
Merged at revision: 18076
Proposed branch: lp:~maxiberta/launchpad/snap-name-extraction
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/snap-store-add-edit-views
Diff against target: 151 lines (+63/-1)
3 files modified
lib/lp/snappy/browser/snap.py (+23/-0)
lib/lp/snappy/browser/tests/test_snap.py (+39/-1)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~maxiberta/launchpad/snap-name-extraction
Reviewer Review Type Date Requested Status
Celso Providelo (community) Approve
Colin Watson (community) Approve
Review via email: mp+295114@code.launchpad.net

This proposal supersedes a proposal from 2016-05-17.

Commit message

Extract Snap.store_name from snapcraft.yaml (Git only).

Description of the change

Extract Snap.store_name from snapcraft.yaml (Git only).

To post a comment you must log in.
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote : Posted in a previous version of this proposal

Should mark https://code.launchpad.net/~cjwatson/launchpad/snap-store-add-edit-views/+merge/294524 as prerequisite? Will probably conflict (just a little); but I thought maybe this change is small enough to be merged directly to devel first.

Revision history for this message
Colin Watson (cjwatson) wrote : Posted in a previous version of this proposal

You should set a prerequisite branch if and only if you actually branched from the unmerged branch in question; otherwise you will get very confusing results.

However, you really will need to redo this on top of snap-store-add-edit-views, I'm afraid. The reason is that Snap.name is unique per person, but the same name in snapcraft.yaml may be associated with a snap that exists in multiple series. Defaulting name from what's in snapcraft.yaml will therefore fill in an initial form value that fails validation in some cases, and we shouldn't do that. We must only use this to pick a default for Snap.store_name, not Snap.name.

review: Needs Resubmitting
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

Replied to inline comment. Thanks

Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Celso Providelo (cprov) wrote :

Thanks Maxi,

We have discussed alternatives for the mock/call-counting strategy for testing the fact we are using yaml.safe_load(). Turns out that actually checking a malicious yaml content can't cause any harm was too involving for this test. So I am happy with the current state.

We also mentioned that, theoretically, we should be able to warn users in the form about repositories missing sufficient (only name for now) "snapcraft.yaml" content and prevent future errors. However, what we have right now is more friendly to initial setups (empty repo, for instance)

review: Approve
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Maximiliano Bertacchini (maxiberta) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/snappy/browser/snap.py'
--- lib/lp/snappy/browser/snap.py 2016-05-27 16:04:55 +0000
+++ lib/lp/snappy/browser/snap.py 2016-05-28 00:35:28 +0000
@@ -25,6 +25,7 @@
25 use_template,25 use_template,
26 )26 )
27from pymacaroons import Macaroon27from pymacaroons import Macaroon
28import yaml
28from zope.component import getUtility29from zope.component import getUtility
29from zope.error.interfaces import IErrorReportingUtility30from zope.error.interfaces import IErrorReportingUtility
30from zope.interface import Interface31from zope.interface import Interface
@@ -52,12 +53,14 @@
52 LaunchpadRadioWidget,53 LaunchpadRadioWidget,
53 )54 )
54from lp.code.browser.widgets.gitref import GitRefWidget55from lp.code.browser.widgets.gitref import GitRefWidget
56from lp.code.errors import GitRepositoryScanFault
55from lp.code.interfaces.gitref import IGitRef57from lp.code.interfaces.gitref import IGitRef
56from lp.registry.enums import VCSType58from lp.registry.enums import VCSType
57from lp.registry.interfaces.pocket import PackagePublishingPocket59from lp.registry.interfaces.pocket import PackagePublishingPocket
58from lp.services.features import getFeatureFlag60from lp.services.features import getFeatureFlag
59from lp.services.helpers import english_list61from lp.services.helpers import english_list
60from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint62from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
63from lp.services.scripts import log
61from lp.services.webapp import (64from lp.services.webapp import (
62 canonical_url,65 canonical_url,
63 ContextMenu,66 ContextMenu,
@@ -372,12 +375,32 @@
372375
373 @property376 @property
374 def initial_values(self):377 def initial_values(self):
378 store_name = None
379 if self.has_snappy_distro_series and IGitRef.providedBy(self.context):
380 # Try to extract Snap store name from snapcraft.yaml file.
381 try:
382 blob = self.context.repository.getBlob(
383 'snapcraft.yaml', self.context.name)
384 # Beware of unsafe yaml.load()!
385 store_name = yaml.safe_load(blob).get('name')
386 except GitRepositoryScanFault:
387 log.exception("Failed to get Snap manifest from Git %s",
388 self.context.unique_name)
389 except (AttributeError, yaml.YAMLError):
390 # Ignore parsing errors from invalid, user-supplied YAML
391 pass
392 except Exception as e:
393 log.exception(
394 "Failed to extract name from Snap manifest at Git %s: %s",
395 self.context.unique_name, unicode(e))
396
375 # XXX cjwatson 2015-09-18: Hack to ensure that we don't end up397 # XXX cjwatson 2015-09-18: Hack to ensure that we don't end up
376 # accidentally selecting ubuntu-rtm/14.09 or similar.398 # accidentally selecting ubuntu-rtm/14.09 or similar.
377 # ubuntu.currentseries will always be in BuildableDistroSeries.399 # ubuntu.currentseries will always be in BuildableDistroSeries.
378 series = getUtility(ILaunchpadCelebrities).ubuntu.currentseries400 series = getUtility(ILaunchpadCelebrities).ubuntu.currentseries
379 sds_set = getUtility(ISnappyDistroSeriesSet)401 sds_set = getUtility(ISnappyDistroSeriesSet)
380 return {402 return {
403 'store_name': store_name,
381 'owner': self.user,404 'owner': self.user,
382 'store_distro_series': sds_set.getByDistroSeries(series).first(),405 'store_distro_series': sds_set.getByDistroSeries(series).first(),
383 }406 }
384407
=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
--- lib/lp/snappy/browser/tests/test_snap.py 2016-05-27 22:05:53 +0000
+++ lib/lp/snappy/browser/tests/test_snap.py 2016-05-28 00:35:28 +0000
@@ -24,6 +24,7 @@
24 HTTMock,24 HTTMock,
25 )25 )
26from mechanize import LinkNotFoundError26from mechanize import LinkNotFoundError
27import mock
27from pymacaroons import Macaroon28from pymacaroons import Macaroon
28import pytz29import pytz
29import soupmatchers30import soupmatchers
@@ -40,6 +41,7 @@
40from lp.app.interfaces.launchpad import ILaunchpadCelebrities41from lp.app.interfaces.launchpad import ILaunchpadCelebrities
41from lp.buildmaster.enums import BuildStatus42from lp.buildmaster.enums import BuildStatus
42from lp.buildmaster.interfaces.processor import IProcessorSet43from lp.buildmaster.interfaces.processor import IProcessorSet
44from lp.code.errors import GitRepositoryScanFault
43from lp.registry.enums import PersonVisibility45from lp.registry.enums import PersonVisibility
44from lp.registry.interfaces.pocket import PackagePublishingPocket46from lp.registry.interfaces.pocket import PackagePublishingPocket
45from lp.registry.interfaces.series import SeriesStatus47from lp.registry.interfaces.series import SeriesStatus
@@ -73,6 +75,7 @@
73 TestCaseWithFactory,75 TestCaseWithFactory,
74 time_counter,76 time_counter,
75 )77 )
78from lp.testing.fakemethod import FakeMethod
76from lp.testing.layers import (79from lp.testing.layers import (
77 DatabaseFunctionalLayer,80 DatabaseFunctionalLayer,
78 LaunchpadFunctionalLayer,81 LaunchpadFunctionalLayer,
@@ -84,8 +87,8 @@
84from lp.testing.pages import (87from lp.testing.pages import (
85 extract_text,88 extract_text,
86 find_main_content,89 find_main_content,
90 find_tag_by_id,
87 find_tags_by_class,91 find_tags_by_class,
88 find_tag_by_id,
89 get_feedback_messages,92 get_feedback_messages,
90 )93 )
91from lp.testing.publication import test_traverse94from lp.testing.publication import test_traverse
@@ -360,6 +363,41 @@
360 }363 }
361 self.assertEqual(expected_args, parse_qs(parsed_location[3]))364 self.assertEqual(expected_args, parse_qs(parsed_location[3]))
362365
366 def test_initial_name_extraction_git(self):
367 [git_ref] = self.factory.makeGitRefs()
368 git_ref.repository.getBlob = FakeMethod(result='name: test-snap')
369 view = create_initialized_view(git_ref, "+new-snap")
370 initial_values = view.initial_values
371 self.assertIn('store_name', initial_values)
372 self.assertEqual('test-snap', initial_values['store_name'])
373
374 def test_initial_name_extraction_git_repo_error(self):
375 [git_ref] = self.factory.makeGitRefs()
376 git_ref.repository.getBlob = FakeMethod(failure=GitRepositoryScanFault)
377 view = create_initialized_view(git_ref, "+new-snap")
378 initial_values = view.initial_values
379 self.assertIn('store_name', initial_values)
380 self.assertIsNone(initial_values['store_name'])
381
382 def test_initial_name_extraction_git_invalid_data(self):
383 for invalid_result in (None, 123, '', '[][]', '#name:test', ']'):
384 [git_ref] = self.factory.makeGitRefs()
385 git_ref.repository.getBlob = FakeMethod(result=invalid_result)
386 view = create_initialized_view(git_ref, "+new-snap")
387 initial_values = view.initial_values
388 self.assertIn('store_name', initial_values)
389 self.assertIsNone(initial_values['store_name'])
390
391 def test_initial_name_extraction_git_safe_yaml(self):
392 [git_ref] = self.factory.makeGitRefs()
393 git_ref.repository.getBlob = FakeMethod(result='Malicious YAML!')
394 view = create_initialized_view(git_ref, "+new-snap")
395 with mock.patch('yaml.load') as unsafe_load:
396 with mock.patch('yaml.safe_load') as safe_load:
397 initial_values = view.initial_values
398 self.assertEqual(0, unsafe_load.call_count)
399 self.assertEqual(1, safe_load.call_count)
400
363401
364class TestSnapAdminView(BrowserTestCase):402class TestSnapAdminView(BrowserTestCase):
365403
366404
=== modified file 'setup.py'
--- setup.py 2016-05-11 11:23:41 +0000
+++ setup.py 2016-05-28 00:35:28 +0000
@@ -83,6 +83,7 @@
83 'python-memcached',83 'python-memcached',
84 'python-openid',84 'python-openid',
85 'pytz',85 'pytz',
86 'PyYAML',
86 'rabbitfixture',87 'rabbitfixture',
87 'requests',88 'requests',
88 'requests-toolbelt',89 'requests-toolbelt',