Merge lp:~rharding/charmworld/proof-config-2 into lp:~juju-jitsu/charmworld/trunk
- proof-config-2
- Merge into trunk
Proposed by
Richard Harding
Status: | Merged |
---|---|
Merged at revision: | 426 |
Proposed branch: | lp:~rharding/charmworld/proof-config-2 |
Merge into: | lp:~juju-jitsu/charmworld/trunk |
Diff against target: |
1009 lines (+419/-407) 9 files modified
charmworld/lib/proof.py (+0/-33) charmworld/lib/tests/test_proof.py (+2/-72) charmworld/models.py (+17/-1) charmworld/tests/test_models.py (+39/-0) charmworld/views/api/__init__.py (+4/-132) charmworld/views/api/proof.py (+145/-0) charmworld/views/tests/test_api.py (+4/-169) charmworld/views/tests/test_proof.py (+175/-0) charmworld/views/utils.py (+33/-0) |
To merge this branch: | bzr merge lp:~rharding/charmworld/proof-config-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju-Jitsu Hackers | Pending | ||
Review via email: mp+192239@code.launchpad.net |
Commit message
Refactor the proof api code to make more usable.
Description of the change
Refactor the proof api code to make more usable.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charmworld/lib/proof.py' | |||
2 | --- charmworld/lib/proof.py 2013-10-18 18:32:48 +0000 | |||
3 | +++ charmworld/lib/proof.py 2013-10-22 20:55:07 +0000 | |||
4 | @@ -1,10 +1,4 @@ | |||
5 | 1 | """Helpers to proof charms and bundles.""" | 1 | """Helpers to proof charms and bundles.""" |
6 | 2 | import yaml | ||
7 | 3 | |||
8 | 4 | from charmworld.models import ( | ||
9 | 5 | BundledCharmDescription, | ||
10 | 6 | resolve_charm_from_description, | ||
11 | 7 | ) | ||
12 | 8 | 2 | ||
13 | 9 | 3 | ||
14 | 10 | class ProofError(Exception): | 4 | class ProofError(Exception): |
15 | @@ -14,33 +8,6 @@ | |||
16 | 14 | self.msg = msg | 8 | self.msg = msg |
17 | 15 | 9 | ||
18 | 16 | 10 | ||
19 | 17 | class BundleProof(object): | ||
20 | 18 | |||
21 | 19 | @staticmethod | ||
22 | 20 | def check_parseable_deployer_file(data, format='yaml'): | ||
23 | 21 | try: | ||
24 | 22 | bundle_data = yaml.safe_load(data) | ||
25 | 23 | except yaml.YAMLError: | ||
26 | 24 | raise ProofError( | ||
27 | 25 | data, | ||
28 | 26 | 'Could not parse the yaml provided.' | ||
29 | 27 | ) | ||
30 | 28 | return bundle_data | ||
31 | 29 | |||
32 | 30 | @staticmethod | ||
33 | 31 | def check_service_exists(db, name, service_data, bundle_config): | ||
34 | 32 | charm_description = BundledCharmDescription( | ||
35 | 33 | 'proof', service_data, bundle_config.get('series')) | ||
36 | 34 | charm_found = resolve_charm_from_description(db, charm_description) | ||
37 | 35 | if not charm_found: | ||
38 | 36 | raise ProofError( | ||
39 | 37 | charm_description.get_process(), | ||
40 | 38 | 'Could not find charm: ' + name, | ||
41 | 39 | ) | ||
42 | 40 | else: | ||
43 | 41 | return charm_found | ||
44 | 42 | |||
45 | 43 | |||
46 | 44 | class CharmProof(object): | 11 | class CharmProof(object): |
47 | 45 | 12 | ||
48 | 46 | @staticmethod | 13 | @staticmethod |
49 | 47 | 14 | ||
50 | === modified file 'charmworld/lib/tests/test_proof.py' | |||
51 | --- charmworld/lib/tests/test_proof.py 2013-10-18 18:57:16 +0000 | |||
52 | +++ charmworld/lib/tests/test_proof.py 2013-10-22 20:55:07 +0000 | |||
53 | @@ -1,11 +1,6 @@ | |||
59 | 1 | from mock import ( | 1 | from mock import Mock |
55 | 2 | Mock, | ||
56 | 3 | patch, | ||
57 | 4 | ) | ||
58 | 5 | import yaml | ||
60 | 6 | 2 | ||
61 | 7 | from charmworld.lib.proof import ( | 3 | from charmworld.lib.proof import ( |
62 | 8 | BundleProof, | ||
63 | 9 | CharmProof, | 4 | CharmProof, |
64 | 10 | ProofError, | 5 | ProofError, |
65 | 11 | ) | 6 | ) |
66 | @@ -13,71 +8,6 @@ | |||
67 | 13 | from charmworld.testing import TestCase | 8 | from charmworld.testing import TestCase |
68 | 14 | 9 | ||
69 | 15 | 10 | ||
70 | 16 | class TestBundleProof(TestCase): | ||
71 | 17 | """Verify that a bundle can be proofed in charmworld.""" | ||
72 | 18 | |||
73 | 19 | sample_deployer_file = """ | ||
74 | 20 | { | ||
75 | 21 | "wiki": { | ||
76 | 22 | "services": { | ||
77 | 23 | "wiki": { | ||
78 | 24 | "charm": "mediawiki", | ||
79 | 25 | "num_units": 2, | ||
80 | 26 | "branch": "lp:charms/precise/mediawiki", | ||
81 | 27 | "constraints": "mem=2" | ||
82 | 28 | } | ||
83 | 29 | }, | ||
84 | 30 | "series": "precise" }}""" | ||
85 | 31 | |||
86 | 32 | def test_check_parseable_yaml(self): | ||
87 | 33 | """A deployer file should be parseable as yaml.""" | ||
88 | 34 | |||
89 | 35 | deployer_file = self.sample_deployer_file | ||
90 | 36 | result = BundleProof.check_parseable_deployer_file(deployer_file) | ||
91 | 37 | self.assertEqual(result['wiki']['services'].keys(), ['wiki']) | ||
92 | 38 | |||
93 | 39 | def test_failing_parsing_yaml_throws_proof_error(self): | ||
94 | 40 | """If the yaml is not parse the yaml thows a ProofError""" | ||
95 | 41 | deployer_file = '{' | ||
96 | 42 | with self.assertRaises(ProofError) as exc: | ||
97 | 43 | BundleProof.check_parseable_deployer_file(deployer_file) | ||
98 | 44 | |||
99 | 45 | self.assertEqual( | ||
100 | 46 | 'Could not parse the yaml provided.', | ||
101 | 47 | exc.exception.msg | ||
102 | 48 | ) | ||
103 | 49 | self.assertEqual( | ||
104 | 50 | deployer_file, | ||
105 | 51 | exc.exception.debug_info | ||
106 | 52 | ) | ||
107 | 53 | |||
108 | 54 | @patch('charmworld.lib.proof.resolve_charm_from_description') | ||
109 | 55 | def test_service_not_found_raises_proof_error(self, resolver): | ||
110 | 56 | """If we cannot find a service specified it's a ProofError""" | ||
111 | 57 | resolver.return_value = None | ||
112 | 58 | deployer = yaml.load(self.sample_deployer_file) | ||
113 | 59 | |||
114 | 60 | with self.assertRaises(ProofError) as exc: | ||
115 | 61 | BundleProof.check_service_exists( | ||
116 | 62 | None, | ||
117 | 63 | 'wiki', | ||
118 | 64 | deployer['wiki']['services']['wiki'], | ||
119 | 65 | deployer['wiki']) | ||
120 | 66 | |||
121 | 67 | self.assertEqual( | ||
122 | 68 | "Could not find charm: wiki", | ||
123 | 69 | exc.exception.msg | ||
124 | 70 | ) | ||
125 | 71 | # We should have an array of logic behind the decision to look up the | ||
126 | 72 | # charm. Just check we've got some logic. This number might get | ||
127 | 73 | # tweaked, but we're just checking it made it to the exception, not | ||
128 | 74 | # what the reasons are. | ||
129 | 75 | self.assertEqual( | ||
130 | 76 | 4, | ||
131 | 77 | len(exc.exception.debug_info) | ||
132 | 78 | ) | ||
133 | 79 | |||
134 | 80 | |||
135 | 81 | class TestCharmProof(TestCase): | 11 | class TestCharmProof(TestCase): |
136 | 82 | 12 | ||
137 | 83 | def make_fake_charm_config(self, options): | 13 | def make_fake_charm_config(self, options): |
138 | @@ -130,7 +60,7 @@ | |||
139 | 130 | ) | 60 | ) |
140 | 131 | 61 | ||
141 | 132 | def test_check_config_fails_when_option_does_not_exist(self): | 62 | def test_check_config_fails_when_option_does_not_exist(self): |
143 | 133 | """Bundles might attempt to set config values that are not valid""" | 63 | """Bundles might attempt to set config values that do not exist""" |
144 | 134 | charm = Mock() | 64 | charm = Mock() |
145 | 135 | charm.options = {'foo': {}} | 65 | charm.options = {'foo': {}} |
146 | 136 | charm._id = 'precise/test' | 66 | charm._id = 'precise/test' |
147 | 137 | 67 | ||
148 | === modified file 'charmworld/models.py' | |||
149 | --- charmworld/models.py 2013-10-15 16:18:49 +0000 | |||
150 | +++ charmworld/models.py 2013-10-22 20:55:07 +0000 | |||
151 | @@ -19,6 +19,7 @@ | |||
152 | 19 | import random | 19 | import random |
153 | 20 | import re | 20 | import re |
154 | 21 | from xml.etree import ElementTree | 21 | from xml.etree import ElementTree |
155 | 22 | import yaml | ||
156 | 22 | 23 | ||
157 | 23 | from bzrlib.branch import Branch | 24 | from bzrlib.branch import Branch |
158 | 24 | from bzrlib.errors import NoSuchRevision | 25 | from bzrlib.errors import NoSuchRevision |
159 | @@ -27,6 +28,7 @@ | |||
160 | 27 | import magic | 28 | import magic |
161 | 28 | import pymongo | 29 | import pymongo |
162 | 29 | 30 | ||
163 | 31 | from charmworld.lib.proof import ProofError | ||
164 | 30 | from charmworld.lp import BASKET_SERIES | 32 | from charmworld.lp import BASKET_SERIES |
165 | 31 | from charmworld.utils import ( | 33 | from charmworld.utils import ( |
166 | 32 | configure_logging, | 34 | configure_logging, |
167 | @@ -1444,6 +1446,21 @@ | |||
168 | 1444 | return hashes.keys() | 1446 | return hashes.keys() |
169 | 1445 | 1447 | ||
170 | 1446 | 1448 | ||
171 | 1449 | class DeployerParser(object): | ||
172 | 1450 | |||
173 | 1451 | def __init__(self, data, format="yaml"): | ||
174 | 1452 | try: | ||
175 | 1453 | self.parsed = yaml.safe_load(data) | ||
176 | 1454 | except yaml.YAMLError, exc: | ||
177 | 1455 | raise ProofError( | ||
178 | 1456 | [ | ||
179 | 1457 | data, | ||
180 | 1458 | str(exc) | ||
181 | 1459 | ], | ||
182 | 1460 | 'Could not parse the yaml provided.' | ||
183 | 1461 | ) | ||
184 | 1462 | |||
185 | 1463 | |||
186 | 1447 | def acquire_session_secret(database): | 1464 | def acquire_session_secret(database): |
187 | 1448 | """Return a secret to use for AuthTkt sessions. | 1465 | """Return a secret to use for AuthTkt sessions. |
188 | 1449 | 1466 | ||
189 | @@ -1528,7 +1545,6 @@ | |||
190 | 1528 | In the deployer, the charm key is given precedence. So we try to | 1545 | In the deployer, the charm key is given precedence. So we try to |
191 | 1529 | mimic that by only using the charm_url if charm is not defined. | 1546 | mimic that by only using the charm_url if charm is not defined. |
192 | 1530 | """ | 1547 | """ |
193 | 1531 | # | ||
194 | 1532 | if not charm and charm_url: | 1548 | if not charm and charm_url: |
195 | 1533 | charm = charm_url | 1549 | charm = charm_url |
196 | 1534 | 1550 | ||
197 | 1535 | 1551 | ||
198 | === modified file 'charmworld/tests/test_models.py' | |||
199 | --- charmworld/tests/test_models.py 2013-10-15 16:18:49 +0000 | |||
200 | +++ charmworld/tests/test_models.py 2013-10-22 20:55:07 +0000 | |||
201 | @@ -25,6 +25,7 @@ | |||
202 | 25 | import pymongo | 25 | import pymongo |
203 | 26 | import yaml | 26 | import yaml |
204 | 27 | 27 | ||
205 | 28 | from charmworld.lib.proof import ProofError | ||
206 | 28 | from charmworld.models import ( | 29 | from charmworld.models import ( |
207 | 29 | acquire_session_secret, | 30 | acquire_session_secret, |
208 | 30 | Bundle, | 31 | Bundle, |
209 | @@ -34,6 +35,7 @@ | |||
210 | 34 | CharmFileSet, | 35 | CharmFileSet, |
211 | 35 | CharmSource, | 36 | CharmSource, |
212 | 36 | construct_charm_id, | 37 | construct_charm_id, |
213 | 38 | DeployerParser, | ||
214 | 37 | FeaturedSource, | 39 | FeaturedSource, |
215 | 38 | find_charms, | 40 | find_charms, |
216 | 39 | _find_charms, | 41 | _find_charms, |
217 | @@ -1999,6 +2001,43 @@ | |||
218 | 1999 | self.assertIsNone(description.revision) | 2001 | self.assertIsNone(description.revision) |
219 | 2000 | 2002 | ||
220 | 2001 | 2003 | ||
221 | 2004 | class TestDeployerParser(TestCase): | ||
222 | 2005 | |||
223 | 2006 | sample_deployer_file = """ | ||
224 | 2007 | { | ||
225 | 2008 | "wiki": { | ||
226 | 2009 | "services": { | ||
227 | 2010 | "wiki": { | ||
228 | 2011 | "charm": "mediawiki", | ||
229 | 2012 | "num_units": 2, | ||
230 | 2013 | "branch": "lp:charms/precise/mediawiki", | ||
231 | 2014 | "constraints": "mem=2" | ||
232 | 2015 | } | ||
233 | 2016 | }, | ||
234 | 2017 | "series": "precise" }}""" | ||
235 | 2018 | |||
236 | 2019 | def test_parses_valid_yaml_file(self): | ||
237 | 2020 | """A deployer file should be parseable as yaml.""" | ||
238 | 2021 | deployer_file = self.sample_deployer_file | ||
239 | 2022 | deployer = DeployerParser(deployer_file) | ||
240 | 2023 | self.assertEqual(deployer.parsed['wiki']['services'].keys(), ['wiki']) | ||
241 | 2024 | |||
242 | 2025 | def test_failing_parsing_yaml_throws_proof_error(self): | ||
243 | 2026 | """If the yaml is not parse the yaml thows a ProofError""" | ||
244 | 2027 | deployer_file = '{' | ||
245 | 2028 | with self.assertRaises(ProofError) as exc: | ||
246 | 2029 | DeployerParser(deployer_file) | ||
247 | 2030 | |||
248 | 2031 | self.assertEqual( | ||
249 | 2032 | 'Could not parse the yaml provided.', | ||
250 | 2033 | exc.exception.msg | ||
251 | 2034 | ) | ||
252 | 2035 | self.assertEqual( | ||
253 | 2036 | deployer_file, | ||
254 | 2037 | exc.exception.debug_info[0] | ||
255 | 2038 | ) | ||
256 | 2039 | |||
257 | 2040 | |||
258 | 2002 | class TestMakeBundleDoc(TestCase): | 2041 | class TestMakeBundleDoc(TestCase): |
259 | 2003 | 2042 | ||
260 | 2004 | def test_bundle_doc(self): | 2043 | def test_bundle_doc(self): |
261 | 2005 | 2044 | ||
262 | === added directory 'charmworld/views/api' | |||
263 | === renamed file 'charmworld/views/api.py' => 'charmworld/views/api/__init__.py' | |||
264 | --- charmworld/views/api.py 2013-10-22 00:33:12 +0000 | |||
265 | +++ charmworld/views/api/__init__.py 2013-10-22 20:55:07 +0000 | |||
266 | @@ -6,7 +6,6 @@ | |||
267 | 6 | ) | 6 | ) |
268 | 7 | from email.utils import parseaddr | 7 | from email.utils import parseaddr |
269 | 8 | from inspect import getargspec | 8 | from inspect import getargspec |
270 | 9 | import json | ||
271 | 10 | from os.path import ( | 9 | from os.path import ( |
272 | 11 | join, | 10 | join, |
273 | 12 | ) | 11 | ) |
274 | @@ -22,11 +21,7 @@ | |||
275 | 22 | from pyramid.view import view_config | 21 | from pyramid.view import view_config |
276 | 23 | from webob import Response | 22 | from webob import Response |
277 | 24 | 23 | ||
283 | 25 | from charmworld.lib.proof import ( | 24 | |
279 | 26 | BundleProof, | ||
280 | 27 | CharmProof, | ||
281 | 28 | ProofError, | ||
282 | 29 | ) | ||
284 | 30 | from charmworld.models import ( | 25 | from charmworld.models import ( |
285 | 31 | Bundle, | 26 | Bundle, |
286 | 32 | BundledCharmDescription, | 27 | BundledCharmDescription, |
287 | @@ -47,37 +42,8 @@ | |||
288 | 47 | quote_key, | 42 | quote_key, |
289 | 48 | timestamp, | 43 | timestamp, |
290 | 49 | ) | 44 | ) |
322 | 50 | 45 | from charmworld.views.api.proof import proof_deployer | |
323 | 51 | 46 | from charmworld.views.utils import json_response | |
293 | 52 | def json_response(status, value, headers=[]): | ||
294 | 53 | """Return a JSON API response. | ||
295 | 54 | |||
296 | 55 | :param status: The HTTP status code to use. | ||
297 | 56 | :param value: A json-serializable value to use as the body, or None for | ||
298 | 57 | no body. | ||
299 | 58 | """ | ||
300 | 59 | # If the value is an object, then it must be representable as a mapping. | ||
301 | 60 | if value is None: | ||
302 | 61 | body = '' | ||
303 | 62 | else: | ||
304 | 63 | while True: | ||
305 | 64 | try: | ||
306 | 65 | body = json.dumps(value, sort_keys=True, indent=2) | ||
307 | 66 | except TypeError: | ||
308 | 67 | # If the serialization wasn't possible, and the value wasn't | ||
309 | 68 | # already a dictionary, convert it to one and try again. | ||
310 | 69 | if not isinstance(value, dict): | ||
311 | 70 | value = dict(value) | ||
312 | 71 | continue | ||
313 | 72 | break | ||
314 | 73 | |||
315 | 74 | return Response(body, | ||
316 | 75 | headerlist=[ | ||
317 | 76 | ('Content-Type', 'application/json'), | ||
318 | 77 | ('Access-Control-Allow-Origin', '*'), | ||
319 | 78 | ('Access-Control-Allow-Headers', 'X-Requested-With'), | ||
320 | 79 | ] + headers, | ||
321 | 80 | status_code=status) | ||
324 | 81 | 47 | ||
325 | 82 | 48 | ||
326 | 83 | @view_config(route_name="search-json-obsolete") | 49 | @view_config(route_name="search-json-obsolete") |
327 | @@ -494,100 +460,6 @@ | |||
328 | 494 | bundle = Bundle.from_query(query, self.request.db) | 460 | bundle = Bundle.from_query(query, self.request.db) |
329 | 495 | return bundle_id, trailing, bundle | 461 | return bundle_id, trailing, bundle |
330 | 496 | 462 | ||
331 | 497 | def _proof_bundle(self, request): | ||
332 | 498 | """Check the bundle provided for proof errors.""" | ||
333 | 499 | # The top level is for general deployer file errors. | ||
334 | 500 | response_data = { | ||
335 | 501 | 'errors': [], | ||
336 | 502 | 'error_messages': [], | ||
337 | 503 | 'debug_info': [], | ||
338 | 504 | } | ||
339 | 505 | bundle_string = request.params.get('deployer_file') | ||
340 | 506 | bundle_format = request.params.get('bundle_format', 'yaml') | ||
341 | 507 | bundle_data = None | ||
342 | 508 | |||
343 | 509 | if not bundle_string: | ||
344 | 510 | response_data['error_messages'].append( | ||
345 | 511 | 'No deployer file data received.') | ||
346 | 512 | return json_response(400, response_data) | ||
347 | 513 | |||
348 | 514 | try: | ||
349 | 515 | bundle_data = BundleProof.check_parseable_deployer_file( | ||
350 | 516 | bundle_string, | ||
351 | 517 | format=bundle_format) | ||
352 | 518 | except ProofError, exc: | ||
353 | 519 | # If we cannot parse the config file there's nothing more to do. | ||
354 | 520 | # Return immediately. | ||
355 | 521 | response_data['error_messages'].append(exc.msg) | ||
356 | 522 | response_data['debug_info'].append(exc.debug_info) | ||
357 | 523 | return json_response(400, response_data) | ||
358 | 524 | |||
359 | 525 | # Proof each bundle in the deployer file. | ||
360 | 526 | for bundle_name, bundle_config in bundle_data.items(): | ||
361 | 527 | bundle_info = { | ||
362 | 528 | 'name': bundle_name, | ||
363 | 529 | 'services': {}, | ||
364 | 530 | 'relations': {}, | ||
365 | 531 | } | ||
366 | 532 | found_charms = {} | ||
367 | 533 | |||
368 | 534 | # Verify we can find the charms at all. | ||
369 | 535 | for name, charm in bundle_config.get('services').items(): | ||
370 | 536 | try: | ||
371 | 537 | charm_found = BundleProof.check_service_exists( | ||
372 | 538 | self.request.db, name, charm, bundle_config) | ||
373 | 539 | found_charms[name] = Charm(charm_found) | ||
374 | 540 | except ProofError, exc: | ||
375 | 541 | # We could not find the charm. No other verification can | ||
376 | 542 | # happen. | ||
377 | 543 | bundle_info['services'][name] = [] | ||
378 | 544 | bundle_info['services'][name].append({ | ||
379 | 545 | 'message': exc.msg, | ||
380 | 546 | 'debug_info': exc.debug_info, | ||
381 | 547 | }) | ||
382 | 548 | |||
383 | 549 | # For the charms we did find in the system, verify that their | ||
384 | 550 | # config is valid for the values allowed by the charm we found. | ||
385 | 551 | for name, charm in found_charms.iteritems(): | ||
386 | 552 | check_config = bundle_config['services'][name].get('options') | ||
387 | 553 | if not check_config: | ||
388 | 554 | # There's no config specified to validate. | ||
389 | 555 | continue | ||
390 | 556 | for test_key, test_value in check_config.iteritems(): | ||
391 | 557 | try: | ||
392 | 558 | CharmProof.check_config(charm, test_key, test_value) | ||
393 | 559 | except ProofError, exc: | ||
394 | 560 | if name not in bundle_info['services']: | ||
395 | 561 | bundle_info['services'][name] = [] | ||
396 | 562 | |||
397 | 563 | bundle_info['services'][name].append({ | ||
398 | 564 | 'message': exc.msg, | ||
399 | 565 | 'debug_info': exc.debug_info, | ||
400 | 566 | }) | ||
401 | 567 | |||
402 | 568 | # If there are errors in this bundles services, make sure we | ||
403 | 569 | # append the info to the response data that gets sent back to the | ||
404 | 570 | # user. | ||
405 | 571 | if bundle_info['services'].keys(): | ||
406 | 572 | response_data['errors'].append(bundle_info) | ||
407 | 573 | |||
408 | 574 | # Build up a root messages list of errors that can be used to help the | ||
409 | 575 | # client go through them all easily. | ||
410 | 576 | error_messages = [] | ||
411 | 577 | if response_data['errors']: | ||
412 | 578 | for bundle in response_data['errors']: | ||
413 | 579 | prefix = bundle['name'] | ||
414 | 580 | for service_name, service in bundle['services'].items(): | ||
415 | 581 | for err in service: | ||
416 | 582 | msg = '%s: %s' % (prefix, err['message']) | ||
417 | 583 | error_messages.append(msg) | ||
418 | 584 | if error_messages: | ||
419 | 585 | response_data['error_messages'].extend(error_messages) | ||
420 | 586 | |||
421 | 587 | # After proofing each deployer file return with any failure info | ||
422 | 588 | # found. | ||
423 | 589 | return json_response(200, response_data) | ||
424 | 590 | |||
425 | 591 | def charm(self, path=None): | 463 | def charm(self, path=None): |
426 | 592 | """Retrieve a charm according to its API ID (the path prefix).""" | 464 | """Retrieve a charm according to its API ID (the path prefix).""" |
427 | 593 | if path is None: | 465 | if path is None: |
428 | @@ -634,7 +506,7 @@ | |||
429 | 634 | path = list(path) | 506 | path = list(path) |
430 | 635 | 507 | ||
431 | 636 | if path[0] == 'proof': | 508 | if path[0] == 'proof': |
433 | 637 | return self._proof_bundle(self.request) | 509 | return proof_deployer(self.request) |
434 | 638 | 510 | ||
435 | 639 | bundle_id, trailing, bundle = self._find_bundle(path) | 511 | bundle_id, trailing, bundle = self._find_bundle(path) |
436 | 640 | 512 | ||
437 | 641 | 513 | ||
438 | === added file 'charmworld/views/api/proof.py' | |||
439 | --- charmworld/views/api/proof.py 1970-01-01 00:00:00 +0000 | |||
440 | +++ charmworld/views/api/proof.py 2013-10-22 20:55:07 +0000 | |||
441 | @@ -0,0 +1,145 @@ | |||
442 | 1 | from charmworld.lib.proof import ( | ||
443 | 2 | CharmProof, | ||
444 | 3 | ProofError, | ||
445 | 4 | ) | ||
446 | 5 | from charmworld.models import ( | ||
447 | 6 | BundledCharmDescription, | ||
448 | 7 | DeployerParser, | ||
449 | 8 | Charm, | ||
450 | 9 | resolve_charm_from_description, | ||
451 | 10 | ) | ||
452 | 11 | from charmworld.views.utils import json_response | ||
453 | 12 | |||
454 | 13 | |||
455 | 14 | def _build_proof_response(response_data): | ||
456 | 15 | # Build up a root messages list of errors that can be used to help the | ||
457 | 16 | # client go through them all easily. | ||
458 | 17 | error_messages = [] | ||
459 | 18 | if response_data['errors']: | ||
460 | 19 | for bundle in response_data['errors']: | ||
461 | 20 | prefix = bundle['name'] | ||
462 | 21 | for service_name, service in bundle['services'].items(): | ||
463 | 22 | for err in service: | ||
464 | 23 | msg = '%s: %s' % (prefix, err['message']) | ||
465 | 24 | error_messages.append(msg) | ||
466 | 25 | if error_messages: | ||
467 | 26 | response_data['error_messages'].extend(error_messages) | ||
468 | 27 | return response_data | ||
469 | 28 | |||
470 | 29 | |||
471 | 30 | def _proof_bundles_charms_exist(db, bundle_info, services, series=None): | ||
472 | 31 | """Verify the charms the bundle requests to deploy exist in the db.""" | ||
473 | 32 | found_charms = {} | ||
474 | 33 | |||
475 | 34 | # Verify we can find the charms at all. | ||
476 | 35 | for name, charm in services.items(): | ||
477 | 36 | try: | ||
478 | 37 | charm_description = BundledCharmDescription( | ||
479 | 38 | 'proof', charm, series) | ||
480 | 39 | charm_found = resolve_charm_from_description( | ||
481 | 40 | db, charm_description) | ||
482 | 41 | if not charm_found: | ||
483 | 42 | raise ProofError( | ||
484 | 43 | charm_description.get_process(), | ||
485 | 44 | 'Could not find charm: ' + name, | ||
486 | 45 | ) | ||
487 | 46 | else: | ||
488 | 47 | found_charms[name] = Charm(charm_found) | ||
489 | 48 | except ProofError, exc: | ||
490 | 49 | # We could not find the charm. No other verification can | ||
491 | 50 | # happen. | ||
492 | 51 | bundle_info['services'][name] = [] | ||
493 | 52 | bundle_info['services'][name].append({ | ||
494 | 53 | 'message': exc.msg, | ||
495 | 54 | 'debug_info': exc.debug_info, | ||
496 | 55 | }) | ||
497 | 56 | return found_charms, bundle_info | ||
498 | 57 | |||
499 | 58 | |||
500 | 59 | def _proof_charm_config(bundle_info, bundle_config, charm_name, charm): | ||
501 | 60 | """Given a charm, proof the config is valid for the charm found. | ||
502 | 61 | |||
503 | 62 | If the deployer attempts to set illegal or non-existant config values to | ||
504 | 63 | the charm treat these as proof errors. | ||
505 | 64 | """ | ||
506 | 65 | check_config = bundle_config['services'][charm_name].get('options') | ||
507 | 66 | if not check_config: | ||
508 | 67 | # There's no config specified to validate. | ||
509 | 68 | return bundle_info | ||
510 | 69 | for test_key, test_value in check_config.iteritems(): | ||
511 | 70 | try: | ||
512 | 71 | CharmProof.check_config(charm, test_key, test_value) | ||
513 | 72 | except ProofError, exc: | ||
514 | 73 | if charm_name not in bundle_info['services']: | ||
515 | 74 | bundle_info['services'][charm_name] = [] | ||
516 | 75 | |||
517 | 76 | bundle_info['services'][charm_name].append({ | ||
518 | 77 | 'message': exc.msg, | ||
519 | 78 | 'debug_info': exc.debug_info, | ||
520 | 79 | }) | ||
521 | 80 | return bundle_info | ||
522 | 81 | |||
523 | 82 | |||
524 | 83 | def proof_deployer(request): | ||
525 | 84 | """Check the bundle provided for proof errors.""" | ||
526 | 85 | # The top level is for general deployer file errors. | ||
527 | 86 | # The errors here may be an error about the deployer file itself, or an | ||
528 | 87 | # object that describes the issues found with a bundle within the deployer | ||
529 | 88 | # file. errro_messages is added as a simple aggregate of all of the error | ||
530 | 89 | # messages found to make life easier for the proof cli tool. | ||
531 | 90 | response_data = { | ||
532 | 91 | 'errors': [], | ||
533 | 92 | 'error_messages': [], | ||
534 | 93 | 'debug_info': [], | ||
535 | 94 | } | ||
536 | 95 | deployer_string = request.params.get('deployer_file') | ||
537 | 96 | deployer_format = request.params.get('deployer_format', 'yaml') | ||
538 | 97 | |||
539 | 98 | # Verify we have a valid deployer file that we can find and parse. | ||
540 | 99 | if not deployer_string: | ||
541 | 100 | response_data['error_messages'].append( | ||
542 | 101 | 'No deployer file data received.') | ||
543 | 102 | return json_response(400, response_data) | ||
544 | 103 | |||
545 | 104 | try: | ||
546 | 105 | deployer = DeployerParser(deployer_string, format=deployer_format) | ||
547 | 106 | deployer_data = deployer.parsed | ||
548 | 107 | except ProofError, exc: | ||
549 | 108 | # If we cannot parse the config file there's nothing more to do. | ||
550 | 109 | # Return immediately. | ||
551 | 110 | response_data['error_messages'].append(exc.msg) | ||
552 | 111 | response_data['debug_info'].append(exc.debug_info) | ||
553 | 112 | return json_response(400, response_data) | ||
554 | 113 | |||
555 | 114 | # Proof each bundle found in the deployer file. | ||
556 | 115 | for bundle_name, bundle_config in deployer_data.items(): | ||
557 | 116 | # Track all of the issues found in this info dict. | ||
558 | 117 | bundle_info = { | ||
559 | 118 | 'name': bundle_name, | ||
560 | 119 | 'services': {}, | ||
561 | 120 | 'relations': {}, | ||
562 | 121 | } | ||
563 | 122 | |||
564 | 123 | # First, verify that we can locate the charms specified in this bundle | ||
565 | 124 | # in the charmworld database. | ||
566 | 125 | found_charms, bundle_info = _proof_bundles_charms_exist( | ||
567 | 126 | request.db, | ||
568 | 127 | bundle_info, | ||
569 | 128 | bundle_config.get('services'), | ||
570 | 129 | bundle_config.get('series')) | ||
571 | 130 | |||
572 | 131 | # For the charms we did find in the system, verify that their | ||
573 | 132 | # config is valid for the values allowed by the charm we found. | ||
574 | 133 | for name, charm in found_charms.iteritems(): | ||
575 | 134 | bundle_info = _proof_charm_config( | ||
576 | 135 | bundle_info, bundle_config, name, charm) | ||
577 | 136 | |||
578 | 137 | # If there are errors in this bundles services, make sure we | ||
579 | 138 | # append the info to the response data that gets sent back to the | ||
580 | 139 | # user. | ||
581 | 140 | if bundle_info['services'].keys(): | ||
582 | 141 | response_data['errors'].append(bundle_info) | ||
583 | 142 | |||
584 | 143 | # After proofing each deployer file return with any failure info | ||
585 | 144 | # found. | ||
586 | 145 | return json_response(200, _build_proof_response(response_data)) | ||
587 | 0 | 146 | ||
588 | === modified file 'charmworld/views/tests/test_api.py' | |||
589 | --- charmworld/views/tests/test_api.py 2013-10-22 00:55:23 +0000 | |||
590 | +++ charmworld/views/tests/test_api.py 2013-10-22 20:55:07 +0000 | |||
591 | @@ -9,7 +9,6 @@ | |||
592 | 9 | ) | 9 | ) |
593 | 10 | import hashlib | 10 | import hashlib |
594 | 11 | import json | 11 | import json |
595 | 12 | import yaml | ||
596 | 13 | 12 | ||
597 | 14 | from mock import patch | 13 | from mock import patch |
598 | 15 | 14 | ||
599 | @@ -41,11 +40,11 @@ | |||
600 | 41 | ) | 40 | ) |
601 | 42 | from charmworld.testing import factory | 41 | from charmworld.testing import factory |
602 | 43 | from charmworld.testing import ( | 42 | from charmworld.testing import ( |
603 | 44 | load_data_file, | ||
604 | 45 | ViewTestBase, | 43 | ViewTestBase, |
605 | 46 | WebTestBase, | 44 | WebTestBase, |
606 | 47 | ) | 45 | ) |
607 | 48 | from charmworld.utils import quote_key | 46 | from charmworld.utils import quote_key |
608 | 47 | from charmworld.views.tests.test_proof import TestBundleProof | ||
609 | 49 | 48 | ||
610 | 50 | 49 | ||
611 | 51 | class TestObsoletedApis(WebTestBase): | 50 | class TestObsoletedApis(WebTestBase): |
612 | @@ -868,176 +867,12 @@ | |||
613 | 868 | self.assertEqual(302, response.status_code) | 867 | self.assertEqual(302, response.status_code) |
614 | 869 | self.assertEqual('/static/img/bundle.svg', response.location) | 868 | self.assertEqual('/static/img/bundle.svg', response.location) |
615 | 870 | 869 | ||
782 | 871 | def test_bundle_proof_no_deployer_file(self): | 870 | |
783 | 872 | request = self.make_request('bundle', remainder='/proof') | 871 | class TestAPI3Bundles(TestAPIBundles, API3Mixin, TestBundleProof): |
618 | 873 | response = self.api_class(request)() | ||
619 | 874 | self.assertEqual(400, response.status_code) | ||
620 | 875 | self.assertEqual( | ||
621 | 876 | 'No deployer file data received.', | ||
622 | 877 | response.json_body['error_messages'][0]) | ||
623 | 878 | |||
624 | 879 | def test_bundle_proof_invalid_deployer_file(self): | ||
625 | 880 | request = self.make_request('bundle', remainder='/proof') | ||
626 | 881 | request.params = { | ||
627 | 882 | 'deployer_file': '{' | ||
628 | 883 | } | ||
629 | 884 | response = self.api_class(request)() | ||
630 | 885 | self.assertEqual(400, response.status_code) | ||
631 | 886 | self.assertIn( | ||
632 | 887 | 'Could not parse', | ||
633 | 888 | response.json_body['error_messages'][0]) | ||
634 | 889 | |||
635 | 890 | def test_bundle_proof_charm_exists(self): | ||
636 | 891 | deployer_file = load_data_file('sample_bundle/bundles.yaml') | ||
637 | 892 | |||
638 | 893 | request = self.make_request('bundle', remainder='/proof') | ||
639 | 894 | request.params = { | ||
640 | 895 | 'deployer_file': deployer_file | ||
641 | 896 | } | ||
642 | 897 | |||
643 | 898 | response = self.api_class(request)() | ||
644 | 899 | self.assertEqual(200, response.status_code) | ||
645 | 900 | results = response.json_body | ||
646 | 901 | self.assertEqual(1, len(results['errors'])) | ||
647 | 902 | self.assertEqual(4, len(results['errors'][0]['services'].keys())) | ||
648 | 903 | self.assertIn( | ||
649 | 904 | 'Could not find ', | ||
650 | 905 | results['errors'][0]['services']['db'][0]['message']) | ||
651 | 906 | |||
652 | 907 | # And each failure has a list of 'process' information. | ||
653 | 908 | self.assertIn( | ||
654 | 909 | 'debug_info', | ||
655 | 910 | results['errors'][0]['services']['db'][0].keys()) | ||
656 | 911 | |||
657 | 912 | def test_bundle_proof_valid(self): | ||
658 | 913 | # We have to create the charm so that we can verify it exists. | ||
659 | 914 | _id, charm = factory.makeCharm( | ||
660 | 915 | self.db, | ||
661 | 916 | description='' | ||
662 | 917 | ) | ||
663 | 918 | |||
664 | 919 | # and we'll cheat and just find it straight by the store url. | ||
665 | 920 | charm = Charm(charm) | ||
666 | 921 | store_url = charm.store_url | ||
667 | 922 | |||
668 | 923 | bundle_config = { | ||
669 | 924 | 'wiki': { | ||
670 | 925 | 'services': { | ||
671 | 926 | 'charm': { | ||
672 | 927 | 'charm_url': str(store_url) | ||
673 | 928 | } | ||
674 | 929 | } | ||
675 | 930 | } | ||
676 | 931 | } | ||
677 | 932 | |||
678 | 933 | request = self.make_request('bundle', remainder='/proof') | ||
679 | 934 | request.params = { | ||
680 | 935 | 'deployer_file': yaml.dump(bundle_config) | ||
681 | 936 | } | ||
682 | 937 | response = self.api_class(request)() | ||
683 | 938 | self.assertEqual(200, response.status_code) | ||
684 | 939 | results = response.json_body | ||
685 | 940 | self.assertEqual(0, len(results['errors'])) | ||
686 | 941 | |||
687 | 942 | def test_bundle_proof_invalid_config_type(self): | ||
688 | 943 | # We have to create the charm so that we can verify it exists. | ||
689 | 944 | _id, charm = factory.makeCharm( | ||
690 | 945 | self.db, | ||
691 | 946 | description='' | ||
692 | 947 | ) | ||
693 | 948 | |||
694 | 949 | # and we'll cheat and just find it straight by the store url. | ||
695 | 950 | charm = Charm(charm) | ||
696 | 951 | store_url = charm.store_url | ||
697 | 952 | |||
698 | 953 | bundle_config = { | ||
699 | 954 | 'wiki': { | ||
700 | 955 | 'services': { | ||
701 | 956 | 'charm': { | ||
702 | 957 | 'charm_url': str(store_url), | ||
703 | 958 | 'options': { | ||
704 | 959 | 'script-interval': 'invalid' | ||
705 | 960 | } | ||
706 | 961 | } | ||
707 | 962 | } | ||
708 | 963 | } | ||
709 | 964 | } | ||
710 | 965 | |||
711 | 966 | request = self.make_request('bundle', remainder='/proof') | ||
712 | 967 | request.params = { | ||
713 | 968 | 'deployer_file': yaml.dump(bundle_config) | ||
714 | 969 | } | ||
715 | 970 | response = self.api_class(request)() | ||
716 | 971 | self.assertEqual(200, response.status_code) | ||
717 | 972 | results = response.json_body | ||
718 | 973 | self.assertEqual(1, len(results['errors'])) | ||
719 | 974 | self.assertEqual(1, len(results['error_messages'])) | ||
720 | 975 | |||
721 | 976 | # Note that the View will prefix the error message from the proof with | ||
722 | 977 | # the bundle name to help identify where the error message came from. | ||
723 | 978 | self.assertEqual( | ||
724 | 979 | 'wiki: script-interval is not of type int.', | ||
725 | 980 | results['error_messages'][0] | ||
726 | 981 | ) | ||
727 | 982 | |||
728 | 983 | def test_bundle_proof_supports_multiple_errors(self): | ||
729 | 984 | _id, charm = factory.makeCharm( | ||
730 | 985 | self.db, | ||
731 | 986 | description='' | ||
732 | 987 | ) | ||
733 | 988 | |||
734 | 989 | # and we'll cheat and just find it straight by the store url. | ||
735 | 990 | charm = Charm(charm) | ||
736 | 991 | store_url = charm.store_url | ||
737 | 992 | |||
738 | 993 | bundle_config = { | ||
739 | 994 | 'wiki': { | ||
740 | 995 | 'services': { | ||
741 | 996 | 'charm': { | ||
742 | 997 | 'charm_url': str(store_url), | ||
743 | 998 | 'options': { | ||
744 | 999 | 'script-interval': 'invalid', | ||
745 | 1000 | 'no-exist': 'hah invalid', | ||
746 | 1001 | } | ||
747 | 1002 | }, | ||
748 | 1003 | 'fail': { | ||
749 | 1004 | 'name': 'will-fail' | ||
750 | 1005 | } | ||
751 | 1006 | } | ||
752 | 1007 | } | ||
753 | 1008 | } | ||
754 | 1009 | |||
755 | 1010 | request = self.make_request('bundle', remainder='/proof') | ||
756 | 1011 | request.params = { | ||
757 | 1012 | 'deployer_file': yaml.dump(bundle_config) | ||
758 | 1013 | } | ||
759 | 1014 | response = self.api_class(request)() | ||
760 | 1015 | self.assertEqual(200, response.status_code) | ||
761 | 1016 | results = response.json_body | ||
762 | 1017 | self.assertEqual(1, len(results['errors'])) | ||
763 | 1018 | self.assertEqual(3, len(results['error_messages'])) | ||
764 | 1019 | |||
765 | 1020 | # One message from not finding a charm. | ||
766 | 1021 | self.assertEqual( | ||
767 | 1022 | 'wiki: Could not find charm: fail', | ||
768 | 1023 | results['error_messages'][0] | ||
769 | 1024 | ) | ||
770 | 1025 | # The other two from checking the config of the other charm. | ||
771 | 1026 | self.assertEqual( | ||
772 | 1027 | 'wiki: script-interval is not of type int.', | ||
773 | 1028 | results['error_messages'][1] | ||
774 | 1029 | ) | ||
775 | 1030 | self.assertEqual( | ||
776 | 1031 | 'wiki: The charm has no option for: no-exist', | ||
777 | 1032 | results['error_messages'][2] | ||
778 | 1033 | ) | ||
779 | 1034 | |||
780 | 1035 | |||
781 | 1036 | class TestAPI3Bundles(TestAPIBundles, API3Mixin): | ||
784 | 1037 | """Test API 3 bundle endpoint.""" | 872 | """Test API 3 bundle endpoint.""" |
785 | 1038 | 873 | ||
786 | 1039 | 874 | ||
788 | 1040 | class TestAPI2Bundles(TestAPIBundles, API2Mixin): | 875 | class TestAPI2Bundles(TestAPIBundles, API2Mixin, TestBundleProof): |
789 | 1041 | """Test API 2 bundle endpoint.""" | 876 | """Test API 2 bundle endpoint.""" |
790 | 1042 | 877 | ||
791 | 1043 | 878 | ||
792 | 1044 | 879 | ||
793 | === added file 'charmworld/views/tests/test_proof.py' | |||
794 | --- charmworld/views/tests/test_proof.py 1970-01-01 00:00:00 +0000 | |||
795 | +++ charmworld/views/tests/test_proof.py 2013-10-22 20:55:07 +0000 | |||
796 | @@ -0,0 +1,175 @@ | |||
797 | 1 | import yaml | ||
798 | 2 | |||
799 | 3 | from charmworld.models import Charm | ||
800 | 4 | from charmworld.testing import ( | ||
801 | 5 | factory, | ||
802 | 6 | load_data_file, | ||
803 | 7 | ) | ||
804 | 8 | |||
805 | 9 | |||
806 | 10 | class TestBundleProof: | ||
807 | 11 | """Verify that a bundle can be proofed in charmworld.""" | ||
808 | 12 | |||
809 | 13 | def test_bundle_proof_no_deployer_file(self): | ||
810 | 14 | request = self.make_request('bundle', remainder='/proof') | ||
811 | 15 | response = self.api_class(request)() | ||
812 | 16 | self.assertEqual(400, response.status_code) | ||
813 | 17 | self.assertEqual( | ||
814 | 18 | 'No deployer file data received.', | ||
815 | 19 | response.json_body['error_messages'][0]) | ||
816 | 20 | |||
817 | 21 | def test_bundle_proof_invalid_deployer_file(self): | ||
818 | 22 | request = self.make_request('bundle', remainder='/proof') | ||
819 | 23 | request.params = { | ||
820 | 24 | 'deployer_file': '{' | ||
821 | 25 | } | ||
822 | 26 | response = self.api_class(request)() | ||
823 | 27 | self.assertEqual(400, response.status_code) | ||
824 | 28 | self.assertIn( | ||
825 | 29 | 'Could not parse', | ||
826 | 30 | response.json_body['error_messages'][0]) | ||
827 | 31 | |||
828 | 32 | def test_bundle_proof_charm_exists(self): | ||
829 | 33 | deployer_file = load_data_file('sample_bundle/bundles.yaml') | ||
830 | 34 | |||
831 | 35 | request = self.make_request('bundle', remainder='/proof') | ||
832 | 36 | request.params = { | ||
833 | 37 | 'deployer_file': deployer_file | ||
834 | 38 | } | ||
835 | 39 | |||
836 | 40 | response = self.api_class(request)() | ||
837 | 41 | self.assertEqual(200, response.status_code) | ||
838 | 42 | results = response.json_body | ||
839 | 43 | self.assertEqual(1, len(results['errors'])) | ||
840 | 44 | self.assertEqual(4, len(results['errors'][0]['services'].keys())) | ||
841 | 45 | self.assertIn( | ||
842 | 46 | 'Could not find ', | ||
843 | 47 | results['errors'][0]['services']['db'][0]['message']) | ||
844 | 48 | |||
845 | 49 | # And each failure has a list of 'process' information. | ||
846 | 50 | self.assertIn( | ||
847 | 51 | 'debug_info', | ||
848 | 52 | results['errors'][0]['services']['db'][0].keys()) | ||
849 | 53 | |||
850 | 54 | def test_bundle_proof_valid(self): | ||
851 | 55 | # We have to create the charm so that we can verify it exists. | ||
852 | 56 | _id, charm = factory.makeCharm( | ||
853 | 57 | self.db, | ||
854 | 58 | description='' | ||
855 | 59 | ) | ||
856 | 60 | |||
857 | 61 | # and we'll cheat and just find it straight by the store url. | ||
858 | 62 | charm = Charm(charm) | ||
859 | 63 | store_url = charm.store_url | ||
860 | 64 | |||
861 | 65 | bundle_config = { | ||
862 | 66 | 'wiki': { | ||
863 | 67 | 'services': { | ||
864 | 68 | 'charm': { | ||
865 | 69 | 'charm_url': str(store_url) | ||
866 | 70 | } | ||
867 | 71 | } | ||
868 | 72 | } | ||
869 | 73 | } | ||
870 | 74 | |||
871 | 75 | request = self.make_request('bundle', remainder='/proof') | ||
872 | 76 | request.params = { | ||
873 | 77 | 'deployer_file': yaml.dump(bundle_config) | ||
874 | 78 | } | ||
875 | 79 | response = self.api_class(request)() | ||
876 | 80 | self.assertEqual(200, response.status_code) | ||
877 | 81 | results = response.json_body | ||
878 | 82 | self.assertEqual(0, len(results['errors'])) | ||
879 | 83 | |||
880 | 84 | def test_bundle_proof_invalid_config_type(self): | ||
881 | 85 | # We have to create the charm so that we can verify it exists. | ||
882 | 86 | _id, charm = factory.makeCharm( | ||
883 | 87 | self.db, | ||
884 | 88 | description='' | ||
885 | 89 | ) | ||
886 | 90 | |||
887 | 91 | # and we'll cheat and just find it straight by the store url. | ||
888 | 92 | charm = Charm(charm) | ||
889 | 93 | store_url = charm.store_url | ||
890 | 94 | |||
891 | 95 | bundle_config = { | ||
892 | 96 | 'wiki': { | ||
893 | 97 | 'services': { | ||
894 | 98 | 'charm': { | ||
895 | 99 | 'charm_url': str(store_url), | ||
896 | 100 | 'options': { | ||
897 | 101 | 'script-interval': 'invalid' | ||
898 | 102 | } | ||
899 | 103 | } | ||
900 | 104 | } | ||
901 | 105 | } | ||
902 | 106 | } | ||
903 | 107 | |||
904 | 108 | request = self.make_request('bundle', remainder='/proof') | ||
905 | 109 | request.params = { | ||
906 | 110 | 'deployer_file': yaml.dump(bundle_config) | ||
907 | 111 | } | ||
908 | 112 | response = self.api_class(request)() | ||
909 | 113 | self.assertEqual(200, response.status_code) | ||
910 | 114 | results = response.json_body | ||
911 | 115 | self.assertEqual(1, len(results['errors'])) | ||
912 | 116 | self.assertEqual(1, len(results['error_messages'])) | ||
913 | 117 | |||
914 | 118 | # Note that the View will prefix the error message from the proof with | ||
915 | 119 | # the bundle name to help identify where the error message came from. | ||
916 | 120 | self.assertEqual( | ||
917 | 121 | 'wiki: script-interval is not of type int.', | ||
918 | 122 | results['error_messages'][0] | ||
919 | 123 | ) | ||
920 | 124 | |||
921 | 125 | def test_bundle_proof_supports_multiple_errors(self): | ||
922 | 126 | _id, charm = factory.makeCharm( | ||
923 | 127 | self.db, | ||
924 | 128 | description='' | ||
925 | 129 | ) | ||
926 | 130 | |||
927 | 131 | # and we'll cheat and just find it straight by the store url. | ||
928 | 132 | charm = Charm(charm) | ||
929 | 133 | store_url = charm.store_url | ||
930 | 134 | |||
931 | 135 | bundle_config = { | ||
932 | 136 | 'wiki': { | ||
933 | 137 | 'services': { | ||
934 | 138 | 'charm': { | ||
935 | 139 | 'charm_url': str(store_url), | ||
936 | 140 | 'options': { | ||
937 | 141 | 'script-interval': 'invalid', | ||
938 | 142 | 'no-exist': 'hah invalid', | ||
939 | 143 | } | ||
940 | 144 | }, | ||
941 | 145 | 'fail': { | ||
942 | 146 | 'name': 'will-fail' | ||
943 | 147 | } | ||
944 | 148 | } | ||
945 | 149 | } | ||
946 | 150 | } | ||
947 | 151 | |||
948 | 152 | request = self.make_request('bundle', remainder='/proof') | ||
949 | 153 | request.params = { | ||
950 | 154 | 'deployer_file': yaml.dump(bundle_config) | ||
951 | 155 | } | ||
952 | 156 | response = self.api_class(request)() | ||
953 | 157 | self.assertEqual(200, response.status_code) | ||
954 | 158 | results = response.json_body | ||
955 | 159 | self.assertEqual(1, len(results['errors'])) | ||
956 | 160 | self.assertEqual(3, len(results['error_messages'])) | ||
957 | 161 | |||
958 | 162 | # One message from not finding a charm. | ||
959 | 163 | self.assertEqual( | ||
960 | 164 | 'wiki: Could not find charm: fail', | ||
961 | 165 | results['error_messages'][0] | ||
962 | 166 | ) | ||
963 | 167 | # The other two from checking the config of the other charm. | ||
964 | 168 | self.assertEqual( | ||
965 | 169 | 'wiki: script-interval is not of type int.', | ||
966 | 170 | results['error_messages'][1] | ||
967 | 171 | ) | ||
968 | 172 | self.assertEqual( | ||
969 | 173 | 'wiki: The charm has no option for: no-exist', | ||
970 | 174 | results['error_messages'][2] | ||
971 | 175 | ) | ||
972 | 0 | 176 | ||
973 | === added file 'charmworld/views/utils.py' | |||
974 | --- charmworld/views/utils.py 1970-01-01 00:00:00 +0000 | |||
975 | +++ charmworld/views/utils.py 2013-10-22 20:55:07 +0000 | |||
976 | @@ -0,0 +1,33 @@ | |||
977 | 1 | import json | ||
978 | 2 | from webob import Response | ||
979 | 3 | |||
980 | 4 | |||
981 | 5 | def json_response(status, value, headers=[]): | ||
982 | 6 | """Return a JSON API response. | ||
983 | 7 | |||
984 | 8 | :param status: The HTTP status code to use. | ||
985 | 9 | :param value: A json-serializable value to use as the body, or None for | ||
986 | 10 | no body. | ||
987 | 11 | """ | ||
988 | 12 | # If the value is an object, then it must be representable as a mapping. | ||
989 | 13 | if value is None: | ||
990 | 14 | body = '' | ||
991 | 15 | else: | ||
992 | 16 | while True: | ||
993 | 17 | try: | ||
994 | 18 | body = json.dumps(value, sort_keys=True, indent=2) | ||
995 | 19 | except TypeError: | ||
996 | 20 | # If the serialization wasn't possible, and the value wasn't | ||
997 | 21 | # already a dictionary, convert it to one and try again. | ||
998 | 22 | if not isinstance(value, dict): | ||
999 | 23 | value = dict(value) | ||
1000 | 24 | continue | ||
1001 | 25 | break | ||
1002 | 26 | |||
1003 | 27 | return Response(body, | ||
1004 | 28 | headerlist=[ | ||
1005 | 29 | ('Content-Type', 'application/json'), | ||
1006 | 30 | ('Access-Control-Allow-Origin', '*'), | ||
1007 | 31 | ('Access-Control-Allow-Headers', 'X-Requested-With'), | ||
1008 | 32 | ] + headers, | ||
1009 | 33 | status_code=status) |