Merge lp:~abentley/charmworld/remove-api-0 into lp:~juju-jitsu/charmworld/trunk

Proposed by Aaron Bentley
Status: Merged
Approved by: Aaron Bentley
Approved revision: 250
Merged at revision: 243
Proposed branch: lp:~abentley/charmworld/remove-api-0
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 727 lines (+140/-317)
13 files modified
charmworld/routes.py (+0/-3)
charmworld/search.py (+24/-7)
charmworld/tests/test_search.py (+30/-6)
charmworld/views/api.py (+0/-30)
charmworld/views/tests/test_api.py (+33/-252)
migrations/migrate.py (+11/-6)
migrations/test_migrate.py (+8/-4)
migrations/versions/001_load_qa_questions.py (+1/-1)
migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py (+1/-1)
migrations/versions/003_remove_provider_qa_questions.py (+1/-1)
migrations/versions/004_remove_charm_errors.py (+3/-1)
migrations/versions/005_delete_icon_field.py (+8/-0)
migrations/versions/tests/test_migrations.py (+20/-5)
To merge this branch: bzr merge lp:~abentley/charmworld/remove-api-0
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+167324@code.launchpad.net

Commit message

Remove API 0

Description of the change

Remove API 0 and introduce a migration to remove icons from charm data.

Since the charm formatting changes, the migration must also update elasticsearch. This is done using reindex, which gains a charms parameter so that mongodb can be used as a data source, and now handles the case where the index does not already exist.

Much test refactoring was done, since all current APIs (i.e. API1) run all
tests now.

copy_index was broken, because it invoked index_charms on itself, not the pending copy. This is now fixed. It is also renamed to "create_replacement", to better-describe its purpose.

unlimited_search is updated to raise IndexMissing if the search errors out with an IndexMissingException.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)
Revision history for this message
Charmworld Lander (charmworld-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charmworld/routes.py'
--- charmworld/routes.py 2013-04-29 18:21:35 +0000
+++ charmworld/routes.py 2013-06-04 16:12:32 +0000
@@ -80,9 +80,6 @@
80 config.add_route('auth_callback', '/auth_callback')80 config.add_route('auth_callback', '/auth_callback')
81 config.add_route('logout', '/logout')81 config.add_route('logout', '/logout')
8282
83 # API
84 config.add_route('api_0', '/api/0/{endpoint}{remainder:.*}')
85 config.add_view('charmworld.views.api.API0', route_name='api_0')
86 config.add_route('api_1', '/api/1/{endpoint}{remainder:.*}')83 config.add_route('api_1', '/api/1/{endpoint}{remainder:.*}')
87 config.add_view('charmworld.views.api.API1', route_name='api_1')84 config.add_view('charmworld.views.api.API1', route_name='api_1')
8885
8986
=== modified file 'charmworld/search.py'
--- charmworld/search.py 2013-05-29 12:36:35 +0000
+++ charmworld/search.py 2013-06-04 16:12:32 +0000
@@ -233,7 +233,8 @@
233 kwargs = {'index': self.index_name}233 kwargs = {'index': self.index_name}
234 if limit is not None:234 if limit is not None:
235 kwargs['size'] = limit235 kwargs['size'] = limit
236 hits = self._client.search(dsl, **kwargs)236 with translate_error():
237 hits = self._client.search(dsl, **kwargs)
237 if limit is None and len(hits['hits']['hits']) < hits['hits']['total']:238 if limit is None and len(hits['hits']['hits']) < hits['hits']['total']:
238 kwargs['size'] = hits['hits']['total']239 kwargs['size'] = hits['hits']['total']
239 hits = self._client.search(dsl, **kwargs)240 hits = self._client.search(dsl, **kwargs)
@@ -348,40 +349,56 @@
348 name = '%s-%d' % (self.index_name, random.randint(1, 99999))349 name = '%s-%d' % (self.index_name, random.randint(1, 99999))
349 return self.__class__(self._client, name)350 return self.__class__(self._client, name)
350351
351 def copy_index(self, name=None):352 def create_replacement(self, name=None, charms=None):
352 """Create a copy of this index with the specified name.353 """Create a replacement for this index.
353354
354 The mapping will not be this index's mapping, it will be the current355 The mapping will not be this index's mapping, it will be the current
355 mapping (e.g. put_mapping).356 mapping (e.g. put_mapping).
356357
358 :param name: If not supplied, a name based on this index's name will
359 be used.
360 :param charms: A list of charms. If None, the charms in the current
361 index will be used.
357 :return: The ElasticSearchClient for the supplied mapping.362 :return: The ElasticSearchClient for the supplied mapping.
358 """363 """
359 copy = self.get_aliasable_client(name)364 copy = self.get_aliasable_client(name)
360 copy.create_index()365 copy.create_index()
361 try:366 try:
362 copy.put_mapping()367 copy.put_mapping()
363 self.index_charms(self.api_search(valid_charms_only=False))368 if charms is None:
369 try:
370 charms = self.api_search(valid_charms_only=False)
371 except IndexMissing:
372 charms = []
373 copy.index_charms(charms)
364 return copy374 return copy
365 except:375 except:
366 copy.delete_index()376 copy.delete_index()
367 raise377 raise
368378
369379
370def reindex(index_client):380def reindex(index_client, charms=None):
371 """Reindex documents with the current mapping.381 """Reindex documents with the current mapping.
372382
373 This works by creating a new index and then aliasing it to the old index.383 This works by creating a new index and then aliasing it to the old index.
374 If the old index was already an alias, this is a simple swap. If not, the384 If the old index was already an alias, this is a simple swap. If not, the
375 old index is deleted.385 old index is deleted.
386
387 :param charms: A list of charms to use. If None, the charms in the index
388 will be used.
376 """389 """
377 new_index = index_client.copy_index()390 new_index = index_client.create_replacement(charms=charms)
378 try:391 try:
379 aliased = index_client.get_aliased()392 aliased = index_client.get_aliased()
380 if aliased == []:393 if aliased == []:
381 index_client.delete_index()394 try:
395 index_client.delete_index()
396 except IndexMissing:
397 pass
382 index_client.update_aliased(new_index.index_name, aliased)398 index_client.update_aliased(new_index.index_name, aliased)
383 except:399 except:
384 new_index.delete_index()400 new_index.delete_index()
401 raise
385 if aliased != []:402 if aliased != []:
386 for name in aliased:403 for name in aliased:
387 ElasticSearchClient(index_client._client, name).delete_index()404 ElasticSearchClient(index_client._client, name).delete_index()
388405
=== modified file 'charmworld/tests/test_search.py'
--- charmworld/tests/test_search.py 2013-05-29 12:36:35 +0000
+++ charmworld/tests/test_search.py 2013-06-04 16:12:32 +0000
@@ -444,12 +444,29 @@
444 alias.update_aliased('bar', alias.get_aliased())444 alias.update_aliased('bar', alias.get_aliased())
445 self.assertEqual(['bar'], alias.get_aliased())445 self.assertEqual(['bar'], alias.get_aliased())
446446
447 def test_copy_index(self):447 def test_create_replacement(self):
448 self.index_client.index_charm({'_id': 'a', 'name': 'foo'})448 self.index_client.index_charm({'_id': 'a', 'name': 'foo'})
449 copy = self.index_client.copy_index('index-copy')449 copy = self.index_client.create_replacement('index-copy')
450 self.addCleanup(copy.delete_index)450 self.addCleanup(copy.delete_index)
451 copy.wait_for_startup()451 copy.wait_for_startup()
452 self.assertIn('series', copy.get_mapping()['charm']['properties'])452 self.assertIn('series', copy.get_mapping()['charm']['properties'])
453 self.assertEqual({'_id': 'a', 'name': 'foo'}, copy.get('a'))
454
455 def test_create_replacement_charms(self):
456 self.index_client.index_charm({'_id': 'a', 'name': 'foo'})
457 copy = self.index_client.create_replacement(
458 charms=[{'_id': 'a', 'name': 'bar'}])
459 self.addCleanup(copy.delete_index)
460 copy.wait_for_startup()
461 self.assertIn('series', copy.get_mapping()['charm']['properties'])
462 self.assertEqual({'_id': 'a', 'name': 'bar'}, copy.get('a'))
463
464 def test_create_replacement_misssing(self):
465 client = ElasticSearchClient.from_settings(get_ini(), 'temp-index')
466 # This should not raise an exception, even though the index has not
467 # been created.
468 copy = client.create_replacement()
469 self.addCleanup(copy.delete_index)
453470
454 def test_put_mapping_incompatible_mapping_error(self):471 def test_put_mapping_incompatible_mapping_error(self):
455 # The error we get from an incompatible mapping is IncompatibleMapping472 # The error we get from an incompatible mapping is IncompatibleMapping
@@ -600,3 +617,10 @@
600 mapping = alias.get_mapping()617 mapping = alias.get_mapping()
601 self.assertEqual('not_analyzed',618 self.assertEqual('not_analyzed',
602 mapping['charm']['properties']['name']['index'])619 mapping['charm']['properties']['name']['index'])
620
621 def test_reindexed_no_client_charms(self):
622 client = ElasticSearchClient.from_settings(get_ini())
623 # This should not raise an exception, even though the index does not
624 # exist.
625 new_client = reindex(client, charms=[])
626 new_client.delete_index()
603627
=== modified file 'charmworld/views/api.py'
--- charmworld/views/api.py 2013-06-03 16:18:58 +0000
+++ charmworld/views/api.py 2013-06-04 16:12:32 +0000
@@ -412,33 +412,3 @@
412 'popular': self._charm_results(popular),412 'popular': self._charm_results(popular),
413 }413 }
414 }414 }
415
416
417class API0(API1):
418 """Implementation of API 0.
419
420 All methods whose names do not begin with an underscore are exposed.
421 Methods may return a webob Response, which is returned directly, or a
422 json-serializable value, which will be returned as a json HTTP response.
423 """
424
425 @classmethod
426 def _charm_result(cls, charm, requires, provides):
427 return cls._format_charm(charm)
428
429 def _charm_results(self, charms):
430 return [self._charm_result(charm, {}, {}) for charm in charms]
431
432 def _charms(self, limit=None, name=None, series=None, owner=None,
433 provides=None, requires=None, type_=None, provider=None,
434 scope=None, category=None, categories=None, text=None):
435 return super(API0, self)._charms(
436 limit, name, series, owner, provides, requires, type_, provider,
437 scope, categories, text)
438
439 @staticmethod
440 def _format_charm(charm):
441 output = API1._format_charm(charm)
442 if 'icon' in charm:
443 output['icon'] = charm['icon']
444 return output
445415
=== modified file 'charmworld/views/tests/test_api.py'
--- charmworld/views/tests/test_api.py 2013-06-03 16:18:58 +0000
+++ charmworld/views/tests/test_api.py 2013-06-04 16:12:32 +0000
@@ -15,7 +15,6 @@
15)15)
16from charmworld.models import CharmSource16from charmworld.models import CharmSource
17from charmworld.views.api import (17from charmworld.views.api import (
18 API0,
19 API1,18 API1,
20 json_response,19 json_response,
21)20)
@@ -73,6 +72,30 @@
73 return self.api_class(request)()72 return self.api_class(request)()
7473
7574
75class API1Mixin(APITestBase):
76
77 api_class = API1
78
79 @staticmethod
80 def weightless(charm):
81 """Remove weight from charm because we cannot predict its value."""
82 charm = dict(charm)
83 del charm['weight']
84 return charm
85
86 @classmethod
87 def weightless_relation(cls, relation):
88 new_relation = {}
89 for interface, charms in relation.items():
90 charms = [cls.weightless(charm) for charm in charms]
91 new_relation[interface] = charms
92 return new_relation
93
94 @classmethod
95 def related_weightless(cls, charm):
96 return cls.weightless(cls.api_class._format_related(charm, 0))
97
98
76class TestAPICharms:99class TestAPICharms:
77100
78 def setUp(self):101 def setUp(self):
@@ -259,19 +282,20 @@
259 self.assertIn('new', data['result'])282 self.assertIn('new', data['result'])
260 self.assertIn('featured', data['result'])283 self.assertIn('featured', data['result'])
261 self.assertEqual(['pop%d' % num for num in range(14, 4, -1)],284 self.assertEqual(['pop%d' % num for num in range(14, 4, -1)],
262 [self.get_name(r) for r in data['result']['popular']])285 [r['charm']['name']
286 for r in data['result']['popular']])
263 self.assertEqual(self.api_class._charm_result(last_popular_charm, {},287 self.assertEqual(self.api_class._charm_result(last_popular_charm, {},
264 {}),288 {}),
265 data['result']['popular'][0])289 data['result']['popular'][0])
266 self.assertEqual(['new%d' % num for num in range(14, 4, -1)],290 self.assertEqual(['new%d' % num for num in range(14, 4, -1)],
267 [self.get_name(r) for r in data['result']['new']])291 [r['charm']['name'] for r in data['result']['new']])
268 self.assertEqual(self.api_class._charm_result(last_new_charm, {}, {}),292 self.assertEqual(self.api_class._charm_result(last_new_charm, {}, {}),
269 data['result']['new'][0])293 data['result']['new'][0])
270 self.assertItemsEqual(294 self.assertItemsEqual(
271 ['featured%d' % num for num in range(14, -1, -1)],295 ['featured%d' % num for num in range(14, -1, -1)],
272 [self.get_name(r) for r in data['result']['featured']])296 [r['charm']['name'] for r in data['result']['featured']])
273 featured14, = [charm for charm in data['result']['featured'] if297 featured14, = [charm for charm in data['result']['featured'] if
274 self.get_name(charm) == 'featured14']298 charm['charm']['name'] == 'featured14']
275 self.assertEqual(299 self.assertEqual(
276 self.api_class._charm_result(last_featured_charm, {}, {}),300 self.api_class._charm_result(last_featured_charm, {}, {}),
277 featured14)301 featured14)
@@ -281,40 +305,6 @@
281 response = self.get_response('charms', categories='asdf')305 response = self.get_response('charms', categories='asdf')
282 self.assertEqual(200, response.status_code)306 self.assertEqual(200, response.status_code)
283307
284
285class API1Mixin:
286
287 api_class = API1
288
289 @staticmethod
290 def weightless(charm):
291 """Remove weight from charm because we cannot predict its value."""
292 charm = dict(charm)
293 del charm['weight']
294 return charm
295
296 @classmethod
297 def weightless_relation(cls, relation):
298 new_relation = {}
299 for interface, charms in relation.items():
300 charms = [cls.weightless(charm) for charm in charms]
301 new_relation[interface] = charms
302 return new_relation
303
304 @classmethod
305 def related_weightless(cls, charm):
306 return cls.weightless(cls.api_class._format_related(charm, 0))
307
308
309class TestAPI1Charms(TestAPICharms, APITestBase, API1Mixin):
310
311 def setUp(self):
312 super(TestAPI1Charms, self).setUp()
313
314 @staticmethod
315 def get_name(charm):
316 return charm['charm']['name']
317
318 def test_charms(self):308 def test_charms(self):
319 """Charms endpoint produces expected result."""309 """Charms endpoint produces expected result."""
320 one_changes = [factory.makeChange(created=datetime(1999, 12, 31),310 one_changes = [factory.makeChange(created=datetime(1999, 12, 31),
@@ -585,190 +575,8 @@
585 )575 )
586576
587577
588class TestAPI0Charms(TestAPICharms, APITestBase):578class TestAPI1Charms(TestAPICharms, API1Mixin):
589579 """Test the AP1 implementation of charms endpoint."""
590 api_class = API0
591
592 @staticmethod
593 def get_name(charm):
594 return charm['name']
595
596 def test_charms(self):
597 """Charms endpoint produces expected result."""
598 one_changes = [factory.makeChange(created=datetime(1999, 12, 31),
599 revno=37, message='Hello')]
600 one_id, one = self.makeCharm(name='name1', changes=one_changes,
601 date_created=datetime(1997, 10, 29,
602 12, 10, 9),
603 promulgated=True)
604 self.index_client.index_charm(one)
605 maintainer2 = 'jrandom@example.com (J. Random Hacker)'
606 provides2 = {'cookie-factory': {
607 'interface': 'imap',
608 }}
609 requires2 = {'postal-service': {
610 'interface': 'envelope',
611 }}
612 options2 = {
613 'foobar': {
614 'type': 'str',
615 'default': 'baz',
616 'description': 'quxx',
617 },
618 }
619 files2 = {
620 'readme': {
621 'filename': 'README.md',
622 'subdir': '',
623 },
624 'config-changed': {
625 'filename': 'config-changed',
626 'subdir': 'hooks',
627 },
628 }
629 two_changes = [factory.makeChange(created=datetime(1998, 11, 30),
630 revno=32, message='Hi',
631 authors=['Baz <baz@example.org>'])]
632 two_id, two = self.makeCharm(owner='jrandom', revision=3,
633 series='warty', summary='Charm two',
634 name='name2', revno=13,
635 maintainer=maintainer2,
636 commit_message='message2',
637 provides=provides2,
638 requires=requires2, options=options2,
639 files=files2, subordinate=True,
640 changes=two_changes,
641 date_created=datetime(1996, 9, 28,
642 11, 9, 8))
643 response = self.get_response('charms')
644 self.assertEqual(200, response.status_code)
645 self.assertEqual('application/json', response.content_type)
646 result = response.json_body['result']
647 self.assertEqual(2, len(result))
648 one_url = make_store_url(1, get_address(one, short=True))
649 self.assertEqual({'owner': 'charmers',
650 'maintainer': {
651 'name': 'John Doe',
652 'email': 'jdoe@example.com',
653 },
654 'id': one_url[3:],
655 'categories': [],
656 'date_created': '1997-10-29T12:10:09',
657 'name': 'name1',
658 'url': one_url,
659 'description': one['description'],
660 'revision': 1,
661 'summary': "Remote terminal classroom over https",
662 'distro_series': 'precise',
663 'is_approved': True,
664 'rating_numerator': 0,
665 'rating_denominator': 0,
666 'code_source': {
667 'type': 'bzr',
668 'location':
669 'lp:~charmers/charms/precise/name1/trunk',
670 'revision': '12',
671 'last_log': 'maintainer',
672 'bugs_link':
673 'https://bugs.launchpad.net/charms/'
674 '+source/name1',
675 'revisions': [{
676 'revno': 37,
677 'message': 'Hello',
678 'date': '1999-12-31T00:00:00Z',
679 'authors': [
680 {
681 'name': 'Foo',
682 'email': 'foo@example.com'
683 },
684 {
685 'name': 'Bar',
686 'email': 'bar@example.com'
687 },
688 ],
689 }],
690 },
691 'relations': {
692 'provides': {
693 'website': {
694 'interface': 'https',
695 },
696 },
697 'requires': {
698 'database': {
699 'interface': 'mongodb',
700 },
701 },
702 },
703 'options': {
704 'script-interval': {
705 'type': 'int',
706 'default': 5,
707 'description':
708 'The interval between script runs.'
709 },
710 },
711 'files': ['README', 'hooks/install'],
712 'tested_providers': {},
713 'downloads_in_past_30_days': 0,
714 'is_subordinate': False,
715 }, result[0])
716 two_url = make_store_url(1, get_address(two, short=False))
717 self.assertEqual({'owner': 'jrandom',
718 'maintainer': {
719 'name': 'J. Random Hacker',
720 'email': 'jrandom@example.com',
721 },
722 'id': two_url[3:],
723 'categories': [],
724 'date_created': '1996-09-28T11:09:08',
725 'name': 'name2',
726 'url': two_url,
727 'description': two['description'],
728 'revision': 3,
729 'summary': 'Charm two',
730 'distro_series': 'warty',
731 'is_approved': False,
732 'rating_numerator': 0,
733 'rating_denominator': 0,
734 'code_source': {
735 'type': 'bzr',
736 'location':
737 'lp:~jrandom/charms/warty/name2/trunk',
738 'revision': '13',
739 'revisions': [{
740 'revno': 32,
741 'message': 'Hi',
742 'date': '1998-11-30T00:00:00Z',
743 'authors': [
744 {
745 'name': 'Baz',
746 'email': 'baz@example.org'
747 },
748 ],
749 }],
750 'last_log': 'message2',
751 'bugs_link':
752 'https://bugs.launchpad.net/charms/+source/name2'
753 },
754 'relations': {
755 'provides': provides2,
756 'requires': requires2,
757
758 },
759 'options': options2,
760 'files': ['README.md', 'hooks/config-changed'],
761 'tested_providers': {},
762 'downloads_in_past_30_days': 0,
763 'is_subordinate': True,
764 }, result[1])
765 self.assertNotEqual(one_id, two_id)
766 self.assertNotEqual(one_url, two_url)
767
768 def test_charms_accepts_category(self):
769 """Category is accepted (but ignored)."""
770 response = self.get_response('charms', category='asdf')
771 self.assertEqual(200, response.status_code)
772580
773581
774class TestAPI:582class TestAPI:
@@ -1151,9 +959,6 @@
1151 # Only the headers matter for a CORS preflight response.959 # Only the headers matter for a CORS preflight response.
1152 self.assertEqual(0, response.content_length)960 self.assertEqual(0, response.content_length)
1153961
1154
1155class TestAPI1(APITestBase, TestAPI, API1Mixin):
1156
1157 def test_charm_result_includes_charm_and_metadata(self):962 def test_charm_result_includes_charm_and_metadata(self):
1158 charm = factory.get_charm_json(promulgated=False)963 charm = factory.get_charm_json(promulgated=False)
1159 result = self.api_class._charm_result(charm, {}, {})964 result = self.api_class._charm_result(charm, {}, {})
@@ -1283,29 +1088,5 @@
1283 self.assertNotIn('icon', formatted)1088 self.assertNotIn('icon', formatted)
12841089
12851090
1286class TestAPI0(APITestBase, TestAPI):1091class TestAPI1(TestAPI, API1Mixin):
12871092 """Test API 1."""
1288 api_class = API0
1289
1290 @staticmethod
1291 def get_id(charm):
1292 return charm['id']
1293
1294 def test_charm_result_excludes_charm_and_metadata(self):
1295 charm = factory.get_charm_json(promulgated=False)
1296 result = self.api_class._charm_result(charm, {}, {})
1297 self.assertNotIn('metadata', result)
1298 self.assertNotIn('charm', result)
1299
1300 def test_format_charm_adds_icon(self):
1301 """If a charm provides an icon, it is added to the data returned
1302 by the API.
1303 """
1304 charmid, charm = factory.makeCharm(self.db)
1305 # The test data does not contain an icon.
1306 self.assertFalse('icon' in charm)
1307 formatted = self.api_class._format_charm(charm)
1308 self.assertFalse('icon' in formatted)
1309 charm['icon'] = 'fake'
1310 formatted = self.api_class._format_charm(charm)
1311 self.assertTrue('icon' in formatted)
13121093
=== modified file 'migrations/migrate.py'
--- migrations/migrate.py 2013-02-12 11:36:14 +0000
+++ migrations/migrate.py 2013-06-04 16:12:32 +0000
@@ -17,6 +17,7 @@
1717
18from charmworld.models import getconnection18from charmworld.models import getconnection
19from charmworld.models import getdb19from charmworld.models import getdb
20from charmworld.search import ElasticSearchClient
20from charmworld.utils import get_ini21from charmworld.utils import get_ini
2122
22SCRIPT_TEMPLATE = """23SCRIPT_TEMPLATE = """
@@ -152,7 +153,13 @@
152 """:returns: Latest version in Collection"""153 """:returns: Latest version in Collection"""
153 return self.version_indexes[-1] if len(self.version_indexes) > 0 else 0154 return self.version_indexes[-1] if len(self.version_indexes) > 0 else 0
154155
155 def upgrade(self, datastore, init):156 def run_migration(self, db, index_client, module_name):
157 module = imp.load_source(
158 module_name.strip('.py'),
159 join(self.versions_dir, module_name))
160 getattr(module, 'upgrade')(db, index_client)
161
162 def upgrade(self, datastore, index_client, init):
156 """Run `upgrade` methods for required version files.163 """Run `upgrade` methods for required version files.
157164
158 :param datastore: An instance of DataStore165 :param datastore: An instance of DataStore
@@ -176,10 +183,7 @@
176 # Let's get processing.183 # Let's get processing.
177 next_version = current_version + 1184 next_version = current_version + 1
178 module_name = self.versions[next_version]185 module_name = self.versions[next_version]
179 module = imp.load_source(186 self.run_migration(datastore.db, index_client, module_name)
180 module_name.strip('.py'),
181 join(self.versions_dir, module_name))
182 getattr(module, 'upgrade')(datastore.db)
183 current_version = next_version187 current_version = next_version
184 datastore.update_version(current_version)188 datastore.update_version(current_version)
185 return current_version189 return current_version
@@ -252,8 +256,9 @@
252 def upgrade(cls, ini, args):256 def upgrade(cls, ini, args):
253 """Upgrade the data store to the latest available migration."""257 """Upgrade the data store to the latest available migration."""
254 ds = cls.get_datastore(ini)258 ds = cls.get_datastore(ini)
259 index_client = ElasticSearchClient.from_settings(ini)
255 migrations = Versions(ini['migrations'])260 migrations = Versions(ini['migrations'])
256 new_version = migrations.upgrade(ds, args.init)261 new_version = migrations.upgrade(ds, index_client, args.init)
257262
258 if new_version is None:263 if new_version is None:
259 print 'There are no new migrations to apply'264 print 'There are no new migrations to apply'
260265
=== modified file 'migrations/test_migrate.py'
--- migrations/test_migrate.py 2013-02-12 18:57:02 +0000
+++ migrations/test_migrate.py 2013-06-04 16:12:32 +0000
@@ -6,10 +6,10 @@
6from os.path import exists6from os.path import exists
7import pymongo7import pymongo
8from tempfile import mkdtemp8from tempfile import mkdtemp
9from unittest import TestCase
109
11from charmworld.models import getconnection10from charmworld.models import getconnection
12from charmworld.models import getdb11from charmworld.models import getdb
12from charmworld.testing import TestCase
13from charmworld.testing.factory import random_string13from charmworld.testing.factory import random_string
14from charmworld.utils import get_ini14from charmworld.utils import get_ini
1515
@@ -155,16 +155,19 @@
155155
156 def test_upgrade_wo_migrations(self):156 def test_upgrade_wo_migrations(self):
157 """Upgrade runs silently if no migrations are created yet."""157 """Upgrade runs silently if no migrations are created yet."""
158 self.use_index_client()
158 self.ds.version_datastore()159 self.ds.version_datastore()
159 tmpdir = mkdtemp()160 tmpdir = mkdtemp()
160 versions = Versions(tmpdir)161 versions = Versions(tmpdir)
161 self.assertEqual(versions.upgrade(self.ds, False), None)162 self.assertEqual(versions.upgrade(self.ds, self.index_client, False),
163 None)
162164
163 def test_upgrade_succeeds(self):165 def test_upgrade_succeeds(self):
164 """upgrade will run the methods and we get a version of 4."""166 """upgrade will run the methods and we get a version of 4."""
165 self.ds.version_datastore()167 self.ds.version_datastore()
168 self.use_index_client()
166 # We should get 4 versions done. The upgrade returns 4169 # We should get 4 versions done. The upgrade returns 4
167 last_version = self.version.upgrade(self.ds, False)170 last_version = self.version.upgrade(self.ds, self.index_client, False)
168 self.assertEqual(4, last_version)171 self.assertEqual(4, last_version)
169172
170 # and we can query the db for the revision of 4173 # and we can query the db for the revision of 4
@@ -172,7 +175,8 @@
172175
173 def test_upgrade_succeeds_from_unversioned(self):176 def test_upgrade_succeeds_from_unversioned(self):
174 """Forcing init no an unversioned db will upgrade successfully."""177 """Forcing init no an unversioned db will upgrade successfully."""
175 last_version = self.version.upgrade(self.ds, True)178 self.use_index_client()
179 last_version = self.version.upgrade(self.ds, self.index_client, True)
176 self.assertEqual(4, last_version)180 self.assertEqual(4, last_version)
177181
178 # and we can query the db for the revision of 4182 # and we can query the db for the revision of 4
179183
=== modified file 'migrations/versions/001_load_qa_questions.py'
--- migrations/versions/001_load_qa_questions.py 2013-02-12 11:36:14 +0000
+++ migrations/versions/001_load_qa_questions.py 2013-06-04 16:12:32 +0000
@@ -9,7 +9,7 @@
9"""9"""
1010
1111
12def upgrade(db):12def upgrade(db, index_client):
13 """Complete this function with work to be done for the migration/update.13 """Complete this function with work to be done for the migration/update.
1414
15 db is the pymongo db instance for our datastore. Charms are in db.charms15 db is the pymongo db instance for our datastore. Charms are in db.charms
1616
=== modified file 'migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py'
--- migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py 2013-02-12 11:36:14 +0000
+++ migrations/versions/002_Escape_dots_dollar_signs_percent_signs_in_mongo_keys.py 2013-06-04 16:12:32 +0000
@@ -5,7 +5,7 @@
55
6from charmworld.utils import quote_yaml6from charmworld.utils import quote_yaml
77
8def upgrade(db):8def upgrade(db, index_client):
9 """Iterate all charm data and replace all dictionary keys containing9 """Iterate all charm data and replace all dictionary keys containing
10 a '.', '$' or '%' with an escaped variant.10 a '.', '$' or '%' with an escaped variant.
11 """11 """
1212
=== modified file 'migrations/versions/003_remove_provider_qa_questions.py'
--- migrations/versions/003_remove_provider_qa_questions.py 2013-04-23 19:23:17 +0000
+++ migrations/versions/003_remove_provider_qa_questions.py 2013-06-04 16:12:32 +0000
@@ -21,7 +21,7 @@
21]21]
2222
2323
24def upgrade(db):24def upgrade(db, index_client):
25 """Update each category of questions from new_category_data.25 """Update each category of questions from new_category_data.
2626
27 This update elaborates on the 001_* migration. Subsequent qa form27 This update elaborates on the 001_* migration. Subsequent qa form
2828
=== modified file 'migrations/versions/004_remove_charm_errors.py'
--- migrations/versions/004_remove_charm_errors.py 2013-05-27 19:02:19 +0000
+++ migrations/versions/004_remove_charm_errors.py 2013-06-04 16:12:32 +0000
@@ -1,2 +1,4 @@
1def upgrade(db):1# Copyright 2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3def upgrade(db, index_client):
2 db.drop_collection('charm-errors')4 db.drop_collection('charm-errors')
35
=== added file 'migrations/versions/005_delete_icon_field.py'
--- migrations/versions/005_delete_icon_field.py 1970-01-01 00:00:00 +0000
+++ migrations/versions/005_delete_icon_field.py 2013-06-04 16:12:32 +0000
@@ -0,0 +1,8 @@
1# Copyright 2013 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3from charmworld.search import reindex
4
5
6def upgrade(db, index_client):
7 db.charms.update({}, {'$unset': {'icon':''}})
8 reindex(index_client, list(db.charms.find({})))
09
=== modified file 'migrations/versions/tests/test_migrations.py'
--- migrations/versions/tests/test_migrations.py 2013-05-27 19:02:19 +0000
+++ migrations/versions/tests/test_migrations.py 2013-06-04 16:12:32 +0000
@@ -2,8 +2,15 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4import imp4import imp
5from charmworld.models import CharmSource
5from charmworld.testing import MongoTestBase6from charmworld.testing import MongoTestBase
6from charmworld.testing.factory import random_string7from charmworld.testing.factory import random_string
8from migrations.migrate import Versions
9
10
11def run_migration(db, index_client, module_name):
12 versions = Versions('migrations/versions/')
13 versions.run_migration(db, index_client, module_name)
714
815
9class TestMigration002(MongoTestBase):16class TestMigration002(MongoTestBase):
@@ -53,10 +60,18 @@
53class TestMigration004(MongoTestBase):60class TestMigration004(MongoTestBase):
5461
55 def test_migration(self):62 def test_migration(self):
63 self.use_index_client()
56 self.db.create_collection('charm-errors')64 self.db.create_collection('charm-errors')
57 module = imp.load_source(65 run_migration(self.db, self.index_client, '004_remove_charm_errors.py')
58 '004_remove_charm_errors',
59 'migrations/versions/'
60 '004_remove_charm_errors.py')
61 module.upgrade(self.db)
62 self.assertNotIn('charm-errors', self.db.collection_names())66 self.assertNotIn('charm-errors', self.db.collection_names())
67
68
69class TestMigration005(MongoTestBase):
70
71 def test_migration(self):
72 self.use_index_client()
73 source = CharmSource.from_request(self)
74 source.save({'_id': 'a', 'icon': 'asdf', 'asdf': 'asdf'})
75 run_migration(self.db, self.index_client, '005_delete_icon_field.py')
76 for charm in source._get_all('a'):
77 self.assertNotIn('icon', charm)

Subscribers

People subscribed via source and target branches