|
36 | 36 | StackName, |
37 | 37 | StackNameOrId, |
38 | 38 | StackStatus, |
| 39 | + UpdateStackInput, |
| 40 | + UpdateStackOutput, |
39 | 41 | ) |
40 | 42 | from localstack.services.cloudformation import api_utils |
41 | 43 | from localstack.services.cloudformation.engine import template_preparer |
42 | 44 | from localstack.services.cloudformation.engine.v2.change_set_model import ( |
43 | 45 | ChangeSetModel, |
| 46 | + ChangeType, |
44 | 47 | NodeTemplate, |
45 | 48 | ) |
46 | 49 | from localstack.services.cloudformation.engine.v2.change_set_model_describer import ( |
@@ -562,6 +565,112 @@ def describe_stack_events( |
562 | 565 | raise StackNotFoundError(stack_name) |
563 | 566 | return DescribeStackEventsOutput(StackEvents=stack.events) |
564 | 567 |
|
| 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 | + |
565 | 674 | @handler("DeleteStack") |
566 | 675 | def delete_stack( |
567 | 676 | self, |
|
0 commit comments