Merge lp:~clint-fewbar/pyjuju/namespace-from-env into lp:pyjuju

Proposed by Clint Byrum
Status: Work in progress
Proposed branch: lp:~clint-fewbar/pyjuju/namespace-from-env
Merge into: lp:pyjuju
Diff against target: 272 lines (+146/-16)
5 files modified
juju/charm/repository.py (+5/-2)
juju/charm/tests/test_url.py (+73/-2)
juju/charm/url.py (+51/-9)
juju/control/deploy.py (+6/-1)
juju/control/tests/test_deploy.py (+11/-2)
To merge this branch: bzr merge lp:~clint-fewbar/pyjuju/namespace-from-env
Reviewer Review Type Date Requested Status
Benjamin Saller (community) Approve
Review via email: mp+94479@code.launchpad.net

Description of the change

Default Charm namespace should be overridable by environment variables.

If JUJU_DEFAULT_NS is specified, juju will infer that namespace.
Otherwise, 'cs:' will still be the default.

https://codereview.appspot.com/5695056/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

LGTM, it wasn't clear from the mailing list if you agreed about gustavo's suggesstion of calling the env var JUJU_DEFAULT_NS or not, also (and I'm not 100% clear how to reference this now that we've split the docs off) you should add a note to the existing docs about this option.

review: Approve
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

From the list discussion this should set namespace and series.

469. By Clint Byrum

working with new proposed ability to override series and user as well

470. By Clint Byrum

handle only user being inferred from environment

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Thanks for sticking with this. I've included some feedback.

https://codereview.appspot.com/5695056/diff/3001/juju/charm/tests/test_url.py
File juju/charm/tests/test_url.py (left):

https://codereview.appspot.com/5695056/diff/3001/juju/charm/tests/test_url.py#oldcode126
juju/charm/tests/test_url.py:126:
All the tests assume the env is set to a proper value, test the case
where it is not.

https://codereview.appspot.com/5695056/diff/3001/juju/charm/url.py
File juju/charm/url.py (right):

https://codereview.appspot.com/5695056/diff/3001/juju/charm/url.py#newcode131
juju/charm/url.py:131: default_schema, rest =
os.environ.get("JUJU_DEFAULT_NS","cs:").split(":",1)
I think this module is too low level to be interacting with the
environment like this. The juju/control layer should consume environment
information and can supply information. Those (deploy and upgrade-charm)
use the repository.resolve function which can take additional arguments
for this (and handle errors in a place where we can inform the user they
set the variable incorrectly. Resolve can take the parsed output of the
environment and pass it to infer. juju/control can even use a util
method from here to help parse the argument but that parsing has to be
more solid that what is here now.

Currently a bad value, missing a ":" for example, will result in a
ValueError when trying to unpack on the left-hand-side. Need to be more
defensive than that.

https://codereview.appspot.com/5695056/

471. By Clint Byrum

move JUJU_DEFAULT_NS to deploy command

472. By Clint Byrum

merging with trunk

473. By Clint Byrum

documenting usage of JUJU_DEFAULT_NS in deploy.

474. By Clint Byrum

cleanup, remove improper env setting leftover rom refactoring

475. By Clint Byrum

get more defensive about bad namespaces

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

This code would be much simpler, and easier to reason about its
correctness, if it did all its parsing up front and just used the
variables instead of conditionally redefining them as it went with
incremental parsing of 'rest'.

https://codereview.appspot.com/5695056/diff/8002/juju/charm/url.py
File juju/charm/url.py (right):

https://codereview.appspot.com/5695056/diff/8002/juju/charm/url.py#newcode155
juju/charm/url.py:155: default_infer_series, rest = rest.split('/',1)
untested

https://codereview.appspot.com/5695056/diff/8002/juju/charm/url.py#newcode182
juju/charm/url.py:182: rest = "%s/%s" % (default_infer_series, rest)
untested

https://codereview.appspot.com/5695056/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Excerpts from Kapil Thangavelu's message of 2012-03-15 18:07:25 UTC:
> This code would be much simpler, and easier to reason about its
> correctness, if it did all its parsing up front and just used the
> variables instead of conditionally redefining them as it went with
> incremental parsing of 'rest'.
>
>
> https://codereview.appspot.com/5695056/diff/8002/juju/charm/url.py
> File juju/charm/url.py (right):
>
> https://codereview.appspot.com/5695056/diff/8002/juju/charm/url.py#newcode155
> juju/charm/url.py:155: default_infer_series, rest = rest.split('/',1)
> untested
>
> https://codereview.appspot.com/5695056/diff/8002/juju/charm/url.py#newcode182
> juju/charm/url.py:182: rest = "%s/%s" % (default_infer_series, rest)
> untested
>
> https://codereview.appspot.com/5695056/
>

diff to pep8 fixes
http://paste.ubuntu.com/885281/

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

i've agreed with clint to take this over the completion and landing of this branch.

Unmerged revisions

475. By Clint Byrum

get more defensive about bad namespaces

474. By Clint Byrum

cleanup, remove improper env setting leftover rom refactoring

473. By Clint Byrum

documenting usage of JUJU_DEFAULT_NS in deploy.

472. By Clint Byrum

merging with trunk

471. By Clint Byrum

move JUJU_DEFAULT_NS to deploy command

470. By Clint Byrum

handle only user being inferred from environment

469. By Clint Byrum

working with new proposed ability to override series and user as well

468. By Clint Byrum

Pull default namespace from environment.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/charm/repository.py'
2--- juju/charm/repository.py 2012-02-16 19:19:42 +0000
3+++ juju/charm/repository.py 2012-03-09 22:39:18 +0000
4@@ -167,7 +167,7 @@
5 returnValue(info["revision"])
6
7
8-def resolve(vague_name, repository_path, default_series):
9+def resolve(vague_name, repository_path, default_series, default_namespace=None):
10 """Get a Charm and associated identifying information
11
12 :param str vague_name: a lazily specified charm name, suitable for use with
13@@ -181,12 +181,15 @@
14 :param str default_series: the Ubuntu series to insert when `charm_name` is
15 inadequately specified.
16
17+ :param str default_namespace: partial charm url that is passed to
18+ :meth:`CharmURL.infer`
19+
20 :return: a tuple of a :class:`juju.charm.url.CharmURL` and a
21 :class:`juju.charm.base.CharmBase` subclass, which together contain
22 both the charm's data and all information necessary to specify its
23 source.
24 """
25- url = CharmURL.infer(vague_name, default_series)
26+ url = CharmURL.infer(vague_name, default_series, default_namespace)
27 if url.collection.schema == "local":
28 repo = LocalCharmRepository(repository_path)
29 elif url.collection.schema == "cs":
30
31=== modified file 'juju/charm/tests/test_url.py'
32--- juju/charm/tests/test_url.py 2011-10-06 21:04:13 +0000
33+++ juju/charm/tests/test_url.py 2012-03-09 22:39:18 +0000
34@@ -98,8 +98,8 @@
35 url3.assert_revision()
36 self.assertEquals(str(url3), "cs:foo/bar-999")
37
38- def assert_infer(self, string, schema, user, series, name, rev):
39- url = CharmURL.infer(string, "default")
40+ def assert_infer(self, string, schema, user, series, name, rev, ns=None):
41+ url = CharmURL.infer(string, "default", ns)
42 self.assert_url(url, schema, user, series, name, rev)
43
44 def test_infer(self):
45@@ -124,6 +124,77 @@
46 self.assert_infer(
47 "local:name-0", "local", None, "default", "name", 0)
48
49+ def test_infer_from_default_namespace(self):
50+ self.assert_infer(
51+ "name", "local", None, "default", "name", None, 'local')
52+ self.assert_infer(
53+ "name-0", "local", None, "default", "name", 0, 'local')
54+ self.assert_infer(
55+ "series/name", "local", None, "series", "name", None, 'local')
56+ self.assert_infer(
57+ "series/name-0", "local", None, "series", "name", 0, 'local')
58+ self.assert_infer(
59+ "name", "local", None, "default", "name", None, 'local:')
60+ self.assert_infer(
61+ "name-0", "local", None, "default", "name", 0, 'local:')
62+ self.assert_infer(
63+ "series/name", "local", None, "series", "name", None, 'local:')
64+ self.assert_infer(
65+ "series/name-0", "local", None, "series", "name", 0, 'local:')
66+
67+ def test_infer_series_from_default_namespace(self):
68+ self.assert_infer(
69+ "name", "cs", None, "envseries", "name", None, 'cs:envseries')
70+ self.assert_infer(
71+ "name-0", "cs", None, "envseries", "name", 0, 'cs:envseries')
72+ self.assert_infer(
73+ "series/name", "cs", None, "series", "name", None, 'cs:envseries')
74+ self.assert_infer(
75+ "series/name-0", "cs", None, "series", "name", 0, 'cs:envseries')
76+
77+ def test_infer_user_from_default_namespace(self):
78+ self.assert_infer(
79+ "name", "cs", "foouser", "default", "name", None, 'cs:~foouser')
80+ self.assert_infer(
81+ "name-0", "cs", "foouser", "default", "name", 0, 'cs:~foouser')
82+ self.assert_infer(
83+ "series/name", "cs", "foouser", "series", "name", None, 'cs:~foouser')
84+ self.assert_infer(
85+ "series/name-0", "cs", "foouser", "series", "name", 0, 'cs:~foouser')
86+
87+ def test_infer_user_and_series_from_environ(self):
88+ self.assert_infer(
89+ "name", "cs", "foouser", "fooseries", "name", None, 'cs:~foouser/fooseries')
90+ self.assert_infer(
91+ "name-0", "cs", "foouser", "fooseries", "name", 0, 'cs:~foouser/fooseries')
92+ self.assert_infer(
93+ "series/name", "cs", "foouser", "series", "name", None, 'cs:~foouser/fooseries')
94+ self.assert_infer(
95+ "series/name-0", "cs", "foouser", "series", "name", 0, 'cs:~foouser/fooseries')
96+
97+ def test_infer_bad_namespace(self):
98+ # Invalid schema "zyzzyx/"
99+ self.assertRaises(
100+ CharmURLError, CharmURL.infer, "name","fooseries", 'zyzzyx/:/~foo///')
101+ self.assertRaises(
102+ CharmURLError, CharmURL.infer, "series/name","fooseries", 'zyzzyx/:/~foo///')
103+ self.assertRaises(
104+ CharmURLError, CharmURL.infer, "~user/series/name","fooseries", 'zyzzyx/:/~foo///')
105+ """ cs:name, does not actually need to be inferred from the
106+ default namespace, but the namespace will still trigger problems. """
107+ self.assertRaises(
108+ CharmURLError, CharmURL.infer, "cs:name","fooseries", 'zyzzyx/:/~foo///')
109+ self.assertRaises(
110+ CharmURLError, CharmURL.infer, "local:name","fooseries", 'zyzzyx/:/~foo///')
111+
112+ # Just a user is too ambiguous
113+ self.assertRaises(
114+ CharmURLError, CharmURL.infer, "name","fooseries", '~someuser')
115+ self.assertRaises(
116+ CharmURLError, CharmURL.infer, "series/name","fooseries", '~someuser')
117+ self.assertRaises(
118+ CharmURLError, CharmURL.infer, "~user/series/name","fooseries", '~someuser')
119+
120 def test_cannot_infer(self):
121 err = self.assertRaises(
122 CharmURLError, CharmURL.infer, "name", "invalid!series")
123
124=== modified file 'juju/charm/url.py'
125--- juju/charm/url.py 2011-10-06 21:04:13 +0000
126+++ juju/charm/url.py 2012-03-09 22:39:18 +0000
127@@ -1,5 +1,6 @@
128 import copy
129 import re
130+import os
131
132 from juju.charm.errors import CharmURLError
133
134@@ -7,6 +8,7 @@
135 _USER_RE = re.compile("^[a-z0-9][a-zA-Z0-9+.-]+$")
136 _SERIES_RE = re.compile("^[a-z]+([a-z-]+[a-z])?$")
137 _NAME_RE = re.compile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
138+_NAMESPACE_RE = re.compile("^[a-zA-Z]+:\~?[a-zA-Z\-]*/?")
139
140
141 class CharmCollection(object):
142@@ -114,7 +116,7 @@
143 return cls(CharmCollection(schema, user, series), name, revision)
144
145 @classmethod
146- def infer(cls, vague_name, default_series):
147+ def infer(cls, vague_name, default_series, default_namespace=None):
148 """Turn a potentially fuzzy alias into a CharmURL."""
149 try:
150 # it might already be a valid URL string
151@@ -127,19 +129,59 @@
152 raise CharmURLError(
153 vague_name, "a URL with a user must specify a schema")
154
155+ if default_namespace is None:
156+ default_namespace = 'cs:'
157+ else:
158+ # schema is the most basic unit, so assume that if no :
159+ if ':' not in default_namespace:
160+ default_namespace = "%s:" % (default_namespace)
161+
162+ if not _NAMESPACE_RE .match(default_namespace):
163+ raise CharmURLError(default_namespace, "invalid default namespace")
164+
165+ default_schema, rest = default_namespace.split(":",1)
166+
167+ # determine what has been defaulted in JUJU_DEFAULT_NS
168+ if len(rest) and rest[0] == '~':
169+ if '/' in rest:
170+ default_infer_user, rest = rest[1:].split('/',1)
171+ else:
172+ default_infer_user = rest[1:]
173+ rest=''
174+ else:
175+ default_infer_user = None
176+
177+ if '/' in rest:
178+ default_infer_series, rest = rest.split('/',1)
179+ elif len(rest):
180+ default_infer_series = rest
181+ else:
182+ default_infer_series = default_series
183+
184+ # determine what is and is not set in vague_name
185 if ":" in vague_name:
186 schema, rest = vague_name.split(":", 1)
187 else:
188- schema, rest = "cs", vague_name
189+ schema = default_schema
190+ rest = vague_name
191+
192+ if rest[0] != '~':
193+ if default_infer_user is None:
194+ if '/' not in rest:
195+ rest = "%s/%s" % (default_infer_series, rest)
196+ else:
197+ if '/' in rest:
198+ rest = "~%s/%s" % (default_infer_user, rest)
199+ else:
200+ rest = "~%s/%s/%s" % (default_infer_user, default_infer_series, rest)
201+ else:
202+ if '/' in rest:
203+ infer_user, rest = rest.split('~')[1].split('/',1)
204+ rest = "~%s/%s/%s" % (infer_user, default_infer_series, rest)
205+ else:
206+ rest = "%s/%s" % (default_infer_series, rest)
207
208 url_string = "%s:%s" % (schema, rest)
209- parts = rest.split("/")
210- if len(parts) == 1:
211- url_string = "%s:%s/%s" % (schema, default_series, rest)
212- elif len(parts) == 2:
213- if parts[0].startswith("~"):
214- url_string = "%s:%s/%s/%s" % (
215- schema, parts[0], default_series, parts[1])
216
217 try:
218 return cls.parse(url_string)
219
220=== modified file 'juju/control/deploy.py'
221--- juju/control/deploy.py 2012-03-09 20:20:20 +0000
222+++ juju/control/deploy.py 2012-03-09 22:39:18 +0000
223@@ -112,9 +112,14 @@
224 a service from the charm, and get it set to be launched
225 on a new machine. If --repository is not specified, it
226 will be taken from the environment variable JUJU_REPOSITORY.
227+ If `charm` has a non-specific URL it will be resolved
228+ using the default namespace, which is "cs:". That default can
229+ be overridden with the environment variable JUJU_DEFAULT_NS.
230 """
231+ default_namespace = os.environ.get('JUJU_DEFAULT_NS')
232 repo, charm_url = resolve(
233- charm_name, repository_path, environment.default_series)
234+ charm_name, repository_path, environment.default_series,
235+ default_namespace)
236
237 log.info("Searching for charm")
238 charm = yield repo.find(charm_url)
239
240=== modified file 'juju/control/tests/test_deploy.py'
241--- juju/control/tests/test_deploy.py 2012-03-09 20:13:36 +0000
242+++ juju/control/tests/test_deploy.py 2012-03-09 22:39:18 +0000
243@@ -210,10 +210,19 @@
244
245 @inlineCallbacks
246 def test_deploy(self):
247+ yield self._test_deploy('local:sample')
248+
249+ @inlineCallbacks
250+ def test_deploy_namespace_from_environ(self):
251+ self.change_environment(JUJU_DEFAULT_NS='local')
252+ yield self._test_deploy('sample')
253+
254+ @inlineCallbacks
255+ def _test_deploy(self, charmname):
256 """Create service, and service unit on machine from charm"""
257 environment = self.config.get("firstenv")
258 yield deploy.deploy(
259- self.config, environment, self.unbundled_repo_path, "local:sample",
260+ self.config, environment, self.unbundled_repo_path, charmname,
261 "myblog", logging.getLogger("deploy"), ["cpu=123"])
262 topology = yield self.get_topology()
263
264@@ -299,7 +308,7 @@
265 repo.type
266 self.mocker.result("store")
267 resolve = self.mocker.replace("juju.control.deploy.resolve")
268- resolve("cs:sample", None, "series")
269+ resolve("cs:sample", None, "series", None)
270 self.mocker.result((repo, CharmURL.infer("cs:sample", "series")))
271 repo.find(MATCH(lambda x: isinstance(x, CharmURL)))
272 self.mocker.result(CharmDirectory(self.sample_dir1))

Subscribers

People subscribed via source and target branches

to status/vote changes: