Merge lp:~abentley/charmworld/config-storage into lp:~juju-jitsu/charmworld/trunk
- config-storage
- Merge into trunk
Proposed by
Aaron Bentley
Status: | Merged |
---|---|
Merged at revision: | 299 |
Proposed branch: | lp:~abentley/charmworld/config-storage |
Merge into: | lp:~juju-jitsu/charmworld/trunk |
Prerequisite: | lp:~abentley/charmworld/slow-migrations-2 |
Diff against target: |
627 lines (+336/-48) 12 files modified
charmworld/jobs/ingest.py (+13/-6) charmworld/jobs/tests/test_scan.py (+34/-2) charmworld/models.py (+41/-3) charmworld/search.py (+20/-5) charmworld/testing/data/sample_charm/config.yaml (+0/-1) charmworld/testing/factory.py (+3/-1) charmworld/tests/test_models.py (+67/-9) charmworld/tests/test_search.py (+1/-1) charmworld/views/charms.py (+3/-0) charmworld/views/tests/test_charms.py (+63/-7) migrations/versions/010_remap_options.py (+17/-0) migrations/versions/tests/test_migrations.py (+74/-13) |
To merge this branch: | bzr merge lp:~abentley/charmworld/config-storage |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Curtis Hovey (community) | code | Approve | |
Review via email: mp+173218@code.launchpad.net |
This proposal supersedes a proposal from 2013-06-27.
Commit message
Description of the change
Change the internal representation of config options, so that the user-supplied option names are used as values, not as keys.
In this update, the upgrade is performed as an exodus, rather than a fast migration. Also, some unused code is deleted.
Note: This branch must not land until a corresponding update to the charm is in place.
To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote : Posted in a previous version of this proposal | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charmworld/jobs/ingest.py' |
2 | --- charmworld/jobs/ingest.py 2013-07-01 14:45:02 +0000 |
3 | +++ charmworld/jobs/ingest.py 2013-07-05 14:17:29 +0000 |
4 | @@ -32,11 +32,14 @@ |
5 | get_branch_info, |
6 | parse_date, |
7 | ) |
8 | -from charmworld.models import getconnection |
9 | -from charmworld.models import getdb |
10 | -from charmworld.models import getfs |
11 | -from charmworld.models import CharmFileSet |
12 | -from charmworld.models import CharmSource |
13 | +from charmworld.models import ( |
14 | + CharmFileSet, |
15 | + CharmSource, |
16 | + getconnection, |
17 | + getdb, |
18 | + getfs, |
19 | + options_to_storage, |
20 | +) |
21 | from charmworld.search import ( |
22 | ElasticSearchClient, |
23 | SearchServiceNotAvailable, |
24 | @@ -491,7 +494,11 @@ |
25 | config_raw = cfile.read() |
26 | |
27 | try: |
28 | - config = quote_yaml(yaml.load(config_raw)) |
29 | + config_yaml = yaml.load(config_raw) |
30 | + if 'options' in config_yaml: |
31 | + config_yaml['options'] = options_to_storage( |
32 | + config_yaml['options']) |
33 | + config = quote_yaml(config_yaml) |
34 | except Exception, exc: |
35 | raise IngestError( |
36 | 'Invalid charm config yaml. %s: %s' % ( |
37 | |
38 | === modified file 'charmworld/jobs/tests/test_scan.py' |
39 | --- charmworld/jobs/tests/test_scan.py 2013-06-24 15:45:29 +0000 |
40 | +++ charmworld/jobs/tests/test_scan.py 2013-07-05 14:17:29 +0000 |
41 | @@ -34,8 +34,40 @@ |
42 | # 'foo.bar' is replaced with 'foo%2Ebar'. |
43 | self.assertFalse('dotted.name' in self.charm) |
44 | self.assertEqual('foo', self.charm['dotted%2Ename']) |
45 | - self.assertFalse('foo.bar' in self.charm['config']['options']) |
46 | - self.assertEqual('baz', self.charm['config']['options']['foo%2Ebar']) |
47 | + |
48 | + def test_options_format(self): |
49 | + del self.charm['config'] |
50 | + scan_charm(self.charm, self.db, self.fs, self.log) |
51 | + expected = [ |
52 | + { |
53 | + 'name': 'source', |
54 | + 'default': 'lp:charmworld', |
55 | + 'type': 'string', |
56 | + 'description': 'The bzr branch to pull the charmworld source' |
57 | + ' from.' |
58 | + }, |
59 | + { |
60 | + 'name': 'revno', |
61 | + 'default': -1, |
62 | + 'type': 'int', |
63 | + 'description': 'The revno of the bzr branch to use. -1 for' |
64 | + ' current tip.' |
65 | + }, |
66 | + { |
67 | + 'name': 'execute-ingest-every', |
68 | + 'default': 15, |
69 | + 'type': 'int', |
70 | + 'description': 'The amount of time (in minutes) that the' |
71 | + ' ingest tasks for charmworld are executed' |
72 | + }, |
73 | + { |
74 | + 'name': 'error-email', |
75 | + 'default': '', |
76 | + 'type': 'string', |
77 | + 'description': 'Address to mail errors to.' |
78 | + } |
79 | + ] |
80 | + self.assertItemsEqual(expected, self.charm['config']['options']) |
81 | |
82 | def test_short_interface_specification(self): |
83 | # Charms can specify the interfaces they require or provide |
84 | |
85 | === modified file 'charmworld/models.py' |
86 | --- charmworld/models.py 2013-07-05 14:17:29 +0000 |
87 | +++ charmworld/models.py 2013-07-05 14:17:29 +0000 |
88 | @@ -544,7 +544,11 @@ |
89 | |
90 | The dict is parsed from the config_raw property's YAML. |
91 | """ |
92 | - return self._representation['config'] |
93 | + config = self._representation['config'] |
94 | + if config is not None and 'options' in config: |
95 | + config = dict(config) |
96 | + config['options'] = storage_to_options(config['options']) |
97 | + return config |
98 | |
99 | @property |
100 | def config_raw(self): |
101 | @@ -557,7 +561,7 @@ |
102 | @property |
103 | def options(self): |
104 | """The charm's configuration options.""" |
105 | - config = self._representation['config'] |
106 | + config = self.config |
107 | if config is not None and 'options' in config: |
108 | return config['options'] |
109 | else: |
110 | @@ -992,7 +996,11 @@ |
111 | |
112 | def save(self, charm): |
113 | self.collection.save(charm) |
114 | - self.index_client.index_charm(charm) |
115 | + try: |
116 | + self.index_client.index_charm(charm) |
117 | + except: |
118 | + self.index_client.delete_charm(charm['_id']) |
119 | + raise |
120 | |
121 | def get_store_charms(self): |
122 | return self.collection.find({'store_url': {'$exists': True}}) |
123 | @@ -1078,3 +1086,33 @@ |
124 | logger.exception('Unable to index charm.') |
125 | else: |
126 | logger.info('Syncing %s to Elasticsearch' % charm_id) |
127 | + |
128 | + |
129 | +def options_to_storage(options): |
130 | + """Convert options, as retrieved from config.yaml, into a list. |
131 | + |
132 | + This format avoids user-defined keys, which are problematic in |
133 | + elasticsearch. |
134 | + """ |
135 | + storage = [] |
136 | + for key, value in options.items(): |
137 | + option = {'name': key} |
138 | + option.update(value) |
139 | + storage.append(option) |
140 | + return storage |
141 | + |
142 | + |
143 | +def storage_to_options(storage): |
144 | + """Convert options, as retrieved from mongodb, into a dict. |
145 | + |
146 | + This format has the same structure as config.yaml. |
147 | + """ |
148 | + options = {} |
149 | + for option in storage: |
150 | + new_option = dict(option) |
151 | + del new_option['name'] |
152 | + value = options.setdefault(option['name'], new_option) |
153 | + if value is not new_option: |
154 | + raise ValueError('Option name "%s" occurs multiple times.' % |
155 | + option['name']) |
156 | + return options |
157 | |
158 | === modified file 'charmworld/search.py' |
159 | --- charmworld/search.py 2013-07-05 14:17:29 +0000 |
160 | +++ charmworld/search.py 2013-07-05 14:17:29 +0000 |
161 | @@ -30,7 +30,7 @@ |
162 | 'name': 10, |
163 | 'summary': 5, |
164 | 'description': 3, |
165 | - 'config.options.*.description': None, |
166 | + 'config.options.description': None, |
167 | 'relations': None, |
168 | } |
169 | |
170 | @@ -154,13 +154,24 @@ |
171 | """ |
172 | exact_index = ['categories', 'name', 'owner', 'i_provides', |
173 | 'i_requires', 'series', 'store_url'] |
174 | + charm_properties = dict( |
175 | + (name, {'type': 'string', 'index': 'not_analyzed'}) |
176 | + for name in exact_index) |
177 | + charm_properties['config'] = { |
178 | + 'properties': { |
179 | + 'options': { |
180 | + 'properties': { |
181 | + 'default': {'type': 'string'}, |
182 | + 'description': {'type': 'string'}, |
183 | + } |
184 | + } |
185 | + } |
186 | + } |
187 | + |
188 | with translate_error(): |
189 | self._client.put_mapping(self.index_name, 'charm', { |
190 | 'charm': { |
191 | - 'properties': dict( |
192 | - (name, {'type': 'string', 'index': 'not_analyzed'}) |
193 | - for name in exact_index |
194 | - ) |
195 | + 'properties': charm_properties, |
196 | } |
197 | }) |
198 | |
199 | @@ -174,6 +185,10 @@ |
200 | self._client.index(self.index_name, 'charm', charm, charm_id, |
201 | refresh=True) |
202 | |
203 | + def delete_charm(self, charm_id): |
204 | + with translate_error(): |
205 | + self._client.delete(self.index_name, 'charm', charm_id) |
206 | + |
207 | def index_charms(self, charms): |
208 | if len(charms) == 0: |
209 | return |
210 | |
211 | === modified file 'charmworld/testing/data/sample_charm/config.yaml' |
212 | --- charmworld/testing/data/sample_charm/config.yaml 2013-02-07 17:26:59 +0000 |
213 | +++ charmworld/testing/data/sample_charm/config.yaml 2013-07-05 14:17:29 +0000 |
214 | @@ -15,4 +15,3 @@ |
215 | type: string |
216 | default: "" |
217 | description: "Address to mail errors to." |
218 | - foo.bar: baz |
219 | |
220 | === modified file 'charmworld/testing/factory.py' |
221 | --- charmworld/testing/factory.py 2013-07-01 14:45:02 +0000 |
222 | +++ charmworld/testing/factory.py 2013-07-05 14:17:29 +0000 |
223 | @@ -24,8 +24,9 @@ |
224 | process_charm, |
225 | ) |
226 | from charmworld.models import ( |
227 | + CharmFileSet, |
228 | getfs, |
229 | - CharmFileSet, |
230 | + options_to_storage, |
231 | ) |
232 | |
233 | |
234 | @@ -185,6 +186,7 @@ |
235 | 'description': 'The interval between script runs.' |
236 | }, |
237 | } |
238 | + options = options_to_storage(options) |
239 | if files is None: |
240 | files = { |
241 | 'readme': { |
242 | |
243 | === modified file 'charmworld/tests/test_models.py' |
244 | --- charmworld/tests/test_models.py 2013-07-01 14:45:02 +0000 |
245 | +++ charmworld/tests/test_models.py 2013-07-05 14:17:29 +0000 |
246 | @@ -14,6 +14,10 @@ |
247 | from os.path import join |
248 | |
249 | from mock import patch |
250 | +from pyelasticsearch.exceptions import ( |
251 | + ElasticHttpError, |
252 | + ElasticHttpNotFoundError, |
253 | +) |
254 | |
255 | from charmworld.models import ( |
256 | acquire_session_secret, |
257 | @@ -22,7 +26,9 @@ |
258 | CharmFileSet, |
259 | CharmSource, |
260 | find_charms, |
261 | + options_to_storage, |
262 | QAData, |
263 | + storage_to_options, |
264 | sync_index, |
265 | UserMgr, |
266 | ) |
267 | @@ -437,11 +443,10 @@ |
268 | |
269 | def test_config(self): |
270 | # The config property is a dict of charm options. |
271 | - config = {'options': {'mode': 'fast'}} |
272 | - charm_data = factory.get_charm_json() |
273 | - charm_data['config'] = config |
274 | + config = {'options': {'key': {'default': 'value', 'type': 'string'}}} |
275 | + charm_data = factory.get_charm_json(options=config['options']) |
276 | charm = Charm(charm_data) |
277 | - self.assertIs(config, charm.config) |
278 | + self.assertEqual(config, charm.config) |
279 | # The default is a dict with a single key named 'options' set to |
280 | # an empty dict. |
281 | charm = Charm({}) |
282 | @@ -460,12 +465,10 @@ |
283 | |
284 | def test_options(self): |
285 | # The options property is a dict from the charm's config. |
286 | - options = {'mode': 'fast'} |
287 | - config = {'options': options} |
288 | - charm_data = factory.get_charm_json() |
289 | - charm_data['config'] = config |
290 | + options = {'key': {'default': 'value', 'type': 'string'}} |
291 | + charm_data = factory.get_charm_json(options=options) |
292 | charm = Charm(charm_data) |
293 | - self.assertIs(options, charm.options) |
294 | + self.assertEqual(options, charm.options) |
295 | # The default is a dict. |
296 | charm = Charm({}) |
297 | self.assertEqual({}, charm.options) |
298 | @@ -1040,3 +1043,58 @@ |
299 | messages = [r.getMessage() for r in handler.buffer] |
300 | self.assertIn('Unable to index charm.', messages) |
301 | self.assertEqual(1, len(self.index_client.api_search())) |
302 | + |
303 | + def test_save_deletes_on_error(self): |
304 | + self.use_index_client() |
305 | + source = CharmSource.from_request(self) |
306 | + source.save({'_id': 'a', 'b': {}}) |
307 | + with self.assertRaises(ElasticHttpError): |
308 | + source.save({'_id': 'a', 'b': 'c'}) |
309 | + with self.assertRaises(ElasticHttpNotFoundError): |
310 | + self.index_client.get('a') |
311 | + |
312 | + |
313 | +class TestOptionsStorage(TestCase): |
314 | + |
315 | + yaml = { |
316 | + 'foo': { |
317 | + 'default': 'bar', |
318 | + 'type': 'string', |
319 | + 'description': 'Hello.' |
320 | + }, |
321 | + 'baz': { |
322 | + 'default': 42, |
323 | + 'type': 'int', |
324 | + 'description': 'Hello.' |
325 | + }, |
326 | + } |
327 | + storage = [{ |
328 | + 'name': 'baz', |
329 | + 'default': 42, |
330 | + 'type': 'int', |
331 | + 'description': 'Hello.' |
332 | + }, { |
333 | + 'name': 'foo', |
334 | + 'default': 'bar', |
335 | + 'type': 'string', |
336 | + 'description': 'Hello.' |
337 | + }] |
338 | + |
339 | + def test_options_to_storage(self): |
340 | + self.assertItemsEqual(self.storage, options_to_storage(self.yaml)) |
341 | + |
342 | + def test_storage_to_options(self): |
343 | + self.assertEqual(self.yaml, storage_to_options(self.storage)) |
344 | + |
345 | + def test_storage_to_options_duplicate_key(self): |
346 | + with self.assertRaises(ValueError) as exc_context: |
347 | + storage_to_options([{'name': 'same'}, {'name': 'same'}]) |
348 | + self.assertEqual('Option name "same" occurs multiple times.', |
349 | + str(exc_context.exception)) |
350 | + |
351 | + def test_roundtrip(self): |
352 | + self.assertEqual( |
353 | + self.yaml, storage_to_options(options_to_storage(self.yaml))) |
354 | + self.assertItemsEqual( |
355 | + self.storage, |
356 | + options_to_storage(storage_to_options(self.storage))) |
357 | |
358 | === modified file 'charmworld/tests/test_search.py' |
359 | --- charmworld/tests/test_search.py 2013-07-01 14:45:02 +0000 |
360 | +++ charmworld/tests/test_search.py 2013-07-05 14:17:29 +0000 |
361 | @@ -173,7 +173,7 @@ |
362 | elif key in ('provides', 'requires'): |
363 | query = query.values()[0].values()[0] |
364 | elif key == 'config': |
365 | - query = query.values()[0].values()[0]['description'] |
366 | + query = query.values()[0][0]['description'] |
367 | elif key == 'store_data': |
368 | query = query['digest'] |
369 | elif key == 'files': |
370 | |
371 | === modified file 'charmworld/views/charms.py' |
372 | --- charmworld/views/charms.py 2013-07-03 21:33:20 +0000 |
373 | +++ charmworld/views/charms.py 2013-07-05 14:17:29 +0000 |
374 | @@ -165,7 +165,10 @@ |
375 | |
376 | |
377 | def _json_charm(charm): |
378 | + charm_config = charm.config |
379 | charm = dict(charm._representation) |
380 | + # Reformat config into old internal representation. |
381 | + charm['config'] = charm_config |
382 | charm_keys = set(charm.keys()) |
383 | for k in SANITIZE.intersection(charm_keys): |
384 | del charm[k] |
385 | |
386 | === modified file 'charmworld/views/tests/test_charms.py' |
387 | --- charmworld/views/tests/test_charms.py 2013-07-03 21:33:20 +0000 |
388 | +++ charmworld/views/tests/test_charms.py 2013-07-05 14:17:29 +0000 |
389 | @@ -1,6 +1,7 @@ |
390 | # Copyright 2012, 2013 Canonical Ltd. This software is licensed under the |
391 | # GNU Affero General Public License version 3 (see the file LICENSE). |
392 | |
393 | +from datetime import date |
394 | from logging import getLogger |
395 | |
396 | from charmworld.jobs.ingest import ( |
397 | @@ -16,7 +17,6 @@ |
398 | from charmworld.testing import ViewTestBase |
399 | from charmworld.testing import WebTestBase |
400 | from charmworld.utils import quote_key |
401 | -from charmworld.views import SANITIZE |
402 | from charmworld.views.api import API2 |
403 | from charmworld.views.charms import ( |
404 | _charm_view, |
405 | @@ -35,7 +35,65 @@ |
406 | quality_edit, |
407 | series_collection, |
408 | ) |
409 | -import json |
410 | + |
411 | + |
412 | +DISTRO_CHARM_DATA = { |
413 | + 'name': 'foo', |
414 | + 'series': 'precise', |
415 | + 'owner': 'bar', |
416 | + 'maintainer': 'John Doe <jdoe@example.com>', |
417 | + 'summary': 'Remote terminal classroom over https', |
418 | + 'description': 'foobar', |
419 | + 'store_url': 'cs:precise/foo-1', |
420 | + 'bzr_branch': '', |
421 | + 'promulgated': True, |
422 | + 'branch_deleted': False, |
423 | + 'categories': [], |
424 | + 'is_featured': False, |
425 | + 'subordinate': False, |
426 | + 'requires': {'database': {'interface': 'mongodb'}}, |
427 | + 'provides': {'website': {'interface': 'https'}}, |
428 | + 'config': { |
429 | + 'options': { |
430 | + 'script-interval': { |
431 | + 'default': 5, |
432 | + 'type': 'int', |
433 | + 'description': 'The interval between script runs.' |
434 | + } |
435 | + } |
436 | + }, |
437 | + 'ensemble': 'formula', |
438 | + 'downloads': 0, |
439 | + 'downloads_in_past_30_days': 0, |
440 | + 'files': { |
441 | + 'readme': { |
442 | + 'subdir': '', |
443 | + 'filename': 'README', |
444 | + }, |
445 | + 'install': { |
446 | + 'subdir': 'hooks', |
447 | + 'filename': 'install', |
448 | + }, |
449 | + }, |
450 | + 'tests': {}, |
451 | + 'test_results': {}, |
452 | + 'proof': { |
453 | + 'w': [ |
454 | + ' no README file', |
455 | + ' (install:31) - hook accesses EC2 metadata service' |
456 | + ' directly' |
457 | + ] |
458 | + }, |
459 | + 'date_created': '2012-01-01', |
460 | + 'error': '', |
461 | + 'qa': None, |
462 | + 'last_change': { |
463 | + 'committer': u'John Doe <jdoe@example.com>', |
464 | + 'message': u'maintainer', |
465 | + 'revno': 12, |
466 | + 'created': 1343082725.06 |
467 | + } |
468 | +} |
469 | |
470 | |
471 | class TestCharms(ViewTestBase): |
472 | @@ -368,17 +426,15 @@ |
473 | self.enable_routes() |
474 | ignore, charm = factory.makeCharm( |
475 | self.db, name='foo', owner='bar', series='precise', |
476 | - promulgated=True) |
477 | + description='foobar', promulgated=True, |
478 | + date_created=date(2012, 1, 1)) |
479 | request = self.getRequest() |
480 | request.matchdict = { |
481 | 'charm': 'foo', |
482 | 'series': 'precise', |
483 | } |
484 | - charm_data = dict(Charm(charm)._representation) |
485 | - for key in SANITIZE.intersection(charm_data.keys()): |
486 | - del charm_data[key] |
487 | response = distro_charm_json(request) |
488 | - self.assertEqual(charm_data, json.loads(response.body)) |
489 | + self.assertEqual(DISTRO_CHARM_DATA, response.json_body) |
490 | |
491 | def test_valid_others(self): |
492 | charm = factory.makeCharm(self.db, files={})[1] |
493 | |
494 | === added file 'migrations/versions/010_remap_options.py' |
495 | --- migrations/versions/010_remap_options.py 1970-01-01 00:00:00 +0000 |
496 | +++ migrations/versions/010_remap_options.py 2013-07-05 14:17:29 +0000 |
497 | @@ -0,0 +1,17 @@ |
498 | +# Copyright 2013 Canonical Ltd. This software is licensed under the |
499 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
500 | + |
501 | +from charmworld.models import ( |
502 | + options_to_storage, |
503 | +) |
504 | + |
505 | + |
506 | +def exodus_update(source, target): |
507 | + for charm in source.collection.find(): |
508 | + config = charm.get('config') |
509 | + if config is not None and 'options' in config: |
510 | + config['options'] = options_to_storage(config['options']) |
511 | + try: |
512 | + target.save(charm) |
513 | + except Exception as e: |
514 | + print e |
515 | |
516 | === modified file 'migrations/versions/tests/test_migrations.py' |
517 | --- migrations/versions/tests/test_migrations.py 2013-06-11 19:18:47 +0000 |
518 | +++ migrations/versions/tests/test_migrations.py 2013-07-05 14:17:29 +0000 |
519 | @@ -1,34 +1,95 @@ |
520 | # Copyright 2012, 2013 Canonical Ltd. This software is licensed under the |
521 | # GNU Affero General Public License version 3 (see the file LICENSE). |
522 | |
523 | +from pyelasticsearch.exceptions import ( |
524 | + ElasticHttpNotFoundError, |
525 | +) |
526 | from charmworld.models import CharmSource |
527 | from charmworld.testing import MongoTestBase |
528 | from migrations.migrate import Versions |
529 | |
530 | |
531 | -def run_migration(db, index_client, module_name): |
532 | - versions = Versions('migrations/versions/') |
533 | - versions.run_migration(db, index_client, module_name) |
534 | - |
535 | - |
536 | -class TestMigration004(MongoTestBase): |
537 | +class MigrationTestBase(MongoTestBase): |
538 | + |
539 | + def setUp(self): |
540 | + super(MigrationTestBase, self).setUp() |
541 | + self.versions = Versions('migrations/versions/') |
542 | + |
543 | + |
544 | +class TestMigration004(MigrationTestBase): |
545 | |
546 | def test_migration(self): |
547 | self.use_index_client() |
548 | self.db.create_collection('charm-errors') |
549 | - run_migration(self.db, self.index_client, '004_remove_charm_errors.py') |
550 | + self.versions.run_migration(self.db, self.index_client, |
551 | + '004_remove_charm_errors.py') |
552 | self.assertNotIn('charm-errors', self.db.collection_names()) |
553 | |
554 | |
555 | -class TestMigration008(MongoTestBase): |
556 | +class TestMigration008(MigrationTestBase): |
557 | |
558 | def test_migration(self): |
559 | self.use_index_client() |
560 | source = CharmSource.from_request(self) |
561 | source.save({'_id': 'a', 'icon': 'asdf', 'asdf': 'asdf'}) |
562 | source.save({'_id': 'b', 'icon': 'asdf', 'asdf': 'asdf'}) |
563 | - run_migration(self.db, self.index_client, '008_delete_icon_field.py') |
564 | - for charm in source._get_all('a'): |
565 | - self.assertNotIn('icon', charm) |
566 | - for charm in source._get_all('b'): |
567 | - self.assertNotIn('icon', charm) |
568 | + self.versions.run_migration(self.db, self.index_client, |
569 | + '008_delete_icon_field.py') |
570 | + for charm in source._get_all('a'): |
571 | + self.assertNotIn('icon', charm) |
572 | + for charm in source._get_all('b'): |
573 | + self.assertNotIn('icon', charm) |
574 | + |
575 | + |
576 | +class TestMigration010(MigrationTestBase): |
577 | + |
578 | + def test_migration(self): |
579 | + self.use_index_client() |
580 | + source = CharmSource.from_request(self) |
581 | + source.save({ |
582 | + '_id': 'a', |
583 | + 'config': None, |
584 | + }) |
585 | + source.save({ |
586 | + '_id': 'b', |
587 | + 'config': { |
588 | + 'options': {}, |
589 | + }, |
590 | + }) |
591 | + source.save({ |
592 | + '_id': 'c', |
593 | + 'config': { |
594 | + 'options': {'foo': {'default': 'bar'}}, |
595 | + }, |
596 | + }) |
597 | + source.save({'_id': 'd'}) |
598 | + # This is not acceptable to Elasticsearch, because config is a list. |
599 | + self.db.charms.save({ |
600 | + '_id': 'e', |
601 | + 'config': ['a'], |
602 | + }) |
603 | + self.versions.get_exodus('010_remap_options.py')(source, source) |
604 | + for charm in source._get_all('a'): |
605 | + self.assertEqual({ |
606 | + '_id': 'a', |
607 | + 'config': None, |
608 | + }, charm) |
609 | + for charm in source._get_all('b'): |
610 | + self.assertEqual({ |
611 | + '_id': 'b', |
612 | + 'config': {'options': []}, |
613 | + }, charm) |
614 | + for charm in source._get_all('c'): |
615 | + self.assertEqual({ |
616 | + '_id': 'c', |
617 | + 'config': {'options': [{ |
618 | + 'name': 'foo', |
619 | + 'default': 'bar', |
620 | + }]} |
621 | + }, charm) |
622 | + for charm in source._get_all('d'): |
623 | + self.assertEqual({'_id': 'd'}, charm) |
624 | + with self.assertRaises(ElasticHttpNotFoundError): |
625 | + self.index_client.get('e') |
626 | + self.assertEqual({'_id': 'e', 'config': ['a']}, |
627 | + self.db.charms.find_one({'_id': 'e'})) |
We talked on IRC to clarify that some charms are not in a sane state and migration needs to handle cases where config is not a dict with a single item of key, sub-dict. We might want migrate to log in the future. We know from the 007 migration problem seen with production's bogus charms that the juju-log is capturing stdout.