From 40e4d80fc085c797ba3dbce3827d66f949044db6 Mon Sep 17 00:00:00 2001 From: Dan Boitnott Date: Fri, 23 Dec 2022 12:19:44 -0600 Subject: [PATCH] gh-1276: Reformatted with Black --- .circleci/documentation-versions.py | 8 +- .flake8 | 1 + docs/_source/conf.py | 85 +- integration-tests/environment.py | 40 +- .../templates/missing_sceptre_handler.py | 2 +- .../templates/valid_template_json.py | 10 +- .../templates/valid_template_yaml.py | 10 +- integration-tests/steps/change_sets.py | 129 ++- integration-tests/steps/drift.py | 18 +- integration-tests/steps/helpers.py | 22 +- .../steps/project_dependencies.py | 53 +- integration-tests/steps/stack_groups.py | 88 +- integration-tests/steps/stack_policies.py | 32 +- integration-tests/steps/stacks.py | 194 +++-- integration-tests/steps/templates.py | 65 +- sceptre/__init__.py | 8 +- sceptre/cli/__init__.py | 55 +- sceptre/cli/create.py | 9 +- sceptre/cli/delete.py | 17 +- sceptre/cli/describe.py | 20 +- sceptre/cli/diff.py | 86 +- sceptre/cli/drift.py | 27 +- sceptre/cli/dump.py | 7 +- sceptre/cli/execute.py | 8 +- sceptre/cli/helpers.py | 131 ++- sceptre/cli/launch.py | 27 +- sceptre/cli/list.py | 44 +- sceptre/cli/new.py | 27 +- sceptre/cli/policy.py | 12 +- sceptre/cli/prune.py | 21 +- sceptre/cli/status.py | 12 +- sceptre/cli/template.py | 43 +- sceptre/cli/update.py | 13 +- sceptre/config/__init__.py | 6 +- sceptre/config/graph.py | 4 +- sceptre/config/reader.py | 129 +-- sceptre/config/strategies.py | 8 +- sceptre/connection_manager.py | 52 +- sceptre/context.py | 30 +- sceptre/diffing/diff_writer.py | 55 +- sceptre/diffing/stack_differ.py | 166 ++-- sceptre/exceptions.py | 25 + sceptre/helpers.py | 28 +- sceptre/hooks/__init__.py | 3 + sceptre/hooks/asg_scaling_processes.py | 18 +- sceptre/hooks/cmd.py | 2 +- sceptre/plan/__init__.py | 6 +- sceptre/plan/actions.py | 339 ++++---- sceptre/plan/executor.py | 6 +- sceptre/plan/plan.py | 13 +- sceptre/resolvers/__init__.py | 100 ++- sceptre/resolvers/placeholders.py | 18 +- sceptre/resolvers/stack_attr.py | 10 +- sceptre/resolvers/stack_output.py | 41 +- sceptre/stack.py | 115 +-- sceptre/stack_status.py | 2 + sceptre/stack_status_colourer.py | 8 +- sceptre/template.py | 71 +- sceptre/template_handlers/__init__.py | 18 +- sceptre/template_handlers/file.py | 21 +- sceptre/template_handlers/helper.py | 28 +- sceptre/template_handlers/http.py | 50 +- sceptre/template_handlers/s3.py | 29 +- setup.py | 33 +- tests/fixtures-vpc/hooks/custom_hook.py | 1 + tests/fixtures-vpc/templates/vpc.py | 62 +- tests/fixtures-vpc/templates/vpc_sgt.py | 55 +- tests/fixtures-vpc/templates/vpc_sud.py | 48 +- tests/fixtures-vpc/templates/vpc_t.py | 62 +- tests/fixtures/hooks/custom_hook.py | 1 + tests/fixtures/templates/vpc.py | 62 +- tests/fixtures/templates/vpc_sgt.py | 55 +- tests/fixtures/templates/vpc_sud.py | 48 +- tests/fixtures/templates/vpc_t.py | 62 +- tests/test_actions.py | 779 ++++++++--------- tests/test_cli/test_cli_commands.py | 806 +++++++++--------- tests/test_cli/test_launch.py | 50 +- tests/test_cli/test_prune.py | 48 +- tests/test_config_reader.py | 405 ++++----- tests/test_connection_manager.py | 113 +-- tests/test_context.py | 17 +- tests/test_diffing/test_diff_writer.py | 256 +++--- tests/test_diffing/test_stack_differ.py | 516 +++++------ tests/test_helpers.py | 40 +- .../test_hooks/test_asg_scaling_processes.py | 72 +- tests/test_hooks/test_cmd.py | 10 +- tests/test_hooks/test_hooks.py | 7 +- tests/test_plan.py | 31 +- .../test_environment_variable.py | 5 +- tests/test_resolvers/test_file_contents.py | 7 +- tests/test_resolvers/test_placeholders.py | 48 +- tests/test_resolvers/test_resolver.py | 382 ++++----- tests/test_resolvers/test_stack_attr.py | 56 +- tests/test_resolvers/test_stack_output.py | 158 ++-- tests/test_stack.py | 201 +++-- tests/test_stack_status_colourer.py | 26 +- tests/test_template.py | 136 ++- tests/test_template_handlers/test_file.py | 100 ++- tests/test_template_handlers/test_helper.py | 114 +-- tests/test_template_handlers/test_http.py | 94 +- tests/test_template_handlers/test_s3.py | 84 +- .../test_template_handlers.py | 10 +- 102 files changed, 3869 insertions(+), 3916 deletions(-) diff --git a/.circleci/documentation-versions.py b/.circleci/documentation-versions.py index 73db5987e..d0a7378c8 100644 --- a/.circleci/documentation-versions.py +++ b/.circleci/documentation-versions.py @@ -52,13 +52,13 @@ def main(): if item.is_dir() and VERSION_REGEX.match(item.name) ), reverse=True, - key=attrgetter("name") + key=attrgetter("name"), ) active_versions = ( - ["latest", "dev"] - + [item.name for item in documentation_directories[:NUMBER_OF_VERSIONS_TO_KEEP]] - + KEEP_VERSIONS + ["latest", "dev"] + + [item.name for item in documentation_directories[:NUMBER_OF_VERSIONS_TO_KEEP]] + + KEEP_VERSIONS ) versions_to_remove = ( item.path diff --git a/.flake8 b/.flake8 index 8b0b66b94..c1f90c901 100644 --- a/.flake8 +++ b/.flake8 @@ -12,4 +12,5 @@ max-complexity = 12 per-file-ignores = docs/_api/conf.py: E265 integration-tests/steps/*: E501,F811,F403,F405 +extend-ignore = E203 max-line-length = 120 diff --git a/docs/_source/conf.py b/docs/_source/conf.py index 847535267..4fe77fd2d 100644 --- a/docs/_source/conf.py +++ b/docs/_source/conf.py @@ -19,7 +19,7 @@ import os import sys -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..' + os.path.sep + '..')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".." + os.path.sep + "..")) import sceptre # noqa # The short X.Y version @@ -27,8 +27,8 @@ # The full version, including alpha/beta/rc tags release = version -project = 'Sceptre' -copyright = '2018, Cloudreach' +project = "Sceptre" +copyright = "2018, Cloudreach" author = sceptre.__author__ # -- General configuration --------------------------------------------------- @@ -41,34 +41,34 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx_autodoc_typehints', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.coverage', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'sphinx_click.ext', + "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.coverage", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", + "sphinx_click.ext", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set 'language' from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -83,7 +83,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -110,7 +110,7 @@ # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'Sceptredoc' +htmlhelp_basename = "Sceptredoc" # -- Options for LaTeX output ------------------------------------------------ @@ -133,14 +133,14 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'Sceptre.tex', 'Sceptre Documentation', 'Cloudreach', 'manual') + (master_doc, "Sceptre.tex", "Sceptre Documentation", "Cloudreach", "manual") ] # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, 'sceptre', 'Sceptre Documentation', [author], 1)] +man_pages = [(master_doc, "sceptre", "Sceptre Documentation", [author], 1)] # -- Options for Texinfo output ---------------------------------------------- @@ -150,12 +150,12 @@ texinfo_documents = [ ( master_doc, - 'Sceptre', - 'Sceptre Documentation', + "Sceptre", + "Sceptre Documentation", author, - 'Sceptre', - 'One line description of project.', - 'Miscellaneous', + "Sceptre", + "One line description of project.", + "Miscellaneous", ) ] @@ -177,7 +177,7 @@ # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # -- Extension configuration ------------------------------------------------- @@ -185,15 +185,12 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { - 'python': ('https://docs.python.org/3/', None), - 'boto3': ( - 'https://boto3.amazonaws.com/v1/documentation/api/latest/', - 'https://boto3.amazonaws.com/v1/documentation/api/latest/objects.inv', + "python": ("https://docs.python.org/3/", None), + "boto3": ( + "https://boto3.amazonaws.com/v1/documentation/api/latest/", + "https://boto3.amazonaws.com/v1/documentation/api/latest/objects.inv", ), - 'deepdiff': ( - 'https://zepworks.com/deepdiff/current/', - None - ) + "deepdiff": ("https://zepworks.com/deepdiff/current/", None), } # other configuration @@ -201,17 +198,17 @@ nitpicky = True nitpick_ignore = [ - ('py:class', 'json.encoder.JSONEncoder'), - ('py:class', 'sceptre.config.reader.Attributes'), - ('py:class', 'sceptre.diffing.stack_differ.DiffType'), - ('py:obj', 'sceptre.diffing.stack_differ.DiffType'), - ('py:class', 'DiffType'), - ('py:class', 'TextIO'), - ('py:class', '_io.StringIO'), - ('py:class', 'yaml.loader.SafeLoader'), - ('py:class', 'yaml.dumper.Dumper'), - ('py:class', 'cfn_tools.odict.ODict'), - ('py:class', 'T_Container') + ("py:class", "json.encoder.JSONEncoder"), + ("py:class", "sceptre.config.reader.Attributes"), + ("py:class", "sceptre.diffing.stack_differ.DiffType"), + ("py:obj", "sceptre.diffing.stack_differ.DiffType"), + ("py:class", "DiffType"), + ("py:class", "TextIO"), + ("py:class", "_io.StringIO"), + ("py:class", "yaml.loader.SafeLoader"), + ("py:class", "yaml.dumper.Dumper"), + ("py:class", "cfn_tools.odict.ODict"), + ("py:class", "T_Container"), ] set_type_checking_flag = True diff --git a/integration-tests/environment.py b/integration-tests/environment.py index ad88db0aa..4b90f67b5 100644 --- a/integration-tests/environment.py +++ b/integration-tests/environment.py @@ -8,16 +8,14 @@ def before_all(context): - random_str = ''.join(random.choices(string.ascii_lowercase + string.digits, k=16)) - context.TEST_ARTIFACT_BUCKET_NAME = f'sceptre-test-artifacts-{random_str}' + random_str = "".join(random.choices(string.ascii_lowercase + string.digits, k=16)) + context.TEST_ARTIFACT_BUCKET_NAME = f"sceptre-test-artifacts-{random_str}" context.region = boto3.session.Session().region_name context.uuid = uuid.uuid1().hex - context.project_code = "sceptre-integration-tests-{0}".format( - context.uuid - ) + context.project_code = "sceptre-integration-tests-{0}".format(context.uuid) sts = boto3.client("sts") - account_number = sts.get_caller_identity()['Account'] + account_number = sts.get_caller_identity()["Account"] context.bucket_name = "sceptre-integration-tests-templates-{}".format( account_number ) @@ -26,12 +24,10 @@ def before_all(context): os.getcwd(), "integration-tests", "sceptre-project" ) update_config(context) - context.cloudformation = boto3.resource('cloudformation') + context.cloudformation = boto3.resource("cloudformation") context.client = boto3.client("cloudformation") - config_path = os.path.join( - context.sceptre_dir, "config", "9/B" + ".yaml" - ) + config_path = os.path.join(context.sceptre_dir, "config", "9/B" + ".yaml") with open(config_path, "r") as file: file_data = file.read() @@ -51,28 +47,22 @@ def before_scenario(context, scenario): def update_config(context): - config_path = os.path.join( - context.sceptre_dir, "config", "config.yaml" - ) + config_path = os.path.join(context.sceptre_dir, "config", "config.yaml") with open(config_path) as config_file: stack_group_config = yaml.safe_load(config_file) stack_group_config["template_bucket_name"] = context.bucket_name stack_group_config["project_code"] = context.project_code - with open(config_path, 'w') as config_file: - yaml.safe_dump( - stack_group_config, config_file, default_flow_style=False - ) + with open(config_path, "w") as config_file: + yaml.safe_dump(stack_group_config, config_file, default_flow_style=False) def after_all(context): response = context.client.describe_stacks() for stack in response["Stacks"]: if stack["StackName"].startswith(context.project_code): - context.client.delete_stack( - StackName=stack["StackName"] - ) + context.client.delete_stack(StackName=stack["StackName"]) time.sleep(2) context.project_code = "sceptre-integration-tests" context.bucket_name = "sceptre-integration-tests-templates" @@ -84,11 +74,11 @@ def before_feature(context, feature): Create a test bucket with a unique name and upload test artifact to the bucket for the S3 template handler to reference """ - if 's3-template-handler' in feature.tags: - bucket = boto3.resource('s3').Bucket(context.TEST_ARTIFACT_BUCKET_NAME) + if "s3-template-handler" in feature.tags: + bucket = boto3.resource("s3").Bucket(context.TEST_ARTIFACT_BUCKET_NAME) if bucket.creation_date is None: bucket.create( - CreateBucketConfiguration={'LocationConstraint': context.region} + CreateBucketConfiguration={"LocationConstraint": context.region} ) @@ -96,8 +86,8 @@ def after_feature(context, feature): """ Do a full cleanup of the test artifacts and the test bucket """ - if 's3-template-handler' in feature.tags: - bucket = boto3.resource('s3').Bucket(context.TEST_ARTIFACT_BUCKET_NAME) + if "s3-template-handler" in feature.tags: + bucket = boto3.resource("s3").Bucket(context.TEST_ARTIFACT_BUCKET_NAME) if bucket.creation_date is not None: bucket.objects.all().delete() bucket.delete() diff --git a/integration-tests/sceptre-project/templates/missing_sceptre_handler.py b/integration-tests/sceptre-project/templates/missing_sceptre_handler.py index 54b08a6ad..244a421fe 100644 --- a/integration-tests/sceptre-project/templates/missing_sceptre_handler.py +++ b/integration-tests/sceptre-project/templates/missing_sceptre_handler.py @@ -1,2 +1,2 @@ -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/integration-tests/sceptre-project/templates/valid_template_json.py b/integration-tests/sceptre-project/templates/valid_template_json.py index 2bbeb2a27..ef7813a79 100644 --- a/integration-tests/sceptre-project/templates/valid_template_json.py +++ b/integration-tests/sceptre-project/templates/valid_template_json.py @@ -3,11 +3,11 @@ def sceptre_handler(scepter_user_data): template = { - "Resources": { - "WaitConditionHandle": { - "Type": "AWS::CloudFormation::WaitConditionHandle", - "Properties": {} + "Resources": { + "WaitConditionHandle": { + "Type": "AWS::CloudFormation::WaitConditionHandle", + "Properties": {}, + } } - } } return json.dumps(template) diff --git a/integration-tests/sceptre-project/templates/valid_template_yaml.py b/integration-tests/sceptre-project/templates/valid_template_yaml.py index b374549a9..e7c209ea1 100644 --- a/integration-tests/sceptre-project/templates/valid_template_yaml.py +++ b/integration-tests/sceptre-project/templates/valid_template_yaml.py @@ -3,11 +3,11 @@ def sceptre_handler(scepter_user_data): template = { - "Resources": { - "WaitConditionHandle": { - "Type": "AWS::CloudFormation::WaitConditionHandle", - "Properties": {} + "Resources": { + "WaitConditionHandle": { + "Type": "AWS::CloudFormation::WaitConditionHandle", + "Properties": {}, + } } - } } return yaml.dump(template) diff --git a/integration-tests/steps/change_sets.py b/integration-tests/steps/change_sets.py index 4f140ec1c..2c5fa0c07 100644 --- a/integration-tests/steps/change_sets.py +++ b/integration-tests/steps/change_sets.py @@ -8,17 +8,19 @@ from helpers import retry_boto_call -@given( - 'stack "{stack_name}" has change set "{change_set_name}" using "{filename}"' -) +@given('stack "{stack_name}" has change set "{change_set_name}" using "{filename}"') def step_impl(context, stack_name, change_set_name, filename): full_name = get_cloudformation_stack_name(context, stack_name) retry_boto_call( context.client.create_change_set, StackName=full_name, - Capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], + Capabilities=[ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], ChangeSetName=change_set_name, - TemplateBody=read_template_file(context, filename) + TemplateBody=read_template_file(context, filename), ) wait_for_final_state(context, stack_name, change_set_name) @@ -29,38 +31,35 @@ def step_impl(context, stack_name, change_set_name): retry_boto_call( context.client.delete_change_set, ChangeSetName=change_set_name, - StackName=full_name + StackName=full_name, ) @given('stack "{stack_name}" has no change sets') def step_impl(context, stack_name): full_name = get_cloudformation_stack_name(context, stack_name) - response = retry_boto_call( - context.client.list_change_sets, StackName=full_name - ) + response = retry_boto_call(context.client.list_change_sets, StackName=full_name) for change_set in response["Summaries"]: time.sleep(1) retry_boto_call( context.client.delete_change_set, - ChangeSetName=change_set['ChangeSetName'], - StackName=full_name + ChangeSetName=change_set["ChangeSetName"], + StackName=full_name, ) @when('the user creates change set "{change_set_name}" for stack "{stack_name}"') def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: sceptre_plan.create_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: @@ -69,20 +68,22 @@ def step_impl(context, change_set_name, stack_name): wait_for_final_state(context, stack_name, change_set_name) -@when('the user creates change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies') +@when( + 'the user creates change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies' +) def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: sceptre_plan.create_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: @@ -94,40 +95,41 @@ def step_impl(context, change_set_name, stack_name): @when('the user deletes change set "{change_set_name}" for stack "{stack_name}"') def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} sceptre_plan.delete_change_set(change_set_name) try: sceptre_plan.delete_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: raise e -@when('the user deletes change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies') +@when( + 'the user deletes change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies' +) def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} sceptre_plan.delete_change_set(change_set_name) try: sceptre_plan.delete_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: @@ -137,17 +139,16 @@ def step_impl(context, change_set_name, stack_name): @when('the user lists change sets for stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: context.output = sceptre_plan.list_change_sets().values() except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return raise e @@ -156,18 +157,18 @@ def step_impl(context, stack_name): @when('the user lists change sets for stack "{stack_name}" with ignore dependencies') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: context.output = sceptre_plan.list_change_sets().values() except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return raise e @@ -176,38 +177,39 @@ def step_impl(context, stack_name): @when('the user executes change set "{change_set_name}" for stack "{stack_name}"') def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: sceptre_plan.execute_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: raise e -@when('the user executes change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies') +@when( + 'the user executes change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies' +) def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: sceptre_plan.execute_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: @@ -217,17 +219,16 @@ def step_impl(context, change_set_name, stack_name): @when('the user describes change set "{change_set_name}" for stack "{stack_name}"') def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: responses = sceptre_plan.describe_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: @@ -235,21 +236,23 @@ def step_impl(context, change_set_name, stack_name): context.output = responses -@when('the user describes change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies') +@when( + 'the user describes change set "{change_set_name}" for stack "{stack_name}" with ignore dependencies' +) def step_impl(context, change_set_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) - allowed_errors = {'ValidationError', 'ChangeSetNotFound'} + allowed_errors = {"ValidationError", "ChangeSetNotFound"} try: responses = sceptre_plan.describe_change_set(change_set_name) except ClientError as e: - if e.response['Error']['Code'] in allowed_errors: + if e.response["Error"]["Code"] in allowed_errors: context.error = e return else: @@ -277,10 +280,7 @@ def step_impl(context, stack_name, change_set_name): @then('the change sets for stack "{stack_name}" are listed') def step_impl(context, stack_name): full_name = get_cloudformation_stack_name(context, stack_name) - response = retry_boto_call( - context.client.list_change_sets, - StackName=full_name - ) + response = retry_boto_call(context.client.list_change_sets, StackName=full_name) for output in context.output: assert output == {stack_name: response.get("Summaries", {})} @@ -297,7 +297,7 @@ def step_impl(context, change_set_name, stack_name): response = retry_boto_call( context.client.describe_change_set, StackName=full_name, - ChangeSetName=change_set_name + ChangeSetName=change_set_name, ) del response["ResponseMetadata"] @@ -311,10 +311,7 @@ def step_impl(context, change_set_name, stack_name): @then('stack "{stack_name}" was updated with change set "{change_set_name}"') def step_impl(context, stack_name, change_set_name): full_name = get_cloudformation_stack_name(context, stack_name) - response = retry_boto_call( - context.client.describe_stacks, - StackName=full_name - ) + response = retry_boto_call(context.client.describe_stacks, StackName=full_name) change_set_id = response["Stacks"][0]["ChangeSetId"] stack_status = response["Stacks"][0]["StackStatus"] @@ -328,10 +325,10 @@ def get_change_set_status(context, stack_name, change_set_name): response = retry_boto_call( context.client.describe_change_set, ChangeSetName=change_set_name, - StackName=stack_name + StackName=stack_name, ) except ClientError as e: - if e.response['Error']['Code'] == 'ChangeSetNotFound': + if e.response["Error"]["Code"] == "ChangeSetNotFound": return None else: raise e diff --git a/integration-tests/steps/drift.py b/integration-tests/steps/drift.py index 52dbc37a9..09d411079 100644 --- a/integration-tests/steps/drift.py +++ b/integration-tests/steps/drift.py @@ -13,9 +13,7 @@ def step_impl(context, stack_name): topic_arn = _get_output("TopicName", full_name) client = boto3.client("sns") client.set_topic_attributes( - TopicArn=topic_arn, - AttributeName="DisplayName", - AttributeValue="WrongName" + TopicArn=topic_arn, AttributeName="DisplayName", AttributeValue="WrongName" ) @@ -30,8 +28,7 @@ def _get_output(output_name, stack_name): @when('the user detects drift on stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) values = sceptre_plan.drift_detect().values() @@ -41,8 +38,7 @@ def step_impl(context, stack_name): @when('the user shows drift on stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) values = sceptre_plan.drift_show().values() @@ -52,8 +48,7 @@ def step_impl(context, stack_name): @when('the user detects drift on stack_group "{stack_group_name}"') def step_impl(context, stack_group_name): sceptre_context = SceptreContext( - command_path=stack_group_name, - project_path=context.sceptre_dir + command_path=stack_group_name, project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) values = sceptre_plan.drift_detect().values() @@ -67,7 +62,10 @@ def step_impl(context, desired_status): @then('stack resource drift status is "{desired_status}"') def step_impl(context, desired_status): - assert context.output[0][1]["StackResourceDrifts"][0]["StackResourceDriftStatus"] == desired_status + assert ( + context.output[0][1]["StackResourceDrifts"][0]["StackResourceDriftStatus"] + == desired_status + ) @then('stack_group drift statuses are each one of "{statuses}"') diff --git a/integration-tests/steps/helpers.py b/integration-tests/steps/helpers.py index 1995b8cf2..587aa1267 100644 --- a/integration-tests/steps/helpers.py +++ b/integration-tests/steps/helpers.py @@ -13,24 +13,24 @@ @then('the user is told "{message}"') def step_impl(context, message): if message == "stack does not exist": - msg = context.error.response['Error']['Message'] + msg = context.error.response["Error"]["Message"] assert msg.endswith("does not exist") elif message == "change set does not exist": - msg = context.error.response['Error']['Message'] + msg = context.error.response["Error"]["Message"] assert msg.endswith("does not exist") elif message == "the template is valid": for stack, status in context.response.items(): assert status["ResponseMetadata"]["HTTPStatusCode"] == 200 elif message == "the template is malformed": - msg = context.error.response['Error']['Message'] + msg = context.error.response["Error"]["Message"] assert msg.startswith("Template format error") else: raise Exception("Step has incorrect message") -@then('no exception is raised') +@then("no exception is raised") def step_impl(context): - assert (context.error is None) + assert context.error is None @then('a "{exception_type}" is raised') @@ -53,11 +53,9 @@ def step_impl(context, exception_type): @given('stack_group "{stack_group}" has AWS config "{config}" set') def step_impl(context, stack_group, config): - config_path = os.path.join( - context.sceptre_dir, "config", stack_group, config - ) + config_path = os.path.join(context.sceptre_dir, "config", stack_group, config) - os.environ['AWS_CONFIG_FILE'] = config_path + os.environ["AWS_CONFIG_FILE"] = config_path def read_template_file(context, template_name): @@ -67,9 +65,7 @@ def read_template_file(context, template_name): def get_cloudformation_stack_name(context, stack_name): - return "-".join( - [context.project_code, stack_name.replace("/", "-")] - ) + return "-".join([context.project_code, stack_name.replace("/", "-")]) def retry_boto_call(func, *args, **kwargs): @@ -82,7 +78,7 @@ def retry_boto_call(func, *args, **kwargs): response = func(*args, **kwargs) return response except ClientError as e: - if e.response['Error']['Code'] == 'Throttling': + if e.response["Error"]["Code"] == "Throttling": time.sleep(delay) else: raise e diff --git a/integration-tests/steps/project_dependencies.py b/integration-tests/steps/project_dependencies.py index 0d826208b..f96cdca96 100644 --- a/integration-tests/steps/project_dependencies.py +++ b/integration-tests/steps/project_dependencies.py @@ -17,13 +17,11 @@ def step_impl(context: Context, stack_name): delete it; Otherwise, it will fail since you can't delete a bucket with objects in it. """ context.add_cleanup( - cleanup_template_files_in_bucket, - context.sceptre_dir, - stack_name + cleanup_template_files_in_bucket, context.sceptre_dir, stack_name ) -@given('placeholders are allowed') +@given("placeholders are allowed") def step_impl(context: Context): placeholder_context = use_resolver_placeholders_on_error() placeholder_context.__enter__() @@ -33,8 +31,7 @@ def step_impl(context: Context): @when('the user validates stack_group "{group}"') def step_impl(context: Context, group): sceptre_context = SceptreContext( - command_path=group, - project_path=context.sceptre_dir + command_path=group, project_path=context.sceptre_dir ) plan = SceptrePlan(sceptre_context) result = plan.validate() @@ -44,24 +41,22 @@ def step_impl(context: Context, group): @then('the template for stack "{stack_name}" has been uploaded') def step_impl(context: Context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) plan = SceptrePlan(sceptre_context) buckets = get_template_buckets(plan) assert len(buckets) > 0 - filtered_objects = list(chain.from_iterable( - bucket.objects.filter( - Prefix=stack_name + filtered_objects = list( + chain.from_iterable( + bucket.objects.filter(Prefix=stack_name) for bucket in buckets ) - for bucket in buckets - )) + ) assert len(filtered_objects) == len(plan.command_stacks) for stack in plan.command_stacks: for obj in filtered_objects: if obj.key.startswith(stack.name): - s3_template = obj.get()['Body'].read().decode('utf-8') + s3_template = obj.get()["Body"].read().decode("utf-8") expected = stack.template.body assert s3_template == expected break @@ -69,12 +64,14 @@ def step_impl(context: Context, stack_name): assert False, "Could not found uploaded template" -@then('the stack "{resource_stack_name}" has a notification defined by stack "{topic_stack_name}"') +@then( + 'the stack "{resource_stack_name}" has a notification defined by stack "{topic_stack_name}"' +) def step_impl(context, resource_stack_name, topic_stack_name): topic_stack_resources = get_stack_resources(context, topic_stack_name) - topic = topic_stack_resources[0]['PhysicalResourceId'] + topic = topic_stack_resources[0]["PhysicalResourceId"] resource_stack = describe_stack(context, resource_stack_name) - notification_arns = resource_stack['NotificationARNs'] + notification_arns = resource_stack["NotificationARNs"] assert topic in notification_arns @@ -93,8 +90,7 @@ def step_impl(context, key, stack_name): def cleanup_template_files_in_bucket(sceptre_dir, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=sceptre_dir + command_path=stack_name + ".yaml", project_path=sceptre_dir ) plan = SceptrePlan(sceptre_context) buckets = get_template_buckets(plan) @@ -103,7 +99,7 @@ def cleanup_template_files_in_bucket(sceptre_dir, stack_name): def get_template_buckets(plan: SceptrePlan): - s3_resource = boto3.resource('s3') + s3_resource = boto3.resource("s3") return [ s3_resource.Bucket(stack.template_bucket_name) for stack in plan.command_stacks @@ -114,28 +110,21 @@ def get_template_buckets(plan: SceptrePlan): def get_stack_resources(context, stack_name): cf_stack_name = get_cloudformation_stack_name(context, stack_name) resources = retry_boto_call( - context.client.describe_stack_resources, - StackName=cf_stack_name + context.client.describe_stack_resources, StackName=cf_stack_name ) - return resources['StackResources'] + return resources["StackResources"] def get_stack_tags(context, stack_name) -> Dict[str, str]: description = describe_stack(context, stack_name) - tags = { - tag['Key']: tag['Value'] - for tag in description['Tags'] - } + tags = {tag["Key"]: tag["Value"] for tag in description["Tags"]} return tags def describe_stack(context, stack_name) -> dict: cf_stack_name = get_cloudformation_stack_name(context, stack_name) - response = retry_boto_call( - context.client.describe_stacks, - StackName=cf_stack_name - ) - return response['Stacks'][0] + response = retry_boto_call(context.client.describe_stacks, StackName=cf_stack_name) + return response["Stacks"][0] def exit_placeholder_context(placeholder_context: ContextManager): diff --git a/integration-tests/steps/stack_groups.py b/integration-tests/steps/stack_groups.py index 5b31d391f..b6ef69b4d 100644 --- a/integration-tests/steps/stack_groups.py +++ b/integration-tests/steps/stack_groups.py @@ -62,11 +62,13 @@ def step_impl(context, stack_group_name): launch_stack_group(context, stack_group_name, False, True) -def launch_stack_group(context, stack_group_name, prune=False, ignore_dependencies=False): +def launch_stack_group( + context, stack_group_name, prune=False, ignore_dependencies=False +): sceptre_context = SceptreContext( command_path=stack_group_name, project_path=context.sceptre_dir, - ignore_dependencies=ignore_dependencies + ignore_dependencies=ignore_dependencies, ) launcher = Launcher(sceptre_context) @@ -76,8 +78,7 @@ def launch_stack_group(context, stack_group_name, prune=False, ignore_dependenci @when('the user deletes stack_group "{stack_group_name}"') def step_impl(context, stack_group_name): sceptre_context = SceptreContext( - command_path=stack_group_name, - project_path=context.sceptre_dir + command_path=stack_group_name, project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) @@ -89,7 +90,7 @@ def step_impl(context, stack_group_name): sceptre_context = SceptreContext( command_path=stack_group_name, project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) @@ -99,8 +100,7 @@ def step_impl(context, stack_group_name): @when('the user describes stack_group "{stack_group_name}"') def step_impl(context, stack_group_name): sceptre_context = SceptreContext( - command_path=stack_group_name, - project_path=context.sceptre_dir + command_path=stack_group_name, project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) @@ -112,8 +112,8 @@ def step_impl(context, stack_group_name): for response in responses.values(): if response is None: continue - for stack in response['Stacks']: - cfn_stacks[stack['StackName']] = stack['StackStatus'] + for stack in response["Stacks"]: + cfn_stacks[stack["StackName"]] = stack["StackStatus"] context.response = [ {short_name: cfn_stacks[full_name]} @@ -127,7 +127,7 @@ def step_impl(context, stack_group_name): sceptre_context = SceptreContext( command_path=stack_group_name, project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) @@ -139,8 +139,8 @@ def step_impl(context, stack_group_name): for response in responses.values(): if response is None: continue - for stack in response['Stacks']: - cfn_stacks[stack['StackName']] = stack['StackStatus'] + for stack in response["Stacks"]: + cfn_stacks[stack["StackName"]] = stack["StackStatus"] context.response = [ {short_name: cfn_stacks[full_name]} @@ -152,20 +152,21 @@ def step_impl(context, stack_group_name): @when('the user describes resources in stack_group "{stack_group_name}"') def step_impl(context, stack_group_name): sceptre_context = SceptreContext( - command_path=stack_group_name, - project_path=context.sceptre_dir + command_path=stack_group_name, project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) context.response = sceptre_plan.describe_resources().values() -@when('the user describes resources in stack_group "{stack_group_name}" with ignore dependencies') +@when( + 'the user describes resources in stack_group "{stack_group_name}" with ignore dependencies' +) def step_impl(context, stack_group_name): sceptre_context = SceptreContext( command_path=stack_group_name, project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) @@ -179,7 +180,9 @@ def step_impl(context, stack_group_name, status): check_stack_status(context, full_stack_names, status) -@then('only the stacks in stack_group "{stack_group_name}", excluding dependencies are in "{status}"') +@then( + 'only the stacks in stack_group "{stack_group_name}", excluding dependencies are in "{status}"' +) def step_impl(context, stack_group_name, status): full_stack_names = get_full_stack_names(context, stack_group_name).values() @@ -201,7 +204,7 @@ def step_impl(context, stack_group_name, status): assert response in expected_response -@then('no resources are described') +@then("no resources are described") def step_impl(context): for stack_resources in context.response: stack_name = next(iter(stack_resources)) @@ -210,10 +213,10 @@ def step_impl(context): @then('stack "{stack_name}" is described as "{status}"') def step_impl(context, stack_name, status): - response = next(( - stack for stack in context.response - if stack_name in stack - ), {stack_name: 'PENDING'}) + response = next( + (stack for stack in context.response if stack_name in stack), + {stack_name: "PENDING"}, + ) assert response[stack_name] == status @@ -230,8 +233,7 @@ def step_impl(context, stack_group_name): for short_name, full_name in stacks_names.items(): time.sleep(1) response = retry_boto_call( - context.client.describe_stack_resources, - StackName=full_name + context.client.describe_stack_resources, StackName=full_name ) expected_resources[short_name] = response["StackResources"] @@ -253,7 +255,7 @@ def step_impl(context, stack_name): response = retry_boto_call( context.client.describe_stack_resources, - StackName=get_cloudformation_stack_name(context, stack_name) + StackName=get_cloudformation_stack_name(context, stack_name), ) expected_resources[stack_name] = response["StackResources"] @@ -268,7 +270,7 @@ def step_impl(context, stack_name): def step_impl(context, first_stack, second_stack): stacks = [ get_cloudformation_stack_name(context, first_stack), - get_cloudformation_stack_name(context, second_stack) + get_cloudformation_stack_name(context, second_stack), ] creation_times = get_stack_creation_times(context, stacks) @@ -278,18 +280,11 @@ def step_impl(context, first_stack, second_stack): @when('the user diffs stack group "{group_name}" with "{diff_type}"') def step_impl(context, group_name, diff_type): sceptre_context = SceptreContext( - command_path=group_name, - project_path=context.sceptre_dir + command_path=group_name, project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) - differ_classes = { - 'deepdiff': DeepDiffStackDiffer, - 'difflib': DifflibStackDiffer - } - writer_class = { - 'deepdiff': DeepDiffWriter, - 'difflib': DeepDiffWriter - } + differ_classes = {"deepdiff": DeepDiffStackDiffer, "difflib": DifflibStackDiffer} + writer_class = {"deepdiff": DeepDiffWriter, "difflib": DeepDiffWriter} differ = differ_classes[diff_type]() context.writer_class = writer_class[diff_type] @@ -317,17 +312,17 @@ def get_stack_creation_times(context, stacks): def get_stack_names(context, stack_group_name): - config_dir = Path(context.sceptre_dir) / 'config' + config_dir = Path(context.sceptre_dir) / "config" path = config_dir / stack_group_name stack_names = [] - for child in path.rglob('*'): - if child.is_dir() or child.stem == 'config': + for child in path.rglob("*"): + if child.is_dir() or child.stem == "config": continue relative_path = child.relative_to(config_dir) - stack_name = sceptreise_path(str(relative_path).replace(child.suffix, '')) + stack_name = sceptreise_path(str(relative_path).replace(child.suffix, "")) stack_names.append(stack_name) return stack_names @@ -348,15 +343,12 @@ def create_stacks(context, stack_names): time.sleep(1) try: retry_boto_call( - context.client.create_stack, - StackName=stack_name, - TemplateBody=body + context.client.create_stack, StackName=stack_name, TemplateBody=body ) except ClientError as e: - if ( - e.response['Error']['Code'] == 'AlreadyExistsException' - and e.response['Error']['Message'].endswith("already exists") - ): + if e.response["Error"]["Code"] == "AlreadyExistsException" and e.response[ + "Error" + ]["Message"].endswith("already exists"): pass else: raise e @@ -365,7 +357,7 @@ def create_stacks(context, stack_names): def delete_stacks(context, stack_names): - waiter = context.client.get_waiter('stack_delete_complete') + waiter = context.client.get_waiter("stack_delete_complete") waiter.config.delay = 5 waiter.config.max_attempts = 240 diff --git a/integration-tests/steps/stack_policies.py b/integration-tests/steps/stack_policies.py index 4610df167..52f4e44b7 100644 --- a/integration-tests/steps/stack_policies.py +++ b/integration-tests/steps/stack_policies.py @@ -14,15 +14,14 @@ def step_impl(context, stack_name, state): retry_boto_call( context.client.set_stack_policy, StackName=full_name, - StackPolicyBody=generate_stack_policy(state) + StackPolicyBody=generate_stack_policy(state), ) @when('the user unlocks stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) @@ -36,8 +35,7 @@ def step_impl(context, stack_name): @when('the user locks stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) @@ -52,19 +50,19 @@ def step_impl(context, stack_name, state): full_name = get_cloudformation_stack_name(context, stack_name) policy = get_stack_policy(context, full_name) - if state == 'not set': - assert (policy is None) + if state == "not set": + assert policy is None def get_stack_policy(context, stack_name): try: response = retry_boto_call( - context.client.get_stack_policy, - StackName=stack_name + context.client.get_stack_policy, StackName=stack_name ) except ClientError as e: - if e.response['Error']['Code'] == 'ValidationError' \ - and e.response['Error']['Message'].endswith("does not exist"): + if e.response["Error"]["Code"] == "ValidationError" and e.response["Error"][ + "Message" + ].endswith("does not exist"): return None else: raise e @@ -72,28 +70,28 @@ def get_stack_policy(context, stack_name): def generate_stack_policy(policy_type): - data = '' - if policy_type == 'allow all': + data = "" + if policy_type == "allow all": data = { "Statement": [ { "Effect": "Allow", "Action": "Update:*", "Principal": "*", - "Resource": "*" + "Resource": "*", } ] } - elif policy_type == 'deny all': + elif policy_type == "deny all": data = { "Statement": [ { "Effect": "Deny", "Action": "Update:*", "Principal": "*", - "Resource": "*" + "Resource": "*", } ] } - return json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')) + return json.dumps(data, sort_keys=True, indent=4, separators=(",", ": ")) diff --git a/integration-tests/steps/stacks.py b/integration-tests/steps/stacks.py index b35579919..231659b80 100644 --- a/integration-tests/steps/stacks.py +++ b/integration-tests/steps/stacks.py @@ -16,22 +16,24 @@ from sceptre.cli.launch import Launcher from sceptre.cli.prune import Pruner from sceptre.diffing.diff_writer import DeepDiffWriter, DiffWriter -from sceptre.diffing.stack_differ import DeepDiffStackDiffer, DifflibStackDiffer, StackDiff +from sceptre.diffing.stack_differ import ( + DeepDiffStackDiffer, + DifflibStackDiffer, + StackDiff, +) from sceptre.plan.plan import SceptrePlan from sceptre.context import SceptreContext def set_stack_timeout(context, stack_name, stack_timeout): - config_path = os.path.join( - context.sceptre_dir, "config", stack_name + ".yaml" - ) + config_path = os.path.join(context.sceptre_dir, "config", stack_name + ".yaml") with open(config_path) as config_file: stack_config = yaml.safe_load(config_file) stack_config["stack_timeout"] = int(stack_timeout) - with open(config_path, 'w') as config_file: + with open(config_path, "w") as config_file: yaml.safe_dump(stack_config, config_file, default_flow_style=False) @@ -42,7 +44,7 @@ def step_impl(context, stack_name): if status is not None: delete_stack(context, full_name) status = get_stack_status(context, full_name) - assert (status is None) + assert status is None @given('stack "{stack_name}" does not exist in "{region_name}"') @@ -53,7 +55,7 @@ def step_impl(context, stack_name, region_name): if status is not None: delete_stack(context, full_name) status = get_stack_status(context, full_name) - assert (status is None) + assert status is None @given('stack "{stack_name}" exists in "{desired_status}" state') @@ -81,7 +83,7 @@ def step_impl(context, stack_name, desired_status): create_stack(context, full_name, body, **kwargs) status = get_stack_status(context, full_name) - assert (status == desired_status) + assert status == desired_status @given('stack "{stack_name}" exists using "{template_name}"') @@ -95,7 +97,7 @@ def step_impl(context, stack_name, template_name): create_stack(context, full_name, body) status = get_stack_status(context, full_name) - assert (status == "CREATE_COMPLETE") + assert status == "CREATE_COMPLETE" @given('the stack_timeout for stack "{stack_name}" is "{stack_timeout}"') @@ -106,11 +108,10 @@ def step_impl(context, stack_name, stack_timeout): @given('stack "{dependant_stack_name}" depends on stack "{stack_name}"') def step_impl(context, dependant_stack_name, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) plan = SceptrePlan(sceptre_context) - plan.resolve('create') + plan.resolve("create") if plan.launch_order: for stack in plan.launch_order: stk = stack.pop() @@ -125,41 +126,39 @@ def step_impl(context, dependant_stack_name, stack_name): @given('the stack config for stack "{stack_name}" has changed') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) - yaml_file = Path(sceptre_context.full_config_path()) / f'{stack_name}.yaml' - with yaml_file.open(mode='r') as f: + yaml_file = Path(sceptre_context.full_config_path()) / f"{stack_name}.yaml" + with yaml_file.open(mode="r") as f: loaded = yaml.load(f) original_config = deepcopy(loaded) - loaded['stack_tags'] = { - 'NewTag': 'NewValue' - } + loaded["stack_tags"] = {"NewTag": "NewValue"} dump_stack_config(yaml_file, loaded) context.add_cleanup(dump_stack_config, yaml_file, original_config) def dump_stack_config(config_path: Path, config_dict: dict): - with config_path.open(mode='w') as f: + with config_path.open(mode="w") as f: yaml.safe_dump(config_dict, f) @when('the user creates stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) try: sceptre_plan.create() except ClientError as e: - if e.response['Error']['Code'] == 'AlreadyExistsException' \ - and e.response['Error']['Message'].endswith("already exists"): + if e.response["Error"]["Code"] == "AlreadyExistsException" and e.response[ + "Error" + ]["Message"].endswith("already exists"): return else: raise e @@ -168,17 +167,18 @@ def step_impl(context, stack_name): @when('the user creates stack "{stack_name}" with ignore dependencies') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) try: sceptre_plan.create() except ClientError as e: - if e.response['Error']['Code'] == 'AlreadyExistsException' \ - and e.response['Error']['Message'].endswith("already exists"): + if e.response["Error"]["Code"] == "AlreadyExistsException" and e.response[ + "Error" + ]["Message"].endswith("already exists"): return else: raise e @@ -187,17 +187,17 @@ def step_impl(context, stack_name): @when('the user updates stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) try: sceptre_plan.update() except ClientError as e: - message = e.response['Error']['Message'] - if e.response['Error']['Code'] == 'ValidationError' \ - and message.endswith("does not exist"): + message = e.response["Error"]["Message"] + if e.response["Error"]["Code"] == "ValidationError" and message.endswith( + "does not exist" + ): return else: raise e @@ -206,18 +206,19 @@ def step_impl(context, stack_name): @when('the user updates stack "{stack_name}" with ignore dependencies') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) try: sceptre_plan.update() except ClientError as e: - message = e.response['Error']['Message'] - if e.response['Error']['Code'] == 'ValidationError' \ - and message.endswith("does not exist"): + message = e.response["Error"]["Message"] + if e.response["Error"]["Code"] == "ValidationError" and message.endswith( + "does not exist" + ): return else: raise e @@ -226,19 +227,20 @@ def step_impl(context, stack_name): @when('the user deletes stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - full_scan=True + full_scan=True, ) sceptre_plan = SceptrePlan(sceptre_context) - sceptre_plan.resolve(command='delete', reverse=True) + sceptre_plan.resolve(command="delete", reverse=True) try: sceptre_plan.delete() except ClientError as e: - if e.response['Error']['Code'] == 'ValidationError' \ - and e.response['Error']['Message'].endswith("does not exist"): + if e.response["Error"]["Code"] == "ValidationError" and e.response["Error"][ + "Message" + ].endswith("does not exist"): return else: raise e @@ -247,20 +249,21 @@ def step_impl(context, stack_name): @when('the user deletes stack "{stack_name}" with ignore dependencies') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, ignore_dependencies=True, - full_scan=True + full_scan=True, ) sceptre_plan = SceptrePlan(sceptre_context) - sceptre_plan.resolve(command='delete', reverse=True) + sceptre_plan.resolve(command="delete", reverse=True) try: sceptre_plan.delete() except ClientError as e: - if e.response['Error']['Code'] == 'ValidationError' \ - and e.response['Error']['Message'].endswith("does not exist"): + if e.response["Error"]["Code"] == "ValidationError" and e.response["Error"][ + "Message" + ].endswith("does not exist"): return else: raise e @@ -289,9 +292,9 @@ def step_impl(context, path): def launch_stack(context, stack_name, prune=False, ignore_dependencies=False): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=ignore_dependencies + ignore_dependencies=ignore_dependencies, ) launcher = Launcher(sceptre_context) @@ -310,20 +313,21 @@ def step_impl(context, stack_name): @when('the user describes the resources of stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) context.output = list(sceptre_plan.describe_resources().values()) -@when('the user describes the resources of stack "{stack_name}" with ignore dependencies') +@when( + 'the user describes the resources of stack "{stack_name}" with ignore dependencies' +) def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) @@ -333,48 +337,37 @@ def step_impl(context, stack_name): @when('the user diffs stack "{stack_name}" with "{diff_type}"') def step_impl(context, stack_name, diff_type): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) - differ_classes = { - 'deepdiff': DeepDiffStackDiffer, - 'difflib': DifflibStackDiffer - } - writer_class = { - 'deepdiff': DeepDiffWriter, - 'difflib': DeepDiffWriter - } + differ_classes = {"deepdiff": DeepDiffStackDiffer, "difflib": DifflibStackDiffer} + writer_class = {"deepdiff": DeepDiffWriter, "difflib": DeepDiffWriter} differ = differ_classes[diff_type]() context.writer_class = writer_class[diff_type] context.output = list(sceptre_plan.diff(differ).values()) -@then( - 'stack "{stack_name}" in "{region_name}" ' - 'exists in "{desired_status}" state' -) +@then('stack "{stack_name}" in "{region_name}" ' 'exists in "{desired_status}" state') def step_impl(context, stack_name, region_name, desired_status): with region(region_name): full_name = get_cloudformation_stack_name(context, stack_name) status = get_stack_status(context, full_name, region_name) - assert (status == desired_status) + assert status == desired_status @then('stack "{stack_name}" exists in "{desired_status}" state') def step_impl(context, stack_name, desired_status): full_name = get_cloudformation_stack_name(context, stack_name) sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) status = sceptre_plan.get_status() status = get_stack_status(context, full_name) - assert (status == desired_status) + assert status == desired_status @then('stack "{stack_name}" has "{tag_name}" tag with "{desired_tag_value}" value') @@ -382,24 +375,23 @@ def step_impl(context, stack_name, tag_name, desired_tag_value): full_name = get_cloudformation_stack_name(context, stack_name) tags = get_stack_tags(context, full_name) - tag = next((tag for tag in tags if tag['Key'] == tag_name), {'Value': None}) + tag = next((tag for tag in tags if tag["Key"] == tag_name), {"Value": None}) - assert (tag['Value'] == desired_tag_value) + assert tag["Value"] == desired_tag_value @then('stack "{stack_name}" does not exist') def step_impl(context, stack_name): full_name = get_cloudformation_stack_name(context, stack_name) status = get_stack_status(context, full_name) - assert (status is None) + assert status is None @then('the resources of stack "{stack_name}" are described') def step_impl(context, stack_name): full_name = get_cloudformation_stack_name(context, stack_name) response = retry_boto_call( - context.client.describe_stack_resources, - StackName=full_name + context.client.describe_stack_resources, StackName=full_name ) properties = {"LogicalResourceId", "PhysicalResourceId"} formatted_response = [ @@ -410,22 +402,23 @@ def step_impl(context, stack_name): assert [{stack_name: formatted_response}] == context.output -@then('stack "{stack_name}" does not exist and stack "{dependant_stack_name}" exists in "{desired_state}"') +@then( + 'stack "{stack_name}" does not exist and stack "{dependant_stack_name}" exists in "{desired_state}"' +) def step_impl(context, stack_name, dependant_stack_name, desired_state): full_name = get_cloudformation_stack_name(context, stack_name) status = get_stack_status(context, full_name) - assert (status is None) + assert status is None dep_full_name = get_cloudformation_stack_name(context, dependant_stack_name) sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) dep_status = sceptre_plan.get_status() dep_status = get_stack_status(context, dep_full_name) - assert (dep_status == desired_state) + assert dep_status == desired_state @then('a diff is returned with "{attribute}" = "{value}"') @@ -438,25 +431,25 @@ def step_impl(context, attribute, value): @then('a diff is returned with {a_or_no} "{kind}" difference') def step_impl(context, a_or_no, kind): - if a_or_no == 'a': + if a_or_no == "a": test_value = True - elif a_or_no == 'no': + elif a_or_no == "no": test_value = False else: raise ValueError('Only "a" or "no" accepted in this condition') writer_class: Type[DiffWriter] = context.writer_class - difference_property = f'has_{kind}_difference' + difference_property = f"has_{kind}_difference" for diff in context.output: diff: StackDiff - writer = writer_class(diff, StringIO(), 'yaml') + writer = writer_class(diff, StringIO(), "yaml") assert getattr(writer, difference_property) is test_value def get_stack_tags(context, stack_name, region_name=None): if region_name is not None: - stack = boto3.resource('cloudformation', region_name=region_name).Stack + stack = boto3.resource("cloudformation", region_name=region_name).Stack else: stack = context.cloudformation.Stack @@ -465,8 +458,9 @@ def get_stack_tags(context, stack_name, region_name=None): retry_boto_call(stack.load) return stack.tags except ClientError as e: - if e.response['Error']['Code'] == 'ValidationError' \ - and e.response['Error']['Message'].endswith("does not exist"): + if e.response["Error"]["Code"] == "ValidationError" and e.response["Error"][ + "Message" + ].endswith("does not exist"): return None else: raise e @@ -474,7 +468,7 @@ def get_stack_tags(context, stack_name, region_name=None): def get_stack_status(context, stack_name, region_name=None): if region_name is not None: - Stack = boto3.resource('cloudformation', region_name=region_name).Stack + Stack = boto3.resource("cloudformation", region_name=region_name).Stack else: Stack = context.cloudformation.Stack @@ -483,8 +477,9 @@ def get_stack_status(context, stack_name, region_name=None): retry_boto_call(stack.load) return stack.stack_status except ClientError as e: - if e.response['Error']['Code'] == 'ValidationError' \ - and e.response['Error']['Message'].endswith("does not exist"): + if e.response["Error"]["Code"] == "ValidationError" and e.response["Error"][ + "Message" + ].endswith("does not exist"): return None else: raise e @@ -494,12 +489,13 @@ def create_stack(context, stack_name, body, **kwargs): retry_boto_call( context.client.create_stack, StackName=stack_name, - TemplateBody=body, **kwargs, + TemplateBody=body, + **kwargs, Capabilities=[ - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND' - ] + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], ) wait_for_final_state(context, stack_name) @@ -516,7 +512,7 @@ def delete_stack(context, stack_name): stack = retry_boto_call(context.cloudformation.Stack, stack_name) retry_boto_call(stack.delete) - waiter = context.client.get_waiter('stack_delete_complete') + waiter = context.client.get_waiter("stack_delete_complete") waiter.config.delay = 5 waiter.config.max_attempts = 240 waiter.wait(StackName=stack_name) diff --git a/integration-tests/steps/templates.py b/integration-tests/steps/templates.py index a2cdc0ca5..98ef3473d 100644 --- a/integration-tests/steps/templates.py +++ b/integration-tests/steps/templates.py @@ -12,18 +12,15 @@ def set_template_path(context, stack_name, template_name): sceptre_context = SceptreContext( - command_path=stack_name + ".yaml", - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) config_path = sceptre_context.full_config_path() template_path = os.path.join( - sceptre_context.project_path, - sceptre_context.templates_path, - template_name + sceptre_context.project_path, sceptre_context.templates_path, template_name ) - with open(os.path.join(config_path, stack_name + '.yaml')) as config_file: + with open(os.path.join(config_path, stack_name + ".yaml")) as config_file: stack_config = yaml.safe_load(config_file) if "template_path" in stack_config: @@ -31,15 +28,15 @@ def set_template_path(context, stack_name, template_name): if "template" in stack_config: stack_config["template"]["type"] = stack_config["template"].get("type", "file") template_handler_type = stack_config["template"]["type"] - if template_handler_type.lower() == 's3': - segments = stack_config["template"]["path"].split('/') + if template_handler_type.lower() == "s3": + segments = stack_config["template"]["path"].split("/") bucket = context.TEST_ARTIFACT_BUCKET_NAME key = "/".join(segments[1:]) - stack_config["template"]["path"] = f'{bucket}/{key}' + stack_config["template"]["path"] = f"{bucket}/{key}" else: stack_config["template"]["path"] = template_path - with open(os.path.join(config_path, stack_name + '.yaml'), 'w') as config_file: + with open(os.path.join(config_path, stack_name + ".yaml"), "w") as config_file: yaml.safe_dump(stack_config, config_file, default_flow_style=False) @@ -51,8 +48,7 @@ def step_impl(context, stack_name, template_name): @when('the user validates the template for stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) sceptre_plan = SceptrePlan(sceptre_context) @@ -63,12 +59,14 @@ def step_impl(context, stack_name): context.error = e -@when('the user validates the template for stack "{stack_name}" with ignore dependencies') +@when( + 'the user validates the template for stack "{stack_name}" with ignore dependencies' +) def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) @@ -82,24 +80,23 @@ def step_impl(context, stack_name): @when('the user generates the template for stack "{stack_name}"') def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', - project_path=context.sceptre_dir + command_path=stack_name + ".yaml", project_path=context.sceptre_dir ) config_path = sceptre_context.full_config_path() template_path = sceptre_context.full_templates_path() - with open(os.path.join(config_path, stack_name + '.yaml')) as config_file: + with open(os.path.join(config_path, stack_name + ".yaml")) as config_file: stack_config = yaml.safe_load(config_file) if "template" in stack_config and stack_config["template"]["type"].lower() == "s3": - segments = stack_config["template"]["path"].split('/') + segments = stack_config["template"]["path"].split("/") bucket = segments[0] key = "/".join(segments[1:]) - source_file = f'{template_path}/{segments[-1]}' - boto3.client('s3').upload_file(source_file, bucket, key) + source_file = f"{template_path}/{segments[-1]}" + boto3.client("s3").upload_file(source_file, bucket, key) else: config_path = sceptre_context.full_config_path() - with open(os.path.join(config_path, stack_name + '.yaml')) as config_file: + with open(os.path.join(config_path, stack_name + ".yaml")) as config_file: stack_config = yaml.safe_load(config_file) sceptre_plan = SceptrePlan(sceptre_context) @@ -109,12 +106,14 @@ def step_impl(context, stack_name): context.error = e -@when('the user generates the template for stack "{stack_name}" with ignore dependencies') +@when( + 'the user generates the template for stack "{stack_name}" with ignore dependencies' +) def step_impl(context, stack_name): sceptre_context = SceptreContext( - command_path=stack_name + '.yaml', + command_path=stack_name + ".yaml", project_path=context.sceptre_dir, - ignore_dependencies=True + ignore_dependencies=True, ) sceptre_plan = SceptrePlan(sceptre_context) try: @@ -125,23 +124,23 @@ def step_impl(context, stack_name): @then('the output is the same as the contents of "{filename}" template') def step_impl(context, filename): - filepath = os.path.join( - context.sceptre_dir, "templates", filename - ) + filepath = os.path.join(context.sceptre_dir, "templates", filename) with open(filepath) as template: body = template.read() for template in context.output.values(): - assert yaml.load(body, Loader=CfnYamlLoader) == yaml.load(template, CfnYamlLoader) + assert yaml.load(body, Loader=CfnYamlLoader) == yaml.load( + template, CfnYamlLoader + ) @then('the output is the same as the contents returned by "{filename}"') def step_impl(context, filename): - filepath = os.path.join( - context.sceptre_dir, "templates", filename - ) + filepath = os.path.join(context.sceptre_dir, "templates", filename) module = SourceFileLoader("template", filepath).load_module() body = module.sceptre_handler({}) for template in context.output.values(): - assert yaml.load(body, Loader=CfnYamlLoader) == yaml.load(template, CfnYamlLoader) + assert yaml.load(body, Loader=CfnYamlLoader) == yaml.load( + template, CfnYamlLoader + ) diff --git a/sceptre/__init__.py b/sceptre/__init__.py index e1ecd715e..a292dfc95 100644 --- a/sceptre/__init__.py +++ b/sceptre/__init__.py @@ -4,9 +4,9 @@ import warnings -__author__ = 'Cloudreach' -__email__ = 'sceptre@cloudreach.com' -__version__ = '3.2.0' +__author__ = "Cloudreach" +__email__ = "sceptre@cloudreach.com" +__version__ = "3.2.0" # Set up logging to ``/dev/null`` like a library is supposed to. @@ -19,4 +19,4 @@ def emit(self, record): with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) -logging.getLogger('sceptre').addHandler(NullHandler()) +logging.getLogger("sceptre").addHandler(NullHandler()) diff --git a/sceptre/cli/__init__.py b/sceptre/cli/__init__.py index 00da04d0d..affc418cf 100644 --- a/sceptre/cli/__init__.py +++ b/sceptre/cli/__init__.py @@ -30,10 +30,12 @@ from sceptre.cli.status import status_command from sceptre.cli.helpers import catch_exceptions, setup_vars -from sceptre.cli.template import (validate_command, - generate_command, - estimate_cost_command, - fetch_remote_template_command) +from sceptre.cli.template import ( + validate_command, + generate_command, + estimate_cost_command, + fetch_remote_template_command, +) @click.group() @@ -41,25 +43,46 @@ @click.option("--debug", is_flag=True, help="Turn on debug logging.") @click.option("--dir", "directory", help="Specify sceptre directory.") @click.option( - "--output", type=click.Choice(["text", "yaml", "json"]), default="text", - help="The formatting style for command output.") + "--output", + type=click.Choice(["text", "yaml", "json"]), + default="text", + help="The formatting style for command output.", +) @click.option("--no-colour", is_flag=True, help="Turn off output colouring.") @click.option( - "--var", multiple=True, - help="A variable to replace the value of an item in config file.") + "--var", + multiple=True, + help="A variable to replace the value of an item in config file.", +) @click.option( - "--var-file", multiple=True, type=click.File("rb"), - help="A YAML file of variables to replace the values of items in config files.") + "--var-file", + multiple=True, + type=click.File("rb"), + help="A YAML file of variables to replace the values of items in config files.", +) @click.option( - "--ignore-dependencies", is_flag=True, - help="Ignore dependencies when executing command.") + "--ignore-dependencies", + is_flag=True, + help="Ignore dependencies when executing command.", +) @click.option( - "--merge-vars", is_flag=True, default=False, - help="Merge variables from successive --vars and var files") + "--merge-vars", + is_flag=True, + default=False, + help="Merge variables from successive --vars and var files", +) @click.pass_context @catch_exceptions def cli( - ctx, debug, directory, output, no_colour, var, var_file, ignore_dependencies, merge_vars + ctx, + debug, + directory, + output, + no_colour, + var, + var_file, + ignore_dependencies, + merge_vars, ): """ Sceptre is a tool to manage your cloud native infrastructure deployments. @@ -72,7 +95,7 @@ def cli( "output_format": output, "no_colour": no_colour, "ignore_dependencies": ignore_dependencies, - "project_path": directory if directory else os.getcwd() + "project_path": directory if directory else os.getcwd(), } diff --git a/sceptre/cli/create.py b/sceptre/cli/create.py index 0b717e641..ff4f336c4 100644 --- a/sceptre/cli/create.py +++ b/sceptre/cli/create.py @@ -9,9 +9,7 @@ @click.command(name="create", short_help="Creates a stack or a change set.") @click.argument("path") @click.argument("change-set-name", required=False) -@click.option( - "-y", "--yes", is_flag=True, help="Assume yes to all questions." -) +@click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") @click.pass_context @catch_exceptions def create_command(ctx, path, change_set_name, yes): @@ -32,15 +30,14 @@ def create_command(ctx, path, change_set_name, yes): project_path=ctx.obj.get("project_path"), user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) action = "create" plan = SceptrePlan(context) if change_set_name: - confirmation(action, yes, change_set=change_set_name, - command_path=path) + confirmation(action, yes, change_set=change_set_name, command_path=path) plan.create_change_set(change_set_name) else: confirmation(action, yes, command_path=path) diff --git a/sceptre/cli/delete.py b/sceptre/cli/delete.py index 6ceaeea72..f363f028a 100644 --- a/sceptre/cli/delete.py +++ b/sceptre/cli/delete.py @@ -12,9 +12,7 @@ @click.command(name="delete", short_help="Deletes a stack or a change set.") @click.argument("path") @click.argument("change-set-name", required=False) -@click.option( - "-y", "--yes", is_flag=True, help="Assume yes to all questions." -) +@click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") @click.pass_context @catch_exceptions def delete_command(ctx, path, change_set_name, yes): @@ -40,24 +38,23 @@ def delete_command(ctx, path, change_set_name, yes): ) plan = SceptrePlan(context) - plan.resolve(command='delete', reverse=True) + plan.resolve(command="delete", reverse=True) if change_set_name: - delete_msg = "The Change Set will be delete on the following stacks, if applicable:\n" + delete_msg = ( + "The Change Set will be delete on the following stacks, if applicable:\n" + ) else: delete_msg = "The following stacks, in the following order, will be deleted:\n" - dependencies = '' + dependencies = "" for stack in plan: dependencies += "{}{}{}\n".format(Fore.YELLOW, stack.name, Style.RESET_ALL) print(delete_msg + "{}".format(dependencies)) confirmation( - plan.delete.__name__, - yes, - change_set=change_set_name, - command_path=path + plan.delete.__name__, yes, change_set=change_set_name, command_path=path ) if change_set_name: plan.delete_change_set(change_set_name) diff --git a/sceptre/cli/describe.py b/sceptre/cli/describe.py index 5b54c8fd8..9d2baa3c6 100644 --- a/sceptre/cli/describe.py +++ b/sceptre/cli/describe.py @@ -1,11 +1,7 @@ import click from sceptre.context import SceptreContext -from sceptre.cli.helpers import ( - catch_exceptions, - simplify_change_set_description, - write -) +from sceptre.cli.helpers import catch_exceptions, simplify_change_set_description, write from sceptre.plan.plan import SceptrePlan @@ -21,9 +17,7 @@ def describe_group(ctx): @describe_group.command(name="change-set") @click.argument("path") @click.argument("change-set-name") -@click.option( - "-v", "--verbose", is_flag=True, help="Display verbose output." -) +@click.option("-v", "--verbose", is_flag=True, help="Display verbose output.") @click.pass_context @catch_exceptions def describe_change_set(ctx, path, change_set_name, verbose): @@ -45,7 +39,7 @@ def describe_change_set(ctx, path, change_set_name, verbose): options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), no_colour=ctx.obj.get("no_colour"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) @@ -77,14 +71,10 @@ def describe_policy(ctx, path): options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), no_colour=ctx.obj.get("no_colour"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) responses = plan.get_policy() for response in responses.values(): - write( - response, - context.output_format, - context.no_colour - ) + write(response, context.output_format, context.no_colour) diff --git a/sceptre/cli/diff.py b/sceptre/cli/diff.py index 4a723c8c2..ee9b12435 100644 --- a/sceptre/cli/diff.py +++ b/sceptre/cli/diff.py @@ -8,8 +8,17 @@ from sceptre.cli.helpers import catch_exceptions from sceptre.context import SceptreContext -from sceptre.diffing.diff_writer import DeepDiffWriter, DiffLibWriter, ColouredDiffLibWriter, DiffWriter -from sceptre.diffing.stack_differ import DeepDiffStackDiffer, DifflibStackDiffer, StackDiff +from sceptre.diffing.diff_writer import ( + DeepDiffWriter, + DiffLibWriter, + ColouredDiffLibWriter, + DiffWriter, +) +from sceptre.diffing.stack_differ import ( + DeepDiffStackDiffer, + DifflibStackDiffer, + StackDiff, +) from sceptre.helpers import null_context from sceptre.plan.plan import SceptrePlan from sceptre.resolvers.placeholders import use_resolver_placeholders_on_error @@ -18,42 +27,45 @@ logger = getLogger(__name__) -@click.command(name="diff", short_help="Compares deployed infrastructure with current configurations") +@click.command( + name="diff", + short_help="Compares deployed infrastructure with current configurations", +) @click.option( - '-t', - '--type', - 'differ', - type=click.Choice(['deepdiff', 'difflib']), - default='deepdiff', + "-t", + "--type", + "differ", + type=click.Choice(["deepdiff", "difflib"]), + default="deepdiff", help='The type of differ to use. Use "deepdiff" for recursive key/value comparison. "difflib" ' - 'produces a more traditional "diff" result. Defaults to deepdiff.' + 'produces a more traditional "diff" result. Defaults to deepdiff.', ) @click.option( - '-s', - '--show-no-echo', + "-s", + "--show-no-echo", is_flag=True, - help='If set, will display the unmasked values of NoEcho parameters generated LOCALLY (NoEcho ' - 'parameters for deployed stacks will always be masked when retrieved from CloudFormation.). ' - 'If not set (the default), parameters identified as NoEcho on the local template will be ' - 'masked when presented in the diff.' + help="If set, will display the unmasked values of NoEcho parameters generated LOCALLY (NoEcho " + "parameters for deployed stacks will always be masked when retrieved from CloudFormation.). " + "If not set (the default), parameters identified as NoEcho on the local template will be " + "masked when presented in the diff.", ) @click.option( - '-n', - '--no-placeholders', + "-n", + "--no-placeholders", is_flag=True, - help="If set, no placeholder values will be supplied for resolvers that cannot be resolved." + help="If set, no placeholder values will be supplied for resolvers that cannot be resolved.", ) @click.option( - '-a', - '--all', - 'all_', + "-a", + "--all", + "all_", is_flag=True, help=( - "If set, will perform diffing on ALL stacks, including ignored and obsolete ones; Otherwise, " - "it will diff only stacks that would be created or updated when running the launch command." - ) + "If set, will perform diffing on ALL stacks, including ignored and obsolete ones; Otherwise, " + "it will diff only stacks that would be created or updated when running the launch command." + ), ) -@click.argument('path') +@click.argument("path") @click.pass_context @catch_exceptions def diff_command( @@ -62,7 +74,7 @@ def diff_command( show_no_echo: bool, no_placeholders: bool, all_: bool, - path: str + path: str, ): """Indicates the difference between the currently DEPLOYED stacks in the command path and the stacks configured in Sceptre right now. This command will compare both the templates as well @@ -104,7 +116,7 @@ def diff_command( options=ctx.obj.get("options"), ignore_dependencies=ctx.obj.get("ignore_dependencies"), output_format=ctx.obj.get("output_format"), - no_colour=no_colour + no_colour=no_colour, ) output_format = context.output_format plan = SceptrePlan(context) @@ -120,16 +132,18 @@ def diff_command( else: raise ValueError(f"Unexpected differ type: {differ}") - execution_context = null_context() if no_placeholders else use_resolver_placeholders_on_error() + execution_context = ( + null_context() if no_placeholders else use_resolver_placeholders_on_error() + ) with execution_context: diffs: Dict[Stack, StackDiff] = plan.diff(stack_differ) - num_stacks_with_diff = output_diffs(diffs.values(), writer_class, sys.stdout, output_format) + num_stacks_with_diff = output_diffs( + diffs.values(), writer_class, sys.stdout, output_format + ) if num_stacks_with_diff: - logger.warning( - f"{num_stacks_with_diff} stacks with differences detected." - ) + logger.warning(f"{num_stacks_with_diff} stacks with differences detected.") def output_diffs( @@ -161,7 +175,9 @@ def output_diffs( return num_stacks_with_diff -def output_buffer_with_normalized_bar_lengths(buffer: io.StringIO, output_stream: TextIO): +def output_buffer_with_normalized_bar_lengths( + buffer: io.StringIO, output_stream: TextIO +): """Takes the output from a buffer and ensures that the star and line bars are the same length across the entire buffer and that their length is the full width of longest line. @@ -171,8 +187,8 @@ def output_buffer_with_normalized_bar_lengths(buffer: io.StringIO, output_stream buffer.seek(0) max_length = len(max(buffer, key=len)) buffer.seek(0) - full_length_star_bar = '*' * max_length - full_length_line_bar = '-' * max_length + full_length_star_bar = "*" * max_length + full_length_line_bar = "-" * max_length for line in buffer: if DiffWriter.STAR_BAR in line: line = line.replace(DiffWriter.STAR_BAR, full_length_star_bar) diff --git a/sceptre/cli/drift.py b/sceptre/cli/drift.py index 282d9b816..104e56aef 100644 --- a/sceptre/cli/drift.py +++ b/sceptre/cli/drift.py @@ -4,16 +4,9 @@ from sceptre.context import SceptreContext from sceptre.plan.plan import SceptrePlan -from sceptre.cli.helpers import ( - catch_exceptions, - deserialize_json_properties, - write -) +from sceptre.cli.helpers import catch_exceptions, deserialize_json_properties, write -BAD_STATUSES = [ - "DETECTION_FAILED", - "TIMED_OUT" -] +BAD_STATUSES = ["DETECTION_FAILED", "TIMED_OUT"] @click.group(name="drift") @@ -24,7 +17,9 @@ def drift_group(): pass -@drift_group.command(name="detect", short_help="Run detect stack drift on running stacks.") +@drift_group.command( + name="detect", short_help="Run detect stack drift on running stacks." +) @click.argument("path") @click.pass_context @catch_exceptions @@ -46,7 +41,7 @@ def drift_detect(ctx: Context, path: str): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) @@ -61,7 +56,9 @@ def drift_detect(ctx: Context, path: str): exit_status += 1 for key in ["Timestamp", "ResponseMetadata"]: response.pop(key, None) - write({stack.external_name: deserialize_json_properties(response)}, output_format) + write( + {stack.external_name: deserialize_json_properties(response)}, output_format + ) exit(exit_status) @@ -91,7 +88,7 @@ def drift_show(ctx, path, drifted): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) @@ -103,6 +100,8 @@ def drift_show(ctx, path, drifted): for stack, (status, response) in responses.items(): if status in BAD_STATUSES: exit_status += 1 - write({stack.external_name: deserialize_json_properties(response)}, output_format) + write( + {stack.external_name: deserialize_json_properties(response)}, output_format + ) exit(exit_status) diff --git a/sceptre/cli/dump.py b/sceptre/cli/dump.py index 334be27de..4653bf400 100644 --- a/sceptre/cli/dump.py +++ b/sceptre/cli/dump.py @@ -2,10 +2,7 @@ import click from sceptre.context import SceptreContext -from sceptre.cli.helpers import ( - catch_exceptions, - write -) +from sceptre.cli.helpers import catch_exceptions, write from sceptre.plan.plan import SceptrePlan logger = logging.getLogger(__name__) @@ -36,7 +33,7 @@ def dump_config(ctx, path): user_variables=ctx.obj.get("user_variables"), output_format=ctx.obj.get("output_format"), options=ctx.obj.get("options"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) responses = plan.dump_config() diff --git a/sceptre/cli/execute.py b/sceptre/cli/execute.py index f36855b7f..47bd7af8e 100644 --- a/sceptre/cli/execute.py +++ b/sceptre/cli/execute.py @@ -8,9 +8,7 @@ @click.command(name="execute") @click.argument("path") @click.argument("change-set-name") -@click.option( - "-y", "--yes", is_flag=True, help="Assume yes to all questions." -) +@click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") @click.pass_context @catch_exceptions def execute_command(ctx, path, change_set_name, yes): @@ -30,7 +28,7 @@ def execute_command(ctx, path, change_set_name, yes): project_path=ctx.obj.get("project_path"), user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) @@ -38,6 +36,6 @@ def execute_command(ctx, path, change_set_name, yes): plan.execute_change_set.__name__, yes, change_set=change_set_name, - command_path=path + command_path=path, ) plan.execute_change_set(change_set_name) diff --git a/sceptre/cli/helpers.py b/sceptre/cli/helpers.py index 5f48da844..47efb87e2 100644 --- a/sceptre/cli/helpers.py +++ b/sceptre/cli/helpers.py @@ -27,6 +27,7 @@ def catch_exceptions(func): simplified. :returns: The decorated function. """ + def logging_level(): logger = logging.getLogger(__name__) return logger.getEffectiveLevel() @@ -40,8 +41,13 @@ def decorated(*args, **kwargs): """ try: return func(*args, **kwargs) - except (SceptreException, BotoCoreError, ClientError, Boto3Error, - TemplateError) as error: + except ( + SceptreException, + BotoCoreError, + ClientError, + Boto3Error, + TemplateError, + ) as error: if logging_level() == logging.DEBUG: raise write(error) @@ -50,15 +56,11 @@ def decorated(*args, **kwargs): return decorated -def confirmation( - command, ignore, command_path, change_set=None -): +def confirmation(command, ignore, command_path, change_set=None): if not ignore: msg = "Do you want to {} ".format(command) if change_set: - msg = msg + "change set '{0}' for '{1}'".format( - change_set, command_path - ) + msg = msg + "change set '{0}' for '{1}'".format(change_set, command_path) else: msg = msg + "'{0}'".format(command_path) click.confirm(msg, abort=True) @@ -113,24 +115,17 @@ def _generate_json(stream): def _generate_yaml(stream): - kwargs = { - "default_flow_style": False, - "explicit_start": True - } + kwargs = {"default_flow_style": False, "explicit_start": True} if isinstance(stream, (list, set)): items = [] for item in stream: try: if isinstance(item, dict): - items.append( - yaml.safe_dump(item, **kwargs) - ) + items.append(yaml.safe_dump(item, **kwargs)) else: items.append( - yaml.safe_dump( - yaml.load(item, Loader=CfnYamlLoader), **kwargs - ) + yaml.safe_dump(yaml.load(item, Loader=CfnYamlLoader), **kwargs) ) except Exception: print("An error occured whilst writing the YAML object.") @@ -171,9 +166,9 @@ def _generate_text(stream): col_widths = [max(len(c) for c in b) for b in zip(*items)] rows = [] for row in items: - rows.append("".join( - [field for field, width in zip(row, cycle(col_widths))] - )) + rows.append( + "".join([field for field, width in zip(row, cycle(col_widths))]) + ) return "\n".join(rows) return stream @@ -233,10 +228,12 @@ def _nested_set(dic, keys, value): if merge_vars: message += "{0}. Using values from: {1}.".format( - ", ".join(overloaded_keys), fh.name) + ", ".join(overloaded_keys), fh.name + ) else: message += "{0}. Performing deep merge, {1} wins.".format( - ", ".join(overloaded_keys), fh.name) + ", ".join(overloaded_keys), fh.name + ) logger.debug(message) @@ -263,9 +260,7 @@ def _deep_merge(source, destination): def stack_status_exit_code(statuses): - if not all( - status == StackStatus.COMPLETE - for status in statuses): + if not all(status == StackStatus.COMPLETE for status in statuses): return 1 else: return 0 @@ -306,8 +301,7 @@ def setup_logging(debug, no_colour): formatter_class = logging.Formatter if no_colour else ColouredFormatter formatter = formatter_class( - fmt="[%(asctime)s] - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S" + fmt="[%(asctime)s] - %(message)s", datefmt="%Y-%m-%d %H:%M:%S" ) log_handler = logging.StreamHandler() @@ -333,7 +327,7 @@ def simplify_change_set_description(response): "ExecutionStatus", "StackName", "Status", - "StatusReason" + "StatusReason", ] desired_resource_changes = [ "Action", @@ -341,12 +335,10 @@ def simplify_change_set_description(response): "PhysicalResourceId", "Replacement", "ResourceType", - "Scope" + "Scope", ] formatted_response = { - k: v - for k, v in response.items() - if k in desired_response_items + k: v for k, v in response.items() if k in desired_response_items } formatted_response["Changes"] = [ { @@ -363,24 +355,16 @@ def simplify_change_set_description(response): def deserialize_json_properties(value): if isinstance(value, str): - is_json = ( - (value.startswith("{") and value.endswith("}")) - or - (value.startswith("[") and value.endswith("]")) + is_json = (value.startswith("{") and value.endswith("}")) or ( + value.startswith("[") and value.endswith("]") ) if is_json: return json.loads(value) return value if isinstance(value, dict): - return { - key: deserialize_json_properties(val) - for key, val in value.items() - } + return {key: deserialize_json_properties(val) for key, val in value.items()} if isinstance(value, list): - return [ - deserialize_json_properties(item) - for item in value - ] + return [deserialize_json_properties(item) for item in value] return value @@ -424,39 +408,38 @@ def default(self, item): CFN_FNS = [ - 'And', - 'Base64', - 'Cidr', - 'Equals', - 'FindInMap', - 'GetAtt', - 'GetAZs', - 'If', - 'ImportValue', - 'Join', - 'Not', - 'Or', - 'Select', - 'Split', - 'Sub', - 'Transform', + "And", + "Base64", + "Cidr", + "Equals", + "FindInMap", + "GetAtt", + "GetAZs", + "If", + "ImportValue", + "Join", + "Not", + "Or", + "Select", + "Split", + "Sub", + "Transform", ] CFN_TAGS = [ - 'Condition', - 'Ref', + "Condition", + "Ref", ] def _getatt_constructor(loader, node): if isinstance(node.value, six.text_type): - return node.value.split('.', 1) + return node.value.split(".", 1) elif isinstance(node.value, list): seq = loader.construct_sequence(node) for item in seq: if not isinstance(item, six.text_type): - raise ValueError( - "Fn::GetAtt does not support complex datastructures") + raise ValueError("Fn::GetAtt does not support complex datastructures") return seq else: raise ValueError("Fn::GetAtt only supports string or list values") @@ -464,11 +447,13 @@ def _getatt_constructor(loader, node): def _tag_constructor(loader, tag_suffix, node): if tag_suffix not in CFN_FNS and tag_suffix not in CFN_TAGS: - raise ValueError("Bad tag: !{tag_suffix}. Supported tags are: " - "{supported_tags}".format( - tag_suffix=tag_suffix, - supported_tags=", ".join(sorted(CFN_TAGS + CFN_FNS)) - )) + raise ValueError( + "Bad tag: !{tag_suffix}. Supported tags are: " + "{supported_tags}".format( + tag_suffix=tag_suffix, + supported_tags=", ".join(sorted(CFN_TAGS + CFN_FNS)), + ) + ) if tag_suffix in CFN_FNS: tag_suffix = "Fn::{tag_suffix}".format(tag_suffix=tag_suffix) @@ -476,8 +461,8 @@ def _tag_constructor(loader, tag_suffix, node): data = {} yield data - if tag_suffix == 'Fn::GetAtt': - constructor = partial(_getatt_constructor, (loader, )) + if tag_suffix == "Fn::GetAtt": + constructor = partial(_getatt_constructor, (loader,)) elif isinstance(node, yaml.ScalarNode): constructor = loader.construct_scalar elif isinstance(node, yaml.SequenceNode): diff --git a/sceptre/cli/launch.py b/sceptre/cli/launch.py index da9c2f140..cff2d5bb7 100644 --- a/sceptre/cli/launch.py +++ b/sceptre/cli/launch.py @@ -22,7 +22,7 @@ "-p", "--prune", is_flag=True, - help="If set, will delete all stacks in the command path marked as obsolete." + help="If set, will delete all stacks in the command path marked as obsolete.", ) @click.pass_context @catch_exceptions @@ -44,7 +44,7 @@ def launch_command(ctx: Context, path: str, yes: bool, prune: bool): project_path=ctx.obj.get("project_path"), user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) launcher = Launcher(context) launcher.print_operations(prune) @@ -61,7 +61,10 @@ class Launcher: :param context: The Sceptre context to use for launching :param plan_factory: A callable with the signature of (SceptreContext) -> SceptrePlan """ - def __init__(self, context: SceptreContext, plan_factory=SceptrePlan, pruner_factory=Pruner): + + def __init__( + self, context: SceptreContext, plan_factory=SceptrePlan, pruner_factory=Pruner + ): self._context = context self._make_plan = plan_factory self._make_pruner = pruner_factory @@ -103,16 +106,24 @@ def _create_deploy_plan(self) -> SceptrePlan: return self._plan def _get_stacks_to_skip(self, deploy_plan: SceptrePlan, prune: bool) -> List[Stack]: - return [stack for stack in deploy_plan if stack.ignore or (stack.obsolete and not prune)] - - def _get_stacks_to_prune(self, deploy_plan: SceptrePlan, prune: bool) -> List[Stack]: + return [ + stack + for stack in deploy_plan + if stack.ignore or (stack.obsolete and not prune) + ] + + def _get_stacks_to_prune( + self, deploy_plan: SceptrePlan, prune: bool + ) -> List[Stack]: return [stack for stack in deploy_plan if prune and stack.obsolete] def _exclude_stacks_from_plan(self, deployment_plan: SceptrePlan, *stacks: Stack): for stack in stacks: deployment_plan.remove_stack_from_plan(stack) - def _validate_launch_for_missing_dependencies(self, deploy_plan: SceptrePlan, prune: bool): + def _validate_launch_for_missing_dependencies( + self, deploy_plan: SceptrePlan, prune: bool + ): validated_stacks = set() skipped_dependencies = set() @@ -151,7 +162,7 @@ def _print_stacks_with_message(self, stacks: List[Stack], message: str): if not len(stacks): return - message = f'* {message}\n' + message = f"* {message}\n" for stack in stacks: message += f"{Fore.YELLOW}{stack.name}{Style.RESET_ALL}\n" diff --git a/sceptre/cli/list.py b/sceptre/cli/list.py index 401c353f0..cf1ec799f 100644 --- a/sceptre/cli/list.py +++ b/sceptre/cli/list.py @@ -2,10 +2,7 @@ import click from sceptre.context import SceptreContext -from sceptre.cli.helpers import ( - catch_exceptions, - write -) +from sceptre.cli.helpers import catch_exceptions, write from sceptre.plan.plan import SceptrePlan logger = logging.getLogger(__name__) @@ -37,13 +34,12 @@ def list_resources(ctx, path): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) responses = [ - response for response - in plan.describe_resources().values() if response + response for response in plan.describe_resources().values() if response ] write(responses, context.output_format) @@ -52,8 +48,10 @@ def list_resources(ctx, path): @list_group.command(name="outputs") @click.argument("path") @click.option( - "-e", "--export", type=click.Choice(["envvar"]), - help="Specify the export formatting." + "-e", + "--export", + type=click.Choice(["envvar"]), + help="Specify the export formatting.", ) @click.pass_context @catch_exceptions @@ -73,31 +71,28 @@ def list_outputs(ctx, path, export): user_variables=ctx.obj.get("user_variables", {}), options=ctx.obj.get("options", {}), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) - responses = [ - response for response - in plan.describe_outputs().values() if response - ] + responses = [response for response in plan.describe_outputs().values() if response] if export == "envvar": for response in responses: for stack in response.values(): for output in stack: - write("export SCEPTRE_{0}='{1}'".format( - output.get("OutputKey"), - output.get("OutputValue") - ), 'text') + write( + "export SCEPTRE_{0}='{1}'".format( + output.get("OutputKey"), output.get("OutputValue") + ), + "text", + ) else: write(responses, context.output_format) @list_group.command(name="change-sets") -@click.option( - "-U", "--url", is_flag=True, help="Instead write a URL." -) +@click.option("-U", "--url", is_flag=True, help="Instead write a URL.") @click.argument("path") @click.pass_context @catch_exceptions @@ -117,14 +112,13 @@ def list_change_sets(ctx, path, url): user_variables=ctx.obj.get("user_variables"), output_format=ctx.obj.get("output_format"), options=ctx.obj.get("options"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) responses = [ - response for response - in plan.list_change_sets(url).values() if response + response for response in plan.list_change_sets(url).values() if response ] for response in responses: @@ -148,7 +142,7 @@ def list_stacks(ctx, path): user_variables=ctx.obj.get("user_variables"), output_format=ctx.obj.get("output_format"), options=ctx.obj.get("options"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) diff --git a/sceptre/cli/new.py b/sceptre/cli/new.py index 8235e4cb4..66ec30869 100644 --- a/sceptre/cli/new.py +++ b/sceptre/cli/new.py @@ -18,8 +18,10 @@ def new_group(): pass -@new_group.command("group", short_help="Creates a new Stack Group directory in a project.") -@click.argument('stack_group') +@new_group.command( + "group", short_help="Creates a new Stack Group directory in a project." +) +@click.argument("stack_group") @catch_exceptions @click.pass_context def new_stack_group(ctx, stack_group): @@ -41,7 +43,7 @@ def new_stack_group(ctx, stack_group): @new_group.command("project", short_help="Creates a new project.") @catch_exceptions -@click.argument('project_name') +@click.argument("project_name") @click.pass_context def new_project(ctx, project_name): """ @@ -61,7 +63,7 @@ def new_project(ctx, project_name): # Check if stack_group folder already exists if e.errno == errno.EEXIST: raise ProjectAlreadyExistsError( - 'Folder \"{0}\" already exists.'.format(project_name) + 'Folder "{0}" already exists.'.format(project_name) ) else: raise @@ -72,7 +74,7 @@ def new_project(ctx, project_name): defaults = { "project_code": project_name, - "region": os.environ.get("AWS_DEFAULT_REGION", "") + "region": os.environ.get("AWS_DEFAULT_REGION", ""), } config_path = os.path.join(cwd, project_name, "config") @@ -92,7 +94,7 @@ def _create_new_stack_group(config_dir, new_path): """ # Create full path to stack_group folder_path = os.path.join(config_dir, new_path) - new_config_msg = 'Do you want initialise config.yaml?' + new_config_msg = "Do you want initialise config.yaml?" # Make folders for the stack_group try: @@ -100,8 +102,7 @@ def _create_new_stack_group(config_dir, new_path): except OSError as e: # Check if stack_group folder already exists if e.errno == errno.EEXIST: - new_config_msg =\ - 'StackGroup path exists. ' + new_config_msg + new_config_msg = "StackGroup path exists. " + new_config_msg else: raise @@ -158,9 +159,7 @@ def _create_config_file(config_dir, path, defaults={}): # Ask for new values for key, value in config.items(): - config[key] = click.prompt( - 'Please enter a {0}'.format(key), default=value - ) + config[key] = click.prompt("Please enter a {0}".format(key), default=value) # Remove values if parent config are the same config = {k: v for k, v in config.items() if parent_config.get(k) != v} @@ -168,9 +167,7 @@ def _create_config_file(config_dir, path, defaults={}): # Write config.yaml if config not empty filepath = os.path.join(path, "config.yaml") if config: - with open(filepath, 'w') as config_file: - yaml.safe_dump( - config, stream=config_file, default_flow_style=False - ) + with open(filepath, "w") as config_file: + yaml.safe_dump(config, stream=config_file, default_flow_style=False) else: click.echo("No config.yaml file needed - covered by parent config.") diff --git a/sceptre/cli/policy.py b/sceptre/cli/policy.py index 8ecb27d93..754032f41 100644 --- a/sceptre/cli/policy.py +++ b/sceptre/cli/policy.py @@ -9,8 +9,10 @@ @click.argument("path") @click.argument("policy-file", required=False) @click.option( - "-b", "--built-in", type=click.Choice(["deny-all", "allow-all"]), - help="Specify a built in stack policy." + "-b", + "--built-in", + type=click.Choice(["deny-all", "allow-all"]), + help="Specify a built in stack policy.", ) @click.pass_context @catch_exceptions @@ -31,13 +33,13 @@ def set_policy_command(ctx, path, policy_file, built_in): project_path=ctx.obj.get("project_path"), user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) - if built_in == 'deny-all': + if built_in == "deny-all": plan.lock() - elif built_in == 'allow-all': + elif built_in == "allow-all": plan.unlock() else: plan.set_policy(policy_file) diff --git a/sceptre/cli/prune.py b/sceptre/cli/prune.py index e24ac38e6..ed96daff9 100644 --- a/sceptre/cli/prune.py +++ b/sceptre/cli/prune.py @@ -11,9 +11,7 @@ @click.command(name="prune", short_help="Deletes all obsolete stacks in the project") -@click.option( - "-y", "--yes", is_flag=True, help="Assume yes to all questions." -) +@click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") @click.argument("path", default=PATH_FOR_WHOLE_PROJECT) @click.pass_context @catch_exceptions @@ -50,7 +48,8 @@ class Pruner: :param context: The Sceptre context to use for pruning :param plan_factory: A callable with the signature of (SceptreContext) -> SceptrePlan """ - def __init__(self, context: SceptreContext, plan_factory=SceptrePlan): + + def __init__(self, context: SceptreContext, plan_factory=SceptrePlan): self._context = context self._make_plan = plan_factory @@ -97,9 +96,7 @@ def _create_plan(self): else: stacks = plan.command_stacks - plan.command_stacks = { - stack for stack in stacks if stack.obsolete - } + plan.command_stacks = {stack for stack in stacks if stack.obsolete} self._resolve_plan(plan) self._plan = plan return self._plan @@ -108,7 +105,9 @@ def _plan_has_obsolete_stacks(self, plan: SceptrePlan): return len(plan.command_stacks) > 0 def _print_no_obsolete_stacks(self): - click.echo("* There are no stacks marked obsolete, so there is nothing to prune.") + click.echo( + "* There are no stacks marked obsolete, so there is nothing to prune." + ) def _resolve_plan(self, plan: SceptrePlan): if len(plan.command_stacks) > 0: @@ -149,9 +148,11 @@ def check_for_non_obsolete_dependencies(stack: Stack): check_for_non_obsolete_dependencies(stack) def _print_stacks_to_be_deleted(self, plan: SceptrePlan): - delete_msg = "* The following obsolete stacks will be deleted (if they exist on AWS):\n" + delete_msg = ( + "* The following obsolete stacks will be deleted (if they exist on AWS):\n" + ) - stacks_list = '' + stacks_list = "" for stack in plan: # It's possible there could be stacks in the plan that aren't obsolete because those # stacks depend on obsolete stacks. They won't pass validation, but that's not the diff --git a/sceptre/cli/status.py b/sceptre/cli/status.py index d3ffd1e4f..755d39140 100644 --- a/sceptre/cli/status.py +++ b/sceptre/cli/status.py @@ -1,10 +1,7 @@ import click from sceptre.context import SceptreContext -from sceptre.cli.helpers import ( - catch_exceptions, - write -) +from sceptre.cli.helpers import catch_exceptions, write from sceptre.plan.plan import SceptrePlan @@ -28,11 +25,12 @@ def status_command(ctx, path): options=ctx.obj.get("options"), no_colour=ctx.obj.get("no_colour"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) responses = plan.get_status() - message = "\n".join("{}: {}".format(stack.name, status) - for stack, status in responses.items()) + message = "\n".join( + "{}: {}".format(stack.name, status) for stack, status in responses.items() + ) write(message, no_colour=context.no_colour) diff --git a/sceptre/cli/template.py b/sceptre/cli/template.py index abee83ed9..91973c790 100644 --- a/sceptre/cli/template.py +++ b/sceptre/cli/template.py @@ -3,10 +3,7 @@ import click -from sceptre.cli.helpers import ( - catch_exceptions, - write -) +from sceptre.cli.helpers import catch_exceptions, write from sceptre.context import SceptreContext from sceptre.helpers import null_context from sceptre.plan.plan import SceptrePlan @@ -17,10 +14,10 @@ @click.command(name="validate", short_help="Validates the template.") @click.option( - '-n', - '--no-placeholders', + "-n", + "--no-placeholders", is_flag=True, - help="If True, no placeholder values will be supplied for resolvers that cannot be resolved." + help="If True, no placeholder values will be supplied for resolvers that cannot be resolved.", ) @click.argument("path") @click.pass_context @@ -39,28 +36,30 @@ def validate_command(ctx, no_placeholders, path): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) - execution_context = null_context() if no_placeholders else use_resolver_placeholders_on_error() + execution_context = ( + null_context() if no_placeholders else use_resolver_placeholders_on_error() + ) with execution_context: responses = plan.validate() for stack, response in responses.items(): - if response['ResponseMetadata']['HTTPStatusCode'] == 200: - del response['ResponseMetadata'] + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + del response["ResponseMetadata"] click.echo("Template {} is valid. Template details:\n".format(stack.name)) write(response, context.output_format) @click.command(name="generate", short_help="Prints the template.") @click.option( - '-n', - '--no-placeholders', + "-n", + "--no-placeholders", is_flag=True, - help="If True, no placeholder values will be supplied for resolvers that cannot be resolved." + help="If True, no placeholder values will be supplied for resolvers that cannot be resolved.", ) @click.argument("path") @click.pass_context @@ -79,12 +78,14 @@ def generate_command(ctx, no_placeholders, path): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) - execution_context = null_context() if no_placeholders else use_resolver_placeholders_on_error() + execution_context = ( + null_context() if no_placeholders else use_resolver_placeholders_on_error() + ) with execution_context: responses = plan.generate() @@ -112,19 +113,19 @@ def estimate_cost_command(ctx, path): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) responses = plan.estimate_cost() for stack, response in responses.items(): - if response['ResponseMetadata']['HTTPStatusCode'] == 200: - del response['ResponseMetadata'] + if response["ResponseMetadata"]["HTTPStatusCode"] == 200: + del response["ResponseMetadata"] click.echo("View the estimated cost for {} at:".format(stack.name)) response = response["Url"] webbrowser.open(response, new=2) - write(response + "\n", 'text') + write(response + "\n", "text") @click.command(name="fetch-remote-template", short_help="Prints the remote template.") @@ -145,7 +146,7 @@ def fetch_remote_template_command(ctx, path): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) diff --git a/sceptre/cli/update.py b/sceptre/cli/update.py index 56c9b242f..3718f4a2e 100644 --- a/sceptre/cli/update.py +++ b/sceptre/cli/update.py @@ -13,15 +13,10 @@ @click.command(name="update", short_help="Update a stack.") @click.argument("path") @click.option( - "-c", "--change-set", is_flag=True, - help="Create a change set before updating." -) -@click.option( - "-v", "--verbose", is_flag=True, help="Display verbose output." -) -@click.option( - "-y", "--yes", is_flag=True, help="Assume yes to all questions." + "-c", "--change-set", is_flag=True, help="Create a change set before updating." ) +@click.option("-v", "--verbose", is_flag=True, help="Display verbose output.") +@click.option("-y", "--yes", is_flag=True, help="Assume yes to all questions.") @click.pass_context @catch_exceptions def update_command(ctx, path, change_set, verbose, yes): @@ -46,7 +41,7 @@ def update_command(ctx, path, change_set, verbose, yes): user_variables=ctx.obj.get("user_variables"), options=ctx.obj.get("options"), output_format=ctx.obj.get("output_format"), - ignore_dependencies=ctx.obj.get("ignore_dependencies") + ignore_dependencies=ctx.obj.get("ignore_dependencies"), ) plan = SceptrePlan(context) diff --git a/sceptre/config/__init__.py b/sceptre/config/__init__.py index 1656562d8..4f8bf5472 100644 --- a/sceptre/config/__init__.py +++ b/sceptre/config/__init__.py @@ -3,8 +3,8 @@ import logging -__author__ = 'Cloudreach' -__email__ = 'sceptre@cloudreach.com' +__author__ = "Cloudreach" +__email__ = "sceptre@cloudreach.com" # Set up logging to ``/dev/null`` like a library is supposed to. @@ -14,4 +14,4 @@ def emit(self, record): pass -logging.getLogger('sceptre').addHandler(NullHandler()) +logging.getLogger("sceptre").addHandler(NullHandler()) diff --git a/sceptre/config/graph.py b/sceptre/config/graph.py index b77ba0194..34d6b79d8 100644 --- a/sceptre/config/graph.py +++ b/sceptre/config/graph.py @@ -90,9 +90,7 @@ def _generate_edges(self, stack: Stack, dependencies: List[Stack]): :param stack: A Sceptre Stack :param dependencies: a collection of dependency paths """ - self.logger.debug( - "Generate dependencies for stack {0}".format(stack) - ) + self.logger.debug("Generate dependencies for stack {0}".format(stack)) for dependency in set(dependencies): self.graph.add_edge(dependency, stack) if not nx.is_directed_acyclic_graph(self.graph): diff --git a/sceptre/config/reader.py b/sceptre/config/reader.py index b7eb8e3a4..0cb31f979 100644 --- a/sceptre/config/reader.py +++ b/sceptre/config/reader.py @@ -60,20 +60,17 @@ "template_path": strategies.child_wins, "template": strategies.child_wins, "ignore": strategies.child_wins, - "obsolete": strategies.child_wins + "obsolete": strategies.child_wins, } STACK_GROUP_CONFIG_ATTRIBUTES = ConfigAttributes( - { - "project_code", - "region" - }, + {"project_code", "region"}, { "template_bucket_name", "template_key_prefix", "required_version", - "j2_environment" - } + "j2_environment", + }, ) STACK_CONFIG_ATTRIBUTES = ConfigAttributes( @@ -94,8 +91,8 @@ "sceptre_user_data", "stack_name", "stack_tags", - "stack_timeout" - } + "stack_timeout", + }, ) INTERNAL_CONFIG_ATTRIBUTES = ConfigAttributes( @@ -103,8 +100,7 @@ "project_path", "stack_group_path", }, - { - } + {}, ) REQUIRED_KEYS = STACK_GROUP_CONFIG_ATTRIBUTES.required.union( @@ -146,9 +142,11 @@ def _iterate_entry_points(group): """ if sys.version_info < (3, 10): from pkg_resources import iter_entry_points + return iter_entry_points(group) else: from importlib.metadata import entry_points + return entry_points(group=group) def _add_yaml_constructors(self, entry_point_groups): @@ -188,7 +186,7 @@ def class_constructor(loader, node): for entry_point in self._iterate_entry_points(group): # Retrieve name and class from entry point - node_tag = u'!' + entry_point.name + node_tag = "!" + entry_point.name node_class = entry_point.load() # Add constructor to PyYAML loader @@ -197,7 +195,8 @@ def class_constructor(loader, node): ) self.logger.debug( "Added constructor for %s with node tag %s", - str(node_class), node_tag + str(node_class), + node_tag, ) def resolve_node_tag(self, loader, node): @@ -227,8 +226,8 @@ def construct_stacks(self) -> Tuple[Set[Stack], Set[Stack]]: else: todo = set() for directory_name, sub_directories, files in walk(root, followlinks=True): - for filename in fnmatch.filter(files, '*.yaml'): - if filename.startswith('config.'): + for filename in fnmatch.filter(files, "*.yaml"): + if filename.startswith("config."): continue todo.add(path.join(directory_name, filename)) @@ -239,15 +238,15 @@ def construct_stacks(self) -> Tuple[Set[Stack], Set[Stack]]: while todo: abs_path = todo.pop() - rel_path = path.relpath( - abs_path, start=self.context.full_config_path()) + rel_path = path.relpath(abs_path, start=self.context.full_config_path()) directory, filename = path.split(rel_path) if directory in stack_group_configs: stack_group_config = stack_group_configs[directory] else: - stack_group_config = stack_group_configs[directory] = \ - self.read(path.join(directory, self.context.config_file)) + stack_group_config = stack_group_configs[directory] = self.read( + path.join(directory, self.context.config_file) + ) stack = self._construct_stack(rel_path, stack_group_config) for dep in stack.dependencies: @@ -256,8 +255,10 @@ def construct_stacks(self) -> Tuple[Set[Stack], Set[Stack]]: raise DependencyDoesNotExistError( "{stackname}: Dependency {dep} not found. " "Please make sure that your dependencies stack_outputs " - "have their full path from `config` defined." - .format(stackname=stack.name, dep=dep)) + "have their full path from `config` defined.".format( + stackname=stack.name, dep=dep + ) + ) if full_dep not in full_todo and full_dep not in deps_todo: todo.add(full_dep) @@ -266,8 +267,9 @@ def construct_stacks(self) -> Tuple[Set[Stack], Set[Stack]]: stack_map[sceptreise_path(rel_path)] = stack full_command_path = self.context.full_command_path() - if abs_path == full_command_path\ - or abs_path.startswith(full_command_path.rstrip(path.sep) + path.sep): + if abs_path == full_command_path or abs_path.startswith( + full_command_path.rstrip(path.sep) + path.sep + ): command_stacks.add(stack) stacks = self.resolve_stacks(stack_map) @@ -300,9 +302,12 @@ def resolve_stacks(self, stack_map) -> Set[Stack]: "Valid dependency names are: " "{stackkeys}. " "Please make sure that your dependencies stack_outputs " - "have their full path from `config` defined." - .format(stackname=stack.name, dep=dep, - stackkeys=", ".join(stack_map.keys()))) + "have their full path from `config` defined.".format( + stackname=stack.name, + dep=dep, + stackkeys=", ".join(stack_map.keys()), + ) + ) # We deduplicate the dependencies using a set here, since it's possible that a given # dependency ends up in the list multiple times. stack.dependencies = list(set(stack.dependencies)) @@ -330,7 +335,7 @@ def read(self, rel_path, base_config=None): # Adding properties from class config = { "project_path": self.context.project_path, - "stack_group_path": directory_path + "stack_group_path": directory_path, } # Adding defaults from base config. @@ -338,20 +343,19 @@ def read(self, rel_path, base_config=None): config.update(base_config) # Check if file exists, but ignore config.yaml as can be inherited. - if not path.isfile(abs_path)\ - and not filename.endswith(self.context.config_file): + if not path.isfile(abs_path) and not filename.endswith( + self.context.config_file + ): raise ConfigFileNotFoundError( - "Config file \"{0}\" not found.".format(rel_path) + 'Config file "{0}" not found.'.format(rel_path) ) # Parse and read in the config files. this_config = self._recursive_read(directory_path, filename, config) if "dependencies" in config or "dependencies" in this_config: - this_config['dependencies'] = \ - CONFIG_MERGE_STRATEGIES['dependencies']( - this_config.get("dependencies"), - config.get("dependencies") + this_config["dependencies"] = CONFIG_MERGE_STRATEGIES["dependencies"]( + this_config.get("dependencies"), config.get("dependencies") ) config.update(this_config) @@ -360,7 +364,9 @@ def read(self, rel_path, base_config=None): self.logger.debug("Config: %s", config) return config - def _recursive_read(self, directory_path: str, filename: str, stack_group_config: dict) -> dict: + def _recursive_read( + self, directory_path: str, filename: str, stack_group_config: dict + ) -> dict: """ Traverses the directory_path, from top to bottom, reading in all relevant config files. If config attributes are encountered further @@ -379,7 +385,9 @@ def _recursive_read(self, directory_path: str, filename: str, stack_group_config config = {} if directory_path: - config = self._recursive_read(parent_directory, filename, stack_group_config) + config = self._recursive_read( + parent_directory, filename, stack_group_config + ) # Combine the stack_group_config with the nested config dict config_group = stack_group_config.copy() @@ -389,9 +397,7 @@ def _recursive_read(self, directory_path: str, filename: str, stack_group_config child_config = self._render(directory_path, filename, config_group) or {} for config_key, strategy in CONFIG_MERGE_STRATEGIES.items(): - value = strategy( - config.get(config_key), child_config.get(config_key) - ) + value = strategy(config.get(config_key), child_config.get(config_key)) if value: child_config[config_key] = value @@ -419,22 +425,23 @@ def _render(self, directory_path, basename, stack_group_config): if path.isfile(path.join(abs_directory_path, basename)): default_j2_environment_config = { "autoescape": select_autoescape( - disabled_extensions=('yaml',), + disabled_extensions=("yaml",), default=True, ), "loader": FileSystemLoader(abs_directory_path), - "undefined": StrictUndefined + "undefined": StrictUndefined, } j2_environment_config = strategies.dict_merge( default_j2_environment_config, - stack_group_config.get("j2_environment", {})) + stack_group_config.get("j2_environment", {}), + ) j2_environment = Environment(**j2_environment_config) template = j2_environment.get_template(basename) self.templating_vars.update(stack_group_config) rendered_template = template.render( self.templating_vars, command_path=self.context.command_path.split(path.sep), - environment_variable=environ + environment_variable=environ, ) try: @@ -468,14 +475,12 @@ def _check_version(self, config): :raises: sceptre.exceptions.VersionIncompatibleException """ sceptre_version = __version__ - if 'required_version' in config: - required_version = config['required_version'] + if "required_version" in config: + required_version = config["required_version"] if Version(sceptre_version) not in SpecifierSet(required_version, True): raise VersionIncompatibleError( "Current sceptre version ({0}) does not meet version " - "requirements: {1}".format( - sceptre_version, required_version - ) + "requirements: {1}".format(sceptre_version, required_version) ) @staticmethod @@ -494,13 +499,16 @@ def _collect_s3_details(stack_name, config): # If the config explicitly sets the template_bucket_name to None, we don't want to enter # this conditional block. if config.get("template_bucket_name") is not None: - template_key = "/".join([ - sceptreise_path(stack_name), "{time_stamp}.json".format( - time_stamp=datetime.datetime.utcnow().strftime( - "%Y-%m-%d-%H-%M-%S-%fZ" - ) - ) - ]) + template_key = "/".join( + [ + sceptreise_path(stack_name), + "{time_stamp}.json".format( + time_stamp=datetime.datetime.utcnow().strftime( + "%Y-%m-%d-%H-%M-%S-%fZ" + ) + ), + ] + ) if "template_key_prefix" in config: prefix = config["template_key_prefix"] @@ -508,7 +516,7 @@ def _collect_s3_details(stack_name, config): s3_details = { "bucket_name": config["template_bucket_name"], - "bucket_key": template_key + "bucket_key": template_key, } return s3_details @@ -543,9 +551,7 @@ def _construct_stack(self, rel_path, stack_group_config=None): ) ) - s3_details = self._collect_s3_details( - stack_name, config - ) + s3_details = self._collect_s3_details(stack_name, config) stack = Stack( name=stack_name, @@ -573,7 +579,7 @@ def _construct_stack(self, rel_path, stack_group_config=None): stack_timeout=config.get("stack_timeout", 0), ignore=config.get("ignore", False), obsolete=config.get("obsolete", False), - stack_group_config=parsed_stack_group_config + stack_group_config=parsed_stack_group_config, ) del self.templating_vars["stack_group_config"] @@ -587,8 +593,7 @@ def _parsed_stack_group_config(self, stack_group_config): """ parsed_config = { key: stack_group_config[key] - for key in - set(stack_group_config) - set(CONFIG_MERGE_STRATEGIES) + for key in set(stack_group_config) - set(CONFIG_MERGE_STRATEGIES) } parsed_config.pop("stack_group_path") return parsed_config diff --git a/sceptre/config/strategies.py b/sceptre/config/strategies.py index f04955d3f..29b37d9ea 100644 --- a/sceptre/config/strategies.py +++ b/sceptre/config/strategies.py @@ -21,10 +21,10 @@ def list_join(a, b): :rtype: list """ if a and not isinstance(a, list): - raise TypeError('{} is not a list'.format(a)) + raise TypeError("{} is not a list".format(a)) if b and not isinstance(b, list): - raise TypeError('{} is not a list'.format(b)) + raise TypeError("{} is not a list".format(b)) if a is None: return deepcopy(b) @@ -47,9 +47,9 @@ def dict_merge(a, b): :rtype: dict """ if a and not isinstance(a, dict): - raise TypeError('{} is not a dict'.format(a)) + raise TypeError("{} is not a dict".format(a)) if b and not isinstance(b, dict): - raise TypeError('{} is not a dict'.format(b)) + raise TypeError("{} is not a dict".format(b)) if a is None: return deepcopy(b) diff --git a/sceptre/connection_manager.py b/sceptre/connection_manager.py index 4d664807b..1ce50cded 100644 --- a/sceptre/connection_manager.py +++ b/sceptre/connection_manager.py @@ -63,9 +63,7 @@ def decorated(*args, **kwargs): else: raise raise RetryLimitExceededError( - "Exceeded request limit {0} times. Aborting.".format( - max_retries - ) + "Exceeded request limit {0} times. Aborting.".format(max_retries) ) return decorated @@ -93,8 +91,12 @@ class ConnectionManager(object): _stack_keys = {} def __init__( - self, region, profile=None, stack_name=None, - iam_role=None, iam_role_session_duration=None + self, + region, + profile=None, + stack_name=None, + iam_role=None, + iam_role_session_duration=None, ): self.logger = logging.getLogger(__name__) @@ -112,7 +114,11 @@ def __repr__(self): return ( "sceptre.connection_manager.ConnectionManager(region='{0}', " "profile='{1}', stack_name='{2}', iam_role='{3}', iam_role_session_duration='{4}')".format( - self.region, self.profile, self.stack_name, self.iam_role, self.iam_role_session_duration + self.region, + self.profile, + self.stack_name, + self.iam_role, + self.iam_role_session_duration, ) ) @@ -143,7 +149,7 @@ def _get_session(self, profile, region, iam_role): "region_name": region, "aws_access_key_id": environ.get("AWS_ACCESS_KEY_ID"), "aws_secret_access_key": environ.get("AWS_SECRET_ACCESS_KEY"), - "aws_session_token": environ.get("AWS_SESSION_TOKEN") + "aws_session_token": environ.get("AWS_SESSION_TOKEN"), } session = boto3.session.Session(**config) @@ -161,11 +167,13 @@ def _get_session(self, profile, region, iam_role): # maximum session name length is 64 chars. 56 + "-session" = 64 session_name = f'{iam_role.split("/")[-1][:56]}-session' assume_role_kwargs = { - 'RoleArn': iam_role, - 'RoleSessionName': session_name, + "RoleArn": iam_role, + "RoleSessionName": session_name, } if self.iam_role_session_duration: - assume_role_kwargs['DurationSeconds'] = self.iam_role_session_duration + assume_role_kwargs[ + "DurationSeconds" + ] = self.iam_role_session_duration sts_response = sts_client.assume_role(**assume_role_kwargs) credentials = sts_response["Credentials"] @@ -173,7 +181,7 @@ def _get_session(self, profile, region, iam_role): aws_access_key_id=credentials["AccessKeyId"], aws_secret_access_key=credentials["SecretAccessKey"], aws_session_token=credentials["SessionToken"], - region_name=region + region_name=region, ) if session.get_credentials() is None: @@ -189,14 +197,12 @@ def _get_session(self, profile, region, iam_role): "Using credential set from %s: %s", session.get_credentials().method, { - "AccessKeyId": mask_key( - session.get_credentials().access_key - ), + "AccessKeyId": mask_key(session.get_credentials().access_key), "SecretAccessKey": mask_key( session.get_credentials().secret_key ), - "Region": session.region_name - } + "Region": session.region_name, + }, ) self.logger.debug("Boto3 session created") @@ -218,9 +224,7 @@ def _get_client(self, service, region, profile, stack_name, iam_role): with self._client_lock: key = (service, region, profile, stack_name, iam_role) if self._clients.get(key) is None: - self.logger.debug( - "No %s client found, creating one...", service - ) + self.logger.debug("No %s client found, creating one...", service) self._clients[key] = self._get_session( profile, region, iam_role ).client(service) @@ -229,8 +233,14 @@ def _get_client(self, service, region, profile, stack_name, iam_role): @_retry_boto_call def call( - self, service, command, kwargs=None, profile=None, region=None, - stack_name=None, iam_role=None + self, + service, + command, + kwargs=None, + profile=None, + region=None, + stack_name=None, + iam_role=None, ): """ Makes a thread-safe Boto3 client call. diff --git a/sceptre/context.py b/sceptre/context.py index 6376b7c07..2b6c825d7 100644 --- a/sceptre/context.py +++ b/sceptre/context.py @@ -46,9 +46,17 @@ class SceptreContext(object): :type full_scan: bool """ - def __init__(self, project_path, command_path, - user_variables=None, options=None, output_format=None, - no_colour=False, ignore_dependencies=False, full_scan=False): + def __init__( + self, + project_path, + command_path, + user_variables=None, + options=None, + output_format=None, + no_colour=False, + ignore_dependencies=False, + full_scan=False, + ): # project_path: absolute path to the base sceptre project folder # e.g. absolute_path/to/sceptre_directory self.project_path = normalise_path(project_path) @@ -72,12 +80,13 @@ def __init__(self, project_path, command_path, self.templates_path = "templates" self.user_variables = user_variables if user_variables else {} - self.user_variables = user_variables\ - if user_variables is not None else {} + self.user_variables = user_variables if user_variables is not None else {} self.options = options if options else {} self.output_format = output_format if output_format else "" self.no_colour = no_colour if no_colour is True else False - self.ignore_dependencies = ignore_dependencies if ignore_dependencies is True else False + self.ignore_dependencies = ( + ignore_dependencies if ignore_dependencies is True else False + ) self.full_scan = full_scan if full_scan is True else False def full_config_path(self): @@ -97,8 +106,7 @@ def full_command_path(self): :returns: The absolute path to the path that will be executed :rtype: str """ - return path.join(self.project_path, self.config_path, - self.command_path) + return path.join(self.project_path, self.config_path, self.command_path) def full_templates_path(self): """ @@ -117,11 +125,7 @@ def command_path_is_stack(self): :rtype: bool """ return path.isfile( - path.join( - self.project_path, - self.config_path, - self.command_path - ) + path.join(self.project_path, self.config_path, self.command_path) ) def clone(self) -> "SceptreContext": diff --git a/sceptre/diffing/diff_writer.py b/sceptre/diffing/diff_writer.py index 8d2d67298..b79c259ab 100644 --- a/sceptre/diffing/diff_writer.py +++ b/sceptre/diffing/diff_writer.py @@ -14,7 +14,7 @@ deepdiff_json_defaults = { datetime.date: lambda x: x.isoformat(), - StackConfiguration: lambda x: dict(x._asdict()) + StackConfiguration: lambda x: dict(x._asdict()), } @@ -23,10 +23,13 @@ class DiffWriter(Generic[DiffType]): readable. This is an abstract base class, so the abstract methods need to be implemented to create a DiffWriter for a given DiffType. """ - STAR_BAR = '*' * 80 - LINE_BAR = '-' * 80 - def __init__(self, stack_diff: StackDiff, output_stream: TextIO, output_format: str): + STAR_BAR = "*" * 80 + LINE_BAR = "-" * 80 + + def __init__( + self, stack_diff: StackDiff, output_stream: TextIO, output_format: str + ): """Initializes the DiffWriter :param stack_diff: The diff this writer will be outputting @@ -70,20 +73,20 @@ def write(self): def _write_new_stack_details(self): stack_config_text = self._dump_stack_config(self.stack_diff.generated_config) self._output( - 'This stack is not deployed yet!', + "This stack is not deployed yet!", self.LINE_BAR, - 'New Config:', - '', + "New Config:", + "", stack_config_text, self.LINE_BAR, - 'New Template:', - '', - self.stack_diff.generated_template + "New Template:", + "", + self.stack_diff.generated_template, ) return def _output(self, *lines: str): - lines_with_breaks = [f'{line}\n' for line in lines] + lines_with_breaks = [f"{line}\n" for line in lines] self.output_stream.writelines(lines_with_breaks) def _dump_stack_config(self, stack_config: StackConfiguration) -> str: @@ -107,27 +110,19 @@ def _write_config_difference(self): return diff_text = self.dump_diff(self.config_diff) - self._output( - f'Config difference for {self.stack_name}:', - '', - diff_text - ) + self._output(f"Config difference for {self.stack_name}:", "", diff_text) def _write_template_difference(self): if not self.has_template_difference: - self._output('No template difference') + self._output("No template difference") return diff_text = self.dump_diff(self.template_diff) - self._output( - f'Template difference for {self.stack_name}:', - '', - diff_text - ) + self._output(f"Template difference for {self.stack_name}:", "", diff_text) @abstractmethod def dump_diff(self, diff: DiffType) -> str: - """"Implement this method to write the DiffType to string""" + """ "Implement this method to write the DiffType to string""" @property @abstractmethod @@ -153,11 +148,11 @@ def has_template_difference(self) -> bool: def dump_diff(self, diff: DeepDiff) -> str: as_diff_dict = diff.to_dict() - if self.output_format == 'json': + if self.output_format == "json": return json.dumps( as_diff_dict, indent=4, - default=json_convertor_default(default_mapping=deepdiff_json_defaults) + default=json_convertor_default(default_mapping=deepdiff_json_defaults), ) compatible = self._make_strings_block_compatible(as_diff_dict) @@ -188,7 +183,7 @@ def _make_strings_block_compatible(self, obj): elif isinstance(obj, list): return [self._make_strings_block_compatible(item) for item in obj] elif isinstance(obj, str): - return re.sub('[ ]*\n', '\n', obj) + return re.sub("[ ]*\n", "\n", obj) else: return obj @@ -207,7 +202,7 @@ def has_template_difference(self) -> bool: def dump_diff(self, diff: List[str]) -> str: # Difflib doesn't care about the output format since it only outputs strings. We would have # accounted for the output format in the differ itself rather than here. - return '\n'.join(diff) + return "\n".join(diff) class ColouredDiffLibWriter(DiffLibWriter): @@ -215,11 +210,11 @@ class ColouredDiffLibWriter(DiffLibWriter): def _colour_diff(self, diff: List[str]): for line in diff: - if line.startswith('+'): + if line.startswith("+"): yield Fore.GREEN + line + Fore.RESET - elif line.startswith('-'): + elif line.startswith("-"): yield Fore.RED + line + Fore.RESET - elif line.startswith('^'): + elif line.startswith("^"): yield Fore.BLUE + line + Fore.RESET else: yield line diff --git a/sceptre/diffing/stack_differ.py b/sceptre/diffing/stack_differ.py index 87931e1de..577a4592e 100644 --- a/sceptre/diffing/stack_differ.py +++ b/sceptre/diffing/stack_differ.py @@ -1,7 +1,17 @@ import difflib import logging from abc import abstractmethod -from typing import NamedTuple, Dict, List, Optional, Callable, Tuple, Generic, TypeVar, Union +from typing import ( + NamedTuple, + Dict, + List, + Optional, + Callable, + Tuple, + Generic, + TypeVar, + Union, +) import cfn_flip import deepdiff @@ -12,13 +22,14 @@ from sceptre.plan.actions import StackActions from sceptre.stack import Stack -DiffType = TypeVar('DiffType') +DiffType = TypeVar("DiffType") logger = logging.getLogger(__name__) class StackConfiguration(NamedTuple): """A data container to represent the comparable parts of a Stack.""" + stack_name: str parameters: Dict[str, Union[str, List[str]]] stack_tags: Dict[str, str] @@ -30,6 +41,7 @@ class StackDiff(NamedTuple): """A data container to represent the full difference between a deployed stack and the stack as it exists locally within Sceptre. """ + stack_name: str template_diff: DiffType config_diff: DiffType @@ -47,8 +59,8 @@ def repr_str(dumper: Dumper, data: str) -> str: :param data: The string to serialize :return: The represented string """ - if '\n' in data: - return dumper.represent_scalar(u'tag:yaml.org,2002:str', data, style='|') + if "\n" in data: + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") return dumper.represent_str(data) @@ -82,10 +94,11 @@ class StackDiffer(Generic[DiffType]): As an abstract base class, the two comparison methods need to be implemented so that the StackDiff can be generated. """ + STACK_STATUSES_INDICATING_NOT_DEPLOYED = [ - 'CREATE_FAILED', - 'ROLLBACK_COMPLETE', - 'DELETE_COMPLETE', + "CREATE_FAILED", + "ROLLBACK_COMPLETE", + "DELETE_COMPLETE", ] NO_ECHO_REPLACEMENT = "***HIDDEN***" @@ -111,12 +124,18 @@ def diff(self, stack_actions: StackActions) -> StackDiff: is_stack_deployed = bool(deployed_config) generated_template = self._generate_template(stack_actions) - deployed_template = self._get_deployed_template(stack_actions, is_stack_deployed) + deployed_template = self._get_deployed_template( + stack_actions, is_stack_deployed + ) - self._handle_special_parameter_situations(stack_actions, generated_config, deployed_config) + self._handle_special_parameter_situations( + stack_actions, generated_config, deployed_config + ) template_diff = self.compare_templates(deployed_template, generated_template) - config_diff = self.compare_stack_configurations(deployed_config, generated_config) + config_diff = self.compare_stack_configurations( + deployed_config, generated_config + ) return StackDiff( stack_actions.stack.external_name, @@ -124,7 +143,7 @@ def diff(self, stack_actions: StackActions) -> StackDiff: config_diff, is_stack_deployed, generated_config, - generated_template + generated_template, ) def _create_generated_config(self, stack: Stack) -> StackConfiguration: @@ -134,7 +153,7 @@ def _create_generated_config(self, stack: Stack) -> StackConfiguration: parameters=parameters, stack_tags=stack.tags, notifications=stack.notifications, - role_arn=stack.role_arn + role_arn=stack.role_arn, ) return stack_configuration @@ -149,41 +168,39 @@ def _extract_parameters_from_generated_stack(self, stack: Stack) -> dict: formatted_parameters = {} for key, value in stack.parameters.items(): if isinstance(value, list): - value = ','.join(item.rstrip('\n') for item in value) - formatted_parameters[key] = value.rstrip('\n') + value = ",".join(item.rstrip("\n") for item in value) + formatted_parameters[key] = value.rstrip("\n") return formatted_parameters - def _create_deployed_stack_config(self, stack_actions: StackActions) -> Optional[StackConfiguration]: + def _create_deployed_stack_config( + self, stack_actions: StackActions + ) -> Optional[StackConfiguration]: description = stack_actions.describe() if description is None: # This means the stack has not been deployed yet return None - stacks = description['Stacks'] + stacks = description["Stacks"] for stack in stacks: - if stack['StackStatus'] in self.STACK_STATUSES_INDICATING_NOT_DEPLOYED: + if stack["StackStatus"] in self.STACK_STATUSES_INDICATING_NOT_DEPLOYED: return None return StackConfiguration( parameters={ - param['ParameterKey']: param['ParameterValue'] - for param - in stack.get('Parameters', []) + param["ParameterKey"]: param["ParameterValue"] + for param in stack.get("Parameters", []) }, - stack_tags={ - tag['Key']: tag['Value'] - for tag in stack['Tags'] - }, - stack_name=stack['StackName'], - notifications=stack['NotificationARNs'], - role_arn=stack.get('RoleARN') + stack_tags={tag["Key"]: tag["Value"] for tag in stack["Tags"]}, + stack_name=stack["StackName"], + notifications=stack["NotificationARNs"], + role_arn=stack.get("RoleARN"), ) def _handle_special_parameter_situations( self, stack_actions: StackActions, generated_config: StackConfiguration, - deployed_config: StackConfiguration + deployed_config: StackConfiguration, ): deployed_template_summary = stack_actions.fetch_remote_template_summary() generated_template_summary = stack_actions.fetch_local_template_summary() @@ -193,17 +210,14 @@ def _handle_special_parameter_situations( # and can sometimes come from using the !file_contents resolver, but ultimately they # shouldn't affect the diff. We'll ignore all trailing linebreaks. self._remove_terminating_linebreaks_from_deployed_parameters( - deployed_template_summary, - deployed_config + deployed_template_summary, deployed_config ) # If the parameter is not passed by Sceptre and the value on the deployed parameter is # the default value, we'll actually remove it from the deployed parameters list so it # doesn't show up as a false positive. self._remove_deployed_default_parameters_that_arent_passed( - deployed_template_summary, - generated_config, - deployed_config + deployed_template_summary, generated_config, deployed_config ) if not self.show_no_echo: # We don't actually want to show parameters Sceptre is passing that the local template @@ -212,25 +226,23 @@ def _handle_special_parameter_situations( self._mask_no_echo_parameters(generated_template_summary, generated_config) def _remove_terminating_linebreaks_from_deployed_parameters( - self, - template_summary: Optional[dict], - deployed_config: StackConfiguration + self, template_summary: Optional[dict], deployed_config: StackConfiguration ): if template_summary is None: return parameter_types = { - parameter['ParameterKey']: parameter['ParameterType'] - for parameter in template_summary['Parameters'] + parameter["ParameterKey"]: parameter["ParameterType"] + for parameter in template_summary["Parameters"] } for key, value in deployed_config.parameters.items(): parameter_type = parameter_types[key] - if parameter_type == 'CommaDelimitedList': + if parameter_type == "CommaDelimitedList": # If it's a list of strings, remove trailing linebreaks for each item - value = ','.join([item.rstrip('\n') for item in value.split(',')]) + value = ",".join([item.rstrip("\n") for item in value.split(",")]) - deployed_config.parameters[key] = value.rstrip('\n') + deployed_config.parameters[key] = value.rstrip("\n") def _remove_deployed_default_parameters_that_arent_passed( self, @@ -255,11 +267,11 @@ def _get_parameter_default_map(self, template_summary: dict) -> Dict[str, str]: if template_summary is None: return {} - parameters = template_summary['Parameters'] + parameters = template_summary["Parameters"] default_map = {} for parameter in parameters: - key = parameter['ParameterKey'] + key = parameter["ParameterKey"] value = self._handle_default_value(parameter) if value is not None: default_map[key] = value @@ -267,43 +279,45 @@ def _get_parameter_default_map(self, template_summary: dict) -> Dict[str, str]: return default_map def _handle_default_value(self, parameter): - default_value = parameter.get('DefaultValue') - param_type = parameter['ParameterType'] + default_value = parameter.get("DefaultValue") + param_type = parameter["ParameterType"] if default_value is None: return None - if parameter.get('NoEcho'): - default_value = '****' - elif 'List' in param_type: + if parameter.get("NoEcho"): + default_value = "****" + elif "List" in param_type: # Eliminate whitespace around commas - default_value = ','.join(value.strip() for value in default_value.split(',')) + default_value = ",".join( + value.strip() for value in default_value.split(",") + ) return default_value - def _mask_no_echo_parameters(self, template_summary: dict, generated_config: StackConfiguration): - parameters = template_summary['Parameters'] + def _mask_no_echo_parameters( + self, template_summary: dict, generated_config: StackConfiguration + ): + parameters = template_summary["Parameters"] for parameter in parameters: - key = parameter['ParameterKey'] - if parameter.get('NoEcho') and key in generated_config.parameters: + key = parameter["ParameterKey"] + if parameter.get("NoEcho") and key in generated_config.parameters: generated_config.parameters[key] = self.NO_ECHO_REPLACEMENT def _generate_template(self, stack_actions: StackActions) -> str: return stack_actions.generate() - def _get_deployed_template(self, stack_actions: StackActions, is_deployed: bool) -> str: + def _get_deployed_template( + self, stack_actions: StackActions, is_deployed: bool + ) -> str: if is_deployed: - return stack_actions.fetch_remote_template() or '{}' + return stack_actions.fetch_remote_template() or "{}" else: - return '{}' + return "{}" @abstractmethod - def compare_templates( - self, - deployed: str, - generated: str - ) -> DiffType: + def compare_templates(self, deployed: str, generated: str) -> DiffType: """Implement this method to return the diff for the templates :param deployed: The stack template as it has been deployed @@ -313,9 +327,7 @@ def compare_templates( @abstractmethod def compare_stack_configurations( - self, - deployed: Optional[StackConfiguration], - generated: StackConfiguration + self, deployed: Optional[StackConfiguration], generated: StackConfiguration ) -> DiffType: """Implement this method to return the diff for the stack configurations. @@ -335,6 +347,7 @@ class DeepDiffStackDiffer(StackDiffer[deepdiff.DeepDiff]): are read in as dictionaries and compared this way, so json or yaml formatting changes will not be reflected, only changes in value. """ + VERBOSITY_LEVEL_TO_INDICATE_CHANGED_VALUES = 2 def __init__( @@ -362,7 +375,7 @@ def compare_stack_configurations( return deepdiff.DeepDiff( deployed, generated, - verbose_level=self.VERBOSITY_LEVEL_TO_INDICATE_CHANGED_VALUES + verbose_level=self.VERBOSITY_LEVEL_TO_INDICATE_CHANGED_VALUES, ) def compare_templates(self, deployed: str, generated: str) -> deepdiff.DeepDiff: @@ -374,7 +387,7 @@ def compare_templates(self, deployed: str, generated: str) -> deepdiff.DeepDiff: return deepdiff.DeepDiff( deployed_dict, generated_dict, - verbose_level=self.VERBOSITY_LEVEL_TO_INDICATE_CHANGED_VALUES + verbose_level=self.VERBOSITY_LEVEL_TO_INDICATE_CHANGED_VALUES, ) @@ -415,18 +428,18 @@ def compare_stack_configurations( comparable_generated = self._make_stack_configuration_comparable(generated) deployed_string = cfn_flip.dump_yaml(comparable_deployed) generated_string = cfn_flip.dump_yaml(comparable_generated) - return self._make_string_diff( - deployed_string, - generated_string - ) + return self._make_string_diff(deployed_string, generated_string) - def _make_stack_configuration_comparable(self, config: Optional[StackConfiguration]): + def _make_stack_configuration_comparable( + self, config: Optional[StackConfiguration] + ): as_dict = dict(config._asdict()) return { - key: value for key, value in as_dict.items() + key: value + for key, value in as_dict.items() # stack_name isn't always going to be the same, otherwise we wouldn't be comparing them. # It's more confusing to have it in the diff output than to just remove it. - if value not in (None, [], {}) and key != 'stack_name' + if value not in (None, [], {}) and key != "stack_name" } def compare_templates( @@ -444,10 +457,7 @@ def compare_templates( # there being no diff. deployed_dict, _ = self.load_template(deployed) generated_dict, generated_format = self.load_template(generated) - dumpers = { - 'json': cfn_flip.dump_json, - 'yaml': cfn_flip.dump_yaml - } + dumpers = {"json": cfn_flip.dump_json, "yaml": cfn_flip.dump_yaml} deployed_reformatted = dumpers[generated_format](deployed_dict) generated_reformatted = dumpers[generated_format](generated_dict) @@ -459,6 +469,6 @@ def _make_string_diff(self, deployed: str, generated: str) -> List[str]: generated.splitlines(), fromfile="deployed", tofile="generated", - lineterm="" + lineterm="", ) return list(diff_lines) diff --git a/sceptre/exceptions.py b/sceptre/exceptions.py index 6bf73bd20..50478ce7a 100644 --- a/sceptre/exceptions.py +++ b/sceptre/exceptions.py @@ -5,6 +5,7 @@ class SceptreException(Exception): """ Base class for all Sceptre errors """ + pass @@ -12,6 +13,7 @@ class ProjectAlreadyExistsError(SceptreException): """ Error raised when Sceptre project already exists. """ + pass @@ -19,6 +21,7 @@ class InvalidSceptreDirectoryError(SceptreException): """ Error raised if a sceptre directory is invalid. """ + pass @@ -26,6 +29,7 @@ class UnsupportedTemplateFileTypeError(SceptreException): """ Error raised if an unsupported template file type is used. """ + pass @@ -33,6 +37,7 @@ class TemplateSceptreHandlerError(SceptreException): """ Error raised if sceptre_handler() is not defined correctly in the template. """ + pass @@ -40,6 +45,7 @@ class DependencyDoesNotExistError(SceptreException): """ Error raised when a dependency cannot be found """ + pass @@ -47,6 +53,7 @@ class DependencyStackNotLaunchedError(SceptreException): """ Error raised when a dependency stack has not been launched """ + pass @@ -54,6 +61,7 @@ class DependencyStackMissingOutputError(SceptreException): """ Error raised if a dependency stack does not have the correct outputs. """ + pass @@ -61,6 +69,7 @@ class CircularDependenciesError(SceptreException): """ Error raised if there are circular dependencies """ + pass @@ -68,6 +77,7 @@ class UnknownStackStatusError(SceptreException): """ Error raised if an unknown stack status is received. """ + pass @@ -75,6 +85,7 @@ class RetryLimitExceededError(SceptreException): """ Error raised if the request limit is exceeded. """ + pass @@ -88,6 +99,7 @@ class VersionIncompatibleError(SceptreException): """ Error raised if configuration incompatible with running version. """ + pass @@ -95,6 +107,7 @@ class ProtectedStackError(SceptreException): """ Error raised upon execution of an action under active protection """ + pass @@ -102,6 +115,7 @@ class UnknownStackChangeSetStatusError(SceptreException): """ Error raised if an unknown stack change set status is received. """ + pass @@ -109,6 +123,7 @@ class InvalidHookArgumentTypeError(SceptreException): """ Error raised if a hook's argument type is invalid. """ + pass @@ -116,6 +131,7 @@ class InvalidHookArgumentSyntaxError(SceptreException): """ Error raised if a hook's argument syntax is invalid. """ + pass @@ -123,6 +139,7 @@ class InvalidHookArgumentValueError(SceptreException): """ Error raised if a hook's argument value is invalid. """ + pass @@ -130,6 +147,7 @@ class CannotUpdateFailedStackError(SceptreException): """ Error raised when a failed stack is updated. """ + pass @@ -137,6 +155,7 @@ class StackDoesNotExistError(SceptreException): """ Error raised when a stack does not exist. """ + pass @@ -144,6 +163,7 @@ class ConfigFileNotFoundError(SceptreException): """ Error raised when a config file does not exist. """ + pass @@ -151,6 +171,7 @@ class InvalidConfigFileError(SceptreException): """ Error raised when a config file lacks mandatory keys. """ + pass @@ -158,6 +179,7 @@ class PathConversionError(SceptreException): """ Error raised when a path is unable to be converted. """ + pass @@ -165,6 +187,7 @@ class InvalidAWSCredentialsError(SceptreException): """ Error raised when AWS credentials are invalid. """ + pass @@ -172,6 +195,7 @@ class TemplateHandlerNotFoundError(SceptreException): """ Error raised when a Template Handler of a certain type is not found """ + pass @@ -186,6 +210,7 @@ class TemplateNotFoundError(SceptreException): """ Error raised when a Template file is not found """ + pass diff --git a/sceptre/helpers.py b/sceptre/helpers.py index b8a1cf129..3392f29b9 100644 --- a/sceptre/helpers.py +++ b/sceptre/helpers.py @@ -19,10 +19,7 @@ def get_external_stack_name(project_code, stack_name): :returns: The name given to the stack in CloudFormation. :rtype: str """ - return "-".join([ - project_code, - stack_name.replace("/", "-") - ]) + return "-".join([project_code, stack_name.replace("/", "-")]) def mask_key(key): @@ -39,10 +36,7 @@ def mask_key(key): """ num_mask_chars = len(key) - 4 - return "".join([ - "*" if i < num_mask_chars else c - for i, c in enumerate(key) - ]) + return "".join(["*" if i < num_mask_chars else c for i, c in enumerate(key)]) def _call_func_on_values(func, attr, cls): @@ -83,10 +77,10 @@ def normalise_path(path): :returns: A normalised path with forward slashes. :returns: string """ - if sep == '/': - path = path.replace('\\', '/') - elif sep == '\\': - path = path.replace('/', '\\') + if sep == "/": + path = path.replace("\\", "/") + elif sep == "\\": + path = path.replace("/", "\\") if path.endswith("/") or path.endswith("\\"): raise PathConversionError( "'{0}' is an invalid path string. Paths should " @@ -106,7 +100,7 @@ def sceptreise_path(path): :returns: A normalised path with forward slashes. :returns: string """ - path = path.replace('\\', '/') + path = path.replace("\\", "/") if path.endswith("/") or path.endswith("\\"): raise PathConversionError( "'{0}' is an invalid path string. Paths should " @@ -123,7 +117,9 @@ def null_context(): yield -def extract_datetime_from_aws_response_headers(boto_response: dict) -> Optional[datetime]: +def extract_datetime_from_aws_response_headers( + boto_response: dict, +) -> Optional[datetime]: """Returns a datetime.datetime extracted from the response metadata in a boto response or None if it's unable to find or parse one. :param boto_response: A dictionary returned from a boto client call @@ -132,7 +128,9 @@ def extract_datetime_from_aws_response_headers(boto_response: dict) -> Optional[ if boto_response is None: return None try: - return dateutil.parser.parse(boto_response["ResponseMetadata"]["HTTPHeaders"]["date"]) + return dateutil.parser.parse( + boto_response["ResponseMetadata"]["HTTPHeaders"]["date"] + ) except (KeyError, dateutil.parser.ParserError): # We expect a KeyError if the date isn't present in the response. We # expect a ParserError if it's not well-formed. Any other error we want diff --git a/sceptre/hooks/__init__.py b/sceptre/hooks/__init__.py index e987d336c..44c7616a3 100644 --- a/sceptre/hooks/__init__.py +++ b/sceptre/hooks/__init__.py @@ -14,6 +14,7 @@ class Hook(object): :param stack: The associated stack of the hook. :type stack: sceptre.stack.Stack """ + __metaclass__ = abc.ABCMeta def __init__(self, argument=None, stack=None): @@ -66,6 +67,7 @@ def __set__(self, instance, value): data structure `value` and calls the setup method. """ + def setup(attr, key, value): value.stack = instance value.setup() @@ -98,6 +100,7 @@ def add_stack_hooks(func): :param func: a function that operates on a stack :type func: function """ + @wraps(func) def decorated(self, *args, **kwargs): execute_hooks(self.stack.hooks.get("before_" + func.__name__)) diff --git a/sceptre/hooks/asg_scaling_processes.py b/sceptre/hooks/asg_scaling_processes.py index f377265c5..0ea3c263a 100644 --- a/sceptre/hooks/asg_scaling_processes.py +++ b/sceptre/hooks/asg_scaling_processes.py @@ -30,14 +30,15 @@ def run(self): if not isinstance(self.argument, string_types): raise InvalidHookArgumentTypeError( 'The argument "{0}" is the wrong type - asg_scaling_processes ' - 'hooks require arguments of type string.'.format(self.argument) + "hooks require arguments of type string.".format(self.argument) ) if "::" not in str(self.argument): raise InvalidHookArgumentSyntaxError( 'Wrong syntax for the argument "{0}" - asg_scaling_processes ' - 'hooks use:' - '- !asg_scaling_processes ::' - .format(self.argument) + "hooks use:" + "- !asg_scaling_processes ::".format( + self.argument + ) ) action, scaling_processes = self.argument.split("::") @@ -45,8 +46,7 @@ def run(self): if action not in ["resume", "suspend"]: raise InvalidHookArgumentValueError( 'The argument "{0}" is invalid - valid arguments for ' - 'asg_scaling_processes hooks are "resume" or "suspend".' - .format(action) + 'asg_scaling_processes hooks are "resume" or "suspend".'.format(action) ) action += "_processes" @@ -58,8 +58,8 @@ def run(self): command=action, kwargs={ "AutoScalingGroupName": autoscaling_group, - "ScalingProcesses": [scaling_processes] - } + "ScalingProcesses": [scaling_processes], + }, ) def _get_stack_resources(self): @@ -70,7 +70,7 @@ def _get_stack_resources(self): response = self.stack.connection_manager.call( service="cloudformation", command="describe_stack_resources", - kwargs={"StackName": self.stack.external_name} + kwargs={"StackName": self.stack.external_name}, ) return response.get("StackResources", []) diff --git a/sceptre/hooks/cmd.py b/sceptre/hooks/cmd.py index 4cf6a1f8b..dcba668c4 100644 --- a/sceptre/hooks/cmd.py +++ b/sceptre/hooks/cmd.py @@ -23,5 +23,5 @@ def run(self): except TypeError: raise InvalidHookArgumentTypeError( 'The argument "{0}" is the wrong type - cmd hooks require ' - 'arguments of type string.'.format(self.argument) + "arguments of type string.".format(self.argument) ) diff --git a/sceptre/plan/__init__.py b/sceptre/plan/__init__.py index 1656562d8..4f8bf5472 100644 --- a/sceptre/plan/__init__.py +++ b/sceptre/plan/__init__.py @@ -3,8 +3,8 @@ import logging -__author__ = 'Cloudreach' -__email__ = 'sceptre@cloudreach.com' +__author__ = "Cloudreach" +__email__ = "sceptre@cloudreach.com" # Set up logging to ``/dev/null`` like a library is supposed to. @@ -14,4 +14,4 @@ def emit(self, record): pass -logging.getLogger('sceptre').addHandler(NullHandler()) +logging.getLogger("sceptre").addHandler(NullHandler()) diff --git a/sceptre/plan/actions.py b/sceptre/plan/actions.py index 147de60a4..5bb490a4e 100644 --- a/sceptre/plan/actions.py +++ b/sceptre/plan/actions.py @@ -21,12 +21,14 @@ from sceptre.config.reader import ConfigReader from sceptre.connection_manager import ConnectionManager -from sceptre.exceptions import (CannotUpdateFailedStackError, - ProtectedStackError, StackDoesNotExistError, - UnknownStackChangeSetStatusError, - UnknownStackStatusError) -from sceptre.helpers import (extract_datetime_from_aws_response_headers, - normalise_path) +from sceptre.exceptions import ( + CannotUpdateFailedStackError, + ProtectedStackError, + StackDoesNotExistError, + UnknownStackChangeSetStatusError, + UnknownStackStatusError, +) +from sceptre.helpers import extract_datetime_from_aws_response_headers, normalise_path from sceptre.hooks import add_stack_hooks from sceptre.stack import Stack from sceptre.stack_status import StackChangeSetStatus, StackStatus @@ -49,9 +51,11 @@ def __init__(self, stack: Stack): self.name = self.stack.name self.logger = logging.getLogger(__name__) self.connection_manager = ConnectionManager( - self.stack.region, self.stack.profile, - self.stack.external_name, self.stack.iam_role, - self.stack.iam_role_session_duration + self.stack.region, + self.stack.profile, + self.stack.external_name, + self.stack.iam_role, + self.stack.iam_role_session_duration, ) @add_stack_hooks @@ -67,18 +71,20 @@ def create(self): create_stack_kwargs = { "StackName": self.stack.external_name, "Parameters": self._format_parameters(self.stack.parameters), - "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "NotificationARNs": self.stack.notifications, "Tags": [ - {"Key": str(k), "Value": str(v)} - for k, v in self.stack.tags.items() - ] + {"Key": str(k), "Value": str(v)} for k, v in self.stack.tags.items() + ], } if self.stack.on_failure: create_stack_kwargs.update({"OnFailure": self.stack.on_failure}) - create_stack_kwargs.update( - self.stack.template.get_boto_call_parameter()) + create_stack_kwargs.update(self.stack.template.get_boto_call_parameter()) create_stack_kwargs.update(self._get_role_arn()) create_stack_kwargs.update(self._get_stack_timeout()) @@ -86,7 +92,7 @@ def create(self): response = self.connection_manager.call( service="cloudformation", command="create_stack", - kwargs=create_stack_kwargs + kwargs=create_stack_kwargs, ) self.logger.debug( @@ -96,9 +102,7 @@ def create(self): status = self._wait_for_completion(boto_response=response) except botocore.exceptions.ClientError as exp: if exp.response["Error"]["Code"] == "AlreadyExistsException": - self.logger.info( - "%s - Stack already exists", self.stack.name - ) + self.logger.info("%s - Stack already exists", self.stack.name) status = StackStatus.COMPLETE else: @@ -121,25 +125,25 @@ def update(self): "StackName": self.stack.external_name, "Parameters": self._format_parameters(self.stack.parameters), "Capabilities": [ - 'CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND' + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", ], "NotificationARNs": self.stack.notifications, "Tags": [ - {"Key": str(k), "Value": str(v)} - for k, v in self.stack.tags.items() - ] + {"Key": str(k), "Value": str(v)} for k, v in self.stack.tags.items() + ], } - update_stack_kwargs.update( - self.stack.template.get_boto_call_parameter()) + update_stack_kwargs.update(self.stack.template.get_boto_call_parameter()) update_stack_kwargs.update(self._get_role_arn()) response = self.connection_manager.call( service="cloudformation", command="update_stack", - kwargs=update_stack_kwargs + kwargs=update_stack_kwargs, + ) + status = self._wait_for_completion( + self.stack.stack_timeout, boto_response=response ) - status = self._wait_for_completion(self.stack.stack_timeout, boto_response=response) self.logger.debug( "%s - Update Stack response: %s", self.stack.name, response ) @@ -152,9 +156,7 @@ def update(self): except botocore.exceptions.ClientError as exp: error_message = exp.response["Error"]["Message"] if error_message == "No updates are to be performed.": - self.logger.info( - "%s - No updates to perform.", self.stack.name - ) + self.logger.info("%s - No updates to perform.", self.stack.name) return StackStatus.COMPLETE else: raise @@ -167,13 +169,12 @@ def cancel_stack_update(self): :rtype: sceptre.stack_status.StackStatus """ self.logger.warning( - "%s - Update Stack time exceeded the specified timeout", - self.stack.name + "%s - Update Stack time exceeded the specified timeout", self.stack.name ) response = self.connection_manager.call( service="cloudformation", command="cancel_update_stack", - kwargs={"StackName": self.stack.external_name} + kwargs={"StackName": self.stack.external_name}, ) self.logger.debug( "%s - Cancel update Stack response: %s", self.stack.name, response @@ -214,7 +215,8 @@ def launch(self) -> StackStatus: elif existing_status.endswith("IN_PROGRESS"): self.logger.info( "%s - Stack action is already in progress state and cannot " - "be updated", self.stack.name + "be updated", + self.stack.name, ) status = StackStatus.IN_PROGRESS elif existing_status.endswith("FAILED"): @@ -224,9 +226,7 @@ def launch(self) -> StackStatus: ) ) else: - raise UnknownStackStatusError( - "{0} is unknown".format(existing_status) - ) + raise UnknownStackStatusError("{0} is unknown".format(existing_status)) return status @add_stack_hooks @@ -250,9 +250,7 @@ def delete(self): delete_stack_kwargs = {"StackName": self.stack.external_name} delete_stack_kwargs.update(self._get_role_arn()) response = self.connection_manager.call( - service="cloudformation", - command="delete_stack", - kwargs=delete_stack_kwargs + service="cloudformation", command="delete_stack", kwargs=delete_stack_kwargs ) try: @@ -275,7 +273,7 @@ def lock(self): # need to get to the base install path. __file__ will take us into # sceptre/actions so need to walk up the path. path.abspath(path.join(__file__, "..", "..")), - "stack_policies/lock.json" + "stack_policies/lock.json", ) self.set_policy(policy_path) self.logger.info("%s - Successfully locked Stack", self.stack.name) @@ -288,7 +286,7 @@ def unlock(self): # need to get to the base install path. __file__ will take us into # sceptre/actions so need to walk up the path. path.abspath(path.join(__file__, "..", "..")), - "stack_policies/unlock.json" + "stack_policies/unlock.json", ) self.set_policy(policy_path) self.logger.info("%s - Successfully unlocked Stack", self.stack.name) @@ -304,7 +302,7 @@ def describe(self): return self.connection_manager.call( service="cloudformation", command="describe_stacks", - kwargs={"StackName": self.stack.external_name} + kwargs={"StackName": self.stack.external_name}, ) except botocore.exceptions.ClientError as e: if e.response["Error"]["Message"].endswith("does not exist"): @@ -321,7 +319,7 @@ def describe_events(self): return self.connection_manager.call( service="cloudformation", command="describe_stack_events", - kwargs={"StackName": self.stack.external_name} + kwargs={"StackName": self.stack.external_name}, ) def describe_resources(self): @@ -336,7 +334,7 @@ def describe_resources(self): response = self.connection_manager.call( service="cloudformation", command="describe_stack_resources", - kwargs={"StackName": self.stack.external_name} + kwargs={"StackName": self.stack.external_name}, ) except botocore.exceptions.ClientError as e: if e.response["Error"]["Message"].endswith("does not exist"): @@ -344,17 +342,17 @@ def describe_resources(self): raise self.logger.debug( - "%s - Describe Stack resource response: %s", - self.stack.name, - response + "%s - Describe Stack resource response: %s", self.stack.name, response ) desired_properties = ["LogicalResourceId", "PhysicalResourceId"] - formatted_response = {self.stack.name: [ - {k: v for k, v in item.items() if k in desired_properties} - for item in response["StackResources"] - ]} + formatted_response = { + self.stack.name: [ + {k: v for k, v in item.items() if k in desired_properties} + for item in response["StackResources"] + ] + } return formatted_response def describe_outputs(self): @@ -379,18 +377,16 @@ def continue_update_rollback(self): UPDATE_ROLLBACK_COMPLETE. """ self.logger.debug("%s - Continuing update rollback", self.stack.name) - continue_update_rollback_kwargs = { - "StackName": self.stack.external_name - } + continue_update_rollback_kwargs = {"StackName": self.stack.external_name} continue_update_rollback_kwargs.update(self._get_role_arn()) self.connection_manager.call( service="cloudformation", command="continue_update_rollback", - kwargs=continue_update_rollback_kwargs + kwargs=continue_update_rollback_kwargs, ) self.logger.info( "%s - Successfully initiated continuation of update rollback", - self.stack.name + self.stack.name, ) def set_policy(self, policy_path): @@ -404,19 +400,12 @@ def set_policy(self, policy_path): with open(policy_path) as f: policy = f.read() - self.logger.debug( - "%s - Setting Stack policy: \n%s", - self.stack.name, - policy - ) + self.logger.debug("%s - Setting Stack policy: \n%s", self.stack.name, policy) self.connection_manager.call( service="cloudformation", command="set_stack_policy", - kwargs={ - "StackName": self.stack.external_name, - "StackPolicyBody": policy - } + kwargs={"StackName": self.stack.external_name, "StackPolicyBody": policy}, ) self.logger.info("%s - Successfully set Stack Policy", self.stack.name) @@ -431,12 +420,11 @@ def get_policy(self): response = self.connection_manager.call( service="cloudformation", command="get_stack_policy", - kwargs={ - "StackName": self.stack.external_name - } + kwargs={"StackName": self.stack.external_name}, + ) + json_formatting = json.loads( + response.get("StackPolicyBody", json.dumps("No Policy Information")) ) - json_formatting = json.loads(response.get( - "StackPolicyBody", json.dumps("No Policy Information"))) return {self.stack.name: json_formatting} @add_stack_hooks @@ -450,17 +438,18 @@ def create_change_set(self, change_set_name): create_change_set_kwargs = { "StackName": self.stack.external_name, "Parameters": self._format_parameters(self.stack.parameters), - "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "ChangeSetName": change_set_name, "NotificationARNs": self.stack.notifications, "Tags": [ - {"Key": str(k), "Value": str(v)} - for k, v in self.stack.tags.items() - ] + {"Key": str(k), "Value": str(v)} for k, v in self.stack.tags.items() + ], } - create_change_set_kwargs.update( - self.stack.template.get_boto_call_parameter() - ) + create_change_set_kwargs.update(self.stack.template.get_boto_call_parameter()) create_change_set_kwargs.update(self._get_role_arn()) self.logger.debug( "%s - Creating Change Set '%s'", self.stack.name, change_set_name @@ -468,13 +457,14 @@ def create_change_set(self, change_set_name): self.connection_manager.call( service="cloudformation", command="create_change_set", - kwargs=create_change_set_kwargs + kwargs=create_change_set_kwargs, ) # After the call successfully completes, AWS CloudFormation # starts creating the Change Set. self.logger.info( "%s - Successfully initiated creation of Change Set '%s'", - self.stack.name, change_set_name + self.stack.name, + change_set_name, ) def delete_change_set(self, change_set_name): @@ -492,14 +482,15 @@ def delete_change_set(self, change_set_name): command="delete_change_set", kwargs={ "ChangeSetName": change_set_name, - "StackName": self.stack.external_name - } + "StackName": self.stack.external_name, + }, ) # If the call successfully completes, AWS CloudFormation # successfully deleted the Change Set. self.logger.info( "%s - Successfully deleted Change Set '%s'", - self.stack.name, change_set_name + self.stack.name, + change_set_name, ) def describe_change_set(self, change_set_name): @@ -519,8 +510,8 @@ def describe_change_set(self, change_set_name): command="describe_change_set", kwargs={ "ChangeSetName": change_set_name, - "StackName": self.stack.external_name - } + "StackName": self.stack.external_name, + }, ) def execute_change_set(self, change_set_name): @@ -536,11 +527,13 @@ def execute_change_set(self, change_set_name): change_set = self.describe_change_set(change_set_name) status = change_set.get("Status") reason = change_set.get("StatusReason") - if status == "FAILED" and self.change_set_creation_failed_due_to_no_changes(reason): + if status == "FAILED" and self.change_set_creation_failed_due_to_no_changes( + reason + ): self.logger.info( - "Skipping ChangeSet on Stack: {} - there are no changes".format( - change_set.get("StackName") - ) + "Skipping ChangeSet on Stack: {} - there are no changes".format( + change_set.get("StackName") + ) ) return 0 @@ -552,8 +545,8 @@ def execute_change_set(self, change_set_name): command="execute_change_set", kwargs={ "ChangeSetName": change_set_name, - "StackName": self.stack.external_name - } + "StackName": self.stack.external_name, + }, ) status = self._wait_for_completion(boto_response=response) return status @@ -567,7 +560,7 @@ def change_set_creation_failed_due_to_no_changes(self, reason: str) -> bool: reason = reason.lower() no_change_substrings = ( "submitted information didn't contain changes", - "no updates are to be performed" # The reason returned for SAM templates + "no updates are to be performed", # The reason returned for SAM templates ) for substring in no_change_substrings: @@ -599,9 +592,7 @@ def _list_change_sets(self): return self.connection_manager.call( service="cloudformation", command="list_change_sets", - kwargs={ - "StackName": self.stack.external_name - } + kwargs={"StackName": self.stack.external_name}, ) except botocore.exceptions.ClientError: return [] @@ -618,10 +609,9 @@ def _convert_to_url(self, summaries): change_set_id = summary["ChangeSetId"] region = self.stack.region - encoded = urllib.parse.urlencode({ - "stackId": stack_id, - "changeSetId": change_set_id - }) + encoded = urllib.parse.urlencode( + {"stackId": stack_id, "changeSetId": change_set_id} + ) new_summaries.append( f"https://{region}.console.aws.amazon.com/cloudformation/home?" @@ -652,7 +642,7 @@ def validate(self): response = self.connection_manager.call( service="cloudformation", command="validate_template", - kwargs=self.stack.template.get_boto_call_parameter() + kwargs=self.stack.template.get_boto_call_parameter(), ) self.logger.debug( "%s - Validate Template response: %s", self.stack.name, response @@ -670,17 +660,15 @@ def estimate_cost(self): self.logger.debug("%s - Estimating template cost", self.stack.name) parameters = [ - {'ParameterKey': key, 'ParameterValue': value} + {"ParameterKey": key, "ParameterValue": value} for key, value in self.stack.parameters.items() ] kwargs = self.stack.template.get_boto_call_parameter() - kwargs.update({'Parameters': parameters}) + kwargs.update({"Parameters": parameters}) response = self.connection_manager.call( - service="cloudformation", - command="estimate_template_cost", - kwargs=kwargs + service="cloudformation", command="estimate_template_cost", kwargs=kwargs ) self.logger.debug( "%s - Estimate Stack cost response: %s", self.stack.name, response @@ -714,10 +702,7 @@ def _format_parameters(self, parameters): continue if isinstance(value, list): value = ",".join(value) - formatted_parameters.append({ - "ParameterKey": name, - "ParameterValue": value - }) + formatted_parameters.append({"ParameterKey": name, "ParameterValue": value}) return formatted_parameters @@ -731,9 +716,7 @@ def _get_role_arn(self): :rtype: dict """ if self.stack.role_arn: - return { - "RoleARN": self.stack.role_arn - } + return {"RoleARN": self.stack.role_arn} else: return {} @@ -746,9 +729,7 @@ def _get_stack_timeout(self): :rtype: dict """ if self.stack.stack_timeout: - return { - "TimeoutInMinutes": self.stack.stack_timeout - } + return {"TimeoutInMinutes": self.stack.stack_timeout} else: return {} @@ -764,7 +745,9 @@ def _protect_execution(self): "currently enabled".format(self.stack.name) ) - def _wait_for_completion(self, timeout=0, boto_response: Optional[dict] = None) -> StackStatus: + def _wait_for_completion( + self, timeout=0, boto_response: Optional[dict] = None + ) -> StackStatus: """ Waits for a Stack operation to finish. Prints CloudFormation events while it waits. @@ -788,7 +771,9 @@ def timed_out(elapsed): elapsed = 0 while status == StackStatus.IN_PROGRESS and not timed_out(elapsed): status = self._get_simplified_status(self._get_status()) - most_recent_event_datetime = self._log_new_events(most_recent_event_datetime) + most_recent_event_datetime = self._log_new_events( + most_recent_event_datetime + ) time.sleep(4) elapsed += 4 @@ -798,7 +783,7 @@ def _describe(self): return self.connection_manager.call( service="cloudformation", command="describe_stacks", - kwargs={"StackName": self.stack.external_name} + kwargs={"StackName": self.stack.external_name}, ) def _get_status(self): @@ -837,9 +822,7 @@ def _get_simplified_status(status): elif status.endswith("_FAILED"): return StackStatus.FAILED else: - raise UnknownStackStatusError( - "{0} is unknown".format(status) - ) + raise UnknownStackStatusError("{0} is unknown".format(status)) def _log_new_events(self, after_datetime: datetime) -> datetime: """ @@ -850,18 +833,19 @@ def _log_new_events(self, after_datetime: datetime) -> datetime: """ events = self.describe_events()["StackEvents"] events.reverse() - new_events = [ - event for event in events - if event["Timestamp"] > after_datetime - ] + new_events = [event for event in events if event["Timestamp"] > after_datetime] for event in new_events: - self.logger.info(" ".join([ - self.stack.name, - event["LogicalResourceId"], - event["ResourceType"], - event["ResourceStatus"], - event.get("ResourceStatusReason", "") - ])) + self.logger.info( + " ".join( + [ + self.stack.name, + event["LogicalResourceId"], + event["ResourceType"], + event["ResourceStatus"], + event.get("ResourceStatusReason", ""), + ] + ) + ) after_datetime = event["Timestamp"] return after_datetime @@ -896,12 +880,19 @@ def _get_cs_status(self, change_set_name): cs_status = cs_description["Status"] cs_exec_status = cs_description["ExecutionStatus"] possible_statuses = [ - "CREATE_PENDING", "CREATE_IN_PROGRESS", - "CREATE_COMPLETE", "DELETE_COMPLETE", "FAILED" + "CREATE_PENDING", + "CREATE_IN_PROGRESS", + "CREATE_COMPLETE", + "DELETE_COMPLETE", + "FAILED", ] possible_execution_statuses = [ - "UNAVAILABLE", "AVAILABLE", "EXECUTE_IN_PROGRESS", - "EXECUTE_COMPLETE", "EXECUTE_FAILED", "OBSOLETE" + "UNAVAILABLE", + "AVAILABLE", + "EXECUTE_IN_PROGRESS", + "EXECUTE_COMPLETE", + "EXECUTE_FAILED", + "OBSOLETE", ] if cs_status not in possible_statuses: @@ -913,25 +904,20 @@ def _get_cs_status(self, change_set_name): "ExecutionStatus {0} is unknown".format(cs_status) ) - if ( - cs_status == "CREATE_COMPLETE" and - cs_exec_status == "AVAILABLE" - ): + if cs_status == "CREATE_COMPLETE" and cs_exec_status == "AVAILABLE": return StackChangeSetStatus.READY - elif ( - cs_status in [ - "CREATE_PENDING", "CREATE_IN_PROGRESS", "CREATE_COMPLETE" - ] and - cs_exec_status in ["UNAVAILABLE", "AVAILABLE"] - ): + elif cs_status in [ + "CREATE_PENDING", + "CREATE_IN_PROGRESS", + "CREATE_COMPLETE", + ] and cs_exec_status in ["UNAVAILABLE", "AVAILABLE"]: return StackChangeSetStatus.PENDING - elif ( - cs_status in ["DELETE_COMPLETE", "FAILED"] or - cs_exec_status in [ - "EXECUTE_IN_PROGRESS", "EXECUTE_COMPLETE", - "EXECUTE_FAILED", "OBSOLETE" - ] - ): + elif cs_status in ["DELETE_COMPLETE", "FAILED"] or cs_exec_status in [ + "EXECUTE_IN_PROGRESS", + "EXECUTE_COMPLETE", + "EXECUTE_FAILED", + "OBSOLETE", + ]: return StackChangeSetStatus.DEFUNCT else: # pragma: no cover raise Exception("This else should not be reachable.") @@ -962,14 +948,14 @@ def _fetch_original_template_stage(self) -> Optional[Union[str, dict]]: command="get_template", kwargs={ "StackName": self.stack.external_name, - "TemplateStage": 'Original' - } + "TemplateStage": "Original", + }, ) - return response['TemplateBody'] + return response["TemplateBody"] # Sometimes boto returns a string, sometimes a dictionary except botocore.exceptions.ClientError as e: # AWS returns a ValidationError if the stack doesn't exist - if e.response['Error']['Code'] == 'ValidationError': + if e.response["Error"]["Code"] == "ValidationError": return None raise @@ -983,16 +969,14 @@ def fetch_local_template_summary(self): def _get_template_summary(self, **kwargs) -> Optional[dict]: try: template_summary = self.connection_manager.call( - service='cloudformation', - command='get_template_summary', - kwargs=kwargs + service="cloudformation", command="get_template_summary", kwargs=kwargs ) return template_summary except botocore.exceptions.ClientError as e: - error_response = e.response['Error'] + error_response = e.response["Error"] if ( - error_response['Code'] == 'ValidationError' - and 'does not exist' in error_response['Message'] + error_response["Code"] == "ValidationError" + and "does not exist" in error_response["Message"] ): return None raise @@ -1025,7 +1009,7 @@ def drift_detect(self) -> Dict[str, str]: self.logger.info(f"{self.stack.name} - Does not exist.") return { "DetectionStatus": "STACK_DOES_NOT_EXIST", - "StackDriftStatus": "STACK_DOES_NOT_EXIST" + "StackDriftStatus": "STACK_DOES_NOT_EXIST", } response = self._detect_stack_drift() @@ -1035,10 +1019,7 @@ def drift_detect(self) -> Dict[str, str]: response = self._wait_for_drift_status(detection_id) except TimeoutError as exc: self.logger.info(f"{self.stack.name} - {exc}") - response = { - "DetectionStatus": "TIMED_OUT", - "StackDriftStatus": "TIMED_OUT" - } + response = {"DetectionStatus": "TIMED_OUT", "StackDriftStatus": "TIMED_OUT"} return response @@ -1099,14 +1080,12 @@ def _log_drift_status(self, response: dict) -> None: "StackDriftDetectionId", "DetectionStatus", "DetectionStatusReason", - "StackDriftStatus" + "StackDriftStatus", ] for key in keys: if key in response: - self.logger.debug( - f"{self.stack.name} - {key} - {response[key]}" - ) + self.logger.debug(f"{self.stack.name} - {key} - {response[key]}") def _detect_stack_drift(self) -> dict: """ @@ -1117,9 +1096,7 @@ def _detect_stack_drift(self) -> dict: return self.connection_manager.call( service="cloudformation", command="detect_stack_drift", - kwargs={ - "StackName": self.stack.external_name - } + kwargs={"StackName": self.stack.external_name}, ) def _describe_stack_drift_detection_status(self, detection_id: str) -> dict: @@ -1131,9 +1108,7 @@ def _describe_stack_drift_detection_status(self, detection_id: str) -> dict: return self.connection_manager.call( service="cloudformation", command="describe_stack_drift_detection_status", - kwargs={ - "StackDriftDetectionId": detection_id - } + kwargs={"StackDriftDetectionId": detection_id}, ) def _describe_stack_resource_drifts(self) -> dict: @@ -1145,9 +1120,7 @@ def _describe_stack_resource_drifts(self) -> dict: return self.connection_manager.call( service="cloudformation", command="describe_stack_resource_drifts", - kwargs={ - "StackName": self.stack.external_name - } + kwargs={"StackName": self.stack.external_name}, ) def _filter_drifts(self, response: dict, drifted: bool) -> dict: diff --git a/sceptre/plan/executor.py b/sceptre/plan/executor.py index 0789213a4..501edf14c 100644 --- a/sceptre/plan/executor.py +++ b/sceptre/plan/executor.py @@ -15,7 +15,6 @@ class SceptrePlanExecutor(object): - def __init__(self, command: str, launch_order: List[Set[Stack]]): """ Initialises a SceptrePlanExecutor, generates the launch order, threads @@ -45,8 +44,9 @@ def execute(self, *args): with ThreadPoolExecutor(max_workers=self.num_threads) as executor: for batch in self.launch_order: - futures = [executor.submit(self._execute, stack, *args) - for stack in batch] + futures = [ + executor.submit(self._execute, stack, *args) for stack in batch + ] for future in as_completed(futures): stack, status = future.result() diff --git a/sceptre/plan/plan.py b/sceptre/plan/plan.py index 7fafbaa4a..7b40dff1b 100644 --- a/sceptre/plan/plan.py +++ b/sceptre/plan/plan.py @@ -32,7 +32,6 @@ def wrapped(self: "SceptrePlan", *args, **kwargs): class SceptrePlan(object): - def __init__(self, context: SceptreContext): """ Intialises a SceptrePlan and generates the Stacks, StackGraph and @@ -74,8 +73,10 @@ def _generate_launch_order(self, reverse=False) -> List[Set[Stack]]: if not launch_order: raise ConfigFileNotFoundError( - "No stacks detected from the given path '{}'. Valid stack paths are: {}" - .format(sceptreise_path(self.context.command_path), self._valid_stack_paths()) + "No stacks detected from the given path '{}'. Valid stack paths are: {}".format( + sceptreise_path(self.context.command_path), + self._valid_stack_paths(), + ) ) return launch_order @@ -245,7 +246,7 @@ def continue_update_rollback(self, *args): :returns: A dictionary of Stacks :rtype: dict - """ + """ self.resolve(command=self.continue_update_rollback.__name__) return self._execute(*args) @@ -388,7 +389,9 @@ def generate(self, *args): def _valid_stack_paths(self): return [ - sceptreise_path(path.relpath(path.join(dirpath, f), self.context.config_path)) + sceptreise_path( + path.relpath(path.join(dirpath, f), self.context.config_path) + ) for dirpath, dirnames, files in walk(self.context.config_path) for f in files if not f.endswith(self.context.config_file) diff --git a/sceptre/resolvers/__init__.py b/sceptre/resolvers/__init__.py index 1515d61f4..2a895cd1a 100644 --- a/sceptre/resolvers/__init__.py +++ b/sceptre/resolvers/__init__.py @@ -8,13 +8,14 @@ from sceptre.helpers import _call_func_on_values from sceptre.resolvers.placeholders import ( create_placeholder_value, - are_placeholders_enabled, PlaceholderType + are_placeholders_enabled, + PlaceholderType, ) if TYPE_CHECKING: from sceptre import stack -T_Container = TypeVar('T_Container', bound=Union[dict, list]) +T_Container = TypeVar("T_Container", bound=Union[dict, list]) class RecursiveResolve(Exception): @@ -30,7 +31,7 @@ class Resolver(abc.ABC): :param stack: The associated stack of the resolver. """ - def __init__(self, argument: Any = None, stack: 'stack.Stack' = None): + def __init__(self, argument: Any = None, stack: "stack.Stack" = None): self.logger = logging.getLogger(__name__) self.argument = argument self.stack = stack @@ -53,7 +54,7 @@ def resolve(self): """ pass # pragma: no cover - def clone(self, stack: 'stack.Stack') -> 'Resolver': + def clone(self, stack: "stack.Stack") -> "Resolver": """ Produces a "fresh" copy of the Resolver, with the specified stack. @@ -79,7 +80,7 @@ def __init__(self, name: str, placeholder_type=PlaceholderType.explicit): self._lock = RLock() - def __get__(self, stack: 'stack.Stack', stack_class: Type['stack.Stack']) -> Any: + def __get__(self, stack: "stack.Stack", stack_class: Type["stack.Stack"]) -> Any: """ Attribute getter which resolves the resolver(s). @@ -92,7 +93,7 @@ def __get__(self, stack: 'stack.Stack', stack_class: Type['stack.Stack']) -> Any if hasattr(stack, self.name): return self.get_resolved_value(stack, stack_class) - def __set__(self, stack: 'stack.Stack', value: Any): + def __set__(self, stack: "stack.Stack", value: Any): """ Attribute setter which adds a stack reference to any resolvers in the data structure `value` and calls the setup method. @@ -104,23 +105,27 @@ def __set__(self, stack: 'stack.Stack', value: Any): self.assign_value_to_stack(stack, value) @contextmanager - def _no_recursive_get(self, stack: 'stack.Stack'): + def _no_recursive_get(self, stack: "stack.Stack"): # We don't care about recursive gets on the same property but different Stack instances, # only recursive gets on the same stack. Some Resolvers access the same property on OTHER # stacks and that actually shouldn't be a problem. Remember, these descriptor instances are # set on the CLASS and so instance variables on them are shared across all classes that # access them. Thus, we set this "get_in_progress" attribute on the stack instance rather # than the descriptor instance. - get_status_name = f'_{self.name}_get_in_progress' + get_status_name = f"_{self.name}_get_in_progress" if getattr(stack, get_status_name, False): - raise RecursiveResolve(f"Resolving Stack.{self.name[1:]} required resolving itself") + raise RecursiveResolve( + f"Resolving Stack.{self.name[1:]} required resolving itself" + ) setattr(stack, get_status_name, True) try: yield finally: setattr(stack, get_status_name, False) - def get_setup_resolver_for_stack(self, stack: 'stack.Stack', resolver: Resolver) -> Resolver: + def get_setup_resolver_for_stack( + self, stack: "stack.Stack", resolver: Resolver + ) -> Resolver: """Obtains a clone of the resolver with the stack set on it and the setup method having been called on it. @@ -136,16 +141,18 @@ def get_setup_resolver_for_stack(self, stack: 'stack.Stack', resolver: Resolver) return clone @abc.abstractmethod - def get_resolved_value(self, stack: 'stack.Stack', stack_class: Type['stack.Stack']) -> Any: + def get_resolved_value( + self, stack: "stack.Stack", stack_class: Type["stack.Stack"] + ) -> Any: """Implement this method to return the value of the resolvable_property.""" pass @abc.abstractmethod - def assign_value_to_stack(self, stack: 'stack.Stack', value: Any): + def assign_value_to_stack(self, stack: "stack.Stack", value: Any): """Implement this method to assign the value to the resolvable property.""" pass - def resolve_resolver_value(self, resolver: 'Resolver') -> Any: + def resolve_resolver_value(self, resolver: "Resolver") -> Any: """Returns the resolved parameter value. If the resolver happens to raise an error and placeholders are currently allowed for resolvers, @@ -161,7 +168,9 @@ def resolve_resolver_value(self, resolver: 'Resolver') -> Any: raise except Exception: if are_placeholders_enabled(): - placeholder_value = create_placeholder_value(resolver, self.placeholder_type) + placeholder_value = create_placeholder_value( + resolver, self.placeholder_type + ) self.logger.debug( "Error encountered while resolving the resolver. This is allowed for the current " @@ -171,7 +180,7 @@ def resolve_resolver_value(self, resolver: 'Resolver') -> Any: raise def __repr__(self) -> str: - return f'<{self.__class__.__name__}({self.name[1:]})>' + return f"<{self.__class__.__name__}({self.name[1:]})>" class ResolvableContainerProperty(ResolvableProperty): @@ -187,7 +196,9 @@ class ResolvableContainerProperty(ResolvableProperty): :type name: str """ - def __get__(self, stack: 'stack.Stack', stack_class: Type['stack.Stack']) -> T_Container: + def __get__( + self, stack: "stack.Stack", stack_class: Type["stack.Stack"] + ) -> T_Container: container = super().__get__(stack, stack_class) with self._lock: @@ -196,7 +207,9 @@ def __get__(self, stack: 'stack.Stack', stack_class: Type['stack.Stack']) -> T_C return container - def get_resolved_value(self, stack: 'stack.Stack', stack_class: Type['stack.Stack']) -> T_Container: + def get_resolved_value( + self, stack: "stack.Stack", stack_class: Type["stack.Stack"] + ) -> T_Container: """Obtains the resolved value for this property. Any resolvers that resolve to None will have their key/index removed from their dict/list where they are. Other resolvers will have their key/index's value replace with the resolved value to avoid redundant resolutions. @@ -212,7 +225,9 @@ def resolve(attr: Union[dict, list], key: Union[int, str], value: Resolver): try: result = self.resolve_resolver_value(value) if result is None: - self.logger.debug(f"Removing item {key} because resolver returned None.") + self.logger.debug( + f"Removing item {key} because resolver returned None." + ) # We gather up resolvers (and their immediate containers) that resolve to None, # since that really means the resolver resolves to nothing. This is not common, # but should be supported. We gather these rather than immediately remove them @@ -234,9 +249,7 @@ def resolve(attr: Union[dict, list], key: Union[int, str], value: Resolver): ) container = getattr(stack, self.name) - _call_func_on_values( - resolve, container, Resolver - ) + _call_func_on_values(resolve, container, Resolver) # Remove keys and indexes from their containers that had resolvers resolve to None. list_items_to_delete = [] for attr, key in keys_to_delete: @@ -254,7 +267,7 @@ def resolve(attr: Union[dict, list], key: Union[int, str], value: Resolver): return container - def assign_value_to_stack(self, stack: 'stack.Stack', value: Union[dict, list]): + def assign_value_to_stack(self, stack: "stack.Stack", value: Union[dict, list]): """Assigns a COPY of the specified value to the stack instance. This method copies the value rather than directly assigns it to avoid bugs related to shared objects in memory. @@ -265,9 +278,7 @@ def assign_value_to_stack(self, stack: 'stack.Stack', value: Union[dict, list]): setattr(stack, self.name, cloned) def _clone_container_with_resolvers( - self, - container: T_Container, - stack: 'stack.Stack' + self, container: T_Container, stack: "stack.Stack" ) -> T_Container: """Recurses into the container, cloning and setting up resolvers and creating a copy of all nested containers. @@ -276,24 +287,19 @@ def _clone_container_with_resolvers( :param stack: The stack the container is being copied for :return: The fully copied container with resolvers fully set up. """ + def recurse(obj): if isinstance(obj, Resolver): return self.get_setup_resolver_for_stack(stack, obj) if isinstance(obj, list): - return [ - recurse(item) - for item in obj - ] + return [recurse(item) for item in obj] elif isinstance(obj, dict): - return { - key: recurse(val) - for key, val in obj.items() - } + return {key: recurse(val) for key, val in obj.items()} return obj return recurse(container) - def _resolve_deferred_resolvers(self, stack: 'stack.Stack', container: T_Container): + def _resolve_deferred_resolvers(self, stack: "stack.Stack", container: T_Container): def raise_if_not_resolved(attr, key, value): # If this function has been hit, it means that after attempting to resolve all the # ResolveLaters, there STILL are ResolveLaters left in the container. Rather than @@ -301,25 +307,21 @@ def raise_if_not_resolved(attr, key, value): # break that infinite loop. This situation would happen if a resolver accesses a resolver # in the same container, which then accesses another resolver (possibly the same one) in # the same container. - raise RecursiveResolve(f"Resolving Stack.{self.name[1:]} required resolving itself") + raise RecursiveResolve( + f"Resolving Stack.{self.name[1:]} required resolving itself" + ) - has_been_resolved_attr_name = f'{self.name}_is_resolved' + has_been_resolved_attr_name = f"{self.name}_is_resolved" if not getattr(stack, has_been_resolved_attr_name, False): # We set it first rather than after to avoid entering this block again on this property # for this stack. setattr(stack, has_been_resolved_attr_name, True) _call_func_on_values( - lambda attr, key, value: value(), - container, - self.ResolveLater + lambda attr, key, value: value(), container, self.ResolveLater ) # Search the container to see if there are any ResolveLaters left; # Raise a RecursiveResolve if there are. - _call_func_on_values( - raise_if_not_resolved, - container, - self.ResolveLater - ) + _call_func_on_values(raise_if_not_resolved, container, self.ResolveLater) class ResolveLater: """Represents a value that could not yet be resolved but can be resolved in the future.""" @@ -335,7 +337,9 @@ def __call__(self): attr = getattr(self._instance, self._name) result = self._resolution_function() if result is None: - self.logger.debug(f"Removing item {self._key} because resolver returned None.") + self.logger.debug( + f"Removing item {self._key} because resolver returned None." + ) del attr[self._key] else: attr[self._key] = result @@ -352,7 +356,9 @@ class ResolvableValueProperty(ResolvableProperty): :type name: str """ - def get_resolved_value(self, stack: 'stack.Stack', stack_class: Type['stack.Stack']) -> Any: + def get_resolved_value( + self, stack: "stack.Stack", stack_class: Type["stack.Stack"] + ) -> Any: """Gets the fully-resolved value from the property. Resolvers will be replaced on the stack instance with their resolved value to avoid redundant resolutions. @@ -371,7 +377,7 @@ def get_resolved_value(self, stack: 'stack.Stack', stack_class: Type['stack.Stac return value - def assign_value_to_stack(self, stack: 'stack.Stack', value: Any): + def assign_value_to_stack(self, stack: "stack.Stack", value: Any): """Assigns the value to the Stack instance passed, setting up and cloning the value if it is a Resolver. diff --git a/sceptre/resolvers/placeholders.py b/sceptre/resolvers/placeholders.py index d84fc1bfc..f93afa3f3 100644 --- a/sceptre/resolvers/placeholders.py +++ b/sceptre/resolvers/placeholders.py @@ -46,12 +46,14 @@ def are_placeholders_enabled() -> bool: return _RESOLVE_PLACEHOLDER_ON_ERROR -def create_placeholder_value(resolver: 'resolvers.Resolver', placeholder_type: PlaceholderType) -> Any: +def create_placeholder_value( + resolver: "resolvers.Resolver", placeholder_type: PlaceholderType +) -> Any: placeholder_func = _placeholders[placeholder_type] return placeholder_func(resolver) -def _create_explicit_resolver_placeholder(resolver: 'resolvers.Resolver') -> str: +def _create_explicit_resolver_placeholder(resolver: "resolvers.Resolver") -> str: """Creates a placeholder value to be substituted for the resolved value when placeholders are allowed and the value cannot be resolved. @@ -63,13 +65,13 @@ def _create_explicit_resolver_placeholder(resolver: 'resolvers.Resolver') -> str :param resolver: The resolver to create a placeholder for :return: The placeholder value """ - base = f'!{type(resolver).__name__}' - suffix = f'({resolver.argument})' if resolver.argument is not None else '' + base = f"!{type(resolver).__name__}" + suffix = f"({resolver.argument})" if resolver.argument is not None else "" # double-braces in an f-string is just an escaped single brace - return f'{{ {base}{suffix} }}' + return f"{{ {base}{suffix} }}" -def _create_alphanumeric_placeholder(resolver: 'resolvers.Resolver') -> str: +def _create_alphanumeric_placeholder(resolver: "resolvers.Resolver") -> str: """Creates a placeholder value that is only composed of alphanumeric characters. This is more useful when performing operations that send a template to CloudFormation, which will have stricter requirements for values in templates. @@ -86,12 +88,12 @@ def _create_alphanumeric_placeholder(resolver: 'resolvers.Resolver') -> str: :return: The placeholder value """ explicit_placeholder = _create_explicit_resolver_placeholder(resolver) - alphanum_placeholder = ''.join(c for c in explicit_placeholder if c.isalnum()) + alphanum_placeholder = "".join(c for c in explicit_placeholder if c.isalnum()) return alphanum_placeholder _placeholders = { PlaceholderType.explicit: _create_explicit_resolver_placeholder, PlaceholderType.alphanum: _create_alphanumeric_placeholder, - PlaceholderType.none: lambda resolver: None + PlaceholderType.none: lambda resolver: None, } diff --git a/sceptre/resolvers/stack_attr.py b/sceptre/resolvers/stack_attr.py index 3d717463c..d620e6aae 100644 --- a/sceptre/resolvers/stack_attr.py +++ b/sceptre/resolvers/stack_attr.py @@ -22,15 +22,15 @@ class StackAttr(Resolver): # These are all the attributes on Stack Configs whose names are changed when they are assigned # to the Stack instance. STACK_ATTR_MAP = { - 'template': 'template_handler_config', - 'protect': 'protected', - 'stack_name': 'external_name', - 'stack_tags': 'tags' + "template": "template_handler_config", + "protect": "protected", + "stack_name": "external_name", + "stack_tags": "tags", } def resolve(self) -> Any: """Returns the resolved value of the field referenced by the resolver's argument.""" - segments = self.argument.split('.') + segments = self.argument.split(".") # Remap top-level attributes to match stack config first_segment = segments[0] diff --git a/sceptre/resolvers/stack_output.py b/sceptre/resolvers/stack_output.py index 9ed350787..2dedcdaa8 100644 --- a/sceptre/resolvers/stack_output.py +++ b/sceptre/resolvers/stack_output.py @@ -25,7 +25,9 @@ def __init__(self, *args, **kwargs): self.logger = logging.getLogger(__name__) super(StackOutputBase, self).__init__(*args, **kwargs) - def _get_output_value(self, stack_name, output_key, profile=None, region=None, iam_role=None): + def _get_output_value( + self, stack_name, output_key, profile=None, region=None, iam_role=None + ): """ Attempts to get the Stack output named by ``output_key`` @@ -59,9 +61,7 @@ def _get_stack_outputs(self, stack_name, profile=None, region=None, iam_role=Non :rtype: dict :raises: sceptre.stack.DependencyStackNotLaunchedException """ - self.logger.debug("Collecting outputs from '{0}'...".format( - stack_name - )) + self.logger.debug("Collecting outputs from '{0}'...".format(stack_name)) connection_manager = self.stack.connection_manager try: @@ -72,7 +72,7 @@ def _get_stack_outputs(self, stack_name, profile=None, region=None, iam_role=Non profile=profile, region=region, stack_name=stack_name, - iam_role=iam_role + iam_role=iam_role, ) except ClientError as e: if "does not exist" in e.response["Error"]["Message"]: @@ -85,8 +85,7 @@ def _get_stack_outputs(self, stack_name, profile=None, region=None, iam_role=Non self.logger.debug("Outputs: {0}".format(outputs)) formatted_outputs = dict( - (output["OutputKey"], output["OutputValue"]) - for output in outputs + (output["OutputKey"], output["OutputValue"]) for output in outputs ) return formatted_outputs @@ -125,13 +124,22 @@ def resolve(self): friendly_stack_name = self.dependency_stack_name.replace(TEMPLATE_EXTENSION, "") stack = next( - stack for stack in self.stack.dependencies if stack.name == friendly_stack_name + stack + for stack in self.stack.dependencies + if stack.name == friendly_stack_name ) - stack_name = "-".join([stack.project_code, friendly_stack_name.replace("/", "-")]) + stack_name = "-".join( + [stack.project_code, friendly_stack_name.replace("/", "-")] + ) - return self._get_output_value(stack_name, self.output_key, profile=stack.profile, - region=stack.region, iam_role=stack.iam_role) + return self._get_output_value( + stack_name, + self.output_key, + profile=stack.profile, + region=stack.region, + iam_role=stack.iam_role, + ) class StackOutputExternal(StackOutputBase): @@ -153,9 +161,7 @@ def resolve(self): :returns: The value of the Stack output. :rtype: str """ - self.logger.debug( - "Resolving external Stack output: {0}".format(self.argument) - ) + self.logger.debug("Resolving external Stack output: {0}".format(self.argument)) profile = None region = None @@ -169,6 +175,9 @@ def resolve(self): dependency_stack_name, output_key = stack_argument.split("::") return self._get_output_value( - dependency_stack_name, output_key, - profile or None, region or None, iam_role or None + dependency_stack_name, + output_key, + profile or None, + region or None, + iam_role or None, ) diff --git a/sceptre/stack.py b/sceptre/stack.py index 9b2688a2d..138cc7555 100644 --- a/sceptre/stack.py +++ b/sceptre/stack.py @@ -18,7 +18,7 @@ ResolvableContainerProperty, ResolvableValueProperty, RecursiveResolve, - PlaceholderType + PlaceholderType, ) from sceptre.template import Template @@ -105,63 +105,81 @@ class Stack(object): :param stack_group_config: The StackGroup config for the Stack """ + parameters = ResolvableContainerProperty("parameters") sceptre_user_data = ResolvableContainerProperty( - "sceptre_user_data", - PlaceholderType.alphanum + "sceptre_user_data", PlaceholderType.alphanum ) notifications = ResolvableContainerProperty("notifications") - tags = ResolvableContainerProperty('tags') + tags = ResolvableContainerProperty("tags") # placeholder_override=None here means that if the template_bucket_name is a resolver, # placeholders have been enabled, and that stack hasn't been deployed yet, commands that would # otherwise attempt to upload the template (like validate) won't actually use the template bucket # and will act as if there was no template bucket set. - s3_details = ResolvableContainerProperty( - "s3_details", - PlaceholderType.none - ) + s3_details = ResolvableContainerProperty("s3_details", PlaceholderType.none) template_handler_config = ResolvableContainerProperty( - 'template_handler_config', - PlaceholderType.alphanum + "template_handler_config", PlaceholderType.alphanum ) template_bucket_name = ResolvableValueProperty( - "template_bucket_name", - PlaceholderType.none + "template_bucket_name", PlaceholderType.none ) # Similarly, the placeholder_override=None for iam_role means that actions that would otherwise # use the iam_role will act as if there was no iam role when the iam_role stack has not been # deployed for commands that allow placeholders (like validate). - iam_role = ResolvableValueProperty( - 'iam_role', - PlaceholderType.none - ) - role_arn = ResolvableValueProperty('role_arn') + iam_role = ResolvableValueProperty("iam_role", PlaceholderType.none) + role_arn = ResolvableValueProperty("role_arn") hooks = HookProperty("hooks") def __init__( - self, name: str, project_code: str, region: str, template_path: str = None, - template_handler_config: dict = None, template_bucket_name: str = None, template_key_prefix: str = None, - required_version: str = None, parameters: dict = None, sceptre_user_data: dict = None, hooks: Hook = None, - s3_details: dict = None, iam_role: str = None, dependencies: List["Stack"] = None, role_arn: str = None, - protected: bool = False, tags: dict = None, external_name: str = None, notifications: List[str] = None, - on_failure: str = None, profile: str = None, stack_timeout: int = 0, iam_role_session_duration: int = 0, - ignore=False, obsolete=False, stack_group_config: dict = {} + self, + name: str, + project_code: str, + region: str, + template_path: str = None, + template_handler_config: dict = None, + template_bucket_name: str = None, + template_key_prefix: str = None, + required_version: str = None, + parameters: dict = None, + sceptre_user_data: dict = None, + hooks: Hook = None, + s3_details: dict = None, + iam_role: str = None, + dependencies: List["Stack"] = None, + role_arn: str = None, + protected: bool = False, + tags: dict = None, + external_name: str = None, + notifications: List[str] = None, + on_failure: str = None, + profile: str = None, + stack_timeout: int = 0, + iam_role_session_duration: int = 0, + ignore=False, + obsolete=False, + stack_group_config: dict = {}, ): self.logger = logging.getLogger(__name__) if template_path and template_handler_config: - raise InvalidConfigFileError("Both 'template_path' and 'template' are set, specify one or the other") + raise InvalidConfigFileError( + "Both 'template_path' and 'template' are set, specify one or the other" + ) if not template_path and not template_handler_config: - raise InvalidConfigFileError("Neither 'template_path' nor 'template' is set") + raise InvalidConfigFileError( + "Neither 'template_path' nor 'template' is set" + ) self.name = sceptreise_path(name) self.project_code = project_code self.region = region self.required_version = required_version - self.external_name = external_name or get_external_stack_name(self.project_code, self.name) + self.external_name = external_name or get_external_stack_name( + self.project_code, self.name + ) self.template_path = template_path self.dependencies = dependencies or [] self.protected = protected @@ -239,21 +257,21 @@ def __eq__(self, stack): # Stack to a set, which is done very early in plan resolution. Trying to reference resolvers # before the plan is fully resolved can potentially blow up. return ( - self.name == stack.name and - self.external_name == stack.external_name and - self.project_code == stack.project_code and - self.template_path == stack.template_path and - self.region == stack.region and - self.template_key_prefix == stack.template_key_prefix and - self.required_version == stack.required_version and - self.iam_role_session_duration == stack.iam_role_session_duration and - self.profile == stack.profile and - self.dependencies == stack.dependencies and - self.protected == stack.protected and - self.on_failure == stack.on_failure and - self.stack_timeout == stack.stack_timeout and - self.ignore == stack.ignore and - self.obsolete == stack.obsolete + self.name == stack.name + and self.external_name == stack.external_name + and self.project_code == stack.project_code + and self.template_path == stack.template_path + and self.region == stack.region + and self.template_key_prefix == stack.template_key_prefix + and self.required_version == stack.required_version + and self.iam_role_session_duration == stack.iam_role_session_duration + and self.profile == stack.profile + and self.dependencies == stack.dependencies + and self.protected == stack.protected + and self.on_failure == stack.on_failure + and self.stack_timeout == stack.stack_timeout + and self.ignore == stack.ignore + and self.obsolete == stack.obsolete ) def __hash__(self): @@ -285,7 +303,11 @@ def connection_manager(self) -> ConnectionManager: cache_connection_manager = False connection_manager = ConnectionManager( - self.region, self.profile, self.external_name, iam_role, self.iam_role_session_duration + self.region, + self.profile, + self.external_name, + iam_role, + self.iam_role_session_duration, ) if cache_connection_manager: self._connection_manager = connection_manager @@ -304,10 +326,7 @@ def template(self): """ if self._template is None: if self.template_path: - handler_config = { - "type": "file", - "path": self.template_path - } + handler_config = {"type": "file", "path": self.template_path} else: handler_config = self.template_handler_config @@ -317,6 +336,6 @@ def template(self): sceptre_user_data=self.sceptre_user_data, stack_group_config=self.stack_group_config, s3_details=self.s3_details, - connection_manager=self.connection_manager + connection_manager=self.connection_manager, ) return self._template diff --git a/sceptre/stack_status.py b/sceptre/stack_status.py index 67bddb1da..34cc516ac 100644 --- a/sceptre/stack_status.py +++ b/sceptre/stack_status.py @@ -12,6 +12,7 @@ class StackStatus(object): """ StackStatus stores simplified Stack statuses. """ + COMPLETE = "complete" FAILED = "failed" IN_PROGRESS = "in progress" @@ -22,6 +23,7 @@ class StackChangeSetStatus(object): """ StackChangeSetStatus stores simplified ChangeSet statuses. """ + PENDING = "pending" READY = "ready" DEFUNCT = "defunct" diff --git a/sceptre/stack_status_colourer.py b/sceptre/stack_status_colourer.py index a0b9a2725..ed5de72ab 100644 --- a/sceptre/stack_status_colourer.py +++ b/sceptre/stack_status_colourer.py @@ -34,12 +34,10 @@ class StackStatusColourer(object): "UPDATE_ROLLBACK_COMPLETE": Fore.GREEN, "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS": Fore.YELLOW, "UPDATE_ROLLBACK_FAILED": Fore.RED, - "UPDATE_ROLLBACK_IN_PROGRESS": Fore.YELLOW + "UPDATE_ROLLBACK_IN_PROGRESS": Fore.YELLOW, } - STACK_STATUS_PATTERN = re.compile( - r"\b({0})\b".format("|".join(STACK_STATUS_CODES)) - ) + STACK_STATUS_PATTERN = re.compile(r"\b({0})\b".format("|".join(STACK_STATUS_CODES))) def colour(self, string): """ @@ -60,6 +58,6 @@ def colour(self, string): "{0}{1}{2}".format( self.STACK_STATUS_CODES[status], status, Style.RESET_ALL ), - string + string, ) return string diff --git a/sceptre/template.py b/sceptre/template.py index 288b1c7e7..cdd24b8ae 100644 --- a/sceptre/template.py +++ b/sceptre/template.py @@ -46,15 +46,20 @@ class Template(object): _boto_s3_lock = threading.Lock() def __init__( - self, name, handler_config, sceptre_user_data, - stack_group_config, connection_manager=None, s3_details=None + self, + name, + handler_config, + sceptre_user_data, + stack_group_config, + connection_manager=None, + s3_details=None, ): self.logger = logging.getLogger(__name__) self.name = name self.handler_config = handler_config - if self.handler_config is not None and self.handler_config.get('type') is None: - self.handler_config['type'] = 'file' + if self.handler_config is not None and self.handler_config.get("type") is None: + self.handler_config["type"] = "file" self.sceptre_user_data = sceptre_user_data self.stack_group_config = stack_group_config self.connection_manager = connection_manager @@ -86,12 +91,12 @@ def body(self): arguments={k: v for k, v in self.handler_config.items() if k != "type"}, sceptre_user_data=self.sceptre_user_data, connection_manager=self.connection_manager, - stack_group_config=self.stack_group_config + stack_group_config=self.stack_group_config, ) handler.validate() body = handler.handle() if isinstance(body, bytes): - body = body.decode('utf-8') + body = body.decode("utf-8") if not str(body).startswith("---"): body = "---\n{}".format(body) self._body = body @@ -122,7 +127,9 @@ def upload_to_s3(self): self.logger.debug( "%s - Uploading template to: 's3://%s/%s'", - self.name, bucket_name, bucket_key + self.name, + bucket_name, + bucket_key, ) self.connection_manager.call( service="s3", @@ -131,12 +138,15 @@ def upload_to_s3(self): "Bucket": bucket_name, "Key": bucket_key, "Body": self.body, - "ServerSideEncryption": "AES256" - } + "ServerSideEncryption": "AES256", + }, ) url = "https://{}.s3.{}.amazonaws.{}/{}".format( - bucket_name, bucket_region, self._domain_from_region(bucket_region), bucket_key + bucket_name, + bucket_region, + self._domain_from_region(bucket_region), + bucket_key, ) self.logger.debug("%s - Template URL: '%s'", self.name, url) @@ -154,26 +164,19 @@ def _bucket_exists(self): """ bucket_name = self.s3_details["bucket_name"] self.logger.debug( - "%s - Attempting to find template bucket '%s'", - self.name, bucket_name + "%s - Attempting to find template bucket '%s'", self.name, bucket_name ) try: self.connection_manager.call( - service="s3", - command="head_bucket", - kwargs={"Bucket": bucket_name} + service="s3", command="head_bucket", kwargs={"Bucket": bucket_name} ) except botocore.exceptions.ClientError as exp: if exp.response["Error"]["Message"] == "Not Found": - self.logger.debug( - "%s - %s bucket not found.", self.name, bucket_name - ) + self.logger.debug("%s - %s bucket not found.", self.name, bucket_name) return False else: raise - self.logger.debug( - "%s - Found template bucket '%s'", self.name, bucket_name - ) + self.logger.debug("%s - Found template bucket '%s'", self.name, bucket_name) return True def _create_bucket(self): @@ -185,15 +188,11 @@ def _create_bucket(self): """ bucket_name = self.s3_details["bucket_name"] - self.logger.debug( - "%s - Creating new bucket '%s'", self.name, bucket_name - ) + self.logger.debug("%s - Creating new bucket '%s'", self.name, bucket_name) if self.connection_manager.region == "us-east-1": self.connection_manager.call( - service="s3", - command="create_bucket", - kwargs={"Bucket": bucket_name} + service="s3", command="create_bucket", kwargs={"Bucket": bucket_name} ) else: self.connection_manager.call( @@ -203,8 +202,8 @@ def _create_bucket(self): "Bucket": bucket_name, "CreateBucketConfiguration": { "LocationConstraint": self.connection_manager.region - } - } + }, + }, ) def get_boto_call_parameter(self): @@ -226,9 +225,7 @@ def get_boto_call_parameter(self): def _bucket_region(self, bucket_name): region = self.connection_manager.call( - service="s3", - command="get_bucket_location", - kwargs={"Bucket": bucket_name} + service="s3", command="get_bucket_location", kwargs={"Bucket": bucket_name} ).get("LocationConstraint") return region if region else "us-east-1" @@ -244,9 +241,11 @@ def _iterate_entry_points(group, name): """ if sys.version_info < (3, 10): from pkg_resources import iter_entry_points + return iter_entry_points(group, name) else: from importlib.metadata import entry_points + return entry_points(group=group, name=name) def _get_handler_of_type(self, type): @@ -261,10 +260,14 @@ def _get_handler_of_type(self, type): if not self._registry: self._registry = {} - for entry_point in self._iterate_entry_points("sceptre.template_handlers", type): + for entry_point in self._iterate_entry_points( + "sceptre.template_handlers", type + ): self._registry[entry_point.name] = entry_point.load() if type not in self._registry: - raise TemplateHandlerNotFoundError('Handler of type "{0}" not found'.format(type)) + raise TemplateHandlerNotFoundError( + 'Handler of type "{0}" not found'.format(type) + ) return self._registry[type] diff --git a/sceptre/template_handlers/__init__.py b/sceptre/template_handlers/__init__.py index 2c63b66ec..51529cec4 100644 --- a/sceptre/template_handlers/__init__.py +++ b/sceptre/template_handlers/__init__.py @@ -35,10 +35,20 @@ class TemplateHandler: standard_template_extensions = [".json", ".yaml", ".template"] jinja_template_extensions = [".j2"] python_template_extensions = [".py"] - supported_template_extensions = standard_template_extensions + \ - jinja_template_extensions + python_template_extensions - - def __init__(self, name, arguments=None, sceptre_user_data=None, connection_manager=None, stack_group_config=None): + supported_template_extensions = ( + standard_template_extensions + + jinja_template_extensions + + python_template_extensions + ) + + def __init__( + self, + name, + arguments=None, + sceptre_user_data=None, + connection_manager=None, + stack_group_config=None, + ): self.logger = logging.getLogger(__name__) self.name = name self.arguments = arguments diff --git a/sceptre/template_handlers/file.py b/sceptre/template_handlers/file.py index 3d2a21707..9db6605bd 100644 --- a/sceptre/template_handlers/file.py +++ b/sceptre/template_handlers/file.py @@ -23,7 +23,7 @@ def schema(self): "properties": { "path": {"type": "string"}, }, - "required": ["path"] + "required": ["path"], } def handle(self): @@ -33,7 +33,8 @@ def handle(self): if input_path.suffix not in self.supported_template_extensions: raise UnsupportedTemplateFileTypeError( "Template has file extension %s. Only %s are supported.", - input_path.suffix, ",".join(self.supported_template_extensions) + input_path.suffix, + ",".join(self.supported_template_extensions), ) try: @@ -41,12 +42,13 @@ def handle(self): with open(path) as template_file: return template_file.read() elif input_path.suffix in self.jinja_template_extensions: - return helper.render_jinja_template(path, - {"sceptre_user_data": self.sceptre_user_data}, - self.stack_group_config.get("j2_environment", {})) + return helper.render_jinja_template( + path, + {"sceptre_user_data": self.sceptre_user_data}, + self.stack_group_config.get("j2_environment", {}), + ) elif input_path.suffix in self.python_template_extensions: - return helper.call_sceptre_handler(path, - self.sceptre_user_data) + return helper.call_sceptre_handler(path, self.sceptre_user_data) except Exception as e: helper.print_template_traceback(path) raise e @@ -60,6 +62,7 @@ def _resolve_template_path(self, template_path): if the input is absolute. """ return path.join( - self.stack_group_config["project_path"], "templates", - normalise_path(template_path) + self.stack_group_config["project_path"], + "templates", + normalise_path(template_path), ) diff --git a/sceptre/template_handlers/helper.py b/sceptre/template_handlers/helper.py index 9bdc0632a..c02fb7c31 100644 --- a/sceptre/template_handlers/helper.py +++ b/sceptre/template_handlers/helper.py @@ -35,16 +35,13 @@ def call_sceptre_handler(path, sceptre_user_data): # NB: this is a horrible hack... relpath = os.path.relpath(path, os.getcwd()).split(os.path.sep) relpaths_to_add = [ - os.path.sep.join(relpath[:i + 1]) - for i in range(len(relpath[:-1])) + os.path.sep.join(relpath[: i + 1]) for i in range(len(relpath[:-1])) ] # Add any directory between the current working directory and where # the template is to the python path for directory in relpaths_to_add: sys.path.append(os.path.join(os.getcwd(), directory)) - logger.debug( - "Getting CloudFormation from %s", path - ) + logger.debug("Getting CloudFormation from %s", path) if not os.path.isfile(path): raise TemplateNotFoundError("No such template file: '%s'", path) @@ -54,7 +51,7 @@ def call_sceptre_handler(path, sceptre_user_data): try: body = module.sceptre_handler(sceptre_user_data) except AttributeError as e: - if 'sceptre_handler' in str(e): + if "sceptre_handler" in str(e): raise TemplateSceptreHandlerError( "The template does not have the required " "'sceptre_handler(sceptre_user_data)' function." @@ -78,13 +75,16 @@ def print_template_traceback(path): """ def _print_frame(filename, line, fcn, line_text): - logger.error("{}:{}: Template error in '{}'\n=> `{}`".format( - filename, line, fcn, line_text)) + logger.error( + "{}:{}: Template error in '{}'\n=> `{}`".format( + filename, line, fcn, line_text + ) + ) try: _, _, tb = sys.exc_info() stack_trace = traceback.extract_tb(tb) - search_string = os.path.join('', 'templates', '') + search_string = os.path.join("", "templates", "") if search_string in path: template_path = path.split(search_string)[0] + search_string else: @@ -99,9 +99,9 @@ def _print_frame(filename, line, fcn, line_text): _print_frame(frame.filename, frame.lineno, frame.name, frame.line) except Exception as tb_exception: logger.error( - 'A template error occured. ' + - 'Additionally, a traceback exception occured. Exception: %s', - tb_exception + "A template error occured. " + + "Additionally, a traceback exception occured. Exception: %s", + tb_exception, ) @@ -129,11 +129,11 @@ def render_jinja_template(path, jinja_vars, j2_environment): logger.debug("%s Rendering CloudFormation template", path) default_j2_environment_config = { "autoescape": select_autoescape( - disabled_extensions=('j2',), + disabled_extensions=("j2",), default=True, ), "loader": FileSystemLoader(path.parent), - "undefined": StrictUndefined + "undefined": StrictUndefined, } j2_environment_config = strategies.dict_merge( default_j2_environment_config, j2_environment diff --git a/sceptre/template_handlers/http.py b/sceptre/template_handlers/http.py index 983e228a1..e1387c72a 100644 --- a/sceptre/template_handlers/http.py +++ b/sceptre/template_handlers/http.py @@ -25,16 +25,15 @@ class Http(TemplateHandler): while references to jinja (.j2) and python (.py) templates are downloaded, transformed into CFN templates then deployed to AWS. """ + def __init__(self, *args, **kwargs): super(Http, self).__init__(*args, **kwargs) def schema(self): return { "type": "object", - "properties": { - "url": {"type": "string"} - }, - "required": ["url"] + "properties": {"url": {"type": "string"}}, + "required": ["url"], } def handle(self): @@ -47,14 +46,22 @@ def handle(self): if path.suffix not in self.supported_template_extensions: raise UnsupportedTemplateFileTypeError( "Template has file extension %s. Only %s are supported.", - path.suffix, ",".join(self.supported_template_extensions) + path.suffix, + ",".join(self.supported_template_extensions), ) - retries = self._get_handler_option(HANDLER_RETRIES_OPTION_PARAM, DEFAULT_RETRIES_OPTION) - timeout = self._get_handler_option(HANDLER_TIMEOUT_OPTION_PARAM, DEFAULT_TIMEOUT_OPTION) + retries = self._get_handler_option( + HANDLER_RETRIES_OPTION_PARAM, DEFAULT_RETRIES_OPTION + ) + timeout = self._get_handler_option( + HANDLER_TIMEOUT_OPTION_PARAM, DEFAULT_TIMEOUT_OPTION + ) try: template = self._get_template(url, retries=retries, timeout=timeout) - if path.suffix in self.jinja_template_extensions + self.python_template_extensions: + if ( + path.suffix + in self.jinja_template_extensions + self.python_template_extensions + ): file = tempfile.NamedTemporaryFile(prefix=path.stem) self.logger.debug("Template file saved to: %s", file.name) with file as f: @@ -62,13 +69,14 @@ def handle(self): f.seek(0) f.read() if path.suffix in self.jinja_template_extensions: - template = helper.render_jinja_template(f.name, - {"sceptre_user_data": self.sceptre_user_data}, - self.stack_group_config.get("j2_environment", {})) + template = helper.render_jinja_template( + f.name, + {"sceptre_user_data": self.sceptre_user_data}, + self.stack_group_config.get("j2_environment", {}), + ) elif path.suffix in self.python_template_extensions: template = helper.call_sceptre_handler( - f.name, - self.sceptre_user_data + f.name, self.sceptre_user_data ) except Exception as e: @@ -94,11 +102,13 @@ def _get_template(self, url: str, retries: int, timeout: int) -> str: return response.content - def _get_retry_session(self, - retries, - backoff_factor=0.3, - status_forcelist=(429, 500, 502, 503, 504), - session=None): + def _get_retry_session( + self, + retries, + backoff_factor=0.3, + status_forcelist=(429, 500, 502, 503, 504), + session=None, + ): """ Get a request session with retries. Retry options are explained in the request libraries https://urllib3.readthedocs.io/en/latest/reference/urllib3.util.html#module-urllib3.util.retry @@ -112,8 +122,8 @@ def _get_retry_session(self, status_forcelist=status_forcelist, ) adapter = HTTPAdapter(max_retries=retry) - session.mount('http://', adapter) - session.mount('https://', adapter) + session.mount("http://", adapter) + session.mount("https://", adapter) return session def _get_handler_option(self, name, default): diff --git a/sceptre/template_handlers/s3.py b/sceptre/template_handlers/s3.py index 0ec462134..833298381 100644 --- a/sceptre/template_handlers/s3.py +++ b/sceptre/template_handlers/s3.py @@ -21,10 +21,8 @@ def __init__(self, *args, **kwargs): def schema(self): return { "type": "object", - "properties": { - "path": {"type": "string"} - }, - "required": ["path"] + "properties": {"path": {"type": "string"}}, + "required": ["path"], } def handle(self): @@ -37,12 +35,15 @@ def handle(self): standard_template_suffix = [".json", ".yaml", ".template"] jinja_template_suffix = [".j2"] python_template_suffix = [".py"] - supported_suffix = standard_template_suffix + jinja_template_suffix + python_template_suffix + supported_suffix = ( + standard_template_suffix + jinja_template_suffix + python_template_suffix + ) if path.suffix not in supported_suffix: raise UnsupportedTemplateFileTypeError( "Template has file extension %s. Only %s are supported.", - path.suffix, ",".join(supported_suffix) + path.suffix, + ",".join(supported_suffix), ) try: @@ -54,13 +55,14 @@ def handle(self): f.seek(0) f.read() if path.suffix in jinja_template_suffix: - template = helper.render_jinja_template(f.name, - {"sceptre_user_data": self.sceptre_user_data}, - self.stack_group_config.get("j2_environment", {})) + template = helper.render_jinja_template( + f.name, + {"sceptre_user_data": self.sceptre_user_data}, + self.stack_group_config.get("j2_environment", {}), + ) elif path.suffix in python_template_suffix: template = helper.call_sceptre_handler( - f.name, - self.sceptre_user_data + f.name, self.sceptre_user_data ) except Exception as e: @@ -86,10 +88,7 @@ def _get_template(self, path): response = self.connection_manager.call( service="s3", command="get_object", - kwargs={ - "Bucket": bucket, - "Key": key - } + kwargs={"Bucket": bucket, "Key": key}, ) return response["Body"].read() except Exception as e: diff --git a/setup.py b/setup.py index b6636bd04..4b0548128 100755 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def read_file(rel_path): here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, rel_path), 'r') as fp: + with codecs.open(os.path.join(here, rel_path), "r") as fp: return fp.read() @@ -34,7 +34,7 @@ def get_version(rel_path): "sceptre-cmd-resolver>=1.1.3,<2", "sceptre-file-resolver>=1.0.4,<2", "six>=1.11.0,<2.0.0", - "networkx>=2.6,<2.7" + "networkx>=2.6,<2.7", ] extra_requirements = { @@ -50,21 +50,17 @@ def get_version(rel_path): long_description_content_type="text/markdown", author="Cloudreach", author_email="sceptre@cloudreach.com", - license='Apache2', + license="Apache2", url="https://github.com/Sceptre/sceptre", packages=find_packages(exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), - package_dir={ - "sceptre": "sceptre" - }, + package_dir={"sceptre": "sceptre"}, py_modules=["sceptre"], entry_points={ - "console_scripts": [ - 'sceptre = sceptre.cli:cli' - ], + "console_scripts": ["sceptre = sceptre.cli:cli"], "sceptre.hooks": [ "asg_scheduled_actions =" "sceptre.hooks.asg_scaling_processes:ASGScalingProcesses", - "cmd = sceptre.hooks.cmd:Cmd" + "cmd = sceptre.hooks.cmd:Cmd", ], "sceptre.resolvers": [ "environment_variable =" @@ -74,19 +70,22 @@ def get_version(rel_path): "stack_output_external =" "sceptre.resolvers.stack_output:StackOutputExternal", "no_value = sceptre.resolvers.no_value:NoValue", - "stack_attr = sceptre.resolvers.stack_attr:StackAttr" + "stack_attr = sceptre.resolvers.stack_attr:StackAttr", ], "sceptre.template_handlers": [ "file = sceptre.template_handlers.file:File", "s3 = sceptre.template_handlers.s3:S3", - "http = sceptre.template_handlers.http:Http" - ] + "http = sceptre.template_handlers.http:Http", + ], }, data_files=[ - (os.path.join("sceptre", "stack_policies"), [ - os.path.join("sceptre", "stack_policies", "lock.json"), - os.path.join("sceptre", "stack_policies", "unlock.json") - ]) + ( + os.path.join("sceptre", "stack_policies"), + [ + os.path.join("sceptre", "stack_policies", "lock.json"), + os.path.join("sceptre", "stack_policies", "unlock.json"), + ], + ) ], include_package_data=True, zip_safe=False, diff --git a/tests/fixtures-vpc/hooks/custom_hook.py b/tests/fixtures-vpc/hooks/custom_hook.py index af631231d..0e054117f 100644 --- a/tests/fixtures-vpc/hooks/custom_hook.py +++ b/tests/fixtures-vpc/hooks/custom_hook.py @@ -8,6 +8,7 @@ class CustomHook(Hook): This is a test task. """ + def __init__(self, *args, **kwargs): super(CustomHook, self).__init__(*args, **kwargs) diff --git a/tests/fixtures-vpc/templates/vpc.py b/tests/fixtures-vpc/templates/vpc.py index 812c46877..3f51ce506 100644 --- a/tests/fixtures-vpc/templates/vpc.py +++ b/tests/fixtures-vpc/templates/vpc.py @@ -8,33 +8,37 @@ def sceptre_handler(sceptre_user_data): t = Template() - cidr_block_param = t.add_parameter(Parameter( - "CidrBlock", - Type="String", - Default="10.0.0.0/16", - )) - - vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=Ref(cidr_block_param), - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, - )) - - igw = t.add_resource(InternetGateway( - "InternetGateway", - )) - - t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(vpc), - InternetGatewayId=Ref(igw), - )) - - t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(vpc) - )) + cidr_block_param = t.add_parameter( + Parameter( + "CidrBlock", + Type="String", + Default="10.0.0.0/16", + ) + ) + + vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=Ref(cidr_block_param), + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) + ) + + igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) + ) + + t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(vpc), + InternetGatewayId=Ref(igw), + ) + ) + + t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) return t.to_json() diff --git a/tests/fixtures-vpc/templates/vpc_sgt.py b/tests/fixtures-vpc/templates/vpc_sgt.py index e5c319469..dca58ca6b 100644 --- a/tests/fixtures-vpc/templates/vpc_sgt.py +++ b/tests/fixtures-vpc/templates/vpc_sgt.py @@ -6,7 +6,6 @@ class VpcTemplate(object): - def __init__(self): self.template = Template() @@ -20,44 +19,48 @@ def __init__(self): def add_parameters(self): t = self.template - self.cidr_block_param = t.add_parameter(Parameter( - "CidrBlock", - Type="String", - Default="10.0.0.0/16", - )) + self.cidr_block_param = t.add_parameter( + Parameter( + "CidrBlock", + Type="String", + Default="10.0.0.0/16", + ) + ) def add_vpc(self): t = self.template - self.vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=Ref(self.cidr_block_param), - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, - )) + self.vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=Ref(self.cidr_block_param), + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) + ) def add_igw(self): t = self.template - self.igw = t.add_resource(InternetGateway( - "InternetGateway", - )) + self.igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) + ) - t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(self.vpc), - InternetGatewayId=Ref(self.igw), - )) + t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(self.vpc), + InternetGatewayId=Ref(self.igw), + ) + ) def add_outputs(self): t = self.template - t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(self.vpc) - )) + t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(self.vpc))) def sceptre_handler(sceptre_user_data): diff --git a/tests/fixtures-vpc/templates/vpc_sud.py b/tests/fixtures-vpc/templates/vpc_sud.py index 52a3143d4..1b2e317f2 100644 --- a/tests/fixtures-vpc/templates/vpc_sud.py +++ b/tests/fixtures-vpc/templates/vpc_sud.py @@ -9,28 +9,30 @@ def sceptre_handler(sceptre_user_data): t = Template() - vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=sceptre_user_data["cidr_block"], - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, - )) - - igw = t.add_resource(InternetGateway( - "InternetGateway", - )) - - t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(vpc), - InternetGatewayId=Ref(igw), - )) - - t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(vpc) - )) + vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=sceptre_user_data["cidr_block"], + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) + ) + + igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) + ) + + t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(vpc), + InternetGatewayId=Ref(igw), + ) + ) + + t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) return t.to_json() diff --git a/tests/fixtures-vpc/templates/vpc_t.py b/tests/fixtures-vpc/templates/vpc_t.py index 1f9ae8ffe..8f1ee2d55 100644 --- a/tests/fixtures-vpc/templates/vpc_t.py +++ b/tests/fixtures-vpc/templates/vpc_t.py @@ -6,32 +6,36 @@ t = Template() -cidr_block_param = t.add_parameter(Parameter( - "CidrBlock", - Type="String", - Default="10.0.0.0/16", -)) - -vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=Ref(cidr_block_param), - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, -)) - -igw = t.add_resource(InternetGateway( - "InternetGateway", -)) - -igw_attachment = t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(vpc), - InternetGatewayId=Ref(igw), -)) - -vpc_id_output = t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(vpc) -)) +cidr_block_param = t.add_parameter( + Parameter( + "CidrBlock", + Type="String", + Default="10.0.0.0/16", + ) +) + +vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=Ref(cidr_block_param), + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) +) + +igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) +) + +igw_attachment = t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(vpc), + InternetGatewayId=Ref(igw), + ) +) + +vpc_id_output = t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) diff --git a/tests/fixtures/hooks/custom_hook.py b/tests/fixtures/hooks/custom_hook.py index af631231d..0e054117f 100644 --- a/tests/fixtures/hooks/custom_hook.py +++ b/tests/fixtures/hooks/custom_hook.py @@ -8,6 +8,7 @@ class CustomHook(Hook): This is a test task. """ + def __init__(self, *args, **kwargs): super(CustomHook, self).__init__(*args, **kwargs) diff --git a/tests/fixtures/templates/vpc.py b/tests/fixtures/templates/vpc.py index 812c46877..3f51ce506 100644 --- a/tests/fixtures/templates/vpc.py +++ b/tests/fixtures/templates/vpc.py @@ -8,33 +8,37 @@ def sceptre_handler(sceptre_user_data): t = Template() - cidr_block_param = t.add_parameter(Parameter( - "CidrBlock", - Type="String", - Default="10.0.0.0/16", - )) - - vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=Ref(cidr_block_param), - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, - )) - - igw = t.add_resource(InternetGateway( - "InternetGateway", - )) - - t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(vpc), - InternetGatewayId=Ref(igw), - )) - - t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(vpc) - )) + cidr_block_param = t.add_parameter( + Parameter( + "CidrBlock", + Type="String", + Default="10.0.0.0/16", + ) + ) + + vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=Ref(cidr_block_param), + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) + ) + + igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) + ) + + t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(vpc), + InternetGatewayId=Ref(igw), + ) + ) + + t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) return t.to_json() diff --git a/tests/fixtures/templates/vpc_sgt.py b/tests/fixtures/templates/vpc_sgt.py index e5c319469..dca58ca6b 100644 --- a/tests/fixtures/templates/vpc_sgt.py +++ b/tests/fixtures/templates/vpc_sgt.py @@ -6,7 +6,6 @@ class VpcTemplate(object): - def __init__(self): self.template = Template() @@ -20,44 +19,48 @@ def __init__(self): def add_parameters(self): t = self.template - self.cidr_block_param = t.add_parameter(Parameter( - "CidrBlock", - Type="String", - Default="10.0.0.0/16", - )) + self.cidr_block_param = t.add_parameter( + Parameter( + "CidrBlock", + Type="String", + Default="10.0.0.0/16", + ) + ) def add_vpc(self): t = self.template - self.vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=Ref(self.cidr_block_param), - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, - )) + self.vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=Ref(self.cidr_block_param), + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) + ) def add_igw(self): t = self.template - self.igw = t.add_resource(InternetGateway( - "InternetGateway", - )) + self.igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) + ) - t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(self.vpc), - InternetGatewayId=Ref(self.igw), - )) + t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(self.vpc), + InternetGatewayId=Ref(self.igw), + ) + ) def add_outputs(self): t = self.template - t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(self.vpc) - )) + t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(self.vpc))) def sceptre_handler(sceptre_user_data): diff --git a/tests/fixtures/templates/vpc_sud.py b/tests/fixtures/templates/vpc_sud.py index 52a3143d4..1b2e317f2 100644 --- a/tests/fixtures/templates/vpc_sud.py +++ b/tests/fixtures/templates/vpc_sud.py @@ -9,28 +9,30 @@ def sceptre_handler(sceptre_user_data): t = Template() - vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=sceptre_user_data["cidr_block"], - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, - )) - - igw = t.add_resource(InternetGateway( - "InternetGateway", - )) - - t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(vpc), - InternetGatewayId=Ref(igw), - )) - - t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(vpc) - )) + vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=sceptre_user_data["cidr_block"], + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) + ) + + igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) + ) + + t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(vpc), + InternetGatewayId=Ref(igw), + ) + ) + + t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) return t.to_json() diff --git a/tests/fixtures/templates/vpc_t.py b/tests/fixtures/templates/vpc_t.py index 1f9ae8ffe..8f1ee2d55 100644 --- a/tests/fixtures/templates/vpc_t.py +++ b/tests/fixtures/templates/vpc_t.py @@ -6,32 +6,36 @@ t = Template() -cidr_block_param = t.add_parameter(Parameter( - "CidrBlock", - Type="String", - Default="10.0.0.0/16", -)) - -vpc = t.add_resource(VPC( - "VirtualPrivateCloud", - CidrBlock=Ref(cidr_block_param), - InstanceTenancy="default", - EnableDnsSupport=True, - EnableDnsHostnames=True, -)) - -igw = t.add_resource(InternetGateway( - "InternetGateway", -)) - -igw_attachment = t.add_resource(VPCGatewayAttachment( - "IGWAttachment", - VpcId=Ref(vpc), - InternetGatewayId=Ref(igw), -)) - -vpc_id_output = t.add_output(Output( - "VpcId", - Description="New VPC ID", - Value=Ref(vpc) -)) +cidr_block_param = t.add_parameter( + Parameter( + "CidrBlock", + Type="String", + Default="10.0.0.0/16", + ) +) + +vpc = t.add_resource( + VPC( + "VirtualPrivateCloud", + CidrBlock=Ref(cidr_block_param), + InstanceTenancy="default", + EnableDnsSupport=True, + EnableDnsHostnames=True, + ) +) + +igw = t.add_resource( + InternetGateway( + "InternetGateway", + ) +) + +igw_attachment = t.add_resource( + VPCGatewayAttachment( + "IGWAttachment", + VpcId=Ref(vpc), + InternetGatewayId=Ref(igw), + ) +) + +vpc_id_output = t.add_output(Output("VpcId", Description="New VPC ID", Value=Ref(vpc))) diff --git a/tests/test_actions.py b/tests/test_actions.py index 611a90202..b275681f1 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -12,7 +12,7 @@ ProtectedStackError, StackDoesNotExistError, UnknownStackChangeSetStatusError, - UnknownStackStatusError + UnknownStackStatusError, ) from sceptre.plan.actions import StackActions from sceptre.stack import Stack @@ -21,40 +21,46 @@ class TestStackActions(object): - def setup_method(self, test_method): self.patcher_connection_manager = patch( "sceptre.plan.actions.ConnectionManager" ) self.mock_ConnectionManager = self.patcher_connection_manager.start() self.stack = Stack( - name='prod/app/stack', project_code=sentinel.project_code, - template_path=sentinel.template_path, region=sentinel.region, - profile=sentinel.profile, parameters={"key1": "val1"}, sceptre_user_data=sentinel.sceptre_user_data, - hooks={}, s3_details=None, dependencies=sentinel.dependencies, - role_arn=sentinel.role_arn, protected=False, - tags={"tag1": "val1"}, external_name=sentinel.external_name, + name="prod/app/stack", + project_code=sentinel.project_code, + template_path=sentinel.template_path, + region=sentinel.region, + profile=sentinel.profile, + parameters={"key1": "val1"}, + sceptre_user_data=sentinel.sceptre_user_data, + hooks={}, + s3_details=None, + dependencies=sentinel.dependencies, + role_arn=sentinel.role_arn, + protected=False, + tags={"tag1": "val1"}, + external_name=sentinel.external_name, notifications=[sentinel.notification], on_failure=sentinel.on_failure, stack_timeout=sentinel.stack_timeout, - ) self.actions = StackActions(self.stack) self.stack_group_config = {} self.template = Template( - "fixtures/templates", self.stack.template_handler_config, - self.stack.sceptre_user_data, self.stack_group_config, - self.actions.connection_manager, self.stack.s3_details + "fixtures/templates", + self.stack.template_handler_config, + self.stack.sceptre_user_data, + self.stack_group_config, + self.actions.connection_manager, + self.stack.s3_details, ) - self.template._body = json.dumps({ - 'AWSTemplateFormatVersion': '2010-09-09', - 'Resources': { - 'Bucket': { - 'Type': 'AWS::S3::Bucket', - 'Properties': {} - } + self.template._body = json.dumps( + { + "AWSTemplateFormatVersion": "2010-09-09", + "Resources": {"Bucket": {"Type": "AWS::S3::Bucket", "Properties": {}}}, } - }) + ) self.stack._template = self.template def teardown_method(self, test_method): @@ -67,15 +73,12 @@ def test_template_loads_template(self, mock_Template): response = self.stack.template mock_Template.assert_called_once_with( - name='prod/app/stack', - handler_config={ - "type": "file", - "path": sentinel.template_path - }, + name="prod/app/stack", + handler_config={"type": "file", "path": sentinel.template_path}, sceptre_user_data=sentinel.sceptre_user_data, stack_group_config={}, connection_manager=self.stack.connection_manager, - s3_details=None + s3_details=None, ) assert response == sentinel.template @@ -86,9 +89,11 @@ def test_template_returns_template_if_it_exists(self): def test_external_name_with_custom_stack_name(self): stack = Stack( - name="stack_name", project_code="project_code", - template_path="template_path", region="region", - external_name="external_name" + name="stack_name", + project_code="project_code", + template_path="template_path", + region="region", + external_name="external_name", ) assert stack.external_name == "external_name" @@ -96,13 +101,11 @@ def test_external_name_with_custom_stack_name(self): @patch("sceptre.plan.actions.StackActions._wait_for_completion") @patch("sceptre.plan.actions.StackActions._get_stack_timeout") def test_create_sends_correct_request( - self, mock_get_stack_timeout, mock_wait_for_completion + self, mock_get_stack_timeout, mock_wait_for_completion ): self.template._body = sentinel.template - mock_get_stack_timeout.return_value = { - "TimeoutInMinutes": sentinel.timeout - } + mock_get_stack_timeout.return_value = {"TimeoutInMinutes": sentinel.timeout} self.actions.create() self.actions.connection_manager.call.assert_called_with( @@ -111,21 +114,18 @@ def test_create_sends_correct_request( kwargs={ "StackName": sentinel.external_name, "TemplateBody": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "RoleARN": sentinel.role_arn, "NotificationARNs": [sentinel.notification], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ], + "Tags": [{"Key": "tag1", "Value": "val1"}], "OnFailure": sentinel.on_failure, - "TimeoutInMinutes": sentinel.timeout - } + "TimeoutInMinutes": sentinel.timeout, + }, ) mock_wait_for_completion.assert_called_once_with(boto_response=ANY) @@ -146,21 +146,18 @@ def test_create_sends_correct_request_no_notifications( kwargs={ "StackName": sentinel.external_name, "Template": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "RoleARN": sentinel.role_arn, "NotificationARNs": [], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ], + "Tags": [{"Key": "tag1", "Value": "val1"}], "OnFailure": sentinel.on_failure, - "TimeoutInMinutes": sentinel.stack_timeout - } + "TimeoutInMinutes": sentinel.stack_timeout, + }, ) mock_wait_for_completion.assert_called_once_with(boto_response=ANY) @@ -180,27 +177,22 @@ def test_create_sends_correct_request_with_no_failure_no_timeout( kwargs={ "StackName": sentinel.external_name, "TemplateBody": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "RoleARN": sentinel.role_arn, "NotificationARNs": [sentinel.notification], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ] - } + "Tags": [{"Key": "tag1", "Value": "val1"}], + }, ) mock_wait_for_completion.assert_called_once_with(boto_response=ANY) @patch("sceptre.plan.actions.StackActions._wait_for_completion") - def test_create_stack_already_exists( - self, mock_wait_for_completion - ): + def test_create_stack_already_exists(self, mock_wait_for_completion): self.actions.stack._template = Mock(spec=Template) self.actions.stack._template.get_boto_call_parameter.return_value = { "Template": sentinel.template @@ -209,10 +201,12 @@ def test_create_stack_already_exists( { "Error": { "Code": "AlreadyExistsException", - "Message": "Stack already [{}] exists".format(self.actions.stack.name) + "Message": "Stack already [{}] exists".format( + self.actions.stack.name + ), } }, - sentinel.operation + sentinel.operation, ) response = self.actions.create() assert response == StackStatus.COMPLETE @@ -231,23 +225,19 @@ def test_update_sends_correct_request(self, mock_wait_for_completion): kwargs={ "StackName": sentinel.external_name, "Template": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "RoleARN": sentinel.role_arn, "NotificationARNs": [sentinel.notification], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ] - } + "Tags": [{"Key": "tag1", "Value": "val1"}], + }, ) mock_wait_for_completion.assert_called_once_with( - sentinel.stack_timeout, - boto_response=ANY + sentinel.stack_timeout, boto_response=ANY ) @patch("sceptre.plan.actions.StackActions._wait_for_completion") @@ -266,23 +256,22 @@ def test_update_cancels_after_timeout(self, mock_wait_for_completion): kwargs={ "StackName": sentinel.external_name, "Template": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "RoleARN": sentinel.role_arn, "NotificationARNs": [sentinel.notification], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ] - }), + "Tags": [{"Key": "tag1", "Value": "val1"}], + }, + ), call( service="cloudformation", command="cancel_update_stack", - kwargs={"StackName": sentinel.external_name}) + kwargs={"StackName": sentinel.external_name}, + ), ] self.actions.connection_manager.call.assert_has_calls(calls) mock_wait_for_completion.assert_has_calls( @@ -291,7 +280,7 @@ def test_update_cancels_after_timeout(self, mock_wait_for_completion): @patch("sceptre.plan.actions.StackActions._wait_for_completion") def test_update_sends_correct_request_no_notification( - self, mock_wait_for_completion + self, mock_wait_for_completion ): self.actions.stack._template = Mock(spec=Template) self.actions.stack._template.get_boto_call_parameter.return_value = { @@ -306,28 +295,24 @@ def test_update_sends_correct_request_no_notification( kwargs={ "StackName": sentinel.external_name, "Template": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "RoleARN": sentinel.role_arn, "NotificationARNs": [], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ] - } + "Tags": [{"Key": "tag1", "Value": "val1"}], + }, ) mock_wait_for_completion.assert_called_once_with( - sentinel.stack_timeout, - boto_response=ANY + sentinel.stack_timeout, boto_response=ANY ) @patch("sceptre.plan.actions.StackActions._wait_for_completion") def test_update_with_complete_stack_with_no_updates_to_perform( - self, mock_wait_for_completion + self, mock_wait_for_completion ): self.actions.stack._template = Mock(spec=Template) self.actions.stack._template.get_boto_call_parameter.return_value = { @@ -337,31 +322,27 @@ def test_update_with_complete_stack_with_no_updates_to_perform( { "Error": { "Code": "NoUpdateToPerformError", - "Message": "No updates are to be performed." + "Message": "No updates are to be performed.", } }, - sentinel.operation + sentinel.operation, ) response = self.actions.update() assert response == StackStatus.COMPLETE @patch("sceptre.plan.actions.StackActions._wait_for_completion") - def test_cancel_update_sends_correct_request( - self, mock_wait_for_completion - ): + def test_cancel_update_sends_correct_request(self, mock_wait_for_completion): self.actions.cancel_stack_update() self.actions.connection_manager.call.assert_called_once_with( service="cloudformation", command="cancel_update_stack", - kwargs={"StackName": sentinel.external_name} + kwargs={"StackName": sentinel.external_name}, ) mock_wait_for_completion.assert_called_once_with(boto_response=ANY) @patch("sceptre.plan.actions.StackActions.create") @patch("sceptre.plan.actions.StackActions._get_status") - def test_launch_with_stack_that_does_not_exist( - self, mock_get_status, mock_create - ): + def test_launch_with_stack_that_does_not_exist(self, mock_get_status, mock_create): mock_get_status.side_effect = StackDoesNotExistError() mock_create.return_value = sentinel.launch_response response = self.actions.launch() @@ -372,7 +353,7 @@ def test_launch_with_stack_that_does_not_exist( @patch("sceptre.plan.actions.StackActions.delete") @patch("sceptre.plan.actions.StackActions._get_status") def test_launch_with_stack_that_failed_to_create( - self, mock_get_status, mock_delete, mock_create + self, mock_get_status, mock_delete, mock_create ): mock_get_status.return_value = "CREATE_FAILED" mock_create.return_value = sentinel.launch_response @@ -384,7 +365,7 @@ def test_launch_with_stack_that_failed_to_create( @patch("sceptre.plan.actions.StackActions.update") @patch("sceptre.plan.actions.StackActions._get_status") def test_launch_with_complete_stack_with_updates_to_perform( - self, mock_get_status, mock_update + self, mock_get_status, mock_update ): mock_get_status.return_value = "CREATE_COMPLETE" mock_update.return_value = sentinel.launch_response @@ -395,7 +376,7 @@ def test_launch_with_complete_stack_with_updates_to_perform( @patch("sceptre.plan.actions.StackActions.update") @patch("sceptre.plan.actions.StackActions._get_status") def test_launch_with_complete_stack_with_no_updates_to_perform( - self, mock_get_status, mock_update + self, mock_get_status, mock_update ): mock_get_status.return_value = "CREATE_COMPLETE" mock_update.return_value = StackStatus.COMPLETE @@ -406,17 +387,11 @@ def test_launch_with_complete_stack_with_no_updates_to_perform( @patch("sceptre.plan.actions.StackActions.update") @patch("sceptre.plan.actions.StackActions._get_status") def test_launch_with_complete_stack_with_unknown_client_error( - self, mock_get_status, mock_update + self, mock_get_status, mock_update ): mock_get_status.return_value = "CREATE_COMPLETE" mock_update.side_effect = ClientError( - { - "Error": { - "Code": "Boom!", - "Message": "Boom!" - } - }, - sentinel.operation + {"Error": {"Code": "Boom!", "Message": "Boom!"}}, sentinel.operation ) with pytest.raises(ClientError): self.actions.launch() @@ -442,25 +417,20 @@ def test_launch_with_unknown_stack_status(self, mock_get_status): @patch("sceptre.plan.actions.StackActions._wait_for_completion") @patch("sceptre.plan.actions.StackActions._get_status") - def test_delete_with_created_stack( - self, mock_get_status, mock_wait_for_completion - ): + def test_delete_with_created_stack(self, mock_get_status, mock_wait_for_completion): mock_get_status.return_value = "CREATE_COMPLETE" self.actions.delete() self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="delete_stack", - kwargs={ - "StackName": sentinel.external_name, - "RoleARN": sentinel.role_arn - } + kwargs={"StackName": sentinel.external_name, "RoleARN": sentinel.role_arn}, ) @patch("sceptre.plan.actions.StackActions._wait_for_completion") @patch("sceptre.plan.actions.StackActions._get_status") def test_delete_when_wait_for_completion_raises_stack_does_not_exist_error( - self, mock_get_status, mock_wait_for_completion + self, mock_get_status, mock_wait_for_completion ): mock_get_status.return_value = "CREATE_COMPLETE" mock_wait_for_completion.side_effect = StackDoesNotExistError() @@ -470,17 +440,17 @@ def test_delete_when_wait_for_completion_raises_stack_does_not_exist_error( @patch("sceptre.plan.actions.StackActions._wait_for_completion") @patch("sceptre.plan.actions.StackActions._get_status") def test_delete_when_wait_for_completion_raises_non_existent_client_error( - self, mock_get_status, mock_wait_for_completion + self, mock_get_status, mock_wait_for_completion ): mock_get_status.return_value = "CREATE_COMPLETE" mock_wait_for_completion.side_effect = ClientError( { "Error": { "Code": "DoesNotExistException", - "Message": "Stack does not exist" + "Message": "Stack does not exist", } }, - sentinel.operation + sentinel.operation, ) status = self.actions.delete() assert status == StackStatus.COMPLETE @@ -488,17 +458,12 @@ def test_delete_when_wait_for_completion_raises_non_existent_client_error( @patch("sceptre.plan.actions.StackActions._wait_for_completion") @patch("sceptre.plan.actions.StackActions._get_status") def test_delete_when_wait_for_completion_raises_unexpected_client_error( - self, mock_get_status, mock_wait_for_completion + self, mock_get_status, mock_wait_for_completion ): mock_get_status.return_value = "CREATE_COMPLETE" mock_wait_for_completion.side_effect = ClientError( - { - "Error": { - "Code": "DoesNotExistException", - "Message": "Boom" - } - }, - sentinel.operation + {"Error": {"Code": "DoesNotExistException", "Message": "Boom"}}, + sentinel.operation, ) with pytest.raises(ClientError): self.actions.delete() @@ -506,7 +471,7 @@ def test_delete_when_wait_for_completion_raises_unexpected_client_error( @patch("sceptre.plan.actions.StackActions._wait_for_completion") @patch("sceptre.plan.actions.StackActions._get_status") def test_delete_with_non_existent_stack( - self, mock_get_status, mock_wait_for_completion + self, mock_get_status, mock_wait_for_completion ): mock_get_status.side_effect = StackDoesNotExistError() status = self.actions.delete() @@ -517,7 +482,7 @@ def test_describe_stack_sends_correct_request(self): self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="describe_stacks", - kwargs={"StackName": sentinel.external_name} + kwargs={"StackName": sentinel.external_name}, ) def test_describe_events_sends_correct_request(self): @@ -525,7 +490,7 @@ def test_describe_events_sends_correct_request(self): self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="describe_stack_events", - kwargs={"StackName": sentinel.external_name} + kwargs={"StackName": sentinel.external_name}, ) def test_describe_resources_sends_correct_request(self): @@ -534,7 +499,7 @@ def test_describe_resources_sends_correct_request(self): { "LogicalResourceId": sentinel.logical_resource_id, "PhysicalResourceId": sentinel.physical_resource_id, - "OtherParam": sentinel.other_param + "OtherParam": sentinel.other_param, } ] } @@ -542,33 +507,27 @@ def test_describe_resources_sends_correct_request(self): self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="describe_stack_resources", - kwargs={"StackName": sentinel.external_name} + kwargs={"StackName": sentinel.external_name}, ) - assert response == {self.stack.name: [ - { - "LogicalResourceId": sentinel.logical_resource_id, - "PhysicalResourceId": sentinel.physical_resource_id - } - ]} + assert response == { + self.stack.name: [ + { + "LogicalResourceId": sentinel.logical_resource_id, + "PhysicalResourceId": sentinel.physical_resource_id, + } + ] + } @patch("sceptre.plan.actions.StackActions._describe") def test_describe_outputs_sends_correct_request(self, mock_describe): - mock_describe.return_value = { - "Stacks": [{ - "Outputs": sentinel.outputs - }] - } + mock_describe.return_value = {"Stacks": [{"Outputs": sentinel.outputs}]} response = self.actions.describe_outputs() mock_describe.assert_called_once_with() assert response == {self.stack.name: sentinel.outputs} @patch("sceptre.plan.actions.StackActions._describe") - def test_describe_outputs_handles_stack_with_no_outputs( - self, mock_describe - ): - mock_describe.return_value = { - "Stacks": [{}] - } + def test_describe_outputs_handles_stack_with_no_outputs(self, mock_describe): + mock_describe.return_value = {"Stacks": [{}]} response = self.actions.describe_outputs() assert response == {self.stack.name: []} @@ -577,10 +536,7 @@ def test_continue_update_rollback_sends_correct_request(self): self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="continue_update_rollback", - kwargs={ - "StackName": sentinel.external_name, - "RoleARN": sentinel.role_arn - } + kwargs={"StackName": sentinel.external_name, "RoleARN": sentinel.role_arn}, ) def test_set_stack_policy_sends_correct_request(self): @@ -600,24 +556,22 @@ def test_set_stack_policy_sends_correct_request(self): } ] } -""" - } +""", + }, ) @patch("sceptre.plan.actions.json") def test_get_stack_policy_sends_correct_request(self, mock_Json): - mock_Json.loads.return_value = '{}' - mock_Json.dumps.return_value = '{}' + mock_Json.loads.return_value = "{}" + mock_Json.dumps.return_value = "{}" response = self.actions.get_policy() self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="get_stack_policy", - kwargs={ - "StackName": sentinel.external_name - } + kwargs={"StackName": sentinel.external_name}, ) - assert response == {'prod/app/stack': '{}'} + assert response == {"prod/app/stack": "{}"} def test_create_change_set_sends_correct_request(self): self.template._body = sentinel.template @@ -629,20 +583,17 @@ def test_create_change_set_sends_correct_request(self): kwargs={ "StackName": sentinel.external_name, "TemplateBody": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "ChangeSetName": sentinel.change_set_name, "RoleARN": sentinel.role_arn, "NotificationARNs": [sentinel.notification], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ] - } + "Tags": [{"Key": "tag1", "Value": "val1"}], + }, ) def test_create_change_set_sends_correct_request_no_notifications(self): @@ -659,20 +610,17 @@ def test_create_change_set_sends_correct_request_no_notifications(self): kwargs={ "StackName": sentinel.external_name, "Template": sentinel.template, - "Parameters": [{ - "ParameterKey": "key1", - "ParameterValue": "val1" - }], - "Capabilities": ['CAPABILITY_IAM', - 'CAPABILITY_NAMED_IAM', - 'CAPABILITY_AUTO_EXPAND'], + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], "ChangeSetName": sentinel.change_set_name, "RoleARN": sentinel.role_arn, "NotificationARNs": [], - "Tags": [ - {"Key": "tag1", "Value": "val1"} - ] - } + "Tags": [{"Key": "tag1", "Value": "val1"}], + }, ) def test_delete_change_set_sends_correct_request(self): @@ -682,8 +630,8 @@ def test_delete_change_set_sends_correct_request(self): command="delete_change_set", kwargs={ "ChangeSetName": sentinel.change_set_name, - "StackName": sentinel.external_name - } + "StackName": sentinel.external_name, + }, ) def test_describe_change_set_sends_correct_request(self): @@ -693,42 +641,41 @@ def test_describe_change_set_sends_correct_request(self): command="describe_change_set", kwargs={ "ChangeSetName": sentinel.change_set_name, - "StackName": sentinel.external_name - } + "StackName": sentinel.external_name, + }, ) @patch("sceptre.plan.actions.StackActions._wait_for_completion") - def test_execute_change_set_sends_correct_request( - self, mock_wait_for_completion - ): + def test_execute_change_set_sends_correct_request(self, mock_wait_for_completion): self.actions.execute_change_set(sentinel.change_set_name) self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="execute_change_set", kwargs={ "ChangeSetName": sentinel.change_set_name, - "StackName": sentinel.external_name - } + "StackName": sentinel.external_name, + }, ) mock_wait_for_completion.assert_called_once_with(boto_response=ANY) def test_execute_change_set__change_set_is_failed_for_no_changes__returns_0(self): def fake_describe(service, command, kwargs): - assert (service, command) == ('cloudformation', 'describe_change_set') + assert (service, command) == ("cloudformation", "describe_change_set") return { - 'Status': 'FAILED', - 'StatusReason': "The submitted information didn't contain changes", + "Status": "FAILED", + "StatusReason": "The submitted information didn't contain changes", } + self.actions.connection_manager.call.side_effect = fake_describe result = self.actions.execute_change_set(sentinel.change_set_name) assert result == 0 def test_execute_change_set__change_set_is_failed_for_no_updates__returns_0(self): def fake_describe(service, command, kwargs): - assert (service, command) == ('cloudformation', 'describe_change_set') + assert (service, command) == ("cloudformation", "describe_change_set") return { - 'Status': 'FAILED', - 'StatusReason': "No updates are to be performed", + "Status": "FAILED", + "StatusReason": "No updates are to be performed", } self.actions.connection_manager.call.side_effect = fake_describe @@ -740,21 +687,18 @@ def test_list_change_sets_sends_correct_request(self): self.actions.connection_manager.call.assert_called_with( service="cloudformation", command="list_change_sets", - kwargs={"StackName": sentinel.external_name} + kwargs={"StackName": sentinel.external_name}, ) @patch("sceptre.plan.actions.StackActions._list_change_sets") - def test_list_change_sets( - self, mock_list_change_sets - ): + def test_list_change_sets(self, mock_list_change_sets): mock_list_change_sets_return_value = {"Summaries": []} expected_responses = [] for num in ["1", "2"]: - response = [{ - "ChangeSetId": "mychangesetid{num}", - "StackId": "mystackid{num}" - }] + response = [ + {"ChangeSetId": "mychangesetid{num}", "StackId": "mystackid{num}"} + ] mock_list_change_sets_return_value["Summaries"].append(response) expected_responses.append(response) @@ -765,18 +709,15 @@ def test_list_change_sets( @patch("sceptre.plan.actions.urllib.parse.urlencode") @patch("sceptre.plan.actions.StackActions._list_change_sets") - def test_list_change_sets_url_mode( - self, mock_list_change_sets, mock_urlencode - ): + def test_list_change_sets_url_mode(self, mock_list_change_sets, mock_urlencode): mock_list_change_sets_return_value = {"Summaries": []} mock_urlencode_side_effect = [] expected_urls = [] for num in ["1", "2"]: - mock_list_change_sets_return_value["Summaries"].append({ - "ChangeSetId": "mychangesetid{num}", - "StackId": "mystackid{num}" - }) + mock_list_change_sets_return_value["Summaries"].append( + {"ChangeSetId": "mychangesetid{num}", "StackId": "mystackid{num}"} + ) urlencoded = "stackId=mystackid{num}&changeSetId=mychangesetid{num}" mock_urlencode_side_effect.append(urlencoded) expected_urls.append( @@ -792,18 +733,14 @@ def test_list_change_sets_url_mode( @pytest.mark.parametrize("url_mode", [True, False]) @patch("sceptre.plan.actions.StackActions._list_change_sets") - def test_list_change_sets_empty( - self, mock_list_change_sets, url_mode - ): + def test_list_change_sets_empty(self, mock_list_change_sets, url_mode): mock_list_change_sets.return_value = {"Summaries": []} response = self.actions.list_change_sets(url=url_mode) assert response == {"prod/app/stack": []} @patch("sceptre.plan.actions.StackActions.set_policy") @patch("os.path.join") - def test_lock_calls_set_stack_policy_with_policy( - self, mock_join, mock_set_policy - ): + def test_lock_calls_set_stack_policy_with_policy(self, mock_join, mock_set_policy): mock_join.return_value = "tests/fixtures/stack_policies/lock.json" self.actions.lock() mock_set_policy.assert_called_once_with( @@ -813,7 +750,7 @@ def test_lock_calls_set_stack_policy_with_policy( @patch("sceptre.plan.actions.StackActions.set_policy") @patch("os.path.join") def test_unlock_calls_set_stack_policy_with_policy( - self, mock_join, mock_set_policy + self, mock_join, mock_set_policy ): mock_join.return_value = "tests/fixtures/stack_policies/unlock.json" self.actions.unlock() @@ -822,111 +759,92 @@ def test_unlock_calls_set_stack_policy_with_policy( ) def test_format_parameters_with_sting_values(self): - parameters = { - "key1": "value1", - "key2": "value2", - "key3": "value3" - } + parameters = {"key1": "value1", "key2": "value2", "key3": "value3"} formatted_parameters = self.actions._format_parameters(parameters) sorted_formatted_parameters = sorted( - formatted_parameters, - key=lambda x: x["ParameterKey"] + formatted_parameters, key=lambda x: x["ParameterKey"] ) assert sorted_formatted_parameters == [ {"ParameterKey": "key1", "ParameterValue": "value1"}, {"ParameterKey": "key2", "ParameterValue": "value2"}, - {"ParameterKey": "key3", "ParameterValue": "value3"} + {"ParameterKey": "key3", "ParameterValue": "value3"}, ] def test_format_parameters_with_none_values(self): - parameters = { - "key1": None, - "key2": None, - "key3": None - } + parameters = {"key1": None, "key2": None, "key3": None} formatted_parameters = self.actions._format_parameters(parameters) sorted_formatted_parameters = sorted( - formatted_parameters, - key=lambda x: x["ParameterKey"] + formatted_parameters, key=lambda x: x["ParameterKey"] ) assert sorted_formatted_parameters == [] def test_format_parameters_with_none_and_string_values(self): - parameters = { - "key1": "value1", - "key2": None, - "key3": "value3" - } + parameters = {"key1": "value1", "key2": None, "key3": "value3"} formatted_parameters = self.actions._format_parameters(parameters) sorted_formatted_parameters = sorted( - formatted_parameters, - key=lambda x: x["ParameterKey"] + formatted_parameters, key=lambda x: x["ParameterKey"] ) assert sorted_formatted_parameters == [ {"ParameterKey": "key1", "ParameterValue": "value1"}, - {"ParameterKey": "key3", "ParameterValue": "value3"} + {"ParameterKey": "key3", "ParameterValue": "value3"}, ] def test_format_parameters_with_list_values(self): parameters = { "key1": ["value1", "value2", "value3"], "key2": ["value4", "value5", "value6"], - "key3": ["value7", "value8", "value9"] + "key3": ["value7", "value8", "value9"], } formatted_parameters = self.actions._format_parameters(parameters) sorted_formatted_parameters = sorted( - formatted_parameters, - key=lambda x: x["ParameterKey"] + formatted_parameters, key=lambda x: x["ParameterKey"] ) assert sorted_formatted_parameters == [ {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"}, {"ParameterKey": "key2", "ParameterValue": "value4,value5,value6"}, - {"ParameterKey": "key3", "ParameterValue": "value7,value8,value9"} + {"ParameterKey": "key3", "ParameterValue": "value7,value8,value9"}, ] def test_format_parameters_with_none_and_list_values(self): parameters = { "key1": ["value1", "value2", "value3"], "key2": None, - "key3": ["value7", "value8", "value9"] + "key3": ["value7", "value8", "value9"], } formatted_parameters = self.actions._format_parameters(parameters) sorted_formatted_parameters = sorted( - formatted_parameters, - key=lambda x: x["ParameterKey"] + formatted_parameters, key=lambda x: x["ParameterKey"] ) assert sorted_formatted_parameters == [ {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"}, - {"ParameterKey": "key3", "ParameterValue": "value7,value8,value9"} + {"ParameterKey": "key3", "ParameterValue": "value7,value8,value9"}, ] def test_format_parameters_with_list_and_string_values(self): parameters = { "key1": ["value1", "value2", "value3"], "key2": "value4", - "key3": ["value5", "value6", "value7"] + "key3": ["value5", "value6", "value7"], } formatted_parameters = self.actions._format_parameters(parameters) sorted_formatted_parameters = sorted( - formatted_parameters, - key=lambda x: x["ParameterKey"] + formatted_parameters, key=lambda x: x["ParameterKey"] ) assert sorted_formatted_parameters == [ {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"}, {"ParameterKey": "key2", "ParameterValue": "value4"}, - {"ParameterKey": "key3", "ParameterValue": "value5,value6,value7"} + {"ParameterKey": "key3", "ParameterValue": "value5,value6,value7"}, ] def test_format_parameters_with_none_list_and_string_values(self): parameters = { "key1": ["value1", "value2", "value3"], "key2": "value4", - "key3": None + "key3": None, } formatted_parameters = self.actions._format_parameters(parameters) sorted_formatted_parameters = sorted( - formatted_parameters, - key=lambda x: x["ParameterKey"] + formatted_parameters, key=lambda x: x["ParameterKey"] ) assert sorted_formatted_parameters == [ {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"}, @@ -935,9 +853,7 @@ def test_format_parameters_with_none_list_and_string_values(self): @patch("sceptre.plan.actions.StackActions._describe") def test_get_status_with_created_stack(self, mock_describe): - mock_describe.return_value = { - "Stacks": [{"StackStatus": "CREATE_COMPLETE"}] - } + mock_describe.return_value = {"Stacks": [{"StackStatus": "CREATE_COMPLETE"}]} status = self.actions.get_status() assert status == "CREATE_COMPLETE" @@ -947,23 +863,18 @@ def test_get_status_with_non_existent_stack(self, mock_describe): { "Error": { "Code": "DoesNotExistException", - "Message": "Stack does not exist" + "Message": "Stack does not exist", } }, - sentinel.operation + sentinel.operation, ) assert self.actions.get_status() == "PENDING" @patch("sceptre.plan.actions.StackActions._describe") def test_get_status_with_unknown_clinet_error(self, mock_describe): mock_describe.side_effect = ClientError( - { - "Error": { - "Code": "DoesNotExistException", - "Message": "Boom!" - } - }, - sentinel.operation + {"Error": {"Code": "DoesNotExistException", "Message": "Boom!"}}, + sentinel.operation, ) with pytest.raises(ClientError): self.actions.get_status() @@ -991,8 +902,7 @@ def test_protect_execution_with_protection(self): @patch("sceptre.plan.actions.StackActions._get_status") @patch("sceptre.plan.actions.StackActions._get_simplified_status") def test_wait_for_completion_calls_log_new_events( - self, mock_get_simplified_status, mock_get_status, - mock_log_new_events + self, mock_get_simplified_status, mock_get_status, mock_log_new_events ): mock_get_simplified_status.return_value = StackStatus.COMPLETE @@ -1000,14 +910,17 @@ def test_wait_for_completion_calls_log_new_events( mock_log_new_events.assert_called_once() assert type(mock_log_new_events.mock_calls[0].args[0]) is datetime.datetime - @pytest.mark.parametrize("test_input,expected", [ - ("ROLLBACK_COMPLETE", StackStatus.FAILED), - ("STACK_COMPLETE", StackStatus.COMPLETE), - ("STACK_IN_PROGRESS", StackStatus.IN_PROGRESS), - ("STACK_FAILED", StackStatus.FAILED) - ]) + @pytest.mark.parametrize( + "test_input,expected", + [ + ("ROLLBACK_COMPLETE", StackStatus.FAILED), + ("STACK_COMPLETE", StackStatus.COMPLETE), + ("STACK_IN_PROGRESS", StackStatus.IN_PROGRESS), + ("STACK_FAILED", StackStatus.FAILED), + ], + ) def test_get_simplified_status_with_known_stack_statuses( - self, test_input, expected + self, test_input, expected ): response = self.actions._get_simplified_status(test_input) assert response == expected @@ -1018,9 +931,7 @@ def test_get_simplified_status_with_stack_in_unknown_state(self): @patch("sceptre.plan.actions.StackActions.describe_events") def test_log_new_events_calls_describe_events(self, mock_describe_events): - mock_describe_events.return_value = { - "StackEvents": [] - } + mock_describe_events.return_value = {"StackEvents": []} self.actions._log_new_events(datetime.datetime.utcnow()) self.actions.describe_events.assert_called_once_with() @@ -1035,7 +946,7 @@ def test_log_new_events_prints_correct_event(self, mock_describe_events): ), "LogicalResourceId": "id-2", "ResourceType": "type-2", - "ResourceStatus": "resource-status" + "ResourceStatus": "resource-status", }, { "Timestamp": datetime.datetime( @@ -1044,206 +955,248 @@ def test_log_new_events_prints_correct_event(self, mock_describe_events): "LogicalResourceId": "id-1", "ResourceType": "type-1", "ResourceStatus": "resource", - "ResourceStatusReason": "User Initiated" - } + "ResourceStatusReason": "User Initiated", + }, ] } - self.actions._log_new_events(datetime.datetime(2016, 3, 15, 14, 0, 0, 0, tzinfo=tzutc())) + self.actions._log_new_events( + datetime.datetime(2016, 3, 15, 14, 0, 0, 0, tzinfo=tzutc()) + ) @patch("sceptre.plan.actions.StackActions._get_cs_status") - def test_wait_for_cs_completion_calls_get_cs_status( - self, mock_get_cs_status - ): + def test_wait_for_cs_completion_calls_get_cs_status(self, mock_get_cs_status): mock_get_cs_status.side_effect = [ - StackChangeSetStatus.PENDING, StackChangeSetStatus.READY + StackChangeSetStatus.PENDING, + StackChangeSetStatus.READY, ] self.actions.wait_for_cs_completion(sentinel.change_set_name) mock_get_cs_status.assert_called_with(sentinel.change_set_name) @patch("sceptre.plan.actions.StackActions.describe_change_set") - def test_get_cs_status_handles_all_statuses( - self, mock_describe_change_set - ): + def test_get_cs_status_handles_all_statuses(self, mock_describe_change_set): scss = StackChangeSetStatus - return_values = { # NOQA - "Status": ('CREATE_PENDING', 'CREATE_IN_PROGRESS', 'CREATE_COMPLETE', 'DELETE_COMPLETE', 'FAILED'), # NOQA - "ExecutionStatus": { # NOQA - 'UNAVAILABLE': (scss.PENDING, scss.PENDING, scss.PENDING, scss.DEFUNCT, scss.DEFUNCT), # NOQA - 'AVAILABLE': (scss.PENDING, scss.PENDING, scss.READY, scss.DEFUNCT, scss.DEFUNCT), # NOQA - 'EXECUTE_IN_PROGRESS': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT), # NOQA - 'EXECUTE_COMPLETE': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT), # NOQA - 'EXECUTE_FAILED': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT), # NOQA - 'OBSOLETE': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT), # NOQA - } # NOQA - } # NOQA - - for i, status in enumerate(return_values['Status']): - for exec_status, returns in \ - return_values['ExecutionStatus'].items(): + return_values = { # NOQA + "Status": ( + "CREATE_PENDING", + "CREATE_IN_PROGRESS", + "CREATE_COMPLETE", + "DELETE_COMPLETE", + "FAILED", + ), # NOQA + "ExecutionStatus": { # NOQA + "UNAVAILABLE": ( + scss.PENDING, + scss.PENDING, + scss.PENDING, + scss.DEFUNCT, + scss.DEFUNCT, + ), # NOQA + "AVAILABLE": ( + scss.PENDING, + scss.PENDING, + scss.READY, + scss.DEFUNCT, + scss.DEFUNCT, + ), # NOQA + "EXECUTE_IN_PROGRESS": ( + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + ), # NOQA + "EXECUTE_COMPLETE": ( + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + ), # NOQA + "EXECUTE_FAILED": ( + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + ), # NOQA + "OBSOLETE": ( + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + scss.DEFUNCT, + ), # NOQA + }, # NOQA + } # NOQA + + for i, status in enumerate(return_values["Status"]): + for exec_status, returns in return_values["ExecutionStatus"].items(): mock_describe_change_set.return_value = { "Status": status, - "ExecutionStatus": exec_status + "ExecutionStatus": exec_status, } - response = self.actions._get_cs_status( - sentinel.change_set_name - ) + response = self.actions._get_cs_status(sentinel.change_set_name) assert response == returns[i] - for status in return_values['Status']: + for status in return_values["Status"]: mock_describe_change_set.return_value = { "Status": status, - "ExecutionStatus": 'UNKOWN_STATUS' + "ExecutionStatus": "UNKOWN_STATUS", } with pytest.raises(UnknownStackChangeSetStatusError): self.actions._get_cs_status(sentinel.change_set_name) - for exec_status in return_values['ExecutionStatus'].keys(): + for exec_status in return_values["ExecutionStatus"].keys(): mock_describe_change_set.return_value = { - "Status": 'UNKOWN_STATUS', - "ExecutionStatus": exec_status + "Status": "UNKOWN_STATUS", + "ExecutionStatus": exec_status, } with pytest.raises(UnknownStackChangeSetStatusError): self.actions._get_cs_status(sentinel.change_set_name) mock_describe_change_set.return_value = { - "Status": 'UNKOWN_STATUS', - "ExecutionStatus": 'UNKOWN_STATUS', + "Status": "UNKOWN_STATUS", + "ExecutionStatus": "UNKOWN_STATUS", } with pytest.raises(UnknownStackChangeSetStatusError): self.actions._get_cs_status(sentinel.change_set_name) @patch("sceptre.plan.actions.StackActions.describe_change_set") - def test_get_cs_status_raises_unexpected_exceptions( - self, mock_describe_change_set - ): + def test_get_cs_status_raises_unexpected_exceptions(self, mock_describe_change_set): mock_describe_change_set.side_effect = ClientError( { "Error": { "Code": "ChangeSetNotFound", - "Message": "ChangeSet [*] does not exist" + "Message": "ChangeSet [*] does not exist", } }, - sentinel.operation + sentinel.operation, ) with pytest.raises(ClientError): self.actions._get_cs_status(sentinel.change_set_name) - def test_fetch_remote_template__cloudformation_returns_validation_error__returns_none(self): + def test_fetch_remote_template__cloudformation_returns_validation_error__returns_none( + self, + ): self.actions.connection_manager.call.side_effect = ClientError( { "Error": { "Code": "ValidationError", "Message": "An error occurred (ValidationError) " - "when calling the GetTemplate operation: " - "Stack with id foo does not exist" + "when calling the GetTemplate operation: " + "Stack with id foo does not exist", } }, - sentinel.operation + sentinel.operation, ) result = self.actions.fetch_remote_template() assert result is None def test_fetch_remote_template__calls_cloudformation_get_template(self): - self.actions.connection_manager.call.return_value = {'TemplateBody': ''} + self.actions.connection_manager.call.return_value = {"TemplateBody": ""} self.actions.fetch_remote_template() self.actions.connection_manager.call.assert_called_with( - service='cloudformation', - command='get_template', - kwargs={ - 'StackName': self.stack.external_name, - 'TemplateStage': 'Original' - } + service="cloudformation", + command="get_template", + kwargs={"StackName": self.stack.external_name, "TemplateStage": "Original"}, ) def test_fetch_remote_template__dict_template__returns_json(self): - template_body = { - 'AWSTemplateFormatVersion': '2010-09-09', - 'Resources': {} - } + template_body = {"AWSTemplateFormatVersion": "2010-09-09", "Resources": {}} self.actions.connection_manager.call.return_value = { - 'TemplateBody': template_body + "TemplateBody": template_body } expected = json.dumps(template_body, indent=4) result = self.actions.fetch_remote_template() assert result == expected - def test_fetch_remote_template__cloudformation_returns_string_template__returns_that_string(self): + def test_fetch_remote_template__cloudformation_returns_string_template__returns_that_string( + self, + ): template_body = "This is my template" self.actions.connection_manager.call.return_value = { - 'TemplateBody': template_body + "TemplateBody": template_body } result = self.actions.fetch_remote_template() assert result == template_body - def test_fetch_remote_template_summary__calls_cloudformation_get_template_summary(self): + def test_fetch_remote_template_summary__calls_cloudformation_get_template_summary( + self, + ): self.actions.fetch_remote_template_summary() self.actions.connection_manager.call.assert_called_with( - service='cloudformation', - command='get_template_summary', + service="cloudformation", + command="get_template_summary", kwargs={ - 'StackName': self.stack.external_name, - } + "StackName": self.stack.external_name, + }, ) def test_fetch_remote_template_summary__returns_response_from_cloudformation(self): def get_template_summary(service, command, kwargs): - assert (service, command) == ('cloudformation', 'get_template_summary') - return {'template': 'summary'} + assert (service, command) == ("cloudformation", "get_template_summary") + return {"template": "summary"} self.actions.connection_manager.call.side_effect = get_template_summary result = self.actions.fetch_remote_template_summary() - assert result == {'template': 'summary'} + assert result == {"template": "summary"} - def test_fetch_local_template_summary__calls_cloudformation_get_template_summary(self): + def test_fetch_local_template_summary__calls_cloudformation_get_template_summary( + self, + ): self.actions.fetch_local_template_summary() self.actions.connection_manager.call.assert_called_with( - service='cloudformation', - command='get_template_summary', + service="cloudformation", + command="get_template_summary", kwargs={ - 'TemplateBody': self.stack.template.body, - } + "TemplateBody": self.stack.template.body, + }, ) def test_fetch_local_template_summary__returns_response_from_cloudformation(self): def get_template_summary(service, command, kwargs): - assert (service, command) == ('cloudformation', 'get_template_summary') - return {'template': 'summary'} + assert (service, command) == ("cloudformation", "get_template_summary") + return {"template": "summary"} self.actions.connection_manager.call.side_effect = get_template_summary result = self.actions.fetch_local_template_summary() - assert result == {'template': 'summary'} + assert result == {"template": "summary"} - def test_fetch_local_template_summary__cloudformation_returns_validation_error_invalid_stack__raises_it(self): + def test_fetch_local_template_summary__cloudformation_returns_validation_error_invalid_stack__raises_it( + self, + ): self.actions.connection_manager.call.side_effect = ClientError( { "Error": { "Code": "ValidationError", "Message": "Template format error: Resource name {Invalid::Resource} is " - "non alphanumeric.'" + "non alphanumeric.'", } }, - sentinel.operation + sentinel.operation, ) with pytest.raises(ClientError): self.actions.fetch_local_template_summary() - def test_fetch_remote_template_summary__cloudformation_returns_validation_error_for_no_stack__returns_none(self): + def test_fetch_remote_template_summary__cloudformation_returns_validation_error_for_no_stack__returns_none( + self, + ): self.actions.connection_manager.call.side_effect = ClientError( { "Error": { "Code": "ValidationError", "Message": "An error occurred (ValidationError) " - "when calling the GetTemplate operation: " - "Stack with id foo does not exist" + "when calling the GetTemplate operation: " + "Stack with id foo does not exist", } }, - sentinel.operation + sentinel.operation, ) result = self.actions.fetch_remote_template_summary() assert result is None @@ -1265,7 +1218,7 @@ def test_drift_detect( self, mock_sleep, mock_detect_stack_drift, - mock_describe_stack_drift_detection_status + mock_describe_stack_drift_detection_status, ): mock_sleep.return_value = None @@ -1278,7 +1231,7 @@ def test_drift_detect( "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62", "DetectionStatus": "DETECTION_IN_PROGRESS", "StackDriftStatus": "NOT_CHECKED", - "DetectionStatusReason": "User Initiated" + "DetectionStatusReason": "User Initiated", } final_response = { @@ -1286,17 +1239,20 @@ def test_drift_detect( "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62", "StackDriftStatus": "IN_SYNC", "DetectionStatus": "DETECTION_COMPLETE", - "DriftedStackResourceCount": 0 + "DriftedStackResourceCount": 0, } mock_describe_stack_drift_detection_status.side_effect = [ - first_response, final_response + first_response, + final_response, ] response = self.actions.drift_detect() assert response == final_response - @pytest.mark.parametrize("detection_status", ["DETECTION_COMPLETE", "DETECTION_FAILED"]) + @pytest.mark.parametrize( + "detection_status", ["DETECTION_COMPLETE", "DETECTION_FAILED"] + ) @patch("sceptre.plan.actions.StackActions._describe_stack_resource_drifts") @patch("sceptre.plan.actions.StackActions._describe_stack_drift_detection_status") @patch("sceptre.plan.actions.StackActions._detect_stack_drift") @@ -1307,7 +1263,7 @@ def test_drift_show( mock_detect_stack_drift, mock_describe_stack_drift_detection_status, mock_describe_stack_resource_drifts, - detection_status + detection_status, ): mock_sleep.return_value = None @@ -1320,15 +1276,15 @@ def test_drift_show( "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62", "DetectionStatus": "DETECTION_IN_PROGRESS", "StackDriftStatus": "FOO", - "DetectionStatusReason": "User Initiated" + "DetectionStatusReason": "User Initiated", }, { "StackId": "fake-stack-id", "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62", "StackDriftStatus": "FOO", "DetectionStatus": detection_status, - "DriftedStackResourceCount": 0 - } + "DriftedStackResourceCount": 0, + }, ] expected_drifts = { @@ -1341,7 +1297,7 @@ def test_drift_show( "ExpectedProperties": '{"foo":"bar"}', "ActualProperties": '{"foo":"bar"}', "PropertyDifferences": [], - "StackResourceDriftStatus": detection_status + "StackResourceDriftStatus": detection_status, } ] } @@ -1362,7 +1318,7 @@ def test_drift_show_drift_only( mock_sleep, mock_detect_stack_drift, mock_describe_stack_drift_detection_status, - mock_describe_stack_resource_drifts + mock_describe_stack_resource_drifts, ): mock_sleep.return_value = None @@ -1374,7 +1330,7 @@ def test_drift_show_drift_only( "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62", "StackDriftStatus": "DRIFTED", "DetectionStatus": "DETECTION_COMPLETE", - "DriftedStackResourceCount": 0 + "DriftedStackResourceCount": 0, } input_drifts = { @@ -1392,14 +1348,15 @@ def test_drift_show_drift_only( "ResourceType": "AWS::EC2::Instance", "StackId": "fake-stack-id", "StackResourceDriftStatus": "DELETED", - } + }, ] } mock_describe_stack_resource_drifts.return_value = input_drifts - expected_response = ("DETECTION_COMPLETE", { - "StackResourceDrifts": [input_drifts["StackResourceDrifts"][1]] - }) + expected_response = ( + "DETECTION_COMPLETE", + {"StackResourceDrifts": [input_drifts["StackResourceDrifts"][1]]}, + ) response = self.actions.drift_show(drifted=True) @@ -1410,9 +1367,9 @@ def test_drift_show_with_stack_that_does_not_exist(self, mock_get_status): mock_get_status.side_effect = StackDoesNotExistError() response = self.actions.drift_show(drifted=False) assert response == ( - 'STACK_DOES_NOT_EXIST', { - 'StackResourceDriftStatus': 'STACK_DOES_NOT_EXIST' - }) + "STACK_DOES_NOT_EXIST", + {"StackResourceDriftStatus": "STACK_DOES_NOT_EXIST"}, + ) @patch("sceptre.plan.actions.StackActions._describe_stack_resource_drifts") @patch("sceptre.plan.actions.StackActions._describe_stack_drift_detection_status") @@ -1436,7 +1393,7 @@ def test_drift_show_times_out( "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62", "DetectionStatus": "DETECTION_IN_PROGRESS", "StackDriftStatus": "FOO", - "DetectionStatusReason": "User Initiated" + "DetectionStatusReason": "User Initiated", } side_effect = [] @@ -1454,7 +1411,7 @@ def test_drift_show_times_out( "ExpectedProperties": '{"foo":"bar"}', "ActualProperties": '{"foo":"bar"}', "PropertyDifferences": [], - "StackResourceDriftStatus": "DETECTION_IN_PROGRESS" + "StackResourceDriftStatus": "DETECTION_IN_PROGRESS", } ] } diff --git a/tests/test_cli/test_cli_commands.py b/tests/test_cli/test_cli_commands.py index a7cccb093..c3b4fd518 100644 --- a/tests/test_cli/test_cli_commands.py +++ b/tests/test_cli/test_cli_commands.py @@ -15,14 +15,22 @@ from unittest.mock import MagicMock, patch, sentinel from sceptre.cli import cli -from sceptre.cli.helpers import CustomJsonEncoder, \ - catch_exceptions, setup_logging, write, \ - ColouredFormatter, deserialize_json_properties +from sceptre.cli.helpers import ( + CustomJsonEncoder, + catch_exceptions, + setup_logging, + write, + ColouredFormatter, + deserialize_json_properties, +) from sceptre.config.reader import ConfigReader -from sceptre.diffing.stack_differ import \ - DeepDiffStackDiffer, DifflibStackDiffer, StackDiff +from sceptre.diffing.stack_differ import ( + DeepDiffStackDiffer, + DifflibStackDiffer, + StackDiff, +) from sceptre.exceptions import SceptreException from sceptre.plan.actions import StackActions @@ -31,7 +39,6 @@ class TestCli: - def setup_method(self, test_method): self.patcher_ConfigReader = patch("sceptre.plan.plan.ConfigReader") self.patcher_StackActions = patch("sceptre.plan.executor.StackActions") @@ -46,16 +53,17 @@ def setup_method(self, test_method): spec=Stack, region=None, profile=None, - external_name='mock-stack-external', + external_name="mock-stack-external", dependencies=[], ignore=False, obsolete=False, ) - self.mock_stack.name = 'mock-stack' + self.mock_stack.name = "mock-stack" - self.mock_config_reader.construct_stacks.return_value = \ - set([self.mock_stack]), set([self.mock_stack]) + self.mock_config_reader.construct_stacks.return_value = set( + [self.mock_stack] + ), set([self.mock_stack]) self.mock_stack_actions.stack = self.mock_stack @@ -88,219 +96,230 @@ def raises_exception(): with pytest.raises(SceptreException): raises_exception() - @pytest.mark.parametrize("command,files,output", [ - # one --var option - ( - ["--var", "a=1", "noop"], - {}, - {"a": "1"} - ), - # multiple --var options - ( - ["--var", "a=1", "--var", "b=2", "noop"], - {}, - {"a": "1", "b": "2"} - ), - # multiple --var options same key - ( - ["--var", "a=1", "--var", "a=2", "noop"], - {}, - {"a": "2"} - ), - ( - ["--var-file", "foo.yaml", "--var", "key3.subkey1.id=id2", "noop"], - { - "foo.yaml": { - "key1": "val1", - "key2": "val2", - "key3": { - "subkey1": { - "id": "id1" - } + @pytest.mark.parametrize( + "command,files,output", + [ + # one --var option + (["--var", "a=1", "noop"], {}, {"a": "1"}), + # multiple --var options + (["--var", "a=1", "--var", "b=2", "noop"], {}, {"a": "1", "b": "2"}), + # multiple --var options same key + (["--var", "a=1", "--var", "a=2", "noop"], {}, {"a": "2"}), + ( + ["--var-file", "foo.yaml", "--var", "key3.subkey1.id=id2", "noop"], + { + "foo.yaml": { + "key1": "val1", + "key2": "val2", + "key3": {"subkey1": {"id": "id1"}}, } - } - }, - { - "key1": "val1", - "key2": "val2", - "key3": {"subkey1": {"id": "id2"}} - } - ), - # one --var-file option - ( - ["--var-file", "foo.yaml", "noop"], - { - "foo.yaml": {"key1": "val1", "key2": "val2"} - }, - {"key1": "val1", "key2": "val2"} - ), - # multiple --var-file option - ( - ["--var-file", "foo.yaml", "--var-file", "bar.yaml", "noop"], - { - "foo.yaml": {"key1": "parent_value1", "key2": "parent_value2"}, - "bar.yaml": {"key2": "child_value2", "key3": "child_value3"} - }, - { - "key1": "parent_value1", - "key2": "child_value2", - "key3": "child_value3" - } - ), - # mix of --var and --var-file - ( - ["--var-file", "foo.yaml", "--var", "key2=var2", "noop"], - { - "foo.yaml": {"key1": "file1", "key2": "file2"} - }, - {"key1": "file1", "key2": "var2"} - ), - # multiple --var-file option, illustrating dictionaries not merged. - ( - ["--var-file", "foo.yaml", "--var-file", "bar.yaml", "noop"], - { - "foo.yaml": {"key1": {"a": "b"}}, - "bar.yaml": {"key1": {"c": "d"}} - }, - { - "key1": {"c": "d"} - } - ), - # multiple --var-file option, dictionaries merged. - ( - ["--merge-vars", "--var-file", "foo.yaml", "--var-file", "bar.yaml", "noop"], - { - "foo.yaml": {"key1": {"a": "b"}}, - "bar.yaml": {"key1": {"c": "d"}} - }, - { - "key1": {"a": "b", "c": "d"} - } - ), - # multiple --var-file option, dictionaries merged, complex example. - ( - ["--merge-vars", "--var-file", "common.yaml", "--var-file", "dev.yaml", "noop"], - { - "common.yaml": { + }, + {"key1": "val1", "key2": "val2", "key3": {"subkey1": {"id": "id2"}}}, + ), + # one --var-file option + ( + ["--var-file", "foo.yaml", "noop"], + {"foo.yaml": {"key1": "val1", "key2": "val2"}}, + {"key1": "val1", "key2": "val2"}, + ), + # multiple --var-file option + ( + ["--var-file", "foo.yaml", "--var-file", "bar.yaml", "noop"], + { + "foo.yaml": {"key1": "parent_value1", "key2": "parent_value2"}, + "bar.yaml": {"key2": "child_value2", "key3": "child_value3"}, + }, + { + "key1": "parent_value1", + "key2": "child_value2", + "key3": "child_value3", + }, + ), + # mix of --var and --var-file + ( + ["--var-file", "foo.yaml", "--var", "key2=var2", "noop"], + {"foo.yaml": {"key1": "file1", "key2": "file2"}}, + {"key1": "file1", "key2": "var2"}, + ), + # multiple --var-file option, illustrating dictionaries not merged. + ( + ["--var-file", "foo.yaml", "--var-file", "bar.yaml", "noop"], + {"foo.yaml": {"key1": {"a": "b"}}, "bar.yaml": {"key1": {"c": "d"}}}, + {"key1": {"c": "d"}}, + ), + # multiple --var-file option, dictionaries merged. + ( + [ + "--merge-vars", + "--var-file", + "foo.yaml", + "--var-file", + "bar.yaml", + "noop", + ], + {"foo.yaml": {"key1": {"a": "b"}}, "bar.yaml": {"key1": {"c": "d"}}}, + {"key1": {"a": "b", "c": "d"}}, + ), + # multiple --var-file option, dictionaries merged, complex example. + ( + [ + "--merge-vars", + "--var-file", + "common.yaml", + "--var-file", + "dev.yaml", + "noop", + ], + { + "common.yaml": { + "CommonTags": { + "Organization": "Parts Unlimited", + "Department": "IT Operations", + } + }, + "dev.yaml": {"CommonTags": {"Environment": "dev"}}, + }, + { "CommonTags": { "Organization": "Parts Unlimited", - "Department": "IT Operations" + "Department": "IT Operations", + "Environment": "dev", } }, - "dev.yaml": {"CommonTags": {"Environment": "dev"}} - }, - { - "CommonTags": { - "Organization": "Parts Unlimited", - "Department": "IT Operations", - "Environment": "dev" - } - } - ), - # multiple --var-file option, dictionaries merged, complex example, with overrides. - ( - ["--merge-vars", "--var-file", "common.yaml", "--var-file", "dev.yaml", "noop"], - { - "common.yaml": { + ), + # multiple --var-file option, dictionaries merged, complex example, with overrides. + ( + [ + "--merge-vars", + "--var-file", + "common.yaml", + "--var-file", + "dev.yaml", + "noop", + ], + { + "common.yaml": { + "CommonTags": { + "Organization": "Parts Unlimited", + "Department": "IT Operations", + "Environment": "sandbox", + } + }, + "dev.yaml": {"CommonTags": {"Environment": "dev"}}, + }, + { "CommonTags": { "Organization": "Parts Unlimited", "Department": "IT Operations", - "Environment": "sandbox" + "Environment": "dev", } }, - "dev.yaml": {"CommonTags": {"Environment": "dev"}}, - }, - { - "CommonTags": { - "Organization": "Parts Unlimited", - "Department": "IT Operations", - "Environment": "dev" - } - } - ), - # multiple --var-file option, dictionaries merged, complex example, with lists. - ( - ["--merge-vars", "--var-file", "common.yaml", "--var-file", "test.yaml", "noop"], - { - "common.yaml": { + ), + # multiple --var-file option, dictionaries merged, complex example, with lists. + ( + [ + "--merge-vars", + "--var-file", + "common.yaml", + "--var-file", + "test.yaml", + "noop", + ], + { + "common.yaml": { + "CommonTags": { + "Organization": "Parts Unlimited", + "Department": "IT Operations", + "Envlist": ["sandbox", "dev"], + } + }, + "test.yaml": {"CommonTags": {"Envlist": ["test"]}}, + }, + { "CommonTags": { "Organization": "Parts Unlimited", "Department": "IT Operations", - "Envlist": ["sandbox", "dev"] + "Envlist": ["test"], } }, - "test.yaml": {"CommonTags": {"Envlist": ["test"]}}, - }, - { - "CommonTags": { - "Organization": "Parts Unlimited", - "Department": "IT Operations", - "Envlist": ["test"] - } - } - ), - # multiple --var-file option, dictionaries merged, multiple levels. - ( - ["--merge-vars", "--var-file", "common.yaml", "--var-file", "test.yaml", "noop"], - { - "common.yaml": {"a": {"b": {"c": "p", "d": "q"}}}, - "test.yaml": {"a": {"b": {"c": "r", "e": "s"}}} - }, - { - "a": {"b": {"c": "r", "d": "q", "e": "s"}} - } - ), - # a --var-file and --var combined. - ( - ["--merge-vars", "--var-file", "common.yaml", "--var", "CommonTags.Version=1.0.0", "noop"], - { - "common.yaml": { + ), + # multiple --var-file option, dictionaries merged, multiple levels. + ( + [ + "--merge-vars", + "--var-file", + "common.yaml", + "--var-file", + "test.yaml", + "noop", + ], + { + "common.yaml": {"a": {"b": {"c": "p", "d": "q"}}}, + "test.yaml": {"a": {"b": {"c": "r", "e": "s"}}}, + }, + {"a": {"b": {"c": "r", "d": "q", "e": "s"}}}, + ), + # a --var-file and --var combined. + ( + [ + "--merge-vars", + "--var-file", + "common.yaml", + "--var", + "CommonTags.Version=1.0.0", + "noop", + ], + { + "common.yaml": { + "CommonTags": { + "Organization": "Parts Unlimited", + "Department": "IT Operations", + "Envlist": ["sandbox", "dev"], + } + } + }, + { "CommonTags": { "Organization": "Parts Unlimited", "Department": "IT Operations", - "Envlist": ["sandbox", "dev"] + "Envlist": ["sandbox", "dev"], + "Version": "1.0.0", } - } - }, - { - "CommonTags": { - "Organization": "Parts Unlimited", - "Department": "IT Operations", - "Envlist": ["sandbox", "dev"], - "Version": "1.0.0" - } - } - ), - # multiple --var-file and --var combined. - ( - [ - "--merge-vars", "--var-file", "common.yaml", "--var-file", "test.yaml", - "--var", "CommonTags.Project=Unboxing", "noop" - ], - { - "common.yaml": { + }, + ), + # multiple --var-file and --var combined. + ( + [ + "--merge-vars", + "--var-file", + "common.yaml", + "--var-file", + "test.yaml", + "--var", + "CommonTags.Project=Unboxing", + "noop", + ], + { + "common.yaml": { + "CommonTags": { + "Organization": "Parts Unlimited", + "Department": "IT Operations", + "Envlist": ["sandbox", "dev"], + } + }, + "test.yaml": {"CommonTags": {"Project": "Boxing"}}, + }, + { "CommonTags": { "Organization": "Parts Unlimited", "Department": "IT Operations", - "Envlist": ["sandbox", "dev"] + "Envlist": ["sandbox", "dev"], + "Project": "Unboxing", } }, - "test.yaml": { - "CommonTags": { - "Project": "Boxing" - } - } - }, - { - "CommonTags": { - "Organization": "Parts Unlimited", - "Department": "IT Operations", - "Envlist": ["sandbox", "dev"], - "Project": "Unboxing" - } - } - ) - ]) + ), + ], + ) def test_user_variables(self, command, files, output): @cli.command() @click.pass_context @@ -321,90 +340,84 @@ def noop(ctx): def test_validate_template_with_valid_template(self): self.mock_stack_actions.validate.return_value = { "Parameters": "Example", - "ResponseMetadata": { - "HTTPStatusCode": 200 - } + "ResponseMetadata": {"HTTPStatusCode": 200}, } - result_json = json.dumps({'Parameters': 'Example'}, indent=4) - result = self.runner.invoke(cli, ["--output", "json", "validate", "dev/vpc.yaml"]) + result_json = json.dumps({"Parameters": "Example"}, indent=4) + result = self.runner.invoke( + cli, ["--output", "json", "validate", "dev/vpc.yaml"] + ) self.mock_stack_actions.validate.assert_called_with() - assert result.output == "Template mock-stack is valid. Template details:\n\n{}\n".format( - result_json) + assert ( + result.output + == "Template mock-stack is valid. Template details:\n\n{}\n".format( + result_json + ) + ) def test_validate_template_with_invalid_template(self): client_error = ClientError( { - "Errors": - { + "Errors": { "Message": "Unrecognized resource types", "Code": "ValidationError", } }, - "ValidateTemplate" + "ValidateTemplate", ) self.mock_stack_actions.validate.side_effect = client_error expected_result = str(client_error) + "\n" - result = self.runner.invoke(cli, ["--output", "json", "validate", "dev/vpc.yaml"]) - assert expected_result in result.output.replace("\"", "") + result = self.runner.invoke( + cli, ["--output", "json", "validate", "dev/vpc.yaml"] + ) + assert expected_result in result.output.replace('"', "") def test_estimate_template_cost_with_browser(self): self.mock_stack_actions.estimate_cost.return_value = { "Url": "https://docs.sceptre-project.org", - "ResponseMetadata": { - "HTTPStatusCode": 200 - } + "ResponseMetadata": {"HTTPStatusCode": 200}, } args = ["estimate-cost", "dev/vpc.yaml"] - with patch('webbrowser.open', return_value=None): # Do not open a web browser + with patch("webbrowser.open", return_value=None): # Do not open a web browser result = self.runner.invoke(cli, args) self.mock_stack_actions.estimate_cost.assert_called_with() - assert result.output == \ - '{0}{1}'.format("View the estimated cost for mock-stack at:\n", - "https://docs.sceptre-project.org\n\n") + assert result.output == "{0}{1}".format( + "View the estimated cost for mock-stack at:\n", + "https://docs.sceptre-project.org\n\n", + ) def test_estimate_template_cost_with_no_browser(self): client_error = ClientError( { - "Errors": - { + "Errors": { "Message": "No Browser", "Code": "Error", } }, - "Webbrowser" + "Webbrowser", ) self.mock_stack_actions.estimate_cost.side_effect = client_error expected_result = "{}\n".format(client_error) - result = self.runner.invoke( - cli, - ["estimate-cost", "dev/vpc.yaml"] - ) - assert expected_result in result.output.replace("\"", "") + result = self.runner.invoke(cli, ["estimate-cost", "dev/vpc.yaml"]) + assert expected_result in result.output.replace('"', "") def test_lock_stack(self): - self.runner.invoke( - cli, ["set-policy", "dev/vpc.yaml", "-b", "deny-all"] - ) + self.runner.invoke(cli, ["set-policy", "dev/vpc.yaml", "-b", "deny-all"]) self.mock_config_reader.construct_stacks.assert_called_with() self.mock_stack_actions.lock.assert_called_with() def test_unlock_stack(self): - self.runner.invoke( - cli, ["set-policy", "dev/vpc.yaml", "-b", "allow-all"] - ) + self.runner.invoke(cli, ["set-policy", "dev/vpc.yaml", "-b", "allow-all"]) self.mock_config_reader.construct_stacks.assert_called_with() self.mock_stack_actions.unlock.assert_called_with() def test_set_policy_with_file_flag(self): policy_file = "tests/fixtures/stack_policies/lock.json" - result = self.runner.invoke(cli, [ - "set-policy", "dev/vpc.yaml", policy_file - ]) + result = self.runner.invoke(cli, ["set-policy", "dev/vpc.yaml", policy_file]) assert result.exit_code == 0 def test_describe_policy_with_existing_policy(self): @@ -416,8 +429,9 @@ def test_describe_policy_with_existing_policy(self): cli, ["--output", "json", "describe", "policy", "dev/vpc.yaml"] ) assert result.exit_code == 0 - assert result.output == "{}\n".format(json.dumps( - {'dev/vpc': {'Statement': ['Body']}}, indent=4)) + assert result.output == "{}\n".format( + json.dumps({"dev/vpc": {"Statement": ["Body"]}}, indent=4) + ) def test_list_group_resources(self): response = { @@ -425,7 +439,7 @@ def test_list_group_resources(self): "StackResources": [ { "LogicalResourceId": "logical-resource-id", - "PhysicalResourceId": "physical-resource-id" + "PhysicalResourceId": "physical-resource-id", } ] }, @@ -433,13 +447,15 @@ def test_list_group_resources(self): "StackResources": [ { "LogicalResourceId": "logical-resource-id", - "PhysicalResourceId": "physical-resource-id" + "PhysicalResourceId": "physical-resource-id", } ] - } + }, } self.mock_stack_actions.describe_resources.return_value = response - result = self.runner.invoke(cli, ["--output", "yaml", "list", "resources", "dev"]) + result = self.runner.invoke( + cli, ["--output", "yaml", "list", "resources", "dev"] + ) assert yaml.safe_load(result.output) == [response] assert result.exit_code == 0 @@ -449,17 +465,20 @@ def test_list_stack_resources(self): "StackResources": [ { "LogicalResourceId": "logical-resource-id", - "PhysicalResourceId": "physical-resource-id" + "PhysicalResourceId": "physical-resource-id", } ] } self.mock_stack_actions.describe_resources.return_value = response - result = self.runner.invoke(cli, ["--output", "yaml", "list", "resources", "dev/vpc.yaml"]) + result = self.runner.invoke( + cli, ["--output", "yaml", "list", "resources", "dev/vpc.yaml"] + ) assert yaml.safe_load(result.output) == [response] assert result.exit_code == 0 @pytest.mark.parametrize( - "command,success,yes_flag,exit_code", [ + "command,success,yes_flag,exit_code", + [ ("create", True, True, 0), ("create", False, True, 1), ("create", True, False, 0), @@ -475,13 +494,14 @@ def test_list_stack_resources(self): ("launch", True, True, 0), ("launch", False, True, 1), ("launch", True, False, 0), - ("launch", False, False, 1) - ] + ("launch", False, False, 1), + ], ) def test_stack_commands(self, command, success, yes_flag, exit_code): run_command = getattr(self.mock_stack_actions, command) - run_command.return_value = \ + run_command.return_value = ( StackStatus.COMPLETE if success else StackStatus.FAILED + ) kwargs = {"args": [command, "dev/vpc.yaml"]} if yes_flag: @@ -495,12 +515,13 @@ def test_stack_commands(self, command, success, yes_flag, exit_code): assert result.exit_code == exit_code @pytest.mark.parametrize( - "command, ignore_dependencies", [ + "command, ignore_dependencies", + [ ("create", True), ("create", False), ("delete", True), ("delete", False), - ] + ], ) def test_ignore_dependencies_commands(self, command, ignore_dependencies): args = [command, "dev/vpc.yaml", "cs-1", "-y"] @@ -510,14 +531,15 @@ def test_ignore_dependencies_commands(self, command, ignore_dependencies): assert result.exit_code == 0 @pytest.mark.parametrize( - "command,yes_flag", [ + "command,yes_flag", + [ ("create", True), ("create", False), ("delete", True), ("delete", False), ("execute", True), - ("execute", False) - ] + ("execute", False), + ], ) def test_change_set_commands(self, command, yes_flag): stack_command = command + "_change_set" @@ -530,16 +552,10 @@ def test_change_set_commands(self, command, yes_flag): result = self.runner.invoke(cli, **kwargs) - getattr(self.mock_stack_actions, - stack_command).assert_called_with("cs1") + getattr(self.mock_stack_actions, stack_command).assert_called_with("cs1") assert result.exit_code == 0 - @pytest.mark.parametrize( - "verbose_flag,", [ - (False), - (True) - ] - ) + @pytest.mark.parametrize("verbose_flag,", [(False), (True)]) def test_describe_change_set(self, verbose_flag): response = { "VerboseProperty": "VerboseProperty", @@ -558,10 +574,10 @@ def test_describe_change_set(self, verbose_flag): "Replacement": "Replacement", "ResourceType": "ResourceType", "Scope": "Scope", - "VerboseProperty": "VerboseProperty" + "VerboseProperty": "VerboseProperty", } } - ] + ], } args = ["describe", "change-set", "region/vpc.yaml", "cs1"] if verbose_flag: @@ -576,19 +592,13 @@ def test_describe_change_set(self, verbose_flag): assert result.exit_code == 0 def test_list_change_sets_with_200(self): - self.mock_stack_actions.list_change_sets.return_value = { - "ChangeSets": "Test" - } - result = self.runner.invoke( - cli, ["list", "change-sets", "dev/vpc.yaml"] - ) + self.mock_stack_actions.list_change_sets.return_value = {"ChangeSets": "Test"} + result = self.runner.invoke(cli, ["list", "change-sets", "dev/vpc.yaml"]) assert result.exit_code == 0 assert yaml.safe_load(result.output) == {"ChangeSets": "Test"} def test_list_change_sets_without_200(self): - response = { - "ChangeSets": "Test" - } + response = {"ChangeSets": "Test"} self.mock_stack_actions.list_change_sets.return_value = response result = self.runner.invoke( @@ -613,21 +623,21 @@ def test_list_outputs_yaml(self): cli, ["--output", "yaml", "list", "outputs", "dev/vpc.yaml"] ) assert result.exit_code == 0 - expected_output = '---\n- OutputKey: Key\n OutputValue: Value\n\n' + expected_output = "---\n- OutputKey: Key\n OutputValue: Value\n\n" assert result.output == expected_output def test_list_outputs_text(self): - outputs = {"StackName": [{'OutputKey': "Key", "OutputValue": "Value"}]} + outputs = {"StackName": [{"OutputKey": "Key", "OutputValue": "Value"}]} self.mock_stack_actions.describe_outputs.return_value = outputs result = self.runner.invoke( cli, ["--output", "text", "list", "outputs", "dev/vpc.yaml"] ) assert result.exit_code == 0 - expected_output = 'StackOutputKeyOutputValue\n\nStackNameKeyValue\n' - assert result.output.replace(' ', '') == expected_output + expected_output = "StackOutputKeyOutputValue\n\nStackNameKeyValue\n" + assert result.output.replace(" ", "") == expected_output def test_list_outputs_with_export(self): - outputs = {'stack': [{'OutputKey': 'Key', 'OutputValue': 'Value'}]} + outputs = {"stack": [{"OutputKey": "Key", "OutputValue": "Value"}]} self.mock_stack_actions.describe_outputs.return_value = outputs result = self.runner.invoke( cli, ["list", "outputs", "dev/vpc.yaml", "-e", "envvar"] @@ -635,12 +645,19 @@ def test_list_outputs_with_export(self): assert result.exit_code == 0 assert result.output == "export SCEPTRE_Key='Value'\n" - @pytest.mark.parametrize("path,output_format,expected_output", [ - ("dev/vpc.yaml", "yaml", '---\nmock-stack.yaml: mock-stack-external\n\n'), - ("dev/vpc.yaml", "text", '---\nmock-stack.yaml: mock-stack-external\n\n'), - ("dev/vpc.yaml", "json", '{\n "mock-stack.yaml": "mock-stack-external"\n}\n'), - ("dev", "yaml", '---\nmock-stack.yaml: mock-stack-external\n\n') - ]) + @pytest.mark.parametrize( + "path,output_format,expected_output", + [ + ("dev/vpc.yaml", "yaml", "---\nmock-stack.yaml: mock-stack-external\n\n"), + ("dev/vpc.yaml", "text", "---\nmock-stack.yaml: mock-stack-external\n\n"), + ( + "dev/vpc.yaml", + "json", + '{\n "mock-stack.yaml": "mock-stack-external"\n}\n', + ), + ("dev", "yaml", "---\nmock-stack.yaml: mock-stack-external\n\n"), + ], + ) def test_list_stacks(self, path, output_format, expected_output): result = self.runner.invoke( cli, ["--output", output_format, "list", "stacks", path] @@ -649,13 +666,14 @@ def test_list_stacks(self, path, output_format, expected_output): assert result.stdout == expected_output def test_status_with_group(self): - self.mock_stack_actions.get_status.return_value = { - "stack": "status" - } + self.mock_stack_actions.get_status.return_value = {"stack": "status"} result = self.runner.invoke(cli, ["--output", "json", "status", "dev"]) assert result.exit_code == 0 - assert result.output == '{\n "mock-stack": {\n \"stack\": \"status\"\n }\n}\n' + assert ( + result.output + == '{\n "mock-stack": {\n "stack": "status"\n }\n}\n' + ) def test_status_with_stack(self): self.mock_stack_actions.get_status.return_value = "status" @@ -665,15 +683,12 @@ def test_status_with_stack(self): def test_new_project_non_existant(self): with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") template_dir = os.path.join(project_path, "templates") region = "test-region" os.environ["AWS_DEFAULT_REGION"] = region - defaults = { - "project_code": "example", - "region": region - } + defaults = {"project_code": "example", "region": region} result = self.runner.invoke(cli, ["new", "project", "example"]) assert not result.exception @@ -687,7 +702,7 @@ def test_new_project_non_existant(self): def test_new_project_already_exist(self): with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") template_dir = os.path.join(project_path, "templates") existing_config = {"Test": "Test"} @@ -697,7 +712,7 @@ def test_new_project_already_exist(self): os.mkdir(template_dir) config_filepath = os.path.join(config_dir, "config.yaml") - with open(config_filepath, 'w') as config_file: + with open(config_filepath, "w") as config_file: yaml.dump(existing_config, config_file) result = self.runner.invoke(cli, ["new", "project", "example"]) @@ -720,21 +735,15 @@ def test_new_project_another_exception(self): assert str(result.exception) == str(OSError(errno.EINVAL)) @pytest.mark.parametrize( - "stack_group,config_structure,stdin,result", [ - ( - "A", - {"": {}}, - 'y\nA\nA\n', {"project_code": "A", "region": "A"} - ), + "stack_group,config_structure,stdin,result", + [ + ("A", {"": {}}, "y\nA\nA\n", {"project_code": "A", "region": "A"}), + ("A", {"": {"project_code": "top", "region": "top"}}, "y\n\n\n", {}), ( "A", {"": {"project_code": "top", "region": "top"}}, - 'y\n\n\n', {} - ), - ( - "A", - {"": {"project_code": "top", "region": "top"}}, - 'y\nA\nA\n', {"project_code": "A", "region": "A"} + "y\nA\nA\n", + {"project_code": "A", "region": "A"}, ), ( "A/A", @@ -742,7 +751,8 @@ def test_new_project_another_exception(self): "": {"project_code": "top", "region": "top"}, "A": {"project_code": "A", "region": "A"}, }, - 'y\nA/A\nA/A\n', {"project_code": "A/A", "region": "A/A"} + "y\nA/A\nA/A\n", + {"project_code": "A/A", "region": "A/A"}, ), ( "A/A", @@ -750,15 +760,16 @@ def test_new_project_another_exception(self): "": {"project_code": "top", "region": "top"}, "A": {"project_code": "A", "region": "A"}, }, - 'y\nA\nA\n', {} - ) - ] + "y\nA\nA\n", + {}, + ), + ], ) def test_create_new_stack_group_folder( self, stack_group, config_structure, stdin, result ): with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") os.makedirs(config_dir) @@ -774,21 +785,17 @@ def test_create_new_stack_group_folder( raise filepath = os.path.join(path, "config.yaml") - with open(filepath, 'w') as config_file: - yaml.safe_dump( - config, stream=config_file, default_flow_style=False - ) + with open(filepath, "w") as config_file: + yaml.safe_dump(config, stream=config_file, default_flow_style=False) os.chdir(project_path) cmd_result = self.runner.invoke( - cli, ["new", "group", stack_group], - input=stdin + cli, ["new", "group", stack_group], input=stdin ) if result: - with open(os.path.join(stack_group_dir, "config.yaml"))\ - as config_file: + with open(os.path.join(stack_group_dir, "config.yaml")) as config_file: config = yaml.safe_load(config_file) assert config == result else: @@ -798,29 +805,25 @@ def test_create_new_stack_group_folder( def test_new_stack_group_folder_with_existing_folder(self): with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") stack_group_dir = os.path.join(config_dir, "A") os.makedirs(stack_group_dir) os.chdir(project_path) - cmd_result = self.runner.invoke( - cli, ["new", "group", "A"], input="y\n\n\n" - ) + cmd_result = self.runner.invoke(cli, ["new", "group", "A"], input="y\n\n\n") assert cmd_result.output.startswith( - "StackGroup path exists. " - "Do you want initialise config.yaml?" + "StackGroup path exists. " "Do you want initialise config.yaml?" ) - with open(os.path.join( - stack_group_dir, "config.yaml")) as config_file: + with open(os.path.join(stack_group_dir, "config.yaml")) as config_file: config = yaml.safe_load(config_file) assert config == {"project_code": "", "region": ""} def test_new_stack_group_folder_with_another_exception(self): with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") stack_group_dir = os.path.join(config_dir, "A") @@ -834,38 +837,19 @@ def test_new_stack_group_folder_with_another_exception(self): assert str(result.exception) == str(OSError(errno.EINVAL)) @pytest.mark.parametrize( - "cli_module,command,output_format,no_colour", [ - ( - 'describe', - ['describe', 'change-set', 'somepath', 'cs1'], - 'yaml', - True - ), - ( - 'describe', - ['describe', 'change-set', 'somepath', 'cs1'], - 'json', - False - ), - ( - 'describe', - ['describe', 'policy', 'somepolicy'], - 'yaml', - True - ), - ( - 'describe', - ['describe', 'policy', 'somepolicy'], - 'json', - False - ) - ] + "cli_module,command,output_format,no_colour", + [ + ("describe", ["describe", "change-set", "somepath", "cs1"], "yaml", True), + ("describe", ["describe", "change-set", "somepath", "cs1"], "json", False), + ("describe", ["describe", "policy", "somepolicy"], "yaml", True), + ("describe", ["describe", "policy", "somepolicy"], "json", False), + ], ) def test_write_output_format_flags( self, cli_module, command, output_format, no_colour ): - no_colour_flag = ['--no-colour'] if no_colour else [] - output_format_flag = ['--output', output_format] + no_colour_flag = ["--no-colour"] if no_colour else [] + output_format_flag = ["--output", output_format] args = output_format_flag + no_colour_flag + command with patch("sceptre.cli." + cli_module + ".write") as mock_write: @@ -879,8 +863,7 @@ def test_write_output_format_flags( def test_setup_logging_with_debug(self): logger = setup_logging(True, False) assert logger.getEffectiveLevel() == logging.DEBUG - assert logging.getLogger("botocore").getEffectiveLevel() == \ - logging.INFO + assert logging.getLogger("botocore").getEffectiveLevel() == logging.INFO # Silence logging for the rest of the tests logger.setLevel(logging.CRITICAL) @@ -888,24 +871,22 @@ def test_setup_logging_with_debug(self): def test_setup_logging_without_debug(self): logger = setup_logging(False, False) assert logger.getEffectiveLevel() == logging.INFO - assert logging.getLogger("botocore").getEffectiveLevel() == \ - logging.CRITICAL + assert logging.getLogger("botocore").getEffectiveLevel() == logging.CRITICAL # Silence logging for the rest of the tests logger.setLevel(logging.CRITICAL) @patch("sceptre.cli.click.echo") @pytest.mark.parametrize( - "output_format,no_colour,expected_output", [ + "output_format,no_colour,expected_output", + [ ("json", True, '{\n "stack": "CREATE_COMPLETE"\n}'), - ("json", False, '{\n "stack": "\x1b[32mCREATE_COMPLETE\x1b[0m\"\n}'), - ("yaml", True, '---\nstack: CREATE_COMPLETE\n'), - ("yaml", False, '---\nstack: \x1b[32mCREATE_COMPLETE\x1b[0m\n') - ] + ("json", False, '{\n "stack": "\x1b[32mCREATE_COMPLETE\x1b[0m"\n}'), + ("yaml", True, "---\nstack: CREATE_COMPLETE\n"), + ("yaml", False, "---\nstack: \x1b[32mCREATE_COMPLETE\x1b[0m\n"), + ], ) - def test_write_formats( - self, mock_echo, output_format, no_colour, expected_output - ): + def test_write_formats(self, mock_echo, output_format, no_colour, expected_output): write({"stack": "CREATE_COMPLETE"}, output_format, no_colour) mock_echo.assert_called_once_with(expected_output) @@ -923,9 +904,7 @@ def test_write_status_without_colour(self, mock_echo): @patch("sceptre.cli.helpers.StackStatusColourer.colour") @patch("sceptre.cli.helpers.logging.Formatter.format") - def test_ColouredFormatter_format_with_string( - self, mock_format, mock_colour - ): + def test_ColouredFormatter_format_with_string(self, mock_format, mock_colour): mock_format.return_value = sentinel.response mock_colour.return_value = sentinel.coloured_response coloured_formatter = ColouredFormatter() @@ -939,21 +918,25 @@ def test_CustomJsonEncoder_with_non_json_serialisable_object(self): response = encoder.encode(datetime.datetime(2016, 5, 3)) assert response == '"2016-05-03 00:00:00"' - def test_diff_command__diff_type_is_deepdiff__passes_deepdiff_stack_differ_to_actions(self): - self.runner.invoke(cli, 'diff -t deepdiff dev/vpc.yaml') + def test_diff_command__diff_type_is_deepdiff__passes_deepdiff_stack_differ_to_actions( + self, + ): + self.runner.invoke(cli, "diff -t deepdiff dev/vpc.yaml") differ_used = self.mock_stack_actions.diff.call_args[0][0] assert isinstance(differ_used, DeepDiffStackDiffer) - def test_diff_command__diff_type_is_difflib__passes_difflib_stack_differ_to_actions(self): - self.runner.invoke(cli, 'diff -t difflib dev/vpc.yaml') + def test_diff_command__diff_type_is_difflib__passes_difflib_stack_differ_to_actions( + self, + ): + self.runner.invoke(cli, "diff -t difflib dev/vpc.yaml") differ_used = self.mock_stack_actions.diff.call_args[0][0] assert isinstance(differ_used, DifflibStackDiffer) - self.runner.invoke(cli, 'diff stacks', catch_exceptions=False) + self.runner.invoke(cli, "diff stacks", catch_exceptions=False) def test_diff_command__stack_diffs_have_differences__returns_0(self): stacks = {deepcopy(self.mock_stack) for _ in range(3)} - stack_name_iterator = iter(['first', 'second', 'third']) + stack_name_iterator = iter(["first", "second", "third"]) def fake_diff(differ): name = next(stack_name_iterator) @@ -963,18 +946,18 @@ def fake_diff(differ): config_diff=DeepDiff("same", "same"), is_deployed=True, generated_config=None, - generated_template=None + generated_template=None, ) self.mock_stack_actions.diff.side_effect = fake_diff self.mock_config_reader.construct_stacks.return_value = (stacks, stacks) - result = self.runner.invoke(cli, 'diff stacks', catch_exceptions=False) + result = self.runner.invoke(cli, "diff stacks", catch_exceptions=False) assert result.exit_code == 0 def test_diff_command__no_differences__returns_0(self): stacks = {deepcopy(self.mock_stack) for _ in range(3)} - stack_name_iterator = iter(['first', 'second', 'third']) + stack_name_iterator = iter(["first", "second", "third"]) def fake_diff(differ): name = next(stack_name_iterator) @@ -984,22 +967,19 @@ def fake_diff(differ): config_diff=DeepDiff("same", "same"), is_deployed=True, generated_config=None, - generated_template=None + generated_template=None, ) self.mock_stack_actions.diff.side_effect = fake_diff self.mock_config_reader.construct_stacks.return_value = (stacks, stacks) - result = self.runner.invoke(cli, 'diff stacks', catch_exceptions=False) + result = self.runner.invoke(cli, "diff stacks", catch_exceptions=False) assert result.exit_code == 0 - @pytest.mark.parametrize( - ['bar'], - [('**********',), ('----------',)] - ) + @pytest.mark.parametrize(["bar"], [("**********",), ("----------",)]) def test_diff_command__bars_are_all_full_width_of_output(self, bar): stacks = {deepcopy(self.mock_stack) for _ in range(3)} - stack_name_iterator = iter(['first', 'second', 'third']) + stack_name_iterator = iter(["first", "second", "third"]) def fake_diff(differ): name = next(stack_name_iterator) @@ -1009,28 +989,31 @@ def fake_diff(differ): config_diff=DeepDiff("same", "same"), is_deployed=True, generated_config=None, - generated_template=None + generated_template=None, ) self.mock_stack_actions.diff.side_effect = fake_diff self.mock_config_reader.construct_stacks.return_value = (stacks, stacks) - result = self.runner.invoke(cli, 'diff stacks', catch_exceptions=False) + result = self.runner.invoke(cli, "diff stacks", catch_exceptions=False) output_lines = result.stdout.splitlines() max_line_length = len(max(output_lines, key=len)) star_bars = [line for line in output_lines if bar in line] assert all(len(line) == max_line_length for line in star_bars) - @pytest.mark.parametrize("input,expected_output", [ - ( - {"a_dict": '{"with_embedded":"json"}'}, - {"a_dict": {"with_embedded": "json"}} - ), - ( - {"a_dict": ['{"with_embedded":"json"}']}, - {"a_dict": [{"with_embedded": "json"}]} - ), - ]) + @pytest.mark.parametrize( + "input,expected_output", + [ + ( + {"a_dict": '{"with_embedded":"json"}'}, + {"a_dict": {"with_embedded": "json"}}, + ), + ( + {"a_dict": ['{"with_embedded":"json"}']}, + {"a_dict": [{"with_embedded": "json"}]}, + ), + ], + ) def test_deserialize_json_properties(self, input, expected_output): output = deserialize_json_properties(input) assert output == expected_output @@ -1041,28 +1024,25 @@ def test_drift_detect(self): "StackDriftDetectionId": "3fb76910-f660-11eb-80ac-0246f7a6da62", "StackDriftStatus": "IN_SYNC", "DetectionStatus": "DETECTION_COMPLETE", - "DriftedStackResourceCount": 0 + "DriftedStackResourceCount": 0, } - result = self.runner.invoke( - cli, ["drift", "detect", "dev/vpc.yaml"] - ) + result = self.runner.invoke(cli, ["drift", "detect", "dev/vpc.yaml"]) assert result.exit_code == 0 assert result.output == ( - '---\n' - 'mock-stack-external:\n' - ' DetectionStatus: DETECTION_COMPLETE\n' - ' DriftedStackResourceCount: 0\n' - ' StackDriftDetectionId: 3fb76910-f660-11eb-80ac-0246f7a6da62\n' - ' StackDriftStatus: IN_SYNC\n' - ' StackId: fake-stack-id\n\n' + "---\n" + "mock-stack-external:\n" + " DetectionStatus: DETECTION_COMPLETE\n" + " DriftedStackResourceCount: 0\n" + " StackDriftDetectionId: 3fb76910-f660-11eb-80ac-0246f7a6da62\n" + " StackDriftStatus: IN_SYNC\n" + " StackId: fake-stack-id\n\n" ) def test_drift_show(self): self.mock_stack_actions.drift_show.return_value = ( - "DETECTION_COMPLETE", {"some": "json"} - ) - result = self.runner.invoke( - cli, ["drift", "show", "dev/vpc.yaml"] + "DETECTION_COMPLETE", + {"some": "json"}, ) + result = self.runner.invoke(cli, ["drift", "show", "dev/vpc.yaml"]) assert result.exit_code == 0 assert result.output == "---\nmock-stack-external:\n some: json\n\n" diff --git a/tests/test_cli/test_launch.py b/tests/test_cli/test_launch.py index e4bba240f..584516979 100644 --- a/tests/test_cli/test_launch.py +++ b/tests/test_cli/test_launch.py @@ -35,13 +35,8 @@ def __init__( self.executions = [] def _execute(self, *args): - self.executions.append( - (self.command, self.launch_order.copy(), args) - ) - return { - stack: self.statuses_to_return[stack] - for stack in self - } + self.executions.append((self.command, self.launch_order.copy(), args)) + return {stack: self.statuses_to_return[stack] for stack in self} def _generate_launch_order(self, reverse=False) -> List[Set[Stack]]: launch_order = [self.command_stacks] @@ -52,7 +47,8 @@ def _generate_launch_order(self, reverse=False) -> List[Set[Stack]]: for start_index in range(0, len(all_stacks), 2): chunk = { - stack for stack in all_stacks[start_index: start_index + 2] + stack + for stack in all_stacks[start_index : start_index + 2] if stack not in self.command_stacks and self._has_dependency_on_a_command_stack(stack) } @@ -74,7 +70,6 @@ def _has_dependency_on_a_command_stack(self, stack): class TestLauncher: - def setup_method(self, test_method): self.plans: List[FakePlan] = [] @@ -85,10 +80,10 @@ def setup_method(self, test_method): self.cloned_context = self.context.clone() # Since contexts don't have a __eq__ method, you can't assert easily off the result of # clone without some hijinks. - self.context = Mock(wraps=self.context, **{ - 'clone.return_value': self.cloned_context, - 'ignore_dependencies': False - }) + self.context = Mock( + wraps=self.context, + **{"clone.return_value": self.cloned_context, "ignore_dependencies": False}, + ) self.all_stacks = [ Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), @@ -96,18 +91,16 @@ def setup_method(self, test_method): Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), - Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]) + Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), ] for index, stack in enumerate(self.all_stacks): - stack.name = f'stacks/stack-{index}.yaml' + stack.name = f"stacks/stack-{index}.yaml" self.command_stacks = list(self.all_stacks) self.statuses_to_return = defaultdict(lambda: StackStatus.COMPLETE) - self.fake_pruner = Mock(spec=Pruner, **{ - 'prune.return_value': 0 - }) + self.fake_pruner = Mock(spec=Pruner, **{"prune.return_value": 0}) self.plan_factory = create_autospec(SceptrePlan) self.plan_factory.side_effect = self.fake_plan_factory @@ -121,7 +114,7 @@ def fake_plan_factory(self, sceptre_context): sceptre_context, set(self.command_stacks), set(self.all_stacks), - self.statuses_to_return + self.statuses_to_return, ) self.plans.append(fake_plan) return fake_plan @@ -158,10 +151,9 @@ def test_launch__no_prune__obsolete_stacks__does_not_delete_any_stacks(self): assert len(self.plans) == 1 assert self.plans[0].executions[0][0] == "launch" - @pytest.mark.parametrize("prune", [ - pytest.param(True, id="prune"), - pytest.param(False, id="no prune") - ]) + @pytest.mark.parametrize( + "prune", [pytest.param(True, id="prune"), pytest.param(False, id="no prune")] + ) def test_launch__returns_0(self, prune): assert all(not s.ignore and not s.obsolete for s in self.all_stacks) result = self.launcher.launch(prune) @@ -179,7 +171,9 @@ def test_launch__does_not_launch_stacks_that_should_be_excluded(self): assert expected_stacks == launched_stacks assert self.plans[0].executions[0][0] == "launch" - def test_launch__prune__stack_with_dependency_marked_obsolete__raises_dependency_does_not_exist_error(self): + def test_launch__prune__stack_with_dependency_marked_obsolete__raises_dependency_does_not_exist_error( + self, + ): self.all_stacks[0].obsolete = True self.all_stacks[1].dependencies.append(self.all_stacks[0]) @@ -188,7 +182,9 @@ def test_launch__prune__stack_with_dependency_marked_obsolete__raises_dependency with pytest.raises(DependencyDoesNotExistError): self.launcher.launch(True) - def test_launch__prune__ignore_dependencies__stack_with_dependency_marked_obsolete__raises_no_error(self): + def test_launch__prune__ignore_dependencies__stack_with_dependency_marked_obsolete__raises_no_error( + self, + ): self.all_stacks[0].obsolete = True self.all_stacks[1].dependencies.append(self.all_stacks[0]) @@ -201,7 +197,9 @@ def test_launch__no_prune__does_not_raise_error(self): self.all_stacks[1].dependencies.append(self.all_stacks[0]) self.launcher.launch(False) - def test_launch__stacks_are_pruned__delete_and_deploy_actions_succeed__returns_0(self): + def test_launch__stacks_are_pruned__delete_and_deploy_actions_succeed__returns_0( + self, + ): self.all_stacks[0].obsolete = True code = self.launcher.launch(True) diff --git a/tests/test_cli/test_prune.py b/tests/test_cli/test_prune.py index 70cf390b0..2f8865d1c 100644 --- a/tests/test_cli/test_prune.py +++ b/tests/test_cli/test_prune.py @@ -34,13 +34,8 @@ def __init__( self.executions = [] def _execute(self, *args): - self.executions.append( - (self.command, self.launch_order.copy(), args) - ) - return { - stack: self.statuses_to_return[stack] - for stack in self - } + self.executions.append((self.command, self.launch_order.copy(), args)) + return {stack: self.statuses_to_return[stack] for stack in self} def _generate_launch_order(self, reverse=False) -> List[Set[Stack]]: launch_order = [self.command_stacks] @@ -51,7 +46,8 @@ def _generate_launch_order(self, reverse=False) -> List[Set[Stack]]: for start_index in range(0, len(all_stacks), 2): chunk = { - stack for stack in all_stacks[start_index: start_index + 2] + stack + for stack in all_stacks[start_index : start_index + 2] if stack not in self.command_stacks and self._has_dependency_on_a_command_stack(stack) } @@ -73,7 +69,6 @@ def _has_dependency_on_a_command_stack(self, stack): class TestPruner: - def setup_method(self, test_method): self.plans: List[FakePlan] = [] @@ -88,15 +83,12 @@ def setup_method(self, test_method): Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), - Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]) + Mock(spec=Stack, ignore=False, obsolete=False, dependencies=[]), ] for index, stack in enumerate(self.all_stacks): - stack.name = f'stacks/stack-{index}.yaml' + stack.name = f"stacks/stack-{index}.yaml" - self.command_stacks = [ - self.all_stacks[2], - self.all_stacks[4] - ] + self.command_stacks = [self.all_stacks[2], self.all_stacks[4]] self.statuses_to_return = defaultdict(lambda: StackStatus.COMPLETE) @@ -110,7 +102,7 @@ def fake_plan_factory(self, sceptre_context): sceptre_context, set(self.command_stacks), set(self.all_stacks), - self.statuses_to_return + self.statuses_to_return, ) self.plans.append(fake_plan) return fake_plan @@ -135,16 +127,18 @@ def test_prune__whole_project__obsolete_stacks__deletes_all_obsolete_stacks(self self.pruner.prune() - assert self.plans[0].executions[0][0] == 'delete' + assert self.plans[0].executions[0][0] == "delete" assert set(self.executed_stacks) == {self.all_stacks[4], self.all_stacks[5]} - def test_prune__command_path__obsolete_stacks__deletes_only_obsolete_stacks_on_path(self): + def test_prune__command_path__obsolete_stacks__deletes_only_obsolete_stacks_on_path( + self, + ): self.all_stacks[4].obsolete = True # On command path self.all_stacks[5].obsolete = True # not on command path self.context.command_path = "my/command/path" self.pruner.prune() - assert self.plans[0].executions[0][0] == 'delete' + assert self.plans[0].executions[0][0] == "delete" assert set(self.executed_stacks) == {self.all_stacks[4]} def test_prune__obsolete_stacks__returns_zero(self): @@ -154,7 +148,9 @@ def test_prune__obsolete_stacks__returns_zero(self): code = self.pruner.prune() assert code == 0 - def test_prune__obsolete_stacks_depend_on_other_obsolete_stacks__deletes_only_obsolete_stacks(self): + def test_prune__obsolete_stacks_depend_on_other_obsolete_stacks__deletes_only_obsolete_stacks( + self, + ): self.all_stacks[1].obsolete = True self.all_stacks[3].obsolete = True self.all_stacks[4].obsolete = True @@ -165,7 +161,7 @@ def test_prune__obsolete_stacks_depend_on_other_obsolete_stacks__deletes_only_ob self.pruner.prune() - assert self.plans[0].executions[0][0] == 'delete' + assert self.plans[0].executions[0][0] == "delete" assert set(self.executed_stacks) == { self.all_stacks[1], self.all_stacks[3], @@ -173,7 +169,9 @@ def test_prune__obsolete_stacks_depend_on_other_obsolete_stacks__deletes_only_ob self.all_stacks[5], } - def test_prune__non_obsolete_stacks_depend_on_obsolete_stacks__raises_cannot_prune_stack_error(self): + def test_prune__non_obsolete_stacks_depend_on_obsolete_stacks__raises_cannot_prune_stack_error( + self, + ): self.all_stacks[1].obsolete = True self.all_stacks[3].obsolete = False self.all_stacks[4].obsolete = False @@ -185,7 +183,9 @@ def test_prune__non_obsolete_stacks_depend_on_obsolete_stacks__raises_cannot_pru with pytest.raises(CannotPruneStackError): self.pruner.prune() - def test_prune__non_obsolete_stacks_depend_on_obsolete_stacks__ignore_dependencies__deletes_obsolete_stacks(self): + def test_prune__non_obsolete_stacks_depend_on_obsolete_stacks__ignore_dependencies__deletes_obsolete_stacks( + self, + ): self.all_stacks[1].obsolete = True self.all_stacks[3].obsolete = False self.all_stacks[4].obsolete = False @@ -196,7 +196,7 @@ def test_prune__non_obsolete_stacks_depend_on_obsolete_stacks__ignore_dependenci self.context.ignore_dependencies = True self.pruner.prune() - assert self.plans[0].executions[0][0] == 'delete' + assert self.plans[0].executions[0][0] == "delete" assert set(self.executed_stacks) == { self.all_stacks[1], } diff --git a/tests/test_config_reader.py b/tests/test_config_reader.py index b592afccc..9b541e124 100644 --- a/tests/test_config_reader.py +++ b/tests/test_config_reader.py @@ -16,7 +16,7 @@ VersionIncompatibleError, ConfigFileNotFoundError, InvalidSceptreDirectoryError, - InvalidConfigFileError + InvalidConfigFileError, ) @@ -24,12 +24,9 @@ class TestConfigReader(object): @patch("sceptre.config.reader.ConfigReader._check_valid_project_path") def setup_method(self, test_method, mock_check_valid_project_path): self.runner = CliRunner() - self.test_project_path = os.path.join( - os.getcwd(), "tests", "fixtures" - ) + self.test_project_path = os.path.join(os.getcwd(), "tests", "fixtures") self.context = SceptreContext( - project_path=self.test_project_path, - command_path="A" + project_path=self.test_project_path, command_path="A" ) def test_config_reader_correctly_initialised(self): @@ -45,7 +42,7 @@ def create_project(self): Creates a new random temporary directory with a config subdirectory """ with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") os.makedirs(config_dir) return (project_path, config_dir) @@ -63,22 +60,17 @@ def write_config(self, abs_path, config): if exc.errno != errno.EEXIST: raise - with open(abs_path, 'w') as config_file: - yaml.safe_dump( - config, stream=config_file, default_flow_style=False - ) - - @pytest.mark.parametrize("filepaths,target", [ - ( - ["A/1.yaml"], "A/1.yaml" - ), - ( - ["A/1.yaml", "A/B/1.yaml"], "A/B/1.yaml" - ), - ( - ["A/1.yaml", "A/B/1.yaml", "A/B/C/1.yaml"], "A/B/C/1.yaml" - ) - ]) + with open(abs_path, "w") as config_file: + yaml.safe_dump(config, stream=config_file, default_flow_style=False) + + @pytest.mark.parametrize( + "filepaths,target", + [ + (["A/1.yaml"], "A/1.yaml"), + (["A/1.yaml", "A/B/1.yaml"], "A/B/1.yaml"), + (["A/1.yaml", "A/B/1.yaml", "A/B/C/1.yaml"], "A/B/C/1.yaml"), + ], + ) def test_read_reads_config_file(self, filepaths, target): project_path, config_dir = self.create_project() @@ -93,12 +85,12 @@ def test_read_reads_config_file(self, filepaths, target): assert config == { "project_path": project_path, "stack_group_path": os.path.split(target)[0], - "filepath": target + "filepath": target, } def test_read_nested_configs(self): with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") stack_group_dir_a = os.path.join(config_dir, "A") stack_group_dir_b = os.path.join(stack_group_dir_a, "B") @@ -108,25 +100,22 @@ def test_read_nested_configs(self): config_filename = "config.yaml" config_a = {"keyA": "A", "shared": "A"} - with open(os.path.join(stack_group_dir_a, config_filename), 'w') as\ - config_file: - yaml.safe_dump( - config_a, stream=config_file, default_flow_style=False - ) + with open( + os.path.join(stack_group_dir_a, config_filename), "w" + ) as config_file: + yaml.safe_dump(config_a, stream=config_file, default_flow_style=False) config_b = {"keyB": "B", "parent": "{{ keyA }}", "shared": "B"} - with open(os.path.join(stack_group_dir_b, config_filename), 'w') as\ - config_file: - yaml.safe_dump( - config_b, stream=config_file, default_flow_style=False - ) + with open( + os.path.join(stack_group_dir_b, config_filename), "w" + ) as config_file: + yaml.safe_dump(config_b, stream=config_file, default_flow_style=False) config_c = {"keyC": "C", "parent": "{{ keyB }}", "shared": "C"} - with open(os.path.join(stack_group_dir_c, config_filename), 'w') as\ - config_file: - yaml.safe_dump( - config_c, stream=config_file, default_flow_style=False - ) + with open( + os.path.join(stack_group_dir_c, config_filename), "w" + ) as config_file: + yaml.safe_dump(config_c, stream=config_file, default_flow_style=False) self.context.project_path = project_path reader = ConfigReader(self.context) @@ -137,7 +126,7 @@ def test_read_nested_configs(self): "project_path": project_path, "stack_group_path": "A", "keyA": "A", - "shared": "A" + "shared": "A", } config_b = reader.read("A/B/config.yaml") @@ -148,12 +137,10 @@ def test_read_nested_configs(self): "keyA": "A", "keyB": "B", "shared": "B", - "parent": "A" + "parent": "A", } - config_c = reader.read( - "A/B/C/config.yaml" - ) + config_c = reader.read("A/B/C/config.yaml") assert config_c == { "project_path": project_path, @@ -162,37 +149,30 @@ def test_read_nested_configs(self): "keyB": "B", "keyC": "C", "shared": "C", - "parent": "B" + "parent": "B", } def test_read_reads_config_file_with_base_config(self): with self.runner.isolated_filesystem(): - project_path = os.path.abspath('./example') + project_path = os.path.abspath("./example") config_dir = os.path.join(project_path, "config") stack_group_dir = os.path.join(config_dir, "A") os.makedirs(stack_group_dir) config = {"config": "config"} - with open(os.path.join(stack_group_dir, "stack.yaml"), 'w') as\ - config_file: - yaml.safe_dump( - config, stream=config_file, default_flow_style=False - ) - - base_config = { - "base_config": "base_config" - } + with open(os.path.join(stack_group_dir, "stack.yaml"), "w") as config_file: + yaml.safe_dump(config, stream=config_file, default_flow_style=False) + + base_config = {"base_config": "base_config"} self.context.project_path = project_path - config = ConfigReader(self.context).read( - "A/stack.yaml", base_config - ) + config = ConfigReader(self.context).read("A/stack.yaml", base_config) assert config == { "project_path": project_path, "stack_group_path": "A", "config": "config", - "base_config": "base_config" + "base_config": "base_config", } def test_read_with_nonexistant_filepath(self): @@ -203,12 +183,10 @@ def test_read_with_nonexistant_filepath(self): def test_read_with_empty_config_file(self): config_reader = ConfigReader(self.context) - config = config_reader.read( - "account/stack-group/region/subnets.yaml" - ) + config = config_reader.read("account/stack-group/region/subnets.yaml") assert config == { "project_path": self.test_project_path, - "stack_group_path": "account/stack-group/region" + "stack_group_path": "account/stack-group/region", } def test_read_with_templated_config_file(self): @@ -219,15 +197,13 @@ def test_read_with_templated_config_file(self): "region": "region_region", "project_code": "account_project_code", "required_version": "'>1.0'", - "template_bucket_name": "stack_group_template_bucket_name" + "template_bucket_name": "stack_group_template_bucket_name", } os.environ["TEST_ENV_VAR"] = "environment_variable_value" - config = config_reader.read( - "account/stack-group/region/security_groups.yaml" - ) + config = config_reader.read("account/stack-group/region/security_groups.yaml") assert config == { - 'project_path': self.context.project_path, + "project_path": self.context.project_path, "stack_group_path": "account/stack-group/region", "parameters": { "param1": "user_variable_value", @@ -235,56 +211,51 @@ def test_read_with_templated_config_file(self): "param3": "region_region", "param4": "account_project_code", "param5": ">1.0", - "param6": "stack_group_template_bucket_name" - } + "param6": "stack_group_template_bucket_name", + }, } def test_aborts_on_incompatible_version_requirement(self): - config = { - 'required_version': '<0' - } + config = {"required_version": "<0"} with pytest.raises(VersionIncompatibleError): ConfigReader(self.context)._check_version(config) @freeze_time("2012-01-01") - @pytest.mark.parametrize("stack_name,config,expected", [ - ( - "name", - { - "template_bucket_name": "bucket-name", - "template_key_prefix": "prefix", - "region": "eu-west-1" - }, - { - "bucket_name": "bucket-name", - "bucket_key": "prefix/name/2012-01-01-00-00-00-000000Z.json" - } - ), - ( - "name", - { - "template_bucket_name": "bucket-name", - "region": "eu-west-1" - }, - { - "bucket_name": "bucket-name", - "bucket_key": "name/2012-01-01-00-00-00-000000Z.json" - } - ), - ( - "name", - { - "template_bucket_name": "bucket-name", - }, - { - "bucket_name": "bucket-name", - "bucket_key": "name/2012-01-01-00-00-00-000000Z.json" - } - ), - ( - "name", {}, None - ) - ] + @pytest.mark.parametrize( + "stack_name,config,expected", + [ + ( + "name", + { + "template_bucket_name": "bucket-name", + "template_key_prefix": "prefix", + "region": "eu-west-1", + }, + { + "bucket_name": "bucket-name", + "bucket_key": "prefix/name/2012-01-01-00-00-00-000000Z.json", + }, + ), + ( + "name", + {"template_bucket_name": "bucket-name", "region": "eu-west-1"}, + { + "bucket_name": "bucket-name", + "bucket_key": "name/2012-01-01-00-00-00-000000Z.json", + }, + ), + ( + "name", + { + "template_bucket_name": "bucket-name", + }, + { + "bucket_name": "bucket-name", + "bucket_key": "name/2012-01-01-00-00-00-000000Z.json", + }, + ), + ("name", {}, None), + ], ) def test_collect_s3_details(self, stack_name, config, expected): details = ConfigReader._collect_s3_details(stack_name, config) @@ -323,93 +294,77 @@ def test_construct_stacks_constructs_stack( notifications=None, on_failure=None, stack_timeout=0, - required_version='>1.0', - template_bucket_name='stack_group_template_bucket_name', + required_version=">1.0", + template_bucket_name="stack_group_template_bucket_name", template_key_prefix=None, ignore=False, obsolete=False, stack_group_config={ "project_path": self.context.project_path, - "custom_key": "custom_value" - } + "custom_key": "custom_value", + }, ) assert stacks == ({sentinel.stack}, {sentinel.stack}) - @pytest.mark.parametrize("command_path,filepaths,expected_stacks,expected_command_stacks,full_scan", [ - ( - "", - ["A/1.yaml"], - {"A/1"}, - {"A/1"}, - False - ), - ( - "", - ["A/1.yaml", "A/2.yaml", "A/3.yaml"], - {"A/3", "A/2", "A/1"}, - {"A/3", "A/2", "A/1"}, - False - ), - ( - "", - ["A/1.yaml", "A/A/1.yaml"], - {"A/1", "A/A/1"}, - {"A/1", "A/A/1"}, - False - ), - ( - "", - ["A/1.yaml", "A/A/1.yaml", "A/A/2.yaml"], - {"A/1", "A/A/1", "A/A/2"}, - {"A/1", "A/A/1", "A/A/2"}, - False - ), - ( - "", - ["A/A/1.yaml", "A/B/1.yaml"], - {"A/A/1", "A/B/1"}, - {"A/A/1", "A/B/1"}, - False - ), - ( - "Abd", - ["Abc/1.yaml", "Abd/1.yaml"], - {"Abd/1"}, - {"Abd/1"}, - False - ), - ( - "Abd", - ["Abc/1.yaml", "Abd/Abc/1.yaml", "Abd/2.yaml"], - {"Abd/2", "Abd/Abc/1"}, - {"Abd/2", "Abd/Abc/1"}, - False - ), - ( - "Abd/Abc", - ["Abc/1.yaml", "Abd/Abc/1.yaml", "Abd/2.yaml"], - {"Abd/Abc/1"}, - {"Abd/Abc/1"}, - False - ), - ( - "Ab", - ["Abc/1.yaml", "Abd/1.yaml"], - set(), - set(), - False - ), - ( - "Abd/Abc", - ["Abc/1.yaml", "Abd/Abc/1.yaml", "Abd/2.yaml"], - {"Abc/1", "Abd/Abc/1", "Abd/2"}, - {"Abd/Abc/1"}, - True - ), - ]) + @pytest.mark.parametrize( + "command_path,filepaths,expected_stacks,expected_command_stacks,full_scan", + [ + ("", ["A/1.yaml"], {"A/1"}, {"A/1"}, False), + ( + "", + ["A/1.yaml", "A/2.yaml", "A/3.yaml"], + {"A/3", "A/2", "A/1"}, + {"A/3", "A/2", "A/1"}, + False, + ), + ("", ["A/1.yaml", "A/A/1.yaml"], {"A/1", "A/A/1"}, {"A/1", "A/A/1"}, False), + ( + "", + ["A/1.yaml", "A/A/1.yaml", "A/A/2.yaml"], + {"A/1", "A/A/1", "A/A/2"}, + {"A/1", "A/A/1", "A/A/2"}, + False, + ), + ( + "", + ["A/A/1.yaml", "A/B/1.yaml"], + {"A/A/1", "A/B/1"}, + {"A/A/1", "A/B/1"}, + False, + ), + ("Abd", ["Abc/1.yaml", "Abd/1.yaml"], {"Abd/1"}, {"Abd/1"}, False), + ( + "Abd", + ["Abc/1.yaml", "Abd/Abc/1.yaml", "Abd/2.yaml"], + {"Abd/2", "Abd/Abc/1"}, + {"Abd/2", "Abd/Abc/1"}, + False, + ), + ( + "Abd/Abc", + ["Abc/1.yaml", "Abd/Abc/1.yaml", "Abd/2.yaml"], + {"Abd/Abc/1"}, + {"Abd/Abc/1"}, + False, + ), + ("Ab", ["Abc/1.yaml", "Abd/1.yaml"], set(), set(), False), + ( + "Abd/Abc", + ["Abc/1.yaml", "Abd/Abc/1.yaml", "Abd/2.yaml"], + {"Abc/1", "Abd/Abc/1", "Abd/2"}, + {"Abd/Abc/1"}, + True, + ), + ], + ) def test_construct_stacks_with_valid_config( - self, command_path, filepaths, expected_stacks, expected_command_stacks, full_scan + self, + command_path, + filepaths, + expected_stacks, + expected_command_stacks, + full_scan, ): project_path, config_dir = self.create_project() @@ -418,7 +373,7 @@ def test_construct_stacks_with_valid_config( config = { "region": "region", "project_code": "project_code", - "template_path": rel_path + "template_path": rel_path, } abs_path = os.path.join(config_dir, rel_path) @@ -432,13 +387,14 @@ def test_construct_stacks_with_valid_config( assert {str(stack) for stack in all_stacks} == expected_stacks assert {str(stack) for stack in command_stacks} == expected_command_stacks - @pytest.mark.parametrize("filepaths, del_key", [ - (["A/1.yaml"], "project_code"), - (["A/1.yaml"], "region"), - ]) - def test_missing_attr( - self, filepaths, del_key - ): + @pytest.mark.parametrize( + "filepaths, del_key", + [ + (["A/1.yaml"], "project_code"), + (["A/1.yaml"], "region"), + ], + ) + def test_missing_attr(self, filepaths, del_key): project_path, config_dir = self.create_project() for rel_path in filepaths: @@ -446,7 +402,7 @@ def test_missing_attr( config = { "project_code": "project_code", "region": "region", - "template_path": rel_path + "template_path": rel_path, } # Delete the mandatory key to be tested. del config[del_key] @@ -466,13 +422,14 @@ def test_missing_attr( else: assert False - @pytest.mark.parametrize("filepaths, dependency", [ - (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "A/1.yaml"), - (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "B/1.yaml"), - ]) - def test_existing_dependency( - self, filepaths, dependency - ): + @pytest.mark.parametrize( + "filepaths, dependency", + [ + (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "A/1.yaml"), + (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "B/1.yaml"), + ], + ) + def test_existing_dependency(self, filepaths, dependency): project_path, config_dir = self.create_project() for rel_path in filepaths: @@ -481,7 +438,7 @@ def test_existing_dependency( "project_code": "project_code", "region": "region", "template_path": rel_path, - "dependencies": [dependency] + "dependencies": [dependency], } abs_path = os.path.join(config_dir, rel_path) @@ -496,13 +453,14 @@ def test_existing_dependency( else: assert True - @pytest.mark.parametrize("filepaths, dependency", [ - (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "A/2.yaml"), - (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "1.yaml"), - ]) - def test_missing_dependency( - self, filepaths, dependency - ): + @pytest.mark.parametrize( + "filepaths, dependency", + [ + (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "A/2.yaml"), + (["A/1.yaml", "B/1.yaml", "B/2.yaml"], "1.yaml"), + ], + ) + def test_missing_dependency(self, filepaths, dependency): project_path, config_dir = self.create_project() for rel_path in filepaths: @@ -511,7 +469,7 @@ def test_missing_dependency( "project_code": "project_code", "region": "region", "template_path": rel_path, - "dependencies": [dependency] + "dependencies": [dependency], } abs_path = os.path.join(config_dir, rel_path) @@ -530,16 +488,17 @@ def test_missing_dependency( assert False @pytest.mark.parametrize( - "filepaths, dependency, parent_config_path", [ - (["A/1.yaml", "A/2.yaml", "B/1.yaml"], "B/1.yaml", 'A/config.yaml'), - (["A/1.yaml", "A/2.yaml"], "A/1.yaml", 'A/config.yaml'), - ] + "filepaths, dependency, parent_config_path", + [ + (["A/1.yaml", "A/2.yaml", "B/1.yaml"], "B/1.yaml", "A/config.yaml"), + (["A/1.yaml", "A/2.yaml"], "A/1.yaml", "A/config.yaml"), + ], ) - def test_inherited_dependency_already_resolved(self, filepaths, dependency, parent_config_path): + def test_inherited_dependency_already_resolved( + self, filepaths, dependency, parent_config_path + ): project_path, config_dir = self.create_project() - parent_config = { - 'dependencies': [dependency] - } + parent_config = {"dependencies": [dependency]} abs_path = os.path.join(config_dir, parent_config_path) self.write_config(abs_path, parent_config) @@ -573,4 +532,4 @@ def test_resolve_node_tag(self): config_reader = ConfigReader(self.context) new_node = config_reader.resolve_node_tag(mock_loader, mock_node) - assert new_node.tag == 'new_tag' + assert new_node.tag == "new_tag" diff --git a/tests/test_connection_manager.py b/tests/test_connection_manager.py index b8b91ec0b..b7cf79756 100644 --- a/tests/test_connection_manager.py +++ b/tests/test_connection_manager.py @@ -13,7 +13,6 @@ class TestConnectionManager(object): - def setup_method(self, test_method): self.stack_name = None self.profile = None @@ -33,7 +32,7 @@ def setup_method(self, test_method): region=self.region, stack_name=self.stack_name, profile=self.profile, - iam_role=self.iam_role + iam_role=self.iam_role, ) def test_connection_manager_initialised_with_no_optional_parameters(self): @@ -52,7 +51,7 @@ def test_connection_manager_initialised_with_all_parameters(self): stack_name="stack", profile="profile", iam_role="iam_role", - iam_role_session_duration=21600 + iam_role_session_duration=21600, ) assert connection_manager.stack_name == "stack" @@ -72,9 +71,11 @@ def test_repr(self): self.connection_manager.region = "region" self.connection_manager.iam_role = "iam_role" response = self.connection_manager.__repr__() - assert response == "sceptre.connection_manager.ConnectionManager(" \ - "region='region', profile='profile', stack_name='stack', "\ + assert ( + response == "sceptre.connection_manager.ConnectionManager(" + "region='region', profile='profile', stack_name='stack', " "iam_role='iam_role', iam_role_session_duration='None')" + ) def test_repr_with_iam_role_session_duration(self): self.connection_manager.stack_name = "stack" @@ -83,9 +84,11 @@ def test_repr_with_iam_role_session_duration(self): self.connection_manager.iam_role = "iam_role" self.connection_manager.iam_role_session_duration = 21600 response = self.connection_manager.__repr__() - assert response == "sceptre.connection_manager.ConnectionManager(" \ - "region='region', profile='profile', stack_name='stack', "\ + assert ( + response == "sceptre.connection_manager.ConnectionManager(" + "region='region', profile='profile', stack_name='stack', " "iam_role='iam_role', iam_role_session_duration='21600')" + ) def test_boto_session_with_cache(self): self.connection_manager._boto_sessions["test"] = sentinel.boto_session @@ -94,9 +97,7 @@ def test_boto_session_with_cache(self): assert boto_session == sentinel.boto_session @patch("sceptre.connection_manager.boto3.session.Session") - def test_boto_session_with_no_profile( - self, mock_Session - ): + def test_boto_session_with_no_profile(self, mock_Session): self.connection_manager._boto_sessions = {} self.connection_manager.profile = None @@ -110,7 +111,7 @@ def test_boto_session_with_no_profile( region_name="eu-west-1", aws_access_key_id=ANY, aws_secret_access_key=ANY, - aws_session_token=ANY + aws_session_token=ANY, ) @patch("sceptre.connection_manager.boto3.session.Session") @@ -128,13 +129,11 @@ def test_boto_session_with_profile(self, mock_Session): region_name="eu-west-1", aws_access_key_id=ANY, aws_secret_access_key=ANY, - aws_session_token=ANY + aws_session_token=ANY, ) @patch("sceptre.connection_manager.boto3.session.Session") - def test_boto_session_with_no_iam_role( - self, mock_Session - ): + def test_boto_session_with_no_iam_role(self, mock_Session): self.connection_manager._boto_sessions = {} self.connection_manager.iam_role = None @@ -148,7 +147,7 @@ def test_boto_session_with_no_iam_role( region_name="eu-west-1", aws_access_key_id=ANY, aws_secret_access_key=ANY, - aws_session_token=ANY + aws_session_token=ANY, ) boto_session.client().assume_role.assert_not_called() @@ -168,14 +167,14 @@ def test_boto_session_with_iam_role(self, mock_Session): region_name="eu-west-1", aws_access_key_id=ANY, aws_secret_access_key=ANY, - aws_session_token=ANY + aws_session_token=ANY, ) boto_session.client().assume_role.assert_called_once_with( RoleArn=self.connection_manager.iam_role, RoleSessionName="{0}-session".format( self.connection_manager.iam_role.split("/")[-1] - ) + ), ) credentials = boto_session.client().assume_role()["Credentials"] @@ -184,7 +183,7 @@ def test_boto_session_with_iam_role(self, mock_Session): region_name="eu-west-1", aws_access_key_id=credentials["AccessKeyId"], aws_secret_access_key=credentials["SecretAccessKey"], - aws_session_token=credentials["SessionToken"] + aws_session_token=credentials["SessionToken"], ) @patch("sceptre.connection_manager.boto3.session.Session") @@ -202,7 +201,7 @@ def test_boto_session_with_iam_role_session_duration(self, mock_Session): RoleSessionName="{0}-session".format( self.connection_manager.iam_role.split("/")[-1] ), - DurationSeconds=21600 + DurationSeconds=21600, ) @patch("sceptre.connection_manager.boto3.session.Session") @@ -211,7 +210,11 @@ def test_boto_session_with_iam_role_returning_empty_credentials(self, mock_Sessi self.connection_manager.iam_role = "iam_role" mock_Session.return_value.get_credentials.side_effect = [ - MagicMock(), None, MagicMock(), MagicMock(), MagicMock() + MagicMock(), + None, + MagicMock(), + MagicMock(), + MagicMock(), ] with pytest.raises(InvalidAWSCredentialsError): @@ -223,7 +226,7 @@ def test_boto_session_with_iam_role_returning_empty_credentials(self, mock_Sessi def test_two_boto_sessions(self, mock_Session): self.connection_manager._boto_sessions = { "one": mock_Session, - "two": mock_Session + "two": mock_Session, } boto_session_1 = self.connection_manager._boto_sessions["one"] @@ -231,9 +234,7 @@ def test_two_boto_sessions(self, mock_Session): assert boto_session_1 == boto_session_2 @patch("sceptre.connection_manager.boto3.session.Session.get_credentials") - def test_get_client_with_no_pre_existing_clients( - self, mock_get_credentials - ): + def test_get_client_with_no_pre_existing_clients(self, mock_get_credentials): service = "s3" region = "eu-west-1" profile = None @@ -277,7 +278,7 @@ def test_get_client_with_exisiting_client(self, mock_get_credentials): @patch("sceptre.connection_manager.boto3.session.Session.get_credentials") def test_get_client_with_exisiting_client_and_profile_none( - self, mock_get_credentials + self, mock_get_credentials ): service = "cloudformation" region = "eu-west-1" @@ -296,30 +297,24 @@ def test_get_client_with_exisiting_client_and_profile_none( @mock_s3 def test_call_with_valid_service_and_call(self): - service = 's3' - command = 'list_buckets' + service = "s3" + command = "list_buckets" return_value = self.connection_manager.call(service, command, {}) - assert return_value['ResponseMetadata']['HTTPStatusCode'] == 200 + assert return_value["ResponseMetadata"]["HTTPStatusCode"] == 200 @mock_s3 def test_call_with_valid_service_and_stack_name_call(self): - service = 's3' - command = 'list_buckets' - - connection_manager = ConnectionManager( - region=self.region, - stack_name='stack' - ) + service = "s3" + command = "list_buckets" - return_value = connection_manager.call( - service, command, {}, stack_name='stack' - ) - assert return_value['ResponseMetadata']['HTTPStatusCode'] == 200 + connection_manager = ConnectionManager(region=self.region, stack_name="stack") + return_value = connection_manager.call(service, command, {}, stack_name="stack") + assert return_value["ResponseMetadata"]["HTTPStatusCode"] == 200 -class TestRetry(): +class TestRetry: def test_retry_boto_call_returns_response_correctly(self): def func(*args, **kwargs): return sentinel.response @@ -329,21 +324,14 @@ def func(*args, **kwargs): assert response == sentinel.response @patch("sceptre.connection_manager.time.sleep") - def test_retry_boto_call_pauses_when_request_limit_hit( - self, mock_sleep - ): + def test_retry_boto_call_pauses_when_request_limit_hit(self, mock_sleep): mock_func = Mock() mock_func.side_effect = [ ClientError( - { - "Error": { - "Code": "Throttling", - "Message": "Request limit hit" - } - }, - sentinel.operation + {"Error": {"Code": "Throttling", "Message": "Request limit hit"}}, + sentinel.operation, ), - sentinel.response + sentinel.response, ] # The attribute function.__name__ is required by the decorator @wraps. mock_func.__name__ = "mock_func" @@ -354,13 +342,7 @@ def test_retry_boto_call_pauses_when_request_limit_hit( def test_retry_boto_call_raises_non_throttling_error(self): mock_func = Mock() mock_func.side_effect = ClientError( - { - "Error": { - "Code": 500, - "Message": "Boom!" - } - }, - sentinel.operation + {"Error": {"Code": 500, "Message": "Boom!"}}, sentinel.operation ) # The attribute function.__name__ is required by the decorator @wraps. mock_func.__name__ = "mock_func" @@ -371,18 +353,11 @@ def test_retry_boto_call_raises_non_throttling_error(self): assert e.value.response["Error"]["Message"] == "Boom!" @patch("sceptre.connection_manager.time.sleep") - def test_retry_boto_call_raises_retry_limit_exceeded_exception( - self, mock_sleep - ): + def test_retry_boto_call_raises_retry_limit_exceeded_exception(self, mock_sleep): mock_func = Mock() mock_func.side_effect = ClientError( - { - "Error": { - "Code": "Throttling", - "Message": "Request limit hit" - } - }, - sentinel.operation + {"Error": {"Code": "Throttling", "Message": "Request limit hit"}}, + sentinel.operation, ) # The attribute function.__name__ is required by the decorator @wraps. mock_func.__name__ = "mock_func" diff --git a/tests/test_context.py b/tests/test_context.py index fc7ca694c..968b85c4e 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -4,7 +4,6 @@ class TestSceptreContext(object): - def setup_method(self, test_method): self.templates_path = "templates" self.config_path = "config" @@ -18,11 +17,11 @@ def test_context_with_path(self): options=sentinel.options, output_format=sentinel.output_format, no_colour=sentinel.no_colour, - ignore_dependencies=sentinel.ignore_dependencies + ignore_dependencies=sentinel.ignore_dependencies, ) sentinel.project_path = "project_path/to/sceptre" - assert self.context.project_path.replace(path.sep, '/') == sentinel.project_path + assert self.context.project_path.replace(path.sep, "/") == sentinel.project_path def test_full_config_path_returns_correct_path(self): context = SceptreContext( @@ -32,7 +31,7 @@ def test_full_config_path_returns_correct_path(self): options=sentinel.options, output_format=sentinel.output_format, no_colour=sentinel.no_colour, - ignore_dependencies=sentinel.ignore_dependencies + ignore_dependencies=sentinel.ignore_dependencies, ) full_config_path = path.join("project_path", self.config_path) @@ -46,11 +45,9 @@ def test_full_command_path_returns_correct_path(self): options=sentinel.options, output_format=sentinel.output_format, no_colour=sentinel.no_colour, - ignore_dependencies=sentinel.ignore_dependencies + ignore_dependencies=sentinel.ignore_dependencies, ) - full_command_path = path.join("project_path", - self.config_path, - "command") + full_command_path = path.join("project_path", self.config_path, "command") assert context.full_command_path() == full_command_path @@ -62,7 +59,7 @@ def test_full_templates_path_returns_correct_path(self): options=sentinel.options, output_format=sentinel.output_format, no_colour=sentinel.no_colour, - ignore_dependencies=sentinel.ignore_dependencies + ignore_dependencies=sentinel.ignore_dependencies, ) full_templates_path = path.join("project_path", self.templates_path) assert context.full_templates_path() == full_templates_path @@ -75,7 +72,7 @@ def test_clone__returns_full_clone_of_context(self): options={"hello": "there"}, output_format=sentinel.output_format, no_colour=sentinel.no_colour, - ignore_dependencies=sentinel.ignore_dependencies + ignore_dependencies=sentinel.ignore_dependencies, ) clone = context.clone() assert clone is not context diff --git a/tests/test_diffing/test_diff_writer.py b/tests/test_diffing/test_diff_writer.py index 769a2f55d..29d051790 100644 --- a/tests/test_diffing/test_diff_writer.py +++ b/tests/test_diffing/test_diff_writer.py @@ -9,20 +9,24 @@ import yaml from deepdiff import DeepDiff -from sceptre.diffing.diff_writer import DiffWriter, DeepDiffWriter, deepdiff_json_defaults, \ - DiffLibWriter, ColouredDiffLibWriter +from sceptre.diffing.diff_writer import ( + DiffWriter, + DeepDiffWriter, + deepdiff_json_defaults, + DiffLibWriter, + ColouredDiffLibWriter, +) from sceptre.diffing.stack_differ import StackDiff, DiffType, StackConfiguration from colorama import Fore class ImplementedDiffWriter(DiffWriter): - def __init__( self, stack_diff: StackDiff, output_stream: TextIO, output_format: str, - capturing_mock: Mock + capturing_mock: Mock, ): super().__init__(stack_diff, output_stream, output_format) self.capturing_mock = capturing_mock @@ -40,22 +44,21 @@ def has_template_difference(self) -> bool: class TestDiffWriter: - def setup_method(self, method): - self.diff_output = 'diff' - self.capturing_mock = Mock(**{ - 'dump_diff.return_value': self.diff_output - }) - self.stack_name = 'stack' + self.diff_output = "diff" + self.capturing_mock = Mock(**{"dump_diff.return_value": self.diff_output}) + self.stack_name = "stack" self.template_diff = Mock() self.config_diff = Mock() self.is_deployed = True - self.generated_template = 'my template' - self.output_format = 'yaml' + self.generated_template = "my template" + self.output_format = "yaml" self.output_stream = StringIO() - self.diff_detected_message = f'--> Difference detected for stack {self.stack_name}!' + self.diff_detected_message = ( + f"--> Difference detected for stack {self.stack_name}!" + ) @property def generated_config(self): @@ -64,7 +67,7 @@ def generated_config(self): parameters={}, stack_tags={}, notifications=[], - role_arn=None + role_arn=None, ) @property @@ -75,31 +78,30 @@ def diff(self): self.config_diff, self.is_deployed, self.generated_config, - self.generated_template + self.generated_template, ) @property def writer(self): return ImplementedDiffWriter( - self.diff, - self.output_stream, - self.output_format, - self.capturing_mock + self.diff, self.output_stream, self.output_format, self.capturing_mock ) def assert_expected_output(self, *expected_segments): - expected_segments = [f'{line}\n' for line in expected_segments] - joined = ''.join(expected_segments) + expected_segments = [f"{line}\n" for line in expected_segments] + joined = "".join(expected_segments) expected_split_lines = joined.splitlines() received_lines = self.output_stream.getvalue().splitlines() - diff = list(difflib.unified_diff( - received_lines, - expected_split_lines, - fromfile='actual', - tofile='expected' - )) - assert not diff, '\n'.join(diff) + diff = list( + difflib.unified_diff( + received_lines, + expected_split_lines, + fromfile="actual", + tofile="expected", + ) + ) + assert not diff, "\n".join(diff) def test_write__no_difference__writes_no_difference(self): self.capturing_mock.has_config_difference = False @@ -108,19 +110,20 @@ def test_write__no_difference__writes_no_difference(self): self.writer.write() self.assert_expected_output( - DiffWriter.STAR_BAR, - f'No difference to deployed stack {self.stack_name}' + DiffWriter.STAR_BAR, f"No difference to deployed stack {self.stack_name}" ) @pytest.mark.parametrize( - 'output_format, config_serializer', + "output_format, config_serializer", [ - pytest.param('yaml', cfn_flip.dump_yaml, id='output format is yaml'), - pytest.param('json', cfn_flip.dump_json, id='output format is json'), - pytest.param('text', cfn_flip.dump_yaml, id='output format is text') - ] + pytest.param("yaml", cfn_flip.dump_yaml, id="output format is yaml"), + pytest.param("json", cfn_flip.dump_json, id="output format is json"), + pytest.param("text", cfn_flip.dump_yaml, id="output format is text"), + ], ) - def test_write__new_stack__writes_new_stack_config_and_template(self, output_format, config_serializer): + def test_write__new_stack__writes_new_stack_config_and_template( + self, output_format, config_serializer + ): self.is_deployed = False self.output_format = output_format @@ -129,15 +132,15 @@ def test_write__new_stack__writes_new_stack_config_and_template(self, output_for self.assert_expected_output( DiffWriter.STAR_BAR, self.diff_detected_message, - 'This stack is not deployed yet!', + "This stack is not deployed yet!", DiffWriter.LINE_BAR, - 'New Config:', - '', + "New Config:", + "", config_serializer(dict(self.generated_config._asdict())), DiffWriter.LINE_BAR, - 'New Template:', - '', - self.generated_template + "New Template:", + "", + self.generated_template, ) def test_write__only_config_is_different__writes_config_difference(self): @@ -150,11 +153,11 @@ def test_write__only_config_is_different__writes_config_difference(self): DiffWriter.STAR_BAR, self.diff_detected_message, DiffWriter.LINE_BAR, - f'Config difference for {self.stack_name}:', - '', + f"Config difference for {self.stack_name}:", + "", self.diff_output, DiffWriter.LINE_BAR, - 'No template difference' + "No template difference", ) def test_write__only_template_is_different__writes_template_difference(self): @@ -167,11 +170,11 @@ def test_write__only_template_is_different__writes_template_difference(self): DiffWriter.STAR_BAR, self.diff_detected_message, DiffWriter.LINE_BAR, - 'No stack config difference', + "No stack config difference", DiffWriter.LINE_BAR, - f'Template difference for {self.stack_name}:', - '', - self.diff_output + f"Template difference for {self.stack_name}:", + "", + self.diff_output, ) def test_write__config_and_template_are_different__writes_both_differences(self): @@ -184,22 +187,22 @@ def test_write__config_and_template_are_different__writes_both_differences(self) DiffWriter.STAR_BAR, self.diff_detected_message, DiffWriter.LINE_BAR, - f'Config difference for {self.stack_name}:', - '', + f"Config difference for {self.stack_name}:", + "", self.diff_output, DiffWriter.LINE_BAR, - f'Template difference for {self.stack_name}:', - '', - self.diff_output + f"Template difference for {self.stack_name}:", + "", + self.diff_output, ) class TestDeepDiffWriter: def setup_method(self, method): - self.stack_name = 'stack' + self.stack_name = "stack" self.is_deployed = True - self.output_format = 'yaml' + self.output_format = "yaml" self.output_stream = StringIO() @@ -208,13 +211,13 @@ def setup_method(self, method): parameters={}, stack_tags={}, notifications=[], - role_arn=None + role_arn=None, ) self.config2 = deepcopy(self.config1) - self.template1 = 'template' - self.template2 = 'template' + self.template1 = "template" + self.template2 = "template" @property def template_diff(self): @@ -232,7 +235,7 @@ def diff(self): self.config_diff, self.is_deployed, self.config1, - self.template1 + self.template1, ) @property @@ -244,30 +247,36 @@ def writer(self): ) def test_has_config_difference__config_difference_is_present__returns_true(self): - self.config2.parameters['new_key'] = 'new value' + self.config2.parameters["new_key"] = "new value" assert self.writer.has_config_difference def test_has_config_difference__config_difference_is_absent__returns_false(self): assert self.writer.has_config_difference is False - def test_has_template_difference__template_difference_is_present__returns_true(self): - self.template2 = 'new' + def test_has_template_difference__template_difference_is_present__returns_true( + self, + ): + self.template2 = "new" assert self.writer.has_template_difference - def test_has_template_difference__template_difference_is_absent__returns_false(self): + def test_has_template_difference__template_difference_is_absent__returns_false( + self, + ): assert self.writer.has_template_difference is False def test_dump_diff__output_format_is_json__outputs_to_json(self): - self.output_format = 'json' - self.config2.parameters['new_key'] = 'new value' + self.output_format = "json" + self.config2.parameters["new_key"] = "new value" result = self.writer.dump_diff(self.config_diff) - expected = self.config_diff.to_json(indent=4, default_mapping=deepdiff_json_defaults) + expected = self.config_diff.to_json( + indent=4, default_mapping=deepdiff_json_defaults + ) assert result == expected def test_dump_diff__output_format_is_yaml__outputs_to_yaml(self): - self.output_format = 'yaml' - self.config2.parameters['new_key'] = 'new value' + self.output_format = "yaml" + self.config2.parameters["new_key"] = "new value" result = self.writer.dump_diff(self.config_diff) expected_dict = self.config_diff.to_dict() @@ -275,38 +284,53 @@ def test_dump_diff__output_format_is_yaml__outputs_to_yaml(self): assert result == expected_yaml def test_dump_diff__output_format_is_text__outputs_to_yaml(self): - self.output_format = 'text' - self.config2.parameters['new_key'] = 'new value' + self.output_format = "text" + self.config2.parameters["new_key"] = "new value" result = self.writer.dump_diff(self.config_diff) expected_dict = self.config_diff.to_dict() expected_yaml = yaml.dump(expected_dict, indent=4) assert result == expected_yaml - def test_dump_diff__output_format_is_yaml__diff_has_multiline_strings__strips_out_extra_spaces(self): - self.config1.parameters['long_param'] = 'here \nis \nmy \nlong \nstring' - self.config2.parameters['long_param'] = 'here \nis \nmy \nother \nlong \nstring' + def test_dump_diff__output_format_is_yaml__diff_has_multiline_strings__strips_out_extra_spaces( + self, + ): + self.config1.parameters["long_param"] = "here \nis \nmy \nlong \nstring" + self.config2.parameters["long_param"] = "here \nis \nmy \nother \nlong \nstring" dumped = self.writer.dump_diff(self.config_diff) loaded = yaml.safe_load(dumped) - assert ' ' not in loaded['values_changed']["root.parameters['long_param']"]['new_value'] - assert ' ' not in loaded['values_changed']["root.parameters['long_param']"]['old_value'] - expected_diff = '\n'.join( + assert ( + " " + not in loaded["values_changed"]["root.parameters['long_param']"][ + "new_value" + ] + ) + assert ( + " " + not in loaded["values_changed"]["root.parameters['long_param']"][ + "old_value" + ] + ) + expected_diff = "\n".join( difflib.unified_diff( - self.config1.parameters['long_param'].splitlines(), - self.config2.parameters['long_param'].splitlines(), - lineterm='' + self.config1.parameters["long_param"].splitlines(), + self.config2.parameters["long_param"].splitlines(), + lineterm="", ) - ).replace(' \n', '\n') - assert expected_diff == loaded['values_changed']["root.parameters['long_param']"]['diff'] + ).replace(" \n", "\n") + assert ( + expected_diff + == loaded["values_changed"]["root.parameters['long_param']"]["diff"] + ) class TestDiffLibWriter: def setup_method(self, method): - self.stack_name = 'stack' + self.stack_name = "stack" self.is_deployed = True - self.output_format = 'yaml' + self.output_format = "yaml" self.output_stream = StringIO() @@ -315,13 +339,13 @@ def setup_method(self, method): parameters={}, stack_tags={}, notifications=[], - role_arn=None + role_arn=None, ) self.config2 = deepcopy(self.config1) - self.template1 = 'template' - self.template2 = 'template' + self.template1 = "template" + self.template2 = "template" @property def template_diff(self): @@ -341,7 +365,7 @@ def diff(self): self.config_diff, self.is_deployed, self.config1, - self.template1 + self.template1, ) @property @@ -353,31 +377,35 @@ def writer(self): ) def test_has_config_difference__config_difference_is_present__returns_true(self): - self.config2.parameters['new_key'] = 'new value' + self.config2.parameters["new_key"] = "new value" assert self.writer.has_config_difference def test_has_config_difference__config_difference_is_absent__returns_false(self): assert self.writer.has_config_difference is False - def test_has_template_difference__template_difference_is_present__returns_true(self): - self.template2 = 'new' + def test_has_template_difference__template_difference_is_present__returns_true( + self, + ): + self.template2 = "new" assert self.writer.has_template_difference - def test_has_template_difference__template_difference_is_absent__returns_false(self): + def test_has_template_difference__template_difference_is_absent__returns_false( + self, + ): assert self.writer.has_template_difference is False def test_dump_diff__returns_joined_list(self): result = self.writer.dump_diff(self.diff.config_diff) - expected = '\n'.join(self.diff.config_diff) + expected = "\n".join(self.diff.config_diff) assert result == expected class TestColouredDiffLibWriter: def setup_method(self, method): - self.stack_name = 'stack' + self.stack_name = "stack" self.is_deployed = True - self.output_format = 'yaml' + self.output_format = "yaml" self.output_stream = StringIO() @@ -386,7 +414,7 @@ def setup_method(self, method): parameters={}, stack_tags={}, notifications=[], - role_arn=None + role_arn=None, ) self.template1 = "foo" @@ -394,15 +422,15 @@ def setup_method(self, method): @property def template_diff(self): return [ - '--- file1.txt 2018-01-11 10:39:38.237464052 +0000\n', - '+++ file2.txt 2018-01-11 10:40:00.323423021 +0000\n', - '@@ -1,4 +1,4 @@\n', - ' cat\n', - '-mv\n', - '-comm\n', - ' cp\n', - '+diff\n', - '+comm\n' + "--- file1.txt 2018-01-11 10:39:38.237464052 +0000\n", + "+++ file2.txt 2018-01-11 10:40:00.323423021 +0000\n", + "@@ -1,4 +1,4 @@\n", + " cat\n", + "-mv\n", + "-comm\n", + " cp\n", + "+diff\n", + "+comm\n", ] @property @@ -417,27 +445,23 @@ def diff(self): self.config_diff, self.is_deployed, self.config1, - self.template1 + self.template1, ) @property def writer(self): - return ColouredDiffLibWriter( - self.diff, - self.output_stream, - self.output_format - ) + return ColouredDiffLibWriter(self.diff, self.output_stream, self.output_format) def test_lines_are_coloured(self): coloured = ( - f'{Fore.RED}--- file1.txt 2018-01-11 10:39:38.237464052 +0000\n{Fore.RESET}\n' + f"{Fore.RED}--- file1.txt 2018-01-11 10:39:38.237464052 +0000\n{Fore.RESET}\n" f"{Fore.GREEN}+++ file2.txt 2018-01-11 10:40:00.323423021 +0000\n{Fore.RESET}\n" - '@@ -1,4 +1,4 @@\n\n' + "@@ -1,4 +1,4 @@\n\n" " cat\n\n" - f'{Fore.RED}-mv\n{Fore.RESET}\n' + f"{Fore.RED}-mv\n{Fore.RESET}\n" f"{Fore.RED}-comm\n{Fore.RESET}\n" - ' cp\n\n' + " cp\n\n" f"{Fore.GREEN}+diff\n{Fore.RESET}\n" - f'{Fore.GREEN}+comm\n{Fore.RESET}' + f"{Fore.GREEN}+comm\n{Fore.RESET}" ) assert self.writer.dump_diff(self.template_diff) == coloured diff --git a/tests/test_diffing/test_stack_differ.py b/tests/test_diffing/test_stack_differ.py index a41649939..8a664c357 100644 --- a/tests/test_diffing/test_stack_differ.py +++ b/tests/test_diffing/test_stack_differ.py @@ -14,14 +14,13 @@ StackConfiguration, DiffType, DeepDiffStackDiffer, - DifflibStackDiffer + DifflibStackDiffer, ) from sceptre.plan.actions import StackActions from sceptre.stack import Stack class ImplementedStackDiffer(StackDiffer): - def __init__(self, command_capturer: Mock): super().__init__() self.command_capturer = command_capturer @@ -30,34 +29,25 @@ def compare_templates(self, deployed: str, generated: str) -> DiffType: return self.command_capturer.compare_templates(deployed, generated) def compare_stack_configurations( - self, - deployed: Optional[StackConfiguration], - generated: StackConfiguration + self, deployed: Optional[StackConfiguration], generated: StackConfiguration ) -> DiffType: return self.command_capturer.compare_stack_configurations(deployed, generated) class TestStackDiffer: - def setup_method(self, method): - self.name = 'my/stack' + self.name = "my/stack" self.external_name = "full-stack-name" - self.role_arn = 'role_arn' - self.parameters_on_stack_config = { - 'param': 'some_value' - } - self.tags = { - 'tag_name': 'tag_value' - } - self.notifications = [ - 'notification_arn1' - ] + self.role_arn = "role_arn" + self.parameters_on_stack_config = {"param": "some_value"} + self.tags = {"tag_name": "tag_value"} + self.notifications = ["notification_arn1"] self.sceptre_user_data = {} self.deployed_parameters = deepcopy(self.parameters_on_stack_config) self.deployed_parameter_defaults = {} self.deployed_no_echo_parameters = [] - self.deployed_parameter_types = defaultdict(lambda: 'String') + self.deployed_parameter_types = defaultdict(lambda: "String") self.local_no_echo_parameters = [] self.deployed_tags = dict(self.tags) self.deployed_notification_arns = list(self.notifications) @@ -65,7 +55,7 @@ def setup_method(self, method): self.command_capturer = Mock() self.differ = ImplementedStackDiffer(self.command_capturer) - self.stack_status = 'CREATE_COMPLETE' + self.stack_status = "CREATE_COMPLETE" self._stack = None self._actions = None @@ -87,10 +77,12 @@ def stack(self) -> Union[Stack, Mock]: role_arn=self.role_arn, tags=self.tags, notifications=self.notifications, - __sceptre_user_data=self.sceptre_user_data + __sceptre_user_data=self.sceptre_user_data, ) self._stack.name = self.name - type(self._stack).parameters = PropertyMock(side_effect=lambda: self.parameters_on_stack) + type(self._stack).parameters = PropertyMock( + side_effect=lambda: self.parameters_on_stack + ) return self._stack @property @@ -98,36 +90,33 @@ def actions(self) -> Union[StackActions, Mock]: if not self._actions: self._actions = Mock( **{ - 'spec': StackActions, - 'stack': self.stack, - 'describe.side_effect': self.describe_stack, - 'fetch_remote_template_summary.side_effect': self.get_remote_template_summary, - 'fetch_local_template_summary.side_effect': self.get_local_template_summary, + "spec": StackActions, + "stack": self.stack, + "describe.side_effect": self.describe_stack, + "fetch_remote_template_summary.side_effect": self.get_remote_template_summary, + "fetch_local_template_summary.side_effect": self.get_local_template_summary, } ) return self._actions def describe_stack(self): return { - 'Stacks': [ + "Stacks": [ { - 'StackName': self.stack.external_name, - 'Parameters': [ + "StackName": self.stack.external_name, + "Parameters": [ { - 'ParameterKey': key, - 'ParameterValue': value, - 'ResolvedValue': "I'm resolved and don't matter for the diff!" + "ParameterKey": key, + "ParameterValue": value, + "ResolvedValue": "I'm resolved and don't matter for the diff!", } for key, value in self.deployed_parameters.items() ], - 'StackStatus': self.stack_status, - 'NotificationARNs': self.deployed_notification_arns, - 'RoleARN': self.deployed_role_arn, - 'Tags': [ - { - 'Key': key, - 'Value': value - } + "StackStatus": self.stack_status, + "NotificationARNs": self.deployed_notification_arns, + "RoleARN": self.deployed_role_arn, + "Tags": [ + {"Key": key, "Value": value} for key, value in self.deployed_tags.items() ], }, @@ -138,36 +127,32 @@ def get_remote_template_summary(self): params = [] for param, value in self.deployed_parameters.items(): entry = { - 'ParameterKey': param, - 'ParameterType': self.deployed_parameter_types[param] + "ParameterKey": param, + "ParameterType": self.deployed_parameter_types[param], } if param in self.deployed_parameter_defaults: default_value = self.deployed_parameter_defaults[param] - if 'List' in entry['ParameterType']: - default_value = ', '.join(val.strip() for val in default_value.split(',')) - entry['DefaultValue'] = default_value + if "List" in entry["ParameterType"]: + default_value = ", ".join( + val.strip() for val in default_value.split(",") + ) + entry["DefaultValue"] = default_value if param in self.deployed_no_echo_parameters: - entry['NoEcho'] = True + entry["NoEcho"] = True params.append(entry) - return { - 'Parameters': params - } + return {"Parameters": params} def get_local_template_summary(self): params = [] for param, value in self.parameters_on_stack.items(): - entry = { - 'ParameterKey': param - } + entry = {"ParameterKey": param} if param in self.local_no_echo_parameters: - entry['NoEcho'] = True + entry["NoEcho"] = True params.append(entry) - return { - 'Parameters': params - } + return {"Parameters": params} @property def expected_generated_config(self): @@ -176,7 +161,7 @@ def expected_generated_config(self): parameters=self.parameters_on_stack_config, stack_tags=deepcopy(self.tags), notifications=deepcopy(self.notifications), - role_arn=self.role_arn + role_arn=self.role_arn, ) @property @@ -186,7 +171,7 @@ def expected_deployed_config(self): parameters=self.deployed_parameters, stack_tags=deepcopy(self.deployed_tags), notifications=deepcopy(self.deployed_notification_arns), - role_arn=self.deployed_role_arn + role_arn=self.deployed_role_arn, ) def test_diff__compares_deployed_template_to_generated_template(self): @@ -194,28 +179,32 @@ def test_diff__compares_deployed_template_to_generated_template(self): self.command_capturer.compare_templates.assert_called_with( self.actions.fetch_remote_template.return_value, - self.actions.generate.return_value + self.actions.generate.return_value, ) def test_diff__template_diff_is_value_returned_by_implemented_differ(self): diff = self.differ.diff(self.actions) - assert diff.template_diff == self.command_capturer.compare_templates.return_value + assert ( + diff.template_diff == self.command_capturer.compare_templates.return_value + ) def test_diff__compares_deployed_stack_config_to_generated_stack_config(self): - self.deployed_parameters['new'] = 'value' + self.deployed_parameters["new"] = "value" self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_deployed_config, - self.expected_generated_config + self.expected_deployed_config, self.expected_generated_config ) def test_diff__config_diff_is_value_returned_by_implemented_differ(self): diff = self.differ.diff(self.actions) - assert diff.config_diff == self.command_capturer.compare_stack_configurations.return_value + assert ( + diff.config_diff + == self.command_capturer.compare_stack_configurations.return_value + ) def test_diff__returned_diff_has_stack_name_of_external_name(self): diff = self.differ.diff(self.actions) @@ -238,180 +227,192 @@ def test_diff__deployed_stack_does_not_exist__returns_is_deployed_as_false(self) diff = self.differ.diff(self.actions) assert diff.is_deployed is False - def test_diff__deployed_stack_does_not_exist__compares_none_to_generated_config(self): + def test_diff__deployed_stack_does_not_exist__compares_none_to_generated_config( + self, + ): self.actions.describe.return_value = self.actions.describe.side_effect = None self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - None, - self.expected_generated_config + None, self.expected_generated_config ) - def test_diff__deployed_stack_does_not_exist__compares_empty_dict_string_to_generated_template(self): + def test_diff__deployed_stack_does_not_exist__compares_empty_dict_string_to_generated_template( + self, + ): self.actions.fetch_remote_template.return_value = None self.differ.diff(self.actions) self.command_capturer.compare_templates.assert_called_with( - '{}', - self.actions.generate.return_value + "{}", self.actions.generate.return_value ) @pytest.mark.parametrize( - 'status', + "status", [ pytest.param(status) for status in [ - 'CREATE_FAILED', - 'ROLLBACK_COMPLETE', - 'DELETE_COMPLETE', + "CREATE_FAILED", + "ROLLBACK_COMPLETE", + "DELETE_COMPLETE", ] - ] + ], ) - def test_diff__non_deployed_stack_status__compares_none_to_generated_config(self, status): + def test_diff__non_deployed_stack_status__compares_none_to_generated_config( + self, status + ): self.stack_status = status self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - None, - self.expected_generated_config + None, self.expected_generated_config ) @pytest.mark.parametrize( - 'status', + "status", [ pytest.param(status) for status in [ - 'CREATE_FAILED', - 'ROLLBACK_COMPLETE', - 'DELETE_COMPLETE', + "CREATE_FAILED", + "ROLLBACK_COMPLETE", + "DELETE_COMPLETE", ] - ] + ], ) - def test_diff__non_deployed_stack_status__compares_empty_dict_string_to_generated_template(self, status): + def test_diff__non_deployed_stack_status__compares_empty_dict_string_to_generated_template( + self, status + ): self.stack_status = status self.differ.diff(self.actions) self.command_capturer.compare_templates.assert_called_with( - '{}', - self.actions.generate.return_value + "{}", self.actions.generate.return_value ) - def test_diff__deployed_stack_has_default_values__doesnt_pass_parameter__compares_identical_configs(self): - self.deployed_parameters['new'] = 'default value' - self.deployed_parameter_defaults['new'] = 'default value' + def test_diff__deployed_stack_has_default_values__doesnt_pass_parameter__compares_identical_configs( + self, + ): + self.deployed_parameters["new"] = "default value" + self.deployed_parameter_defaults["new"] = "default value" self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_generated_config, - self.expected_generated_config + self.expected_generated_config, self.expected_generated_config ) - def test_diff__deployed_stack_has_list_default_parameter__doesnt_pass_parameter__compares_identical_configs(self): - self.deployed_parameters['new'] = 'first,second,third' - self.deployed_parameter_defaults['new'] = 'first, second, third' - self.deployed_parameter_types['new'] = 'CommaDelimitedList' + def test_diff__deployed_stack_has_list_default_parameter__doesnt_pass_parameter__compares_identical_configs( + self, + ): + self.deployed_parameters["new"] = "first,second,third" + self.deployed_parameter_defaults["new"] = "first, second, third" + self.deployed_parameter_types["new"] = "CommaDelimitedList" self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_generated_config, - self.expected_generated_config + self.expected_generated_config, self.expected_generated_config ) - def test_diff__deployed_stack_has_default_values__passes_the_parameter__compares_identical_configs(self): - self.deployed_parameters['new'] = 'default value' - self.deployed_parameter_defaults['new'] = 'default value' - self.parameters_on_stack_config['new'] = 'default value' + def test_diff__deployed_stack_has_default_values__passes_the_parameter__compares_identical_configs( + self, + ): + self.deployed_parameters["new"] = "default value" + self.deployed_parameter_defaults["new"] = "default value" + self.parameters_on_stack_config["new"] = "default value" self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_generated_config, - self.expected_generated_config + self.expected_generated_config, self.expected_generated_config ) - def test_diff__deployed_stack_has_default_values__passes_different_value__compares_different_configs(self): - self.deployed_parameters['new'] = 'default value' - self.deployed_parameter_defaults['new'] = 'default value' - self.parameters_on_stack_config['new'] = 'custom value' + def test_diff__deployed_stack_has_default_values__passes_different_value__compares_different_configs( + self, + ): + self.deployed_parameters["new"] = "default value" + self.deployed_parameter_defaults["new"] = "default value" + self.parameters_on_stack_config["new"] = "custom value" self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_deployed_config, - self.expected_generated_config + self.expected_deployed_config, self.expected_generated_config ) - def test_diff__stack_exists_with_same_config_but_template_does_not__compares_identical_configs(self): + def test_diff__stack_exists_with_same_config_but_template_does_not__compares_identical_configs( + self, + ): self.actions.fetch_remote_template_summary.side_effect = None self.actions.fetch_remote_template_summary.return_value = None self.actions.fetch_remote_template.return_value = None self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_generated_config, - self.expected_generated_config + self.expected_generated_config, self.expected_generated_config ) - def test_diff__deployed_parameter_has_linebreak_but_otherwise_no_difference__compares_identical_configs(self): - self.deployed_parameters['param'] = self.deployed_parameters['param'] + '\n' + def test_diff__deployed_parameter_has_linebreak_but_otherwise_no_difference__compares_identical_configs( + self, + ): + self.deployed_parameters["param"] = self.deployed_parameters["param"] + "\n" self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_generated_config, - self.expected_generated_config + self.expected_generated_config, self.expected_generated_config ) - def test_diff__parameter_has_identical_string_linebreak__compares_identical_configs(self): - self.deployed_parameters['param'] = self.deployed_parameters['param'] + '\n' - self.parameters_on_stack_config['param'] = self.parameters_on_stack_config['param'] + '\n' + def test_diff__parameter_has_identical_string_linebreak__compares_identical_configs( + self, + ): + self.deployed_parameters["param"] = self.deployed_parameters["param"] + "\n" + self.parameters_on_stack_config["param"] = ( + self.parameters_on_stack_config["param"] + "\n" + ) self.differ.diff(self.actions) generated_config = deepcopy(self.expected_generated_config._asdict()) - generated_parameters = generated_config.pop('parameters') + generated_parameters = generated_config.pop("parameters") expected_config = StackConfiguration( parameters={ - key: value.rstrip('\n') - for key, value in generated_parameters.items() + key: value.rstrip("\n") for key, value in generated_parameters.items() }, **generated_config, ) self.command_capturer.compare_stack_configurations.assert_called_with( - expected_config, - expected_config + expected_config, expected_config ) - def test_diff__parameter_has_identical_list_linebreaks__compares_identical_configs(self): - self.deployed_parameter_types['param'] = 'CommaDelimitedList' - self.deployed_parameters['param'] = 'testing\n,this\n,out\n' - self.parameters_on_stack_config['param'] = [ - 'testing\n', - 'this\n', - 'out\n' - ] + def test_diff__parameter_has_identical_list_linebreaks__compares_identical_configs( + self, + ): + self.deployed_parameter_types["param"] = "CommaDelimitedList" + self.deployed_parameters["param"] = "testing\n,this\n,out\n" + self.parameters_on_stack_config["param"] = ["testing\n", "this\n", "out\n"] self.differ.diff(self.actions) generated_config = deepcopy(self.expected_generated_config._asdict()) - generated_parameters = generated_config.pop('parameters') - generated_parameters['param'] = 'testing,this,out' + generated_parameters = generated_config.pop("parameters") + generated_parameters["param"] = "testing,this,out" expected_config = StackConfiguration( parameters=generated_parameters, **generated_config, ) self.command_capturer.compare_stack_configurations.assert_called_with( - expected_config, - expected_config + expected_config, expected_config ) - def test_diff__no_echo_default_parameter__generated_stack_doesnt_pass_parameter__compares_identical_configs(self): - self.deployed_parameters['new'] = '****' - self.deployed_parameter_defaults['new'] = 'default value' - self.deployed_no_echo_parameters.append('new') + def test_diff__no_echo_default_parameter__generated_stack_doesnt_pass_parameter__compares_identical_configs( + self, + ): + self.deployed_parameters["new"] = "****" + self.deployed_parameter_defaults["new"] = "default value" + self.deployed_no_echo_parameters.append("new") self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with( - self.expected_generated_config, - self.expected_generated_config + self.expected_generated_config, self.expected_generated_config ) def test_diff__generated_template_has_no_echo_parameter__masks_value(self): - self.parameters_on_stack_config['hide_me'] = "don't look at me!" - self.local_no_echo_parameters.append('hide_me') + self.parameters_on_stack_config["hide_me"] = "don't look at me!" + self.local_no_echo_parameters.append("hide_me") expected_generated_config = self.expected_generated_config - expected_generated_config.parameters['hide_me'] = StackDiffer.NO_ECHO_REPLACEMENT + expected_generated_config.parameters[ + "hide_me" + ] = StackDiffer.NO_ECHO_REPLACEMENT self.differ.diff(self.actions) @@ -420,9 +421,11 @@ def test_diff__generated_template_has_no_echo_parameter__masks_value(self): expected_generated_config, ) - def test_diff__generated_template_has_no_echo_parameter__show_no_echo__shows_value(self): - self.parameters_on_stack_config['hide_me'] = "don't look at me!" - self.local_no_echo_parameters.append('hide_me') + def test_diff__generated_template_has_no_echo_parameter__show_no_echo__shows_value( + self, + ): + self.parameters_on_stack_config["hide_me"] = "don't look at me!" + self.local_no_echo_parameters.append("hide_me") self.differ.show_no_echo = True self.differ.diff(self.actions) @@ -433,78 +436,85 @@ def test_diff__generated_template_has_no_echo_parameter__show_no_echo__shows_val class TestDeepDiffStackDiffer: - def setup_method(self, method): self.differ = DeepDiffStackDiffer() self.config1 = StackConfiguration( - stack_name='stack', - parameters={'pk1': 'pv1'}, - stack_tags={'tk1': 'tv1'}, - notifications=['notification'], - role_arn=None + stack_name="stack", + parameters={"pk1": "pv1"}, + stack_tags={"tk1": "tv1"}, + notifications=["notification"], + role_arn=None, ) self.config2 = StackConfiguration( - stack_name='stack', - parameters={'pk1': 'pv1', 'pk2': 'pv2'}, - stack_tags={'tk1': 'tv1'}, - notifications=['notification'], - role_arn='new_role' + stack_name="stack", + parameters={"pk1": "pv1", "pk2": "pv2"}, + stack_tags={"tk1": "tv1"}, + notifications=["notification"], + role_arn="new_role", ) self.template_dict_1 = { - 'AWSTemplateFormat': '2010-09-09', - 'Description': 'deployed', - 'Parameters': {'pk1': 'pv1'}, - 'Resources': {} + "AWSTemplateFormat": "2010-09-09", + "Description": "deployed", + "Parameters": {"pk1": "pv1"}, + "Resources": {}, } self.template_dict_2 = { - 'AWSTemplateFormat': '2010-09-09', - 'Description': 'deployed', - 'Parameters': {'pk1': 'pv1'}, - 'Resources': { - 'MyBucket': { - 'Type': 'AWS::S3::Bucket', - 'Properties': { - 'BucketName': 'test' - } + "AWSTemplateFormat": "2010-09-09", + "Description": "deployed", + "Parameters": {"pk1": "pv1"}, + "Resources": { + "MyBucket": { + "Type": "AWS::S3::Bucket", + "Properties": {"BucketName": "test"}, } - } + }, } - def test_compare_stack_configurations__returns_deepdiff_of_deployed_and_generated(self): - comparison = self.differ.compare_stack_configurations(self.config1, self.config2) + def test_compare_stack_configurations__returns_deepdiff_of_deployed_and_generated( + self, + ): + comparison = self.differ.compare_stack_configurations( + self.config1, self.config2 + ) assert comparison.t1 == self.config1 assert comparison.t2 == self.config2 def test_compare_stack_configurations__returned_deepdiff_has_verbosity_of_2(self): - comparison = self.differ.compare_stack_configurations(self.config1, self.config2) + comparison = self.differ.compare_stack_configurations( + self.config1, self.config2 + ) assert comparison.verbose_level == 2 - def test_compare_stack_configurations__deployed_is_none__returns_deepdiff_with_none_for_t1(self): + def test_compare_stack_configurations__deployed_is_none__returns_deepdiff_with_none_for_t1( + self, + ): comparison = self.differ.compare_stack_configurations(None, self.config2) assert comparison.t1 is None @pytest.mark.parametrize( - 't1_serializer, t2_serializer', + "t1_serializer, t2_serializer", [ - pytest.param(json.dumps, json.dumps, id='templates are json'), - pytest.param(yaml.dump, yaml.dump, id='templates are yaml'), + pytest.param(json.dumps, json.dumps, id="templates are json"), + pytest.param(yaml.dump, yaml.dump, id="templates are yaml"), pytest.param(json.dumps, yaml.dump, id="templates are mixed formats"), - ] + ], ) def test_compare_templates__templates_are_json__returns_deepdiff_of_dicts( - self, - t1_serializer, - t2_serializer + self, t1_serializer, t2_serializer ): - template1, template2 = t1_serializer(self.template_dict_1), t2_serializer(self.template_dict_2) + template1, template2 = t1_serializer(self.template_dict_1), t2_serializer( + self.template_dict_2 + ) comparison = self.differ.compare_templates(template1, template2) assert comparison.t1 == self.template_dict_1 assert comparison.t2 == self.template_dict_2 - def test_compare_templates__templates_are_yaml_with_intrinsic_functions__returns_deepdiff_of_dicts(self): + def test_compare_templates__templates_are_yaml_with_intrinsic_functions__returns_deepdiff_of_dicts( + self, + ): template = """ Resources: MyBucket: @@ -517,74 +527,76 @@ def test_compare_templates__templates_are_yaml_with_intrinsic_functions__returns expected = cfn_flip.load_yaml(template) assert (comparison.t1, comparison.t2) == (expected, expected) - def test_compare_templates__deployed_is_empty_dict_string__returns_deepdiff_with_empty_dict_for_t1(self): + def test_compare_templates__deployed_is_empty_dict_string__returns_deepdiff_with_empty_dict_for_t1( + self, + ): template = json.dumps(self.template_dict_1) - comparison = self.differ.compare_templates('{}', template) + comparison = self.differ.compare_templates("{}", template) assert comparison.t1 == {} class TestDifflibStackDiffer: - def setup_method(self, method): self.serialize = cfn_flip.dump_yaml self.differ = DifflibStackDiffer() self.config1 = StackConfiguration( - stack_name='stack', - parameters={'pk1': 'pv1'}, - stack_tags={'tk1': 'tv1'}, - notifications=['notification'], - role_arn=None + stack_name="stack", + parameters={"pk1": "pv1"}, + stack_tags={"tk1": "tv1"}, + notifications=["notification"], + role_arn=None, ) self.config2 = StackConfiguration( - stack_name='stack', - parameters={'pk1': 'pv1', 'pk2': 'pv2'}, - stack_tags={'tk1': 'tv1'}, - notifications=['notification'], - role_arn='new_role' + stack_name="stack", + parameters={"pk1": "pv1", "pk2": "pv2"}, + stack_tags={"tk1": "tv1"}, + notifications=["notification"], + role_arn="new_role", ) self.template_dict_1 = { - 'AWSTemplateFormat': '2010-09-09', - 'Description': 'deployed', - 'Parameters': {'pk1': 'pv1'}, - 'Resources': {} + "AWSTemplateFormat": "2010-09-09", + "Description": "deployed", + "Parameters": {"pk1": "pv1"}, + "Resources": {}, } self.template_dict_2 = { - 'AWSTemplateFormat': '2010-09-09', - 'Description': 'deployed', - 'Parameters': {'pk1': 'pv1'}, - 'Resources': { - 'MyBucket': { - 'Type': 'AWS::S3::Bucket', - 'Properties': { - 'BucketName': 'test' - } + "AWSTemplateFormat": "2010-09-09", + "Description": "deployed", + "Parameters": {"pk1": "pv1"}, + "Resources": { + "MyBucket": { + "Type": "AWS::S3::Bucket", + "Properties": {"BucketName": "test"}, } - } + }, } def create_expected_diff(self, first, second, already_formatted=False): if not already_formatted: deployed_dict, deployed_format = cfn_flip.load(first) generated_dict, generated_format = cfn_flip.load(second) - dumpers = { - 'json': cfn_flip.dump_json, - 'yaml': cfn_flip.dump_yaml - } + dumpers = {"json": cfn_flip.dump_json, "yaml": cfn_flip.dump_yaml} first = dumpers[generated_format](deployed_dict) second = dumpers[generated_format](generated_dict) first_list, second_list = first.splitlines(), second.splitlines() - return list(difflib.unified_diff( - first_list, - second_list, - fromfile='deployed', - tofile='generated', - lineterm='' - )) - - def test_compare_stack_configurations__returns_diff_of_deployed_and_generated_when_converted_to_dicts(self): - comparison = self.differ.compare_stack_configurations(self.config1, self.config2) + return list( + difflib.unified_diff( + first_list, + second_list, + fromfile="deployed", + tofile="generated", + lineterm="", + ) + ) + + def test_compare_stack_configurations__returns_diff_of_deployed_and_generated_when_converted_to_dicts( + self, + ): + comparison = self.differ.compare_stack_configurations( + self.config1, self.config2 + ) expected_config_1_dict = self.make_config_comparable(self.config1) expected_config_2_dict = self.make_config_comparable(self.config2) expected_config_1 = self.serialize(expected_config_1_dict) @@ -598,70 +610,84 @@ def make_config_comparable(self, config: StackConfiguration): without_empty_values = { key: value for key, value in config_dict.items() - if value not in (None, [], {}) and key != 'stack_name' + if value not in (None, [], {}) and key != "stack_name" } return without_empty_values - def test_compare_stack_configurations__deployed_is_none__returns_diff_with_none(self): + def test_compare_stack_configurations__deployed_is_none__returns_diff_with_none( + self, + ): comparison = self.differ.compare_stack_configurations(None, self.config2) expected = self.create_expected_diff( self.serialize(None), - self.serialize(self.make_config_comparable(self.config2)) + self.serialize(self.make_config_comparable(self.config2)), ) assert comparison == expected - def test_compare_stack_configurations__deployed_is_none__all_configs_are_falsey__returns_diff_with_none(self): + def test_compare_stack_configurations__deployed_is_none__all_configs_are_falsey__returns_diff_with_none( + self, + ): empty_config = StackConfiguration( - stack_name='stack', + stack_name="stack", parameters={}, stack_tags={}, notifications=[], - role_arn=None + role_arn=None, ) comparison = self.differ.compare_stack_configurations(None, empty_config) expected = self.create_expected_diff( self.serialize(None), self.serialize(self.make_config_comparable(empty_config)), - already_formatted=True + already_formatted=True, ) assert comparison == expected @pytest.mark.parametrize( - 'serializer', + "serializer", [ - pytest.param(json.dumps, id='templates are json'), - pytest.param(yaml.dump, id='templates are yaml'), - ] + pytest.param(json.dumps, id="templates are json"), + pytest.param(yaml.dump, id="templates are yaml"), + ], ) def test_compare_templates__templates_are_json__returns_deepdiff_of_dicts( self, serializer, ): - template1, template2 = serializer(self.template_dict_1), serializer(self.template_dict_2) + template1, template2 = serializer(self.template_dict_1), serializer( + self.template_dict_2 + ) comparison = self.differ.compare_templates(template1, template2) expected = self.create_expected_diff(template1, template2) assert comparison == expected - def test_compare_templates__deployed_is_empty_dict_string__returns_diff_with_empty_string(self): + def test_compare_templates__deployed_is_empty_dict_string__returns_diff_with_empty_string( + self, + ): template = json.dumps(self.template_dict_1) - comparison = self.differ.compare_templates('{}', template) - expected = self.create_expected_diff('{}', template) + comparison = self.differ.compare_templates("{}", template) + expected = self.create_expected_diff("{}", template) assert comparison == expected - def test_compare_templates__json_template__only_indentation_diff__returns_no_diff(self): + def test_compare_templates__json_template__only_indentation_diff__returns_no_diff( + self, + ): template1 = json.dumps(self.template_dict_1, indent=2) template2 = json.dumps(self.template_dict_1, indent=4) comparison = self.differ.compare_templates(template1, template2) assert len(comparison) == 0 - def test_compare_templates__yaml_template__only_indentation_diff__returns_no_diff(self): + def test_compare_templates__yaml_template__only_indentation_diff__returns_no_diff( + self, + ): template1 = yaml.dump(self.template_dict_1, indent=2) template2 = yaml.dump(self.template_dict_1, indent=4) comparison = self.differ.compare_templates(template1, template2) assert len(comparison) == 0 - def test_compare_templates__opposite_template_types_but_identical_template__returns_no_diff(self): + def test_compare_templates__opposite_template_types_but_identical_template__returns_no_diff( + self, + ): template1 = json.dumps(self.template_dict_1) template2 = yaml.dump(self.template_dict_1) comparison = self.differ.compare_templates(template1, template2) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 37335bcf4..911d02c5a 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -32,7 +32,7 @@ def test_normalise_path_with_backslashes_in_path(self): assert path == join("valid", "path") def test_normalise_path_with_double_backslashes_in_path(self): - path = normalise_path('valid\\path') + path = normalise_path("valid\\path") assert path == join("valid", "path") def test_normalise_path_with_leading_slash(self): @@ -40,40 +40,32 @@ def test_normalise_path_with_leading_slash(self): assert path == join("{}this".format(sep), "is", "valid") def test_normalise_path_with_leading_backslash(self): - path = normalise_path('\\this\\path\\is\\valid') + path = normalise_path("\\this\\path\\is\\valid") assert path == join("{}this".format(sep), "path", "is", "valid") def test_normalise_path_with_trailing_slash(self): with pytest.raises(PathConversionError): - normalise_path( - "this/path/is/invalid/" - ) + normalise_path("this/path/is/invalid/") def test_normalise_path_with_trailing_backslash(self): with pytest.raises(PathConversionError): - normalise_path( - 'this\\path\\is\\invalid\\' - ) + normalise_path("this\\path\\is\\invalid\\") def test_sceptreise_path_with_valid_path(self): - path = 'dev/app/stack' + path = "dev/app/stack" assert sceptreise_path(path) == path def test_sceptreise_path_with_windows_path(self): - windows_path = 'dev\\app\\stack' - assert sceptreise_path(windows_path) == 'dev/app/stack' + windows_path = "dev\\app\\stack" + assert sceptreise_path(windows_path) == "dev/app/stack" def test_sceptreise_path_with_trailing_slash(self): with pytest.raises(PathConversionError): - sceptreise_path( - "this/path/is/invalid/" - ) + sceptreise_path("this/path/is/invalid/") def test_sceptreise_path_with_trailing_backslash(self): with pytest.raises(PathConversionError): - sceptreise_path( - 'this\\path\\is\\invalid\\' - ) + sceptreise_path("this\\path\\is\\invalid\\") def test_get_response_datetime__response_is_valid__returns_datetime(self): resp = { @@ -81,7 +73,9 @@ def test_get_response_datetime__response_is_valid__returns_datetime(self): "HTTPHeaders": {"date": "Wed, 16 Oct 2019 07:28:00 GMT"} } } - assert extract_datetime_from_aws_response_headers(resp) == datetime(2019, 10, 16, 7, 28, tzinfo=timezone.utc) + assert extract_datetime_from_aws_response_headers(resp) == datetime( + 2019, 10, 16, 7, 28, tzinfo=timezone.utc + ) def test_get_response_datetime__response_has_offset__returns_datetime(self): resp = { @@ -90,14 +84,12 @@ def test_get_response_datetime__response_has_offset__returns_datetime(self): } } offset = timezone(timedelta(hours=4)) - assert extract_datetime_from_aws_response_headers(resp) == datetime(2019, 10, 16, 7, 28, tzinfo=offset) + assert extract_datetime_from_aws_response_headers(resp) == datetime( + 2019, 10, 16, 7, 28, tzinfo=offset + ) def test_get_response_datetime__date_string_is_invalid__returns_none(self): - resp = { - "ResponseMetadata": { - "HTTPHeaders": {"date": "garbage"} - } - } + resp = {"ResponseMetadata": {"HTTPHeaders": {"date": "garbage"}}} assert extract_datetime_from_aws_response_headers(resp) is None def test_get_response_datetime__response_is_empty__returns_none(self): diff --git a/tests/test_hooks/test_asg_scaling_processes.py b/tests/test_hooks/test_asg_scaling_processes.py index 2754c65a0..be9f504d6 100644 --- a/tests/test_hooks/test_asg_scaling_processes.py +++ b/tests/test_hooks/test_asg_scaling_processes.py @@ -23,18 +23,18 @@ def test_get_stack_resources_sends_correct_request(self): "StackResources": [ { "ResourceType": "AWS::AutoScaling::AutoScalingGroup", - 'PhysicalResourceId': 'cloudreach-examples-asg' + "PhysicalResourceId": "cloudreach-examples-asg", } ] } self.asg_scaling_processes._get_stack_resources() self.stack.connection_manager.call.assert_called_with( - service="cloudformation", - command="describe_stack_resources", - kwargs={ - "StackName": "external_name", - } - ) + service="cloudformation", + command="describe_stack_resources", + kwargs={ + "StackName": "external_name", + }, + ) @patch( "sceptre.hooks.asg_scaling_processes" @@ -43,14 +43,16 @@ def test_get_stack_resources_sends_correct_request(self): def test_find_autoscaling_groups_with_stack_with_asgs( self, mock_get_stack_resources ): - mock_get_stack_resources.return_value = [{ - 'LogicalResourceId': 'AutoScalingGroup', - 'PhysicalResourceId': 'cloudreach-examples-asg', - 'ResourceStatus': 'CREATE_COMPLETE', - 'ResourceType': 'AWS::AutoScaling::AutoScalingGroup', - 'StackId': 'arn:aws:...', - 'StackName': 'cloudreach-examples-dev-vpc' - }] + mock_get_stack_resources.return_value = [ + { + "LogicalResourceId": "AutoScalingGroup", + "PhysicalResourceId": "cloudreach-examples-asg", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::AutoScaling::AutoScalingGroup", + "StackId": "arn:aws:...", + "StackName": "cloudreach-examples-dev-vpc", + } + ] response = self.asg_scaling_processes._find_autoscaling_groups() assert response == ["cloudreach-examples-asg"] @@ -72,19 +74,17 @@ def test_find_autoscaling_groups_with_stack_without_asgs( ".ASGScalingProcesses._find_autoscaling_groups" ) def test_run_with_resume_argument(self, mock_find_autoscaling_groups): - self.asg_scaling_processes.argument = u"resume::ScheduledActions" + self.asg_scaling_processes.argument = "resume::ScheduledActions" mock_find_autoscaling_groups.return_value = ["autoscaling_group_1"] self.asg_scaling_processes.run() self.stack.connection_manager.call.assert_called_once_with( - service="autoscaling", - command="resume_processes", - kwargs={ - "AutoScalingGroupName": "autoscaling_group_1", - "ScalingProcesses": [ - "ScheduledActions" - ] - } - ) + service="autoscaling", + command="resume_processes", + kwargs={ + "AutoScalingGroupName": "autoscaling_group_1", + "ScalingProcesses": ["ScheduledActions"], + }, + ) @patch( "sceptre.hooks.asg_scaling_processes" @@ -95,24 +95,20 @@ def test_run_with_suspend_argument(self, mock_find_autoscaling_groups): mock_find_autoscaling_groups.return_value = ["autoscaling_group_1"] self.asg_scaling_processes.run() self.stack.connection_manager.call.assert_called_once_with( - service="autoscaling", - command="suspend_processes", - kwargs={ - "AutoScalingGroupName": "autoscaling_group_1", - "ScalingProcesses": [ - "ScheduledActions" - ] - } - ) + service="autoscaling", + command="suspend_processes", + kwargs={ + "AutoScalingGroupName": "autoscaling_group_1", + "ScalingProcesses": ["ScheduledActions"], + }, + ) @patch( "sceptre.hooks.asg_scaling_processes" ".ASGScalingProcesses._find_autoscaling_groups" ) - def test_run_with_invalid_string_argument( - self, mock_find_autoscaling_groups - ): - self.asg_scaling_processes.argument = u"invalid_string" + def test_run_with_invalid_string_argument(self, mock_find_autoscaling_groups): + self.asg_scaling_processes.argument = "invalid_string" mock_find_autoscaling_groups.return_value = ["autoscaling_group_1"] with pytest.raises(InvalidHookArgumentSyntaxError): self.asg_scaling_processes.run() diff --git a/tests/test_hooks/test_cmd.py b/tests/test_hooks/test_cmd.py index 5e4fd12bf..630ab6d63 100644 --- a/tests/test_hooks/test_cmd.py +++ b/tests/test_hooks/test_cmd.py @@ -17,15 +17,15 @@ def test_run_with_non_str_argument(self): with pytest.raises(InvalidHookArgumentTypeError): self.cmd.run() - @patch('sceptre.hooks.cmd.subprocess.check_call') + @patch("sceptre.hooks.cmd.subprocess.check_call") def test_run_with_str_argument(self, mock_call): - self.cmd.argument = u"echo hello" + self.cmd.argument = "echo hello" self.cmd.run() - mock_call.assert_called_once_with(u"echo hello", shell=True) + mock_call.assert_called_once_with("echo hello", shell=True) - @patch('sceptre.hooks.cmd.subprocess.check_call') + @patch("sceptre.hooks.cmd.subprocess.check_call") def test_run_with_erroring_command(self, mock_call): mock_call.side_effect = subprocess.CalledProcessError(1, "echo") - self.cmd.argument = u"echo hello" + self.cmd.argument = "echo hello" with pytest.raises(subprocess.CalledProcessError): self.cmd.run() diff --git a/tests/test_hooks/test_hooks.py b/tests/test_hooks/test_hooks.py index 980c7da1b..2af9d8f0d 100644 --- a/tests/test_hooks/test_hooks.py +++ b/tests/test_hooks/test_hooks.py @@ -22,8 +22,8 @@ def test_add_stack_hooks(self): mock_object = MagicMock() mock_object.stack.hooks = { - 'before_mock_function': [mock_hook_before], - 'after_mock_function': [mock_hook_after] + "before_mock_function": [mock_hook_before], + "after_mock_function": [mock_hook_after], } def mock_function(self): @@ -31,7 +31,7 @@ def mock_function(self): assert mock_hook_after.run.call_count == 0 mock_object.mock_function = mock_function - mock_object.mock_function.__name__ = 'mock_function' + mock_object.mock_function.__name__ = "mock_function" add_stack_hooks(mock_object.mock_function)(mock_object) @@ -74,7 +74,6 @@ class MockClass(object): class TestHookPropertyDescriptor(object): - def setup_method(self, test_method): self.mock_object = MockClass() diff --git a/tests/test_plan.py b/tests/test_plan.py index dafe9fb80..44d6767ad 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -8,28 +8,33 @@ class TestSceptrePlan(object): - def setup_method(self, test_method): self.patcher_SceptrePlan = patch("sceptre.plan.plan.SceptrePlan") self.stack = Stack( - name='dev/app/stack', project_code=sentinel.project_code, - template_path=sentinel.template_path, region=sentinel.region, - profile=sentinel.profile, parameters={"key1": "val1"}, - sceptre_user_data=sentinel.sceptre_user_data, hooks={}, - s3_details=None, dependencies=sentinel.dependencies, - role_arn=sentinel.role_arn, protected=False, - tags={"tag1": "val1"}, external_name=sentinel.external_name, + name="dev/app/stack", + project_code=sentinel.project_code, + template_path=sentinel.template_path, + region=sentinel.region, + profile=sentinel.profile, + parameters={"key1": "val1"}, + sceptre_user_data=sentinel.sceptre_user_data, + hooks={}, + s3_details=None, + dependencies=sentinel.dependencies, + role_arn=sentinel.role_arn, + protected=False, + tags={"tag1": "val1"}, + external_name=sentinel.external_name, notifications=[sentinel.notification], on_failure=sentinel.on_failure, - stack_timeout=sentinel.stack_timeout + stack_timeout=sentinel.stack_timeout, ) self.mock_context = MagicMock(spec=SceptreContext) self.mock_config_reader = MagicMock(spec=ConfigReader) self.mock_context.project_path = sentinel.project_path self.mock_context.command_path = sentinel.command_path self.mock_context.config_file = sentinel.config_file - self.mock_context.full_config_path.return_value =\ - sentinel.full_config_path + self.mock_context.full_config_path.return_value = sentinel.full_config_path self.mock_context.user_variables = {} self.mock_context.options = {} self.mock_context.no_colour = True @@ -47,8 +52,8 @@ def test_planner_executes_with_params(self): plan = MagicMock(spec=SceptrePlan) plan.context = self.mock_context plan.launch.return_value = sentinel.success - result = plan.launch('test-attribute') - plan.launch.assert_called_once_with('test-attribute') + result = plan.launch("test-attribute") + plan.launch.assert_called_once_with("test-attribute") assert result == sentinel.success def test_command_not_found_error_raised(self): diff --git a/tests/test_resolvers/test_environment_variable.py b/tests/test_resolvers/test_environment_variable.py index f53e672d6..e6fe9cc5e 100644 --- a/tests/test_resolvers/test_environment_variable.py +++ b/tests/test_resolvers/test_environment_variable.py @@ -6,11 +6,8 @@ class TestEnvironmentVariableResolver(object): - def setup_method(self, test_method): - self.environment_variable_resolver = EnvironmentVariable( - argument=None - ) + self.environment_variable_resolver = EnvironmentVariable(argument=None) @patch("sceptre.resolvers.environment_variable.os") def test_resolving_with_set_environment_variable(self, mock_os): diff --git a/tests/test_resolvers/test_file_contents.py b/tests/test_resolvers/test_file_contents.py index 935536a7f..d6be34fa1 100644 --- a/tests/test_resolvers/test_file_contents.py +++ b/tests/test_resolvers/test_file_contents.py @@ -7,14 +7,11 @@ class TestFileContentsResolver(object): - def setup_method(self, test_method): - self.file_contents_resolver = FileContents( - argument=None - ) + self.file_contents_resolver = FileContents(argument=None) def test_resolving_with_existing_file(self): - with tempfile.NamedTemporaryFile(mode='w+') as f: + with tempfile.NamedTemporaryFile(mode="w+") as f: f.write("file contents") f.seek(0) self.file_contents_resolver.argument = f.name diff --git a/tests/test_resolvers/test_placeholders.py b/tests/test_resolvers/test_placeholders.py index 76f52cc58..fa2377fa9 100644 --- a/tests/test_resolvers/test_placeholders.py +++ b/tests/test_resolvers/test_placeholders.py @@ -4,12 +4,11 @@ from sceptre.resolvers.placeholders import ( use_resolver_placeholders_on_error, PlaceholderType, - create_placeholder_value + create_placeholder_value, ) class TestPlaceholders: - def test_are_placeholders_enabled__returns_false(self): assert are_placeholders_enabled() is False @@ -23,33 +22,52 @@ def test_are_placeholders_enabled__out_of_placeholder_context__returns_false(sel assert are_placeholders_enabled() is False - def test_are_placeholders_enabled__error_in_placeholder_context__returns_false(self): + def test_are_placeholders_enabled__error_in_placeholder_context__returns_false( + self, + ): with pytest.raises(ValueError), use_resolver_placeholders_on_error(): raise ValueError() assert are_placeholders_enabled() is False @pytest.mark.parametrize( - 'placeholder_type,argument,expected', + "placeholder_type,argument,expected", [ - pytest.param(PlaceholderType.explicit, None, '{ !MyResolver }', id='explicit no argument'), pytest.param( PlaceholderType.explicit, - 'argument', - '{ !MyResolver(argument) }', - id='explicit string argument' + None, + "{ !MyResolver }", + id="explicit no argument", ), pytest.param( PlaceholderType.explicit, - {'key': 'value'}, + "argument", + "{ !MyResolver(argument) }", + id="explicit string argument", + ), + pytest.param( + PlaceholderType.explicit, + {"key": "value"}, "{ !MyResolver({'key': 'value'}) }", - id='explicit dict argument' + id="explicit dict argument", + ), + pytest.param( + PlaceholderType.alphanum, None, "MyResolver", id="alphanum no argument" + ), + pytest.param( + PlaceholderType.alphanum, + "argument", + "MyResolverargument", + id="alphanum string argument", + ), + pytest.param( + PlaceholderType.alphanum, + {"key": "value"}, + "MyResolverkeyvalue", + id="alphanum dict argument", ), - pytest.param(PlaceholderType.alphanum, None, 'MyResolver', id='alphanum no argument'), - pytest.param(PlaceholderType.alphanum, 'argument', 'MyResolverargument', id='alphanum string argument'), - pytest.param(PlaceholderType.alphanum, {'key': 'value'}, 'MyResolverkeyvalue', id='alphanum dict argument'), - pytest.param(PlaceholderType.none, 'something', None) - ] + pytest.param(PlaceholderType.none, "something", None), + ], ) def test_create_placeholder_value(self, placeholder_type, argument, expected): class MyResolver(Resolver): diff --git a/tests/test_resolvers/test_resolver.py b/tests/test_resolvers/test_resolver.py index 48c2507e9..2bbdde0a4 100644 --- a/tests/test_resolvers/test_resolver.py +++ b/tests/test_resolvers/test_resolver.py @@ -8,9 +8,13 @@ Resolver, ResolvableContainerProperty, ResolvableValueProperty, - RecursiveResolve + RecursiveResolve, +) +from sceptre.resolvers.placeholders import ( + use_resolver_placeholders_on_error, + create_placeholder_value, + PlaceholderType, ) -from sceptre.resolvers.placeholders import use_resolver_placeholders_on_error, create_placeholder_value, PlaceholderType class MockResolver(Resolver): @@ -25,25 +29,23 @@ def resolve(self): class MockClass(object): - resolvable_container_property = ResolvableContainerProperty("resolvable_container_property") + resolvable_container_property = ResolvableContainerProperty( + "resolvable_container_property" + ) container_with_alphanum_placeholder = ResolvableContainerProperty( - "container_with_placeholder_override", - PlaceholderType.alphanum + "container_with_placeholder_override", PlaceholderType.alphanum ) - resolvable_value_property = ResolvableValueProperty('resolvable_value_property') + resolvable_value_property = ResolvableValueProperty("resolvable_value_property") value_with_none_placeholder = ResolvableValueProperty( - 'value_with_placeholder_override', - PlaceholderType.none + "value_with_placeholder_override", PlaceholderType.none ) config = MagicMock() class TestResolver(object): - def setup_method(self, test_method): self.mock_resolver = MockResolver( - argument=sentinel.argument, - stack=sentinel.stack + argument=sentinel.argument, stack=sentinel.stack ) def test_init(self): @@ -52,7 +54,6 @@ def test_init(self): class TestResolvableContainerPropertyDescriptor: - def setup_method(self, test_method): self.mock_object = MockClass() @@ -69,16 +70,8 @@ def test_setting_resolvable_property_with_nested_lists(self): [ mock_resolver, "String", - [ - [ - mock_resolver, - "String", - None - ], - mock_resolver, - "String" - ] - ] + [[mock_resolver, "String", None], mock_resolver, "String"], + ], ] cloned_data_structure = [ @@ -88,23 +81,16 @@ def test_setting_resolvable_property_with_nested_lists(self): mock_resolver.clone.return_value, "String", [ - [ - mock_resolver.clone.return_value, - "String", - None - ], + [mock_resolver.clone.return_value, "String", None], mock_resolver.clone.return_value, - "String" - ] - ] + "String", + ], + ], ] self.mock_object.resolvable_container_property = complex_data_structure assert self.mock_object._resolvable_container_property == cloned_data_structure - expected_calls = [ - call(self.mock_object), - call().setup() - ] * 4 + expected_calls = [call(self.mock_object), call().setup()] * 4 mock_resolver.clone.assert_has_calls(expected_calls) def test_getting_resolvable_property_with_none(self): @@ -121,18 +107,10 @@ def test_getting_resolvable_property_with_nested_lists(self): [ mock_resolver, "String", - [ - [ - mock_resolver, - "String", - None - ], - mock_resolver, - "String" - ], - None + [[mock_resolver, "String", None], mock_resolver, "String"], + None, ], - None + None, ] resolved_complex_data_structure = [ @@ -141,27 +119,17 @@ def test_getting_resolvable_property_with_nested_lists(self): [ "Resolved", "String", - [ - [ - "Resolved", - "String", - None - ], - "Resolved", - "String" - ], - None + [["Resolved", "String", None], "Resolved", "String"], + None, ], - None + None, ] self.mock_object._resolvable_container_property = complex_data_structure prop = self.mock_object.resolvable_container_property assert prop == resolved_complex_data_structure - def test_getting_resolvable_property_with_nested_dictionaries_and_lists( - self - ): + def test_getting_resolvable_property_with_nested_dictionaries_and_lists(self): mock_resolver = MagicMock(spec=MockResolver) mock_resolver.resolve.return_value = "Resolved" @@ -170,42 +138,28 @@ def test_getting_resolvable_property_with_nested_dictionaries_and_lists( "None": None, "Resolver": mock_resolver, "List": [ - [ - mock_resolver, - "String", - None - ], - { - "Dictionary": {}, - "String": "String", - "None": None, - "Resolver": mock_resolver, - "List": [ - mock_resolver - ] - }, - mock_resolver, - "String" + [mock_resolver, "String", None], + { + "Dictionary": {}, + "String": "String", + "None": None, + "Resolver": mock_resolver, + "List": [mock_resolver], + }, + mock_resolver, + "String", ], "Dictionary": { "Resolver": mock_resolver, "Dictionary": { - "List": [ - [ - mock_resolver, - "String", - None - ], - mock_resolver, - "String" - ], + "List": [[mock_resolver, "String", None], mock_resolver, "String"], "String": "String", "None": None, - "Resolver": mock_resolver + "Resolver": mock_resolver, }, "String": "String", - "None": None - } + "None": None, + }, } resolved_complex_data_structure = { @@ -213,42 +167,28 @@ def test_getting_resolvable_property_with_nested_dictionaries_and_lists( "None": None, "Resolver": "Resolved", "List": [ - [ - "Resolved", - "String", - None - ], - { - "Dictionary": {}, - "String": "String", - "None": None, - "Resolver": "Resolved", - "List": [ - "Resolved" - ] - }, - "Resolved", - "String" + ["Resolved", "String", None], + { + "Dictionary": {}, + "String": "String", + "None": None, + "Resolver": "Resolved", + "List": ["Resolved"], + }, + "Resolved", + "String", ], "Dictionary": { "Resolver": "Resolved", "Dictionary": { - "List": [ - [ - "Resolved", - "String", - None - ], - "Resolved", - "String" - ], + "List": [["Resolved", "String", None], "Resolved", "String"], "String": "String", "None": None, - "Resolver": "Resolved" + "Resolver": "Resolved", }, "String": "String", - "None": None - } + "None": None, + }, } self.mock_object._resolvable_container_property = complex_data_structure @@ -269,11 +209,11 @@ def test_getting_resolvable_property_with_nested_dictionaries(self): "Dictionary": {}, "String": "String", "None": None, - "Resolver": mock_resolver + "Resolver": mock_resolver, }, "String": "String", - "None": None - } + "None": None, + }, } resolved_complex_data_structure = { @@ -286,11 +226,11 @@ def test_getting_resolvable_property_with_nested_dictionaries(self): "Dictionary": {}, "String": "String", "None": None, - "Resolver": "Resolved" + "Resolver": "Resolved", }, "String": "String", - "None": None - } + "None": None, + }, } self.mock_object._resolvable_container_property = complex_data_structure @@ -300,111 +240,109 @@ def test_getting_resolvable_property_with_nested_dictionaries(self): def test_get__resolver_references_same_property_for_other_value__resolves_it(self): class MyResolver(Resolver): def resolve(self): - return self.stack.resolvable_container_property['other_value'] + return self.stack.resolvable_container_property["other_value"] resolver = MyResolver() self.mock_object.resolvable_container_property = { - 'other_value': 'abc', - 'resolver': resolver + "other_value": "abc", + "resolver": resolver, } - assert self.mock_object.resolvable_container_property['resolver'] == 'abc' + assert self.mock_object.resolvable_container_property["resolver"] == "abc" def test_get__resolver_references_itself__raises_recursive_resolve(self): class RecursiveResolver(Resolver): def resolve(self): - return self.stack.resolvable_container_property['resolver'] + return self.stack.resolvable_container_property["resolver"] resolver = RecursiveResolver() - self.mock_object.resolvable_container_property = { - 'resolver': resolver - } + self.mock_object.resolvable_container_property = {"resolver": resolver} with pytest.raises(RecursiveResolve): self.mock_object.resolvable_container_property - def test_get__resolvable_container_property_references_same_property_of_other_stack__resolves(self): + def test_get__resolvable_container_property_references_same_property_of_other_stack__resolves( + self, + ): stack1 = MockClass() - stack1.resolvable_container_property = { - 'testing': 'stack1' - } + stack1.resolvable_container_property = {"testing": "stack1"} class OtherStackResolver(Resolver): def resolve(self): - return stack1.resolvable_container_property['testing'] + return stack1.resolvable_container_property["testing"] stack2 = MockClass() - stack2.resolvable_container_property = { - 'resolver': OtherStackResolver() - } + stack2.resolvable_container_property = {"resolver": OtherStackResolver()} - assert stack2.resolvable_container_property == { - 'resolver': 'stack1' - } + assert stack2.resolvable_container_property == {"resolver": "stack1"} - def test_get__resolver_resolves_to_none__value_is_dict__deletes_those_items_from_dict(self): + def test_get__resolver_resolves_to_none__value_is_dict__deletes_those_items_from_dict( + self, + ): class MyResolver(Resolver): def resolve(self): return None resolver = MyResolver() self.mock_object.resolvable_container_property = { - 'a': 4, - 'b': resolver, - 'c': 3, - 'd': resolver, - 'e': resolver, - 'f': 5, + "a": 4, + "b": resolver, + "c": 3, + "d": resolver, + "e": resolver, + "f": 5, } - expected = {'a': 4, 'c': 3, 'f': 5} + expected = {"a": 4, "c": 3, "f": 5} assert self.mock_object.resolvable_container_property == expected - def test_get__resolver_resolves_to_none__value_is_dict__deletes_those_items_from_complex_structure(self): + def test_get__resolver_resolves_to_none__value_is_dict__deletes_those_items_from_complex_structure( + self, + ): class MyResolver(Resolver): def resolve(self): return None resolver = MyResolver() self.mock_object.resolvable_container_property = { - 'a': 4, - 'b': [ + "a": 4, + "b": [ resolver, ], - 'c': [{ - 'v': resolver - }], - 'd': 3 + "c": [{"v": resolver}], + "d": 3, } - expected = {'a': 4, 'b': [], 'c': [{}], 'd': 3} + expected = {"a": 4, "b": [], "c": [{}], "d": 3} assert self.mock_object.resolvable_container_property == expected - def test_get__resolver_resolves_to_none__value_is_list__deletes_that_item_from_list(self): + def test_get__resolver_resolves_to_none__value_is_list__deletes_that_item_from_list( + self, + ): class MyResolver(Resolver): def resolve(self): return None resolver = MyResolver() - self.mock_object.resolvable_container_property = [ - 1, - resolver, - 3 - ] + self.mock_object.resolvable_container_property = [1, resolver, 3] expected = [1, 3] assert self.mock_object.resolvable_container_property == expected - def test_get__resolver_resolves_to_none__value_is_dict__deletes_that_key_from_dict(self): + def test_get__resolver_resolves_to_none__value_is_dict__deletes_that_key_from_dict( + self, + ): class MyResolver(Resolver): def resolve(self): return None resolver = MyResolver() self.mock_object.resolvable_container_property = { - 'some key': 'some value', - 'resolver': resolver + "some key": "some value", + "resolver": resolver, } - expected = {'some key': 'some value'} + expected = {"some key": "some value"} assert self.mock_object.resolvable_container_property == expected - def test_get__resolvers_resolves_to_none__value_is_list__deletes_those_items_from_list(self): + def test_get__resolvers_resolves_to_none__value_is_list__deletes_those_items_from_list( + self, + ): class MyResolver(Resolver): def resolve(self): return None @@ -417,56 +355,52 @@ def resolve(self): 3, resolver, resolver, - 6 + 6, ] expected = [1, 3, 6] assert self.mock_object.resolvable_container_property == expected - def test_get__resolvers_resolves_to_none__value_is_list__deletes_all_items_from_list(self): + def test_get__resolvers_resolves_to_none__value_is_list__deletes_all_items_from_list( + self, + ): class MyResolver(Resolver): def resolve(self): return None resolver = MyResolver() - self.mock_object.resolvable_container_property = [ - resolver, - resolver, - resolver - ] + self.mock_object.resolvable_container_property = [resolver, resolver, resolver] expected = [] assert self.mock_object.resolvable_container_property == expected def test_get__value_in_list_is_none__returns_list_with_none(self): - self.mock_object.resolvable_container_property = [ - 1, - None, - 3 - ] + self.mock_object.resolvable_container_property = [1, None, 3] expected = [1, None, 3] assert self.mock_object.resolvable_container_property == expected def test_get__value_in_dict_is_none__returns_dict_with_none(self): self.mock_object.resolvable_container_property = { - 'some key': 'some value', - 'none key': None + "some key": "some value", + "none key": None, } - expected = {'some key': 'some value', 'none key': None} + expected = {"some key": "some value", "none key": None} assert self.mock_object.resolvable_container_property == expected - def test_get__resolver_raises_error__placeholders_allowed__returns_placeholder(self): + def test_get__resolver_raises_error__placeholders_allowed__returns_placeholder( + self, + ): class ErroringResolver(Resolver): def resolve(self): raise ValueError() resolver = ErroringResolver() - self.mock_object.resolvable_container_property = { - 'resolver': resolver - } + self.mock_object.resolvable_container_property = {"resolver": resolver} with use_resolver_placeholders_on_error(): result = self.mock_object.resolvable_container_property - assert result == {'resolver': create_placeholder_value(resolver, PlaceholderType.explicit)} + assert result == { + "resolver": create_placeholder_value(resolver, PlaceholderType.explicit) + } def test_get__resolver_raises_error__placeholders_not_allowed__raises_error(self): class ErroringResolver(Resolver): @@ -474,65 +408,63 @@ def resolve(self): raise ValueError() resolver = ErroringResolver() - self.mock_object.resolvable_container_property = { - 'resolver': resolver - } + self.mock_object.resolvable_container_property = {"resolver": resolver} with pytest.raises(ValueError): self.mock_object.resolvable_container_property - def test_get__resolver_raises_recursive_resolve__placeholders_allowed__raises_error(self): + def test_get__resolver_raises_recursive_resolve__placeholders_allowed__raises_error( + self, + ): class RecursiveResolver(Resolver): def resolve(self): raise RecursiveResolve() resolver = RecursiveResolver() - self.mock_object.resolvable_container_property = { - 'resolver': resolver - } + self.mock_object.resolvable_container_property = {"resolver": resolver} with use_resolver_placeholders_on_error(), pytest.raises(RecursiveResolve): self.mock_object.resolvable_container_property - def test_get__resolver_raises_error__placeholders_allowed__alternate_placeholder_type__uses_alternate(self): + def test_get__resolver_raises_error__placeholders_allowed__alternate_placeholder_type__uses_alternate( + self, + ): class ErroringResolver(Resolver): def resolve(self): raise ValueError() resolver = ErroringResolver() - self.mock_object.container_with_alphanum_placeholder = { - 'resolver': resolver - } + self.mock_object.container_with_alphanum_placeholder = {"resolver": resolver} with use_resolver_placeholders_on_error(): result = self.mock_object.container_with_alphanum_placeholder - assert result == {'resolver': create_placeholder_value(resolver, PlaceholderType.alphanum)} + assert result == { + "resolver": create_placeholder_value(resolver, PlaceholderType.alphanum) + } class TestResolvableValueProperty: def setup_method(self, test_method): self.mock_object = MockClass() - @pytest.mark.parametrize( - 'value', - ['string', True, 123, 1.23, None] - ) + @pytest.mark.parametrize("value", ["string", True, 123, 1.23, None]) def test_set__non_resolver__sets_private_variable_as_value(self, value): self.mock_object.resolvable_value_property = value assert self.mock_object._resolvable_value_property == value - def test_set__resolver__sets_private_variable_with_clone_of_resolver_with_instance(self): + def test_set__resolver__sets_private_variable_with_clone_of_resolver_with_instance( + self, + ): resolver = Mock(spec=MockResolver) self.mock_object.resolvable_value_property = resolver - assert self.mock_object._resolvable_value_property == resolver.clone.return_value + assert ( + self.mock_object._resolvable_value_property == resolver.clone.return_value + ) def test_set__resolver__sets_up_cloned_resolver(self): resolver = Mock(spec=MockResolver) self.mock_object.resolvable_value_property = resolver resolver.clone.return_value.setup.assert_any_call() - @pytest.mark.parametrize( - 'value', - ['string', True, 123, 1.23, None] - ) + @pytest.mark.parametrize("value", ["string", True, 123, 1.23, None]) def test_get__non_resolver__returns_value(self, value): self.mock_object._resolvable_value_property = value assert self.mock_object.resolvable_value_property == value @@ -540,15 +472,21 @@ def test_get__non_resolver__returns_value(self, value): def test_get__resolver__returns_resolved_value(self): resolver = Mock(spec=MockResolver) self.mock_object._resolvable_value_property = resolver - assert self.mock_object.resolvable_value_property == resolver.resolve.return_value + assert ( + self.mock_object.resolvable_value_property == resolver.resolve.return_value + ) def test_get__resolver__updates_set_value_with_resolved_value(self): resolver = Mock(spec=MockResolver) self.mock_object._resolvable_value_property = resolver self.mock_object.resolvable_value_property - assert self.mock_object._resolvable_value_property == resolver.resolve.return_value + assert ( + self.mock_object._resolvable_value_property == resolver.resolve.return_value + ) - def test_get__resolver__resolver_attempts_to_access_resolver__raises_recursive_resolve(self): + def test_get__resolver__resolver_attempts_to_access_resolver__raises_recursive_resolve( + self, + ): class RecursiveResolver(Resolver): def resolve(self): # This should blow up! @@ -560,9 +498,11 @@ def resolve(self): with pytest.raises(RecursiveResolve): self.mock_object.resolvable_value_property - def test_get__resolvable_value_property_references_same_property_of_other_stack__resolves(self): + def test_get__resolvable_value_property_references_same_property_of_other_stack__resolves( + self, + ): stack1 = MockClass() - stack1.resolvable_value_property = 'stack1' + stack1.resolvable_value_property = "stack1" class OtherStackResolver(Resolver): def resolve(self): @@ -571,9 +511,11 @@ def resolve(self): stack2 = MockClass() stack2.resolvable_value_property = OtherStackResolver() - assert stack2.resolvable_value_property == 'stack1' + assert stack2.resolvable_value_property == "stack1" - def test_get__resolver_raises_error__placeholders_allowed__returns_placeholder(self): + def test_get__resolver_raises_error__placeholders_allowed__returns_placeholder( + self, + ): class ErroringResolver(Resolver): def resolve(self): raise ValueError() @@ -595,7 +537,9 @@ def resolve(self): with pytest.raises(ValueError): self.mock_object.resolvable_value_property - def test_get__resolver_raises_recursive_resolve__placeholders_allowed__raises_error(self): + def test_get__resolver_raises_recursive_resolve__placeholders_allowed__raises_error( + self, + ): class RecursiveResolver(Resolver): def resolve(self): raise RecursiveResolve() @@ -605,7 +549,9 @@ def resolve(self): with use_resolver_placeholders_on_error(), pytest.raises(RecursiveResolve): self.mock_object.resolvable_value_property - def test_get__resolver_raises_error__placeholders_allowed__alternate_placeholder_type__uses_alternate_type(self): + def test_get__resolver_raises_error__placeholders_allowed__alternate_placeholder_type__uses_alternate_type( + self, + ): class ErroringResolver(Resolver): def resolve(self): raise ValueError() diff --git a/tests/test_resolvers/test_stack_attr.py b/tests/test_resolvers/test_stack_attr.py index 0cc0b5912..bce39090b 100644 --- a/tests/test_resolvers/test_stack_attr.py +++ b/tests/test_resolvers/test_stack_attr.py @@ -7,7 +7,6 @@ class TestResolver(object): - def setup_method(self, test_method): self.stack_group_config = {} self.stack = Mock(spec=Stack, stack_group_config=self.stack_group_config) @@ -15,54 +14,51 @@ def setup_method(self, test_method): self.resolver = StackAttr(stack=self.stack) def test__resolve__returns_attribute_off_stack(self): - self.resolver.argument = 'testing_this' - self.stack.testing_this = 'hurray!' + self.resolver.argument = "testing_this" + self.stack.testing_this = "hurray!" result = self.resolver.resolve() - assert result == 'hurray!' + assert result == "hurray!" def test_resolve__nested_attribute__accesses_nested_value(self): - self.stack.testing_this = { - 'top': [ - {'thing': 'first'}, - {'thing': 'second'} - ] - } + self.stack.testing_this = {"top": [{"thing": "first"}, {"thing": "second"}]} - self.resolver.argument = 'testing_this.top.1.thing' + self.resolver.argument = "testing_this.top.1.thing" result = self.resolver.resolve() - assert result == 'second' + assert result == "second" def test_resolve__attribute_not_defined__accesses_it_off_stack_group_config(self): - self.stack.stack_group_config['testing_this'] = { - 'top': [ - {'thing': 'first'}, - {'thing': 'second'} - ] + self.stack.stack_group_config["testing_this"] = { + "top": [{"thing": "first"}, {"thing": "second"}] } - self.resolver.argument = 'testing_this.top.1.thing' + self.resolver.argument = "testing_this.top.1.thing" result = self.resolver.resolve() - assert result == 'second' - - @pytest.mark.parametrize('config,attr_name', [ - ('template', 'template_handler_config'), - ('protect', 'protected'), - ('stack_name', 'external_name'), - ('stack_tags', 'tags') - ]) - def test_resolve__accessing_attribute_renamed_on_stack__resolves_correct_value(self, config, attr_name): - setattr(self.stack, attr_name, 'value') + assert result == "second" + + @pytest.mark.parametrize( + "config,attr_name", + [ + ("template", "template_handler_config"), + ("protect", "protected"), + ("stack_name", "external_name"), + ("stack_tags", "tags"), + ], + ) + def test_resolve__accessing_attribute_renamed_on_stack__resolves_correct_value( + self, config, attr_name + ): + setattr(self.stack, attr_name, "value") self.resolver.argument = config result = self.resolver.resolve() - assert result == 'value' + assert result == "value" def test_resolve__attribute_not_defined__raises_attribute_error(self): - self.resolver.argument = 'nonexistant' + self.resolver.argument = "nonexistant" with pytest.raises(AttributeError): self.resolver.resolve() diff --git a/tests/test_resolvers/test_stack_output.py b/tests/test_resolvers/test_stack_output.py index 5b7c5cd37..6242fb80c 100644 --- a/tests/test_resolvers/test_stack_output.py +++ b/tests/test_resolvers/test_stack_output.py @@ -8,16 +8,16 @@ from botocore.exceptions import ClientError from sceptre.connection_manager import ConnectionManager -from sceptre.resolvers.stack_output import \ - StackOutput, StackOutputExternal, StackOutputBase +from sceptre.resolvers.stack_output import ( + StackOutput, + StackOutputExternal, + StackOutputBase, +) from sceptre.stack import Stack class TestStackOutputResolver(object): - - @patch( - "sceptre.resolvers.stack_output.StackOutput._get_output_value" - ) + @patch("sceptre.resolvers.stack_output.StackOutput._get_output_value") def test_resolver(self, mock_get_output_value): stack = MagicMock(spec=Stack) stack.dependencies = [] @@ -33,9 +33,7 @@ def test_resolver(self, mock_get_output_value): mock_get_output_value.return_value = "output_value" - stack_output_resolver = StackOutput( - "account/dev/vpc.yaml::VpcId", stack - ) + stack_output_resolver = StackOutput("account/dev/vpc.yaml::VpcId", stack) stack_output_resolver.setup() assert stack.dependencies == ["account/dev/vpc.yaml"] @@ -44,14 +42,14 @@ def test_resolver(self, mock_get_output_value): result = stack_output_resolver.resolve() assert result == "output_value" mock_get_output_value.assert_called_once_with( - "meh-account-dev-vpc", "VpcId", - profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role" + "meh-account-dev-vpc", + "VpcId", + profile="dependency_profile", + region="dependency_region", + iam_role="dependency_iam_role", ) - @patch( - "sceptre.resolvers.stack_output.StackOutput._get_output_value" - ) + @patch("sceptre.resolvers.stack_output.StackOutput._get_output_value") def test_resolver_with_existing_dependencies(self, mock_get_output_value): stack = MagicMock(spec=Stack) stack.dependencies = ["existing"] @@ -67,9 +65,7 @@ def test_resolver_with_existing_dependencies(self, mock_get_output_value): mock_get_output_value.return_value = "output_value" - stack_output_resolver = StackOutput( - "account/dev/vpc.yaml::VpcId", stack - ) + stack_output_resolver = StackOutput("account/dev/vpc.yaml::VpcId", stack) stack_output_resolver.setup() assert stack.dependencies == ["existing", "account/dev/vpc.yaml"] @@ -78,17 +74,15 @@ def test_resolver_with_existing_dependencies(self, mock_get_output_value): result = stack_output_resolver.resolve() assert result == "output_value" mock_get_output_value.assert_called_once_with( - "meh-account-dev-vpc", "VpcId", - profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role" + "meh-account-dev-vpc", + "VpcId", + profile="dependency_profile", + region="dependency_region", + iam_role="dependency_iam_role", ) - @patch( - "sceptre.resolvers.stack_output.StackOutput._get_output_value" - ) - def test_resolve_with_implicit_stack_reference( - self, mock_get_output_value - ): + @patch("sceptre.resolvers.stack_output.StackOutput._get_output_value") + def test_resolve_with_implicit_stack_reference(self, mock_get_output_value): stack = MagicMock(spec=Stack) stack.dependencies = [] stack.project_code = "project-code" @@ -113,14 +107,14 @@ def test_resolve_with_implicit_stack_reference( result = stack_output_resolver.resolve() assert result == "output_value" mock_get_output_value.assert_called_once_with( - "meh-account-dev-vpc", "VpcId", - profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role" + "meh-account-dev-vpc", + "VpcId", + profile="dependency_profile", + region="dependency_region", + iam_role="dependency_iam_role", ) - @patch( - "sceptre.resolvers.stack_output.StackOutput._get_output_value" - ) + @patch("sceptre.resolvers.stack_output.StackOutput._get_output_value") def test_resolve_with_implicit_stack_reference_top_level( self, mock_get_output_value ): @@ -148,17 +142,16 @@ def test_resolve_with_implicit_stack_reference_top_level( result = stack_output_resolver.resolve() assert result == "output_value" mock_get_output_value.assert_called_once_with( - "meh-vpc", "VpcId", - profile="dependency_profile", region="dependency_region", - iam_role="dependency_iam_role" + "meh-vpc", + "VpcId", + profile="dependency_profile", + region="dependency_region", + iam_role="dependency_iam_role", ) class TestStackOutputExternalResolver(object): - - @patch( - "sceptre.resolvers.stack_output.StackOutputExternal._get_output_value" - ) + @patch("sceptre.resolvers.stack_output.StackOutputExternal._get_output_value") def test_resolve(self, mock_get_output_value): stack = MagicMock(spec=Stack) stack.dependencies = [] @@ -173,9 +166,7 @@ def test_resolve(self, mock_get_output_value): ) assert stack.dependencies == [] - @patch( - "sceptre.resolvers.stack_output.StackOutputExternal._get_output_value" - ) + @patch("sceptre.resolvers.stack_output.StackOutputExternal._get_output_value") def test_resolve_with_args(self, mock_get_output_value): stack = MagicMock(spec=Stack) stack.dependencies = [] @@ -207,19 +198,12 @@ def resolve(self): class TestStackOutputBaseResolver(object): - def setup_method(self, test_method): self.stack = MagicMock(spec=Stack) - self.stack._connection_manager = MagicMock( - spec=ConnectionManager - ) - self.base_stack_output_resolver = MockStackOutputBase( - None, self.stack - ) + self.stack._connection_manager = MagicMock(spec=ConnectionManager) + self.base_stack_output_resolver = MockStackOutputBase(None, self.stack) - @patch( - "sceptre.resolvers.stack_output.StackOutputBase._get_stack_outputs" - ) + @patch("sceptre.resolvers.stack_output.StackOutputBase._get_stack_outputs") def test_get_output_value_with_valid_key(self, mock_get_stack_outputs): mock_get_stack_outputs.return_value = {"key": "value"} @@ -229,9 +213,7 @@ def test_get_output_value_with_valid_key(self, mock_get_stack_outputs): assert response == "value" - @patch( - "sceptre.resolvers.stack_output.StackOutputBase._get_stack_outputs" - ) + @patch("sceptre.resolvers.stack_output.StackOutputBase._get_stack_outputs") def test_get_output_value_with_invalid_key(self, mock_get_stack_outputs): mock_get_stack_outputs.return_value = {"key": "value"} @@ -242,35 +224,32 @@ def test_get_output_value_with_invalid_key(self, mock_get_stack_outputs): def test_get_stack_outputs_with_valid_stack(self): self.stack.connection_manager.call.return_value = { - "Stacks": [{ - "Outputs": [ - { - "OutputKey": "key_1", - "OutputValue": "value_1", - "Description": "description_1" - }, - { - "OutputKey": "key_2", - "OutputValue": "value_2", - "Description": "description_2" - } - ] - }] + "Stacks": [ + { + "Outputs": [ + { + "OutputKey": "key_1", + "OutputValue": "value_1", + "Description": "description_1", + }, + { + "OutputKey": "key_2", + "OutputValue": "value_2", + "Description": "description_2", + }, + ] + } + ] } response = self.base_stack_output_resolver._get_stack_outputs( sentinel.stack_name ) - assert response == { - "key_1": "value_1", - "key_2": "value_2" - } + assert response == {"key_1": "value_1", "key_2": "value_2"} def test_get_stack_outputs_with_valid_stack_without_outputs(self): - self.stack.connection_manager.call.return_value = { - "Stacks": [{}] - } + self.stack.connection_manager.call.return_value = {"Stacks": [{}]} response = self.base_stack_output_resolver._get_stack_outputs( sentinel.stack_name @@ -279,32 +258,17 @@ def test_get_stack_outputs_with_valid_stack_without_outputs(self): def test_get_stack_outputs_with_unlaunched_stack(self): self.stack.connection_manager.call.side_effect = ClientError( - { - "Error": { - "Code": "404", - "Message": "stack does not exist" - } - }, - sentinel.operation + {"Error": {"Code": "404", "Message": "stack does not exist"}}, + sentinel.operation, ) with pytest.raises(StackDoesNotExistError): - self.base_stack_output_resolver._get_stack_outputs( - sentinel.stack_name - ) + self.base_stack_output_resolver._get_stack_outputs(sentinel.stack_name) def test_get_stack_outputs_with_unkown_boto_error(self): self.stack.connection_manager.call.side_effect = ClientError( - { - "Error": { - "Code": "500", - "Message": "Boom!" - } - }, - sentinel.operation + {"Error": {"Code": "500", "Message": "Boom!"}}, sentinel.operation ) with pytest.raises(ClientError): - self.base_stack_output_resolver._get_stack_outputs( - sentinel.stack_name - ) + self.base_stack_output_resolver._get_stack_outputs(sentinel.stack_name) diff --git a/tests/test_stack.py b/tests/test_stack.py index d8a7d8ba9..ea38177d9 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -12,65 +12,73 @@ def stack_factory(**kwargs): call_kwargs = { - 'name': 'dev/app/stack', - 'project_code': sentinel.project_code, - 'template_bucket_name': sentinel.template_bucket_name, - 'template_key_prefix': sentinel.template_key_prefix, - 'required_version': sentinel.required_version, - 'template_path': sentinel.template_path, - 'region': sentinel.region, - 'profile': sentinel.profile, - 'parameters': {"key1": "val1"}, - 'sceptre_user_data': sentinel.sceptre_user_data, - 'hooks': {}, - 's3_details': None, - 'dependencies': sentinel.dependencies, - 'role_arn': sentinel.role_arn, - 'protected': False, - 'tags': {"tag1": "val1"}, - 'external_name': sentinel.external_name, - 'notifications': [sentinel.notification], - 'on_failure': sentinel.on_failure, - 'stack_timeout': sentinel.stack_timeout, - 'stack_group_config': {} + "name": "dev/app/stack", + "project_code": sentinel.project_code, + "template_bucket_name": sentinel.template_bucket_name, + "template_key_prefix": sentinel.template_key_prefix, + "required_version": sentinel.required_version, + "template_path": sentinel.template_path, + "region": sentinel.region, + "profile": sentinel.profile, + "parameters": {"key1": "val1"}, + "sceptre_user_data": sentinel.sceptre_user_data, + "hooks": {}, + "s3_details": None, + "dependencies": sentinel.dependencies, + "role_arn": sentinel.role_arn, + "protected": False, + "tags": {"tag1": "val1"}, + "external_name": sentinel.external_name, + "notifications": [sentinel.notification], + "on_failure": sentinel.on_failure, + "stack_timeout": sentinel.stack_timeout, + "stack_group_config": {}, } call_kwargs.update(kwargs) return Stack(**call_kwargs) class TestStack(object): - def setup_method(self, test_method): self.stack = Stack( - name='dev/app/stack', project_code=sentinel.project_code, + name="dev/app/stack", + project_code=sentinel.project_code, template_bucket_name=sentinel.template_bucket_name, template_key_prefix=sentinel.template_key_prefix, required_version=sentinel.required_version, template_path=sentinel.template_path, region=sentinel.region, - profile=sentinel.profile, parameters={"key1": "val1"}, - sceptre_user_data=sentinel.sceptre_user_data, hooks={}, - s3_details=None, dependencies=sentinel.dependencies, - role_arn=sentinel.role_arn, protected=False, - tags={"tag1": "val1"}, external_name=sentinel.external_name, + profile=sentinel.profile, + parameters={"key1": "val1"}, + sceptre_user_data=sentinel.sceptre_user_data, + hooks={}, + s3_details=None, + dependencies=sentinel.dependencies, + role_arn=sentinel.role_arn, + protected=False, + tags={"tag1": "val1"}, + external_name=sentinel.external_name, notifications=[sentinel.notification], - on_failure=sentinel.on_failure, iam_role=sentinel.iam_role, + on_failure=sentinel.on_failure, + iam_role=sentinel.iam_role, iam_role_session_duration=sentinel.iam_role_session_duration, stack_timeout=sentinel.stack_timeout, - stack_group_config={} + stack_group_config={}, ) self.stack._template = MagicMock(spec=Template) def test_initialize_stack_with_template_path(self): stack = Stack( - name='dev/stack/app', project_code=sentinel.project_code, + name="dev/stack/app", + project_code=sentinel.project_code, template_path=sentinel.template_path, template_bucket_name=sentinel.template_bucket_name, template_key_prefix=sentinel.template_key_prefix, required_version=sentinel.required_version, - region=sentinel.region, external_name=sentinel.external_name + region=sentinel.region, + external_name=sentinel.external_name, ) - assert stack.name == 'dev/stack/app' + assert stack.name == "dev/stack/app" assert stack.project_code == sentinel.project_code assert stack.template_bucket_name == sentinel.template_bucket_name assert stack.template_key_prefix == sentinel.template_key_prefix @@ -94,14 +102,16 @@ def test_initialize_stack_with_template_path(self): def test_initialize_stack_with_template_handler(self): stack = Stack( - name='dev/stack/app', project_code=sentinel.project_code, + name="dev/stack/app", + project_code=sentinel.project_code, template_handler_config=sentinel.template_handler_config, template_bucket_name=sentinel.template_bucket_name, template_key_prefix=sentinel.template_key_prefix, required_version=sentinel.required_version, - region=sentinel.region, external_name=sentinel.external_name + region=sentinel.region, + external_name=sentinel.external_name, ) - assert stack.name == 'dev/stack/app' + assert stack.name == "dev/stack/app" assert stack.project_code == sentinel.project_code assert stack.template_bucket_name == sentinel.template_bucket_name assert stack.template_key_prefix == sentinel.template_key_prefix @@ -126,67 +136,76 @@ def test_initialize_stack_with_template_handler(self): def test_raises_exception_if_path_and_handler_configured(self): with pytest.raises(InvalidConfigFileError): Stack( - name="stack_name", project_code="project_code", - template_path="template_path", template_handler_config={"type": "file"}, - region="region" + name="stack_name", + project_code="project_code", + template_path="template_path", + template_handler_config={"type": "file"}, + region="region", ) def test_init__non_boolean_ignore_value__raises_invalid_config_file_error(self): with pytest.raises(InvalidConfigFileError): Stack( - name='dev/stack/app', project_code=sentinel.project_code, + name="dev/stack/app", + project_code=sentinel.project_code, template_handler_config=sentinel.template_handler_config, template_bucket_name=sentinel.template_bucket_name, template_key_prefix=sentinel.template_key_prefix, required_version=sentinel.required_version, - region=sentinel.region, external_name=sentinel.external_name, - ignore="true" + region=sentinel.region, + external_name=sentinel.external_name, + ignore="true", ) def test_init__non_boolean_obsolete_value__raises_invalid_config_file_error(self): with pytest.raises(InvalidConfigFileError): Stack( - name='dev/stack/app', project_code=sentinel.project_code, + name="dev/stack/app", + project_code=sentinel.project_code, template_handler_config=sentinel.template_handler_config, template_bucket_name=sentinel.template_bucket_name, template_key_prefix=sentinel.template_key_prefix, required_version=sentinel.required_version, - region=sentinel.region, external_name=sentinel.external_name, - obsolete="true" + region=sentinel.region, + external_name=sentinel.external_name, + obsolete="true", ) def test_stack_repr(self): - assert self.stack.__repr__() == \ - "sceptre.stack.Stack(" \ - "name='dev/app/stack', " \ - "project_code=sentinel.project_code, " \ - "template_path=sentinel.template_path, " \ - "template_handler_config=None, " \ - "region=sentinel.region, " \ - "template_bucket_name=sentinel.template_bucket_name, "\ - "template_key_prefix=sentinel.template_key_prefix, "\ - "required_version=sentinel.required_version, "\ - "iam_role=sentinel.iam_role, "\ - "iam_role_session_duration=sentinel.iam_role_session_duration, "\ - "profile=sentinel.profile, " \ - "sceptre_user_data=sentinel.sceptre_user_data, " \ - "parameters={'key1': 'val1'}, "\ - "hooks={}, "\ - "s3_details=None, " \ - "dependencies=sentinel.dependencies, "\ - "role_arn=sentinel.role_arn, "\ - "protected=False, "\ - "tags={'tag1': 'val1'}, "\ - "external_name=sentinel.external_name, " \ - "notifications=[sentinel.notification], " \ - "on_failure=sentinel.on_failure, " \ - "stack_timeout=sentinel.stack_timeout, " \ - "stack_group_config={}, " \ - "ignore=False, " \ - "obsolete=False" \ + assert ( + self.stack.__repr__() == "sceptre.stack.Stack(" + "name='dev/app/stack', " + "project_code=sentinel.project_code, " + "template_path=sentinel.template_path, " + "template_handler_config=None, " + "region=sentinel.region, " + "template_bucket_name=sentinel.template_bucket_name, " + "template_key_prefix=sentinel.template_key_prefix, " + "required_version=sentinel.required_version, " + "iam_role=sentinel.iam_role, " + "iam_role_session_duration=sentinel.iam_role_session_duration, " + "profile=sentinel.profile, " + "sceptre_user_data=sentinel.sceptre_user_data, " + "parameters={'key1': 'val1'}, " + "hooks={}, " + "s3_details=None, " + "dependencies=sentinel.dependencies, " + "role_arn=sentinel.role_arn, " + "protected=False, " + "tags={'tag1': 'val1'}, " + "external_name=sentinel.external_name, " + "notifications=[sentinel.notification], " + "on_failure=sentinel.on_failure, " + "stack_timeout=sentinel.stack_timeout, " + "stack_group_config={}, " + "ignore=False, " + "obsolete=False" ")" + ) - def test_configuration_manager__iam_role_raises_recursive_resolve__returns_connection_manager_with_no_role(self): + def test_configuration_manager__iam_role_raises_recursive_resolve__returns_connection_manager_with_no_role( + self, + ): class FakeResolver(Resolver): def resolve(self): return self.stack.iam_role @@ -196,8 +215,9 @@ def resolve(self): connection_manager = self.stack.connection_manager assert connection_manager.iam_role is None - def test_configuration_manager__iam_role_returns_value_second_access__returns_value_on_second_access(self): - + def test_configuration_manager__iam_role_returns_value_second_access__returns_value_on_second_access( + self, + ): class FakeResolver(Resolver): access_count = 0 @@ -206,22 +226,24 @@ def resolve(self): self.access_count += 1 return self.stack.iam_role else: - return 'role' + return "role" self.stack.iam_role = FakeResolver() assert self.stack.connection_manager.iam_role is None - assert self.stack.connection_manager.iam_role == 'role' + assert self.stack.connection_manager.iam_role == "role" - def test_configuration_manager__iam_role_returns_value__returns_connection_manager_with_that_role(self): + def test_configuration_manager__iam_role_returns_value__returns_connection_manager_with_that_role( + self, + ): class FakeResolver(Resolver): def resolve(self): - return 'role' + return "role" self.stack.iam_role = FakeResolver() connection_manager = self.stack.connection_manager - assert connection_manager.iam_role == 'role' + assert connection_manager.iam_role == "role" class TestStackSceptreUserData(object): @@ -230,8 +252,8 @@ def test_user_data_is_accessible(self): .sceptre_user_data is a property. Let's make sure it accesses the right data. """ - stack = stack_factory(sceptre_user_data={'test_key': sentinel.test_value}) - assert stack.sceptre_user_data['test_key'] is sentinel.test_value + stack = stack_factory(sceptre_user_data={"test_key": sentinel.test_value}) + assert stack.sceptre_user_data["test_key"] is sentinel.test_value def test_user_data_gets_resolved(self): class TestResolver(Resolver): @@ -241,24 +263,25 @@ def setup(self): def resolve(self): return sentinel.resolved_value - stack = stack_factory(sceptre_user_data={'test_key': TestResolver()}) - assert stack.sceptre_user_data['test_key'] is sentinel.resolved_value + stack = stack_factory(sceptre_user_data={"test_key": TestResolver()}) + assert stack.sceptre_user_data["test_key"] is sentinel.resolved_value def test_recursive_user_data_gets_resolved(self): """ .sceptre_user_data can have resolvers that refer to .sceptre_user_data itself. Those must be instantiated before the attribute can be used. """ + class TestResolver(Resolver): def setup(self): pass def resolve(self): - return self.stack.sceptre_user_data['primitive'] + return self.stack.sceptre_user_data["primitive"] stack = stack_factory() stack.sceptre_user_data = { - 'primitive': sentinel.primitive_value, - 'resolved': TestResolver(stack=stack), + "primitive": sentinel.primitive_value, + "resolved": TestResolver(stack=stack), } - assert stack.sceptre_user_data['resolved'] == sentinel.primitive_value + assert stack.sceptre_user_data["resolved"] == sentinel.primitive_value diff --git a/tests/test_stack_status_colourer.py b/tests/test_stack_status_colourer.py index 3842f6b7a..802c1ee34 100644 --- a/tests/test_stack_status_colourer.py +++ b/tests/test_stack_status_colourer.py @@ -5,7 +5,6 @@ class TestStackStatusColourer(object): - def setup_method(self, test_method): init() self.stack_status_colourer = StackStatusColourer() @@ -26,7 +25,7 @@ def setup_method(self, test_method): "UPDATE_ROLLBACK_COMPLETE": Fore.GREEN, "UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS": Fore.YELLOW, "UPDATE_ROLLBACK_FAILED": Fore.RED, - "UPDATE_ROLLBACK_IN_PROGRESS": Fore.YELLOW + "UPDATE_ROLLBACK_IN_PROGRESS": Fore.YELLOW, } def test_colour_with_string_with_no_stack_statuses(self): @@ -39,16 +38,11 @@ def test_colour_with_string_with_single_stack_status(self): for status in sorted(self.statuses.keys()) ] - responses = [ - self.stack_status_colourer.colour(string) - for string in strings - ] + responses = [self.stack_status_colourer.colour(string) for string in strings] assert responses == [ "string string {0}{1}{2} string".format( - self.statuses[status], - status, - Style.RESET_ALL + self.statuses[status], status, Style.RESET_ALL ) for status in sorted(self.statuses.keys()) ] @@ -57,11 +51,9 @@ def test_colour_with_string_with_multiple_stack_statuses(self): response = self.stack_status_colourer.colour( " ".join(sorted(self.statuses.keys())) ) - assert response == " ".join([ - "{0}{1}{2}".format( - self.statuses[status], - status, - Style.RESET_ALL - ) - for status in sorted(self.statuses.keys()) - ]) + assert response == " ".join( + [ + "{0}{1}{2}".format(self.statuses[status], status, Style.RESET_ALL) + for status in sorted(self.statuses.keys()) + ] + ) diff --git a/tests/test_template.py b/tests/test_template.py index 6b3bec8cd..fdfdb8480 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -30,7 +30,6 @@ def handle(self): class TestTemplate(object): - def setup_method(self, test_method): self.region = "region" self.bucket_name = "bucket_name" @@ -44,9 +43,7 @@ def setup_method(self, test_method): name="template_name", handler_config={"type": "file", "path": "/folder/template.py"}, sceptre_user_data={}, - stack_group_config={ - "project_path": "projects" - }, + stack_group_config={"project_path": "projects"}, connection_manager=connection_manager, ) @@ -59,19 +56,27 @@ def test_initialise_template_default_handler_type(self): connection_manager={}, ) - assert template.handler_config == {"type": "file", "path": "/folder/template.py"} + assert template.handler_config == { + "type": "file", + "path": "/folder/template.py", + } def test_initialise_template(self): - assert self.template.handler_config == {"type": "file", "path": "/folder/template.py"} + assert self.template.handler_config == { + "type": "file", + "path": "/folder/template.py", + } assert self.template.name == "template_name" assert self.template.sceptre_user_data == {} assert self.template._body is None def test_repr(self): representation = self.template.__repr__() - assert representation == "sceptre.template.Template(" \ - "name='template_name', handler_config={'type': 'file', 'path': '/folder/template.py'}" \ + assert ( + representation == "sceptre.template.Template(" + "name='template_name', handler_config={'type': 'file', 'path': '/folder/template.py'}" ", sceptre_user_data={}, s3_details=None)" + ) def test_body_with_cache(self): self.template._body = sentinel.body @@ -85,18 +90,19 @@ def test_upload_to_s3_with_valid_s3_details(self, mock_bucket_exists): mock_bucket_exists.return_value = True self.template.s3_details = { "bucket_name": "bucket-name", - "bucket_key": "bucket-key" + "bucket_key": "bucket-key", } self.template.upload_to_s3() - get_bucket_location_call, put_object_call = self.template.connection_manager.call.call_args_list + ( + get_bucket_location_call, + put_object_call, + ) = self.template.connection_manager.call.call_args_list get_bucket_location_call.assert_called_once_with( service="s3", command="get_bucket_location", - kwargs={ - "Bucket": "bucket-name" - } + kwargs={"Bucket": "bucket-name"}, ) put_object_call.assert_called_once_with( service="s3", @@ -105,8 +111,8 @@ def test_upload_to_s3_with_valid_s3_details(self, mock_bucket_exists): "Bucket": "bucket-name", "Key": "bucket-key", "Body": '{"template": "mock"}', - "ServerSideEncryption": "AES256" - } + "ServerSideEncryption": "AES256", + }, ) def test_domain_from_region(self): @@ -119,7 +125,7 @@ def test_bucket_exists_with_bucket_that_exists(self): # behaviour when head_bucket successfully executes. self.template.s3_details = { "bucket_name": "bucket-name", - "bucket_key": "bucket-key" + "bucket_key": "bucket-key", } assert self.template._bucket_exists() is True @@ -128,17 +134,11 @@ def test_create_bucket_with_unreadable_bucket(self): self.template.connection_manager.region = "eu-west-1" self.template.s3_details = { "bucket_name": "bucket-name", - "bucket_key": "bucket-key" + "bucket_key": "bucket-key", } self.template.connection_manager.call.side_effect = ClientError( - { - "Error": { - "Code": 500, - "Message": "Bucket Unreadable" - } - }, - sentinel.operation + {"Error": {"Code": 500, "Message": "Bucket Unreadable"}}, sentinel.operation ) with pytest.raises(ClientError) as e: self.template._create_bucket() @@ -150,20 +150,14 @@ def test_bucket_exists_with_non_existent_bucket(self): # Not Found ClientError only for the first call. self.template.s3_details = { "bucket_name": "bucket-name", - "bucket_key": "bucket-key" + "bucket_key": "bucket-key", } self.template.connection_manager.call.side_effect = [ ClientError( - { - "Error": { - "Code": 404, - "Message": "Not Found" - } - }, - sentinel.operation + {"Error": {"Code": 404, "Message": "Not Found"}}, sentinel.operation ), - None + None, ] existance = self.template._bucket_exists() @@ -176,15 +170,13 @@ def test_create_bucket_in_us_east_1(self): self.template.connection_manager.region = "us-east-1" self.template.s3_details = { "bucket_name": "bucket-name", - "bucket_key": "bucket-key" + "bucket_key": "bucket-key", } self.template._create_bucket() self.template.connection_manager.call.assert_any_call( - service="s3", - command="create_bucket", - kwargs={"Bucket": "bucket-name"} + service="s3", command="create_bucket", kwargs={"Bucket": "bucket-name"} ) @patch("sceptre.template.Template.upload_to_s3") @@ -193,18 +185,20 @@ def test_get_boto_call_parameter_with_s3_details(self, mock_upload_to_s3): mock_upload_to_s3.return_value = sentinel.template_url self.template.s3_details = { "bucket_name": sentinel.bucket_name, - "bucket_key": sentinel.bucket_key + "bucket_key": sentinel.bucket_key, } boto_parameter = self.template.get_boto_call_parameter() assert boto_parameter == {"TemplateURL": sentinel.template_url} - def test_get_boto_call_parameter__has_s3_details_but_bucket_name_is_none__gets_template_body_dict(self): + def test_get_boto_call_parameter__has_s3_details_but_bucket_name_is_none__gets_template_body_dict( + self, + ): self.template._body = sentinel.body self.template.s3_details = { "bucket_name": None, - "bucket_key": sentinel.bucket_key + "bucket_key": sentinel.bucket_key, } boto_parameter = self.template.get_boto_call_parameter() @@ -221,8 +215,7 @@ def test_get_template_details_without_upload(self): def test_body_with_json_template(self): self.template.name = "vpc" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures-vpc/templates/vpc.json" + os.getcwd(), "tests/fixtures-vpc/templates/vpc.json" ) output = self.template.body output_dict = yaml.safe_load(output) @@ -233,8 +226,7 @@ def test_body_with_json_template(self): def test_body_with_yaml_template(self): self.template.name = "vpc" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc.yaml" + os.getcwd(), "tests/fixtures/templates/vpc.yaml" ) output = self.template.body output_dict = yaml.safe_load(output) @@ -245,8 +237,7 @@ def test_body_with_yaml_template(self): def test_body_with_generic_template(self): self.template.name = "vpc" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc.template" + os.getcwd(), "tests/fixtures/templates/vpc.template" ) output = self.template.body output_dict = yaml.safe_load(output) @@ -259,8 +250,7 @@ def test_body_with_chdir_template(self): self.template.name = "chdir" current_dir = os.getcwd() self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/chdir.py" + os.getcwd(), "tests/fixtures/templates/chdir.py" ) try: yaml.safe_load(self.template.body) @@ -278,8 +268,7 @@ def test_body_with_python_template(self): self.template.sceptre_user_data = None self.template.name = "vpc" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc.py" + os.getcwd(), "tests/fixtures/templates/vpc.py" ) actual_output = yaml.safe_load(self.template.body) with open("tests/fixtures/templates/compiled_vpc.json", "r") as f: @@ -290,8 +279,7 @@ def test_body_with_python_template_with_sgt(self): self.template.sceptre_user_data = None self.template.name = "vpc_sgt" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc_sgt.py" + os.getcwd(), "tests/fixtures/templates/vpc_sgt.py" ) actual_output = yaml.safe_load(self.template.body) with open("tests/fixtures/templates/compiled_vpc.json", "r") as f: @@ -301,8 +289,7 @@ def test_body_with_python_template_with_sgt(self): def test_body_injects_yaml_start_marker(self): self.template.name = "vpc" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc.without_start_marker.yaml" + os.getcwd(), "tests/fixtures/templates/vpc.without_start_marker.yaml" ) output = self.template.body with open("tests/fixtures/templates/vpc.yaml", "r") as f: @@ -312,8 +299,7 @@ def test_body_injects_yaml_start_marker(self): def test_body_with_existing_yaml_start_marker(self): self.template.name = "vpc" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc.yaml" + os.getcwd(), "tests/fixtures/templates/vpc.yaml" ) output = self.template.body with open("tests/fixtures/templates/vpc.yaml", "r") as f: @@ -323,25 +309,19 @@ def test_body_with_existing_yaml_start_marker(self): def test_body_with_existing_yaml_start_marker_j2(self): self.template.name = "vpc" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc.yaml.j2" + os.getcwd(), "tests/fixtures/templates/vpc.yaml.j2" ) - self.template.sceptre_user_data = { - "vpc_id": "10.0.0.0/16" - } + self.template.sceptre_user_data = {"vpc_id": "10.0.0.0/16"} output = self.template.body with open("tests/fixtures/templates/compiled_vpc.yaml", "r") as f: expected_output = f.read() assert output == expected_output.rstrip() def test_body_injects_sceptre_user_data(self): - self.template.sceptre_user_data = { - "cidr_block": "10.0.0.0/16" - } + self.template.sceptre_user_data = {"cidr_block": "10.0.0.0/16"} self.template.name = "vpc_sud" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc_sud.py" + os.getcwd(), "tests/fixtures/templates/vpc_sud.py" ) actual_output = yaml.safe_load(self.template.body) @@ -350,45 +330,35 @@ def test_body_injects_sceptre_user_data(self): assert actual_output == expected_output def test_body_injects_sceptre_user_data_incorrect_function(self): - self.template.sceptre_user_data = { - "cidr_block": "10.0.0.0/16" - } + self.template.sceptre_user_data = {"cidr_block": "10.0.0.0/16"} self.template.name = "vpc_sud_incorrect_function" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc_sud_incorrect_function.py" + os.getcwd(), "tests/fixtures/templates/vpc_sud_incorrect_function.py" ) with pytest.raises(TemplateSceptreHandlerError): self.template.body def test_body_injects_sceptre_user_data_incorrect_handler(self): - self.template.sceptre_user_data = { - "cidr_block": "10.0.0.0/16" - } + self.template.sceptre_user_data = {"cidr_block": "10.0.0.0/16"} self.template.name = "vpc_sud_incorrect_handler" self.template.handler_config["path"] = os.path.join( - os.getcwd(), - "tests/fixtures/templates/vpc_sud_incorrect_handler.py" + os.getcwd(), "tests/fixtures/templates/vpc_sud_incorrect_handler.py" ) with pytest.raises(TypeError): self.template.body def test_body_with_incorrect_filetype(self): - self.template.handler_config["path"] = ( - "path/to/something.ext" - ) + self.template.handler_config["path"] = "path/to/something.ext" with pytest.raises(UnsupportedTemplateFileTypeError): self.template.body def test_template_handler_is_called(self): self.template.handler_config = { "type": "test", - "argument": sentinel.template_handler_argument + "argument": sentinel.template_handler_argument, } - self.template._registry = { - "test": MockTemplateHandler - } + self.template._registry = {"test": MockTemplateHandler} result = self.template.body assert result == "---\n" + str(sentinel.template_handler_argument) diff --git a/tests/test_template_handlers/test_file.py b/tests/test_template_handlers/test_file.py index 1e6844e52..04b63c4c3 100644 --- a/tests/test_template_handlers/test_file.py +++ b/tests/test_template_handlers/test_file.py @@ -7,51 +7,107 @@ class TestFile(object): - - @pytest.mark.parametrize("project_path,path,output_path", [ - ("my_project_dir", "my.template.yaml", "my_project_dir/templates/my.template.yaml"), # NOQA - ("/src/my_project_dir", "my.template.yaml", "/src/my_project_dir/templates/my.template.yaml"), # NOQA - ("my_project_dir", "/src/my_project_dir/templates/my.template.yaml", "/src/my_project_dir/templates/my.template.yaml"), # NOQA - ("/src/my_project_dir", "/src/my_project_dir/templates/my.template.yaml", "/src/my_project_dir/templates/my.template.yaml"), # NOQA - ]) + @pytest.mark.parametrize( + "project_path,path,output_path", + [ + ( + "my_project_dir", + "my.template.yaml", + "my_project_dir/templates/my.template.yaml", + ), # NOQA + ( + "/src/my_project_dir", + "my.template.yaml", + "/src/my_project_dir/templates/my.template.yaml", + ), # NOQA + ( + "my_project_dir", + "/src/my_project_dir/templates/my.template.yaml", + "/src/my_project_dir/templates/my.template.yaml", + ), # NOQA + ( + "/src/my_project_dir", + "/src/my_project_dir/templates/my.template.yaml", + "/src/my_project_dir/templates/my.template.yaml", + ), # NOQA + ], + ) @patch("builtins.open", new_callable=mock_open, read_data="some_data") def test_handler_open(self, mocked_open, project_path, path, output_path): template_handler = File( name="file_handler", arguments={"path": path}, - stack_group_config={"project_path": project_path} + stack_group_config={"project_path": project_path}, ) template_handler.handle() mocked_open.assert_called_with(output_path) - @pytest.mark.parametrize("project_path,path,output_path", [ - ("my_project_dir", "my.template.yaml.j2", "my_project_dir/templates/my.template.yaml.j2"), # NOQA - ("/src/my_project_dir", "my.template.yaml.j2", "/src/my_project_dir/templates/my.template.yaml.j2"), # NOQA - ("my_project_dir", "/src/my_project_dir/templates/my.template.yaml.j2", "/src/my_project_dir/templates/my.template.yaml.j2"), # NOQA - ("/src/my_project_dir", "/src/my_project_dir/templates/my.template.yaml.j2", "/src/my_project_dir/templates/my.template.yaml.j2"), # NOQA - ]) + @pytest.mark.parametrize( + "project_path,path,output_path", + [ + ( + "my_project_dir", + "my.template.yaml.j2", + "my_project_dir/templates/my.template.yaml.j2", + ), # NOQA + ( + "/src/my_project_dir", + "my.template.yaml.j2", + "/src/my_project_dir/templates/my.template.yaml.j2", + ), # NOQA + ( + "my_project_dir", + "/src/my_project_dir/templates/my.template.yaml.j2", + "/src/my_project_dir/templates/my.template.yaml.j2", + ), # NOQA + ( + "/src/my_project_dir", + "/src/my_project_dir/templates/my.template.yaml.j2", + "/src/my_project_dir/templates/my.template.yaml.j2", + ), # NOQA + ], + ) @patch("sceptre.template_handlers.helper.render_jinja_template") def test_handler_render(self, mocked_render, project_path, path, output_path): template_handler = File( name="file_handler", arguments={"path": path}, - stack_group_config={"project_path": project_path} + stack_group_config={"project_path": project_path}, ) template_handler.handle() mocked_render.assert_called_with(output_path, {"sceptre_user_data": None}, {}) - @pytest.mark.parametrize("project_path,path,output_path", [ - ("my_project_dir", "my.template.yaml.py", "my_project_dir/templates/my.template.yaml.py"), # NOQA - ("/src/my_project_dir", "my.template.yaml.py", "/src/my_project_dir/templates/my.template.yaml.py"), # NOQA - ("my_project_dir", "/src/my_project_dir/templates/my.template.yaml.py", "/src/my_project_dir/templates/my.template.yaml.py"), # NOQA - ("/src/my_project_dir", "/src/my_project_dir/templates/my.template.yaml.py", "/src/my_project_dir/templates/my.template.yaml.py"), # NOQA - ]) + @pytest.mark.parametrize( + "project_path,path,output_path", + [ + ( + "my_project_dir", + "my.template.yaml.py", + "my_project_dir/templates/my.template.yaml.py", + ), # NOQA + ( + "/src/my_project_dir", + "my.template.yaml.py", + "/src/my_project_dir/templates/my.template.yaml.py", + ), # NOQA + ( + "my_project_dir", + "/src/my_project_dir/templates/my.template.yaml.py", + "/src/my_project_dir/templates/my.template.yaml.py", + ), # NOQA + ( + "/src/my_project_dir", + "/src/my_project_dir/templates/my.template.yaml.py", + "/src/my_project_dir/templates/my.template.yaml.py", + ), # NOQA + ], + ) @patch("sceptre.template_handlers.helper.call_sceptre_handler") def test_handler_handler(self, mocked_handler, project_path, path, output_path): template_handler = File( name="file_handler", arguments={"path": path}, - stack_group_config={"project_path": project_path} + stack_group_config={"project_path": project_path}, ) template_handler.handle() mocked_handler.assert_called_with(output_path, None) diff --git a/tests/test_template_handlers/test_helper.py b/tests/test_template_handlers/test_helper.py index 6ed3f09ca..b798e8549 100644 --- a/tests/test_template_handlers/test_helper.py +++ b/tests/test_template_handlers/test_helper.py @@ -8,11 +8,13 @@ from unittest.mock import patch -@pytest.mark.parametrize("filename,sceptre_user_data,expected", [ - ( - "vpc.j2", - {"vpc_id": "10.0.0.0/16"}, - """Resources: +@pytest.mark.parametrize( + "filename,sceptre_user_data,expected", + [ + ( + "vpc.j2", + {"vpc_id": "10.0.0.0/16"}, + """Resources: VPC: Type: AWS::EC2::VPC Properties: @@ -20,12 +22,12 @@ Outputs: VpcId: Value: - Ref: VPC""" - ), - ( - "vpc.yaml.j2", - {"vpc_id": "10.0.0.0/16"}, - """Resources: + Ref: VPC""", + ), + ( + "vpc.yaml.j2", + {"vpc_id": "10.0.0.0/16"}, + """Resources: VPC: Type: AWS::EC2::VPC Properties: @@ -33,15 +35,15 @@ Outputs: VpcId: Value: - Ref: VPC""" - ), - ( - "sg.j2", - [ - {"name": "sg_a", "inbound_ip": "10.0.0.0"}, - {"name": "sg_b", "inbound_ip": "10.0.0.1"} - ], - """Resources: + Ref: VPC""", + ), + ( + "sg.j2", + [ + {"name": "sg_a", "inbound_ip": "10.0.0.0"}, + {"name": "sg_b", "inbound_ip": "10.0.0.1"}, + ], + """Resources: sg_a: Type: "AWS::EC2::SecurityGroup" Properties: @@ -50,59 +52,65 @@ Type: "AWS::EC2::SecurityGroup" Properties: InboundIp: 10.0.0.1 -""" - ) -]) +""", + ), + ], +) @patch("pathlib.Path.exists") -def test_render_jinja_template(mock_pathlib, - filename, - sceptre_user_data, - expected): +def test_render_jinja_template(mock_pathlib, filename, sceptre_user_data, expected): mock_pathlib.return_value = True jinja_template_path = os.path.join( - os.getcwd(), - "tests/fixtures/templates", - filename + os.getcwd(), "tests/fixtures/templates", filename + ) + result = helper.render_jinja_template( + path=jinja_template_path, + jinja_vars={"sceptre_user_data": sceptre_user_data}, + j2_environment={}, ) - result = helper.render_jinja_template(path=jinja_template_path, - jinja_vars={"sceptre_user_data": sceptre_user_data}, - j2_environment={}) expected_yaml = yaml.safe_load(expected) result_yaml = yaml.safe_load(result) assert expected_yaml == result_yaml @pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8") -@pytest.mark.parametrize("j2_environment,expected_keys", [ - ({}, ["autoescape", "loader", "undefined"]), - ({"lstrip_blocks": True}, - ["autoescape", "loader", "undefined", "lstrip_blocks"]), - ({"lstrip_blocks": True, "extensions": ["test-ext"]}, - ["autoescape", "loader", "undefined", "lstrip_blocks", "extensions"]) -]) +@pytest.mark.parametrize( + "j2_environment,expected_keys", + [ + ({}, ["autoescape", "loader", "undefined"]), + ( + {"lstrip_blocks": True}, + ["autoescape", "loader", "undefined", "lstrip_blocks"], + ), + ( + {"lstrip_blocks": True, "extensions": ["test-ext"]}, + ["autoescape", "loader", "undefined", "lstrip_blocks", "extensions"], + ), + ], +) @patch("sceptre.template_handlers.helper.Environment") @patch("pathlib.Path.exists") -def test_render_jinja_template_j2_environment_config(mock_pathlib, - mock_environment, - j2_environment, - expected_keys): +def test_render_jinja_template_j2_environment_config( + mock_pathlib, mock_environment, j2_environment, expected_keys +): mock_pathlib.return_value = True filename = "vpc.j2" sceptre_user_data = {"vpc_id": "10.0.0.0/16"} jinja_template_path = os.path.join( - os.getcwd(), - "tests/fixtures/templates", - filename + os.getcwd(), "tests/fixtures/templates", filename + ) + _ = helper.render_jinja_template( + path=jinja_template_path, + jinja_vars={"sceptre_user_data": sceptre_user_data}, + j2_environment=j2_environment, ) - _ = helper.render_jinja_template(path=jinja_template_path, - jinja_vars={"sceptre_user_data": sceptre_user_data}, - j2_environment=j2_environment) assert list(mock_environment.call_args.kwargs) == expected_keys def test_render_jinja_template_non_existing_file(): jinja_template_path = os.path.join("/ref/to/nowhere/boom.j2") with pytest.raises(TemplateNotFoundError): - helper.render_jinja_template(path=jinja_template_path, - jinja_vars={"sceptre_user_data": {}}, - j2_environment={}) + helper.render_jinja_template( + path=jinja_template_path, + jinja_vars={"sceptre_user_data": {}}, + j2_environment={}, + ) diff --git a/tests/test_template_handlers/test_http.py b/tests/test_template_handlers/test_http.py index 8947ec65c..fe7fb4866 100644 --- a/tests/test_template_handlers/test_http.py +++ b/tests/test_template_handlers/test_http.py @@ -10,7 +10,6 @@ class TestHttp(object): - def test_get_template(self, requests_mock): url = "https://raw.githubusercontent.com/acme/bucket.yaml" requests_mock.get(url, content=b"Stuff is working") @@ -32,74 +31,91 @@ def test_get_template__request_error__raises_error(self, requests_mock): template_handler.handle() def test_handler_unsupported_type(self): - handler = Http("http_handler", {'url': 'https://raw.githubusercontent.com/acme/bucket.unsupported'}) + handler = Http( + "http_handler", + {"url": "https://raw.githubusercontent.com/acme/bucket.unsupported"}, + ) with pytest.raises(UnsupportedTemplateFileTypeError): handler.handle() - @pytest.mark.parametrize("url", [ - ("https://raw.githubusercontent.com/acme/bucket.json"), - ("https://raw.githubusercontent.com/acme/bucket.yaml"), - ("https://raw.githubusercontent.com/acme/bucket.template") - ]) - @patch('sceptre.template_handlers.http.Http._get_template') + @pytest.mark.parametrize( + "url", + [ + ("https://raw.githubusercontent.com/acme/bucket.json"), + ("https://raw.githubusercontent.com/acme/bucket.yaml"), + ("https://raw.githubusercontent.com/acme/bucket.template"), + ], + ) + @patch("sceptre.template_handlers.http.Http._get_template") def test_handler_raw_template(self, mock_get_template, url): mock_get_template.return_value = {} - handler = Http("http_handler", {'url': url}) + handler = Http("http_handler", {"url": url}) handler.handle() assert mock_get_template.call_count == 1 - @patch('sceptre.template_handlers.helper.render_jinja_template') - @patch('sceptre.template_handlers.http.Http._get_template') - def test_handler_jinja_template(self, mock_get_template, mock_render_jinja_template): + @patch("sceptre.template_handlers.helper.render_jinja_template") + @patch("sceptre.template_handlers.http.Http._get_template") + def test_handler_jinja_template( + self, mock_get_template, mock_render_jinja_template + ): mock_get_template_response = { "Description": "test template", "AWSTemplateFormatVersion": "2010-09-09", "Resources": { - "touchNothing": { - "Type": "AWS::CloudFormation::WaitConditionHandle" - } - } + "touchNothing": {"Type": "AWS::CloudFormation::WaitConditionHandle"} + }, } - mock_get_template.return_value = json.dumps(mock_get_template_response).encode('utf-8') - handler = Http("http_handler", {'url': 'https://raw.githubusercontent.com/acme/bucket.j2'}) + mock_get_template.return_value = json.dumps(mock_get_template_response).encode( + "utf-8" + ) + handler = Http( + "http_handler", {"url": "https://raw.githubusercontent.com/acme/bucket.j2"} + ) handler.handle() assert mock_render_jinja_template.call_count == 1 - @patch('sceptre.template_handlers.helper.call_sceptre_handler') - @patch('sceptre.template_handlers.http.Http._get_template') - def test_handler_python_template(self, mock_get_template, mock_call_sceptre_handler): + @patch("sceptre.template_handlers.helper.call_sceptre_handler") + @patch("sceptre.template_handlers.http.Http._get_template") + def test_handler_python_template( + self, mock_get_template, mock_call_sceptre_handler + ): mock_get_template_response = { "Description": "test template", "AWSTemplateFormatVersion": "2010-09-09", "Resources": { - "touchNothing": { - "Type": "AWS::CloudFormation::WaitConditionHandle" - } - } + "touchNothing": {"Type": "AWS::CloudFormation::WaitConditionHandle"} + }, } - mock_get_template.return_value = json.dumps(mock_get_template_response).encode('utf-8') - handler = Http("http_handler", {'url': 'https://raw.githubusercontent.com/acme/bucket.py'}) + mock_get_template.return_value = json.dumps(mock_get_template_response).encode( + "utf-8" + ) + handler = Http( + "http_handler", {"url": "https://raw.githubusercontent.com/acme/bucket.py"} + ) handler.handle() assert mock_call_sceptre_handler.call_count == 1 - @patch('sceptre.template_handlers.helper.call_sceptre_handler') - @patch('sceptre.template_handlers.http.Http._get_template') - def test_handler_override_handler_options(self, mock_get_template, mock_call_sceptre_handler): + @patch("sceptre.template_handlers.helper.call_sceptre_handler") + @patch("sceptre.template_handlers.http.Http._get_template") + def test_handler_override_handler_options( + self, mock_get_template, mock_call_sceptre_handler + ): mock_get_template_response = { "Description": "test template", "AWSTemplateFormatVersion": "2010-09-09", "Resources": { - "touchNothing": { - "Type": "AWS::CloudFormation::WaitConditionHandle" - } - } + "touchNothing": {"Type": "AWS::CloudFormation::WaitConditionHandle"} + }, } - mock_get_template.return_value = json.dumps(mock_get_template_response).encode('utf-8') + mock_get_template.return_value = json.dumps(mock_get_template_response).encode( + "utf-8" + ) custom_handler_options = {"timeout": 10, "retries": 20} - handler = Http("http_handler", - {'url': 'https://raw.githubusercontent.com/acme/bucket.py'}, - stack_group_config={"http_template_handler": custom_handler_options} - ) + handler = Http( + "http_handler", + {"url": "https://raw.githubusercontent.com/acme/bucket.py"}, + stack_group_config={"http_template_handler": custom_handler_options}, + ) handler.handle() assert mock_get_template.call_count == 1 args, options = mock_get_template.call_args diff --git a/tests/test_template_handlers/test_s3.py b/tests/test_template_handlers/test_s3.py index 48172f2bf..8eaa77f01 100644 --- a/tests/test_template_handlers/test_s3.py +++ b/tests/test_template_handlers/test_s3.py @@ -11,48 +11,37 @@ class TestS3(object): - def test_get_template(self): connection_manager = MagicMock(spec=ConnectionManager) - connection_manager.call.return_value = { - "Body": io.BytesIO(b"Stuff is working") - } + connection_manager.call.return_value = {"Body": io.BytesIO(b"Stuff is working")} template_handler = S3( name="s3_handler", arguments={"path": "bucket/folder/file.yaml"}, - connection_manager=connection_manager + connection_manager=connection_manager, ) result = template_handler.handle() connection_manager.call.assert_called_once_with( service="s3", command="get_object", - kwargs={ - "Bucket": "bucket", - "Key": "folder/file.yaml" - } + kwargs={"Bucket": "bucket", "Key": "folder/file.yaml"}, ) assert result == b"Stuff is working" def test_template_handler(self): connection_manager = MagicMock(spec=ConnectionManager) - connection_manager.call.return_value = { - "Body": io.BytesIO(b"Stuff is working") - } + connection_manager.call.return_value = {"Body": io.BytesIO(b"Stuff is working")} template_handler = S3( name="vpc", arguments={"path": "my-fancy-bucket/account/vpc.yaml"}, - connection_manager=connection_manager + connection_manager=connection_manager, ) result = template_handler.handle() connection_manager.call.assert_called_once_with( service="s3", command="get_object", - kwargs={ - "Bucket": "my-fancy-bucket", - "Key": "account/vpc.yaml" - } + kwargs={"Bucket": "my-fancy-bucket", "Key": "account/vpc.yaml"}, ) assert result == b"Stuff is working" @@ -63,7 +52,7 @@ def test_invalid_response_reraises_exception(self): template_handler = S3( name="vpc", arguments={"path": "my-fancy-bucket/account/vpc.yaml"}, - connection_manager=connection_manager + connection_manager=connection_manager, ) with pytest.raises(SceptreException) as e: @@ -72,52 +61,59 @@ def test_invalid_response_reraises_exception(self): assert str(e.value) == "BOOM!" def test_handler_unsupported_type(self): - s3_handler = S3("s3_handler", {'path': 'bucket/folder/file.unsupported'}) + s3_handler = S3("s3_handler", {"path": "bucket/folder/file.unsupported"}) with pytest.raises(UnsupportedTemplateFileTypeError): s3_handler.handle() - @pytest.mark.parametrize("path", [ - ("bucket/folder/file.json"), - ("bucket/folder/file.yaml"), - ("bucket/folder/file.template") - ]) - @patch('sceptre.template_handlers.s3.S3._get_template') + @pytest.mark.parametrize( + "path", + [ + ("bucket/folder/file.json"), + ("bucket/folder/file.yaml"), + ("bucket/folder/file.template"), + ], + ) + @patch("sceptre.template_handlers.s3.S3._get_template") def test_handler_raw_template(self, mock_get_template, path): mock_get_template.return_value = {} - s3_handler = S3("s3_handler", {'path': path}) + s3_handler = S3("s3_handler", {"path": path}) s3_handler.handle() assert mock_get_template.call_count == 1 - @patch('sceptre.template_handlers.helper.render_jinja_template') - @patch('sceptre.template_handlers.s3.S3._get_template') - def test_handler_jinja_template(slef, mock_get_template, mock_render_jinja_template): + @patch("sceptre.template_handlers.helper.render_jinja_template") + @patch("sceptre.template_handlers.s3.S3._get_template") + def test_handler_jinja_template( + slef, mock_get_template, mock_render_jinja_template + ): mock_get_template_response = { "Description": "test template", "AWSTemplateFormatVersion": "2010-09-09", "Resources": { - "touchNothing": { - "Type": "AWS::CloudFormation::WaitConditionHandle" - } - } + "touchNothing": {"Type": "AWS::CloudFormation::WaitConditionHandle"} + }, } - mock_get_template.return_value = json.dumps(mock_get_template_response).encode('utf-8') - s3_handler = S3("s3_handler", {'path': 'bucket/folder/file.j2'}) + mock_get_template.return_value = json.dumps(mock_get_template_response).encode( + "utf-8" + ) + s3_handler = S3("s3_handler", {"path": "bucket/folder/file.j2"}) s3_handler.handle() assert mock_render_jinja_template.call_count == 1 - @patch('sceptre.template_handlers.helper.call_sceptre_handler') - @patch('sceptre.template_handlers.s3.S3._get_template') - def test_handler_python_template(self, mock_get_template, mock_call_sceptre_handler): + @patch("sceptre.template_handlers.helper.call_sceptre_handler") + @patch("sceptre.template_handlers.s3.S3._get_template") + def test_handler_python_template( + self, mock_get_template, mock_call_sceptre_handler + ): mock_get_template_response = { "Description": "test template", "AWSTemplateFormatVersion": "2010-09-09", "Resources": { - "touchNothing": { - "Type": "AWS::CloudFormation::WaitConditionHandle" - } - } + "touchNothing": {"Type": "AWS::CloudFormation::WaitConditionHandle"} + }, } - mock_get_template.return_value = json.dumps(mock_get_template_response).encode('utf-8') - s3_handler = S3("s3_handler", {'path': 'bucket/folder/file.py'}) + mock_get_template.return_value = json.dumps(mock_get_template_response).encode( + "utf-8" + ) + s3_handler = S3("s3_handler", {"path": "bucket/folder/file.py"}) s3_handler.handle() assert mock_call_sceptre_handler.call_count == 1 diff --git a/tests/test_template_handlers/test_template_handlers.py b/tests/test_template_handlers/test_template_handlers.py index 877034045..2b425a55b 100644 --- a/tests/test_template_handlers/test_template_handlers.py +++ b/tests/test_template_handlers/test_template_handlers.py @@ -11,10 +11,8 @@ def __init__(self, *args, **kwargs): def schema(self): return { "type": "object", - "properties": { - "argument": {"type": "string"} - }, - "required": ["argument"] + "properties": {"argument": {"type": "string"}}, + "required": ["argument"], } def handle(self): @@ -28,5 +26,7 @@ def test_template_handler_validates_schema(self): def test_template_handler_errors_when_arguments_invalid(self): with pytest.raises(TemplateHandlerArgumentsInvalidError): - handler = MockTemplateHandler(name="mock", arguments={"non-existent": "test"}) + handler = MockTemplateHandler( + name="mock", arguments={"non-existent": "test"} + ) handler.validate()