Skip to content

Commit

Permalink
Merge branch 'ah/harkylton/fix-update-changesets' into release-candid…
Browse files Browse the repository at this point in the history
…ate-4.5.0
  • Loading branch information
alexharv074 committed Jun 23, 2024
2 parents d956abd + 79570a5 commit f4aecc4
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 7 deletions.
30 changes: 24 additions & 6 deletions sceptre/cli/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,41 @@ def update_command(
try:
# Wait for change set to be created
statuses = plan.wait_for_cs_completion(change_set_name)
# Exit if change set fails to create
for status in list(statuses.values()):
if status != StackChangeSetStatus.READY:

at_least_one_ready = False

for status in statuses.values():
# Exit if change set fails to create
if status not in (
StackChangeSetStatus.READY,
StackChangeSetStatus.NO_CHANGES,
):
write("Failed to create change set", context.output_format)
exit(1)

if status == StackChangeSetStatus.READY:
at_least_one_ready = True

# If none are ready, and we haven't exited, there are no changes
if not at_least_one_ready:
write("No changes detected", context.output_format)
exit(0)

# Describe changes
descriptions = plan.describe_change_set(change_set_name)
for description in list(descriptions.values()):
for stack, description in descriptions.items():
# No need to print if there are no changes
if statuses[stack] == StackChangeSetStatus.NO_CHANGES:
continue

if not verbose:
description = simplify_change_set_description(description)
write(description, context.output_format)

# Execute change set if happy with changes
if yes or click.confirm("Proceed with stack update?"):
plan.execute_change_set(change_set_name)
except Exception as e:
raise e

finally:
# Clean up by deleting change set
plan.delete_change_set(change_set_name)
Expand Down
7 changes: 7 additions & 0 deletions sceptre/plan/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ def _get_cs_status(self, change_set_name):
cs_description = self.describe_change_set(change_set_name)

cs_status = cs_description["Status"]
cs_reason = cs_description.get("StatusReason")
cs_exec_status = cs_description["ExecutionStatus"]
possible_statuses = [
"CREATE_PENDING",
Expand Down Expand Up @@ -927,6 +928,12 @@ def _get_cs_status(self, change_set_name):
"CREATE_COMPLETE",
] and cs_exec_status in ["UNAVAILABLE", "AVAILABLE"]:
return StackChangeSetStatus.PENDING
elif (
cs_status == "FAILED"
and cs_reason is not None
and self.change_set_creation_failed_due_to_no_changes(cs_reason)
):
return StackChangeSetStatus.NO_CHANGES
elif cs_status in ["DELETE_COMPLETE", "FAILED"] or cs_exec_status in [
"EXECUTE_IN_PROGRESS",
"EXECUTE_COMPLETE",
Expand Down
1 change: 1 addition & 0 deletions sceptre/stack_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ class StackChangeSetStatus(object):
PENDING = "pending"
READY = "ready"
DEFUNCT = "defunct"
NO_CHANGES = "no changes"
9 changes: 9 additions & 0 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1207,6 +1207,15 @@ def test_get_cs_status_handles_all_statuses(self, mock_describe_change_set):
response = self.actions._get_cs_status(sentinel.change_set_name)
assert response == returns[i]

mock_describe_change_set.return_value = {
"Status": "FAILED",
"StatusReason": "The submitted information didn't contain changes. "
"Submit different information to create a change set.",
"ExecutionStatus": "UNAVAILABLE",
}
response = self.actions._get_cs_status(sentinel.change_set_name)
assert response == scss.NO_CHANGES

for status in return_values["Status"]:
mock_describe_change_set.return_value = {
"Status": status,
Expand Down
112 changes: 111 additions & 1 deletion tests/test_cli/test_cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from sceptre.exceptions import SceptreException
from sceptre.plan.actions import StackActions
from sceptre.stack import Stack
from sceptre.stack_status import StackStatus
from sceptre.stack_status import StackChangeSetStatus, StackStatus


class TestCli:
Expand Down Expand Up @@ -514,6 +514,116 @@ def test_stack_commands(self, command, success, yes_flag, exit_code):
run_command.assert_called_with()
assert result.exit_code == exit_code

@pytest.mark.parametrize("verbose_flag", [True, False])
def test_update_with_change_set_ready(self, verbose_flag):
create_command = self.mock_stack_actions.create_change_set
wait_command = self.mock_stack_actions.wait_for_cs_completion
execute_command = self.mock_stack_actions.execute_change_set
delete_command = self.mock_stack_actions.delete_change_set
describe_command = self.mock_stack_actions.describe_change_set

change_set_status = StackChangeSetStatus.READY
wait_command.return_value = change_set_status

response = {
"VerboseProperty": "VerboseProperty",
"ChangeSetName": "ChangeSetName",
"CreationTime": "CreationTime",
"ExecutionStatus": "ExecutionStatus",
"StackName": "StackName",
"Status": "Status",
"StatusReason": "StatusReason",
"Changes": [
{
"ResourceChange": {
"Action": "Action",
"LogicalResourceId": "LogicalResourceId",
"PhysicalResourceId": "PhysicalResourceId",
"Replacement": "Replacement",
"ResourceType": "ResourceType",
"Scope": "Scope",
"VerboseProperty": "VerboseProperty",
}
}
],
}

if not verbose_flag:
del response["VerboseProperty"]
del response["Changes"][0]["ResourceChange"]["VerboseProperty"]

describe_command.return_value = response

kwargs = {"args": ["update", "--change-set", "dev/vpc.yaml", "-y"]}
if verbose_flag:
kwargs["args"].append("-v")

result = self.runner.invoke(cli, **kwargs)

change_set_name = create_command.call_args[0][0]
assert "change-set" in change_set_name

wait_command.assert_called_once_with(change_set_name)
delete_command.assert_called_once_with(change_set_name)
execute_command.assert_called_once_with(change_set_name)
describe_command.assert_called_once_with(change_set_name)

output = result.output.splitlines()[0]
assert yaml.safe_load(output) == response
assert result.exit_code == 0

@pytest.mark.parametrize("yes_flag", [True, False])
def test_update_with_change_set_defunct(self, yes_flag):
create_command = self.mock_stack_actions.create_change_set
wait_command = self.mock_stack_actions.wait_for_cs_completion
delete_command = self.mock_stack_actions.delete_change_set

change_set_status = StackChangeSetStatus.DEFUNCT
wait_command.return_value = change_set_status

kwargs = {"args": ["update", "--change-set", "dev/vpc.yaml"]}
if yes_flag:
kwargs["args"].append("-y")
else:
kwargs["input"] = "y\n"

result = self.runner.invoke(cli, **kwargs)

change_set_name = create_command.call_args[0][0]
assert "change-set" in change_set_name

wait_command.assert_called_once_with(change_set_name)
delete_command.assert_called_once_with(change_set_name)

assert "Failed to create change set" in result.output
assert result.exit_code == 1

@pytest.mark.parametrize("yes_flag", [True, False])
def test_update_with_change_set_no_changes(self, yes_flag):
create_command = self.mock_stack_actions.create_change_set
wait_command = self.mock_stack_actions.wait_for_cs_completion
delete_command = self.mock_stack_actions.delete_change_set

change_set_status = StackChangeSetStatus.NO_CHANGES
wait_command.return_value = change_set_status

kwargs = {"args": ["update", "--change-set", "dev/vpc.yaml"]}
if yes_flag:
kwargs["args"].append("-y")
else:
kwargs["input"] = "y\n"

result = self.runner.invoke(cli, **kwargs)

change_set_name = create_command.call_args[0][0]
assert "change-set" in change_set_name

wait_command.assert_called_once_with(change_set_name)
delete_command.assert_called_once_with(change_set_name)

assert "No changes detected" in result.output
assert result.exit_code == 0

@pytest.mark.parametrize(
"command, ignore_dependencies",
[
Expand Down

0 comments on commit f4aecc4

Please sign in to comment.