Merge lp:~fwereade/pyjuju/check-latest-formulas into lp:pyjuju
- check-latest-formulas
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Gustavo Niemeyer | ||||
Approved revision: | 386 | ||||
Merged at revision: | 387 | ||||
Proposed branch: | lp:~fwereade/pyjuju/check-latest-formulas | ||||
Merge into: | lp:pyjuju | ||||
Prerequisite: | lp:~fwereade/pyjuju/use-remote-formulas | ||||
Diff against target: |
799 lines (+377/-225) 6 files modified
juju/charm/repository.py (+63/-33) juju/charm/tests/test_repository.py (+301/-187) juju/charm/tests/test_url.py (+1/-0) juju/charm/url.py (+4/-0) juju/control/deploy.py (+4/-3) juju/control/upgrade_charm.py (+4/-2) |
||||
To merge this branch: | bzr merge lp:~fwereade/pyjuju/check-latest-formulas | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Approve | ||
Review via email: mp+77357@code.launchpad.net |
Commit message
Description of the change
Use the /latest?
William Reade (fwereade) wrote : | # |
William Reade (fwereade) wrote : | # |
Technically, I suppose, I'm demanding that a charm-url-
Gustavo Niemeyer (niemeyer) wrote : | # |
[1]
+ def latest(self, charm_url):
+ d = self.find(
+ d.addCallback(
+ return d
Eventually the method signature should be closer to the API in
the spec, since we don't want to do a query for every single
charm we have to find out the revision for.
I'm fine to move this forward as an initial implementation, though.
[2]
+ d.addCallback(
This seems clearer, avoids the import, and is shorter:
d.addCallba
[3]
+ url = "%s/%s?charms=%s" % (self.url_base, query, charm_url)
charm_url has to be url-escaped here.
[4]
+ def find(self, charm_url):
+ revision = yield self.latest(
+ charm_url = charm_url.
+ charm = yield self._get_
Why is this hardcoding getting the latest formula at all times?
If charm_url has a revision, there's no reason not to respect it
and proceed exactly the same way, I think.
Also, the original version of this function worked with a single
roundtrip to the server. This one does three of them (one for
the revision, one for the charm, and another one for the sha256).
I think we should tweak the server API so we can get both the
revision and the sha256 at the same time. Will think a bit about
this and post back later today.
William Reade (fwereade) wrote : | # |
Thanks :).
> [1]
>
> + def latest(self, charm_url):
> + d = self.find(
> + d.addCallback(
> + return d
>
> Eventually the method signature should be closer to the API in
> the spec, since we don't want to do a query for every single
> charm we have to find out the revision for.
>
> I'm fine to move this forward as an initial implementation, though.
Agree, but am trying to avoid speculative generality in the plastic bits.
> [2]
>
> + d.addCallback(
>
> This seems clearer, avoids the import, and is shorter:
>
> d.addCallback(
Awww, ok.
> [3]
>
> + url = "%s/%s?charms=%s" % (self.url_base, query, charm_url)
>
> charm_url has to be url-escaped here.
* fwereade hangs head in shame
> [4]
>
> + def find(self, charm_url):
> + revision = yield self.latest(
> + charm_url = charm_url.
> + charm = yield self._get_
>
> Why is this hardcoding getting the latest formula at all times?
>
> If charm_url has a revision, there's no reason not to respect it
> and proceed exactly the same way, I think.
Sounds good, I'll fix it.
> Also, the original version of this function worked with a single
> roundtrip to the server. This one does three of them (one for
> the revision, one for the charm, and another one for the sha256).
> I think we should tweak the server API so we can get both the
> revision and the sha256 at the same time. Will think a bit about
> this and post back later today.
A combined method in the server API would be great, we'd just need one roundtrip on cache hits and two on misses. I'll go ahead assuming one exists and make sure we agree what it should be called before next MP :).
Gustavo Niemeyer (niemeyer) wrote : | # |
[4]
So, here is an idea to sort out the multiple-request problem: let's
replace the /latest entry by one called /charm-info that is called
the same way (/charm-
like this instead:
{charm_url: {"sha256": s, "revision": n}, ...}
How does that sound?
William Reade (fwereade) wrote : | # |
> [4]
>
> So, here is an idea to sort out the multiple-request problem: let's
> replace the /latest entry by one called /charm-info that is called
> the same way (/charm-
> like this instead:
>
> {charm_url: {"sha256": s, "revision": n}, ...}
>
> How does that sound?
Perfect.
- 386. By William Reade
-
merge trunk
Gustavo Niemeyer (niemeyer) wrote : | # |
This is looking great. LGTM, considering just a couple of minors:
[5]
+ try:
+ data = yield getPage(url)
+ returnValue(
+ except (Error, KeyError, ValueError, TypeError):
+ raise CharmNotFound(
Hmmm.. do we really want TypeError and ValueError in this list?
My gut feeling when looking at this is that they would be bugs,
but maybe you have something else in mind.
[6]
+ assert charm.metadata.
+ assert charm.get_sha256() == info["sha256"], "bad bundle hash"
The first seems like a good fit for an assertion, but the bottom one
is a plausible runtime error that deserves a proper exception and
a nice error message to help the user.
William Reade (fwereade) wrote : | # |
[5]
They're intended to guard against bugs in the charm store, really, but I take your point. Hmm. It would certainly be clearer if I assumed that CS will either give me what I asked for, or Error out; can't guard against everything, after all, and the above is indeed potentially masking local bugs. Yep, I'm convinced.
[6]
Sounds good. I'll make sure I delete the bad cache entry as well, so there's some chance of a retry actually working.
- 387. By William Reade
-
address review points
- 388. By William Reade
-
merge trunk
Preview Diff
1 | === modified file 'juju/charm/repository.py' |
2 | --- juju/charm/repository.py 2011-10-03 20:04:45 +0000 |
3 | +++ juju/charm/repository.py 2011-10-05 10:00:46 +0000 |
4 | @@ -1,9 +1,11 @@ |
5 | +import json |
6 | import os |
7 | import tempfile |
8 | +import urllib |
9 | import yaml |
10 | |
11 | from twisted.internet.defer import fail, inlineCallbacks, returnValue, succeed |
12 | -from twisted.web.client import downloadPage |
13 | +from twisted.web.client import downloadPage, getPage |
14 | from twisted.web.error import Error |
15 | |
16 | from juju.charm.provider import get_charm_from_path |
17 | @@ -21,6 +23,11 @@ |
18 | pass |
19 | |
20 | |
21 | +def _cache_key(charm_url): |
22 | + charm_url.assert_revision() |
23 | + return under.quote("%s.charm" % charm_url) |
24 | + |
25 | + |
26 | class LocalCharmRepository(object): |
27 | """Charm repository in a local directory.""" |
28 | |
29 | @@ -50,11 +57,12 @@ |
30 | recent one (greatest revision) will be returned. |
31 | """ |
32 | assert charm_url.collection.schema == "local", "schema mismatch" |
33 | - assert charm_url.revision is None, "find-by-revision not supported yet" |
34 | |
35 | latest = None |
36 | for charm in self._collection(charm_url.collection): |
37 | if charm.metadata.name == charm_url.name: |
38 | + if charm.metadata.revision == charm_url.revision: |
39 | + return succeed(charm) |
40 | if (latest is None or |
41 | latest.metadata.revision < charm.metadata.revision): |
42 | latest = charm |
43 | @@ -64,6 +72,11 @@ |
44 | |
45 | return succeed(latest) |
46 | |
47 | + def latest(self, charm_url): |
48 | + d = self.find(charm_url.with_revision(None)) |
49 | + d.addCallback(lambda c: c.metadata.revision) |
50 | + return d |
51 | + |
52 | |
53 | class RemoteCharmRepository(object): |
54 | |
55 | @@ -75,42 +88,60 @@ |
56 | self.cache_path = cache_path |
57 | |
58 | @inlineCallbacks |
59 | - def _download(self, url): |
60 | + def _get_info(self, charm_url): |
61 | + url = "%s/charm-info?charms=%s" % ( |
62 | + self.url_base, urllib.quote(str(charm_url))) |
63 | + try: |
64 | + data = yield getPage(url) |
65 | + returnValue(json.loads(data)[str(charm_url)]) |
66 | + except Error: |
67 | + raise CharmNotFound(self.url_base, charm_url) |
68 | + |
69 | + @inlineCallbacks |
70 | + def _download(self, charm_url, cache_path): |
71 | + url = "%s/charm/%s" % (self.url_base, urllib.quote(charm_url.path)) |
72 | downloads = os.path.join(self.cache_path, "downloads") |
73 | _makedirs(downloads) |
74 | f = tempfile.NamedTemporaryFile( |
75 | - prefix=under.quote(url), suffix=".charm", dir=downloads, |
76 | + prefix=_cache_key(charm_url), suffix=".part", dir=downloads, |
77 | delete=False) |
78 | f.close() |
79 | - completed_path = f.name |
80 | - downloading_path = completed_path + ".part" |
81 | - yield downloadPage(url, downloading_path) |
82 | - os.rename(downloading_path, completed_path) |
83 | - returnValue(completed_path) |
84 | - |
85 | - def _cache(self, charm_url_base, temp_charm_path): |
86 | - temp_charm = get_charm_from_path(temp_charm_path) |
87 | - revision = temp_charm.metadata.revision |
88 | - charm_key = under.quote( |
89 | - "%s.charm" % (charm_url_base.with_revision(revision))) |
90 | - charm_path = os.path.join(self.cache_path, charm_key) |
91 | - _makedirs(self.cache_path) |
92 | - os.rename(temp_charm_path, charm_path) |
93 | - return get_charm_from_path(charm_path) |
94 | - |
95 | - @inlineCallbacks |
96 | - def find(self, charm_url): |
97 | - assert charm_url.revision is None, "find-by-revision not supported yet" |
98 | - _, path = str(charm_url).split(":", 1) |
99 | - url = "%s/charm/%s" % (self.url_base, path) |
100 | + downloading_path = f.name |
101 | try: |
102 | - temp_charm_path = yield self._download(url) |
103 | + yield downloadPage(url, downloading_path) |
104 | except Error: |
105 | raise CharmNotFound(self.url_base, charm_url) |
106 | - returnValue(self._cache(charm_url, temp_charm_path)) |
107 | - |
108 | - |
109 | -@inlineCallbacks |
110 | + os.rename(downloading_path, cache_path) |
111 | + |
112 | + @inlineCallbacks |
113 | + def find(self, charm_url): |
114 | + info = yield self._get_info(charm_url) |
115 | + revision = info["revision"] |
116 | + if charm_url.revision is None: |
117 | + charm_url = charm_url.with_revision(revision) |
118 | + else: |
119 | + assert revision == charm_url.revision, "bad url revision" |
120 | + |
121 | + cache_path = os.path.join(self.cache_path, _cache_key(charm_url)) |
122 | + cached = os.path.exists(cache_path) |
123 | + if not cached: |
124 | + yield self._download(charm_url, cache_path) |
125 | + charm = get_charm_from_path(cache_path) |
126 | + |
127 | + assert charm.metadata.revision == revision, "bad charm revision" |
128 | + if charm.get_sha256() != info["sha256"]: |
129 | + os.remove(cache_path) |
130 | + name = "%s (%s)" % ( |
131 | + charm_url, "cached" if cached else "downloaded") |
132 | + raise CharmError(name, "SHA256 mismatch") |
133 | + returnValue(charm) |
134 | + |
135 | + @inlineCallbacks |
136 | + def latest(self, charm_url): |
137 | + info = yield self._get_info(charm_url.with_revision(None)) |
138 | + returnValue(info["revision"]) |
139 | + |
140 | + |
141 | def resolve(vague_name, repository_path, default_series): |
142 | """Get a Charm and associated identifying information |
143 | |
144 | @@ -125,7 +156,7 @@ |
145 | :param str default_series: the Ubuntu series to insert when `charm_name` is |
146 | inadequately specified. |
147 | |
148 | - :return: a tuple of a :class:`CharmURL` and a |
149 | + :return: a tuple of a :class:`juju.charm.url.CharmURL` and a |
150 | :class:`juju.charm.base.CharmBase` subclass, which together contain |
151 | both the charm's data and all information necessary to specify its |
152 | source. |
153 | @@ -135,5 +166,4 @@ |
154 | repo = LocalCharmRepository(repository_path) |
155 | elif url.collection.schema == "cs": |
156 | repo = RemoteCharmRepository("https://store.juju.ubuntu.com") |
157 | - charm = yield repo.find(url) |
158 | - returnValue((url, charm)) |
159 | + return repo, url |
160 | |
161 | === modified file 'juju/charm/tests/test_repository.py' |
162 | --- juju/charm/tests/test_repository.py 2011-09-30 10:32:01 +0000 |
163 | +++ juju/charm/tests/test_repository.py 2011-10-05 10:00:46 +0000 |
164 | @@ -1,3 +1,4 @@ |
165 | +import json |
166 | import os |
167 | import inspect |
168 | import shutil |
169 | @@ -12,6 +13,7 @@ |
170 | from juju.charm.repository import ( |
171 | LocalCharmRepository, RemoteCharmRepository, resolve) |
172 | from juju.charm.url import CharmURL |
173 | +from juju.errors import CharmError |
174 | from juju.lib import under |
175 | |
176 | from juju.charm import tests |
177 | @@ -61,25 +63,37 @@ |
178 | return CharmURL.parse("local:series/" + name) |
179 | |
180 | @inlineCallbacks |
181 | - def test_find_charm_by_name_which_is_unbundled(self): |
182 | - charm = yield self.repository1.find(self.charm_url("sample")) |
183 | - self.assertTrue(charm) |
184 | - self.assertTrue(charm.metadata) |
185 | - |
186 | - def test_find_charm_ignores_unknown_files(self): |
187 | + def assert_not_there(self, name): |
188 | + url = self.charm_url(name) |
189 | + msg = "Charm 'local:series/%s' not found in repository %s" % ( |
190 | + name, self.unbundled_repo_path) |
191 | + err = yield self.assertFailure( |
192 | + self.repository1.find(url), CharmNotFound) |
193 | + self.assertEquals(str(err), msg) |
194 | + err = yield self.assertFailure( |
195 | + self.repository1.latest(url), CharmNotFound) |
196 | + self.assertEquals(str(err), msg) |
197 | + |
198 | + def test_find_inappropriate_url(self): |
199 | + url = CharmURL.parse("cs:foo/bar") |
200 | + err = self.assertRaises(AssertionError, self.repository1.find, url) |
201 | + self.assertEquals(str(err), "schema mismatch") |
202 | + |
203 | + def test_completely_missing(self): |
204 | + return self.assert_not_there("zebra") |
205 | + |
206 | + def test_unkown_files_ignored(self): |
207 | self.makeFile( |
208 | "Foobar", |
209 | path=os.path.join(self.repository1.path, "series", "zebra")) |
210 | - return self.assertFailure( |
211 | - self.repository1.find(self.charm_url("zebra")), CharmNotFound) |
212 | + return self.assert_not_there("zebra") |
213 | |
214 | - def test_find_charm_ignores_unknown_directories(self): |
215 | + def test_unknown_directories_ignored(self): |
216 | self.makeDir( |
217 | path=os.path.join(self.repository1.path, "series", "zebra")) |
218 | - return self.assertFailure( |
219 | - self.repository1.find(self.charm_url("zebra")), CharmNotFound) |
220 | + return self.assert_not_there("zebra") |
221 | |
222 | - def test_find_charm_ignores_broken_charms(self): |
223 | + def test_broken_charms_ignored(self): |
224 | charm_path = self.makeDir( |
225 | path=os.path.join(self.repository1.path, "series", "zebra")) |
226 | fh = open(os.path.join(charm_path, "metadata.yaml"), "w+") |
227 | @@ -90,45 +104,27 @@ |
228 | revision: 0 |
229 | summary: hola""") |
230 | fh.close() |
231 | - |
232 | - return self.assertFailure( |
233 | - self.repository1.find(self.charm_url("zebra")), CharmNotFound) |
234 | - |
235 | - @inlineCallbacks |
236 | - def test_find_charm_by_name_which_is_bundled(self): |
237 | - charm = yield self.repository2.find(self.charm_url("sample")) |
238 | - self.assertTrue(charm) |
239 | - self.assertTrue(charm.metadata) |
240 | - |
241 | - @inlineCallbacks |
242 | - def test_find_charm_by_name_fails_with_bad_series(self): |
243 | - error = yield self.assertFailure( |
244 | - self.repository1.find(CharmURL.parse("local:missing/charm")), |
245 | - CharmNotFound) |
246 | - self.assertEquals( |
247 | - str(error), |
248 | - "Charm 'local:missing/charm' not found in repository %s" |
249 | - % self.unbundled_repo_path) |
250 | - |
251 | - def test_find_charm_by_name_fails(self): |
252 | - d = self.assertFailure( |
253 | - self.repository1.find(self.charm_url("missing")), CharmNotFound) |
254 | - |
255 | - def verify(error): |
256 | - self.assertEquals( |
257 | - str(error), |
258 | - "Charm 'local:series/missing' not found in repository %s" |
259 | - % self.unbundled_repo_path) |
260 | - d.addCallback(verify) |
261 | - return d |
262 | - |
263 | - @inlineCallbacks |
264 | - def test_find_charm_with_multiple_versions_returns_latest(self): |
265 | - # Copy the repository out of the codebase, so that we can hack it. |
266 | - charm = yield self.repository1.find(self.charm_url("sample")) |
267 | - self.assertEquals(charm.metadata.revision, 2) |
268 | - |
269 | - # Invert the "latest" logic, to ensure it's not just a coincidence. |
270 | + return self.assert_not_there("zebra") |
271 | + |
272 | + def assert_there(self, name, repo, revision, latest_revision=None): |
273 | + url = self.charm_url(name) |
274 | + charm = yield repo.find(url) |
275 | + self.assertEquals(charm.metadata.revision, revision) |
276 | + latest = yield repo.latest(url) |
277 | + self.assertEquals(latest, latest_revision or revision) |
278 | + |
279 | + def test_success_unbundled(self): |
280 | + return self.assert_there("sample", self.repository1, 2) |
281 | + return self.assert_there("sample-2", self.repository1, 2) |
282 | + |
283 | + def test_success_bundled(self): |
284 | + return self.assert_there("sample", self.repository2, 2) |
285 | + return self.assert_there("sample-2", self.repository2, 2) |
286 | + |
287 | + @inlineCallbacks |
288 | + def test_no_revision_gets_latest(self): |
289 | + yield self.assert_there("sample", self.repository1, 2) |
290 | + |
291 | file = open(os.path.join( |
292 | self.repository1.path, "series/old/metadata.yaml"), "rw+") |
293 | data = yaml.load(file.read()) |
294 | @@ -137,18 +133,8 @@ |
295 | file.write(yaml.dump(data)) |
296 | file.close() |
297 | |
298 | - charm = yield self.repository1.find(self.charm_url("sample")) |
299 | - self.assertEquals(charm.metadata.revision, 3) |
300 | - |
301 | - def test_find_inappropriate_url(self): |
302 | - url = CharmURL.parse("cs:foo/bar") |
303 | - err = self.assertRaises(AssertionError, self.repository1.find, url) |
304 | - self.assertEquals(str(err), "schema mismatch") |
305 | - |
306 | - def test_find_with_revision(self): |
307 | - url = CharmURL.parse("local:foo/bar-1") |
308 | - err = self.assertRaises(AssertionError, self.repository1.find, url) |
309 | - self.assertEquals(str(err), "find-by-revision not supported yet") |
310 | + yield self.assert_there("sample", self.repository1, 3) |
311 | + yield self.assert_there("sample-2", self.repository1, 2, 3) |
312 | |
313 | |
314 | class RemoteRepositoryTest(RepositoryTestBase): |
315 | @@ -157,142 +143,270 @@ |
316 | super(RemoteRepositoryTest, self).setUp() |
317 | self.cache_path = os.path.join( |
318 | tempfile.mkdtemp(), "notexistyet") |
319 | + self.download_path = os.path.join(self.cache_path, "downloads") |
320 | |
321 | def delete(): |
322 | if os.path.exists(self.cache_path): |
323 | shutil.rmtree(self.cache_path) |
324 | self.addCleanup(delete) |
325 | |
326 | + self.charm = CharmDirectory( |
327 | + os.path.join(self.unbundled_repo_path, "series", "dummy")) |
328 | + with open(self.charm.as_bundle().path, "rb") as f: |
329 | + self.bundle_data = f.read() |
330 | + self.sha256 = self.charm.as_bundle().get_sha256() |
331 | + self.getPage = self.mocker.replace("twisted.web.client.getPage") |
332 | + self.downloadPage = self.mocker.replace( |
333 | + "twisted.web.client.downloadPage") |
334 | + |
335 | def repo(self, url_base): |
336 | return RemoteCharmRepository(url_base, self.cache_path) |
337 | |
338 | - @inlineCallbacks |
339 | - def assert_find(self, dns_name, url_str, expect_url): |
340 | - src_charm = CharmDirectory( |
341 | - os.path.join(self.unbundled_repo_path, "series", "dummy")) |
342 | - with open(src_charm.as_bundle().path, "rb") as f: |
343 | - bundle_data = f.read() |
344 | - |
345 | - download_dir = os.path.join(self.cache_path, "downloads") |
346 | - |
347 | - downloadPage = self.mocker.replace("twisted.web.client.downloadPage") |
348 | - downloadPage(expect_url, ANY) |
349 | - |
350 | - def download(_, temp_path): |
351 | - self.assertTrue(temp_path.startswith(download_dir)) |
352 | - with open(temp_path, "wb") as f: |
353 | - f.write(bundle_data) |
354 | + def cache_location(self, url_str, revision): |
355 | + charm_url = CharmURL.parse(url_str) |
356 | + cache_key = under.quote( |
357 | + "%s.charm" % (charm_url.with_revision(revision))) |
358 | + return os.path.join(self.cache_path, cache_key) |
359 | + |
360 | + def charm_info(self, url_str, revision): |
361 | + return json.dumps({ |
362 | + url_str: {"revision": revision, "sha256": self.sha256}}) |
363 | + |
364 | + def mock_charm_info(self, url, result): |
365 | + self.getPage(url) |
366 | + self.mocker.result(result) |
367 | + |
368 | + def mock_download(self, url, error=None): |
369 | + self.downloadPage(url, ANY) |
370 | + if error: |
371 | + return self.mocker.result(fail(error)) |
372 | + |
373 | + def download(_, path): |
374 | + self.assertTrue(path.startswith(self.download_path)) |
375 | + with open(path, "wb") as f: |
376 | + f.write(self.bundle_data) |
377 | return succeed(None) |
378 | self.mocker.call(download) |
379 | - self.mocker.replay() |
380 | - |
381 | - repo = self.repo(dns_name) |
382 | - charm = yield repo.find(CharmURL.parse(url_str)) |
383 | - self.assertEquals(charm.get_sha256(), src_charm.get_sha256()) |
384 | - self.assertEquals(os.listdir(download_dir), []) |
385 | - expect_name = "%s-%s.charm" % ( |
386 | - under.quote(url_str), charm.metadata.revision) |
387 | - self.assertEquals( |
388 | - charm.path, os.path.join(self.cache_path, expect_name)) |
389 | - |
390 | - def test_find_plain(self): |
391 | - return self.assert_find( |
392 | - "https://somewhe.re", "cs:series/name", |
393 | - "https://somewhe.re/charm/series/name") |
394 | - |
395 | - def test_find_user(self): |
396 | - return self.assert_find( |
397 | - "https://somewhereel.se", "cs:~user/series/name", |
398 | - "https://somewhereel.se/charm/~user/series/name") |
399 | - |
400 | - def test_cant_find(self): |
401 | - downloadPage = self.mocker.replace("twisted.web.client.downloadPage") |
402 | - downloadPage("https://anoth.er/charm/series/name", ANY) |
403 | - self.mocker.result(fail(Error("500"))) |
404 | - self.mocker.replay() |
405 | - |
406 | - repo = RemoteCharmRepository("https://anoth.er") |
407 | - d = self.assertFailure( |
408 | - repo.find(CharmURL.parse("cs:series/name")), |
409 | - CharmNotFound) |
410 | - |
411 | - def verify(error): |
412 | - self.assertEquals( |
413 | - str(error), |
414 | - "Charm 'cs:series/name' not found in repository " |
415 | - "https://anoth.er") |
416 | - d.addCallback(verify) |
417 | - return d |
418 | - |
419 | - def test_revision(self): |
420 | - repo = RemoteCharmRepository("whatev.er") |
421 | - d = self.assertFailure( |
422 | - repo.find(CharmURL.parse("cs:series/name-1")), |
423 | - AssertionError) |
424 | - |
425 | - def verify(error): |
426 | - self.assertEquals(str(error), "find-by-revision not supported yet") |
427 | - d.addCallback(verify) |
428 | - return d |
429 | + |
430 | + @inlineCallbacks |
431 | + def assert_find_uncached(self, dns_name, url_str, info_url, find_url): |
432 | + self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1))) |
433 | + self.mock_download(find_url) |
434 | + self.mocker.replay() |
435 | + |
436 | + repo = self.repo(dns_name) |
437 | + charm = yield repo.find(CharmURL.parse(url_str)) |
438 | + self.assertEquals(charm.get_sha256(), self.sha256) |
439 | + self.assertEquals(charm.path, self.cache_location(url_str, 1)) |
440 | + self.assertEquals(os.listdir(self.download_path), []) |
441 | + |
442 | + @inlineCallbacks |
443 | + def assert_find_cached(self, dns_name, url_str, info_url): |
444 | + os.makedirs(self.cache_path) |
445 | + cache_location = self.cache_location(url_str, 1) |
446 | + shutil.copy(self.charm.as_bundle().path, cache_location) |
447 | + |
448 | + self.mock_charm_info(info_url, succeed(self.charm_info(url_str, 1))) |
449 | + self.mocker.replay() |
450 | + |
451 | + repo = self.repo(dns_name) |
452 | + charm = yield repo.find(CharmURL.parse(url_str)) |
453 | + self.assertEquals(charm.get_sha256(), self.sha256) |
454 | + self.assertEquals(charm.path, cache_location) |
455 | + |
456 | + def assert_find_error(self, dns_name, url_str, err_type, message): |
457 | + self.mocker.replay() |
458 | + repo = self.repo(dns_name) |
459 | + d = self.assertFailure(repo.find(CharmURL.parse(url_str)), err_type) |
460 | + |
461 | + def verify(error): |
462 | + self.assertEquals(str(error), message) |
463 | + d.addCallback(verify) |
464 | + return d |
465 | + |
466 | + @inlineCallbacks |
467 | + def assert_latest(self, dns_name, url_str, revision): |
468 | + self.mocker.replay() |
469 | + repo = self.repo(dns_name) |
470 | + result = yield repo.latest(CharmURL.parse(url_str)) |
471 | + self.assertEquals(result, revision) |
472 | + |
473 | + def assert_latest_error(self, dns_name, url_str, err_type, message): |
474 | + self.mocker.replay() |
475 | + repo = self.repo(dns_name) |
476 | + d = self.assertFailure(repo.latest(CharmURL.parse(url_str)), err_type) |
477 | + |
478 | + def verify(error): |
479 | + self.assertEquals(str(error), message) |
480 | + d.addCallback(verify) |
481 | + return d |
482 | + |
483 | + def test_find_plain_uncached(self): |
484 | + return self.assert_find_uncached( |
485 | + "https://somewhe.re", "cs:series/name", |
486 | + "https://somewhe.re/charm-info?charms=cs%3Aseries/name", |
487 | + "https://somewhe.re/charm/series/name-1") |
488 | + |
489 | + def test_find_revision_uncached(self): |
490 | + return self.assert_find_uncached( |
491 | + "https://somewhe.re", "cs:series/name-1", |
492 | + "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1", |
493 | + "https://somewhe.re/charm/series/name-1") |
494 | + |
495 | + def test_find_user_uncached(self): |
496 | + return self.assert_find_uncached( |
497 | + "https://somewhereel.se", "cs:~user/srs/name", |
498 | + "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name", |
499 | + "https://somewhereel.se/charm/%7Euser/srs/name-1") |
500 | + |
501 | + def test_find_plain_cached(self): |
502 | + return self.assert_find_cached( |
503 | + "https://somewhe.re", "cs:series/name", |
504 | + "https://somewhe.re/charm-info?charms=cs%3Aseries/name") |
505 | + |
506 | + def test_find_revision_cached(self): |
507 | + return self.assert_find_cached( |
508 | + "https://somewhe.re", "cs:series/name-1", |
509 | + "https://somewhe.re/charm-info?charms=cs%3Aseries/name-1") |
510 | + |
511 | + def test_find_user_cached(self): |
512 | + return self.assert_find_cached( |
513 | + "https://somewhereel.se", "cs:~user/srs/name", |
514 | + "https://somewhereel.se/charm-info?charms=cs%3A%7Euser/srs/name") |
515 | + |
516 | + def test_find_info_error(self): |
517 | + self.mock_charm_info( |
518 | + "https://anoth.er/charm-info?charms=cs%3Aseries/name", |
519 | + fail(Error("500"))) |
520 | + return self.assert_find_error( |
521 | + "https://anoth.er", "cs:series/name", CharmNotFound, |
522 | + "Charm 'cs:series/name' not found in repository https://anoth.er") |
523 | + |
524 | + def test_find_info_bad_revision(self): |
525 | + self.mock_charm_info( |
526 | + "https://anoth.er/charm-info?charms=cs%3Aseries/name-99", |
527 | + succeed(self.charm_info("cs:series/name-99", 1))) |
528 | + return self.assert_find_error( |
529 | + "https://anoth.er", "cs:series/name-99", AssertionError, |
530 | + "bad url revision") |
531 | + |
532 | + def test_find_download_error(self): |
533 | + self.mock_charm_info( |
534 | + "https://anoth.er/charm-info?charms=cs%3Aseries/name", |
535 | + succeed(json.dumps({"cs:series/name": {"revision": 123}}))) |
536 | + self.mock_download( |
537 | + "https://anoth.er/charm/series/name-123", Error("999")) |
538 | + return self.assert_find_error( |
539 | + "https://anoth.er", "cs:series/name", CharmNotFound, |
540 | + "Charm 'cs:series/name-123' not found in repository " |
541 | + "https://anoth.er") |
542 | + |
543 | + def test_find_charm_revision_mismatch(self): |
544 | + self.mock_charm_info( |
545 | + "https://anoth.er/charm-info?charms=cs%3Aseries/name", |
546 | + succeed(json.dumps({"cs:series/name": {"revision": 99}}))) |
547 | + self.mock_download("https://anoth.er/charm/series/name-99") |
548 | + return self.assert_find_error( |
549 | + "https://anoth.er", "cs:series/name", AssertionError, |
550 | + "bad charm revision") |
551 | + |
552 | + @inlineCallbacks |
553 | + def test_find_downloaded_hash_mismatch(self): |
554 | + cache_location = self.cache_location("cs:series/name-1", 1) |
555 | + self.mock_charm_info( |
556 | + "https://anoth.er/charm-info?charms=cs%3Aseries/name", |
557 | + succeed(json.dumps( |
558 | + {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}}))) |
559 | + self.mock_download("https://anoth.er/charm/series/name-1") |
560 | + yield self.assert_find_error( |
561 | + "https://anoth.er", "cs:series/name", CharmError, |
562 | + "Error processing 'cs:series/name-1 (downloaded)': SHA256 " |
563 | + "mismatch") |
564 | + self.assertFalse(os.path.exists(cache_location)) |
565 | + |
566 | + @inlineCallbacks |
567 | + def test_find_cached_hash_mismatch(self): |
568 | + os.makedirs(self.cache_path) |
569 | + cache_location = self.cache_location("cs:series/name-1", 1) |
570 | + shutil.copy(self.charm.as_bundle().path, cache_location) |
571 | + |
572 | + self.mock_charm_info( |
573 | + "https://anoth.er/charm-info?charms=cs%3Aseries/name", |
574 | + succeed(json.dumps( |
575 | + {"cs:series/name": {"revision": 1, "sha256": "NO YUO"}}))) |
576 | + yield self.assert_find_error( |
577 | + "https://anoth.er", "cs:series/name", CharmError, |
578 | + "Error processing 'cs:series/name-1 (cached)': SHA256 mismatch") |
579 | + self.assertFalse(os.path.exists(cache_location)) |
580 | + |
581 | + def test_latest_plain(self): |
582 | + self.mock_charm_info( |
583 | + "https://somewhe.re/charm-info?charms=cs%3Afoo/bar", |
584 | + succeed(self.charm_info("cs:foo/bar", 99))) |
585 | + return self.assert_latest("https://somewhe.re", "cs:foo/bar", 99) |
586 | + |
587 | + def test_latest_user(self): |
588 | + self.mock_charm_info( |
589 | + "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar", |
590 | + succeed(self.charm_info("cs:~fee/foo/bar", 123))) |
591 | + return self.assert_latest( |
592 | + "https://somewhereel.se", "cs:~fee/foo/bar", 123) |
593 | + |
594 | + def test_latest_revision(self): |
595 | + self.mock_charm_info( |
596 | + "https://somewhereel.se/charm-info?charms=cs%3A%7Efee/foo/bar", |
597 | + succeed(self.charm_info("cs:~fee/foo/bar", 123))) |
598 | + return self.assert_latest( |
599 | + "https://somewhereel.se", "cs:~fee/foo/bar-99", 123) |
600 | + |
601 | + def test_latest_error(self): |
602 | + self.mock_charm_info( |
603 | + "https://andanoth.er/charm-info?charms=cs%3A%7Eblib/blab/blob", |
604 | + fail(Error("404"))) |
605 | + return self.assert_latest_error( |
606 | + "https://andanoth.er", "cs:~blib/blab/blob", CharmNotFound, |
607 | + "Charm 'cs:~blib/blab/blob' not found in repository " |
608 | + "https://andanoth.er") |
609 | |
610 | |
611 | class ResolveTest(RepositoryTestBase): |
612 | |
613 | - @inlineCallbacks |
614 | - def test_resolve_local_with_collection(self): |
615 | - url, charm = yield resolve( |
616 | - "local:series/sample", self.unbundled_repo_path, "series") |
617 | - self.assertEquals(str(url), "local:series/sample") |
618 | - self.assertEquals(charm.metadata.revision, 2) |
619 | - |
620 | - @inlineCallbacks |
621 | - def test_resolve_local_no_collection(self): |
622 | - url, charm = yield resolve( |
623 | - "local:sample", self.unbundled_repo_path, "series") |
624 | - self.assertEquals(str(url), "local:series/sample") |
625 | - self.assertEquals(charm.metadata.revision, 2) |
626 | - |
627 | - @inlineCallbacks |
628 | - def test_resolve_local_alternative_collection(self): |
629 | - url, charm = yield resolve( |
630 | - "local:series/sample", self.unbundled_repo_path, "otherseries") |
631 | - self.assertEquals(str(url), "local:series/sample") |
632 | - self.assertEquals(charm.metadata.revision, 2) |
633 | - |
634 | - def assert_not_found(self, name, default_series): |
635 | - downloadPage = self.mocker.replace("twisted.web.client.downloadPage") |
636 | - downloadPage("https://store.juju.ubuntu.com/charm/series/whatever", ANY) |
637 | - self.mocker.result(fail(Error("404"))) |
638 | - self.mocker.replay() |
639 | - |
640 | - d = self.assertFailure( |
641 | - resolve(name, None, default_series), CharmNotFound) |
642 | - |
643 | - def verify(error): |
644 | - self.assertEquals( |
645 | - str(error), |
646 | - "Charm 'cs:series/whatever' not found in repository " |
647 | - "https://store.juju.ubuntu.com") |
648 | - d.addCallback(verify) |
649 | - return d |
650 | - |
651 | - def test_resolve_minimal(self): |
652 | - return self.assert_not_found("whatever", "series") |
653 | - |
654 | - def test_resolve_no_root(self): |
655 | - return self.assert_not_found("series/whatever", "series") |
656 | - |
657 | - def test_resolve_with_root(self): |
658 | - return self.assert_not_found("cs:series/whatever", "series") |
659 | - |
660 | - def test_resolve_nonsense_root(self): |
661 | - d = self.assertFailure( |
662 | - resolve("blah:whatever", None, "series"), CharmURLError) |
663 | - |
664 | - def verify(error): |
665 | - self.assertEquals( |
666 | - str(error), |
667 | - "Bad charm URL 'blah:series/whatever': invalid schema (URL " |
668 | - "inferred from 'blah:whatever')") |
669 | - d.addCallback(verify) |
670 | - return d |
671 | + def assert_resolve_local(self, vague, default, expect): |
672 | + repo, url = resolve(vague, "/some/path", default) |
673 | + self.assertEquals(str(url), expect) |
674 | + self.assertTrue(isinstance(repo, LocalCharmRepository)) |
675 | + self.assertEquals(repo.path, "/some/path") |
676 | + |
677 | + def test_resolve_local(self): |
678 | + self.assert_resolve_local( |
679 | + "local:series/sample", "default", "local:series/sample") |
680 | + self.assert_resolve_local( |
681 | + "local:sample", "default", "local:default/sample") |
682 | + |
683 | + def assert_resolve_remote(self, vague, default, expect): |
684 | + repo, url = resolve(vague, None, default) |
685 | + self.assertEquals(str(url), expect) |
686 | + self.assertTrue(isinstance(repo, RemoteCharmRepository)) |
687 | + self.assertEquals(repo.url_base, "https://store.juju.ubuntu.com") |
688 | + |
689 | + def test_resolve_remote(self): |
690 | + self.assert_resolve_remote( |
691 | + "sample", "default", "cs:default/sample") |
692 | + self.assert_resolve_remote( |
693 | + "series/sample", "default", "cs:series/sample") |
694 | + self.assert_resolve_remote( |
695 | + "cs:sample", "default", "cs:default/sample") |
696 | + self.assert_resolve_remote( |
697 | + "cs:series/sample", "default", "cs:series/sample") |
698 | + self.assert_resolve_remote( |
699 | + "cs:~user/sample", "default", "cs:~user/default/sample") |
700 | + self.assert_resolve_remote( |
701 | + "cs:~user/series/sample", "default", "cs:~user/series/sample") |
702 | + |
703 | + def test_resolve_nonsense(self): |
704 | + error = self.assertRaises( |
705 | + CharmURLError, resolve, "blah:whatever", None, "series") |
706 | + self.assertEquals( |
707 | + str(error), |
708 | + "Bad charm URL 'blah:series/whatever': invalid schema (URL " |
709 | + "inferred from 'blah:whatever')") |
710 | |
711 | === modified file 'juju/charm/tests/test_url.py' |
712 | --- juju/charm/tests/test_url.py 2011-09-29 03:07:26 +0000 |
713 | +++ juju/charm/tests/test_url.py 2011-10-05 10:00:46 +0000 |
714 | @@ -29,6 +29,7 @@ |
715 | url = CharmURL.parse(string) |
716 | self.assert_url(url, schema, user, series, name, rev) |
717 | self.assertEquals(str(url), string) |
718 | + self.assertEquals(url.path, string.split(":", 1)[1]) |
719 | |
720 | def test_parse(self): |
721 | self.assert_parse( |
722 | |
723 | === modified file 'juju/charm/url.py' |
724 | --- juju/charm/url.py 2011-09-29 03:07:26 +0000 |
725 | +++ juju/charm/url.py 2011-10-05 10:00:46 +0000 |
726 | @@ -56,6 +56,10 @@ |
727 | return "%s/%s" % (self.collection, self.name) |
728 | return "%s/%s-%s" % (self.collection, self.name, self.revision) |
729 | |
730 | + @property |
731 | + def path(self): |
732 | + return str(self).split(":", 1)[1] |
733 | + |
734 | def with_revision(self, revision): |
735 | other = copy.deepcopy(self) |
736 | other.revision = revision |
737 | |
738 | === modified file 'juju/control/deploy.py' |
739 | --- juju/control/deploy.py 2011-09-30 09:48:25 +0000 |
740 | +++ juju/control/deploy.py 2011-10-05 10:00:46 +0000 |
741 | @@ -81,12 +81,11 @@ |
742 | service_name, log, config_file=None): |
743 | """Deploy a charm within an environment. |
744 | |
745 | - |
746 | This will publish the charm to the environment, creating |
747 | a service from the charm, and get it set to be launched |
748 | on a new machine. |
749 | """ |
750 | - charm_url, charm = yield resolve( |
751 | + repo, charm_url = resolve( |
752 | charm_name, repository_path, environment.default_series) |
753 | |
754 | # Validate config options prior to deployment attempt |
755 | @@ -95,6 +94,9 @@ |
756 | if config_file: |
757 | service_options = parse_config_options(config_file, service_name) |
758 | |
759 | + charm = yield repo.find(charm_url) |
760 | + charm_id = str(charm_url.with_revision(charm.metadata.revision)) |
761 | + |
762 | provider = environment.get_machine_provider() |
763 | placement_policy = provider.get_placement_policy() |
764 | client = yield provider.connect() |
765 | @@ -108,7 +110,6 @@ |
766 | |
767 | # Publish the charm to juju |
768 | publisher = CharmPublisher(client, storage) |
769 | - charm_id = str(charm_url.with_revision(charm.metadata.revision)) |
770 | yield publisher.add_charm(charm_id, charm) |
771 | result = yield publisher.publish() |
772 | |
773 | |
774 | === modified file 'juju/control/upgrade_charm.py' |
775 | --- juju/control/upgrade_charm.py 2011-09-29 03:07:26 +0000 |
776 | +++ juju/control/upgrade_charm.py 2011-10-05 10:00:46 +0000 |
777 | @@ -64,11 +64,12 @@ |
778 | |
779 | old_charm_url = CharmURL.parse(old_charm_id) |
780 | old_charm_url.assert_revision() |
781 | - charm_url, charm = yield resolve( |
782 | + repo, charm_url = resolve( |
783 | str(old_charm_url.with_revision(None)), |
784 | repository_path, |
785 | environment.default_series) |
786 | - new_charm_url = charm_url.with_revision(charm.metadata.revision) |
787 | + new_charm_url = charm_url.with_revision( |
788 | + (yield repo.latest(charm_url))) |
789 | new_charm_id = str(new_charm_url) |
790 | |
791 | # Verify its newer than what's deployed |
792 | @@ -86,6 +87,7 @@ |
793 | # Publish the new charm |
794 | storage = provider.get_file_storage() |
795 | publisher = CharmPublisher(client, storage) |
796 | + charm = yield repo.find(charm_url) |
797 | yield publisher.add_charm(new_charm_id, charm) |
798 | result = yield publisher.publish() |
799 | charm_state = result[0] |
Based on request for a cache dir in https:/ /code.launchpad .net/~fwereade/ juju/use- remote- formulas/ +merge/ 77323, this branch now actually uses the cache. It assumes that a charm store implementation will guarantee that a charm-url- with-revision uniquely identifies a given charm, but I think that's a reasonable thing to demand ;-).