Merge lp:~frankban/juju-quickstart/changeset-from-jujubundlelib into lp:juju-quickstart
- changeset-from-jujubundlelib
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 126 |
Proposed branch: | lp:~frankban/juju-quickstart/changeset-from-jujubundlelib |
Merge into: | lp:juju-quickstart |
Diff against target: |
453 lines (+186/-61) 4 files modified
quickstart/models/bundles.py (+17/-25) quickstart/tests/helpers.py (+42/-3) quickstart/tests/models/test_bundles.py (+123/-29) tox.ini (+4/-4) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/changeset-from-jujubundlelib |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+257248@code.launchpad.net |
Commit message
Description of the change
Juju bundlelib validation in quickstart.
Validate bundles using jujubundlelib.
Also add to the bundle model the ability
to return its change set.
Francesco Banconi (frankban) wrote : | # |
- 132. By Francesco Banconi
-
done done
Francesco Banconi (frankban) wrote : | # |
Please take a look.
Jeff Pihach (hatch) wrote : | # |
LGTM!
https:/
File quickstart/
https:/
quickstart/
data['services'
I'm wondering if we shouldn't be a little bit more proactive here by
throwing a warning to the user but then continuing on ignoring that they
supplied their own GUI instance. We could possibly even try to apply
their supplied GUI config.
Francesco Banconi (frankban) wrote : | # |
On 2015/04/23 14:02:41, jeff.pihach wrote:
> LGTM!
https:/
> File quickstart/
https:/
> quickstart/
> data['services'
> I'm wondering if we shouldn't be a little bit more proactive here by
throwing a
> warning to the user but then continuing on ignoring that they supplied
their own
> GUI instance. We could possibly even try to apply their supplied GUI
config.
Thanks for the review!
This is a good suggestion. Something like this can be implemented later,
but it's not trivial in the current approach which makes use of the
deployer in the backend.
Madison Scott-Clary (makyo) wrote : | # |
LGTM - great step forward
Francesco Banconi (frankban) wrote : | # |
Thanks for the reviews!
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Juju bundlelib validation in quickstart.
Validate bundles using jujubundlelib.
Also add to the bundle model the ability
to return its change set.
R=jeff.pihach, matthew.scott
CC=
https:/
Preview Diff
1 | === modified file 'quickstart/models/bundles.py' |
2 | --- quickstart/models/bundles.py 2015-04-21 10:22:46 +0000 |
3 | +++ quickstart/models/bundles.py 2015-04-23 12:16:34 +0000 |
4 | @@ -46,7 +46,11 @@ |
5 | import os |
6 | import re |
7 | |
8 | -from jujubundlelib import references |
9 | +from jujubundlelib import ( |
10 | + changeset, |
11 | + references, |
12 | + validation, |
13 | +) |
14 | |
15 | from quickstart import ( |
16 | charmstore, |
17 | @@ -83,6 +87,13 @@ |
18 | def __repr__(self): |
19 | return b'<Bundle: {}>'.format(bytes(self)) |
20 | |
21 | + def get_changeset(self): |
22 | + """Return the collections of changes required to deploy this bundle. |
23 | + |
24 | + The change set is returned as a generator object. |
25 | + """ |
26 | + return changeset.parse(self.data) |
27 | + |
28 | def serialize(self): |
29 | """Serialize the bundle data as a YAML encoded string.""" |
30 | return serializers.yaml_dump(self.data) |
31 | @@ -277,12 +288,6 @@ |
32 | def parse_yaml(content): |
33 | """Parse and validate the given bundle content as a YAML encoded string. |
34 | |
35 | - Note that the bundle validation performed by Juju Quickstart is weak by |
36 | - design: it just checks that the content looks like a bundle YAML. Contents |
37 | - provided by the charm store are already known as valid. For other sources, |
38 | - a more cogent validation is done down in the stack, when the content is |
39 | - sent to the GUI server and then to the Juju deployer. |
40 | - |
41 | Return the resulting YAML decoded dictionary. |
42 | Raise a ValueError if: |
43 | - the bundle YAML contents are not parsable by YAML; |
44 | @@ -372,29 +377,16 @@ |
45 | def validate(data): |
46 | """Validate the given YAML decoded bundle data. |
47 | |
48 | - Note that the bundle validation performed by Juju Quickstart is weak by |
49 | - design: it just checks that the content looks like a bundle YAML. Contents |
50 | - provided by the charm store are already known as valid. For other sources, |
51 | - a more cogent validation is done down in the stack, when the content is |
52 | - sent to the GUI server and then to the Juju deployer. |
53 | - |
54 | Raise a ValueError if: |
55 | - the YAML contents are not properly structured; |
56 | - the bundle does not include services. |
57 | """ |
58 | - _ensure_is_dict(data) |
59 | - # Retrieve the bundle services. |
60 | - try: |
61 | - services = data['services'].keys() |
62 | - except (AttributeError, KeyError, TypeError): |
63 | - content = serializers.yaml_dump(data).strip() |
64 | - msg = 'unable to retrieve bundle services: {}'.format(content) |
65 | + errors = validation.validate(data) |
66 | + if errors: |
67 | + msg = 'invalid bundle data:\n {}'.format('\n '.join(errors)) |
68 | raise ValueError(msg.encode('utf-8')) |
69 | - # Ensure at least one service is defined in the bundle. |
70 | - if not services: |
71 | - raise ValueError(b'no services found in the bundle') |
72 | - # Check that the Juju GUI charm is not included as a service. |
73 | - if settings.JUJU_GUI_SERVICE_NAME in services: |
74 | + # Check that the Juju GUI charm is not included in the bundle as a service. |
75 | + if settings.JUJU_GUI_SERVICE_NAME in data['services'].keys(): |
76 | raise ValueError( |
77 | b'the provided bundle contains an instance of juju-gui. Juju ' |
78 | b'Quickstart will install the latest version of the Juju GUI ' |
79 | |
80 | === modified file 'quickstart/tests/helpers.py' |
81 | --- quickstart/tests/helpers.py 2015-03-09 18:20:47 +0000 |
82 | +++ quickstart/tests/helpers.py 2015-04-23 12:16:34 +0000 |
83 | @@ -18,7 +18,9 @@ |
84 | |
85 | from __future__ import unicode_literals |
86 | |
87 | +import collections |
88 | from contextlib import contextmanager |
89 | +import copy |
90 | import os |
91 | import shutil |
92 | import socket |
93 | @@ -47,11 +49,33 @@ |
94 | class BundleFileTestsMixin(object): |
95 | """Shared methods for testing Juju bundle files.""" |
96 | |
97 | - bundle_data = {'services': {'wordpress': {}, 'mysql': {}}} |
98 | + bundle_data = { |
99 | + 'services': { |
100 | + 'wordpress': { |
101 | + 'charm': 'cs:trusty/wordpress-42', |
102 | + 'num_units': 1, |
103 | + }, |
104 | + 'mysql': { |
105 | + 'charm': 'cs:vivid/mysql-47', |
106 | + 'num_units': 0, |
107 | + }, |
108 | + }, |
109 | + } |
110 | bundle_content = yaml.safe_dump(bundle_data) |
111 | legacy_bundle_data = { |
112 | - 'bundle1': {'services': {'wordpress': {}, 'mysql': {}}}, |
113 | - 'bundle2': {'services': {'django': {}, 'nodejs': {}}}, |
114 | + 'bundle1': bundle_data, |
115 | + 'bundle2': { |
116 | + 'services': { |
117 | + 'django': { |
118 | + 'charm': 'cs:vivid/django-42', |
119 | + 'num_units': 2, |
120 | + }, |
121 | + 'nodejs': { |
122 | + 'charm': 'nodejs', |
123 | + 'num_units': 1, |
124 | + }, |
125 | + }, |
126 | + }, |
127 | } |
128 | legacy_bundle_content = yaml.safe_dump(legacy_bundle_data) |
129 | |
130 | @@ -269,6 +293,21 @@ |
131 | return {'environments': environments} |
132 | |
133 | |
134 | +def make_ordered_bundle(bundle): |
135 | + """Given a bundle, return a new one with the same data sorted. |
136 | + |
137 | + The provided bundle must be an an instance of |
138 | + "quickstart.models.bundles.Bundle". |
139 | + The resulting bundle can be used to test its corresponding change set in a |
140 | + deterministic way. |
141 | + """ |
142 | + data = copy.deepcopy(bundle.data) |
143 | + for key, value in data.items(): |
144 | + if isinstance(value, collections.Mapping): |
145 | + data[key] = collections.OrderedDict(sorted(value.items())) |
146 | + return bundle.__class__(data, reference=bundle.reference) |
147 | + |
148 | + |
149 | # Mock the builtin print function. |
150 | mock_print = mock.patch('__builtin__.print') |
151 | |
152 | |
153 | === modified file 'quickstart/tests/models/test_bundles.py' |
154 | --- quickstart/tests/models/test_bundles.py 2015-04-21 10:22:46 +0000 |
155 | +++ quickstart/tests/models/test_bundles.py 2015-04-23 12:16:34 +0000 |
156 | @@ -61,17 +61,58 @@ |
157 | bundle = bundles.Bundle(self.bundle_data) |
158 | self.assertEqual('<Bundle: bundle>', repr(bundle)) |
159 | |
160 | + def test_get_changeset(self): |
161 | + # The collection of changes corresponding to the bundle is correctly |
162 | + # returned. |
163 | + expected_changeset = ( |
164 | + {'id': 'addCharm-0', |
165 | + 'method': 'addCharm', |
166 | + 'args': ['cs:vivid/mysql-47'], |
167 | + 'requires': []}, |
168 | + {'id': 'addService-1', |
169 | + 'method': 'deploy', |
170 | + 'args': ['cs:vivid/mysql-47', 'mysql', {}], |
171 | + 'requires': ['addCharm-0']}, |
172 | + {'id': 'addCharm-2', |
173 | + 'method': 'addCharm', |
174 | + 'args': ['cs:trusty/wordpress-42'], |
175 | + 'requires': []}, |
176 | + {'id': 'addService-3', |
177 | + 'method': 'deploy', |
178 | + 'args': ['cs:trusty/wordpress-42', 'wordpress', {}], |
179 | + 'requires': ['addCharm-2']}, |
180 | + {'id': 'addUnit-4', |
181 | + 'method': 'addUnit', |
182 | + 'args': ['$addService-3', 1, None], |
183 | + 'requires': ['addService-3']}, |
184 | + ) |
185 | + bundle = helpers.make_ordered_bundle(self.bundle) |
186 | + self.assertEqual(expected_changeset, tuple(bundle.get_changeset())) |
187 | + |
188 | def test_serialization(self): |
189 | # The bundle data is correctly serialized into a YAML encoded string. |
190 | - content = self.bundle.serialize() |
191 | - self.assertEqual('services:\n mysql: {}\n wordpress: {}\n', content) |
192 | + expected_content = ( |
193 | + 'services:\n' |
194 | + ' mysql:\n' |
195 | + ' charm: cs:vivid/mysql-47\n' |
196 | + ' num_units: 0\n' |
197 | + ' wordpress:\n' |
198 | + ' charm: cs:trusty/wordpress-42\n' |
199 | + ' num_units: 1\n') |
200 | + self.assertEqual(expected_content, self.bundle.serialize()) |
201 | |
202 | def test_legacy_serialization(self): |
203 | # The bundle data can be serialized for legacy API version 3. |
204 | - content = self.bundle.serialize_legacy() |
205 | - self.assertEqual( |
206 | - 'bundle:\n services:\n mysql: {}\n wordpress: {}\n', |
207 | - content) |
208 | + expected_content = ( |
209 | + 'bundle:\n' |
210 | + ' services:\n' |
211 | + ' mysql:\n' |
212 | + ' charm: cs:vivid/mysql-47\n' |
213 | + ' num_units: 0\n' |
214 | + ' wordpress:\n' |
215 | + ' charm: cs:trusty/wordpress-42\n' |
216 | + ' num_units: 1\n') |
217 | + self.assertEqual(expected_content, self.bundle.serialize_legacy()) |
218 | |
219 | def test_services(self): |
220 | # Bundle services can be easily retrieved. |
221 | @@ -167,8 +208,10 @@ |
222 | def test_charmworld_bundle_invalid_content(self): |
223 | # A ValueError is raised if the content associated with the given |
224 | # charmworld URL is not valid. |
225 | + expected_error = ( |
226 | + 'invalid bundle data:\n bundle does not appear to be a bundle') |
227 | with self.patch_urlread(contents='exterminate!'): |
228 | - with self.assert_value_error('invalid YAML content: exterminate!'): |
229 | + with self.assert_value_error(expected_error): |
230 | bundles.from_source('bundle:mediawiki/single') |
231 | |
232 | def test_charmworld_bundle_connection_error(self): |
233 | @@ -307,8 +350,10 @@ |
234 | def test_jujucharms_bundle_invalid_content(self): |
235 | # A ValueError is raised if the content associated with the given |
236 | # jujucharms.com URL are not valid. |
237 | + expected_error = ( |
238 | + 'invalid bundle data:\n bundle does not appear to be a bundle') |
239 | with self.patch_urlread(contents='exterminate!'): |
240 | - with self.assert_value_error('invalid YAML content: exterminate!'): |
241 | + with self.assert_value_error(expected_error): |
242 | bundles.from_source('wordpress-scalable') |
243 | |
244 | def test_jujucharms_bundle_connection_error(self): |
245 | @@ -348,7 +393,12 @@ |
246 | # a legacy version 3 bundle. |
247 | # In this case, the resulting bundle does not have a reference. |
248 | legacy_bundle_data = { |
249 | - 'bundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
250 | + 'bundle': { |
251 | + 'services': { |
252 | + 'wordpress': {'charm': 'wordpress', 'num_units': 1}, |
253 | + 'mysql': {'charm': 'mysql', 'num_units': 1}, |
254 | + } |
255 | + }, |
256 | } |
257 | path = self.make_bundle_file(legacy_bundle_data) |
258 | bundle = bundles.from_source(path) |
259 | @@ -407,7 +457,8 @@ |
260 | # A ValueError is raised if a local file contains an invalid legacy |
261 | # version 3 content. |
262 | path = self.make_bundle_file({'bundle': '42'}) |
263 | - expected_error = "invalid YAML content: 42" |
264 | + expected_error = ( |
265 | + 'invalid bundle data:\n bundle does not appear to be a bundle') |
266 | with self.assert_value_error(expected_error): |
267 | bundles.from_source(path, 'bundle') |
268 | |
269 | @@ -503,46 +554,62 @@ |
270 | |
271 | def test_yaml_invalid_type(self): |
272 | # A ValueError is raised if the bundle content is not well formed. |
273 | - with self.assert_value_error('invalid YAML content: a-string'): |
274 | + expected_error = ( |
275 | + 'invalid bundle data:\n bundle does not appear to be a bundle') |
276 | + with self.assert_value_error(expected_error): |
277 | bundles.parse_yaml('a-string') |
278 | |
279 | def test_yaml_invalid_bundle_data(self): |
280 | # A ValueError is raised if the bundle content is not well formed. |
281 | contents = yaml.safe_dump('not valid') |
282 | - with self.assert_value_error('invalid YAML content: not valid'): |
283 | + expected_error = ( |
284 | + 'invalid bundle data:\n bundle does not appear to be a bundle') |
285 | + with self.assert_value_error(expected_error): |
286 | bundles.parse_yaml(contents) |
287 | |
288 | def test_yaml_no_services(self): |
289 | # A ValueError is raised if the bundle does not include services. |
290 | contents = yaml.safe_dump({}) |
291 | - with self.assert_value_error('unable to retrieve bundle services: {}'): |
292 | + expected_error = ( |
293 | + 'invalid bundle data:\n bundle does not define any services') |
294 | + with self.assert_value_error(expected_error): |
295 | bundles.parse_yaml(contents) |
296 | |
297 | def test_yaml_none_bundle_services(self): |
298 | # A ValueError is raised if services are None. |
299 | contents = yaml.safe_dump({'services': None}) |
300 | - expected = 'unable to retrieve bundle services: services: null' |
301 | - with self.assert_value_error(expected): |
302 | + expected_error = ( |
303 | + 'invalid bundle data:\n bundle does not define any services') |
304 | + with self.assert_value_error(expected_error): |
305 | bundles.parse_yaml(contents) |
306 | |
307 | def test_yaml_invalid_bundle_services_type(self): |
308 | # A ValueError is raised if services have an invalid type. |
309 | contents = yaml.safe_dump({'services': 42}) |
310 | - expected = 'unable to retrieve bundle services: services: 42' |
311 | - with self.assert_value_error(expected): |
312 | + expected_error = ( |
313 | + 'invalid bundle data:\n' |
314 | + ' services spec does not appear to be well-formed') |
315 | + with self.assert_value_error(expected_error): |
316 | bundles.parse_yaml(contents) |
317 | |
318 | def test_no_services(self): |
319 | # A ValueError is raised if the specified bundle does not contain |
320 | # services. |
321 | contents = yaml.safe_dump({'services': {}}) |
322 | - with self.assert_value_error('no services found in the bundle'): |
323 | + expected_error = ( |
324 | + 'invalid bundle data:\n bundle does not define any services') |
325 | + with self.assert_value_error(expected_error): |
326 | bundles.parse_yaml(contents) |
327 | |
328 | def test_yaml_gui_in_services(self): |
329 | # A ValueError is raised if the bundle contains juju-gui. |
330 | contents = yaml.safe_dump({ |
331 | - 'services': {settings.JUJU_GUI_SERVICE_NAME: {}}, |
332 | + 'services': { |
333 | + settings.JUJU_GUI_SERVICE_NAME: { |
334 | + 'charm': 'juju-gui', |
335 | + 'num_units': 1, |
336 | + }, |
337 | + }, |
338 | }) |
339 | expected_error = ( |
340 | 'the provided bundle contains an instance of juju-gui. Juju ' |
341 | @@ -586,25 +653,32 @@ |
342 | |
343 | def test_yaml_no_services(self): |
344 | # A ValueError is raised if the bundle does not include services. |
345 | - with self.assert_value_error('unable to retrieve bundle services: {}'): |
346 | + expected_error = ( |
347 | + 'invalid bundle data:\n bundle does not define any services') |
348 | + with self.assert_value_error(expected_error): |
349 | bundles.validate({}) |
350 | |
351 | def test_yaml_none_bundle_services(self): |
352 | # A ValueError is raised if services are None. |
353 | - expected = 'unable to retrieve bundle services: services: null' |
354 | - with self.assert_value_error(expected): |
355 | + expected_error = ( |
356 | + 'invalid bundle data:\n bundle does not define any services') |
357 | + with self.assert_value_error(expected_error): |
358 | bundles.validate({'services': None}) |
359 | |
360 | def test_yaml_invalid_bundle_services_type(self): |
361 | # A ValueError is raised if services have an invalid type. |
362 | - expected = 'unable to retrieve bundle services: services: 42' |
363 | - with self.assert_value_error(expected): |
364 | + expected_error = ( |
365 | + 'invalid bundle data:\n' |
366 | + ' services spec does not appear to be well-formed') |
367 | + with self.assert_value_error(expected_error): |
368 | bundles.validate({'services': 42}) |
369 | |
370 | def test_no_services(self): |
371 | # A ValueError is raised if the specified bundle does not contain |
372 | # services. |
373 | - with self.assert_value_error('no services found in the bundle'): |
374 | + expected_error = ( |
375 | + 'invalid bundle data:\n bundle does not define any services') |
376 | + with self.assert_value_error(expected_error): |
377 | bundles.validate({'services': {}}) |
378 | |
379 | def test_yaml_gui_in_services(self): |
380 | @@ -613,10 +687,30 @@ |
381 | 'the provided bundle contains an instance of juju-gui. Juju ' |
382 | 'Quickstart will install the latest version of the Juju GUI ' |
383 | 'automatically; please remove juju-gui from the bundle') |
384 | - with self.assert_value_error(expected_error): |
385 | - bundles.validate({ |
386 | - 'services': {settings.JUJU_GUI_SERVICE_NAME: {}}, |
387 | - }) |
388 | + data = { |
389 | + 'services': { |
390 | + settings.JUJU_GUI_SERVICE_NAME: { |
391 | + 'charm': 'juju-gui', |
392 | + 'num_units': 1, |
393 | + }, |
394 | + }, |
395 | + } |
396 | + with self.assert_value_error(expected_error): |
397 | + bundles.validate(data) |
398 | + |
399 | + def test_multiple_errors(self): |
400 | + # A ValueError with multiple bundle errors is properly reported. |
401 | + expected_error = ( |
402 | + 'invalid bundle data:\n' |
403 | + ' no charm specified for service django\n' |
404 | + ' num_units for service django must be an integer\n' |
405 | + ' machine 1 not referred to by a placement directive') |
406 | + data = { |
407 | + 'services': {'django': {}}, |
408 | + 'machines': {'1': {}}, |
409 | + } |
410 | + with self.assert_value_error(expected_error): |
411 | + bundles.validate(data) |
412 | |
413 | def test_success(self): |
414 | # The function succeeds when a valid bundle content is provided. |
415 | |
416 | === modified file 'tox.ini' |
417 | --- tox.ini 2015-04-21 10:13:12 +0000 |
418 | +++ tox.ini 2015-04-23 12:16:34 +0000 |
419 | @@ -72,7 +72,7 @@ |
420 | # See https://launchpad.net/~juju/+archive/ubuntu/stable. |
421 | websocket-client==0.18.0 |
422 | jujuclient==0.50.1 |
423 | - jujubundlelib==0.1.1 |
424 | + jujubundlelib==0.1.3 |
425 | urwid==1.2.1 |
426 | # The distribution PyYAML requirement is used in this case. |
427 | |
428 | @@ -82,7 +82,7 @@ |
429 | # Ubuntu 14.04 (trusty) distro dependencies. |
430 | websocket-client==0.12.0 |
431 | jujuclient==0.17.5 |
432 | - jujubundlelib==0.1.1 |
433 | + jujubundlelib==0.1.3 |
434 | PyYAML==3.10 |
435 | urwid==1.1.1 |
436 | |
437 | @@ -92,7 +92,7 @@ |
438 | # Ubuntu 14.10 (utopic) distro dependencies. |
439 | websocket-client==0.12.0 |
440 | jujuclient==0.17.5 |
441 | - jujubundlelib==0.1.1 |
442 | + jujubundlelib==0.1.3 |
443 | PyYAML==3.11 |
444 | urwid==1.2.1 |
445 | |
446 | @@ -102,7 +102,7 @@ |
447 | # Ubuntu 15.04 (vivid) distro dependencies. |
448 | websocket-client==0.18.0 |
449 | jujuclient==0.18.5 |
450 | - jujubundlelib==0.1.1 |
451 | + jujubundlelib==0.1.3 |
452 | PyYAML==3.11 |
453 | urwid==1.2.1 |
454 |
Reviewers: mp+257248_ code.launchpad. net,
Message:
Please take a look.
Description:
Juju bundlelib validation in quickstart.
Validate bundles using jujubundlelib.
Also add to the bundle model the ability
to return its change set.
https:/ /code.launchpad .net/~frankban/ juju-quickstart /changeset- from-jujubundle lib/+merge/ 257248
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/232890043/
Affected files (+188, -61 lines): models/ bundles. py tests/helpers. py tests/models/ test_bundles. py
A [revision details]
M quickstart/
M quickstart/
M quickstart/
M tox.ini