fix: code style / flake8 automation
This commit is contained in:
parent
129d287ae5
commit
7d6135e099
3
.flake8
Normal file
3
.flake8
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[flake8]
|
||||||
|
extend-ignore = E501
|
||||||
|
exclude = .git,__pycache__,build,dist,report
|
2
Makefile
2
Makefile
@ -20,7 +20,7 @@ dev_setup:
|
|||||||
pip install -r dev-requirements.txt --user
|
pip install -r dev-requirements.txt --user
|
||||||
|
|
||||||
pytest:
|
pytest:
|
||||||
flake8 --ignore=E501 cloudbender tests
|
flake8 cloudbender tests
|
||||||
TEST=True pytest --log-cli-level=DEBUG
|
TEST=True pytest --log-cli-level=DEBUG
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
@ -17,4 +17,4 @@ class NullHandler(logging.Handler): # pragma: no cover
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger('cloudbender').addHandler(NullHandler())
|
logging.getLogger("cloudbender").addHandler(NullHandler())
|
||||||
|
@ -12,6 +12,7 @@ from .utils import setup_logging
|
|||||||
from .exceptions import InvalidProjectDir
|
from .exceptions import InvalidProjectDir
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -27,8 +28,8 @@ def cli(ctx, debug, directory):
|
|||||||
if directory:
|
if directory:
|
||||||
if not os.path.isabs(directory):
|
if not os.path.isabs(directory):
|
||||||
directory = os.path.normpath(os.path.join(os.getcwd(), directory))
|
directory = os.path.normpath(os.path.join(os.getcwd(), directory))
|
||||||
elif os.getenv('CLOUDBENDER_PROJECT_ROOT'):
|
elif os.getenv("CLOUDBENDER_PROJECT_ROOT"):
|
||||||
directory = os.getenv('CLOUDBENDER_PROJECT_ROOT')
|
directory = os.getenv("CLOUDBENDER_PROJECT_ROOT")
|
||||||
else:
|
else:
|
||||||
directory = os.getcwd()
|
directory = os.getcwd()
|
||||||
|
|
||||||
@ -50,7 +51,7 @@ def cli(ctx, debug, directory):
|
|||||||
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def render(cb, stack_names, multi):
|
def render(cb, stack_names, multi):
|
||||||
""" Renders template and its parameters - CFN only"""
|
"""Renders template and its parameters - CFN only"""
|
||||||
|
|
||||||
stacks = _find_stacks(cb, stack_names, multi)
|
stacks = _find_stacks(cb, stack_names, multi)
|
||||||
_render(stacks)
|
_render(stacks)
|
||||||
@ -61,7 +62,7 @@ def render(cb, stack_names, multi):
|
|||||||
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def sync(cb, stack_names, multi):
|
def sync(cb, stack_names, multi):
|
||||||
""" Renders template and provisions it right away """
|
"""Renders template and provisions it right away"""
|
||||||
|
|
||||||
stacks = _find_stacks(cb, stack_names, multi)
|
stacks = _find_stacks(cb, stack_names, multi)
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ def sync(cb, stack_names, multi):
|
|||||||
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def validate(cb, stack_names, multi):
|
def validate(cb, stack_names, multi):
|
||||||
""" Validates already rendered templates using cfn-lint - CFN only"""
|
"""Validates already rendered templates using cfn-lint - CFN only"""
|
||||||
stacks = _find_stacks(cb, stack_names, multi)
|
stacks = _find_stacks(cb, stack_names, multi)
|
||||||
|
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
@ -86,11 +87,17 @@ def validate(cb, stack_names, multi):
|
|||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("stack_names", nargs=-1)
|
@click.argument("stack_names", nargs=-1)
|
||||||
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
||||||
@click.option("--include", default='.*', help="regex matching wanted outputs, default '.*'")
|
@click.option(
|
||||||
@click.option("--values", is_flag=True, help="Only output values, most useful if only one outputs is returned")
|
"--include", default=".*", help="regex matching wanted outputs, default '.*'"
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--values",
|
||||||
|
is_flag=True,
|
||||||
|
help="Only output values, most useful if only one outputs is returned",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def outputs(cb, stack_names, multi, include, values):
|
def outputs(cb, stack_names, multi, include, values):
|
||||||
""" Prints all stack outputs """
|
"""Prints all stack outputs"""
|
||||||
|
|
||||||
stacks = _find_stacks(cb, stack_names, multi)
|
stacks = _find_stacks(cb, stack_names, multi)
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
@ -110,7 +117,7 @@ def outputs(cb, stack_names, multi, include, values):
|
|||||||
@click.option("--graph", is_flag=True, help="Create Dot Graph file")
|
@click.option("--graph", is_flag=True, help="Create Dot Graph file")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def create_docs(cb, stack_names, multi, graph):
|
def create_docs(cb, stack_names, multi, graph):
|
||||||
""" Parses all documentation fragments out of rendered templates creating docs/*.md file """
|
"""Parses all documentation fragments out of rendered templates creating docs/*.md file"""
|
||||||
|
|
||||||
stacks = _find_stacks(cb, stack_names, multi)
|
stacks = _find_stacks(cb, stack_names, multi)
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
@ -122,7 +129,7 @@ def create_docs(cb, stack_names, multi, graph):
|
|||||||
@click.argument("change_set_name")
|
@click.argument("change_set_name")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def create_change_set(cb, stack_name, change_set_name):
|
def create_change_set(cb, stack_name, change_set_name):
|
||||||
""" Creates a change set for an existing stack - CFN only"""
|
"""Creates a change set for an existing stack - CFN only"""
|
||||||
stacks = _find_stacks(cb, [stack_name])
|
stacks = _find_stacks(cb, [stack_name])
|
||||||
|
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
@ -133,29 +140,33 @@ def create_change_set(cb, stack_name, change_set_name):
|
|||||||
@click.argument("stack_name")
|
@click.argument("stack_name")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def refresh(cb, stack_name):
|
def refresh(cb, stack_name):
|
||||||
""" Refreshes Pulumi stack / Drift detection """
|
"""Refreshes Pulumi stack / Drift detection"""
|
||||||
stacks = _find_stacks(cb, [stack_name])
|
stacks = _find_stacks(cb, [stack_name])
|
||||||
|
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
if s.mode == 'pulumi':
|
if s.mode == "pulumi":
|
||||||
s.refresh()
|
s.refresh()
|
||||||
else:
|
else:
|
||||||
logger.info('{} uses Cloudformation, refresh skipped.'.format(s.stackname))
|
logger.info("{} uses Cloudformation, refresh skipped.".format(s.stackname))
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.argument("stack_name")
|
@click.argument("stack_name")
|
||||||
@click.option("--reset", is_flag=True, help="All pending stack operations are removed and the stack will be re-imported")
|
@click.option(
|
||||||
|
"--reset",
|
||||||
|
is_flag=True,
|
||||||
|
help="All pending stack operations are removed and the stack will be re-imported",
|
||||||
|
)
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def export(cb, stack_name, reset=False):
|
def export(cb, stack_name, reset=False):
|
||||||
""" Exports a Pulumi stack to repair state """
|
"""Exports a Pulumi stack to repair state"""
|
||||||
stacks = _find_stacks(cb, [stack_name])
|
stacks = _find_stacks(cb, [stack_name])
|
||||||
|
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
if s.mode == 'pulumi':
|
if s.mode == "pulumi":
|
||||||
s.export(reset)
|
s.export(reset)
|
||||||
else:
|
else:
|
||||||
logger.info('{} uses Cloudformation, export skipped.'.format(s.stackname))
|
logger.info("{} uses Cloudformation, export skipped.".format(s.stackname))
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@ -165,7 +176,7 @@ def export(cb, stack_name, reset=False):
|
|||||||
@click.option("--secret", is_flag=True, help="Value is a secret")
|
@click.option("--secret", is_flag=True, help="Value is a secret")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def set_config(cb, stack_name, key, value, secret=False):
|
def set_config(cb, stack_name, key, value, secret=False):
|
||||||
""" Sets a config value, encrypts with stack key if secret """
|
"""Sets a config value, encrypts with stack key if secret"""
|
||||||
stacks = _find_stacks(cb, [stack_name])
|
stacks = _find_stacks(cb, [stack_name])
|
||||||
|
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
@ -177,7 +188,7 @@ def set_config(cb, stack_name, key, value, secret=False):
|
|||||||
@click.argument("key")
|
@click.argument("key")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def get_config(cb, stack_name, key):
|
def get_config(cb, stack_name, key):
|
||||||
""" Get a config value, decrypted if secret """
|
"""Get a config value, decrypted if secret"""
|
||||||
stacks = _find_stacks(cb, [stack_name])
|
stacks = _find_stacks(cb, [stack_name])
|
||||||
|
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
@ -188,14 +199,18 @@ def get_config(cb, stack_name, key):
|
|||||||
@click.argument("stack_name")
|
@click.argument("stack_name")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def preview(cb, stack_name):
|
def preview(cb, stack_name):
|
||||||
""" Preview of Pulumi stack up operation """
|
"""Preview of Pulumi stack up operation"""
|
||||||
stacks = _find_stacks(cb, [stack_name])
|
stacks = _find_stacks(cb, [stack_name])
|
||||||
|
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
if s.mode == 'pulumi':
|
if s.mode == "pulumi":
|
||||||
s.preview()
|
s.preview()
|
||||||
else:
|
else:
|
||||||
logger.warning('{} uses Cloudformation, use create-change-set for previews.'.format(s.stackname))
|
logger.warning(
|
||||||
|
"{} uses Cloudformation, use create-change-set for previews.".format(
|
||||||
|
s.stackname
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@ -203,7 +218,7 @@ def preview(cb, stack_name):
|
|||||||
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def provision(cb, stack_names, multi):
|
def provision(cb, stack_names, multi):
|
||||||
""" Creates or updates stacks or stack groups """
|
"""Creates or updates stacks or stack groups"""
|
||||||
|
|
||||||
stacks = _find_stacks(cb, stack_names, multi)
|
stacks = _find_stacks(cb, stack_names, multi)
|
||||||
_provision(cb, stacks)
|
_provision(cb, stacks)
|
||||||
@ -214,7 +229,7 @@ def provision(cb, stack_names, multi):
|
|||||||
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
@click.option("--multi", is_flag=True, help="Allow more than one stack to match")
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def delete(cb, stack_names, multi):
|
def delete(cb, stack_names, multi):
|
||||||
""" Deletes stacks or stack groups """
|
"""Deletes stacks or stack groups"""
|
||||||
stacks = _find_stacks(cb, stack_names, multi)
|
stacks = _find_stacks(cb, stack_names, multi)
|
||||||
|
|
||||||
# Reverse steps
|
# Reverse steps
|
||||||
@ -235,16 +250,16 @@ def delete(cb, stack_names, multi):
|
|||||||
@click.command()
|
@click.command()
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
def clean(cb):
|
def clean(cb):
|
||||||
""" Deletes all previously rendered files locally """
|
"""Deletes all previously rendered files locally"""
|
||||||
cb.clean()
|
cb.clean()
|
||||||
|
|
||||||
|
|
||||||
def sort_stacks(cb, stacks):
|
def sort_stacks(cb, stacks):
|
||||||
""" Sort stacks by dependencies """
|
"""Sort stacks by dependencies"""
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
if s.mode == 'pulumi':
|
if s.mode == "pulumi":
|
||||||
data[s.id] = set()
|
data[s.id] = set()
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -253,10 +268,14 @@ def sort_stacks(cb, stacks):
|
|||||||
deps = []
|
deps = []
|
||||||
for d in s.dependencies:
|
for d in s.dependencies:
|
||||||
# For now we assume deps are artifacts so we prepend them with our local profile and region to match stack.id
|
# For now we assume deps are artifacts so we prepend them with our local profile and region to match stack.id
|
||||||
for dep_stack in cb.filter_stacks({'region': s.region, 'profile': s.profile, 'provides': d}):
|
for dep_stack in cb.filter_stacks(
|
||||||
|
{"region": s.region, "profile": s.profile, "provides": d}
|
||||||
|
):
|
||||||
deps.append(dep_stack.id)
|
deps.append(dep_stack.id)
|
||||||
# also look for global services
|
# also look for global services
|
||||||
for dep_stack in cb.filter_stacks({'region': 'global', 'profile': s.profile, 'provides': d}):
|
for dep_stack in cb.filter_stacks(
|
||||||
|
{"region": "global", "profile": s.profile, "provides": d}
|
||||||
|
):
|
||||||
deps.append(dep_stack.id)
|
deps.append(dep_stack.id)
|
||||||
|
|
||||||
data[s.id] = set(deps)
|
data[s.id] = set(deps)
|
||||||
@ -267,7 +286,9 @@ def sort_stacks(cb, stacks):
|
|||||||
v.discard(k)
|
v.discard(k)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
extra_items_in_deps = functools.reduce(set.union, data.values()) - set(data.keys())
|
extra_items_in_deps = functools.reduce(set.union, data.values()) - set(
|
||||||
|
data.keys()
|
||||||
|
)
|
||||||
data.update({item: set() for item in extra_items_in_deps})
|
data.update({item: set() for item in extra_items_in_deps})
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@ -283,41 +304,46 @@ def sort_stacks(cb, stacks):
|
|||||||
result.append(s)
|
result.append(s)
|
||||||
yield result
|
yield result
|
||||||
|
|
||||||
data = {item: (dep - ordered) for item, dep in data.items()
|
data = {
|
||||||
if item not in ordered}
|
item: (dep - ordered) for item, dep in data.items() if item not in ordered
|
||||||
|
}
|
||||||
assert not data, "A cyclic dependency exists amongst %r" % data
|
assert not data, "A cyclic dependency exists amongst %r" % data
|
||||||
|
|
||||||
|
|
||||||
def _find_stacks(cb, stack_names, multi=False):
|
def _find_stacks(cb, stack_names, multi=False):
|
||||||
""" search stacks by name """
|
"""search stacks by name"""
|
||||||
|
|
||||||
stacks = []
|
stacks = []
|
||||||
for s in stack_names:
|
for s in stack_names:
|
||||||
stacks = stacks + cb.resolve_stacks(s)
|
stacks = stacks + cb.resolve_stacks(s)
|
||||||
|
|
||||||
if not multi and len(stacks) > 1:
|
if not multi and len(stacks) > 1:
|
||||||
logger.error('Found more than one stack matching name ({}). Please set --multi if that is what you want.'.format(', '.join(stack_names)))
|
logger.error(
|
||||||
|
"Found more than one stack matching name ({}). Please set --multi if that is what you want.".format(
|
||||||
|
", ".join(stack_names)
|
||||||
|
)
|
||||||
|
)
|
||||||
raise click.Abort()
|
raise click.Abort()
|
||||||
|
|
||||||
if not stacks:
|
if not stacks:
|
||||||
logger.error('Cannot find stack matching: {}'.format(', '.join(stack_names)))
|
logger.error("Cannot find stack matching: {}".format(", ".join(stack_names)))
|
||||||
raise click.Abort()
|
raise click.Abort()
|
||||||
|
|
||||||
return stacks
|
return stacks
|
||||||
|
|
||||||
|
|
||||||
def _render(stacks):
|
def _render(stacks):
|
||||||
""" Utility function to reuse code between tasks """
|
"""Utility function to reuse code between tasks"""
|
||||||
for s in stacks:
|
for s in stacks:
|
||||||
if s.mode != 'pulumi':
|
if s.mode != "pulumi":
|
||||||
s.render()
|
s.render()
|
||||||
s.write_template_file()
|
s.write_template_file()
|
||||||
else:
|
else:
|
||||||
logger.info('{} uses Pulumi, render skipped.'.format(s.stackname))
|
logger.info("{} uses Pulumi, render skipped.".format(s.stackname))
|
||||||
|
|
||||||
|
|
||||||
def _provision(cb, stacks):
|
def _provision(cb, stacks):
|
||||||
""" Utility function to reuse code between tasks """
|
"""Utility function to reuse code between tasks"""
|
||||||
for step in sort_stacks(cb, stacks):
|
for step in sort_stacks(cb, stacks):
|
||||||
if step:
|
if step:
|
||||||
with ThreadPoolExecutor(max_workers=len(step)) as group:
|
with ThreadPoolExecutor(max_workers=len(step)) as group:
|
||||||
@ -348,5 +374,5 @@ cli.add_command(set_config)
|
|||||||
cli.add_command(get_config)
|
cli.add_command(get_config)
|
||||||
cli.add_command(export)
|
cli.add_command(export)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
cli(obj={})
|
cli(obj={})
|
||||||
|
@ -10,7 +10,7 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BotoConnection():
|
class BotoConnection:
|
||||||
_sessions = {}
|
_sessions = {}
|
||||||
_clients = {}
|
_clients = {}
|
||||||
|
|
||||||
@ -27,13 +27,15 @@ class BotoConnection():
|
|||||||
# Change the cache path from the default of ~/.aws/boto/cache to the one used by awscli
|
# Change the cache path from the default of ~/.aws/boto/cache to the one used by awscli
|
||||||
session_vars = {}
|
session_vars = {}
|
||||||
if profile:
|
if profile:
|
||||||
session_vars['profile'] = (None, None, profile, None)
|
session_vars["profile"] = (None, None, profile, None)
|
||||||
if region and region != 'global':
|
if region and region != "global":
|
||||||
session_vars['region'] = (None, None, region, None)
|
session_vars["region"] = (None, None, region, None)
|
||||||
|
|
||||||
session = botocore.session.Session(session_vars=session_vars)
|
session = botocore.session.Session(session_vars=session_vars)
|
||||||
cli_cache = os.path.join(os.path.expanduser('~'), '.aws/cli/cache')
|
cli_cache = os.path.join(os.path.expanduser("~"), ".aws/cli/cache")
|
||||||
session.get_component('credential_provider').get_provider('assume-role').cache = credentials.JSONFileCache(cli_cache)
|
session.get_component("credential_provider").get_provider(
|
||||||
|
"assume-role"
|
||||||
|
).cache = credentials.JSONFileCache(cli_cache)
|
||||||
|
|
||||||
self._sessions[(profile, region)] = session
|
self._sessions[(profile, region)] = session
|
||||||
|
|
||||||
@ -41,7 +43,9 @@ class BotoConnection():
|
|||||||
|
|
||||||
def _get_client(self, service, profile=None, region=None):
|
def _get_client(self, service, profile=None, region=None):
|
||||||
if self._clients.get((profile, region, service)):
|
if self._clients.get((profile, region, service)):
|
||||||
logger.debug("Reusing boto session for {} {} {}".format(profile, region, service))
|
logger.debug(
|
||||||
|
"Reusing boto session for {} {} {}".format(profile, region, service)
|
||||||
|
)
|
||||||
return self._clients[(profile, region, service)]
|
return self._clients[(profile, region, service)]
|
||||||
|
|
||||||
session = self._get_session(profile, region)
|
session = self._get_session(profile, region)
|
||||||
@ -59,8 +63,12 @@ class BotoConnection():
|
|||||||
return getattr(client, command)(**kwargs)
|
return getattr(client, command)(**kwargs)
|
||||||
|
|
||||||
except botocore.exceptions.ClientError as e:
|
except botocore.exceptions.ClientError as e:
|
||||||
if e.response['Error']['Code'] == 'Throttling':
|
if e.response["Error"]["Code"] == "Throttling":
|
||||||
logger.warning("Throttling exception occured during {} - retry after 3s".format(command))
|
logger.warning(
|
||||||
|
"Throttling exception occured during {} - retry after 3s".format(
|
||||||
|
command
|
||||||
|
)
|
||||||
|
)
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
@ -9,7 +9,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class CloudBender(object):
|
class CloudBender(object):
|
||||||
""" Config Class to handle recursive conf/* config tree """
|
"""Config Class to handle recursive conf/* config tree"""
|
||||||
|
|
||||||
def __init__(self, root_path):
|
def __init__(self, root_path):
|
||||||
self.root = pathlib.Path(root_path)
|
self.root = pathlib.Path(root_path)
|
||||||
self.sg = None
|
self.sg = None
|
||||||
@ -20,28 +21,39 @@ class CloudBender(object):
|
|||||||
"hooks_path": self.root.joinpath("hooks"),
|
"hooks_path": self.root.joinpath("hooks"),
|
||||||
"docs_path": self.root.joinpath("docs"),
|
"docs_path": self.root.joinpath("docs"),
|
||||||
"outputs_path": self.root.joinpath("outputs"),
|
"outputs_path": self.root.joinpath("outputs"),
|
||||||
"artifact_paths": [self.root.joinpath("artifacts")]
|
"artifact_paths": [self.root.joinpath("artifacts")],
|
||||||
}
|
}
|
||||||
|
|
||||||
if not self.ctx['config_path'].is_dir():
|
if not self.ctx["config_path"].is_dir():
|
||||||
raise InvalidProjectDir("Check '{0}' exists and is a valid CloudBender project folder.".format(self.ctx['config_path']))
|
raise InvalidProjectDir(
|
||||||
|
"Check '{0}' exists and is a valid CloudBender project folder.".format(
|
||||||
|
self.ctx["config_path"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
"""Load the <path>/config.yaml, <path>/*.yaml as stacks, sub-folders are sub-groups """
|
"""Load the <path>/config.yaml, <path>/*.yaml as stacks, sub-folders are sub-groups"""
|
||||||
|
|
||||||
# Read top level config.yaml and extract CloudBender CTX
|
# Read top level config.yaml and extract CloudBender CTX
|
||||||
_config = read_config_file(self.ctx['config_path'].joinpath('config.yaml'))
|
_config = read_config_file(self.ctx["config_path"].joinpath("config.yaml"))
|
||||||
|
|
||||||
# Legacy naming
|
# Legacy naming
|
||||||
if _config and _config.get('CloudBender'):
|
if _config and _config.get("CloudBender"):
|
||||||
self.ctx.update(_config.get('CloudBender'))
|
self.ctx.update(_config.get("CloudBender"))
|
||||||
|
|
||||||
if _config and _config.get('cloudbender'):
|
if _config and _config.get("cloudbender"):
|
||||||
self.ctx.update(_config.get('cloudbender'))
|
self.ctx.update(_config.get("cloudbender"))
|
||||||
|
|
||||||
# Make sure all paths are abs
|
# Make sure all paths are abs
|
||||||
for k, v in self.ctx.items():
|
for k, v in self.ctx.items():
|
||||||
if k in ['config_path', 'template_path', 'hooks_path', 'docs_path', 'artifact_paths', 'outputs_path']:
|
if k in [
|
||||||
|
"config_path",
|
||||||
|
"template_path",
|
||||||
|
"hooks_path",
|
||||||
|
"docs_path",
|
||||||
|
"artifact_paths",
|
||||||
|
"outputs_path",
|
||||||
|
]:
|
||||||
if isinstance(v, list):
|
if isinstance(v, list):
|
||||||
new_list = []
|
new_list = []
|
||||||
for p in v:
|
for p in v:
|
||||||
@ -56,7 +68,7 @@ class CloudBender(object):
|
|||||||
if not v.is_absolute():
|
if not v.is_absolute():
|
||||||
self.ctx[k] = self.root.joinpath(v)
|
self.ctx[k] = self.root.joinpath(v)
|
||||||
|
|
||||||
self.sg = StackGroup(self.ctx['config_path'], self.ctx)
|
self.sg = StackGroup(self.ctx["config_path"], self.ctx)
|
||||||
self.sg.read_config()
|
self.sg.read_config()
|
||||||
|
|
||||||
self.all_stacks = self.sg.get_stacks()
|
self.all_stacks = self.sg.get_stacks()
|
||||||
@ -77,15 +89,15 @@ class CloudBender(object):
|
|||||||
token = token[7:]
|
token = token[7:]
|
||||||
|
|
||||||
# If path ends with yaml we look for stacks
|
# If path ends with yaml we look for stacks
|
||||||
if token.endswith('.yaml'):
|
if token.endswith(".yaml"):
|
||||||
stacks = self.sg.get_stacks(token, match_by='path')
|
stacks = self.sg.get_stacks(token, match_by="path")
|
||||||
|
|
||||||
# otherwise assume we look for a group, if we find a group return all stacks below
|
# otherwise assume we look for a group, if we find a group return all stacks below
|
||||||
else:
|
else:
|
||||||
# Strip potential trailing slash
|
# Strip potential trailing slash
|
||||||
token = token.rstrip('/')
|
token = token.rstrip("/")
|
||||||
|
|
||||||
sg = self.sg.get_stackgroup(token, match_by='path')
|
sg = self.sg.get_stackgroup(token, match_by="path")
|
||||||
if sg:
|
if sg:
|
||||||
stacks = sg.get_stacks()
|
stacks = sg.get_stacks()
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from functools import wraps
|
|||||||
from .exceptions import InvalidHook
|
from .exceptions import InvalidHook
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -40,7 +41,9 @@ def pulumi_ws(func):
|
|||||||
@wraps(func)
|
@wraps(func)
|
||||||
def decorated(self, *args, **kwargs):
|
def decorated(self, *args, **kwargs):
|
||||||
# setup temp workspace
|
# setup temp workspace
|
||||||
self.work_dir = tempfile.mkdtemp(dir=tempfile.gettempdir(), prefix="cloudbender-")
|
self.work_dir = tempfile.mkdtemp(
|
||||||
|
dir=tempfile.gettempdir(), prefix="cloudbender-"
|
||||||
|
)
|
||||||
|
|
||||||
response = func(self, *args, **kwargs)
|
response = func(self, *args, **kwargs)
|
||||||
|
|
||||||
@ -63,4 +66,4 @@ def cmd(stack, arguments):
|
|||||||
hook = subprocess.run(arguments, stdout=subprocess.PIPE)
|
hook = subprocess.run(arguments, stdout=subprocess.PIPE)
|
||||||
logger.info(hook.stdout.decode("utf-8"))
|
logger.info(hook.stdout.decode("utf-8"))
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise InvalidHook('Invalid argument {}'.format(arguments))
|
raise InvalidHook("Invalid argument {}".format(arguments))
|
||||||
|
@ -24,10 +24,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@jinja2.contextfunction
|
@jinja2.contextfunction
|
||||||
def option(context, attribute, default_value=u'', source='options'):
|
def option(context, attribute, default_value="", source="options"):
|
||||||
""" Get attribute from options data structure, default_value otherwise """
|
"""Get attribute from options data structure, default_value otherwise"""
|
||||||
environment = context.environment
|
environment = context.environment
|
||||||
options = environment.globals['_config'][source]
|
options = environment.globals["_config"][source]
|
||||||
|
|
||||||
if not attribute:
|
if not attribute:
|
||||||
return default_value
|
return default_value
|
||||||
@ -48,7 +48,7 @@ def option(context, attribute, default_value=u'', source='options'):
|
|||||||
@jinja2.contextfunction
|
@jinja2.contextfunction
|
||||||
def include_raw_gz(context, files=None, gz=True, remove_comments=False):
|
def include_raw_gz(context, files=None, gz=True, remove_comments=False):
|
||||||
jenv = context.environment
|
jenv = context.environment
|
||||||
output = ''
|
output = ""
|
||||||
|
|
||||||
# For shell script we can even remove whitespaces so treat them individually
|
# For shell script we can even remove whitespaces so treat them individually
|
||||||
# sed -e '2,$ {/^ *$/d ; /^ *#/d ; /^[ \t] *#/d ; /*^/d ; s/^[ \t]*// ; s/*[ \t]$// ; s/ $//}'
|
# sed -e '2,$ {/^ *$/d ; /^ *#/d ; /^[ \t] *#/d ; /*^/d ; s/^[ \t]*// ; s/*[ \t]$// ; s/ $//}'
|
||||||
@ -57,33 +57,35 @@ def include_raw_gz(context, files=None, gz=True, remove_comments=False):
|
|||||||
|
|
||||||
if remove_comments:
|
if remove_comments:
|
||||||
# Remove full line comments but not shebang
|
# Remove full line comments but not shebang
|
||||||
_re_comment = re.compile(r'^\s*#[^!]')
|
_re_comment = re.compile(r"^\s*#[^!]")
|
||||||
_re_blank = re.compile(r'^\s*$')
|
_re_blank = re.compile(r"^\s*$")
|
||||||
_re_keep = re.compile(r'^## template: jinja$')
|
_re_keep = re.compile(r"^## template: jinja$")
|
||||||
stripped_output = ''
|
stripped_output = ""
|
||||||
for curline in output.splitlines():
|
for curline in output.splitlines():
|
||||||
if re.match(_re_blank, curline):
|
if re.match(_re_blank, curline):
|
||||||
continue
|
continue
|
||||||
elif re.match(_re_keep, curline):
|
elif re.match(_re_keep, curline):
|
||||||
stripped_output = stripped_output + curline + '\n'
|
stripped_output = stripped_output + curline + "\n"
|
||||||
elif re.match(_re_comment, curline):
|
elif re.match(_re_comment, curline):
|
||||||
logger.debug("Removed {}".format(curline))
|
logger.debug("Removed {}".format(curline))
|
||||||
else:
|
else:
|
||||||
stripped_output = stripped_output + curline + '\n'
|
stripped_output = stripped_output + curline + "\n"
|
||||||
|
|
||||||
output = stripped_output
|
output = stripped_output
|
||||||
|
|
||||||
if not gz:
|
if not gz:
|
||||||
return(output)
|
return output
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
f = gzip.GzipFile(mode='w', fileobj=buf, mtime=0)
|
f = gzip.GzipFile(mode="w", fileobj=buf, mtime=0)
|
||||||
f.write(output.encode())
|
f.write(output.encode())
|
||||||
f.close()
|
f.close()
|
||||||
|
|
||||||
# MaxSize is 21847
|
# MaxSize is 21847
|
||||||
logger.info("Compressed user-data from {} to {}".format(len(output), len(buf.getvalue())))
|
logger.info(
|
||||||
return base64.b64encode(buf.getvalue()).decode('utf-8')
|
"Compressed user-data from {} to {}".format(len(output), len(buf.getvalue()))
|
||||||
|
)
|
||||||
|
return base64.b64encode(buf.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
@jinja2.contextfunction
|
@jinja2.contextfunction
|
||||||
@ -92,33 +94,33 @@ def raise_helper(context, msg):
|
|||||||
|
|
||||||
|
|
||||||
# Custom tests
|
# Custom tests
|
||||||
def regex(value='', pattern='', ignorecase=False, match_type='search'):
|
def regex(value="", pattern="", ignorecase=False, match_type="search"):
|
||||||
''' Expose `re` as a boolean filter using the `search` method by default.
|
"""Expose `re` as a boolean filter using the `search` method by default.
|
||||||
This is likely only useful for `search` and `match` which already
|
This is likely only useful for `search` and `match` which already
|
||||||
have their own filters.
|
have their own filters.
|
||||||
'''
|
"""
|
||||||
if ignorecase:
|
if ignorecase:
|
||||||
flags = re.I
|
flags = re.I
|
||||||
else:
|
else:
|
||||||
flags = 0
|
flags = 0
|
||||||
_re = re.compile(pattern, flags=flags)
|
_re = re.compile(pattern, flags=flags)
|
||||||
if getattr(_re, match_type, 'search')(value) is not None:
|
if getattr(_re, match_type, "search")(value) is not None:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def match(value, pattern='', ignorecase=False):
|
def match(value, pattern="", ignorecase=False):
|
||||||
''' Perform a `re.match` returning a boolean '''
|
"""Perform a `re.match` returning a boolean"""
|
||||||
return regex(value, pattern, ignorecase, 'match')
|
return regex(value, pattern, ignorecase, "match")
|
||||||
|
|
||||||
|
|
||||||
def search(value, pattern='', ignorecase=False):
|
def search(value, pattern="", ignorecase=False):
|
||||||
''' Perform a `re.search` returning a boolean '''
|
"""Perform a `re.search` returning a boolean"""
|
||||||
return regex(value, pattern, ignorecase, 'search')
|
return regex(value, pattern, ignorecase, "search")
|
||||||
|
|
||||||
|
|
||||||
# Custom filters
|
# Custom filters
|
||||||
def sub(value='', pattern='', replace='', ignorecase=False):
|
def sub(value="", pattern="", replace="", ignorecase=False):
|
||||||
if ignorecase:
|
if ignorecase:
|
||||||
flags = re.I
|
flags = re.I
|
||||||
else:
|
else:
|
||||||
@ -129,9 +131,16 @@ def sub(value='', pattern='', replace='', ignorecase=False):
|
|||||||
def pyminify(source, obfuscate=False, minify=True):
|
def pyminify(source, obfuscate=False, minify=True):
|
||||||
# pyminifier options
|
# pyminifier options
|
||||||
options = types.SimpleNamespace(
|
options = types.SimpleNamespace(
|
||||||
tabs=False, replacement_length=1, use_nonlatin=0,
|
tabs=False,
|
||||||
obfuscate=0, obf_variables=1, obf_classes=0, obf_functions=0,
|
replacement_length=1,
|
||||||
obf_import_methods=0, obf_builtins=0)
|
use_nonlatin=0,
|
||||||
|
obfuscate=0,
|
||||||
|
obf_variables=1,
|
||||||
|
obf_classes=0,
|
||||||
|
obf_functions=0,
|
||||||
|
obf_import_methods=0,
|
||||||
|
obf_builtins=0,
|
||||||
|
)
|
||||||
|
|
||||||
tokens = pyminifier.token_utils.listified_tokenizer(source)
|
tokens = pyminifier.token_utils.listified_tokenizer(source)
|
||||||
|
|
||||||
@ -141,13 +150,17 @@ def pyminify(source, obfuscate=False, minify=True):
|
|||||||
|
|
||||||
if obfuscate:
|
if obfuscate:
|
||||||
name_generator = pyminifier.obfuscate.obfuscation_machine(use_unicode=False)
|
name_generator = pyminifier.obfuscate.obfuscation_machine(use_unicode=False)
|
||||||
pyminifier.obfuscate.obfuscate("__main__", tokens, options, name_generator=name_generator)
|
pyminifier.obfuscate.obfuscate(
|
||||||
|
"__main__", tokens, options, name_generator=name_generator
|
||||||
|
)
|
||||||
# source = pyminifier.obfuscate.apply_obfuscation(source)
|
# source = pyminifier.obfuscate.apply_obfuscation(source)
|
||||||
|
|
||||||
source = pyminifier.token_utils.untokenize(tokens)
|
source = pyminifier.token_utils.untokenize(tokens)
|
||||||
# logger.info(source)
|
# logger.info(source)
|
||||||
minified_source = pyminifier.compression.gz_pack(source)
|
minified_source = pyminifier.compression.gz_pack(source)
|
||||||
logger.info("Compressed python code from {} to {}".format(len(source), len(minified_source)))
|
logger.info(
|
||||||
|
"Compressed python code from {} to {}".format(len(source), len(minified_source))
|
||||||
|
)
|
||||||
return minified_source
|
return minified_source
|
||||||
|
|
||||||
|
|
||||||
@ -157,10 +170,12 @@ def inline_yaml(block):
|
|||||||
|
|
||||||
def JinjaEnv(template_locations=[]):
|
def JinjaEnv(template_locations=[]):
|
||||||
LoggingUndefined = jinja2.make_logging_undefined(logger=logger, base=Undefined)
|
LoggingUndefined = jinja2.make_logging_undefined(logger=logger, base=Undefined)
|
||||||
jenv = jinja2.Environment(trim_blocks=True,
|
jenv = jinja2.Environment(
|
||||||
lstrip_blocks=True,
|
trim_blocks=True,
|
||||||
undefined=LoggingUndefined,
|
lstrip_blocks=True,
|
||||||
extensions=['jinja2.ext.loopcontrols', 'jinja2.ext.do'])
|
undefined=LoggingUndefined,
|
||||||
|
extensions=["jinja2.ext.loopcontrols", "jinja2.ext.do"],
|
||||||
|
)
|
||||||
|
|
||||||
if template_locations:
|
if template_locations:
|
||||||
jinja_loaders = []
|
jinja_loaders = []
|
||||||
@ -171,29 +186,29 @@ def JinjaEnv(template_locations=[]):
|
|||||||
else:
|
else:
|
||||||
jenv.loader = jinja2.BaseLoader()
|
jenv.loader = jinja2.BaseLoader()
|
||||||
|
|
||||||
jenv.globals['include_raw'] = include_raw_gz
|
jenv.globals["include_raw"] = include_raw_gz
|
||||||
jenv.globals['raise'] = raise_helper
|
jenv.globals["raise"] = raise_helper
|
||||||
jenv.globals['option'] = option
|
jenv.globals["option"] = option
|
||||||
|
|
||||||
jenv.filters['sub'] = sub
|
jenv.filters["sub"] = sub
|
||||||
jenv.filters['pyminify'] = pyminify
|
jenv.filters["pyminify"] = pyminify
|
||||||
jenv.filters['inline_yaml'] = inline_yaml
|
jenv.filters["inline_yaml"] = inline_yaml
|
||||||
|
|
||||||
jenv.tests['match'] = match
|
jenv.tests["match"] = match
|
||||||
jenv.tests['regex'] = regex
|
jenv.tests["regex"] = regex
|
||||||
jenv.tests['search'] = search
|
jenv.tests["search"] = search
|
||||||
|
|
||||||
return jenv
|
return jenv
|
||||||
|
|
||||||
|
|
||||||
def read_config_file(path, variables={}):
|
def read_config_file(path, variables={}):
|
||||||
""" reads yaml config file, passes it through jinja and returns data structre
|
"""reads yaml config file, passes it through jinja and returns data structre
|
||||||
|
|
||||||
- OS ENV are available as {{ ENV.<VAR> }}
|
- OS ENV are available as {{ ENV.<VAR> }}
|
||||||
- variables defined in parent configs are available as {{ <VAR> }}
|
- variables defined in parent configs are available as {{ <VAR> }}
|
||||||
"""
|
"""
|
||||||
jinja_variables = copy.deepcopy(variables)
|
jinja_variables = copy.deepcopy(variables)
|
||||||
jinja_variables['ENV'] = os.environ
|
jinja_variables["ENV"] = os.environ
|
||||||
|
|
||||||
if path.exists():
|
if path.exists():
|
||||||
logger.debug("Reading config file: {}".format(path))
|
logger.debug("Reading config file: {}".format(path))
|
||||||
@ -205,7 +220,8 @@ def read_config_file(path, variables={}):
|
|||||||
auto_reload=False,
|
auto_reload=False,
|
||||||
loader=jinja2.FunctionLoader(_sops_loader),
|
loader=jinja2.FunctionLoader(_sops_loader),
|
||||||
undefined=jinja2.StrictUndefined,
|
undefined=jinja2.StrictUndefined,
|
||||||
extensions=['jinja2.ext.loopcontrols'])
|
extensions=["jinja2.ext.loopcontrols"],
|
||||||
|
)
|
||||||
template = jenv.get_template(str(path))
|
template = jenv.get_template(str(path))
|
||||||
rendered_template = template.render(jinja_variables)
|
rendered_template = template.render(jinja_variables)
|
||||||
data = yaml.safe_load(rendered_template)
|
data = yaml.safe_load(rendered_template)
|
||||||
@ -220,26 +236,37 @@ def read_config_file(path, variables={}):
|
|||||||
|
|
||||||
|
|
||||||
def _sops_loader(path):
|
def _sops_loader(path):
|
||||||
""" Tries to loads yaml file
|
"""Tries to loads yaml file
|
||||||
If "sops" key is detected the file is piped through sops before returned
|
If "sops" key is detected the file is piped through sops before returned
|
||||||
"""
|
"""
|
||||||
with open(path, 'r') as f:
|
with open(path, "r") as f:
|
||||||
config_raw = f.read()
|
config_raw = f.read()
|
||||||
data = yaml.safe_load(config_raw)
|
data = yaml.safe_load(config_raw)
|
||||||
|
|
||||||
if data and 'sops' in data and 'DISABLE_SOPS' not in os.environ:
|
if data and "sops" in data and "DISABLE_SOPS" not in os.environ:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run([
|
result = subprocess.run(
|
||||||
'sops',
|
[
|
||||||
'--input-type', 'yaml',
|
"sops",
|
||||||
'--output-type', 'yaml',
|
"--input-type",
|
||||||
'--decrypt', '/dev/stdin'
|
"yaml",
|
||||||
], stdout=subprocess.PIPE, input=config_raw.encode('utf-8'),
|
"--output-type",
|
||||||
env=dict(os.environ, **{"AWS_SDK_LOAD_CONFIG": "1"}))
|
"yaml",
|
||||||
|
"--decrypt",
|
||||||
|
"/dev/stdin",
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
input=config_raw.encode("utf-8"),
|
||||||
|
env=dict(os.environ, **{"AWS_SDK_LOAD_CONFIG": "1"}),
|
||||||
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logger.exception("SOPS encrypted config {}, but unable to find sops binary! Try eg: https://github.com/mozilla/sops/releases/download/v3.5.0/sops-v3.5.0.linux".format(path))
|
logger.exception(
|
||||||
|
"SOPS encrypted config {}, but unable to find sops binary! Try eg: https://github.com/mozilla/sops/releases/download/v3.5.0/sops-v3.5.0.linux".format(
|
||||||
|
path
|
||||||
|
)
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
return result.stdout.decode('utf-8')
|
return result.stdout.decode("utf-8")
|
||||||
else:
|
else:
|
||||||
return config_raw
|
return config_raw
|
||||||
|
@ -7,30 +7,38 @@ import pkg_resources
|
|||||||
import pulumi
|
import pulumi
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def pulumi_init(stack):
|
def pulumi_init(stack):
|
||||||
|
|
||||||
# Fail early if pulumi binaries are not available
|
# Fail early if pulumi binaries are not available
|
||||||
if not shutil.which('pulumi'):
|
if not shutil.which("pulumi"):
|
||||||
raise FileNotFoundError("Cannot find pulumi binary, see https://www.pulumi.com/docs/get-started/install/")
|
raise FileNotFoundError(
|
||||||
|
"Cannot find pulumi binary, see https://www.pulumi.com/docs/get-started/install/"
|
||||||
|
)
|
||||||
|
|
||||||
# add all artifact_paths/pulumi to the search path for easier imports in the pulumi code
|
# add all artifact_paths/pulumi to the search path for easier imports in the pulumi code
|
||||||
for artifacts_path in stack.ctx['artifact_paths']:
|
for artifacts_path in stack.ctx["artifact_paths"]:
|
||||||
_path = '{}/pulumi'.format(artifacts_path.resolve())
|
_path = "{}/pulumi".format(artifacts_path.resolve())
|
||||||
sys.path.append(_path)
|
sys.path.append(_path)
|
||||||
|
|
||||||
# Try local implementation first, similar to Jinja2 mode
|
# Try local implementation first, similar to Jinja2 mode
|
||||||
_found = False
|
_found = False
|
||||||
try:
|
try:
|
||||||
_stack = importlib.import_module('config.{}.{}'.format(stack.rel_path, stack.template).replace('/', '.'))
|
_stack = importlib.import_module(
|
||||||
|
"config.{}.{}".format(stack.rel_path, stack.template).replace("/", ".")
|
||||||
|
)
|
||||||
_found = True
|
_found = True
|
||||||
|
|
||||||
except ImportError:
|
except ImportError:
|
||||||
for artifacts_path in stack.ctx['artifact_paths']:
|
for artifacts_path in stack.ctx["artifact_paths"]:
|
||||||
try:
|
try:
|
||||||
spec = importlib.util.spec_from_file_location("_stack", '{}/pulumi/{}.py'.format(artifacts_path.resolve(), stack.template))
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"_stack",
|
||||||
|
"{}/pulumi/{}.py".format(artifacts_path.resolve(), stack.template),
|
||||||
|
)
|
||||||
_stack = importlib.util.module_from_spec(spec)
|
_stack = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(_stack)
|
spec.loader.exec_module(_stack)
|
||||||
_found = True
|
_found = True
|
||||||
@ -39,36 +47,61 @@ def pulumi_init(stack):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if not _found:
|
if not _found:
|
||||||
raise FileNotFoundError("Cannot find Pulumi implementation for {}".format(stack.stackname))
|
raise FileNotFoundError(
|
||||||
|
"Cannot find Pulumi implementation for {}".format(stack.stackname)
|
||||||
|
)
|
||||||
|
|
||||||
project_name = stack.parameters['Conglomerate']
|
project_name = stack.parameters["Conglomerate"]
|
||||||
|
|
||||||
# Remove stacknameprefix if equals Conglomerate as Pulumi implicitly prefixes project_name
|
# Remove stacknameprefix if equals Conglomerate as Pulumi implicitly prefixes project_name
|
||||||
pulumi_stackname = re.sub(r'^' + project_name + '-?', '', stack.stackname)
|
pulumi_stackname = re.sub(r"^" + project_name + "-?", "", stack.stackname)
|
||||||
try:
|
try:
|
||||||
pulumi_backend = '{}/{}/{}'.format(stack.pulumi['backend'], project_name, stack.region)
|
pulumi_backend = "{}/{}/{}".format(
|
||||||
|
stack.pulumi["backend"], project_name, stack.region
|
||||||
|
)
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError('Missing pulumi.backend setting !')
|
raise KeyError("Missing pulumi.backend setting !")
|
||||||
|
|
||||||
account_id = stack.connection_manager.call('sts', 'get_caller_identity', profile=stack.profile, region=stack.region)['Account']
|
account_id = stack.connection_manager.call(
|
||||||
|
"sts", "get_caller_identity", profile=stack.profile, region=stack.region
|
||||||
|
)["Account"]
|
||||||
# Ugly hack as Pulumi currently doesnt support MFA_TOKENs during role assumptions
|
# Ugly hack as Pulumi currently doesnt support MFA_TOKENs during role assumptions
|
||||||
# Do NOT set them via 'aws:secretKey' as they end up in the stack.json in plain text !!!
|
# Do NOT set them via 'aws:secretKey' as they end up in the stack.json in plain text !!!
|
||||||
if stack.connection_manager._sessions[(stack.profile, stack.region)].get_credentials().token:
|
if (
|
||||||
os.environ['AWS_SESSION_TOKEN'] = stack.connection_manager._sessions[(stack.profile, stack.region)].get_credentials().token
|
stack.connection_manager._sessions[(stack.profile, stack.region)]
|
||||||
|
.get_credentials()
|
||||||
|
.token
|
||||||
|
):
|
||||||
|
os.environ["AWS_SESSION_TOKEN"] = (
|
||||||
|
stack.connection_manager._sessions[(stack.profile, stack.region)]
|
||||||
|
.get_credentials()
|
||||||
|
.token
|
||||||
|
)
|
||||||
|
|
||||||
os.environ['AWS_ACCESS_KEY_ID'] = stack.connection_manager._sessions[(stack.profile, stack.region)].get_credentials().access_key
|
os.environ["AWS_ACCESS_KEY_ID"] = (
|
||||||
os.environ['AWS_SECRET_ACCESS_KEY'] = stack.connection_manager._sessions[(stack.profile, stack.region)].get_credentials().secret_key
|
stack.connection_manager._sessions[(stack.profile, stack.region)]
|
||||||
os.environ['AWS_DEFAULT_REGION'] = stack.region
|
.get_credentials()
|
||||||
|
.access_key
|
||||||
|
)
|
||||||
|
os.environ["AWS_SECRET_ACCESS_KEY"] = (
|
||||||
|
stack.connection_manager._sessions[(stack.profile, stack.region)]
|
||||||
|
.get_credentials()
|
||||||
|
.secret_key
|
||||||
|
)
|
||||||
|
os.environ["AWS_DEFAULT_REGION"] = stack.region
|
||||||
|
|
||||||
# Secrets provider
|
# Secrets provider
|
||||||
try:
|
try:
|
||||||
secrets_provider = stack.pulumi['secretsProvider']
|
secrets_provider = stack.pulumi["secretsProvider"]
|
||||||
if secrets_provider == 'passphrase' and 'PULUMI_CONFIG_PASSPHRASE' not in os.environ:
|
if (
|
||||||
raise ValueError('Missing PULUMI_CONFIG_PASSPHRASE environment variable!')
|
secrets_provider == "passphrase"
|
||||||
|
and "PULUMI_CONFIG_PASSPHRASE" not in os.environ
|
||||||
|
):
|
||||||
|
raise ValueError("Missing PULUMI_CONFIG_PASSPHRASE environment variable!")
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
logger.warning('Missing pulumi.secretsProvider setting, secrets disabled !')
|
logger.warning("Missing pulumi.secretsProvider setting, secrets disabled !")
|
||||||
secrets_provider = None
|
secrets_provider = None
|
||||||
|
|
||||||
# Set tag for stack file name and version
|
# Set tag for stack file name and version
|
||||||
@ -76,9 +109,11 @@ def pulumi_init(stack):
|
|||||||
try:
|
try:
|
||||||
_version = _stack.VERSION
|
_version = _stack.VERSION
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
_version = 'undefined'
|
_version = "undefined"
|
||||||
|
|
||||||
_tags['zero-downtime.net/cloudbender'] = '{}:{}'.format(os.path.basename(_stack.__file__), _version)
|
_tags["zero-downtime.net/cloudbender"] = "{}:{}".format(
|
||||||
|
os.path.basename(_stack.__file__), _version
|
||||||
|
)
|
||||||
|
|
||||||
_config = {
|
_config = {
|
||||||
"aws:region": stack.region,
|
"aws:region": stack.region,
|
||||||
@ -90,27 +125,36 @@ def pulumi_init(stack):
|
|||||||
|
|
||||||
# inject all parameters as config in the <Conglomerate> namespace
|
# inject all parameters as config in the <Conglomerate> namespace
|
||||||
for p in stack.parameters:
|
for p in stack.parameters:
|
||||||
_config['{}:{}'.format(stack.parameters['Conglomerate'], p)] = stack.parameters[p]
|
_config["{}:{}".format(stack.parameters["Conglomerate"], p)] = stack.parameters[
|
||||||
|
p
|
||||||
|
]
|
||||||
|
|
||||||
stack_settings = pulumi.automation.StackSettings(
|
stack_settings = pulumi.automation.StackSettings(
|
||||||
config=_config,
|
config=_config,
|
||||||
secrets_provider=secrets_provider,
|
secrets_provider=secrets_provider,
|
||||||
encryption_salt=stack.pulumi.get('encryptionsalt', None),
|
encryption_salt=stack.pulumi.get("encryptionsalt", None),
|
||||||
encrypted_key=stack.pulumi.get('encryptedkey', None)
|
encrypted_key=stack.pulumi.get("encryptedkey", None),
|
||||||
)
|
)
|
||||||
|
|
||||||
project_settings = pulumi.automation.ProjectSettings(
|
project_settings = pulumi.automation.ProjectSettings(
|
||||||
name=project_name,
|
name=project_name, runtime="python", backend={"url": pulumi_backend}
|
||||||
runtime="python",
|
)
|
||||||
backend={"url": pulumi_backend})
|
|
||||||
|
|
||||||
ws_opts = pulumi.automation.LocalWorkspaceOptions(
|
ws_opts = pulumi.automation.LocalWorkspaceOptions(
|
||||||
work_dir=stack.work_dir,
|
work_dir=stack.work_dir,
|
||||||
project_settings=project_settings,
|
project_settings=project_settings,
|
||||||
stack_settings={pulumi_stackname: stack_settings},
|
stack_settings={pulumi_stackname: stack_settings},
|
||||||
secrets_provider=secrets_provider)
|
secrets_provider=secrets_provider,
|
||||||
|
)
|
||||||
|
|
||||||
stack = pulumi.automation.create_or_select_stack(stack_name=pulumi_stackname, project_name=project_name, program=_stack.pulumi_program, opts=ws_opts)
|
stack = pulumi.automation.create_or_select_stack(
|
||||||
stack.workspace.install_plugin("aws", pkg_resources.get_distribution("pulumi_aws").version)
|
stack_name=pulumi_stackname,
|
||||||
|
project_name=project_name,
|
||||||
|
program=_stack.pulumi_program,
|
||||||
|
opts=ws_opts,
|
||||||
|
)
|
||||||
|
stack.workspace.install_plugin(
|
||||||
|
"aws", pkg_resources.get_distribution("pulumi_aws").version
|
||||||
|
)
|
||||||
|
|
||||||
return stack
|
return stack
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -13,19 +13,21 @@ class StackGroup(object):
|
|||||||
self.name = None
|
self.name = None
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.path = path
|
self.path = path
|
||||||
self.rel_path = path.relative_to(ctx['config_path'])
|
self.rel_path = path.relative_to(ctx["config_path"])
|
||||||
self.config = {}
|
self.config = {}
|
||||||
self.sgs = []
|
self.sgs = []
|
||||||
self.stacks = []
|
self.stacks = []
|
||||||
|
|
||||||
if self.rel_path == '.':
|
if self.rel_path == ".":
|
||||||
self.rel_path = ''
|
self.rel_path = ""
|
||||||
|
|
||||||
def dump_config(self):
|
def dump_config(self):
|
||||||
for sg in self.sgs:
|
for sg in self.sgs:
|
||||||
sg.dump_config()
|
sg.dump_config()
|
||||||
|
|
||||||
logger.debug("StackGroup {}: {}".format(self.rel_path, pprint.pformat(self.config)))
|
logger.debug(
|
||||||
|
"StackGroup {}: {}".format(self.rel_path, pprint.pformat(self.config))
|
||||||
|
)
|
||||||
|
|
||||||
for s in self.stacks:
|
for s in self.stacks:
|
||||||
s.dump_config()
|
s.dump_config()
|
||||||
@ -35,7 +37,9 @@ class StackGroup(object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# First read config.yaml if present
|
# First read config.yaml if present
|
||||||
_config = read_config_file(self.path.joinpath('config.yaml'), parent_config.get('variables', {}))
|
_config = read_config_file(
|
||||||
|
self.path.joinpath("config.yaml"), parent_config.get("variables", {})
|
||||||
|
)
|
||||||
|
|
||||||
# Stack Group name if not explicit via config is derived from subfolder, or in case of root object the parent folder
|
# Stack Group name if not explicit via config is derived from subfolder, or in case of root object the parent folder
|
||||||
if "stackgroupname" in _config:
|
if "stackgroupname" in _config:
|
||||||
@ -45,19 +49,25 @@ class StackGroup(object):
|
|||||||
|
|
||||||
# Merge config with parent config
|
# Merge config with parent config
|
||||||
self.config = dict_merge(parent_config, _config)
|
self.config = dict_merge(parent_config, _config)
|
||||||
stackname_prefix = self.config.get('stacknameprefix', '')
|
stackname_prefix = self.config.get("stacknameprefix", "")
|
||||||
|
|
||||||
logger.debug("StackGroup {} added.".format(self.name))
|
logger.debug("StackGroup {} added.".format(self.name))
|
||||||
|
|
||||||
# Add stacks
|
# Add stacks
|
||||||
stacks = [s for s in self.path.glob('*.yaml') if not s.name == "config.yaml"]
|
stacks = [s for s in self.path.glob("*.yaml") if not s.name == "config.yaml"]
|
||||||
for stack_path in stacks:
|
for stack_path in stacks:
|
||||||
stackname = stack_path.name.split('.')[0]
|
stackname = stack_path.name.split(".")[0]
|
||||||
template = stackname
|
template = stackname
|
||||||
if stackname_prefix:
|
if stackname_prefix:
|
||||||
stackname = stackname_prefix + stackname
|
stackname = stackname_prefix + stackname
|
||||||
|
|
||||||
new_stack = Stack(name=stackname, template=template, path=stack_path, rel_path=str(self.rel_path), ctx=self.ctx)
|
new_stack = Stack(
|
||||||
|
name=stackname,
|
||||||
|
template=template,
|
||||||
|
path=stack_path,
|
||||||
|
rel_path=str(self.rel_path),
|
||||||
|
ctx=self.ctx,
|
||||||
|
)
|
||||||
new_stack.read_config(self.config)
|
new_stack.read_config(self.config)
|
||||||
self.stacks.append(new_stack)
|
self.stacks.append(new_stack)
|
||||||
|
|
||||||
@ -68,22 +78,24 @@ class StackGroup(object):
|
|||||||
|
|
||||||
self.sgs.append(sg)
|
self.sgs.append(sg)
|
||||||
|
|
||||||
def get_stacks(self, name=None, recursive=True, match_by='name'):
|
def get_stacks(self, name=None, recursive=True, match_by="name"):
|
||||||
""" Returns [stack] matching stack_name or [all] """
|
"""Returns [stack] matching stack_name or [all]"""
|
||||||
stacks = []
|
stacks = []
|
||||||
if name:
|
if name:
|
||||||
logger.debug("Looking for stack {} in group {}".format(name, self.name))
|
logger.debug("Looking for stack {} in group {}".format(name, self.name))
|
||||||
|
|
||||||
for s in self.stacks:
|
for s in self.stacks:
|
||||||
if name:
|
if name:
|
||||||
if match_by == 'name' and s.stackname != name:
|
if match_by == "name" and s.stackname != name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if match_by == 'path' and not s.path.match(name):
|
if match_by == "path" and not s.path.match(name):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self.rel_path:
|
if self.rel_path:
|
||||||
logger.debug("Found stack {} in group {}".format(s.stackname, self.rel_path))
|
logger.debug(
|
||||||
|
"Found stack {} in group {}".format(s.stackname, self.rel_path)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug("Found stack {}".format(s.stackname))
|
logger.debug("Found stack {}".format(s.stackname))
|
||||||
stacks.append(s)
|
stacks.append(s)
|
||||||
@ -96,14 +108,20 @@ class StackGroup(object):
|
|||||||
|
|
||||||
return stacks
|
return stacks
|
||||||
|
|
||||||
def get_stackgroup(self, name=None, recursive=True, match_by='name'):
|
def get_stackgroup(self, name=None, recursive=True, match_by="name"):
|
||||||
""" Returns stack group matching stackgroup_name or all if None """
|
"""Returns stack group matching stackgroup_name or all if None"""
|
||||||
if not name or (self.name == name and match_by == 'name') or (self.path.match(name) and match_by == 'path'):
|
if (
|
||||||
|
not name
|
||||||
|
or (self.name == name and match_by == "name")
|
||||||
|
or (self.path.match(name) and match_by == "path")
|
||||||
|
):
|
||||||
logger.debug("Found stack_group {}".format(self.name))
|
logger.debug("Found stack_group {}".format(self.name))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
if name and self.name != 'config':
|
if name and self.name != "config":
|
||||||
logger.debug("Looking for stack_group {} in group {}".format(name, self.name))
|
logger.debug(
|
||||||
|
"Looking for stack_group {} in group {}".format(name, self.name)
|
||||||
|
)
|
||||||
|
|
||||||
if recursive:
|
if recursive:
|
||||||
for sg in self.sgs:
|
for sg in self.sgs:
|
||||||
|
@ -5,7 +5,7 @@ import re
|
|||||||
|
|
||||||
|
|
||||||
def dict_merge(a, b):
|
def dict_merge(a, b):
|
||||||
""" Deep merge to allow proper inheritance for config files"""
|
"""Deep merge to allow proper inheritance for config files"""
|
||||||
if not a:
|
if not a:
|
||||||
return b
|
return b
|
||||||
|
|
||||||
@ -36,16 +36,14 @@ def setup_logging(debug):
|
|||||||
logging.getLogger("botocore").setLevel(logging.INFO)
|
logging.getLogger("botocore").setLevel(logging.INFO)
|
||||||
|
|
||||||
formatter = logging.Formatter(
|
formatter = logging.Formatter(
|
||||||
fmt="[%(asctime)s] %(name)s %(message)s",
|
fmt="[%(asctime)s] %(name)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
our_level = logging.INFO
|
our_level = logging.INFO
|
||||||
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
logging.getLogger("botocore").setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
formatter = logging.Formatter(
|
formatter = logging.Formatter(
|
||||||
fmt="[%(asctime)s] %(message)s",
|
fmt="[%(asctime)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
log_handler = logging.StreamHandler()
|
log_handler = logging.StreamHandler()
|
||||||
@ -57,8 +55,8 @@ def setup_logging(debug):
|
|||||||
|
|
||||||
|
|
||||||
def search_refs(template, attributes, mode):
|
def search_refs(template, attributes, mode):
|
||||||
""" Traverses a template and searches for any remote references and
|
"""Traverses a template and searches for any remote references and
|
||||||
adds them to the attributes set
|
adds them to the attributes set
|
||||||
"""
|
"""
|
||||||
if isinstance(template, dict):
|
if isinstance(template, dict):
|
||||||
for k, v in template.items():
|
for k, v in template.items():
|
||||||
@ -70,7 +68,7 @@ def search_refs(template, attributes, mode):
|
|||||||
# CloudBender::StackRef
|
# CloudBender::StackRef
|
||||||
if k == "CloudBender::StackRef":
|
if k == "CloudBender::StackRef":
|
||||||
try:
|
try:
|
||||||
attributes.append(v['StackTags']['Artifact'])
|
attributes.append(v["StackTags"]["Artifact"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -91,11 +89,11 @@ def get_s3_url(url, *args):
|
|||||||
bucket = None
|
bucket = None
|
||||||
path = None
|
path = None
|
||||||
|
|
||||||
m = re.match('^(s3://)?([^/]*)(/.*)?', url)
|
m = re.match("^(s3://)?([^/]*)(/.*)?", url)
|
||||||
bucket = m[2]
|
bucket = m[2]
|
||||||
if m[3]:
|
if m[3]:
|
||||||
path = m[3].lstrip('/')
|
path = m[3].lstrip("/")
|
||||||
|
|
||||||
path = os.path.join(path, *args)
|
path = os.path.join(path, *args)
|
||||||
|
|
||||||
return(bucket, path)
|
return (bucket, path)
|
||||||
|
Loading…
Reference in New Issue
Block a user