Skip to content

Commit

Permalink
[Resolve #915] Support ChangeSetType CREATE (#1469)
Browse files Browse the repository at this point in the history
This adds logic supporting the case of creating change sets for a stack that does not already exist. That is, it adds support for change sets of ChangeSetType CREATE. https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cloudformation/client/create_change_set.html

To add some context, there is no obvious reason why Sceptre does not already support change sets of type CREATE since the logic for detecting stack status already exists for the launch command. The project's initial commit already has this inconsistency and essentially the same code. It is probably an oversight.

The changes have already been tested by Rohit S in this Slack thread
https://og-aws.slack.com/archives/C01JNN8RGBB/p1716869822031499

In addition, it is necessary to add logic to handle the REVIEW_IN_PROGRESS state, both for the case of the launch and create action.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html

Creation of a change set on a non-existent stack actually creates a stack and places it into the REVIEW_IN_PROGRESS state. If Sceptre is then to launch this stack, it is necessary to firstly delete the stack, just as it already does in the cases of states CREATE_FAILED and ROLLBACK_COMPLETE.
  • Loading branch information
alexharv074 committed Jul 3, 2024
1 parent 39af284 commit fbb034d
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 3 deletions.
3 changes: 1 addition & 2 deletions integration-tests/features/create-change-set.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 21 additions & 1 deletion sceptre/plan/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down Expand Up @@ -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),
Expand All @@ -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()
Expand Down
40 changes: 40 additions & 0 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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"}],
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit fbb034d

Please sign in to comment.