localstack-core 4.3.1.dev6__py3-none-any.whl → 4.3.1.dev27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. localstack/services/cloudformation/engine/entities.py +18 -1
  2. localstack/services/cloudformation/engine/template_deployer.py +0 -9
  3. localstack/services/cloudformation/engine/v2/change_set_model.py +164 -35
  4. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +143 -69
  5. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +170 -0
  6. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +8 -0
  7. localstack/services/cloudformation/v2/provider.py +72 -6
  8. localstack/services/ec2/patches.py +31 -3
  9. localstack/services/kms/models.py +1 -1
  10. localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py +2 -0
  11. localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py +2 -0
  12. localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +4 -2
  13. localstack/services/lambda_/invocation/assignment.py +4 -2
  14. localstack/services/lambda_/invocation/execution_environment.py +16 -4
  15. localstack/services/lambda_/invocation/logs.py +28 -4
  16. localstack/services/lambda_/provider.py +18 -3
  17. localstack/services/lambda_/runtimes.py +15 -2
  18. localstack/services/s3/presigned_url.py +15 -11
  19. localstack/services/secretsmanager/provider.py +13 -4
  20. localstack/services/sqs/models.py +22 -3
  21. localstack/services/sqs/utils.py +16 -7
  22. localstack/services/ssm/resource_providers/aws_ssm_parameter.py +1 -5
  23. localstack/services/stepfunctions/asl/utils/json_path.py +9 -0
  24. localstack/testing/snapshots/transformer_utility.py +13 -0
  25. localstack/utils/aws/client_types.py +8 -0
  26. localstack/utils/docker_utils.py +2 -2
  27. localstack/version.py +2 -2
  28. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/METADATA +3 -3
  29. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/RECORD +37 -36
  30. localstack_core-4.3.1.dev27.dist-info/plux.json +1 -0
  31. localstack_core-4.3.1.dev6.dist-info/plux.json +0 -1
  32. {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev27.data}/scripts/localstack +0 -0
  33. {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev27.data}/scripts/localstack-supervisor +0 -0
  34. {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev27.data}/scripts/localstack.bat +0 -0
  35. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/WHEEL +0 -0
  36. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/entry_points.txt +0 -0
  37. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/licenses/LICENSE.txt +0 -0
  38. {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/top_level.txt +0 -0
@@ -7,12 +7,16 @@ import localstack.aws.api.cloudformation as cfn_api
7
7
  from localstack.services.cloudformation.engine.v2.change_set_model import (
8
8
  ChangeSetEntity,
9
9
  ChangeType,
10
+ ConditionKey,
11
+ ExportKey,
10
12
  NodeArray,
11
13
  NodeCondition,
12
14
  NodeDivergence,
13
15
  NodeIntrinsicFunction,
14
16
  NodeMapping,
15
17
  NodeObject,
18
+ NodeOutput,
19
+ NodeOutputs,
16
20
  NodeParameter,
17
21
  NodeProperties,
18
22
  NodeProperty,
@@ -26,6 +30,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
26
30
  TerminalValueModified,
27
31
  TerminalValueRemoved,
28
32
  TerminalValueUnchanged,
33
+ ValueKey,
29
34
  )
30
35
  from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
31
36
  ChangeSetModelVisitor,
@@ -47,14 +52,20 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
47
52
  _node_template: Final[NodeTemplate]
48
53
  _changes: Final[cfn_api.Changes]
49
54
  _describe_unit_cache: dict[Scope, DescribeUnit]
55
+ _include_property_values: Final[cfn_api.IncludePropertyValues | None]
50
56
 
51
- def __init__(self, node_template: NodeTemplate):
57
+ def __init__(
58
+ self,
59
+ node_template: NodeTemplate,
60
+ include_property_values: cfn_api.IncludePropertyValues | None = None,
61
+ ):
52
62
  self._node_template = node_template
53
63
  self._changes = list()
54
64
  self._describe_unit_cache = dict()
55
- self.visit(self._node_template)
65
+ self._include_property_values = include_property_values
56
66
 
57
67
  def get_changes(self) -> cfn_api.Changes:
68
+ self.visit(self._node_template)
58
69
  return self._changes
59
70
 
60
71
  @staticmethod
@@ -112,12 +123,13 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
112
123
  return parameter_unit
113
124
 
114
125
  # TODO: check for KNOWN AFTER APPLY values for logical ids coming from intrinsic functions as arguments.
115
- # node_resource = self._get_node_resource_for(
116
- # resource_name=logica_id, node_template=self._node_template
117
- # )
118
- limitation_str = "Cannot yet compute Ref values for Resources"
119
- resource_unit = DescribeUnit(before_context=limitation_str, after_context=limitation_str)
120
- return resource_unit
126
+ node_resource = self._get_node_resource_for(
127
+ resource_name=logica_id, node_template=self._node_template
128
+ )
129
+ resource_unit = self.visit(node_resource)
130
+ before_context = resource_unit.before_context
131
+ after_context = resource_unit.after_context
132
+ return DescribeUnit(before_context=before_context, after_context=after_context)
121
133
 
122
134
  def _resolve_mapping(self, map_name: str, top_level_key: str, second_level_key) -> DescribeUnit:
123
135
  # TODO: add support for nested intrinsic functions, and KNOWN AFTER APPLY logical ids.
@@ -210,29 +222,37 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
210
222
  arguments_unit = self.visit(node_intrinsic_function.arguments)
211
223
  # TODO: validate the return value according to the spec.
212
224
  before_argument_list = arguments_unit.before_context
213
- before_logical_name_of_resource = before_argument_list[0]
214
- before_attribute_name = before_argument_list[1]
215
- before_node_resource = self._get_node_resource_for(
216
- resource_name=before_logical_name_of_resource, node_template=self._node_template
217
- )
218
- node_property: TerminalValue = self._get_node_property_for(
219
- property_name=before_attribute_name, node_resource=before_node_resource
220
- )
225
+ after_argument_list = arguments_unit.after_context
226
+
227
+ before_context = None
228
+ if before_argument_list:
229
+ before_logical_name_of_resource = before_argument_list[0]
230
+ before_attribute_name = before_argument_list[1]
231
+ before_node_resource = self._get_node_resource_for(
232
+ resource_name=before_logical_name_of_resource, node_template=self._node_template
233
+ )
234
+ before_node_property = self._get_node_property_for(
235
+ property_name=before_attribute_name, node_resource=before_node_resource
236
+ )
237
+ before_property_unit = self.visit(before_node_property)
238
+ before_context = before_property_unit.before_context
221
239
 
222
- before_context = node_property.value.value
223
- if node_property.change_type != ChangeType.UNCHANGED:
240
+ after_context = None
241
+ if after_argument_list:
224
242
  after_context = CHANGESET_KNOWN_AFTER_APPLY
225
- else:
226
- after_context = node_property.value.value
243
+ # TODO: the following is the logic to resolve the attribute in the `after` template
244
+ # this should be moved to the new base class and then be masked in this describer.
245
+ # after_logical_name_of_resource = after_argument_list[0]
246
+ # after_attribute_name = after_argument_list[1]
247
+ # after_node_resource = self._get_node_resource_for(
248
+ # resource_name=after_logical_name_of_resource, node_template=self._node_template
249
+ # )
250
+ # after_node_property = self._get_node_property_for(
251
+ # property_name=after_attribute_name, node_resource=after_node_resource
252
+ # )
253
+ # after_property_unit = self.visit(after_node_property)
254
+ # after_context = after_property_unit.after_context
227
255
 
228
- match node_intrinsic_function.change_type:
229
- case ChangeType.MODIFIED:
230
- return DescribeUnit(before_context=before_context, after_context=after_context)
231
- case ChangeType.CREATED:
232
- return DescribeUnit(after_context=after_context)
233
- case ChangeType.REMOVED:
234
- return DescribeUnit(before_context=before_context)
235
- # Unchanged
236
256
  return DescribeUnit(before_context=before_context, after_context=after_context)
237
257
 
238
258
  def visit_node_intrinsic_function_fn_equals(
@@ -342,12 +362,16 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
342
362
 
343
363
  # TODO: add tests with created and deleted parameters and verify this logic holds.
344
364
  before_logical_id = arguments_unit.before_context
345
- before_unit = self._resolve_reference(logica_id=before_logical_id)
346
- before_context = before_unit.before_context
365
+ before_context = None
366
+ if before_logical_id is not None:
367
+ before_unit = self._resolve_reference(logica_id=before_logical_id)
368
+ before_context = before_unit.before_context
347
369
 
348
370
  after_logical_id = arguments_unit.after_context
349
- after_unit = self._resolve_reference(logica_id=after_logical_id)
350
- after_context = after_unit.after_context
371
+ after_context = None
372
+ if after_logical_id is not None:
373
+ after_unit = self._resolve_reference(logica_id=after_logical_id)
374
+ after_context = after_unit.after_context
351
375
 
352
376
  return DescribeUnit(before_context=before_context, after_context=after_context)
353
377
 
@@ -406,21 +430,71 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
406
430
  )
407
431
  return DescribeUnit(before_context=before_context, after_context=after_context)
408
432
 
433
+ def visit_node_output(self, node_output: NodeOutput) -> DescribeUnit:
434
+ # This logic is not required for Describe operations,
435
+ # and should be ported a new base for this class type.
436
+ change_type = node_output.change_type
437
+ value_unit = self.visit(node_output.value)
438
+
439
+ condition_unit = None
440
+ if node_output.condition_reference is not None:
441
+ condition_unit = self._resolve_resource_condition_reference(
442
+ node_output.condition_reference
443
+ )
444
+ condition_before = condition_unit.before_context
445
+ condition_after = condition_unit.after_context
446
+ if not condition_before and condition_after:
447
+ change_type = ChangeType.CREATED
448
+ elif condition_before and not condition_after:
449
+ change_type = ChangeType.REMOVED
450
+
451
+ export_unit = None
452
+ if node_output.export is not None:
453
+ export_unit = self.visit(node_output.export)
454
+
455
+ before_context = None
456
+ after_context = None
457
+ if change_type != ChangeType.REMOVED:
458
+ after_context = {"Name": node_output.name, ValueKey: value_unit.after_context}
459
+ if export_unit:
460
+ after_context[ExportKey] = export_unit.after_context
461
+ if condition_unit:
462
+ after_context[ConditionKey] = condition_unit.after_context
463
+ if change_type != ChangeType.CREATED:
464
+ before_context = {"Name": node_output.name, ValueKey: value_unit.before_context}
465
+ if export_unit:
466
+ before_context[ExportKey] = export_unit.before_context
467
+ if condition_unit:
468
+ before_context[ConditionKey] = condition_unit.before_context
469
+ return DescribeUnit(before_context=before_context, after_context=after_context)
470
+
471
+ def visit_node_outputs(self, node_outputs: NodeOutputs) -> DescribeUnit:
472
+ # This logic is not required for Describe operations,
473
+ # and should be ported a new base for this class type.
474
+ before_context = list()
475
+ after_context = list()
476
+ for node_output in node_outputs.outputs:
477
+ output_unit = self.visit(node_output)
478
+ output_before = output_unit.before_context
479
+ output_after = output_unit.after_context
480
+ if output_before:
481
+ before_context.append(output_before)
482
+ if output_after:
483
+ after_context.append(output_after)
484
+ return DescribeUnit(before_context=before_context, after_context=after_context)
485
+
409
486
  def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit:
410
- condition_unit = self._resolve_resource_condition_reference(
411
- node_resource.condition_reference
412
- )
413
- condition_before = condition_unit.before_context
414
- condition_after = condition_unit.after_context
415
- if not condition_before and condition_after:
416
- change_type = ChangeType.CREATED
417
- elif condition_before and not condition_after:
418
- change_type = ChangeType.REMOVED
419
- else:
420
- change_type = node_resource.change_type
421
- if change_type == ChangeType.UNCHANGED:
422
- # TODO
423
- return None
487
+ change_type = node_resource.change_type
488
+ if node_resource.condition_reference is not None:
489
+ condition_unit = self._resolve_resource_condition_reference(
490
+ node_resource.condition_reference
491
+ )
492
+ condition_before = condition_unit.before_context
493
+ condition_after = condition_unit.after_context
494
+ if not condition_before and condition_after:
495
+ change_type = ChangeType.CREATED
496
+ elif condition_before and not condition_after:
497
+ change_type = ChangeType.REMOVED
424
498
 
425
499
  resource_change = cfn_api.ResourceChange()
426
500
  resource_change["LogicalResourceId"] = node_resource.name
@@ -432,28 +506,28 @@ class ChangeSetModelDescriber(ChangeSetModelVisitor):
432
506
  )
433
507
 
434
508
  properties_describe_unit = self.visit(node_resource.properties)
435
- match change_type:
436
- case ChangeType.MODIFIED:
437
- resource_change["Action"] = cfn_api.ChangeAction.Modify
438
- resource_change["BeforeContext"] = properties_describe_unit.before_context
439
- resource_change["AfterContext"] = properties_describe_unit.after_context
440
- case ChangeType.CREATED:
441
- resource_change["Action"] = cfn_api.ChangeAction.Add
442
- resource_change["AfterContext"] = properties_describe_unit.after_context
443
- case ChangeType.REMOVED:
444
- resource_change["Action"] = cfn_api.ChangeAction.Remove
445
- resource_change["BeforeContext"] = properties_describe_unit.before_context
446
-
447
- self._changes.append(
448
- cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change)
449
- )
450
509
 
451
- # TODO
452
- return None
510
+ if change_type != ChangeType.UNCHANGED:
511
+ match change_type:
512
+ case ChangeType.MODIFIED:
513
+ resource_change["Action"] = cfn_api.ChangeAction.Modify
514
+ resource_change["BeforeContext"] = properties_describe_unit.before_context
515
+ resource_change["AfterContext"] = properties_describe_unit.after_context
516
+ case ChangeType.CREATED:
517
+ resource_change["Action"] = cfn_api.ChangeAction.Add
518
+ resource_change["AfterContext"] = properties_describe_unit.after_context
519
+ case ChangeType.REMOVED:
520
+ resource_change["Action"] = cfn_api.ChangeAction.Remove
521
+ resource_change["BeforeContext"] = properties_describe_unit.before_context
522
+ self._changes.append(
523
+ cfn_api.Change(Type=cfn_api.ChangeType.Resource, ResourceChange=resource_change)
524
+ )
453
525
 
454
- # def visit_node_resources(self, node_resources: NodeResources) -> DescribeUnit:
455
- # for node_resource in node_resources.resources:
456
- # if node_resource.change_type != ChangeType.UNCHANGED:
457
- # self.visit_node_resource(node_resource=node_resource)
458
- # # TODO
459
- # return None
526
+ before_context = None
527
+ after_context = None
528
+ # TODO: reconsider what is the describe unit return value for a resource type.
529
+ if change_type != ChangeType.CREATED:
530
+ before_context = node_resource.name
531
+ if change_type != ChangeType.REMOVED:
532
+ after_context = node_resource.name
533
+ return DescribeUnit(before_context=before_context, after_context=after_context)
@@ -0,0 +1,170 @@
1
+ import logging
2
+ import uuid
3
+ from typing import Final
4
+
5
+ from localstack.aws.api.cloudformation import ChangeAction
6
+ from localstack.constants import INTERNAL_AWS_SECRET_ACCESS_KEY
7
+ from localstack.services.cloudformation.engine.v2.change_set_model import (
8
+ NodeIntrinsicFunction,
9
+ NodeResource,
10
+ NodeTemplate,
11
+ TerminalValue,
12
+ )
13
+ from localstack.services.cloudformation.engine.v2.change_set_model_describer import (
14
+ ChangeSetModelDescriber,
15
+ DescribeUnit,
16
+ )
17
+ from localstack.services.cloudformation.resource_provider import (
18
+ Credentials,
19
+ OperationStatus,
20
+ ProgressEvent,
21
+ ResourceProviderExecutor,
22
+ ResourceProviderPayload,
23
+ get_resource_type,
24
+ )
25
+
26
+ LOG = logging.getLogger(__name__)
27
+
28
+
29
+ class ChangeSetModelExecutor(ChangeSetModelDescriber):
30
+ account_id: Final[str]
31
+ region: Final[str]
32
+
33
+ def __init__(
34
+ self,
35
+ node_template: NodeTemplate,
36
+ account_id: str,
37
+ region: str,
38
+ stack_name: str,
39
+ stack_id: str,
40
+ ):
41
+ super().__init__(node_template)
42
+ self.account_id = account_id
43
+ self.region = region
44
+ self.stack_name = stack_name
45
+ self.stack_id = stack_id
46
+ self.resources = {}
47
+
48
+ def execute(self) -> dict:
49
+ self.visit(self._node_template)
50
+ return self.resources
51
+
52
+ def visit_node_resource(self, node_resource: NodeResource) -> DescribeUnit:
53
+ resource_provider_executor = ResourceProviderExecutor(
54
+ stack_name=self.stack_name, stack_id=self.stack_id
55
+ )
56
+
57
+ # TODO: investigate effects on type changes
58
+ properties_describe_unit = self.visit_node_properties(node_resource.properties)
59
+ LOG.info("SRW: describe unit: %s", properties_describe_unit)
60
+
61
+ action = node_resource.change_type.to_action()
62
+ if action is None:
63
+ raise RuntimeError(
64
+ f"Action should always be present, got change type: {node_resource.change_type}"
65
+ )
66
+
67
+ # TODO
68
+ resource_type = get_resource_type({"Type": "AWS::SSM::Parameter"})
69
+ payload = self.create_resource_provider_payload(
70
+ properties_describe_unit,
71
+ action,
72
+ node_resource.name,
73
+ resource_type,
74
+ )
75
+ resource_provider = resource_provider_executor.try_load_resource_provider(resource_type)
76
+
77
+ extra_resource_properties = {}
78
+ if resource_provider is not None:
79
+ # TODO: stack events
80
+ event = resource_provider_executor.deploy_loop(
81
+ resource_provider, extra_resource_properties, payload
82
+ )
83
+ else:
84
+ event = ProgressEvent(OperationStatus.SUCCESS, resource_model={})
85
+
86
+ self.resources.setdefault(node_resource.name, {"Properties": {}})
87
+ match event.status:
88
+ case OperationStatus.SUCCESS:
89
+ # merge the resources state with the external state
90
+ # TODO: this is likely a duplicate of updating from extra_resource_properties
91
+ self.resources[node_resource.name]["Properties"].update(event.resource_model)
92
+ self.resources[node_resource.name].update(extra_resource_properties)
93
+ # XXX for legacy delete_stack compatibility
94
+ self.resources[node_resource.name]["LogicalResourceId"] = node_resource.name
95
+ self.resources[node_resource.name]["Type"] = resource_type
96
+ case any:
97
+ raise NotImplementedError(f"Event status '{any}' not handled")
98
+
99
+ return DescribeUnit(before_context=None, after_context={})
100
+
101
+ def visit_node_intrinsic_function_fn_get_att(
102
+ self, node_intrinsic_function: NodeIntrinsicFunction
103
+ ) -> DescribeUnit:
104
+ arguments_unit = self.visit(node_intrinsic_function.arguments)
105
+ before_arguments_list = arguments_unit.before_context
106
+ after_arguments_list = arguments_unit.after_context
107
+ if before_arguments_list:
108
+ logical_name_of_resource = before_arguments_list[0]
109
+ attribute_name = before_arguments_list[1]
110
+ before_node_resource = self._get_node_resource_for(
111
+ resource_name=logical_name_of_resource, node_template=self._node_template
112
+ )
113
+ node_property: TerminalValue = self._get_node_property_for(
114
+ property_name=attribute_name, node_resource=before_node_resource
115
+ )
116
+ before_context = self.visit(node_property.value).before_context
117
+ else:
118
+ before_context = None
119
+
120
+ if after_arguments_list:
121
+ logical_name_of_resource = after_arguments_list[0]
122
+ attribute_name = after_arguments_list[1]
123
+ after_node_resource = self._get_node_resource_for(
124
+ resource_name=logical_name_of_resource, node_template=self._node_template
125
+ )
126
+ node_property: TerminalValue = self._get_node_property_for(
127
+ property_name=attribute_name, node_resource=after_node_resource
128
+ )
129
+ after_context = self.visit(node_property.value).after_context
130
+ else:
131
+ after_context = None
132
+
133
+ return DescribeUnit(before_context=before_context, after_context=after_context)
134
+
135
+ def create_resource_provider_payload(
136
+ self,
137
+ describe_unit: DescribeUnit,
138
+ action: ChangeAction,
139
+ logical_resource_id: str,
140
+ resource_type: str,
141
+ ) -> ResourceProviderPayload:
142
+ # FIXME: use proper credentials
143
+ creds: Credentials = {
144
+ "accessKeyId": self.account_id,
145
+ "secretAccessKey": INTERNAL_AWS_SECRET_ACCESS_KEY,
146
+ "sessionToken": "",
147
+ }
148
+ resource_provider_payload: ResourceProviderPayload = {
149
+ "awsAccountId": self.account_id,
150
+ "callbackContext": {},
151
+ "stackId": self.stack_name,
152
+ "resourceType": resource_type,
153
+ "resourceTypeVersion": "000000",
154
+ # TODO: not actually a UUID
155
+ "bearerToken": str(uuid.uuid4()),
156
+ "region": self.region,
157
+ "action": str(action),
158
+ "requestData": {
159
+ "logicalResourceId": logical_resource_id,
160
+ "resourceProperties": describe_unit.after_context["Properties"],
161
+ "previousResourceProperties": describe_unit.before_context["Properties"],
162
+ "callerCredentials": creds,
163
+ "providerCredentials": creds,
164
+ "systemTags": {},
165
+ "previousSystemTags": {},
166
+ "stackTags": {},
167
+ "previousStackTags": {},
168
+ },
169
+ }
170
+ return resource_provider_payload
@@ -10,6 +10,8 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
10
10
  NodeMapping,
11
11
  NodeMappings,
12
12
  NodeObject,
13
+ NodeOutput,
14
+ NodeOutputs,
13
15
  NodeParameter,
14
16
  NodeParameters,
15
17
  NodeProperties,
@@ -53,6 +55,12 @@ class ChangeSetModelVisitor(abc.ABC):
53
55
  def visit_node_mappings(self, node_mappings: NodeMappings):
54
56
  self.visit_children(node_mappings)
55
57
 
58
+ def visit_node_outputs(self, node_outputs: NodeOutputs):
59
+ self.visit_children(node_outputs)
60
+
61
+ def visit_node_output(self, node_output: NodeOutput):
62
+ self.visit_children(node_output)
63
+
56
64
  def visit_node_parameters(self, node_parameters: NodeParameters):
57
65
  self.visit_children(node_parameters)
58
66
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from copy import deepcopy
2
3
 
3
4
  from localstack.aws.api import RequestContext, handler
@@ -5,12 +6,18 @@ from localstack.aws.api.cloudformation import (
5
6
  ChangeSetNameOrId,
6
7
  ChangeSetNotFoundException,
7
8
  ChangeSetType,
9
+ ClientRequestToken,
8
10
  CreateChangeSetInput,
9
11
  CreateChangeSetOutput,
10
12
  DescribeChangeSetOutput,
13
+ DisableRollback,
14
+ ExecuteChangeSetOutput,
15
+ ExecutionStatus,
11
16
  IncludePropertyValues,
17
+ InvalidChangeSetStatusException,
12
18
  NextToken,
13
19
  Parameter,
20
+ RetainExceptOnCreate,
14
21
  StackNameOrId,
15
22
  StackStatus,
16
23
  )
@@ -27,6 +34,9 @@ from localstack.services.cloudformation.engine.template_utils import resolve_sta
27
34
  from localstack.services.cloudformation.engine.v2.change_set_model_describer import (
28
35
  ChangeSetModelDescriber,
29
36
  )
37
+ from localstack.services.cloudformation.engine.v2.change_set_model_executor import (
38
+ ChangeSetModelExecutor,
39
+ )
30
40
  from localstack.services.cloudformation.engine.validations import ValidationError
31
41
  from localstack.services.cloudformation.provider import (
32
42
  ARN_CHANGESET_REGEX,
@@ -41,6 +51,8 @@ from localstack.services.cloudformation.stores import (
41
51
  )
42
52
  from localstack.utils.collections import remove_attributes
43
53
 
54
+ LOG = logging.getLogger(__name__)
55
+
44
56
 
45
57
  class CloudformationProviderV2(CloudformationProvider):
46
58
  @handler("CreateChangeSet", expand=False)
@@ -178,7 +190,12 @@ class CloudformationProviderV2(CloudformationProvider):
178
190
 
179
191
  # create change set for the stack and apply changes
180
192
  change_set = StackChangeSet(
181
- context.account_id, context.region, stack, req_params, transformed_template
193
+ context.account_id,
194
+ context.region,
195
+ stack,
196
+ req_params,
197
+ transformed_template,
198
+ change_set_type=change_set_type,
182
199
  )
183
200
  # only set parameters for the changeset, then switch to stack on execute_change_set
184
201
  change_set.template_body = template_body
@@ -233,14 +250,61 @@ class CloudformationProviderV2(CloudformationProvider):
233
250
 
234
251
  return CreateChangeSetOutput(StackId=change_set.stack_id, Id=change_set.change_set_id)
235
252
 
253
+ @handler("ExecuteChangeSet")
254
+ def execute_change_set(
255
+ self,
256
+ context: RequestContext,
257
+ change_set_name: ChangeSetNameOrId,
258
+ stack_name: StackNameOrId | None = None,
259
+ client_request_token: ClientRequestToken | None = None,
260
+ disable_rollback: DisableRollback | None = None,
261
+ retain_except_on_create: RetainExceptOnCreate | None = None,
262
+ **kwargs,
263
+ ) -> ExecuteChangeSetOutput:
264
+ change_set = find_change_set(
265
+ context.account_id,
266
+ context.region,
267
+ change_set_name,
268
+ stack_name=stack_name,
269
+ active_only=True,
270
+ )
271
+ if not change_set:
272
+ raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
273
+ if change_set.metadata.get("ExecutionStatus") != ExecutionStatus.AVAILABLE:
274
+ LOG.debug("Change set %s not in execution status 'AVAILABLE'", change_set_name)
275
+ raise InvalidChangeSetStatusException(
276
+ f"ChangeSet [{change_set.metadata['ChangeSetId']}] cannot be executed in its current status of [{change_set.metadata.get('Status')}]"
277
+ )
278
+ stack_name = change_set.stack.stack_name
279
+ LOG.debug(
280
+ 'Executing change set "%s" for stack "%s" with %s resources ...',
281
+ change_set_name,
282
+ stack_name,
283
+ len(change_set.template_resources),
284
+ )
285
+ if not change_set.update_graph:
286
+ raise RuntimeError("Programming error: no update graph found for change set")
287
+
288
+ change_set_executor = ChangeSetModelExecutor(
289
+ change_set.update_graph,
290
+ account_id=context.account_id,
291
+ region=context.region,
292
+ stack_name=change_set.stack.stack_name,
293
+ stack_id=change_set.stack.stack_id,
294
+ )
295
+ new_resources = change_set_executor.execute()
296
+ change_set.stack.set_stack_status(f"{change_set.change_set_type or 'UPDATE'}_COMPLETE")
297
+ change_set.stack.resources = new_resources
298
+ return ExecuteChangeSetOutput()
299
+
236
300
  @handler("DescribeChangeSet")
237
301
  def describe_change_set(
238
302
  self,
239
303
  context: RequestContext,
240
304
  change_set_name: ChangeSetNameOrId,
241
- stack_name: StackNameOrId = None,
242
- next_token: NextToken = None,
243
- include_property_values: IncludePropertyValues = None,
305
+ stack_name: StackNameOrId | None = None,
306
+ next_token: NextToken | None = None,
307
+ include_property_values: IncludePropertyValues | None = None,
244
308
  **kwargs,
245
309
  ) -> DescribeChangeSetOutput:
246
310
  # TODO add support for include_property_values
@@ -261,8 +325,10 @@ class CloudformationProviderV2(CloudformationProvider):
261
325
  if not change_set:
262
326
  raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
263
327
 
264
- change_set_describer = ChangeSetModelDescriber(node_template=change_set.update_graph)
265
- resource_changes = change_set_describer.get_resource_changes()
328
+ change_set_describer = ChangeSetModelDescriber(
329
+ node_template=change_set.update_graph, include_property_values=include_property_values
330
+ )
331
+ resource_changes = change_set_describer.get_changes()
266
332
 
267
333
  attrs = [
268
334
  "ChangeSetType",