Merge lp:~bac/juju-quickstart/auth into lp:juju-quickstart
- auth
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 50 |
Proposed branch: | lp:~bac/juju-quickstart/auth |
Merge into: | lp:juju-quickstart |
Diff against target: |
501 lines (+224/-35) 7 files modified
quickstart/app.py (+23/-0) quickstart/manage.py (+20/-5) quickstart/models/envs.py (+39/-17) quickstart/tests/cli/test_views.py (+2/-2) quickstart/tests/models/test_envs.py (+68/-9) quickstart/tests/test_app.py (+20/-0) quickstart/tests/test_manage.py (+52/-2) |
To merge this branch: | bzr merge lp:~bac/juju-quickstart/auth |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+203631@code.launchpad.net |
Commit message
Description of the change
Get admin_secret from juju-generated jenv file.
As of Juju v 1.17 the admin-secret is stored in a generated file, located in
$JUJU_HOME/
If an admin-secret is provided in environments.yaml, it is used and also
stored in the jenv file. If none is given, then juju fabricates one and
stores it in the jenv file.
The admin-secret *field* is marked as optional now. For the case where a
local environment is used, Quickstart previously would generate an
admin-secret. That behavior is maintained by forcing 'admin-secret' to be in
the missing_fields list. This approach ensures backwards compatibility.
Note that the Juju-generated admin-secret is not available until after
bootstrapping the environment. Due to this behavior, when using juju v1.16
and not admin-secret is configured in the environments.yaml file, the failure
will not be reported until after bootstrapping. This approach seemed better
than introducing hard version checking.
Brad Crittenden (bac) wrote : | # |
Francesco Banconi (frankban) wrote : | # |
This branch is nice Brad, thank you.
LGTM with minor changes and a less trivial suggestion I'd like you to
consider.
https:/
File quickstart/
https:/
quickstart/
the admin-secret to a
IIRC Jenv files are generated starting form v1.16.
https:/
quickstart/
{}.'.format(
Please encode('utf-8') the ValueError message.
https:/
quickstart/
the Juju API;
... or None if the admin-secret field is not found in the environments
YAML file... or similar?
https:/
quickstart/
It seems _get_admin_secret can raise exceptions we never catch. I am
considering a different strategy for this, please let me know what you
think about the following:
We always try to parse the jenv file, even if we already have an
admin_secret. Subsequently, we make the following check:
- if the password is found in the jenv, we use that, done.
- if the password is not found in the jenv:
- if we already have an admin secret, we use that;
- othrwise we exit with a (nice) error.
As a side note, this can involve moving the _get_admin_secret function
to the app module.
AFAICT this way we achieve two goals:
- we are still backward compatible with old versions of Juju;
- we support a use case we don't currently handle (in trunk): the case
when a user has a jenv enabled juju-core, runs quickstart to bootstrap
an environment, then runs quickstart interactively to explicitly provide
an admin-secret, and then re-invoke quickstart again on the same
environment (say, in order to deploy a bundle or reopen the GUI). Now
the passwords from the environments file and from the jenv are not the
same, but since we made the latter override the former, everything works
well.
How does it sound?
Another, less important, suggestion follows.
Rather than modifying options, we might just define admin_secret in this
scope, e.g.:
admin_secret = options.
if admin_secret is None:
admin_secret = ...
And then s/options.
This way we keep the (very weak) separation between setup (which sets up
options) and run (which just uses them). <shrug>
https:/
File quickstart/
https:/
quickstart/
Very nice.
- 51. By Brad Crittenden
-
Code reorg from review. Handle exceptions more appropriately.
- 52. By Brad Crittenden
-
lint
Brad Crittenden (bac) wrote : | # |
Please take a look.
https:/
File quickstart/
https:/
quickstart/
the admin-secret to a
On 2014/01/29 11:48:40, frankban wrote:
> IIRC Jenv files are generated starting form v1.16.
Done.
https:/
quickstart/
{}.'.format(
On 2014/01/29 11:48:40, frankban wrote:
> Please encode('utf-8') the ValueError message.
Done.
https:/
quickstart/
the Juju API;
On 2014/01/29 11:48:40, frankban wrote:
> ... or None if the admin-secret field is not found in the environments
YAML
> file... or similar?
Done.
https:/
quickstart/
Good idea. I've moved the definition of get_admin_secret to app.
Brad Crittenden (bac) wrote : | # |
*** Submitted:
Get admin_secret from juju-generated jenv file.
As of Juju v 1.17 the admin-secret is stored in a generated file,
located in
$JUJU_HOME/
If an admin-secret is provided in environments.yaml, it is used and also
stored in the jenv file. If none is given, then juju fabricates one and
stores it in the jenv file.
The admin-secret *field* is marked as optional now. For the case where
a
local environment is used, Quickstart previously would generate an
admin-secret. That behavior is maintained by forcing 'admin-secret' to
be in
the missing_fields list. This approach ensures backwards compatibility.
Note that the Juju-generated admin-secret is not available until after
bootstrapping the environment. Due to this behavior, when using juju
v1.16
and not admin-secret is configured in the environments.yaml file, the
failure
will not be reported until after bootstrapping. This approach seemed
better
than introducing hard version checking.
R=frankban
CC=
https:/
Preview Diff
1 | === modified file 'quickstart/app.py' |
2 | --- quickstart/app.py 2014-01-17 15:25:22 +0000 |
3 | +++ quickstart/app.py 2014-01-29 14:27:01 +0000 |
4 | @@ -35,6 +35,7 @@ |
5 | utils, |
6 | watchers, |
7 | ) |
8 | +from quickstart.models import envs |
9 | |
10 | |
11 | class ProgramExit(Exception): |
12 | @@ -496,3 +497,25 @@ |
13 | env.deploy_bundle(bundle_yaml, name=bundle_name, bundle_id=bundle_id) |
14 | except jujuclient.EnvError as err: |
15 | raise ProgramExit('bad API server response: {}'.format(err.message)) |
16 | + |
17 | + |
18 | +def get_admin_secret(env_name, juju_home): |
19 | + """Read the admin-secret from the generated environment file. |
20 | + |
21 | + At bootstrap, juju (v1.16 and later) writes the admin-secret to a |
22 | + generated file located in JUJU_HOME. Return the value. |
23 | + Raise a ValueError if: |
24 | + - the environment file is not found; |
25 | + - the environment file contents are not parsable by YAML; |
26 | + - the YAML contents are not properly structured; |
27 | + - the admin-secret is not found. |
28 | + """ |
29 | + filename = '{}.jenv'.format(env_name) |
30 | + juju_env_file = os.path.expanduser( |
31 | + os.path.join(juju_home, 'environments', filename)) |
32 | + jenv_db = envs.load_generated(juju_env_file) |
33 | + try: |
34 | + return jenv_db['admin-secret'] |
35 | + except KeyError: |
36 | + msg = 'admin-secret not found in {}'.format(juju_env_file) |
37 | + raise ValueError(msg.encode('utf-8')) |
38 | |
39 | === modified file 'quickstart/manage.py' |
40 | --- quickstart/manage.py 2014-01-17 18:31:16 +0000 |
41 | +++ quickstart/manage.py 2014-01-29 14:27:01 +0000 |
42 | @@ -237,8 +237,10 @@ |
43 | ' - using "juju switch" to select the default environment;\n' |
44 | ' - setting the default environment in {}.'.format(env_file) |
45 | ) |
46 | + |
47 | # Retrieve the environment database (or create an in-memory empty one). |
48 | env_db = _retrieve_env_db(parser, env_file if env_file_exists else None) |
49 | + |
50 | # Validate the environment. |
51 | env_type_db = envs.get_env_type_db() |
52 | if interactive: |
53 | @@ -250,7 +252,7 @@ |
54 | # selected environment before proceeding. |
55 | env_data = _retrieve_env_data(parser, env_type_db, env_db, env_name) |
56 | # Update the options namespace with the new values. |
57 | - options.admin_secret = env_data['admin-secret'] |
58 | + options.admin_secret = env_data.get('admin-secret') |
59 | options.env_file = env_file |
60 | options.env_name = env_data['name'] |
61 | options.env_type = env_data['type'] |
62 | @@ -290,7 +292,8 @@ |
63 | """Set up the application options and logger. |
64 | |
65 | Return the options as a namespace containing the following attributes: |
66 | - - admin_secret: the password to use to access the Juju API; |
67 | + - admin_secret: the password to use to access the Juju API or None if |
68 | + no admin-secret is present in the $JUJU_HOME/environment.yaml file; |
69 | - bundle: the optional bundle (path or URL) to be deployed; |
70 | - charm_url: the Juju GUI charm URL or None if not specified; |
71 | - debug: whether debug mode is activated; |
72 | @@ -389,10 +392,22 @@ |
73 | already_bootstrapped, bsn_series = app.bootstrap( |
74 | options.env_name, is_local=is_local, debug=options.debug) |
75 | |
76 | + # The admin secret was not in the environments file. Retrieve it from |
77 | + # the Juju-generated file. |
78 | + # Find the Juju-generated environment file. |
79 | + try: |
80 | + admin_secret = app.get_admin_secret( |
81 | + options.env_name, settings.JUJU_HOME) |
82 | + except ValueError as err: |
83 | + admin_secret = options.admin_secret |
84 | + if admin_secret is None: |
85 | + msg = err.message + ' or {}.'.format(options.env_file) |
86 | + raise app.ProgramExit(msg.encode('utf-8')) |
87 | + |
88 | print('retrieving the Juju API address') |
89 | api_url = app.get_api_url(options.env_name) |
90 | print('connecting to {}'.format(api_url)) |
91 | - env = app.connect(api_url, options.admin_secret) |
92 | + env = app.connect(api_url, admin_secret) |
93 | |
94 | # It is not possible to deploy on the bootstrap node if we are using the |
95 | # local provider, or if the bootstrap node series is not compatible with |
96 | @@ -406,10 +421,10 @@ |
97 | address = app.watch(env, unit_name) |
98 | env.close() |
99 | url = 'https://{}'.format(address) |
100 | - print('url: {}\npassword: {}'.format(url, options.admin_secret)) |
101 | + print('url: {}\npassword: {}'.format(url, admin_secret)) |
102 | gui_api_url = 'wss://{}:443/ws'.format(address) |
103 | print('connecting to the Juju GUI server') |
104 | - gui_env = app.connect(gui_api_url, options.admin_secret) |
105 | + gui_env = app.connect(gui_api_url, admin_secret) |
106 | |
107 | # Handle bundle deployment. |
108 | if options.bundle is not None: |
109 | |
110 | === modified file 'quickstart/models/envs.py' |
111 | --- quickstart/models/envs.py 2014-01-20 12:02:10 +0000 |
112 | +++ quickstart/models/envs.py 2014-01-29 14:27:01 +0000 |
113 | @@ -134,6 +134,23 @@ |
114 | return {'environments': {}} |
115 | |
116 | |
117 | +def _load_file(env_file): |
118 | + """Load the given file and return the YAML-parsed contents.""" |
119 | + # Load the Juju environments file. |
120 | + try: |
121 | + environments_file = open(env_file.encode('utf-8')) |
122 | + except IOError as err: |
123 | + msg = b'unable to open environments file: {}'.format(err) |
124 | + raise ValueError(msg) |
125 | + # Parse the Juju environments file. |
126 | + try: |
127 | + contents = serializers.yaml_load(environments_file) |
128 | + except Exception as err: |
129 | + msg = b'unable to parse environments file {}: {}' |
130 | + raise ValueError(msg.format(env_file.encode('utf-8'), err)) |
131 | + return contents |
132 | + |
133 | + |
134 | def load(env_file): |
135 | """Load and parse the provided Juju environments.yaml file. |
136 | |
137 | @@ -157,24 +174,13 @@ |
138 | - the environment file contents are not parsable by YAML; |
139 | - the YAML contents are not properly structured. |
140 | """ |
141 | - # Load the Juju environments file. |
142 | - try: |
143 | - environments_file = open(env_file.encode('utf-8')) |
144 | - except IOError as err: |
145 | - msg = b'unable to open environments file: {}'.format(err) |
146 | - raise ValueError(msg) |
147 | - # Parse the Juju environments file. |
148 | - try: |
149 | - contents = serializers.yaml_load(environments_file) |
150 | - except Exception as err: |
151 | - msg = b'unable to parse environments file {}: {}' |
152 | - raise ValueError(msg.format(env_file.encode('utf-8'), err)) |
153 | + contents = _load_file(env_file) |
154 | if contents is None: |
155 | return create_empty_env_db() |
156 | # Retrieve the environment list. |
157 | try: |
158 | env_contents = contents.get('environments', {}).items() |
159 | - except AttributeError as err: |
160 | + except AttributeError: |
161 | msg = 'invalid YAML contents in {}: {}'.format(env_file, contents) |
162 | raise ValueError(msg.encode('utf-8')) |
163 | environments = {} |
164 | @@ -193,6 +199,18 @@ |
165 | return env_db |
166 | |
167 | |
168 | +def load_generated(env_file, section='bootstrap-config'): |
169 | + """Given the path to a YAML file, load the file and return the section.""" |
170 | + contents = _load_file(env_file) |
171 | + try: |
172 | + section_contents = contents[section] |
173 | + except (KeyError, TypeError): |
174 | + msg = 'invalid YAML contents in {}: {}'.format(env_file, contents) |
175 | + raise ValueError(msg.encode('utf-8')) |
176 | + |
177 | + return section_contents |
178 | + |
179 | + |
180 | def save(env_file, env_db, backup_function=None): |
181 | """Save the given env_db to the provided environments.yaml file. |
182 | |
183 | @@ -331,8 +349,12 @@ |
184 | # Retrieve a list of missing required fields. |
185 | missing_fields = [ |
186 | field for field in env_metadata['fields'] |
187 | - if field.required and field.name not in env_data |
188 | - ] |
189 | + if field.required and field.name not in env_data] |
190 | + # The following fields are not generally required but should be |
191 | + # automatically generated for a local environment. |
192 | + forced_to_be_generated = ('admin-secret', ) |
193 | + missing_fields.extend([field for field in env_metadata['fields'] |
194 | + if field.name in forced_to_be_generated]) |
195 | # Assume all missing fields can be automatically generated. |
196 | env_data.update((field.name, field.generate()) for field in missing_fields) |
197 | return env_data |
198 | @@ -375,7 +397,7 @@ |
199 | 'name', label='environment name', required=True, |
200 | help='the environment name to use with Juju (arbitrary string)') |
201 | admin_secret_field = fields.AutoGeneratedPasswordField( |
202 | - 'admin-secret', label='admin secret', required=True, |
203 | + 'admin-secret', label='admin secret', required=False, |
204 | help='the password used to authenticate to the environment') |
205 | default_series_field = fields.ChoiceField( |
206 | 'default-series', choices=settings.JUJU_DEFAULT_SERIES, |
207 | @@ -636,7 +658,7 @@ |
208 | then those keys are assigned a generic default field (fields.Field). |
209 | """ |
210 | data = copy.deepcopy(env_data) |
211 | - # Start building a list of (field, value) pairs preserving fields order. |
212 | + # Start building a list of (field, value) pairs preserving field order. |
213 | field_value_pairs = [ |
214 | (field, data.pop(field.name, None)) |
215 | for field in env_metadata['fields'] |
216 | |
217 | === modified file 'quickstart/tests/cli/test_views.py' |
218 | --- quickstart/tests/cli/test_views.py 2014-01-16 11:17:42 +0000 |
219 | +++ quickstart/tests/cli/test_views.py 2014-01-29 14:27:01 +0000 |
220 | @@ -649,7 +649,7 @@ |
221 | |
222 | def test_save_invalid_form_data(self): |
223 | # Errors are displayed if the user tries to save invalid data. |
224 | - changes = {'admin-secret': ''} |
225 | + changes = {'name': ''} |
226 | with self.patch_create_form(changes=changes) as mock_create_form: |
227 | self.call_view(env_name='ec2-west') |
228 | self.assertTrue(mock_create_form.called) |
229 | @@ -661,7 +661,7 @@ |
230 | # The form has been re-rendered passing the errors. |
231 | self.assertEqual(2, mock_create_form.call_count) |
232 | self.assertEqual( |
233 | - {'admin-secret': 'a value is required for the admin secret field'}, |
234 | + {'name': 'a value is required for the environment name field'}, |
235 | mock_create_form.errors) |
236 | |
237 | def test_save_invalid_new_name(self): |
238 | |
239 | === modified file 'quickstart/tests/models/test_envs.py' |
240 | --- quickstart/tests/models/test_envs.py 2014-01-20 12:02:10 +0000 |
241 | +++ quickstart/tests/models/test_envs.py 2014-01-29 14:27:01 +0000 |
242 | @@ -98,27 +98,32 @@ |
243 | self.assertEqual({'environments': {}}, env_db) |
244 | |
245 | |
246 | -class TestLoad( |
247 | +class TestLoadFile( |
248 | helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin, |
249 | unittest.TestCase): |
250 | |
251 | def test_no_file(self): |
252 | # A ValueError is raised if the environments file is not found. |
253 | expected = ( |
254 | - 'unable to open environments file: ' |
255 | + "unable to open environments file: " |
256 | "[Errno 2] No such file or directory: '/no/such/file.yaml'" |
257 | ) |
258 | with self.assert_value_error(expected): |
259 | - envs.load('/no/such/file.yaml') |
260 | + envs._load_file('/no/such/file.yaml') |
261 | |
262 | def test_invalid_yaml(self): |
263 | # A ValueError is raised if the environments file is not a valid YAML. |
264 | env_file = self.make_env_file(':') |
265 | with self.assertRaises(ValueError) as context_manager: |
266 | - envs.load(env_file) |
267 | + envs._load_file(env_file) |
268 | expected = 'unable to parse environments file {}'.format(env_file) |
269 | self.assertIn(expected, bytes(context_manager.exception)) |
270 | |
271 | + |
272 | +class TestLoad( |
273 | + helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin, |
274 | + unittest.TestCase): |
275 | + |
276 | def test_empty_file(self): |
277 | # An empty environments database is returned if the file is empty. |
278 | env_file = self.make_env_file('') |
279 | @@ -199,6 +204,60 @@ |
280 | self.assertEqual(expected, env_db) |
281 | |
282 | |
283 | +class TestLoadGenerated( |
284 | + helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin, |
285 | + unittest.TestCase): |
286 | + |
287 | + def test_empty_file(self): |
288 | + # A ValueError is raised if the environments file is empty. |
289 | + env_file = self.make_env_file('') |
290 | + expected = 'invalid YAML contents in {}: None'.format(env_file) |
291 | + with self.assert_value_error(expected): |
292 | + envs.load_generated(env_file) |
293 | + |
294 | + def test_invalid_yaml_contents(self): |
295 | + # A ValueError is raised if the environments file is not well formed. |
296 | + env_file = self.make_env_file('a-string') |
297 | + expected = 'invalid YAML contents in {}: a-string'.format(env_file) |
298 | + with self.assert_value_error(expected): |
299 | + envs.load_generated(env_file) |
300 | + |
301 | + def test_section_not_found(self): |
302 | + expected = { |
303 | + 'shoehorn-config': { |
304 | + 'admin-secret': 'Secret!', |
305 | + 'type': 'ec2'}, |
306 | + } |
307 | + yaml_contents = expected.copy() |
308 | + env_file = self.make_env_file(yaml.safe_dump(yaml_contents)) |
309 | + expected = 'invalid YAML contents in {}: {}'.format( |
310 | + env_file, yaml_contents) |
311 | + with self.assert_value_error(expected): |
312 | + envs.load_generated(env_file) |
313 | + |
314 | + def test_successful_default_section(self): |
315 | + expected = { |
316 | + 'bootstrap-config': { |
317 | + 'admin-secret': 'Secret!', |
318 | + 'type': 'ec2'}, |
319 | + } |
320 | + yaml_contents = expected.copy() |
321 | + env_file = self.make_env_file(yaml.safe_dump(yaml_contents)) |
322 | + env_db = envs.load_generated(env_file) |
323 | + self.assertEqual(expected['bootstrap-config'], env_db) |
324 | + |
325 | + def test_successful_specified_section(self): |
326 | + expected = { |
327 | + 'my-config': { |
328 | + 'admin-secret': 'Secret!', |
329 | + 'type': 'ec2'}, |
330 | + } |
331 | + yaml_contents = expected.copy() |
332 | + env_file = self.make_env_file(yaml.safe_dump(yaml_contents)) |
333 | + env_db = envs.load_generated(env_file, section='my-config') |
334 | + self.assertEqual(expected['my-config'], env_db) |
335 | + |
336 | + |
337 | class TestSave(helpers.EnvFileTestsMixin, unittest.TestCase): |
338 | |
339 | def setUp(self): |
340 | @@ -661,7 +720,7 @@ |
341 | env_metadata = self.env_type_db['__fallback__'] |
342 | expected = [ |
343 | 'type', 'name', 'admin-secret', 'default-series', 'is-default'] |
344 | - expected_required = ['type', 'name', 'admin-secret', 'is-default'] |
345 | + expected_required = ['type', 'name', 'is-default'] |
346 | self.assert_fields(expected, env_metadata) |
347 | self.assert_required_fields(expected_required, env_metadata) |
348 | |
349 | @@ -673,7 +732,7 @@ |
350 | 'type', 'name', 'admin-secret', 'default-series', 'root-dir', |
351 | 'storage-port', 'shared-storage-port', 'network-bridge', |
352 | 'is-default'] |
353 | - expected_required = ['type', 'name', 'admin-secret', 'is-default'] |
354 | + expected_required = ['type', 'name', 'is-default'] |
355 | self.assert_fields(expected, env_metadata) |
356 | self.assert_required_fields(expected_required, env_metadata) |
357 | |
358 | @@ -685,7 +744,7 @@ |
359 | 'type', 'name', 'admin-secret', 'default-series', 'access-key', |
360 | 'secret-key', 'control-bucket', 'region', 'is-default'] |
361 | expected_required = [ |
362 | - 'type', 'name', 'admin-secret', 'access-key', 'secret-key', |
363 | + 'type', 'name', 'access-key', 'secret-key', |
364 | 'control-bucket', 'is-default'] |
365 | self.assert_fields(expected, env_metadata) |
366 | self.assert_required_fields(expected_required, env_metadata) |
367 | @@ -700,7 +759,7 @@ |
368 | 'region', 'auth-mode', 'username', 'password', 'access-key', |
369 | 'secret-key', 'is-default'] |
370 | expected_required = [ |
371 | - 'type', 'name', 'admin-secret', 'use-floating-ip', |
372 | + 'type', 'name', 'use-floating-ip', |
373 | 'control-bucket', 'auth-url', 'tenant-name', 'region', |
374 | 'is-default'] |
375 | self.assert_fields(expected, env_metadata) |
376 | @@ -715,7 +774,7 @@ |
377 | 'management-subscription-id', 'management-certificate-path', |
378 | 'storage-account-name', 'is-default'] |
379 | expected_required = [ |
380 | - 'type', 'name', 'admin-secret', 'location', |
381 | + 'type', 'name', 'location', |
382 | 'management-subscription-id', 'management-certificate-path', |
383 | 'storage-account-name', 'is-default'] |
384 | self.assert_fields(expected, env_metadata) |
385 | |
386 | === modified file 'quickstart/tests/test_app.py' |
387 | --- quickstart/tests/test_app.py 2014-01-17 14:05:03 +0000 |
388 | +++ quickstart/tests/test_app.py 2014-01-29 14:27:01 +0000 |
389 | @@ -1311,3 +1311,23 @@ |
390 | with self.assertRaises(ValueError) as context_manager: |
391 | app.deploy_bundle(env, self.yaml, self.name, None) |
392 | self.assertIs(error, context_manager.exception) |
393 | + |
394 | + |
395 | +class TestGetAdminSecret(unittest.TestCase): |
396 | + |
397 | + def test_no_admin_secret(self): |
398 | + with mock.patch('quickstart.manage.envs.load_generated', |
399 | + lambda x: {}): |
400 | + with self.assertRaises(ValueError) as exc: |
401 | + app.get_admin_secret('local', '/home/bac/.juju') |
402 | + expected = ( |
403 | + u'admin-secret not found in ' |
404 | + '/home/bac/.juju/environments/local.jenv') |
405 | + self.assertIn(expected, bytes(exc.exception)) |
406 | + |
407 | + def test_success(self): |
408 | + expected = 'superchunk' |
409 | + with mock.patch('quickstart.manage.envs.load_generated', |
410 | + lambda x: {'admin-secret': expected}): |
411 | + secret = app.get_admin_secret('local', '~bac/.juju') |
412 | + self.assertEqual(expected, secret) |
413 | |
414 | === modified file 'quickstart/tests/test_manage.py' |
415 | --- quickstart/tests/test_manage.py 2014-01-13 15:46:27 +0000 |
416 | +++ quickstart/tests/test_manage.py 2014-01-29 14:27:01 +0000 |
417 | @@ -38,6 +38,7 @@ |
418 | from quickstart.cli import views |
419 | from quickstart.models import envs |
420 | from quickstart.tests import helpers |
421 | +from quickstart import app |
422 | |
423 | |
424 | class TestDescriptionAction(unittest.TestCase): |
425 | @@ -393,8 +394,7 @@ |
426 | self.parser, self.env_type_db, self.env_db, 'local-with-errors') |
427 | self.parser.error.assert_called_once_with( |
428 | 'cannot use the local-with-errors environment:\n' |
429 | - 'the storage port field requires an integer value\n' |
430 | - 'a value is required for the admin secret field') |
431 | + 'the storage port field requires an integer value') |
432 | |
433 | |
434 | class TestSetupEnv(helpers.EnvFileTestsMixin, unittest.TestCase): |
435 | @@ -658,12 +658,24 @@ |
436 | options.update(kwargs) |
437 | return mock.Mock(**options) |
438 | |
439 | + @staticmethod |
440 | + def mock_get_admin_secret_success(name, home): |
441 | + return 'jenv secret' |
442 | + |
443 | + @staticmethod |
444 | + def mock_get_admin_secret_error(name, home): |
445 | + fn = '{}.jenv'.format(name) |
446 | + path = os.path.join(home, 'environments', fn) |
447 | + msg = 'admin-secret not found in {}'.format(path) |
448 | + raise ValueError(msg.encode('utf-8')) |
449 | + |
450 | def test_no_bundle(self, mock_app, mock_open): |
451 | # The application runs correctly if no bundle is provided. |
452 | token = 'AUTHTOKEN' |
453 | mock_app.create_auth_token.return_value = token |
454 | mock_app.watch.return_value = '1.2.3.4' |
455 | mock_app.bootstrap.return_value = (True, 'precise') |
456 | + mock_app.get_admin_secret = self.mock_get_admin_secret_error |
457 | options = self.make_options() |
458 | manage.run(options) |
459 | mock_app.ensure_dependencies.assert_called() |
460 | @@ -722,3 +734,41 @@ |
461 | options = self.make_options(open_browser=False) |
462 | manage.run(options) |
463 | self.assertFalse(mock_open.called) |
464 | + |
465 | + def test_admin_secret_fetched(self, mock_app, mock_open): |
466 | + # If an admin secret is fetched from jenv it is used, even if one is |
467 | + # found in environments.yaml, as set in options.admin_secret. |
468 | + mock_app.get_admin_secret = self.mock_get_admin_secret_success |
469 | + mock_app.bootstrap.return_value = (True, 'precise') |
470 | + options = self.make_options(admin_secret='secret in environments.yaml') |
471 | + manage.run(options) |
472 | + mock_app.connect.assert_has_calls([ |
473 | + mock.call(mock_app.get_api_url(), 'jenv secret'), |
474 | + ]) |
475 | + |
476 | + def test_admin_secret_from_environments_yaml(self, mock_app, mock_open): |
477 | + # If an admin secret is not fetched from jenv, then the one from |
478 | + # environments.yaml is used, as found in options.admin_secret. |
479 | + mock_app.get_admin_secret = self.mock_get_admin_secret_error |
480 | + mock_app.bootstrap.return_value = (True, 'precise') |
481 | + options = self.make_options(admin_secret='secret in environments.yaml') |
482 | + manage.run(options) |
483 | + mock_app.connect.assert_has_calls([ |
484 | + mock.call(mock_app.get_api_url(), 'secret in environments.yaml'), |
485 | + ]) |
486 | + |
487 | + def test_no_admin_secret_found(self, mock_app, mock_open): |
488 | + # If admin-secret cannot be found anywhere a ProgramExit is called. |
489 | + mock_app.ProgramExit = app.ProgramExit |
490 | + mock_app.get_admin_secret = self.mock_get_admin_secret_error |
491 | + mock_app.bootstrap.return_value = (True, 'precise') |
492 | + options = self.make_options( |
493 | + admin_secret=None, |
494 | + env_name='local', |
495 | + env_file='environments.yaml') |
496 | + with self.assertRaises(app.ProgramExit) as context: |
497 | + manage.run(options) |
498 | + expected = ( |
499 | + u'admin-secret not found in ~/.juju/environments/local.jenv ' |
500 | + 'or environments.yaml.') |
501 | + self.assertEqual(expected, context.exception.message) |
Reviewers: mp+203631_ code.launchpad. net,
Message:
Please take a look.
Description:
Get admin_secret from juju-generated jenv file.
As of Juju v 1.17 the admin-secret is stored in a generated file, environments/ <environment> .jenv.
located in
$JUJU_HOME/
If an admin-secret is provided in environments.yaml, it is used and also
stored in the jenv file. If none is given, then juju fabricates one and
stores it in the jenv file.
The admin-secret *field* is marked as optional now. For the case where
a
local environment is used, Quickstart previously would generate an
admin-secret. That behavior is maintained by forcing 'admin-secret' to
be in
the missing_fields list. This approach ensures backwards compatibility.
Note that the Juju-generated admin-secret is not available until after
bootstrapping the environment. Due to this behavior, when using juju
v1.16
and not admin-secret is configured in the environments.yaml file, the
failure
will not be reported until after bootstrapping. This approach seemed
better
than introducing hard version checking.
https:/ /code.launchpad .net/~bac/ juju-quickstart /auth/+ merge/203631
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/57900043/
Affected files (+174, -31 lines): manage. py models/ envs.py tests/cli/ test_views. py tests/models/ test_envs. py tests/test_ manage. py
A [revision details]
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/