diff --git a/integration-tests/features/describe-stack-group-resources.feature b/integration-tests/features/describe-stack-group-resources.feature deleted file mode 100644 index 1dd0f47f3..000000000 --- a/integration-tests/features/describe-stack-group-resources.feature +++ /dev/null @@ -1,23 +0,0 @@ -Feature: Describe stack_group resources - - Scenario: describe resources of a stack_group that does not exist - Given stack_group "2" does not exist - When the user describes resources in stack_group "2" - Then no resources are described - - Scenario: describe resources of a stack_group that already exists - Given all the stacks in stack_group "2" are in "CREATE_COMPLETE" - When the user describes resources in stack_group "2" - Then only all resources in stack_group "2" are described - - Scenario: describe a stack_group that partially exists - Given stack "2/A" exists in "CREATE_COMPLETE" state - and stack "2/B" does not exist - and stack "2/C" does not exist - When the user describes resources in stack_group "2" - Then only resources in stack "2/A" are described - - Scenario: describe resources of a stack_group that already exists with ignore dependencies - Given all the stacks in stack_group "2" are in "CREATE_COMPLETE" - When the user describes resources in stack_group "2" with ignore dependencies - Then only all resources in stack_group "2" are described diff --git a/integration-tests/features/describe-stack-group.feature b/integration-tests/features/describe-stack-group.feature deleted file mode 100644 index 8b1756de8..000000000 --- a/integration-tests/features/describe-stack-group.feature +++ /dev/null @@ -1,25 +0,0 @@ -Feature: Describe stack_group - - Scenario: describe a stack_group that does not exist - Given stack_group "2" does not exist - When the user describes stack_group "2" - Then no resources are described - - Scenario: describe a stack_group that already exists - Given all the stacks in stack_group "2" are in "CREATE_COMPLETE" - When the user describes stack_group "2" - Then all stacks in stack_group "2" are described as "CREATE_COMPLETE" - - Scenario: describe a stack_group that partially exists - Given stack "2/A" exists in "CREATE_COMPLETE" state - and stack "2/B" exists in "UPDATE_COMPLETE" state - and stack "2/C" does not exist - When the user describes stack_group "2" - Then stack "2/A" is described as "CREATE_COMPLETE" - and stack "2/B" is described as "UPDATE_COMPLETE" - and stack "2/C" is described as "PENDING" - - Scenario: describe a stack_group that already exists with ignore dependencies - Given all the stacks in stack_group "2" are in "CREATE_COMPLETE" - When the user describes stack_group "2" with ignore dependencies - Then all stacks in stack_group "2" are described as "CREATE_COMPLETE" diff --git a/integration-tests/features/describe-stack-resources.feature b/integration-tests/features/describe-stack-resources.feature deleted file mode 100644 index f53c77b93..000000000 --- a/integration-tests/features/describe-stack-resources.feature +++ /dev/null @@ -1,11 +0,0 @@ -Feature: Describe-stack-resources - - Scenario: describe the resources of a stack that exists - Given stack "1/A" exists in "CREATE_COMPLETE" state - When the user describes the resources of stack "1/A" - Then the resources of stack "1/A" are described - - Scenario: describe the resources of a stack that exists with ignore dependencies - Given stack "1/A" exists in "CREATE_COMPLETE" state - When the user describes the resources of stack "1/A" with ignore dependencies - Then the resources of stack "1/A" are described diff --git a/integration-tests/steps/stack_groups.py b/integration-tests/steps/stack_groups.py index b6ef69b4d..07a3282a3 100644 --- a/integration-tests/steps/stack_groups.py +++ b/integration-tests/steps/stack_groups.py @@ -97,68 +97,6 @@ def step_impl(context, stack_group_name): sceptre_plan.delete() -@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 - ) - - sceptre_plan = SceptrePlan(sceptre_context) - responses = sceptre_plan.describe() - - stack_names = get_full_stack_names(context, stack_group_name) - cfn_stacks = {} - - for response in responses.values(): - if response is None: - continue - for stack in response["Stacks"]: - cfn_stacks[stack["StackName"]] = stack["StackStatus"] - - context.response = [ - {short_name: cfn_stacks[full_name]} - for short_name, full_name in stack_names.items() - if cfn_stacks.get(full_name) - ] - - -@when('the user describes 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, - ) - - sceptre_plan = SceptrePlan(sceptre_context) - responses = sceptre_plan.describe() - - stack_names = get_full_stack_names(context, stack_group_name) - cfn_stacks = {} - - for response in responses.values(): - if response is None: - continue - for stack in response["Stacks"]: - cfn_stacks[stack["StackName"]] = stack["StackStatus"] - - context.response = [ - {short_name: cfn_stacks[full_name]} - for short_name, full_name in stack_names.items() - if cfn_stacks.get(full_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 - ) - - 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' ) @@ -196,21 +134,6 @@ def step_impl(context, stack_group_name): check_stack_status(context, full_stack_names, None) -@then('all stacks in stack_group "{stack_group_name}" are described as "{status}"') -def step_impl(context, stack_group_name, status): - stacks_names = get_stack_names(context, stack_group_name) - expected_response = [{stack_name: status} for stack_name in stacks_names] - for response in context.response: - assert response in expected_response - - -@then("no resources are described") -def step_impl(context): - for stack_resources in context.response: - stack_name = next(iter(stack_resources)) - assert stack_resources == {stack_name: []} - - @then('stack "{stack_name}" is described as "{status}"') def step_impl(context, stack_name, status): response = next( @@ -221,51 +144,6 @@ def step_impl(context, stack_name, status): assert response[stack_name] == status -@then('only all resources in stack_group "{stack_group_name}" are described') -def step_impl(context, stack_group_name): - stacks_names = get_full_stack_names(context, stack_group_name) - expected_resources = {} - sceptre_response = [] - for stack_resources in context.response: - for resource in stack_resources.values(): - sceptre_response.append(resource[0]["PhysicalResourceId"]) - - for short_name, full_name in stacks_names.items(): - time.sleep(1) - response = retry_boto_call( - context.client.describe_stack_resources, StackName=full_name - ) - expected_resources[short_name] = response["StackResources"] - - for short_name, resources in expected_resources.items(): - for resource in resources: - sceptre_response.remove(resource["PhysicalResourceId"]) - - assert sceptre_response == [] - - -@then('only resources in stack "{stack_name}" are described') -def step_impl(context, stack_name): - expected_resources = {} - sceptre_response = [] - for stack_resources in context.response: - for resource in stack_resources.values(): - if resource: - sceptre_response.append(resource[0].get("PhysicalResourceId")) - - response = retry_boto_call( - context.client.describe_stack_resources, - StackName=get_cloudformation_stack_name(context, stack_name), - ) - expected_resources[stack_name] = response["StackResources"] - - for short_name, resources in expected_resources.items(): - for resource in resources: - sceptre_response.remove(resource["PhysicalResourceId"]) - - assert sceptre_response == [] - - @then('that stack "{first_stack}" was created before "{second_stack}"') def step_impl(context, first_stack, second_stack): stacks = [ diff --git a/integration-tests/steps/stacks.py b/integration-tests/steps/stacks.py index c286920d0..f072d1a49 100644 --- a/integration-tests/steps/stacks.py +++ b/integration-tests/steps/stacks.py @@ -310,30 +310,6 @@ def step_impl(context, stack_name): launch_stack(context, stack_name, False, True) -@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 - ) - - 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' -) -def step_impl(context, stack_name): - sceptre_context = SceptreContext( - command_path=stack_name + ".yaml", - project_path=context.sceptre_dir, - ignore_dependencies=True, - ) - - sceptre_plan = SceptrePlan(sceptre_context) - context.output = list(sceptre_plan.describe_resources().values()) - - @when('the user diffs stack "{stack_name}" with "{diff_type}"') def step_impl(context, stack_name, diff_type): sceptre_context = SceptreContext( @@ -381,21 +357,6 @@ def step_impl(context, stack_name): 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 - ) - properties = {"LogicalResourceId", "PhysicalResourceId"} - formatted_response = [ - {k: v for k, v in item.items() if k in properties} - for item in response["StackResources"] - ] - - assert [{stack_name: formatted_response}] == context.output - - @then( 'stack "{stack_name}" does not exist and stack "{dependant_stack_name}" exists in "{desired_state}"' ) diff --git a/sceptre/diffing/stack_differ.py b/sceptre/diffing/stack_differ.py index e49ef665d..a575c6afc 100644 --- a/sceptre/diffing/stack_differ.py +++ b/sceptre/diffing/stack_differ.py @@ -23,6 +23,8 @@ from sceptre.plan.actions import StackActions from sceptre.stack import Stack +from botocore.exceptions import ClientError + DiffType = TypeVar("DiffType") logger = logging.getLogger(__name__) @@ -192,10 +194,12 @@ def _extract_parameters_from_generated_stack(self, stack: Stack) -> dict: def _create_deployed_stack_config( self, stack_actions: StackActions ) -> Optional[StackConfiguration]: - description = stack_actions.describe() - if description is None: + try: + description = stack_actions.describe() + except ClientError as err: # This means the stack has not been deployed yet - return None + if err.response["Error"]["Message"].endswith("does not exist"): + return None stacks = description["Stacks"] for stack in stacks: diff --git a/sceptre/plan/actions.py b/sceptre/plan/actions.py index 8f75b1e66..17ed61ae4 100644 --- a/sceptre/plan/actions.py +++ b/sceptre/plan/actions.py @@ -303,16 +303,11 @@ def describe(self): :returns: A Stack description. :rtype: dict """ - try: - return self.connection_manager.call( - service="cloudformation", - command="describe_stacks", - kwargs={"StackName": self.stack.external_name}, - ) - except botocore.exceptions.ClientError as e: - if e.response["Error"]["Message"].endswith("does not exist"): - return - raise + return self.connection_manager.call( + service="cloudformation", + command="describe_stacks", + kwargs={"StackName": self.stack.external_name}, + ) def describe_events(self): """ @@ -364,15 +359,11 @@ def describe_outputs(self): """ Returns the Stack's outputs. - :returns: The Stack's outputs. + :returns: The stack's outputs. :rtype: list """ self.logger.debug("%s - Describing stack outputs", self.stack.name) - - try: - response = self._describe() - except botocore.exceptions.ClientError: - return [] + response = self.describe() return {self.stack.name: response["Stacks"][0].get("Outputs", [])} @@ -784,16 +775,9 @@ def timed_out(elapsed): return status - def _describe(self): - return self.connection_manager.call( - service="cloudformation", - command="describe_stacks", - kwargs={"StackName": self.stack.external_name}, - ) - def _get_status(self): try: - status = self._describe()["Stacks"][0]["StackStatus"] + status = self.describe()["Stacks"][0]["StackStatus"] except botocore.exceptions.ClientError as exp: if exp.response["Error"]["Message"].endswith("does not exist"): raise StackDoesNotExistError(exp.response["Error"]["Message"]) diff --git a/tests/test_actions.py b/tests/test_actions.py index 551afa466..a1eb73bf8 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -556,14 +556,15 @@ def test_describe_resources_sends_correct_request(self): ] } - @patch("sceptre.plan.actions.StackActions._describe") + @patch("sceptre.plan.actions.StackActions.describe") def test_describe_outputs_sends_correct_request(self, mock_describe): 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") + @patch("sceptre.plan.actions.StackActions.describe") def test_describe_outputs_handles_stack_with_no_outputs(self, mock_describe): mock_describe.return_value = {"Stacks": [{}]} response = self.actions.describe_outputs() @@ -892,13 +893,13 @@ def test_format_parameters_with_none_list_and_string_values(self): {"ParameterKey": "key2", "ParameterValue": "value4"}, ] - @patch("sceptre.plan.actions.StackActions._describe") + @patch("sceptre.plan.actions.StackActions.describe") def test_get_status_with_created_stack(self, mock_describe): mock_describe.return_value = {"Stacks": [{"StackStatus": "CREATE_COMPLETE"}]} status = self.actions.get_status() assert status == "CREATE_COMPLETE" - @patch("sceptre.plan.actions.StackActions._describe") + @patch("sceptre.plan.actions.StackActions.describe") def test_get_status_with_non_existent_stack(self, mock_describe): mock_describe.side_effect = ClientError( { @@ -911,7 +912,7 @@ def test_get_status_with_non_existent_stack(self, mock_describe): ) assert self.actions.get_status() == "PENDING" - @patch("sceptre.plan.actions.StackActions._describe") + @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!"}}, diff --git a/tests/test_diffing/test_stack_differ.py b/tests/test_diffing/test_stack_differ.py index 83bf7b441..b05a2f045 100644 --- a/tests/test_diffing/test_stack_differ.py +++ b/tests/test_diffing/test_stack_differ.py @@ -20,6 +20,8 @@ from sceptre.plan.actions import StackActions from sceptre.stack import Stack +from botocore.exceptions import ClientError + class ImplementedStackDiffer(StackDiffer): def __init__(self, command_capturer: Mock): @@ -224,14 +226,22 @@ def test_diff__deployed_stack_exists__returns_is_deployed_as_true(self): assert diff.is_deployed is True def test_diff__deployed_stack_does_not_exist__returns_is_deployed_as_false(self): - self.actions.describe.return_value = self.actions.describe.side_effect = None + self.actions.describe.side_effect = ClientError( + {"Error": {"Code": "Whatevs", "Message": "Stack does not exist"}}, + "DescribeStacks", + ) + self.actions.describe.return_value = None 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, ): - self.actions.describe.return_value = self.actions.describe.side_effect = None + self.actions.describe.side_effect = ClientError( + {"Error": {"Code": "Whatevs", "Message": "Stack does not exist"}}, + "DescribeStacks", + ) + self.actions.describe.return_value = None self.differ.diff(self.actions) self.command_capturer.compare_stack_configurations.assert_called_with(