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.
- localstack/services/cloudformation/engine/entities.py +18 -1
- localstack/services/cloudformation/engine/template_deployer.py +0 -9
- localstack/services/cloudformation/engine/v2/change_set_model.py +164 -35
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +143 -69
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +170 -0
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +8 -0
- localstack/services/cloudformation/v2/provider.py +72 -6
- localstack/services/ec2/patches.py +31 -3
- localstack/services/kms/models.py +1 -1
- localstack/services/lambda_/event_source_mapping/pollers/dynamodb_poller.py +2 -0
- localstack/services/lambda_/event_source_mapping/pollers/kinesis_poller.py +2 -0
- localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +4 -2
- localstack/services/lambda_/invocation/assignment.py +4 -2
- localstack/services/lambda_/invocation/execution_environment.py +16 -4
- localstack/services/lambda_/invocation/logs.py +28 -4
- localstack/services/lambda_/provider.py +18 -3
- localstack/services/lambda_/runtimes.py +15 -2
- localstack/services/s3/presigned_url.py +15 -11
- localstack/services/secretsmanager/provider.py +13 -4
- localstack/services/sqs/models.py +22 -3
- localstack/services/sqs/utils.py +16 -7
- localstack/services/ssm/resource_providers/aws_ssm_parameter.py +1 -5
- localstack/services/stepfunctions/asl/utils/json_path.py +9 -0
- localstack/testing/snapshots/transformer_utility.py +13 -0
- localstack/utils/aws/client_types.py +8 -0
- localstack/utils/docker_utils.py +2 -2
- localstack/version.py +2 -2
- {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/METADATA +3 -3
- {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/RECORD +37 -36
- localstack_core-4.3.1.dev27.dist-info/plux.json +1 -0
- localstack_core-4.3.1.dev6.dist-info/plux.json +0 -1
- {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev27.data}/scripts/localstack +0 -0
- {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev27.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.3.1.dev6.data → localstack_core-4.3.1.dev27.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/WHEEL +0 -0
- {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.3.1.dev6.dist-info → localstack_core-4.3.1.dev27.dist-info}/licenses/LICENSE.txt +0 -0
- {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__(
|
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.
|
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
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
-
|
223
|
-
if
|
240
|
+
after_context = None
|
241
|
+
if after_argument_list:
|
224
242
|
after_context = CHANGESET_KNOWN_AFTER_APPLY
|
225
|
-
|
226
|
-
|
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
|
-
|
346
|
-
|
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
|
-
|
350
|
-
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
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
|
-
|
452
|
-
|
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
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
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,
|
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(
|
265
|
-
|
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",
|