Merge lp:~rharding/charmworld/bundle-metadata2 into lp:~juju-jitsu/charmworld/trunk

Proposed by Richard Harding
Status: Merged
Approved by: Richard Harding
Approved revision: 414
Merged at revision: 406
Proposed branch: lp:~rharding/charmworld/bundle-metadata2
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 383 lines (+232/-8)
5 files modified
charmworld/models.py (+67/-4)
charmworld/testing/factory.py (+1/-1)
charmworld/tests/test_models.py (+118/-1)
charmworld/views/api.py (+22/-0)
charmworld/views/tests/test_api.py (+24/-2)
To merge this branch: bzr merge lp:~rharding/charmworld/bundle-metadata2
Reviewer Review Type Date Requested Status
Juju Gui Bot continuous-integration Approve
Benji York (community) Approve
Review via email: mp+189406@code.launchpad.net

Commit message

Add extra file/charm_metadata support to the bundle details api call.

Description of the change

Add extra file/charm_metadata support to the bundle details api call.

The bundle details view needs to provide metadata for the Gui to properly render the token and information about it. This provides the list of files (to find the README for instance) in the bundle as well as charm details of the charms in the bundle.

The charms are keyed off their keys in the bundle services list so that the gui can match from the name used in the bundle to the charm model in the charm_metadata.

The charms are formatted per the normal charm_details method by casting to a Charm object and then running through _format_charm.

QA:

http://charmworld:2464/api/3/bundle/~bac/wiki/wiki

Should output a new large suite of json for each charm in the bundle. It should also show the list of files so that the front end can implement a hasIcon as per the way gui handles charms. It also allows searching for the README file (README, readme.md, etc) just as we do with the charms in the Gui.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

The branch looks good. I saw a few little things that deserve your
attention.

Lines 44 and 85 of the diff: The name "data" is somewhat
information-free, maybe bundle_data would be more informative.

Line 45 of the diff: The docstring summery might be better rendered as
"Construct a query that specifies a charm from data about that charm."

Line 47 of the diff: The bulleted lists in the docstring have
inconsistent indentation.

Lines 64 and 77 of the diff: Please capitalize the sentence in the
comment.

Line 71 of the diff: I would be paranoid and pass "/trunk" to the
.endswith() call so a branch name like "who-is-that-in-the-trunk" won't
be matched.

Line 196 of the diff: It would be slightly better to use .assertIsNone().

Line 357 of the diff: There is an extra space after the colon. I think
lint will pick that up, so you should run lint to see if there is more.

Line 205 of the diff: The TestBundleLoadingCharms test case could use a
test that shows what a request with too-little information to find a
charm will produce.

review: Approve
Revision history for this message
Richard Harding (rharding) wrote :

> Lines 44 and 85 of the diff: The name "data" is somewhat
> information-free, maybe bundle_data would be more informative.

Updated. It's actually the services bit from the bundle so went with bundle_charm_info.

> Line 45 of the diff: The docstring summery might be better rendered as
> "Construct a query that specifies a charm from data about that charm."

Updated

> Line 47 of the diff: The bulleted lists in the docstring have
> inconsistent indentation.

Same

> Lines 64 and 77 of the diff: Please capitalize the sentence in the
> comment.

Thanks

> Line 71 of the diff: I would be paranoid and pass "/trunk" to the
> .endswith() call so a branch name like "who-is-that-in-the-trunk" won't
> be matched.

Makes sense.

> Line 196 of the diff: It would be slightly better to use .assertIsNone().

Updated

> Line 357 of the diff: There is an extra space after the colon. I think
> lint will pick that up, so you should run lint to see if there is more.

Thanks, also had the extra line setting the maxdiff to none that lint caught and removed.

> Line 205 of the diff: The TestBundleLoadingCharms test case could use a
> test that shows what a request with too-little information to find a
> charm will produce.

Thanks! This misbehaved and the test caught that and got it adjusted.

Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/15/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Richard Harding (rharding) wrote :

Updated and sync'd with trunk. Will try to land again.

Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/16/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/17/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Juju Gui Bot (juju-gui-bot) wrote :

FAILED: Autolanding.
More details in the following jenkins job:
http://162.213.35.27:8080/job/charmworld-autoland-lxc/18/
Executed test runs:

review: Needs Fixing (continuous-integration)
Revision history for this message
Juju Gui Bot (juju-gui-bot) :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/models.py'
2--- charmworld/models.py 2013-10-01 00:38:01 +0000
3+++ charmworld/models.py 2013-10-07 14:35:40 +0000
4@@ -1288,7 +1288,7 @@
5 class Bundle:
6 """A model to represent bundles of charms."""
7
8- _COMMON_REPRESENTATION = {
9+ _MONGO_FIELDS = {
10 'owner': '',
11 'basket_name': None,
12 'basket_revision': None,
13@@ -1305,7 +1305,7 @@
14 'inheriting from other bundles is not supported, the bundle '
15 'must be preprocessed to incorporate any inheritance')
16 self._raw_representation = data
17- self._representation = dict(self._COMMON_REPRESENTATION)
18+ self._representation = dict(self._MONGO_FIELDS)
19 self._representation.update(data)
20
21 @classmethod
22@@ -1333,7 +1333,7 @@
23 and self.permanent_url == other.permanent_url)
24
25 def __getattr__(self, name):
26- if name in self._COMMON_REPRESENTATION:
27+ if name in self._MONGO_FIELDS:
28 return self._representation[name]
29 else:
30 raise AttributeError
31@@ -1406,7 +1406,7 @@
32 """
33 yield 'id', self.id
34 yield 'permanent_url', self.permanent_url
35- for key in self._COMMON_REPRESENTATION:
36+ for key in self._MONGO_FIELDS:
37 yield key, self.__getattr__(key)
38
39 @property
40@@ -1487,6 +1487,69 @@
41 sort, max_scan, as_class, all_revisions, **kwargs))
42
43
44+def _build_charm_query_from_bundle_info(bundle_charm_info, series):
45+ """Construct a query for a charm based on the info available in the bundle
46+
47+ Bundles may provide:
48+ - branch
49+ - charm (name)
50+ - If so, the series is required to determine which charm with that
51+ name. This also assumes a promulgated charm.
52+ - charm (store url)
53+ """
54+ charm_query = None
55+ branch, rev = None, None
56+ charm_branch = bundle_charm_info.get('branch')
57+ store_url = bundle_charm_info.get(
58+ 'charm',
59+ bundle_charm_info.get('charm_url', None))
60+
61+ if (store_url and store_url.startswith('cs:')):
62+ charm_query = {'store_url': store_url}
63+ elif charm_branch:
64+ branch, sep, revision = charm_branch.partition('@')
65+
66+ # Strip the lp:charms to match the fields in the db.
67+ if branch.startswith('lp:'):
68+ branch = re.sub('^lp:', '', branch)
69+ # If the branch url doesn't start with an owner assume it's a
70+ # ~charmers branch
71+ if branch[0] != '~':
72+ branch = '~charmers/' + branch
73+ if not branch.endswith('/trunk'):
74+ branch = branch + '/trunk'
75+ charm_query = {u'branch_spec': str(branch)}
76+ if (revision):
77+ charm_query['store_data.revision'] = int(revision)
78+ elif store_url and series:
79+ # The last resort, if there's a series in the bundle config, and we
80+ # have a charm name, we can attempt to load the latest version of the
81+ # charm with that address.
82+ address = 'cs:' + '/'.join([series, store_url])
83+ charm_query = {u'address': address}
84+ return charm_query
85+
86+
87+def resolve_charm_from_bundle_info(db, bundle_charm_info, series):
88+ """Find the charm that the bundle is looking for.
89+
90+ Bundles may provide charm info in several ways. Attempt to find the best
91+ charm we can based on that information.
92+
93+ """
94+ charm_query = _build_charm_query_from_bundle_info(
95+ bundle_charm_info, series)
96+
97+ if charm_query:
98+ # Order all results by revision so that we grab the latest match
99+ # as we might not always have a revision to work with.
100+ sort = [('store_data.revision', pymongo.DESCENDING)]
101+ results = db.charms.find(charm_query, sort=sort).limit(1)
102+ if results.count() > 0:
103+ return results[0]
104+ return None
105+
106+
107 def _find_charms(collection, spec=None, fields=None, skip=0, limit=0,
108 timeout=True, snapshot=False, tailable=False, sort=None,
109 max_scan=None, as_class=None, all_revisions=False, **kwargs):
110
111=== modified file 'charmworld/testing/factory.py'
112--- charmworld/testing/factory.py 2013-10-03 17:13:56 +0000
113+++ charmworld/testing/factory.py 2013-10-07 14:35:40 +0000
114@@ -256,7 +256,7 @@
115 charm = dict(payload)
116 add_store_data(charm, promulgated, charm_error, store_revision)
117 charm.update({
118- '_id': construct_charm_id(charm),
119+ '_id': construct_charm_id(charm, revision),
120 'address': get_address(charm, promulgated),
121 "branch_dir": os.path.join(branch_root, charm['name']),
122 "categories": categories,
123
124=== modified file 'charmworld/tests/test_models.py'
125--- charmworld/tests/test_models.py 2013-09-10 14:45:25 +0000
126+++ charmworld/tests/test_models.py 2013-10-07 14:35:40 +0000
127@@ -27,6 +27,7 @@
128
129 from charmworld.models import (
130 acquire_session_secret,
131+ _build_charm_query_from_bundle_info,
132 Bundle,
133 Charm,
134 CharmFile,
135@@ -40,6 +41,7 @@
136 options_to_storage,
137 QAData,
138 QADataSource,
139+ resolve_charm_from_bundle_info,
140 slurp_files,
141 storage_to_options,
142 store_bundles,
143@@ -1644,7 +1646,7 @@
144 bundle_data = {}
145 bundle = Bundle(bundle_data)
146 self.assertIs(bundle_data, bundle._raw_representation)
147- self.assertEqual(bundle._COMMON_REPRESENTATION, bundle._representation)
148+ self.assertEqual(bundle._MONGO_FIELDS, bundle._representation)
149
150 def test_init_with_minimal_data(self):
151 bundle_data = {
152@@ -1852,6 +1854,51 @@
153 '~bac/sample_bundle/1',
154 bundle.basket_id)
155
156+ def test_charm_resolving_from_bundle_branch(self):
157+ service_data = {
158+ 'charm': 'mediawiki',
159+ 'branch': 'lp:charms/precise/mediawiki'
160+ }
161+ query = _build_charm_query_from_bundle_info(service_data, None)
162+ self.assertEqual(
163+ '~charmers/charms/precise/mediawiki/trunk',
164+ query['branch_spec'])
165+ self.assertNotIn('store_data.revision', query)
166+
167+ # And supports a version.
168+ service_data['branch'] = 'lp:charms/precise/mediawiki@7'
169+ query = _build_charm_query_from_bundle_info(service_data, None)
170+ self.assertEqual(
171+ '~charmers/charms/precise/mediawiki/trunk',
172+ query['branch_spec'])
173+ self.assertEqual(7, query['store_data.revision'])
174+
175+ def test_charm_resolving_from_bundle_charm_csurl(self):
176+ service_data = {
177+ 'charm': 'cs:precise/mediawiki-7'
178+ }
179+ query = _build_charm_query_from_bundle_info(service_data, None)
180+ self.assertEqual(
181+ 'cs:precise/mediawiki-7',
182+ query['store_url'])
183+
184+ def test_charm_resolving_from_bundle_charm_and_series(self):
185+ service_data = {
186+ 'charm': 'mediawiki'
187+ }
188+ series = 'precise'
189+ query = _build_charm_query_from_bundle_info(service_data, series)
190+ self.assertEqual(
191+ 'cs:precise/mediawiki',
192+ query['address'])
193+
194+ def test_charm_resolving_from_bundle_empty_missing_info(self):
195+ service_data = {
196+ 'charm': 'mediawiki'
197+ }
198+ query = _build_charm_query_from_bundle_info(service_data, None)
199+ self.assertIsNone(query)
200+
201
202 class TestMakeBundleDoc(TestCase):
203
204@@ -1953,6 +2000,76 @@
205 self.assertEqual(expected, filelist)
206
207
208+class TestBundleLoadingCharms(MongoTestBase):
209+
210+ def setUp(self):
211+ """Create 3 of the same charms at different revisions"""
212+ super(TestBundleLoadingCharms, self).setUp()
213+ charm_args = {
214+ 'name': 'wikipedia',
215+ 'series': 'precise',
216+ 'owner': 'charmers',
217+ 'promulgated': True
218+ }
219+ charm_args['revision'] = 1
220+ charm_args['store_revision'] = 1
221+ factory.makeCharm(self.db, **charm_args)
222+ charm_args['revision'] = 2
223+ charm_args['store_revision'] = 2
224+ factory.makeCharm(self.db, **charm_args)
225+ charm_args['revision'] = 3
226+ charm_args['store_revision'] = 3
227+ factory.makeCharm(self.db, **charm_args)
228+
229+ def test_resolve_by_store_url(self):
230+ data = {
231+ 'charm': 'cs:precise/wikipedia-1'
232+ }
233+ charm = resolve_charm_from_bundle_info(self.db, data, None)
234+ self.assertIsNotNone(charm)
235+ self.assertEqual('~charmers/precise/wikipedia/1', charm['_id'])
236+ self.assertEqual('wikipedia', charm['name'])
237+ self.assertEqual(1, charm['store_data']['revision'])
238+ self.assertEqual(1, charm['revision'])
239+
240+ def test_resolve_by_name_and_series(self):
241+ data = {
242+ 'charm': 'wikipedia'
243+ }
244+ charm = resolve_charm_from_bundle_info(self.db, data, 'precise')
245+ self.assertIsNotNone(charm)
246+ self.assertEqual('wikipedia', charm['name'])
247+ self.assertEqual(3, charm['store_data']['revision'])
248+ self.assertEqual(3, charm['revision'])
249+
250+ def test_resolve_by_branch(self):
251+ data = {
252+ 'branch': 'lp:charms/precise/wikipedia'
253+ }
254+ charm = resolve_charm_from_bundle_info(self.db, data, 'precise')
255+ self.assertIsNotNone(charm)
256+ self.assertEqual('wikipedia', charm['name'])
257+ self.assertEqual(3, charm['store_data']['revision'])
258+ self.assertEqual(3, charm['revision'])
259+
260+ def test_resolve_by_branch_with_revision(self):
261+ data = {
262+ 'branch': 'lp:charms/precise/wikipedia@2'
263+ }
264+ charm = resolve_charm_from_bundle_info(self.db, data, 'precise')
265+ self.assertIsNotNone(charm)
266+ self.assertEqual('wikipedia', charm['name'])
267+ self.assertEqual(2, charm['store_data']['revision'])
268+ self.assertEqual(2, charm['revision'])
269+
270+ def test_not_enough_info(self):
271+ data = {
272+ 'name': 'wikipedia'
273+ }
274+ charm = resolve_charm_from_bundle_info(self.db, data, None)
275+ self.assertIsNone(charm)
276+
277+
278 class TestFeaturedSource(MongoTestBase):
279
280 def test_set_and_get_work(self):
281
282=== modified file 'charmworld/views/api.py'
283--- charmworld/views/api.py 2013-10-03 17:13:56 +0000
284+++ charmworld/views/api.py 2013-10-07 14:35:40 +0000
285@@ -29,6 +29,7 @@
286 FeaturedSource,
287 getfs,
288 QADataSource,
289+ resolve_charm_from_bundle_info,
290 )
291 from charmworld.search import (
292 BUNDLE,
293@@ -427,6 +428,25 @@
294 query, sort=[('store_data.revision', pymongo.DESCENDING)])
295 return charm_id, trailing, charm
296
297+ def _build_bundle_metadata(self, bundle, db):
298+ bundle_dict = dict(bundle)
299+ # Add the list of files in the bundle.
300+ bundle_dict['files'] = bundle.get_file_list(db)
301+ bundle_dict['charm_metadata'] = {}
302+ # Now load the charm information we require for the services in the
303+ # bundle.
304+ for service, data in bundle.data['services'].iteritems():
305+ charm = resolve_charm_from_bundle_info(
306+ db,
307+ data,
308+ bundle_dict.get('series')
309+ )
310+ if charm:
311+ formatted = self._format_charm(Charm(charm))
312+ bundle_dict['charm_metadata'][service] = formatted
313+
314+ return bundle_dict
315+
316 def _find_bundle(self, path):
317 try:
318 bundle_id, trailing, bundle_bits = self._parse_bundle_id(path)
319@@ -463,6 +483,7 @@
320 if charm_data is None:
321 return json_response(
322 404, {'type': 'no_such_charm', 'charm_id': charm_id})
323+
324 charm = Charm(charm_data)
325 if trailing is None:
326 return self._charm_details(charm_data)
327@@ -503,6 +524,7 @@
328 result = {'type': 'no_such_bundle', 'bundle_id': '/'.join(path)}
329 return json_response(status, result)
330 if not trailing:
331+ bundle = self._build_bundle_metadata(bundle, self.request.db)
332 return json_response(200, bundle)
333 elif trailing and trailing[0] == self.ICON_TRAILING:
334 return self._bundle_file(bundle, filename)
335
336=== modified file 'charmworld/views/tests/test_api.py'
337--- charmworld/views/tests/test_api.py 2013-10-03 17:13:56 +0000
338+++ charmworld/views/tests/test_api.py 2013-10-07 14:35:40 +0000
339@@ -779,7 +779,20 @@
340 self.assertEqual(200, response.status_code)
341
342 def test_results_match(self):
343- self.makeBundle(name='bat', owner='bac', basket_with_rev='byobu/4')
344+ # Make the charm that we'll use as a service.
345+ _id, charm = factory.makeCharm(
346+ self.db,
347+ description=''
348+ )
349+ services = {
350+ u'charm': {
351+ u'branch': charm['branch_spec'],
352+ u'charm': charm['name']
353+ }
354+ }
355+ self.makeBundle(
356+ name='bat', owner='bac',
357+ basket_with_rev='byobu/4', services=services)
358 response = self.get_response('bundle', '~bac/byobu/4/bat')
359 self.assertEqual(200, response.status_code)
360 self.assertEqual(
361@@ -788,12 +801,21 @@
362 u'basket_name': u'byobu',
363 u'basket_revision': 4,
364 u'branch_deleted': False,
365+ u'charm_metadata': {
366+ u'charm': self.api_class._format_charm(Charm(charm))
367+ },
368 u'data': {
369 u'series': u'precise',
370- u'services': {},
371+ u'services': {
372+ u'charm': {
373+ u'branch': charm['branch_spec'],
374+ u'charm': charm['name']
375+ }
376+ },
377 u'relations': {},
378 },
379 u'description': u'',
380+ u'files': [],
381 u'id': u'~bac/byobu/4/bat',
382 u'name': u'bat',
383 u'owner': u'bac',

Subscribers

People subscribed via source and target branches