Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Resolve #1483] Handle errors in change sets #1486

Merged
merged 6 commits into from
Aug 3, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions integration-tests/features/describe-change-set.feature
Original file line number Diff line number Diff line change
@@ -10,8 +10,7 @@ Feature: Describe change sets
Given stack "1/A" exists in "CREATE_COMPLETE" state
and stack "1/A" has no change sets
When the user describes change set "A" for stack "1/A"
Then a "ClientError" is raised
and the user is told "change set does not exist"
Then the user is told "Failed describing Change Set"

Scenario: describe a change set that exists with ignore dependencies
Given stack "1/A" exists in "CREATE_COMPLETE" state
3 changes: 1 addition & 2 deletions integration-tests/features/execute-change-set.feature
Original file line number Diff line number Diff line change
@@ -11,8 +11,7 @@ Feature: Execute change set
Given stack "1/A" exists in "CREATE_COMPLETE" state
And stack "1/A" does not have change set "A"
When the user executes change set "A" for stack "1/A"
Then a "ClientError" is raised
And the user is told "change set does not exist"
Then the user is told "change set does not exist"

Scenario: execute a change set that exists with ignore dependencies
Given stack "1/A" exists in "CREATE_COMPLETE" state
5 changes: 3 additions & 2 deletions integration-tests/steps/helpers.py
Original file line number Diff line number Diff line change
@@ -17,14 +17,15 @@ def step_impl(context, 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"]
assert msg.endswith("does not exist")
assert context.log_capture.find_event("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"]
assert msg.startswith("Template format error")
elif message == "Failed describing Change Set":
assert context.log_capture.find_event(message)
else:
raise Exception("Step has incorrect message")

3 changes: 3 additions & 0 deletions sceptre/cli/helpers.py
Original file line number Diff line number Diff line change
@@ -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",
84 changes: 67 additions & 17 deletions sceptre/plan/actions.py
Original file line number Diff line number Diff line change
@@ -465,8 +465,21 @@ 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):
self.logger.debug(
"%s - Creating Change Set '%s'", self.stack.name, change_set_name
)
@@ -490,6 +503,24 @@ 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):
self.logger.debug(
"%s - Deleting Change Set '%s'", self.stack.name, change_set_name
)
@@ -501,13 +532,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):
"""
@@ -518,6 +542,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
)
@@ -543,16 +582,32 @@ 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(

return_val = 0

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")
)
)
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
)
@@ -567,8 +622,9 @@ 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:
"""Indicates the change set failed when it was created because there were actually
def _change_set_creation_failed_due_to_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.
:param reason: The reason reported by CloudFormation for the Change Set failure
@@ -1103,9 +1159,6 @@ def _log_drift_status(self, response: dict) -> None:
self.logger.debug(f"{self.stack.name} - {key} - {response[key]}")

def _detect_stack_drift(self) -> dict:
"""
Run detect_stack_drift.
"""
self.logger.info(f"{self.stack.name} - Detecting Stack Drift")

return self.connection_manager.call(
@@ -1115,9 +1168,6 @@ def _detect_stack_drift(self) -> dict:
)

def _describe_stack_drift_detection_status(self, detection_id: str) -> dict:
"""
Run describe_stack_drift_detection_status.
"""
self.logger.info(f"{self.stack.name} - Describing Stack Drift Detection Status")

return self.connection_manager.call(