Skip to content
This repository was archived by the owner on Jan 19, 2018. It is now read-only.

Commit a7dccff

Browse files
authored
Merge pull request #720 from rtnpro/refactor-config
Refactor Nulecule config.
2 parents 2f98066 + 685c56a commit a7dccff

File tree

11 files changed

+520
-227
lines changed

11 files changed

+520
-227
lines changed

atomicapp/cli/main.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ def cli_fetch(args):
6464
nm = NuleculeManager(app_spec=argdict['app_spec'],
6565
destination=destination,
6666
cli_answers=argdict['cli_answers'],
67-
answers_file=argdict['answers'])
67+
answers_file=argdict['answers'],
68+
answers_format=argdict.get('answers_format'))
6869
nm.fetch(**argdict)
6970
# Clean up the files if the user asked us to. Otherwise
7071
# notify the user where they can manage the application
@@ -81,7 +82,8 @@ def cli_run(args):
8182
nm = NuleculeManager(app_spec=argdict['app_spec'],
8283
destination=destination,
8384
cli_answers=argdict['cli_answers'],
84-
answers_file=argdict['answers'])
85+
answers_file=argdict['answers'],
86+
answers_format=argdict.get('answers_format'))
8587
nm.run(**argdict)
8688
# Clean up the files if the user asked us to. Otherwise
8789
# notify the user where they can manage the application
@@ -306,7 +308,7 @@ def create_parser(self):
306308
help="A file which will contain anwsers provided in interactive mode")
307309
run_subparser.add_argument(
308310
"--provider",
309-
dest="cli_provider",
311+
dest="provider",
310312
choices=PROVIDERS,
311313
help="The provider to use. Overrides provider value in answerfile.")
312314
run_subparser.add_argument(
@@ -511,7 +513,8 @@ def run(self):
511513
# and make a dictionary of it to pass along in args.
512514
setattr(args, 'cli_answers', {})
513515
for item in ['provider-api', 'provider-cafile', 'provider-auth',
514-
'provider-config', 'provider-tlsverify', 'namespace']:
516+
'provider-config', 'provider-tlsverify', 'namespace',
517+
'provider']:
515518
if hasattr(args, item) and getattr(args, item) is not None:
516519
args.cli_answers[item] = getattr(args, item)
517520

atomicapp/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
DEFAULTNAME_KEY = "default"
4141
PROVIDER_KEY = "provider"
4242
NAMESPACE_KEY = "namespace"
43+
NAMESPACE_SEPARATOR = ":"
4344
REQUIREMENTS_KEY = "requirements"
4445

4546
# Nulecule spec terminology vs the function within /providers

atomicapp/nulecule/base.py

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
along with Atomic App. If not, see <http://www.gnu.org/licenses/>.
1919
"""
2020
import anymarkup
21-
import copy
2221
import logging
2322
import os
2423
import yaml
@@ -38,7 +37,7 @@
3837
NAME_KEY,
3938
INHERIT_KEY,
4039
ARTIFACTS_KEY,
41-
DEFAULT_PROVIDER)
40+
NAMESPACE_SEPARATOR)
4241
from atomicapp.utils import Utils
4342
from atomicapp.requirements import Requirements
4443
from atomicapp.nulecule.lib import NuleculeBase
@@ -76,7 +75,7 @@ def __init__(self, id, specversion, graph, basepath, metadata=None,
7675
metadata (dict): Nulecule metadata
7776
requirements (dict): Requirements for the Nulecule application
7877
params (list): List of params for the Nulecule application
79-
config (dict): Config data for the Nulecule application
78+
config (atomicapp.nulecule.config.Config): Config data
8079
namespace (str): Namespace of the current Nulecule application
8180
8281
Returns:
@@ -88,7 +87,7 @@ def __init__(self, id, specversion, graph, basepath, metadata=None,
8887
self.metadata = metadata or {}
8988
self.graph = graph
9089
self.requirements = requirements
91-
self.config = config or {}
90+
self.config = config
9291

9392
@classmethod
9493
def unpack(cls, image, dest, config=None, namespace=GLOBAL_CONF,
@@ -101,7 +100,7 @@ def unpack(cls, image, dest, config=None, namespace=GLOBAL_CONF,
101100
image (str): A Docker image name.
102101
dest (str): Destination path where Nulecule data from Docker
103102
image should be extracted.
104-
config (dict): Dictionary, config data for Nulecule application.
103+
config: An instance of atomicapp.nulecule.config.Config
105104
namespace (str): Namespace for Nulecule application.
106105
nodeps (bool): Don't pull external Nulecule dependencies when
107106
True.
@@ -115,7 +114,7 @@ def unpack(cls, image, dest, config=None, namespace=GLOBAL_CONF,
115114
if Utils.running_on_openshift():
116115
# pass general config data containing provider specific data
117116
# to Openshift provider
118-
op = OpenshiftProvider(config.get('general', {}), './', False)
117+
op = OpenshiftProvider(config.globals, './', False)
119118
op.artifacts = []
120119
op.init()
121120
op.extract(image, APP_ENT_PATH, dest, update)
@@ -138,7 +137,8 @@ def load_from_path(cls, src, config=None, namespace=GLOBAL_CONF,
138137
139138
Args:
140139
src (str): Path to load Nulecule application from.
141-
config (dict): Config data for Nulecule application.
140+
config (atomicapp.nulecule.config.Config): Config data for
141+
Nulecule application.
142142
namespace (str): Namespace for Nulecule application.
143143
nodeps (bool): Do not pull external applications if True.
144144
dryrun (bool): Do not make any change to underlying host.
@@ -231,25 +231,23 @@ def load_config(self, config=None, ask=False, skip_asking=False):
231231
It updates self.config.
232232
233233
Args:
234-
config (dict): Existing config data, may be from ANSWERS
235-
file or any other source.
234+
config (atomicapp.nulecule.config.Config): Existing config data,
235+
may be from ANSWERS file or any other source.
236236
237237
Returns:
238238
None
239239
"""
240+
if config is None:
241+
config = self.config
240242
super(Nulecule, self).load_config(
241243
config=config, ask=ask, skip_asking=skip_asking)
242-
if self.namespace == GLOBAL_CONF and self.config[GLOBAL_CONF].get('provider') is None:
243-
self.config[GLOBAL_CONF]['provider'] = DEFAULT_PROVIDER
244-
logger.info("Provider not specified, using default provider - {}".
245-
format(DEFAULT_PROVIDER))
244+
246245
for component in self.components:
247246
# FIXME: Find a better way to expose config data to components.
248247
# A component should not get access to all the variables,
249248
# but only to variables it needs.
250-
component.load_config(config=copy.deepcopy(self.config),
249+
component.load_config(config=config,
251250
ask=ask, skip_asking=skip_asking)
252-
self.merge_config(self.config, component.config)
253251

254252
def load_components(self, nodeps=False, dryrun=False):
255253
"""
@@ -270,8 +268,8 @@ def load_components(self, nodeps=False, dryrun=False):
270268
node_name = node[NAME_KEY]
271269
source = Utils.getSourceImage(node)
272270
component = NuleculeComponent(
273-
node_name, self.basepath, source,
274-
node.get(PARAMS_KEY), node.get(ARTIFACTS_KEY),
271+
self._get_component_namespace(node_name), self.basepath,
272+
source, node.get(PARAMS_KEY), node.get(ARTIFACTS_KEY),
275273
self.config)
276274
component.load(nodeps, dryrun)
277275
components.append(component)
@@ -294,6 +292,24 @@ def render(self, provider_key=None, dryrun=False):
294292
for component in self.components:
295293
component.render(provider_key=provider_key, dryrun=dryrun)
296294

295+
def _get_component_namespace(self, component_name):
296+
"""
297+
Get a unique namespace for a Nulecule graph item, by concatinating
298+
the namespace of the current Nulecule (which could be the root Nulecule
299+
app or a child or external Nulecule app) and name of the Nulecule
300+
graph item.
301+
302+
Args:
303+
component_name (str): Name of the Nulecule graph item
304+
305+
Returns:
306+
A string
307+
"""
308+
current_namespace = '' if self.namespace == GLOBAL_CONF else self.namespace
309+
return (
310+
'%s%s%s' % (current_namespace, NAMESPACE_SEPARATOR, component_name)
311+
if current_namespace else component_name)
312+
297313

298314
class NuleculeComponent(NuleculeBase):
299315

@@ -356,12 +372,13 @@ def load_config(self, config=None, ask=False, skip_asking=False):
356372
"""
357373
Load config for the Nulecule component.
358374
"""
375+
if config is None:
376+
config = self.config
359377
super(NuleculeComponent, self).load_config(
360378
config, ask=ask, skip_asking=skip_asking)
361379
if isinstance(self._app, Nulecule):
362-
self._app.load_config(config=copy.deepcopy(self.config),
380+
self._app.load_config(config=self.config,
363381
ask=ask, skip_asking=skip_asking)
364-
self.merge_config(self.config, self._app.config)
365382

366383
def load_external_application(self, dryrun=False, update=False):
367384
"""
@@ -384,7 +401,8 @@ def load_external_application(self, dryrun=False, update=False):
384401
'Found existing external application: %s '
385402
'Loading: ' % self.name)
386403
nulecule = Nulecule.load_from_path(
387-
external_app_path, dryrun=dryrun, update=update)
404+
external_app_path, dryrun=dryrun, update=update,
405+
namespace=self.namespace)
388406
elif not dryrun:
389407
logger.info('Pulling external application: %s' % self.name)
390408
nulecule = Nulecule.unpack(
@@ -436,7 +454,7 @@ def render(self, provider_key=None, dryrun=False):
436454
raise NuleculeException(
437455
"Data for provider \"%s\" are not part of this app"
438456
% provider_key)
439-
context = self.get_context()
457+
context = self.config.context(self.namespace)
440458
for provider in self.artifacts:
441459
if provider_key and provider != provider_key:
442460
continue

atomicapp/nulecule/config.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import copy
2+
import logging
3+
4+
from atomicapp.constants import (GLOBAL_CONF,
5+
LOGGER_COCKPIT,
6+
DEFAULT_PROVIDER,
7+
DEFAULT_ANSWERS)
8+
from collections import defaultdict
9+
10+
cockpit_logger = logging.getLogger(LOGGER_COCKPIT)
11+
12+
13+
class Config(object):
14+
"""
15+
This class allows to store config data in different scopes along with
16+
source info for the data. When fetching the value for a key in a scope,
17+
the source info and the PRIORITY order of sources is taken into account.
18+
19+
Data sources:
20+
cli: Config data coming from the CLI
21+
runtime: Config data resolved during atomic app runtime. For example,
22+
when the value for a parameter in a Nulecule or Nulecule graph
23+
item is missing in answers data, we first try to load the default
24+
value for the parameter. When there's no default value, or when
25+
the user has specified to forcefully ask the user for values, we
26+
ask the user for data. These data collected/resolved during runtime
27+
form the runtime data.
28+
answers: Config data coming from answers file
29+
defaults: Default config data specified in atomicapp/constants.py
30+
31+
The priority order of the data sources is:
32+
cli > runtime > answers > defaults
33+
"""
34+
35+
PRIORITY = (
36+
'cli',
37+
'runtime',
38+
'answers',
39+
'defaults'
40+
)
41+
42+
def __init__(self, answers=None, cli=None):
43+
"""
44+
Initialize a Config instance.
45+
46+
Args:
47+
answers (dict): Answers data
48+
cli (dict): CLI data
49+
"""
50+
answers = answers or {}
51+
cli = cli or {}
52+
# We use a defaultdict of defaultdicts so that we can avoid doing
53+
# redundant checks in a nested dictionary if the value of the keys
54+
# are dictionaries or None.
55+
self._data = defaultdict(defaultdict)
56+
# Initialize default data dict
57+
self._data['defaults'] = defaultdict(defaultdict)
58+
# Initialize answers data dict
59+
self._data['answers'] = defaultdict(defaultdict)
60+
# Initialize cli data dict
61+
self._data['cli'] = defaultdict(defaultdict)
62+
# Initialize runtime data dict
63+
self._data['runtime'] = defaultdict(defaultdict)
64+
65+
# Load default answers
66+
for scope, data in DEFAULT_ANSWERS.items():
67+
for key, value in data.items():
68+
self.set(key, value, scope=scope, source='defaults')
69+
self.set('provider', DEFAULT_PROVIDER, scope=GLOBAL_CONF, source='defaults')
70+
71+
# Load answers data
72+
for scope, data in answers.items():
73+
for key, value in data.items():
74+
self.set(key, value, scope=scope, source='answers')
75+
76+
# Load cli data
77+
for key, value in cli.items():
78+
self.set(key, value, scope=GLOBAL_CONF, source='cli')
79+
80+
def get(self, key, scope=GLOBAL_CONF, ignore_sources=[]):
81+
"""
82+
Get the value of a key in a scope. This takes care of resolving
83+
the value by going through the PRIORITY order of the various
84+
sources of data.
85+
86+
Args:
87+
key (str): Key
88+
scope (str): Scope from which to fetch the value for the key
89+
90+
Returns:
91+
Value for the key.
92+
"""
93+
for source in self.PRIORITY:
94+
if source in ignore_sources:
95+
continue
96+
value = self._data[source][scope].get(key) or self._data[source][
97+
GLOBAL_CONF].get(key)
98+
if value:
99+
return value
100+
return None
101+
102+
def set(self, key, value, source, scope=GLOBAL_CONF):
103+
"""
104+
Set the value for a key within a scope along with specifying the
105+
source of the value.
106+
107+
Args:
108+
key (str): Key
109+
value: Value
110+
scope (str): Scope in which to store the value
111+
source (str): Source of the value
112+
"""
113+
self._data[source][scope][key] = value
114+
115+
def context(self, scope=GLOBAL_CONF):
116+
"""
117+
Get context data for the scope of Nulecule graph item by aggregating
118+
the data from various sources taking their priority order into
119+
account. This context data, which is a flat dictionary, is used to
120+
render the variables in the artifacts of Nulecule graph item.
121+
122+
Args:
123+
scope (str): Scope (or namespace) for the Nulecule graph item.
124+
Returns:
125+
A dictionary
126+
"""
127+
result = {}
128+
for source in reversed(self.PRIORITY):
129+
source_data = self._data[source]
130+
result.update(copy.deepcopy(source_data.get(GLOBAL_CONF) or {}))
131+
if scope != GLOBAL_CONF:
132+
result.update(copy.deepcopy(source_data.get(scope) or {}))
133+
return result
134+
135+
def runtime_answers(self):
136+
"""
137+
Get runtime answers.
138+
139+
Returns:
140+
A defaultdict containing runtime answers data.
141+
"""
142+
answers = defaultdict(dict)
143+
144+
for source in reversed(self.PRIORITY):
145+
for scope, data in (self._data.get(source) or {}).items():
146+
answers[scope].update(copy.deepcopy(data))
147+
148+
# Remove empty sections for answers
149+
for key, value in answers.items():
150+
if not value:
151+
answers.pop(key)
152+
153+
return answers
154+
155+
def update_source(self, source, data):
156+
"""
157+
Update answers data for a source.
158+
159+
Args:
160+
source (str): Source name
161+
data (dict): Answers data
162+
"""
163+
data = data or {}
164+
if source not in self._data:
165+
raise
166+
167+
# clean up source data
168+
for k in self._data[source]:
169+
self._data[source].pop(k)
170+
171+
for scope, data in data.items():
172+
for key, value in data.items():
173+
self.set(key, value, scope=scope, source=source)

0 commit comments

Comments
 (0)