Merge lp:~benji/charmworld/remove-charm into lp:charmworld

Proposed by Benji York
Status: Merged
Approved by: Benji York
Approved revision: 468
Merged at revision: 465
Proposed branch: lp:~benji/charmworld/remove-charm
Merge into: lp:charmworld
Diff against target: 284 lines (+216/-1)
6 files modified
charmworld/models.py (+13/-0)
charmworld/scripts/remove_charm.py (+71/-0)
charmworld/scripts/tests/test_remove_charm.py (+119/-0)
charmworld/search.py (+1/-1)
charmworld/testing/factory.py (+11/-0)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~benji/charmworld/remove-charm
Reviewer Review Type Date Requested Status
Juju Gui Bot continuous-integration Approve
Benji York (community) Approve
Review via email: mp+195839@code.launchpad.net

Commit message

Add a script to remove a charm

https://codereview.appspot.com/29210043/

R=bac

Description of the change

Add a script to remove a charm

https://codereview.appspot.com/29210043/

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

Reviewers: mp+195839_code.launchpad.net,

Message:
Please take a look.

Description:
Add a script to remove a charm

https://code.launchpad.net/~benji/charmworld/remove-charm/+merge/195839

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/29210043/

Affected files (+218, -1 lines):
   A [revision details]
   M charmworld/models.py
   A charmworld/scripts/remove_charm.py
   A charmworld/scripts/tests/test_remove_charm.py
   M charmworld/search.py
   M charmworld/testing/factory.py
   M setup.py

Revision history for this message
Brad Crittenden (bac) wrote :

LGTM. Cannot do QA at this time.

https://codereview.appspot.com/29210043/diff/1/charmworld/scripts/tests/test_remove_charm.py
File charmworld/scripts/tests/test_remove_charm.py (right):

https://codereview.appspot.com/29210043/diff/1/charmworld/scripts/tests/test_remove_charm.py#newcode53
charmworld/scripts/tests/test_remove_charm.py:53: # A non-zero exit
status is set to indicate the error condition.
positive, non-zero

https://codereview.appspot.com/29210043/diff/1/charmworld/scripts/tests/test_remove_charm.py#newcode74
charmworld/scripts/tests/test_remove_charm.py:74: # Silence is a virtue
for sucessfully executing programs.
I love the editorial content.

https://codereview.appspot.com/29210043/diff/1/charmworld/search.py
File charmworld/search.py (right):

https://codereview.appspot.com/29210043/diff/1/charmworld/search.py#newcode102
charmworld/search.py:102: _BAD_CHARS = u'~()/[]^{}'
It is very odd that this caused the test failure you saw. The whole
point is a search with one of these characters would cause an exception.
  I wonder if you have a different version of ES installed that handles
it differently.

I'm confused why this doesn't cause the test you had trouble with
earlier to *always* fail.

https://codereview.appspot.com/29210043/

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

Search QA is ok. Search change just made it so that ~charmers works vs
produces an unparseable error. The ~ is dropped from the actual search
and it goes on with just 'charmers'.

https://codereview.appspot.com/29210043/

Revision history for this message
Benji York (benji) wrote :

Thanks for the review and QA guys!

https://codereview.appspot.com/29210043/diff/1/charmworld/scripts/tests/test_remove_charm.py
File charmworld/scripts/tests/test_remove_charm.py (right):

https://codereview.appspot.com/29210043/diff/1/charmworld/scripts/tests/test_remove_charm.py#newcode53
charmworld/scripts/tests/test_remove_charm.py:53: # A non-zero exit
status is set to indicate the error condition.
On 2013/11/19 19:05:48, bac wrote:
> positive, non-zero

Since POSIX requires it to be an "unsigned decimal integer", all
non-zero values are positive. ;)

https://codereview.appspot.com/29210043/

lp:~benji/charmworld/remove-charm updated
468. By Benji York

remove silly, ineffective argument

Revision history for this message
Benji York (benji) wrote :

Brad reviewed this.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/models.py'
2--- charmworld/models.py 2013-11-13 19:54:55 +0000
3+++ charmworld/models.py 2013-11-19 20:26:17 +0000
4@@ -228,6 +228,7 @@
5
6
7 def construct_charm_id(charm_data, use_revision=True):
8+ """Build the ID of a charm from a mapping of that charm's attributes."""
9 elements = [charm_data['owner'], charm_data['series'], charm_data['name']]
10 if use_revision:
11 elements.append('%d' % charm_data['store_data']['revision'])
12@@ -1288,6 +1289,18 @@
13 def create_collection(self):
14 self.collection.database.create_collection(self.collection.name)
15
16+ @staticmethod
17+ def make_all_revisions_query(charm_data):
18+ return {
19+ 'owner': charm_data['owner'],
20+ 'series': charm_data['series'],
21+ 'name': charm_data['name'],
22+ }
23+
24+ def find_all_revisions(self, charm_data):
25+ query = self.make_all_revisions_query(charm_data)
26+ return list(self.collection.find(query))
27+
28
29 def get_basket_info(revno, **data):
30 owner_segment = '~%s' % data['owner']
31
32=== added file 'charmworld/scripts/remove_charm.py'
33--- charmworld/scripts/remove_charm.py 1970-01-01 00:00:00 +0000
34+++ charmworld/scripts/remove_charm.py 2013-11-19 20:26:17 +0000
35@@ -0,0 +1,71 @@
36+# Copyright 2012, 2013 Canonical Ltd. This software is licensed under the
37+# GNU Affero General Public License version 3 (see the file LICENSE).
38+
39+from __future__ import print_function
40+
41+__metaclass__ = type
42+
43+import sys
44+from pyelasticsearch.exceptions import ElasticHttpNotFoundError
45+from charmworld.utils import get_ini
46+from charmworld.models import (
47+ CharmSource,
48+ FeaturedSource,
49+ getconnection,
50+ getdb,
51+)
52+from charmworld.search import ElasticSearchClient
53+
54+
55+CHARM_ID_MESSAGE = (
56+ 'Please provide a charm ID, including revision. E.g., '
57+ '~charmers/precise/apache2/14')
58+
59+
60+def main(argv, stderr=sys.stderr, db=None, index_client=None):
61+ """The main function for the server start timestamp removal script."""
62+ if len(argv) != 2:
63+ print(
64+ 'Please provide the ID of the charm you wish to remove.',
65+ file=stderr)
66+ return 1
67+
68+ charm_id = argv[1]
69+
70+ if db is None:
71+ settings = get_ini()
72+ connection = getconnection(settings)
73+ db = getdb(connection, settings.get('mongo.database'))
74+
75+ # If the specified charm does not exist, tell the user.
76+ charm_query = {'_id': charm_id}
77+ charm_data = db.charms.find_one(charm_query)
78+ if charm_data is None:
79+ print('Unknown charm ID: %r' % charm_id, file=stderr)
80+ print(CHARM_ID_MESSAGE, file=stderr)
81+ return 1
82+
83+ if index_client is None:
84+ index_client = ElasticSearchClient.from_settings(settings)
85+
86+ # The charm needs to be removed from the search engine.
87+ try:
88+ index_client.delete_charm(charm_data)
89+ except ElasticHttpNotFoundError:
90+ # It is fine if the charm was not indexed.
91+ pass
92+
93+ # If the charm is featured, we want to unfeature it.
94+ source = FeaturedSource.from_db(db)
95+ source.clear_featured(charm_data, 'charm')
96+
97+ # Finally, remove all the revisions of the charm document from the
98+ # database.
99+ charm_query = CharmSource.make_all_revisions_query(charm_data)
100+ db.charms.remove(charm_query)
101+ return 0
102+
103+
104+def entry_point():
105+ """Script entry point for removing a server's start up timestamp."""
106+ sys.exit(main(sys.argv))
107
108=== added file 'charmworld/scripts/tests/test_remove_charm.py'
109--- charmworld/scripts/tests/test_remove_charm.py 1970-01-01 00:00:00 +0000
110+++ charmworld/scripts/tests/test_remove_charm.py 2013-11-19 20:26:17 +0000
111@@ -0,0 +1,119 @@
112+# Copyright 2012, 2013, Canonical Ltd. This software is licensed under the
113+# GNU Affero General Public License version 3 (see the file LICENSE.txt).
114+
115+from cStringIO import StringIO
116+import os
117+
118+from charmworld.models import (
119+ Charm,
120+ CharmSource,
121+ construct_charm_id,
122+ FeaturedSource,
123+)
124+from charmworld.scripts import remove_charm
125+from charmworld.testing import MongoTestBase
126+from charmworld.testing.factory import make_charm
127+from charmworld.utils import project_root
128+
129+
130+class RemoveCharmScriptTests(MongoTestBase):
131+
132+ def setUp(self):
133+ super(RemoveCharmScriptTests, self).setUp()
134+ # This is a lot of work just to create a new revision of a charm.
135+ older_charm = make_charm(self.db)
136+ newer_charm_data = older_charm._representation
137+ newer_charm_data['store_data']['revision'] += 1
138+ newer_charm_data['_id'] = construct_charm_id(newer_charm_data)
139+ self.db.charms.insert(newer_charm_data)
140+ self.charm = Charm(newer_charm_data)
141+ # As a convienience we will create a stderr stand-in here.
142+ self.stderr = StringIO()
143+ # Be paranoid about the charms actually existing in the database.
144+ assert self.db.charms.find_one({'_id': older_charm._id})
145+ assert self.db.charms.find_one({'_id': self.charm._id})
146+
147+ def is_indexed(self, id_):
148+ return self.index_client.get(id_) is not None
149+
150+ def test_entry_point_exists(self):
151+ # The entry point is wrapped in a script by setuptools. We want to be
152+ # sure the entrypoint actually exists.
153+ self.assertIn(
154+ 'remove-charm = charmworld.scripts.remove_charm:entry_point\n',
155+ open(os.path.join(project_root, 'setup.py')).read())
156+
157+ def test_script_chides_you_for_not_providing_an_id(self):
158+ # If no charm ID is provided on the command line the script prints a
159+ # helpful message.
160+ exit_status = remove_charm.main([], self.stderr)
161+ self.assertEqual(
162+ self.stderr.getvalue(),
163+ 'Please provide the ID of the charm you wish to remove.\n')
164+ # A non-zero exit status is set to indicate the error condition.
165+ self.assertGreater(exit_status, 0)
166+
167+ def test_script_reports_unkown_charm_ids(self):
168+ # The script will generate an error message if the given charm ID does
169+ # not exist in the DB.
170+ exit_status = remove_charm.main(['', 'not a charm ID'], self.stderr)
171+ self.assertIn(
172+ "Unknown charm ID: 'not a charm ID'\n",
173+ self.stderr.getvalue())
174+ # The user is also given a hint as to how the ID is structured.
175+ self.assertIn(remove_charm.CHARM_ID_MESSAGE, self.stderr.getvalue())
176+ # A non-zero exit status is set to indicate the error condition.
177+ self.assertGreater(exit_status, 0)
178+
179+ def test_script_removes_charm_from_mongodb(self):
180+ # The script removes the charm with the provided ID from the database.
181+ # ...we can invoke the script to delete it.
182+ exit_status = remove_charm.main(['', self.charm._id], self.stderr)
183+ # Lo and behold, the charm is actually gone.
184+ self.assertFalse(self.db.charms.find_one({'_id': self.charm._id}))
185+ # Silence is a virtue for sucessfully executing programs.
186+ self.assertEqual(self.stderr.getvalue(), '')
187+ # The exit status signals success.
188+ self.assertEqual(exit_status, 0)
189+
190+ def test_script_removes_charm_from_featuerd(self):
191+ # If a charm is featured, its featured status is revoked by the script.
192+ source = FeaturedSource.from_db(self.db)
193+ charm_data = self.charm._representation
194+ source.set_featured(charm_data, 'charm')
195+ exit_status = remove_charm.main(['', self.charm._id], self.stderr)
196+ self.assertFalse(source.is_featured(charm_data, 'charm'))
197+ # The exit status signals success.
198+ self.assertEqual(exit_status, 0)
199+
200+ def test_script_removes_charm_from_elastic_search(self):
201+ # The charm needs to be indexed so the script can remove it.
202+ self.use_index_client()
203+ charm_data = self.charm._representation
204+ self.index_client.index_charm(charm_data)
205+ # Ensure the charm actually got indexed.
206+ assert self.is_indexed(charm_data['_id'])
207+ # Removing a charm should also remove it from the search engine.
208+ exit_status = remove_charm.main(
209+ ['', self.charm._id], self.stderr, index_client=self.index_client)
210+ # The script removed the charm from the index.
211+ self.assertFalse(self.is_indexed(charm_data['_id']))
212+ # The exit status signals success.
213+ self.assertEqual(exit_status, 0)
214+
215+ def test_old_revisions_of_the_charm_are_removed_too(self):
216+ # Requesting the removal of a charm results in all revisions of that
217+ # charm being removed.
218+ charm_data = self.charm._representation
219+ # This can't be the best way to find all the revisions of a particular
220+ # charm.
221+ source = CharmSource(self.db, index_client='fake')
222+ # There should exist more than one revision of the charm in the
223+ # database.
224+ assert len(source.find_all_revisions(charm_data)) > 1
225+ # Run the script.
226+ exit_status = remove_charm.main(['', self.charm._id], self.stderr)
227+ # The script removed all the charm revisions from the database.
228+ self.assertEqual(len(source.find_all_revisions(charm_data)), 0)
229+ # The exit status signals success.
230+ self.assertEqual(exit_status, 0)
231
232=== modified file 'charmworld/search.py'
233--- charmworld/search.py 2013-11-15 20:00:35 +0000
234+++ charmworld/search.py 2013-11-19 20:26:17 +0000
235@@ -99,7 +99,7 @@
236 e)
237
238
239-_BAD_CHARS = u'()/[]^{}'
240+_BAD_CHARS = u'~()/[]^{}'
241 _BAD_CHAR_TABLE = [
242 unichr(c) if unichr(c) not in _BAD_CHARS else u' ' for c in range(128)]
243 _BAD_CHAR_TABLE = u''.join(_BAD_CHAR_TABLE)
244
245=== modified file 'charmworld/testing/factory.py'
246--- charmworld/testing/factory.py 2013-11-08 14:59:51 +0000
247+++ charmworld/testing/factory.py 2013-11-19 20:26:17 +0000
248@@ -30,6 +30,7 @@
249 )
250 from charmworld.models import (
251 Bundle,
252+ Charm,
253 CharmFileSet,
254 construct_charm_id,
255 getfs,
256@@ -317,6 +318,16 @@
257 return record_id, charm
258
259
260+def make_charm(db, **kwargs):
261+ """Create a new charm and add it to the database.
262+
263+ This function returns a charm model.
264+ """
265+ charm_data = get_charm_json(**kwargs)
266+ db.charms.insert(charm_data)
267+ return Charm(charm_data)
268+
269+
270 def get_bundle_data(name=None, owner=None, basket_with_rev=None,
271 series='precise', title='', description='',
272 services=dict(), relations=dict(), promulgated=False,
273
274=== modified file 'setup.py'
275--- setup.py 2013-11-15 16:58:32 +0000
276+++ setup.py 2013-11-19 20:26:17 +0000
277@@ -41,6 +41,7 @@
278 sync-index = charmworld.models:sync_index
279 remove-start-time-entry = \
280 charmworld.scripts.remove_server_start_time:entry_point
281+ remove-charm = charmworld.scripts.remove_charm:entry_point
282 [beaker.backends]
283 mongodb = mongodb_beaker:MongoDBNamespaceManager
284 """,

Subscribers

People subscribed via source and target branches

to all changes: