diff --git a/integration-tests/features/create-change-set.feature b/integration-tests/features/create-change-set.feature index a53cc8563..91d829861 100644 --- a/integration-tests/features/create-change-set.feature +++ b/integration-tests/features/create-change-set.feature @@ -18,8 +18,7 @@ Feature: Create change set Given stack "1/A" does not exist and the template for stack "1/A" is "valid_template.json" When the user creates change set "A" for stack "1/A" - Then a "ClientError" is raised - and the user is told "stack does not exist" + Then stack "1/A" has change set "A" in "CREATE_COMPLETE" state Scenario: create new change set with updated template and ignore dependencies Given stack "1/A" exists in "CREATE_COMPLETE" state diff --git a/sceptre/plan/actions.py b/sceptre/plan/actions.py index 17ed61ae4..370a0a2ab 100644 --- a/sceptre/plan/actions.py +++ b/sceptre/plan/actions.py @@ -212,7 +212,11 @@ def launch(self) -> StackStatus: if existing_status == "PENDING": status = self.create() - elif existing_status in ["CREATE_FAILED", "ROLLBACK_COMPLETE"]: + elif existing_status in [ + "CREATE_FAILED", + "ROLLBACK_COMPLETE", + "REVIEW_IN_PROGRESS", + ]: self.delete() status = self.create() elif existing_status.endswith("COMPLETE"): @@ -431,6 +435,21 @@ def create_change_set(self, change_set_name): :param change_set_name: The name of the Change Set. :type change_set_name: str """ + try: + existing_status = self._get_status() + except StackDoesNotExistError: + existing_status = "PENDING" + + self.logger.info( + "%s - Stack is in the %s state", self.stack.name, existing_status + ) + + change_set_type = ( + "CREATE" + if existing_status in ["PENDING", "REVIEW_IN_PROGRESS"] + else "UPDATE" + ) + create_change_set_kwargs = { "StackName": self.stack.external_name, "Parameters": self._format_parameters(self.stack.parameters), @@ -440,6 +459,7 @@ def create_change_set(self, change_set_name): "CAPABILITY_AUTO_EXPAND", ], "ChangeSetName": change_set_name, + "ChangeSetType": change_set_type, "NotificationARNs": self.stack.notifications, "Tags": [ {"Key": str(k), "Value": str(v)} for k, v in self.stack.tags.items() diff --git a/tests/test_actions.py b/tests/test_actions.py index a1eb73bf8..9e66b7601 100644 --- a/tests/test_actions.py +++ b/tests/test_actions.py @@ -397,6 +397,19 @@ def test_launch_with_stack_that_failed_to_create( mock_create.assert_called_once_with() assert response == sentinel.launch_response + @patch("sceptre.plan.actions.StackActions.create") + @patch("sceptre.plan.actions.StackActions.delete") + @patch("sceptre.plan.actions.StackActions._get_status") + def test_launch_with_stack_in_review_in_progress( + self, mock_get_status, mock_delete, mock_create + ): + mock_get_status.return_value = "REVIEW_IN_PROGRESS" + mock_create.return_value = sentinel.launch_response + response = self.actions.launch() + mock_delete.assert_called_once_with() + mock_create.assert_called_once_with() + assert response == sentinel.launch_response + @patch("sceptre.plan.actions.StackActions.update") @patch("sceptre.plan.actions.StackActions._get_status") def test_launch_with_complete_stack_with_updates_to_perform( @@ -632,6 +645,7 @@ def test_create_change_set_sends_correct_request(self): "CAPABILITY_AUTO_EXPAND", ], "ChangeSetName": sentinel.change_set_name, + "ChangeSetType": "UPDATE", "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [sentinel.notification], "Tags": [{"Key": "tag1", "Value": "val1"}], @@ -659,12 +673,38 @@ def test_create_change_set_sends_correct_request_no_notifications(self): "CAPABILITY_AUTO_EXPAND", ], "ChangeSetName": sentinel.change_set_name, + "ChangeSetType": "UPDATE", "RoleARN": sentinel.cloudformation_service_role, "NotificationARNs": [], "Tags": [{"Key": "tag1", "Value": "val1"}], }, ) + @patch("sceptre.plan.actions.StackActions._get_status") + def test_create_change_set_with_non_existent_stack(self, mock_get_status): + mock_get_status.side_effect = StackDoesNotExistError() + self.template._body = sentinel.template + self.actions.create_change_set(sentinel.change_set_name) + self.actions.connection_manager.call.assert_called_with( + service="cloudformation", + command="create_change_set", + kwargs={ + "StackName": sentinel.external_name, + "TemplateBody": sentinel.template, + "Parameters": [{"ParameterKey": "key1", "ParameterValue": "val1"}], + "Capabilities": [ + "CAPABILITY_IAM", + "CAPABILITY_NAMED_IAM", + "CAPABILITY_AUTO_EXPAND", + ], + "ChangeSetName": sentinel.change_set_name, + "ChangeSetType": "CREATE", + "RoleARN": sentinel.cloudformation_service_role, + "NotificationARNs": [sentinel.notification], + "Tags": [{"Key": "tag1", "Value": "val1"}], + }, + ) + def test_delete_change_set_sends_correct_request(self): self.actions.delete_change_set(sentinel.change_set_name) self.actions.connection_manager.call.assert_called_with(