Skip to content

Commit

Permalink
[Resolve Sceptre#1483] Handle errors in change sets
Browse files Browse the repository at this point in the history
This adds code to handle the case of failures in stack groups that have
missing or errored stacks when creating, describing and executing
change-sets.

Before this, any stack group that contained a stack that had been
deleted, or had errored out, resulted in all change set operations on
that stack group failing.

The reason for this is no error or exception handling had ever been
added for change sets, but assumed all stacks in the stack group were
clean and could respond to the change set APIs.

For example, deleting change sets might error out with:

```
% sceptre delete -y network.yaml sceptre-network
The Change Set will be delete on the following stacks, if applicable:
network

"An error occurred (ValidationError) when calling the DeleteChangeSet
operation: Stack [test-e2e-direct-connect] does not exist"
```

This is particularly an issue for sites that employ as best practice
analysis of change sets for safety prior to launching updates.

This patch adds the missing error handling for `delete_change_set`,
`create_change_set`, `describe_change_set` and
`execute_change_set`.
  • Loading branch information
alexharv074 committed Jul 6, 2024
1 parent c06b3b5 commit 69ace38
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 12 deletions.
3 changes: 3 additions & 0 deletions sceptre/cli/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@ def simplify_change_set_description(response):
:returns: A more concise description of the change set.
:rtype: dict
"""
if not response:
return {"ChangeSetName": "ChangeSetNotFound"}

desired_response_items = [
"ChangeSetName",
"CreationTime",
Expand Down
83 changes: 71 additions & 12 deletions sceptre/plan/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,8 +465,24 @@ def create_change_set(self, change_set_name):
{"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._get_role_arn())

try:
self._create_change_set(change_set_name, create_change_set_kwargs)
except Exception as err:
self.logger.info(
"%s - Failed creating Change Set '%s'\n%s",
self.stack.name,
change_set_name,
err,
)

def _create_change_set(self, change_set_name, create_change_set_kwargs):
"""
Wrap create_change_set.
"""
self.logger.debug(
"%s - Creating Change Set '%s'", self.stack.name, change_set_name
)
Expand All @@ -490,6 +506,27 @@ def delete_change_set(self, change_set_name):
:param change_set_name: The name of the Change Set.
:type change_set_name: str
"""
# If the call successfully completes, AWS CloudFormation
# successfully deleted the Change Set.
try:
self._delete_change_set(change_set_name)
self.logger.info(
"%s - Successfully deleted Change Set '%s'",
self.stack.name,
change_set_name,
)
except Exception as err:
self.logger.info(
"%s - Failed deleting Change Set '%s'\n%s",
self.stack.name,
change_set_name,
err,
)

def _delete_change_set(self, change_set_name):
"""
Wrap delete_change_set.
"""
self.logger.debug(
"%s - Deleting Change Set '%s'", self.stack.name, change_set_name
)
Expand All @@ -501,13 +538,6 @@ def delete_change_set(self, change_set_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,
)

def describe_change_set(self, change_set_name):
"""
Expand All @@ -518,6 +548,21 @@ def describe_change_set(self, change_set_name):
:returns: The description of the Change Set.
:rtype: dict
"""
return_val = {}

try:
return_val = self._describe_change_set(change_set_name)
except Exception as err:
self.logger.info(
"%s - Failed describing Change Set '%s'\n%s",
self.stack.name,
change_set_name,
err,
)

return return_val

def _describe_change_set(self, change_set_name):
self.logger.debug(
"%s - Describing Change Set '%s'", self.stack.name, change_set_name
)
Expand All @@ -543,16 +588,30 @@ 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
):

return_val = 0

if status == "FAILED" and self._change_set_failed_no_changes(reason):
self.logger.info(
"Skipping ChangeSet on Stack: {} - there are no changes".format(
change_set.get("StackName")
)
)
return 0
return return_val

try:
return_val = self._execute_change_set(change_set_name)
except Exception as err:
self.logger.info(
"%s - Failed describing Change Set '%s'\n%s",
self.stack.name,
change_set_name,
err,
)

return return_val

def _execute_change_set(self, change_set_name):
self.logger.debug(
"%s - Executing Change Set '%s'", self.stack.name, change_set_name
)
Expand All @@ -567,7 +626,7 @@ def execute_change_set(self, change_set_name):
status = self._wait_for_completion(boto_response=response)
return status

def change_set_creation_failed_due_to_no_changes(self, reason: str) -> bool:
def _change_set_failed_no_changes(self, reason: str) -> bool:
"""Indicates the change set failed when it was created because there were actually
no changes introduced from the change set.
Expand Down

0 comments on commit 69ace38

Please sign in to comment.