Skip to content

Commit 486bb1d

Browse files
committed
Implement update stack
1 parent 7dffb23 commit 486bb1d

File tree

2 files changed

+109
-1
lines changed
  • localstack-core/localstack/services/cloudformation/v2
  • tests/aws/services/cloudformation/v2/ported_from_v1/api

2 files changed

+109
-1
lines changed

localstack-core/localstack/services/cloudformation/v2/provider.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,14 @@
3636
StackName,
3737
StackNameOrId,
3838
StackStatus,
39+
UpdateStackInput,
40+
UpdateStackOutput,
3941
)
4042
from localstack.services.cloudformation import api_utils
4143
from localstack.services.cloudformation.engine import template_preparer
4244
from localstack.services.cloudformation.engine.v2.change_set_model import (
4345
ChangeSetModel,
46+
ChangeType,
4447
NodeTemplate,
4548
)
4649
from localstack.services.cloudformation.engine.v2.change_set_model_describer import (
@@ -562,6 +565,112 @@ def describe_stack_events(
562565
raise StackNotFoundError(stack_name)
563566
return DescribeStackEventsOutput(StackEvents=stack.events)
564567

568+
@handler("UpdateStack", expand=False)
569+
def update_stack(
570+
self,
571+
context: RequestContext,
572+
request: UpdateStackInput,
573+
) -> UpdateStackOutput:
574+
try:
575+
stack_name = request["StackName"]
576+
except KeyError:
577+
# TODO: proper exception
578+
raise ValidationError("StackName must be specified")
579+
state = get_cloudformation_store(context.account_id, context.region)
580+
template_body = request.get("TemplateBody")
581+
# s3 or secretsmanager url
582+
template_url = request.get("TemplateURL")
583+
584+
# validate and resolve template
585+
if template_body and template_url:
586+
raise ValidationError(
587+
"Specify exactly one of 'TemplateBody' or 'TemplateUrl'"
588+
) # TODO: check proper message
589+
590+
if not template_body and not template_url:
591+
raise ValidationError(
592+
"Specify exactly one of 'TemplateBody' or 'TemplateUrl'"
593+
) # TODO: check proper message
594+
595+
template_body = api_utils.extract_template_body(request)
596+
structured_template = template_preparer.parse_template(template_body)
597+
598+
# this is intentionally not in a util yet. Let's first see how the different operations deal with these before generalizing
599+
# handle ARN stack_name here (not valid for initial CREATE, since stack doesn't exist yet)
600+
stack: Stack
601+
if is_stack_arn(stack_name):
602+
stack = state.stacks_v2.get(stack_name)
603+
if not stack:
604+
raise ValidationError(f"Stack '{stack_name}' does not exist.")
605+
606+
else:
607+
# stack name specified, so fetch the stack by name
608+
stack_candidates: list[Stack] = [
609+
s for stack_arn, s in state.stacks_v2.items() if s.stack_name == stack_name
610+
]
611+
active_stack_candidates = [
612+
s for s in stack_candidates if self._stack_status_is_active(s.status)
613+
]
614+
615+
if not active_stack_candidates:
616+
raise ValidationError(f"Stack '{stack_name}' does not exist.")
617+
elif len(active_stack_candidates) > 1:
618+
raise RuntimeError("Multiple stacks matched, update matching logic")
619+
stack = active_stack_candidates[0]
620+
621+
# TODO: proper status modeling
622+
before_parameters = stack.resolved_parameters
623+
# TODO: reconsider the way parameters are modelled in the update graph process.
624+
# The options might be reduce to using the current style, or passing the extra information
625+
# as a metadata object. The choice should be made considering when the extra information
626+
# is needed for the update graph building, or only looked up in downstream tasks (metadata).
627+
request_parameters = request.get("Parameters", list())
628+
# TODO: handle parameter defaults and resolution
629+
after_parameters: dict[str, Any] = {
630+
parameter["ParameterKey"]: parameter["ParameterValue"]
631+
for parameter in request_parameters
632+
}
633+
before_template = stack.template
634+
after_template = structured_template
635+
636+
change_set = ChangeSet(
637+
stack,
638+
{"ChangeSetName": f"cs-{stack_name}-create", "ChangeSetType": ChangeSetType.CREATE},
639+
template=after_template,
640+
)
641+
self._setup_change_set_model(
642+
change_set=change_set,
643+
before_template=before_template,
644+
after_template=after_template,
645+
before_parameters=before_parameters,
646+
after_parameters=after_parameters,
647+
)
648+
649+
if change_set.update_model.change_type == ChangeType.UNCHANGED:
650+
raise ValidationError("No updates are to be performed.")
651+
652+
stack.set_stack_status(StackStatus.UPDATE_IN_PROGRESS)
653+
change_set_executor = ChangeSetModelExecutor(change_set)
654+
655+
def _run(*args):
656+
try:
657+
result = change_set_executor.execute()
658+
stack.set_stack_status(StackStatus.UPDATE_COMPLETE)
659+
stack.resolved_resources = result.resources
660+
stack.resolved_parameters = result.parameters
661+
stack.resolved_outputs = result.outputs
662+
# if the deployment succeeded, update the stack's template representation to that
663+
# which was just deployed
664+
stack.template = change_set.template
665+
except Exception as e:
666+
LOG.error("Update Stack failed: %s", e, exc_info=LOG.isEnabledFor(logging.WARNING))
667+
stack.set_stack_status(StackStatus.UPDATE_FAILED)
668+
669+
start_worker_thread(_run)
670+
671+
# TODO: stack id
672+
return UpdateStackOutput(StackId=stack.stack_id)
673+
565674
@handler("DeleteStack")
566675
def delete_stack(
567676
self,

tests/aws/services/cloudformation/v2/ported_from_v1/api/test_stacks.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,6 @@ def test_list_stack_resources_for_removed_resource(self, deploy_cfn_template, aw
261261
statuses = {res["ResourceStatus"] for res in resources}
262262
assert statuses == {"UPDATE_COMPLETE"}
263263

264-
@pytest.mark.skip(reason="CFNV2:Validation")
265264
@markers.aws.validated
266265
def test_update_stack_with_same_template_withoutchange(
267266
self, deploy_cfn_template, aws_client, snapshot

0 commit comments

Comments
 (0)