Merge ~matt-goodall/snapstore-client:per-store-config into snapstore-client:master

Proposed by Matt Goodall
Status: Merged
Approved by: Matt Goodall
Approved revision: 819d7029bab7902b0b62c558e503e54fb497ae5f
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~matt-goodall/snapstore-client:per-store-config
Merge into: snapstore-client:master
Diff against target: 1307 lines (+402/-372)
11 files modified
README (+5/-17)
snapstore (+9/-1)
snapstore_client/config.py (+44/-125)
snapstore_client/logic/login.py (+22/-11)
snapstore_client/logic/overrides.py (+36/-4)
snapstore_client/logic/tests/test_login.py (+83/-27)
snapstore_client/logic/tests/test_overrides.py (+35/-11)
snapstore_client/tests/test_config.py (+66/-63)
snapstore_client/tests/test_webservices.py (+51/-56)
snapstore_client/tests/testfixtures.py (+28/-27)
snapstore_client/webservices.py (+23/-30)
Reviewer Review Type Date Requested Status
Matt Goodall (community) Approve
Adam Collard (community) Approve
Review via email: mp+331064@code.launchpad.net

Commit message

Configure a "store" at login and use that config for future calls.

Replaces old config consisting of fixed service URLs (too fine-grained anyway) and per-store credentials. A store's config now consists of the store's URL, SSO's URL, and the user credentials (macaroons etc).

For now, there is only one store - called "default" - but the command line could be extended to login and then select a store with a different name.

Description of the change

The diff makes it look more complicated than it is I think.

To help review ...

The big difference is that there's no global/fixed state any more - config is loaded at the start of a command and explicitly passed through to things like the webservices calls. No more importing config from files relative to Python modules \o/.

As a result, config management changed quite a lot although was largely based on the old Credentials bit of the config. It's actually simpler now.

To post a comment you must log in.
Revision history for this message
Adam Collard (adam-collard) :
review: Approve
Revision history for this message
Matt Goodall (matt-goodall) :
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :
Revision history for this message
Matt Goodall (matt-goodall) :
review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README b/README
2index 8c40b12..6171594 100644
3--- a/README
4+++ b/README
5@@ -1,20 +1,8 @@
6-SIAB client script.
7-===================
8+snapstore administration client
9+===============================
10
11+This repo contains a client to adminster a snapstore.
12
13-This repo contains a few points of interest:
14+After authenticating with a snapstore the administrator can:
15
16- * The serve-snaps.py script will start a simple HTTP server to serve snap
17- files from 'snap_storage'. Please don't use this in production, it's only
18- supposed to be used for dev setups.
19-
20- * The snapstore script is a CLI UI to administer the snap store. Currently the
21- only command is 'upload'. It can do two things:
22-
23- - upload a snap to the store, but don't release it::
24-
25- $ snapstore upload /path/to/file.snap
26-
27- - upload and release a snap to a given channel:
28-
29- $ snapstore upload --channel /path/to/file.snap
30+ - manage revision overrides
31diff --git a/snapstore b/snapstore
32index ec0978d..9c2b0b1 100755
33--- a/snapstore
34+++ b/snapstore
35@@ -17,6 +17,9 @@ from snapstore_client.logic.overrides import (
36 )
37
38
39+DEFAULT_SSO_URL = 'https://login.ubuntu.com/'
40+
41+
42 def main():
43 configure_logging()
44 args = parse_args()
45@@ -27,8 +30,13 @@ def parse_args():
46 parser = argparse.ArgumentParser()
47 subparsers = parser.add_subparsers(help='sub-command help')
48
49- login_parser = subparsers.add_parser('login', help='Sign into a store.')
50+ login_parser = subparsers.add_parser(
51+ 'login', help='Sign into a store.',
52+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
53+ login_parser.add_argument('store_url', help='Store URL')
54 login_parser.add_argument('email', help='Ubuntu One SSO email', nargs='?')
55+ login_parser.add_argument('--sso-url', help='Ubuntu One SSO URL',
56+ default=DEFAULT_SSO_URL)
57 login_parser.set_defaults(func=login)
58
59 list_overrides_parser = subparsers.add_parser(
60diff --git a/snapstore_client/config.py b/snapstore_client/config.py
61index 94df99b..ca7beb9 100644
62--- a/snapstore_client/config.py
63+++ b/snapstore_client/config.py
64@@ -6,150 +6,69 @@
65
66 import configparser
67 import os
68-from urllib.parse import urlparse
69
70 from xdg import BaseDirectory
71
72
73-ROOT = os.path.abspath(
74- os.path.join(
75- os.path.dirname(__file__),
76- '..'
77- )
78-)
79+class Config:
80
81-
82-DEFAULT_TEST_CONFIG = {
83- 'services': {
84- 'snapdevicegw': 'http://localhost:8000/',
85- 'storage': 'http://localhost:8005/',
86- }
87-}
88-
89-
90-def _production_read_config():
91- root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
92- config = configparser.ConfigParser(dict(ROOT=root))
93- default_path = os.path.join(root, 'service.conf')
94- config.read(default_path)
95- return {sec: dict(config.items(sec)) for sec in config.sections()}
96-
97-
98-class ConfigProvider(object):
99-
100- """Provide a single entry point for reading config that can be altered.
101-
102- This class allows production code to call 'read_config' and get a copy of
103- test configuration during test runs, but production config during normal
104- operations.
105- """
106-
107- def __init__(self, default_test_config):
108- self._test_override = []
109- self._default_test_config = default_test_config
110-
111- def __call__(self):
112- if self._test_override:
113- return self._test_override[-1]
114- else:
115- return _production_read_config()
116-
117- def merge_config(self, config, new_config):
118- merged_config = config.copy()
119- for key in new_config.keys():
120- if key in config.keys():
121- if isinstance(config[key], dict) and \
122- isinstance(new_config[key], dict):
123- merged_config[key] = self.merge_config(
124- config[key], new_config[key])
125- else:
126- merged_config[key] = new_config[key]
127- else:
128- merged_config[key] = new_config[key]
129- return merged_config
130-
131- def override_for_test(self, new_config):
132- """Override configuration for a test.
133-
134- In a test, if you wanted to set a value, you can do this::
135-
136- from snapdevicegw.config import read_config
137-
138- def test_something(self):
139- new_config = {
140- 'worker': {
141- 'some_setting': 'some_value'
142- }
143- }
144- self.addCleanup(read_config.override_for_test(new_config))
145-
146- The configuration provided is merged on top of DEFAULT_TEST_CONFIG
147- (see above), so there's no need to specify values that are in the
148- default config.
149- """
150- if self._test_override:
151- source = self._test_override[-1]
152- else:
153- source = self._default_test_config.copy()
154- config = self.merge_config(source, new_config)
155- self._test_override.append(config)
156- return self._test_override.pop
157-
158-
159-read_config = ConfigProvider(DEFAULT_TEST_CONFIG)
160-
161-
162-class Credentials:
163- """Per-user credentials storage."""
164+ xdg_name = 'snapstore-client'
165
166 def __init__(self):
167 self.parser = configparser.ConfigParser()
168 self.load()
169
170- @property
171- def _section_name(self):
172- # The only section we care about is the host from the snapdevicegw
173- # URL.
174- return urlparse(read_config()['services']['snapdevicegw']).netloc
175+ def load(self):
176+ path = BaseDirectory.load_first_config(
177+ self.xdg_name, 'config.ini')
178+ if path is not None and os.path.exists(path):
179+ self.parser.read(path)
180+
181+ def save(self):
182+ path = os.path.join(
183+ BaseDirectory.save_config_path(self.xdg_name),
184+ 'config.ini')
185+ # TODO: better to write atomically
186+ with open(path, 'w') as f:
187+ self.parser.write(f)
188
189- def get(self, option_name):
190+ def get(self, section, option):
191 try:
192- return self.parser.get(self._section_name, option_name)
193+ return self.parser.get(section, option)
194 except (configparser.NoSectionError,
195 configparser.NoOptionError,
196 KeyError):
197 return None
198
199- def set(self, option_name, value):
200- section_name = self._section_name
201- if not self.parser.has_section(section_name):
202- self.parser.add_section(section_name)
203- return self.parser.set(section_name, option_name, value)
204+ def set(self, section, option, value):
205+ if not self.parser.has_section(section):
206+ self.parser.add_section(section)
207+ return self.parser.set(section, option, value)
208
209- def is_empty(self):
210- # Only check the current section
211- section_name = self._section_name
212- if self.parser.has_section(section_name):
213- if self.parser.options(section_name):
214- return False
215- return True
216+ def section(self, name):
217+ return Section(self, name)
218
219- def load(self):
220- path = BaseDirectory.load_first_config(
221- 'snapstore-client', 'credentials.cfg')
222- if path is not None and os.path.exists(path):
223- self.parser.read(path)
224+ def store_section(self, name):
225+ return Section(self, 'store:'+name)
226
227- @staticmethod
228- def save_path():
229- return os.path.join(
230- BaseDirectory.save_config_path('snapstore-client'),
231- 'credentials.cfg')
232
233- def save(self):
234- path = self.save_path()
235- with open(path, 'w') as f:
236- self.parser.write(f)
237+class Section:
238
239- def clear(self):
240- self.parser.remove_section(self._section_name)
241+ def __init__(self, config, name):
242+ self.config = config
243+ self.name = name
244+
245+ def get(self, option):
246+ return self.config.get(self.name, option)
247+
248+ def set(self, option, value):
249+ return self.config.set(self.name, option, value)
250+
251+ # XXX: mostly a hack
252+ # I don't think it's nice to expose save like this because it implies only
253+ # the section is updated. However, the webservices code currently refreshes
254+ # a store's macaroon and therefore needs to update and save config.
255+ # A better approach may be to *always* save config at the end of the
256+ # process so the internal implementation doesn't care about persistence.
257+ def save(self):
258+ self.config.save()
259diff --git a/snapstore_client/logic/login.py b/snapstore_client/logic/login.py
260index 47ab6b8..2963aef 100644
261--- a/snapstore_client/logic/login.py
262+++ b/snapstore_client/logic/login.py
263@@ -16,9 +16,9 @@ from snapstore_client import (
264 logger = logging.getLogger(__name__)
265
266
267-def _extract_caveat_id(root_macaroon):
268+def _extract_caveat_id(sso_url, root_macaroon):
269 macaroon = Macaroon.deserialize(root_macaroon)
270- sso_host = urlparse(config.read_config()['services']['sso']).netloc
271+ sso_host = urlparse(sso_url).netloc
272 for caveat in macaroon.caveats:
273 if caveat.location == sso_host:
274 return caveat.caveat_id
275@@ -27,22 +27,26 @@ def _extract_caveat_id(root_macaroon):
276
277
278 def login(args):
279+ # TODO: validate these before using to avoid ugly errors.
280+ gw_url = args.store_url
281+ sso_url = args.sso_url
282+
283 logger.info('Enter your Ubuntu One SSO credentials.')
284 email = args.email
285 if not email:
286 email = input('Email: ')
287 password = getpass.getpass('Password: ')
288
289- root = ws.issue_store_admin()
290- caveat_id = _extract_caveat_id(root)
291+ root = ws.issue_store_admin(gw_url)
292+ caveat_id = _extract_caveat_id(sso_url, root)
293 try:
294 try:
295 unbound_discharge = ws.get_sso_discharge(
296- email, password, caveat_id)
297+ sso_url, email, password, caveat_id)
298 except exceptions.StoreTwoFactorAuthenticationRequired:
299 one_time_password = input('Second-factor auth: ')
300 unbound_discharge = ws.get_sso_discharge(
301- email, password, caveat_id,
302+ sso_url, email, password, caveat_id,
303 one_time_password=one_time_password)
304 except exceptions.StoreAuthenticationError as e:
305 logger.error('Login failed.')
306@@ -54,10 +58,17 @@ def login(args):
307 logger.error('%s: %s', key, value)
308 return 1
309
310- credentials = config.Credentials()
311- credentials.set('root', root)
312- credentials.set('unbound_discharge', unbound_discharge)
313- credentials.set('email', email)
314- credentials.save()
315+ cfg = config.Config()
316+ # For now, the store is always called "default". In the future we may want
317+ # to support multiple stores by allowing the user to provide a nice name
318+ # for a store at login that can be used to select the store for later
319+ # operations.
320+ store = cfg.store_section('default')
321+ store.set('gw_url', gw_url)
322+ store.set('sso_url', sso_url)
323+ store.set('root', root)
324+ store.set('unbound_discharge', unbound_discharge)
325+ store.set('email', email)
326+ cfg.save()
327
328 return 0
329diff --git a/snapstore_client/logic/overrides.py b/snapstore_client/logic/overrides.py
330index f514c48..54e421e 100644
331--- a/snapstore_client/logic/overrides.py
332+++ b/snapstore_client/logic/overrides.py
333@@ -5,6 +5,7 @@ import logging
334 from requests.exceptions import HTTPError
335
336 from snapstore_client import (
337+ config,
338 exceptions,
339 webservices as ws,
340 )
341@@ -19,13 +20,30 @@ logger = logging.getLogger(__name__)
342
343 def _log_credentials_error(e):
344 logger.error('%s', e)
345- logger.error('Have you run "snapstore login"?')
346+ logger.error('Try to "snapstore login" again.')
347+
348+
349+def _check_default_store(cfg):
350+ """Load the default store from the config."""
351+ store = cfg.store_section('default')
352+ # If the gw URL is configured then everything else should be too.
353+ if not store.get('gw_url'):
354+ logger.error(
355+ 'No store configuration found. Have you run "snapstore login"?')
356+ return None
357+ return store
358
359
360 def list_overrides(args):
361+ cfg = config.Config()
362+ store = _check_default_store(cfg)
363+ if not store:
364+ return 1
365+
366 try:
367 response = ws.refresh_if_necessary(
368- ws.get_overrides, args.snap_name, series=args.series)
369+ store, ws.get_overrides,
370+ store, args.snap_name, series=args.series)
371 except exceptions.InvalidCredentials as e:
372 _log_credentials_error(e)
373 return 1
374@@ -37,6 +55,11 @@ def list_overrides(args):
375
376
377 def override(args):
378+ cfg = config.Config()
379+ store = _check_default_store(cfg)
380+ if not store:
381+ return 1
382+
383 overrides = []
384 for channel_map_entry in args.channel_map_entries:
385 channel, revision = channel_map_string_to_tuple(channel_map_entry)
386@@ -47,7 +70,9 @@ def override(args):
387 'series': args.series,
388 })
389 try:
390- response = ws.refresh_if_necessary(ws.set_overrides, overrides)
391+ response = ws.refresh_if_necessary(
392+ store, ws.set_overrides,
393+ store, overrides)
394 except exceptions.InvalidCredentials as e:
395 _log_credentials_error(e)
396 return 1
397@@ -59,6 +84,11 @@ def override(args):
398
399
400 def delete_override(args):
401+ cfg = config.Config()
402+ store = _check_default_store(cfg)
403+ if not store:
404+ return 1
405+
406 overrides = []
407 for channel in args.channels:
408 overrides.append({
409@@ -68,7 +98,9 @@ def delete_override(args):
410 'series': args.series,
411 })
412 try:
413- response = ws.refresh_if_necessary(ws.set_overrides, overrides)
414+ response = ws.refresh_if_necessary(
415+ store, ws.set_overrides,
416+ store, overrides)
417 except exceptions.InvalidCredentials as e:
418 _log_credentials_error(e)
419 return 1
420diff --git a/snapstore_client/logic/tests/test_login.py b/snapstore_client/logic/tests/test_login.py
421index 20d9d74..25ef14f 100644
422--- a/snapstore_client/logic/tests/test_login.py
423+++ b/snapstore_client/logic/tests/test_login.py
424@@ -2,9 +2,8 @@
425
426 import json
427 from unittest import mock
428-from urllib.parse import urljoin
429+from urllib.parse import urljoin, urlparse
430
431-from acceptable._doubles import set_service_locations
432 import fixtures
433 from pymacaroons import Macaroon
434 import responses
435@@ -20,20 +19,15 @@ from snapstore_client import (
436 exceptions,
437 )
438 from snapstore_client.logic.login import login
439-from snapstore_client.tests import (
440- factory,
441- testfixtures,
442-)
443+from snapstore_client.tests import factory
444
445
446 class LoginTests(TestCase):
447
448 def setUp(self):
449 super().setUp()
450- self.useFixture(testfixtures.ConfigOverrideFixture(
451- {'services': {'sso': 'https://login.example.com/'}}))
452- service_locations = config.read_config()['services']
453- set_service_locations(service_locations)
454+ self.default_gw_url = 'http://store.local/'
455+ self.default_sso_url = 'https://login.staging.ubuntu.com/'
456 self.logger = self.useFixture(fixtures.FakeLogger())
457 self.config_path = self.useFixture(fixtures.TempDir()).path
458 self.useFixture(fixtures.MonkeyPatch(
459@@ -60,27 +54,33 @@ class LoginTests(TestCase):
460 iter_responses = iter(full_responses)
461 return lambda request: next(iter_responses)
462
463- def make_args(self, email=None):
464- return factory.Args(email=email)
465+ def make_args(self, store_url=None, sso_url=None, email=None):
466+ return factory.Args(
467+ store_url=store_url or self.default_gw_url,
468+ sso_url=sso_url or self.default_sso_url,
469+ email=email,
470+ )
471
472- def add_issue_store_admin_response(self, *response_templates):
473- devicegw_root = config.read_config()['services']['snapdevicegw']
474- issue_store_admin_url = urljoin(
475- devicegw_root, '/v2/auth/issue-store-admin')
476+ def add_issue_store_admin_response(self, *response_templates, gw_url=None):
477+ gw_url = gw_url or self.default_gw_url
478+ issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin')
479 responses.add_callback(
480 'POST', issue_store_admin_url,
481 self.make_responses_callback(response_templates))
482
483- def add_get_sso_discharge_response(self, *response_templates):
484- sso_root = config.read_config()['services']['sso']
485- discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge')
486+ def add_get_sso_discharge_response(self, *response_templates,
487+ sso_url=None):
488+ sso_url = sso_url or self.default_sso_url
489+ discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge')
490 responses.add_callback(
491 'POST', discharge_url,
492 self.make_responses_callback(response_templates))
493
494- def make_root_macaroon(self):
495+ def make_root_macaroon(self, sso_url=None):
496 macaroon = Macaroon()
497- macaroon.add_third_party_caveat('login.example.com', 'key', 'payload')
498+ sso_url = sso_url or self.default_sso_url
499+ sso_host = urlparse(sso_url).netloc
500+ macaroon.add_third_party_caveat(sso_host, 'key', 'payload')
501 return macaroon.serialize()
502
503 @responses.activate
504@@ -161,8 +161,10 @@ class LoginTests(TestCase):
505 'caveat_id': 'payload',
506 'otp': '123456',
507 }, json.loads(responses.calls[2].request.body.decode()))
508- self.assertThat(config.Credentials().parser, ContainsDict({
509- 'localhost:8000': MatchesDict({
510+ self.assertThat(config.Config().parser, ContainsDict({
511+ 'store:default': MatchesDict({
512+ 'gw_url': Equals(self.default_gw_url),
513+ 'sso_url': Equals(self.default_sso_url),
514 'root': Equals(root),
515 'unbound_discharge': Equals('dummy'),
516 'email': Equals('user@example.org'),
517@@ -190,8 +192,10 @@ class LoginTests(TestCase):
518 'password': 'secret',
519 'caveat_id': 'payload',
520 }, json.loads(responses.calls[1].request.body.decode()))
521- self.assertThat(config.Credentials().parser, ContainsDict({
522- 'localhost:8000': MatchesDict({
523+ self.assertThat(config.Config().parser, ContainsDict({
524+ 'store:default': MatchesDict({
525+ 'gw_url': Equals(self.default_gw_url),
526+ 'sso_url': Equals(self.default_sso_url),
527 'root': Equals(root),
528 'unbound_discharge': Equals('dummy'),
529 'email': Equals('user@example.org'),
530@@ -219,10 +223,62 @@ class LoginTests(TestCase):
531 'password': 'secret',
532 'caveat_id': 'payload',
533 }, json.loads(responses.calls[1].request.body.decode()))
534- self.assertThat(config.Credentials().parser, ContainsDict({
535- 'localhost:8000': MatchesDict({
536+ self.assertThat(config.Config().parser, ContainsDict({
537+ 'store:default': MatchesDict({
538+ 'gw_url': Equals(self.default_gw_url),
539+ 'sso_url': Equals(self.default_sso_url),
540 'root': Equals(root),
541 'unbound_discharge': Equals('dummy'),
542 'email': Equals('user@example.org'),
543 }),
544 }))
545+
546+ @responses.activate
547+ def test_store_url(self):
548+ gw_url = 'http://otherstore.local:1234/'
549+
550+ self.mock_input.return_value = 'user@example.org'
551+ self.mock_getpass.return_value = 'secret'
552+ root = self.make_root_macaroon()
553+ self.add_issue_store_admin_response(
554+ {'status': 200, 'json': {'macaroon': root}}, gw_url=gw_url)
555+ self.add_get_sso_discharge_response(
556+ {'status': 200, 'json': {'discharge_macaroon': 'dummy'}})
557+ login(self.make_args(store_url=gw_url))
558+
559+ self.assertEqual(2, len(responses.calls))
560+ self.assertEqual(responses.calls[0].request.url[:len(gw_url)], gw_url)
561+ self.assertTrue(
562+ responses.calls[1].request.url.startswith(self.default_sso_url))
563+ self.assertThat(config.Config().parser, ContainsDict({
564+ 'store:default': ContainsDict({
565+ 'gw_url': Equals(gw_url),
566+ 'sso_url': Equals(self.default_sso_url),
567+ }),
568+ }))
569+
570+ @responses.activate
571+ def test_sso_url(self):
572+ sso_url = 'https://othersso.local:1234/'
573+
574+ self.mock_input.return_value = 'user@example.org'
575+ self.mock_getpass.return_value = 'secret'
576+ root = self.make_root_macaroon(sso_url=sso_url)
577+ self.add_issue_store_admin_response(
578+ {'status': 200, 'json': {'macaroon': root}})
579+ self.add_get_sso_discharge_response(
580+ {'status': 200, 'json': {'discharge_macaroon': 'dummy'}},
581+ sso_url=sso_url)
582+ login(self.make_args(sso_url=sso_url))
583+
584+ self.assertEqual(2, len(responses.calls))
585+ self.assertTrue(
586+ responses.calls[0].request.url.startswith(self.default_gw_url))
587+ self.assertEqual(
588+ responses.calls[1].request.url[:len(sso_url)], sso_url)
589+ self.assertThat(config.Config().parser, ContainsDict({
590+ 'store:default': ContainsDict({
591+ 'gw_url': Equals(self.default_gw_url),
592+ 'sso_url': Equals(sso_url),
593+ }),
594+ }))
595diff --git a/snapstore_client/logic/tests/test_overrides.py b/snapstore_client/logic/tests/test_overrides.py
596index 80a929f..b92c966 100644
597--- a/snapstore_client/logic/tests/test_overrides.py
598+++ b/snapstore_client/logic/tests/test_overrides.py
599@@ -3,7 +3,6 @@
600 import json
601 from urllib.parse import urljoin
602
603-from acceptable._doubles import set_service_locations
604 import fixtures
605 import responses
606 from testtools import TestCase
607@@ -22,15 +21,19 @@ from snapstore_client.tests import (
608
609 class OverridesTests(TestCase):
610
611- def setUp(self):
612- super().setUp()
613- service_locations = config.read_config()['services']
614- set_service_locations(service_locations)
615+ def test_list_overrides_no_store_config(self):
616+ self.useFixture(testfixtures.ConfigFixture(empty=True))
617+ logger = self.useFixture(fixtures.FakeLogger())
618+ rc = list_overrides(factory.Args(snap_name='some-snap', series='16'))
619+ self.assertEqual(rc, 1)
620+ self.assertEqual(
621+ logger.output,
622+ 'No store configuration found. Have you run "snapstore login"?\n')
623
624 @responses.activate
625 def test_list_overrides(self):
626+ self.useFixture(testfixtures.ConfigFixture())
627 logger = self.useFixture(fixtures.FakeLogger())
628- self.useFixture(testfixtures.CredentialsFixture())
629 snap_id = factory.generate_snap_id()
630 overrides = [
631 factory.SnapDeviceGateway.Override(
632@@ -43,7 +46,7 @@ class OverridesTests(TestCase):
633 # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once
634 # they exist.
635 overrides_url = urljoin(
636- config.read_config()['services']['snapdevicegw'],
637+ config.Config().store_section('default').get('gw_url'),
638 '/v2/metadata/overrides/mysnap')
639 responses.add(
640 'GET', overrides_url, status=200, json={'overrides': overrides})
641@@ -54,10 +57,21 @@ class OverridesTests(TestCase):
642 'mysnap foo/stable i386 3 (upstream 4)\n',
643 logger.output)
644
645+ def test_override_no_store_config(self):
646+ self.useFixture(testfixtures.ConfigFixture(empty=True))
647+ logger = self.useFixture(fixtures.FakeLogger())
648+ rc = override(factory.Args(
649+ snap_name='some-snap', channel_map_entries=['stable=1'],
650+ series='16'))
651+ self.assertEqual(rc, 1)
652+ self.assertEqual(
653+ logger.output,
654+ 'No store configuration found. Have you run "snapstore login"?\n')
655+
656 @responses.activate
657 def test_override(self):
658+ self.useFixture(testfixtures.ConfigFixture())
659 logger = self.useFixture(fixtures.FakeLogger())
660- self.useFixture(testfixtures.CredentialsFixture())
661 snap_id = factory.generate_snap_id()
662 overrides = [
663 factory.SnapDeviceGateway.Override(
664@@ -70,7 +84,7 @@ class OverridesTests(TestCase):
665 # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once
666 # they exist.
667 overrides_url = urljoin(
668- config.read_config()['services']['snapdevicegw'],
669+ config.Config().store_section('default').get('gw_url'),
670 '/v2/metadata/overrides')
671 responses.add(
672 'POST', overrides_url, status=200, json={'overrides': overrides})
673@@ -98,10 +112,20 @@ class OverridesTests(TestCase):
674 'mysnap foo/stable i386 3 (upstream 4)\n',
675 logger.output)
676
677+ def test_delete_override_no_store_config(self):
678+ self.useFixture(testfixtures.ConfigFixture(empty=True))
679+ logger = self.useFixture(fixtures.FakeLogger())
680+ rc = delete_override(factory.Args(
681+ snap_name='some-snap', channels=['stable'], series='16'))
682+ self.assertEqual(rc, 1)
683+ self.assertEqual(
684+ logger.output,
685+ 'No store configuration found. Have you run "snapstore login"?\n')
686+
687 @responses.activate
688 def test_delete_override(self):
689+ self.useFixture(testfixtures.ConfigFixture())
690 logger = self.useFixture(fixtures.FakeLogger())
691- self.useFixture(testfixtures.CredentialsFixture())
692 snap_id = factory.generate_snap_id()
693 overrides = [
694 factory.SnapDeviceGateway.Override(
695@@ -114,7 +138,7 @@ class OverridesTests(TestCase):
696 # XXX cjwatson 2017-06-26: Use acceptable-generated doubles once
697 # they exist.
698 overrides_url = urljoin(
699- config.read_config()['services']['snapdevicegw'],
700+ config.Config().store_section('default').get('gw_url'),
701 '/v2/metadata/overrides')
702 responses.add(
703 'POST', overrides_url, status=200, json={'overrides': overrides})
704diff --git a/snapstore_client/tests/test_config.py b/snapstore_client/tests/test_config.py
705index 0940828..2ddbc5c 100644
706--- a/snapstore_client/tests/test_config.py
707+++ b/snapstore_client/tests/test_config.py
708@@ -1,71 +1,74 @@
709 # Copyright 2017 Canonical Ltd. This software is licensed under the
710 # GNU General Public License version 3 (see the file LICENSE).
711
712+import os.path
713+from textwrap import dedent
714+
715 from testtools import TestCase
716
717-from snapstore_client.config import ConfigProvider
718-
719-
720-class ConfigProviderTestCase(TestCase):
721-
722- def test_override_works_for_new_section_and_option(self):
723- default = {}
724- override = {
725- 'new_section': {
726- 'new_option': 'new_value'
727- }
728- }
729- expected = override.copy()
730-
731- provider = ConfigProvider(default)
732- provider.override_for_test(override)
733-
734- self.assertEqual(expected, provider())
735-
736- def test_override_works_when_adding_new_option_to_existing_section(self):
737- default = {
738- 'section': {
739- 'option': 'value'
740- }
741- }
742- override = {
743- 'section': {
744- 'new_option': 'new_value'
745- }
746- }
747- expected = {
748- 'section': {
749- 'option': 'value',
750- 'new_option': 'new_value'
751- }
752- }
753-
754- provider = ConfigProvider(default)
755- provider.override_for_test(override)
756-
757- self.assertEqual(expected, provider())
758-
759- def test_override_works_when_changing_existing_option(self):
760- default = {
761- 'section': {
762- 'option': 'value'
763- }
764- }
765- override = {
766- 'section': {
767- 'option': 'new_value'
768- }
769- }
770- expected = {
771- 'section': {
772- 'option': 'new_value',
773- }
774- }
775-
776- provider = ConfigProvider(default)
777- provider.override_for_test(override)
778-
779- self.assertEqual(expected, provider())
780+from snapstore_client.config import Config
781+from snapstore_client.tests.testfixtures import (
782+ ConfigFixture,
783+ XDGConfigDirFixture,
784+)
785+
786+
787+class ConfigTests(TestCase):
788+
789+ def test_no_config(self):
790+ xdg_path = self.useFixture(XDGConfigDirFixture()).path
791+ config_ini = os.path.join(xdg_path, Config.xdg_name, 'config.ini')
792+ self.assertFalse(os.path.exists(config_ini))
793+ Config()
794+
795+ def test_init_loads(self):
796+ self.useFixture(ConfigFixture())
797+ cfg = Config()
798+ self.assertIsNotNone(cfg.get("store:default", "gw_url"))
799+
800+ def test_save(self):
801+ xdg_path = self.useFixture(XDGConfigDirFixture()).path
802+ config_ini = os.path.join(xdg_path, 'snapstore-client', 'config.ini')
803+ self.assertFalse(os.path.exists(config_ini))
804+
805+ cfg = Config()
806+ cfg.set("s", "k", "v")
807+ cfg.save()
808+
809+ self.assertTrue(os.path.exists(config_ini))
810+ with open(config_ini) as f:
811+ content = f.read()
812+ self.assertEqual(content, dedent('''\
813+ [s]
814+ k = v
815+
816+ '''))
817+
818+ def test_get_missing(self):
819+ self.useFixture(XDGConfigDirFixture())
820+ cfg = Config()
821+ self.assertIsNone(cfg.get("s", "k"))
822+
823+ def test_get(self):
824+ self.useFixture(XDGConfigDirFixture())
825+ cfg = Config()
826+ cfg.set("s", "k", "v")
827+ self.assertEqual(cfg.get("s", "k"), "v")
828+
829+ def test_section(self):
830+ self.useFixture(XDGConfigDirFixture())
831+ cfg = Config()
832+ s = cfg.section("s")
833+ s.set("k", "v")
834+ self.assertEqual(s.get("k"), "v")
835+ self.assertEqual(cfg.get("s", "k"), "v")
836+
837+ def test_store_section(self):
838+ self.useFixture(XDGConfigDirFixture())
839+ cfg = Config()
840+ store = cfg.store_section("foo")
841+ store.set("k", "v")
842+ self.assertEqual(cfg.get("store:foo", "k"), "v")
843
844
845 def test_suite():
846diff --git a/snapstore_client/tests/test_webservices.py b/snapstore_client/tests/test_webservices.py
847index fd51f7e..f6bf6bf 100644
848--- a/snapstore_client/tests/test_webservices.py
849+++ b/snapstore_client/tests/test_webservices.py
850@@ -6,7 +6,6 @@ import sys
851 import types
852 from urllib.parse import urljoin
853
854-from acceptable._doubles import set_service_locations
855 import fixtures
856 from requests.exceptions import HTTPError
857 import responses
858@@ -39,39 +38,37 @@ class WebservicesTests(TestCase):
859
860 def setUp(self):
861 super().setUp()
862- service_locations = config.read_config()['services']
863- set_service_locations(service_locations)
864+ self.config = self.useFixture(testfixtures.ConfigFixture())
865
866 @responses.activate
867 def test_issue_store_admin_success(self):
868- devicegw_root = config.read_config()['services']['snapdevicegw']
869- issue_store_admin_url = urljoin(
870- devicegw_root, '/v2/auth/issue-store-admin')
871+ gw_url = 'http://store.local/'
872+ issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin')
873 responses.add(
874 'POST', issue_store_admin_url, status=200,
875 json={'macaroon': 'dummy'})
876
877- self.assertEqual('dummy', webservices.issue_store_admin())
878+ self.assertEqual('dummy', webservices.issue_store_admin(gw_url))
879
880 @responses.activate
881 def test_issue_store_admin_error(self):
882 logger = self.useFixture(fixtures.FakeLogger())
883- devicegw_root = config.read_config()['services']['snapdevicegw']
884- issue_store_admin_url = urljoin(
885- devicegw_root, '/v2/auth/issue-store-admin')
886+ gw_url = 'http://store.local/'
887+ issue_store_admin_url = urljoin(gw_url, '/v2/auth/issue-store-admin')
888 responses.add(
889 'POST', issue_store_admin_url, status=400,
890 json=factory.APIError.single('Something went wrong').to_dict())
891
892- self.assertRaises(HTTPError, webservices.issue_store_admin)
893+ self.assertRaises(
894+ HTTPError, webservices.issue_store_admin, gw_url)
895 self.assertEqual(
896 'Failed to issue store_admin macaroon:\nSomething went wrong\n',
897 logger.output)
898
899 @responses.activate
900 def test_get_sso_discharge_success(self):
901- sso_root = config.read_config()['services']['sso']
902- discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge')
903+ sso_url = 'http://sso.local/'
904+ discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge')
905 responses.add(
906 'POST', discharge_url, status=200,
907 json={'discharge_macaroon': 'dummy'})
908@@ -79,7 +76,7 @@ class WebservicesTests(TestCase):
909 self.assertEqual(
910 'dummy',
911 webservices.get_sso_discharge(
912- 'user@example.org', 'secret', 'caveat'))
913+ sso_url, 'user@example.org', 'secret', 'caveat'))
914 request = responses.calls[0].request
915 self.assertEqual('application/json', request.headers['Content-Type'])
916 self.assertEqual({
917@@ -90,8 +87,8 @@ class WebservicesTests(TestCase):
918
919 @responses.activate
920 def test_get_sso_discharge_success_with_otp(self):
921- sso_root = config.read_config()['services']['sso']
922- discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge')
923+ sso_url = 'http://sso.local/'
924+ discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge')
925 responses.add(
926 'POST', discharge_url, status=200,
927 json={'discharge_macaroon': 'dummy'})
928@@ -99,7 +96,7 @@ class WebservicesTests(TestCase):
929 self.assertEqual(
930 'dummy',
931 webservices.get_sso_discharge(
932- 'user@example.org', 'secret', 'caveat',
933+ sso_url, 'user@example.org', 'secret', 'caveat',
934 one_time_password='123456'))
935 request = responses.calls[0].request
936 self.assertEqual('application/json', request.headers['Content-Type'])
937@@ -112,8 +109,8 @@ class WebservicesTests(TestCase):
938
939 @responses.activate
940 def test_get_sso_discharge_twofactor_required(self):
941- sso_root = config.read_config()['services']['sso']
942- discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge')
943+ sso_url = 'http://sso.local/'
944+ discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge')
945 responses.add(
946 'POST', discharge_url, status=401,
947 json={'error_list': [{'code': 'twofactor-required'}]})
948@@ -121,32 +118,32 @@ class WebservicesTests(TestCase):
949 self.assertRaises(
950 exceptions.StoreTwoFactorAuthenticationRequired,
951 webservices.get_sso_discharge,
952- 'user@example.org', 'secret', 'caveat')
953+ sso_url, 'user@example.org', 'secret', 'caveat')
954
955 @responses.activate
956 def test_get_sso_discharge_structured_error(self):
957- sso_root = config.read_config()['services']['sso']
958- discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge')
959+ sso_url = 'http://sso.local/'
960+ discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge')
961 responses.add(
962 'POST', discharge_url, status=400,
963 json=factory.APIError.single('Something went wrong').to_dict())
964
965 e = self.assertRaises(
966 exceptions.StoreAuthenticationError, webservices.get_sso_discharge,
967- 'user@example.org', 'secret', 'caveat')
968+ sso_url, 'user@example.org', 'secret', 'caveat')
969 self.assertEqual('Something went wrong', e.message)
970
971 @responses.activate
972 def test_get_sso_discharge_unstructured_error(self):
973 logger = self.useFixture(fixtures.FakeLogger())
974- sso_root = config.read_config()['services']['sso']
975- discharge_url = urljoin(sso_root, '/api/v2/tokens/discharge')
976+ sso_url = 'http://sso.local/'
977+ discharge_url = urljoin(sso_url, '/api/v2/tokens/discharge')
978 responses.add(
979 'POST', discharge_url, status=503, body='Try again later.')
980
981 self.assertRaises(
982 HTTPError, webservices.get_sso_discharge,
983- 'user@example.org', 'secret', 'caveat')
984+ sso_url, 'user@example.org', 'secret', 'caveat')
985 self.assertEqual(
986 'Failed to get SSO discharge:\n'
987 '====================\n'
988@@ -157,59 +154,58 @@ class WebservicesTests(TestCase):
989 @responses.activate
990 def test_get_overrides_success(self):
991 logger = self.useFixture(fixtures.FakeLogger())
992- credentials = self.useFixture(testfixtures.CredentialsFixture())
993 overrides = [factory.SnapDeviceGateway.Override()]
994 # XXX cjwatson 2017-06-26: Use acceptable-generated double once it
995 # exists.
996+ store = config.Config().store_section('default')
997 overrides_url = urljoin(
998- config.read_config()['services']['snapdevicegw'],
999- '/v2/metadata/overrides/mysnap')
1000+ store.get('gw_url'), '/v2/metadata/overrides/mysnap')
1001 responses.add('GET', overrides_url, status=200, json=overrides)
1002
1003- self.assertEqual(overrides, webservices.get_overrides('mysnap'))
1004+ self.assertEqual(overrides, webservices.get_overrides(
1005+ store, 'mysnap'))
1006 request = responses.calls[0].request
1007 self.assertThat(
1008 request.headers['Authorization'],
1009- matchers.MacaroonHeaderVerifies(credentials.key))
1010+ matchers.MacaroonHeaderVerifies(self.config.key))
1011 self.assertNotIn('Failed to get overrides:', logger.output)
1012
1013 @responses.activate
1014 def test_get_overrides_error(self):
1015 logger = self.useFixture(fixtures.FakeLogger())
1016- self.useFixture(testfixtures.CredentialsFixture())
1017+ store = config.Config().store_section('default')
1018 overrides_url = urljoin(
1019- config.read_config()['services']['snapdevicegw'],
1020- '/v2/metadata/overrides/mysnap')
1021+ store.get('gw_url'), '/v2/metadata/overrides/mysnap')
1022 responses.add(
1023 'GET', overrides_url, status=400,
1024 json=factory.APIError.single('Something went wrong').to_dict())
1025
1026- self.assertRaises(HTTPError, webservices.get_overrides, 'mysnap')
1027+ self.assertRaises(
1028+ HTTPError, webservices.get_overrides, store, 'mysnap')
1029 self.assertEqual(
1030 'Failed to get overrides:\nSomething went wrong\n', logger.output)
1031
1032 @responses.activate
1033 def test_set_overrides_success(self):
1034 logger = self.useFixture(fixtures.FakeLogger())
1035- credentials = self.useFixture(testfixtures.CredentialsFixture())
1036 override = factory.SnapDeviceGateway.Override()
1037 # XXX cjwatson 2017-06-26: Use acceptable-generated double once it
1038 # exists.
1039- overrides_url = urljoin(
1040- config.read_config()['services']['snapdevicegw'],
1041- '/v2/metadata/overrides')
1042+ store = config.Config().store_section('default')
1043+ overrides_url = urljoin(store.get('gw_url'), '/v2/metadata/overrides')
1044 responses.add('POST', overrides_url, status=200, json=[override])
1045
1046- self.assertEqual([override], webservices.set_overrides([{
1047- 'snap_name': override['snap_name'],
1048- 'revision': override['revision'],
1049- 'channel': override['channel'],
1050- 'series': override['series'],
1051- }]))
1052+ self.assertEqual([override], webservices.set_overrides(
1053+ store, [{
1054+ 'snap_name': override['snap_name'],
1055+ 'revision': override['revision'],
1056+ 'channel': override['channel'],
1057+ 'series': override['series'],
1058+ }]))
1059 request = responses.calls[0].request
1060 self.assertThat(
1061 request.headers['Authorization'],
1062- matchers.MacaroonHeaderVerifies(credentials.key))
1063+ matchers.MacaroonHeaderVerifies(self.config.key))
1064 self.assertEqual([{
1065 'snap_name': override['snap_name'],
1066 'revision': override['revision'],
1067@@ -221,22 +217,21 @@ class WebservicesTests(TestCase):
1068 @responses.activate
1069 def test_set_overrides_error(self):
1070 logger = self.useFixture(fixtures.FakeLogger())
1071- self.useFixture(testfixtures.CredentialsFixture())
1072 override = factory.SnapDeviceGateway.Override()
1073 # XXX cjwatson 2017-06-26: Use acceptable-generated double once it
1074 # exists.
1075- overrides_url = urljoin(
1076- config.read_config()['services']['snapdevicegw'],
1077- '/v2/metadata/overrides')
1078+ store = config.Config().store_section('default')
1079+ overrides_url = urljoin(store.get('gw_url'), '/v2/metadata/overrides')
1080 responses.add(
1081 'POST', overrides_url, status=400,
1082 json=factory.APIError.single('Something went wrong').to_dict())
1083
1084- self.assertRaises(HTTPError, webservices.set_overrides, {
1085- 'snap_name': override['snap_name'],
1086- 'revision': override['revision'],
1087- 'channel': override['channel'],
1088- 'series': override['series'],
1089- })
1090+ self.assertRaises(HTTPError, lambda: webservices.set_overrides(
1091+ store, {
1092+ 'snap_name': override['snap_name'],
1093+ 'revision': override['revision'],
1094+ 'channel': override['channel'],
1095+ 'series': override['series'],
1096+ }))
1097 self.assertEqual(
1098 'Failed to set override:\nSomething went wrong\n', logger.output)
1099diff --git a/snapstore_client/tests/testfixtures.py b/snapstore_client/tests/testfixtures.py
1100index 5070d5a..78bf1dd 100644
1101--- a/snapstore_client/tests/testfixtures.py
1102+++ b/snapstore_client/tests/testfixtures.py
1103@@ -2,7 +2,6 @@
1104
1105 import os.path
1106 from textwrap import dedent
1107-from urllib.parse import urlparse
1108
1109 from fixtures import (
1110 Fixture,
1111@@ -11,23 +10,27 @@ from fixtures import (
1112 )
1113 from pymacaroons import Macaroon
1114
1115-from snapstore_client import config
1116
1117+class XDGConfigDirFixture(Fixture):
1118+ """Set up xdg to read/write config from temp dir."""
1119
1120-class ConfigOverrideFixture(Fixture):
1121-
1122- def __init__(self, new_config):
1123- super().__init__()
1124- self._new_config = new_config
1125+ path = None
1126
1127 def _setUp(self):
1128- self.addCleanup(config.read_config.override_for_test(self._new_config))
1129+ self.path = self.useFixture(TempDir()).path
1130+ self.useFixture(MonkeyPatch(
1131+ 'xdg.BaseDirectory.xdg_config_home', self.path))
1132+ self.useFixture(MonkeyPatch(
1133+ 'xdg.BaseDirectory.xdg_config_dirs', [self.path]))
1134
1135
1136-class CredentialsFixture(Fixture):
1137+class ConfigFixture(Fixture):
1138
1139- def __init__(self):
1140+ def __init__(self, empty=False):
1141 super().__init__()
1142+ self.empty = empty
1143+ self.gw_url = 'http://store.local/'
1144+ self.sso_url = 'http://sso.local/'
1145 self.key = 'random-key'
1146 self.root = Macaroon(key=self.key)
1147 self.root.add_third_party_caveat(
1148@@ -36,22 +39,20 @@ class CredentialsFixture(Fixture):
1149 location='login.example.com', identifier='payload', key='sso-key')
1150
1151 def _setUp(self):
1152- config_path = self.useFixture(TempDir()).path
1153- credentials_path = os.path.join(
1154- config_path, 'snapstore-client', 'credentials.cfg')
1155- os.makedirs(os.path.dirname(credentials_path))
1156- snapdevicegw_netloc = urlparse(
1157- config.read_config()['services']['snapdevicegw']).netloc
1158- with open(credentials_path, 'w') as credentials_file:
1159- credentials = dedent("""\
1160- [{snapdevicegw}]
1161+ xdg_config_path = self.useFixture(XDGConfigDirFixture()).path
1162+ app_config_path = os.path.join(xdg_config_path, 'snapstore-client')
1163+ os.makedirs(app_config_path)
1164+ if self.empty:
1165+ return
1166+ with open(os.path.join(app_config_path, 'config.ini'), 'w') as f:
1167+ f.write(dedent("""
1168+ [store:default]
1169+ gw_url = {gw_url}
1170+ sso_url = {sso_url}
1171 root = {root}
1172 unbound_discharge = {unbound_discharge}
1173- """).format(
1174- snapdevicegw=snapdevicegw_netloc,
1175- root=self.root.serialize(),
1176- unbound_discharge=self.unbound_discharge.serialize(),
1177- )
1178- print(credentials, file=credentials_file)
1179- self.useFixture(MonkeyPatch(
1180- 'xdg.BaseDirectory.xdg_config_dirs', [config_path]))
1181+ """).format(
1182+ gw_url=self.gw_url, sso_url=self.sso_url,
1183+ root=self.root.serialize(),
1184+ unbound_discharge=self.unbound_discharge.serialize(),
1185+ ))
1186diff --git a/snapstore_client/webservices.py b/snapstore_client/webservices.py
1187index fc8c460..fb1927a 100644
1188--- a/snapstore_client/webservices.py
1189+++ b/snapstore_client/webservices.py
1190@@ -8,20 +8,16 @@ import urllib.parse
1191 from pymacaroons import Macaroon
1192 import requests
1193
1194-from snapstore_client import (
1195- config,
1196- exceptions,
1197-)
1198+from snapstore_client import exceptions
1199
1200
1201 logger = logging.getLogger(__name__)
1202
1203
1204-def issue_store_admin():
1205+def issue_store_admin(gw_url):
1206 """Ask the store to issue a store_admin macaroon."""
1207- devicegw_root = config.read_config()['services']['snapdevicegw']
1208 issue_store_admin_url = urllib.parse.urljoin(
1209- devicegw_root, '/v2/auth/issue-store-admin')
1210+ gw_url, '/v2/auth/issue-store-admin')
1211 resp = requests.post(issue_store_admin_url)
1212 if resp.status_code != 200:
1213 _print_error_message('issue store_admin macaroon', resp)
1214@@ -29,9 +25,10 @@ def issue_store_admin():
1215 return resp.json()['macaroon']
1216
1217
1218-def get_sso_discharge(email, password, caveat_id, one_time_password=None):
1219- sso_root = config.read_config()['services']['sso']
1220- discharge_url = urllib.parse.urljoin(sso_root, '/api/v2/tokens/discharge')
1221+def get_sso_discharge(sso_url, email, password, caveat_id,
1222+ one_time_password=None):
1223+ discharge_url = urllib.parse.urljoin(
1224+ sso_url, '/api/v2/tokens/discharge')
1225 data = {'email': email, 'password': password, 'caveat_id': caveat_id}
1226 if one_time_password is not None:
1227 data['otp'] = one_time_password
1228@@ -56,9 +53,9 @@ def get_sso_discharge(email, password, caveat_id, one_time_password=None):
1229 return resp.json()['discharge_macaroon']
1230
1231
1232-def refresh_sso_discharge(unbound_discharge_raw):
1233- sso_root = config.read_config()['services']['sso']
1234- refresh_url = urllib.parse.urljoin(sso_root, '/api/v2/tokens/refresh')
1235+def refresh_sso_discharge(store, unbound_discharge_raw):
1236+ refresh_url = urllib.parse.urljoin(
1237+ store.get('sso_url'), '/api/v2/tokens/refresh')
1238 data = {'discharge_macaroon': unbound_discharge_raw}
1239 resp = requests.post(
1240 refresh_url, headers={'Accept': 'application/json'}, json=data)
1241@@ -78,12 +75,11 @@ def _deserialize_macaroon(name, value):
1242 'failed to deserialize {} macaroon'.format(name))
1243
1244
1245-def _get_macaroon_auth():
1246+def _get_macaroon_auth(store):
1247 """Return an Authorization header containing store macaroons."""
1248- credentials = config.Credentials()
1249- root_raw = credentials.get('root')
1250+ root_raw = store.get('root')
1251 root = _deserialize_macaroon('root', root_raw)
1252- unbound_discharge_raw = credentials.get('unbound_discharge')
1253+ unbound_discharge_raw = store.get('unbound_discharge')
1254 unbound_discharge = _deserialize_macaroon(
1255 'unbound discharge', unbound_discharge_raw)
1256 bound_discharge = root.prepare_for_request(unbound_discharge)
1257@@ -99,27 +95,25 @@ def _raise_needs_refresh(response):
1258 raise exceptions.StoreMacaroonNeedsRefresh()
1259
1260
1261-def refresh_if_necessary(func, *args, **kwargs):
1262+def refresh_if_necessary(store, func, *args, **kwargs):
1263 """Make a request, refreshing macaroons if necessary."""
1264 try:
1265 return func(*args, **kwargs)
1266 except exceptions.StoreMacaroonNeedsRefresh:
1267- credentials = config.Credentials()
1268 unbound_discharge = refresh_sso_discharge(
1269- credentials.get('unbound_discharge'))
1270- credentials.set('unbound_discharge', unbound_discharge)
1271- credentials.save()
1272+ store.get('sso_url'), store.get('unbound_discharge'))
1273+ store.set('unbound_discharge', unbound_discharge)
1274+ store.save()
1275 return func(*args, **kwargs)
1276
1277
1278-def get_overrides(snap_name, series='16'):
1279+def get_overrides(store, snap_name, series='16'):
1280 """Get all overrides for a snap."""
1281- devicegw_root = config.read_config()['services']['snapdevicegw']
1282 overrides_url = urllib.parse.urljoin(
1283- devicegw_root,
1284+ store.get('gw_url'),
1285 '/v2/metadata/overrides/{}'.format(urllib.parse.quote_plus(snap_name)))
1286 headers = {
1287- 'Authorization': _get_macaroon_auth(),
1288+ 'Authorization': _get_macaroon_auth(store),
1289 'X-Ubuntu-Series': series,
1290 }
1291 resp = requests.get(overrides_url, headers=headers)
1292@@ -130,12 +124,11 @@ def get_overrides(snap_name, series='16'):
1293 return resp.json()
1294
1295
1296-def set_overrides(overrides):
1297+def set_overrides(store, overrides):
1298 """Add or remove channel map overrides for a snap."""
1299- devicegw_root = config.read_config()['services']['snapdevicegw']
1300 overrides_url = urllib.parse.urljoin(
1301- devicegw_root, '/v2/metadata/overrides')
1302- headers = {'Authorization': _get_macaroon_auth()}
1303+ store.get('gw_url'), '/v2/metadata/overrides')
1304+ headers = {'Authorization': _get_macaroon_auth(store)}
1305 resp = requests.post(overrides_url, headers=headers, json=overrides)
1306 _raise_needs_refresh(resp)
1307 if resp.status_code != 200:

Subscribers

People subscribed via source and target branches

to all changes: