localstack-core 4.7.1.dev49__py3-none-any.whl → 4.10.1.dev12__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 (253) hide show
  1. localstack/aws/api/cloudformation/__init__.py +18 -4
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +6 -2
  5. localstack/aws/api/dynamodb/__init__.py +30 -0
  6. localstack/aws/api/ec2/__init__.py +1522 -65
  7. localstack/aws/api/iam/__init__.py +7 -0
  8. localstack/aws/api/kinesis/__init__.py +19 -0
  9. localstack/aws/api/kms/__init__.py +6 -0
  10. localstack/aws/api/lambda_/__init__.py +13 -0
  11. localstack/aws/api/logs/__init__.py +15 -0
  12. localstack/aws/api/redshift/__init__.py +9 -3
  13. localstack/aws/api/route53/__init__.py +5 -0
  14. localstack/aws/api/s3/__init__.py +12 -0
  15. localstack/aws/api/s3control/__init__.py +54 -0
  16. localstack/aws/api/ssm/__init__.py +2 -0
  17. localstack/aws/api/transcribe/__init__.py +17 -0
  18. localstack/aws/client.py +7 -2
  19. localstack/aws/forwarder.py +52 -5
  20. localstack/aws/handlers/analytics.py +1 -1
  21. localstack/aws/handlers/internal_requests.py +6 -1
  22. localstack/aws/handlers/logging.py +12 -2
  23. localstack/aws/handlers/metric_handler.py +41 -1
  24. localstack/aws/handlers/service.py +40 -20
  25. localstack/aws/mocking.py +2 -2
  26. localstack/aws/patches.py +2 -2
  27. localstack/aws/protocol/parser.py +459 -32
  28. localstack/aws/protocol/serializer.py +689 -69
  29. localstack/aws/protocol/service_router.py +120 -20
  30. localstack/aws/protocol/validate.py +1 -1
  31. localstack/aws/scaffold.py +1 -1
  32. localstack/aws/skeleton.py +4 -2
  33. localstack/aws/spec-patches.json +58 -0
  34. localstack/aws/spec.py +37 -16
  35. localstack/cli/exceptions.py +1 -1
  36. localstack/cli/localstack.py +6 -6
  37. localstack/cli/lpm.py +3 -4
  38. localstack/cli/plugins.py +1 -1
  39. localstack/cli/profiles.py +1 -2
  40. localstack/config.py +25 -18
  41. localstack/constants.py +4 -29
  42. localstack/dev/kubernetes/__main__.py +130 -7
  43. localstack/dev/run/configurators.py +1 -4
  44. localstack/dev/run/paths.py +1 -1
  45. localstack/dns/plugins.py +5 -1
  46. localstack/dns/server.py +13 -4
  47. localstack/logging/format.py +3 -3
  48. localstack/packages/api.py +9 -8
  49. localstack/packages/core.py +2 -2
  50. localstack/packages/plugins.py +0 -8
  51. localstack/runtime/analytics.py +3 -0
  52. localstack/runtime/hooks.py +1 -1
  53. localstack/runtime/init.py +2 -2
  54. localstack/runtime/main.py +5 -5
  55. localstack/runtime/patches.py +2 -2
  56. localstack/services/apigateway/helpers.py +1 -4
  57. localstack/services/apigateway/legacy/helpers.py +7 -8
  58. localstack/services/apigateway/legacy/integration.py +4 -3
  59. localstack/services/apigateway/legacy/invocations.py +6 -5
  60. localstack/services/apigateway/legacy/provider.py +148 -68
  61. localstack/services/apigateway/legacy/templates.py +1 -1
  62. localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
  63. localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
  64. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  65. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  66. localstack/services/apigateway/next_gen/execute_api/template_mapping.py +2 -2
  67. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
  68. localstack/services/apigateway/next_gen/provider.py +5 -0
  69. localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
  70. localstack/services/cloudformation/api_utils.py +4 -8
  71. localstack/services/cloudformation/cfn_utils.py +1 -1
  72. localstack/services/cloudformation/engine/entities.py +14 -4
  73. localstack/services/cloudformation/engine/template_deployer.py +6 -4
  74. localstack/services/cloudformation/engine/transformers.py +6 -4
  75. localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
  76. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
  77. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
  78. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
  79. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
  80. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
  81. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  82. localstack/services/cloudformation/engine/v2/resolving.py +7 -5
  83. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  84. localstack/services/cloudformation/provider.py +7 -5
  85. localstack/services/cloudformation/resource_provider.py +7 -1
  86. localstack/services/cloudformation/resources.py +24149 -0
  87. localstack/services/cloudformation/service_models.py +2 -2
  88. localstack/services/cloudformation/v2/entities.py +19 -9
  89. localstack/services/cloudformation/v2/provider.py +336 -106
  90. localstack/services/cloudformation/v2/types.py +13 -7
  91. localstack/services/cloudformation/v2/utils.py +4 -1
  92. localstack/services/cloudwatch/alarm_scheduler.py +4 -1
  93. localstack/services/cloudwatch/provider.py +18 -13
  94. localstack/services/cloudwatch/provider_v2.py +25 -28
  95. localstack/services/dynamodb/packages.py +2 -1
  96. localstack/services/dynamodb/provider.py +42 -0
  97. localstack/services/dynamodb/server.py +2 -2
  98. localstack/services/dynamodb/v2/provider.py +42 -0
  99. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  100. localstack/services/edge.py +1 -1
  101. localstack/services/es/provider.py +2 -2
  102. localstack/services/events/event_rule_engine.py +31 -13
  103. localstack/services/events/models.py +4 -5
  104. localstack/services/events/provider.py +17 -14
  105. localstack/services/events/target.py +17 -9
  106. localstack/services/events/v1/provider.py +5 -5
  107. localstack/services/firehose/provider.py +14 -4
  108. localstack/services/iam/provider.py +11 -116
  109. localstack/services/iam/resources/policy_simulator.py +133 -0
  110. localstack/services/kinesis/models.py +15 -2
  111. localstack/services/kinesis/provider.py +86 -3
  112. localstack/services/kms/provider.py +14 -5
  113. localstack/services/lambda_/api_utils.py +6 -3
  114. localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
  115. localstack/services/lambda_/invocation/event_manager.py +1 -1
  116. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  117. localstack/services/lambda_/invocation/lambda_models.py +10 -7
  118. localstack/services/lambda_/invocation/lambda_service.py +5 -1
  119. localstack/services/lambda_/packages.py +1 -1
  120. localstack/services/lambda_/provider.py +4 -3
  121. localstack/services/lambda_/provider_utils.py +1 -1
  122. localstack/services/logs/provider.py +36 -19
  123. localstack/services/moto.py +2 -1
  124. localstack/services/opensearch/cluster.py +15 -7
  125. localstack/services/opensearch/packages.py +26 -7
  126. localstack/services/opensearch/provider.py +8 -2
  127. localstack/services/opensearch/versions.py +56 -7
  128. localstack/services/plugins.py +11 -7
  129. localstack/services/providers.py +10 -2
  130. localstack/services/redshift/provider.py +0 -21
  131. localstack/services/s3/constants.py +5 -2
  132. localstack/services/s3/cors.py +4 -4
  133. localstack/services/s3/models.py +1 -1
  134. localstack/services/s3/notifications.py +55 -39
  135. localstack/services/s3/presigned_url.py +35 -54
  136. localstack/services/s3/provider.py +73 -15
  137. localstack/services/s3/utils.py +42 -22
  138. localstack/services/s3/validation.py +46 -32
  139. localstack/services/s3/website_hosting.py +4 -2
  140. localstack/services/ses/provider.py +18 -8
  141. localstack/services/sns/constants.py +7 -1
  142. localstack/services/sns/executor.py +9 -2
  143. localstack/services/sns/provider.py +8 -5
  144. localstack/services/sns/publisher.py +31 -16
  145. localstack/services/sns/v2/models.py +167 -0
  146. localstack/services/sns/v2/provider.py +867 -0
  147. localstack/services/sns/v2/utils.py +130 -0
  148. localstack/services/sqs/constants.py +1 -1
  149. localstack/services/sqs/developer_api.py +205 -0
  150. localstack/services/sqs/models.py +48 -5
  151. localstack/services/sqs/provider.py +38 -311
  152. localstack/services/sqs/query_api.py +6 -2
  153. localstack/services/sqs/utils.py +121 -2
  154. localstack/services/ssm/provider.py +1 -1
  155. localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
  156. localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
  157. localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
  158. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
  159. localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
  160. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
  161. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
  162. localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
  163. localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
  164. localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
  165. localstack/services/stepfunctions/asl/eval/environment.py +1 -1
  166. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  167. localstack/services/stepfunctions/backend/execution.py +2 -1
  168. localstack/services/stores.py +1 -1
  169. localstack/services/transcribe/provider.py +6 -1
  170. localstack/state/codecs.py +61 -0
  171. localstack/state/core.py +11 -5
  172. localstack/state/pickle.py +10 -49
  173. localstack/testing/aws/cloudformation_utils.py +1 -1
  174. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  175. localstack/testing/pytest/cloudformation/transformers.py +0 -0
  176. localstack/testing/pytest/container.py +4 -5
  177. localstack/testing/pytest/fixtures.py +33 -31
  178. localstack/testing/pytest/in_memory_localstack.py +0 -4
  179. localstack/testing/pytest/marking.py +38 -11
  180. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  181. localstack/testing/pytest/util.py +1 -1
  182. localstack/testing/pytest/validation_tracking.py +1 -2
  183. localstack/testing/snapshots/transformer_utility.py +6 -1
  184. localstack/utils/analytics/events.py +2 -2
  185. localstack/utils/analytics/metadata.py +6 -4
  186. localstack/utils/analytics/metrics/counter.py +8 -15
  187. localstack/utils/analytics/publisher.py +1 -2
  188. localstack/utils/analytics/service_providers.py +19 -0
  189. localstack/utils/analytics/service_request_aggregator.py +2 -2
  190. localstack/utils/archives.py +11 -11
  191. localstack/utils/asyncio.py +2 -2
  192. localstack/utils/aws/arns.py +24 -29
  193. localstack/utils/aws/aws_responses.py +8 -8
  194. localstack/utils/aws/aws_stack.py +2 -3
  195. localstack/utils/aws/dead_letter_queue.py +1 -5
  196. localstack/utils/aws/message_forwarding.py +1 -2
  197. localstack/utils/aws/request_context.py +4 -5
  198. localstack/utils/aws/resources.py +1 -1
  199. localstack/utils/aws/templating.py +1 -1
  200. localstack/utils/batch_policy.py +3 -3
  201. localstack/utils/bootstrap.py +21 -13
  202. localstack/utils/catalog/catalog.py +139 -0
  203. localstack/utils/catalog/catalog_loader.py +119 -0
  204. localstack/utils/catalog/common.py +58 -0
  205. localstack/utils/catalog/plugins.py +28 -0
  206. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  207. localstack/utils/collections.py +7 -8
  208. localstack/utils/config_listener.py +1 -1
  209. localstack/utils/container_networking.py +2 -3
  210. localstack/utils/container_utils/container_client.py +135 -136
  211. localstack/utils/container_utils/docker_cmd_client.py +85 -69
  212. localstack/utils/container_utils/docker_sdk_client.py +69 -66
  213. localstack/utils/crypto.py +10 -10
  214. localstack/utils/diagnose.py +3 -4
  215. localstack/utils/docker_utils.py +9 -5
  216. localstack/utils/files.py +33 -13
  217. localstack/utils/functions.py +4 -3
  218. localstack/utils/http.py +11 -11
  219. localstack/utils/json.py +20 -6
  220. localstack/utils/kinesis/kinesis_connector.py +2 -1
  221. localstack/utils/net.py +15 -9
  222. localstack/utils/no_exit_argument_parser.py +2 -2
  223. localstack/utils/numbers.py +9 -2
  224. localstack/utils/objects.py +7 -6
  225. localstack/utils/patch.py +10 -3
  226. localstack/utils/run.py +12 -11
  227. localstack/utils/scheduler.py +11 -11
  228. localstack/utils/server/tcp_proxy.py +2 -2
  229. localstack/utils/serving.py +3 -4
  230. localstack/utils/strings.py +15 -16
  231. localstack/utils/sync.py +126 -1
  232. localstack/utils/tagging.py +8 -6
  233. localstack/utils/testutil.py +8 -8
  234. localstack/utils/threads.py +2 -2
  235. localstack/utils/time.py +12 -4
  236. localstack/utils/urls.py +1 -3
  237. localstack/utils/xray/traceid.py +1 -1
  238. localstack/version.py +16 -3
  239. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
  240. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
  241. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
  242. localstack_core-4.10.1.dev12.dist-info/plux.json +1 -0
  243. localstack/packages/terraform.py +0 -46
  244. localstack/services/cloudformation/deploy.html +0 -144
  245. localstack/services/cloudformation/deploy_ui.py +0 -47
  246. localstack/services/cloudformation/plugins.py +0 -12
  247. localstack_core-4.7.1.dev49.dist-info/plux.json +0 -1
  248. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
  249. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
  250. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
  251. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
  252. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
  253. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,7 @@ from localstack.services.cloudformation.v2.types import (
14
14
  EngineParameter,
15
15
  engine_parameter_value,
16
16
  )
17
+ from localstack.utils.json import extract_jsonpath
17
18
  from localstack.utils.strings import camel_to_snake_case
18
19
 
19
20
  T = TypeVar("T")
@@ -69,6 +70,9 @@ def parent_change_type_of(children: list[Maybe[ChangeSetEntity]]):
69
70
  change_types = [c.change_type for c in children if not is_nothing(c)]
70
71
  if not change_types:
71
72
  return ChangeType.UNCHANGED
73
+ # TODO: rework this logic. Currently if any values are different then we consider it
74
+ # modified, but e.g. if everything is unchanged or created, the result should probably be
75
+ # "created"
72
76
  first_type = change_types[0]
73
77
  if all(ct == first_type for ct in change_types):
74
78
  return first_type
@@ -106,6 +110,30 @@ class Scope(str):
106
110
  def unwrap(self) -> list[str]:
107
111
  return self.split(self._SEPARATOR)
108
112
 
113
+ @property
114
+ def parent(self) -> Scope:
115
+ return Scope(self._SEPARATOR.join(self.split(self._SEPARATOR)[:-1]))
116
+
117
+ @property
118
+ def jsonpath(self) -> str:
119
+ parts = self.split("/")
120
+ json_parts = []
121
+
122
+ for part in parts:
123
+ if not part: # Skip empty strings from leading/trailing slashes
124
+ continue
125
+
126
+ if part == "divergence":
127
+ continue
128
+
129
+ # Wrap keys with special characters (e.g., colon) in quotes
130
+ if ":" in part:
131
+ json_parts.append(f'"{part}"')
132
+ else:
133
+ json_parts.append(part)
134
+
135
+ return f"$.{'.'.join(json_parts)}"
136
+
109
137
 
110
138
  class ChangeType(enum.Enum):
111
139
  UNCHANGED = "Unchanged"
@@ -127,7 +155,7 @@ class ChangeType(enum.Enum):
127
155
 
128
156
  class ChangeSetEntity(abc.ABC):
129
157
  scope: Final[Scope]
130
- change_type: Final[ChangeType]
158
+ change_type: ChangeType
131
159
 
132
160
  def __init__(self, scope: Scope, change_type: ChangeType):
133
161
  self.scope = scope
@@ -343,11 +371,21 @@ class NodeTransform(ChangeSetNode):
343
371
 
344
372
  class NodeResources(ChangeSetNode):
345
373
  resources: Final[list[NodeResource]]
374
+ fn_transform: Final[Maybe[NodeIntrinsicFunctionFnTransform]]
375
+ fn_foreaches: Final[list[NodeForEach]]
346
376
 
347
- def __init__(self, scope: Scope, resources: list[NodeResource]):
348
- change_type = parent_change_type_of(resources)
377
+ def __init__(
378
+ self,
379
+ scope: Scope,
380
+ resources: list[NodeResource],
381
+ fn_transform: Maybe[NodeIntrinsicFunctionFnTransform],
382
+ fn_foreaches: list[NodeForEach],
383
+ ):
384
+ change_type = parent_change_type_of(resources + [fn_transform] + fn_foreaches)
349
385
  super().__init__(scope=scope, change_type=change_type)
350
386
  self.resources = resources
387
+ self.fn_transform = fn_transform
388
+ self.fn_foreaches = fn_foreaches
351
389
 
352
390
 
353
391
  class NodeResource(ChangeSetNode):
@@ -359,6 +397,7 @@ class NodeResource(ChangeSetNode):
359
397
  requires_replacement: Final[bool]
360
398
  deletion_policy: Final[Maybe[ChangeSetTerminal]]
361
399
  update_replace_policy: Final[Maybe[ChangeSetTerminal]]
400
+ fn_transform: Final[Maybe[NodeIntrinsicFunctionFnTransform]]
362
401
 
363
402
  def __init__(
364
403
  self,
@@ -372,6 +411,7 @@ class NodeResource(ChangeSetNode):
372
411
  requires_replacement: bool,
373
412
  deletion_policy: Maybe[ChangeSetTerminal],
374
413
  update_replace_policy: Maybe[ChangeSetTerminal],
414
+ fn_transform: Maybe[NodeIntrinsicFunctionFnTransform],
375
415
  ):
376
416
  super().__init__(scope=scope, change_type=change_type)
377
417
  self.name = name
@@ -382,15 +422,23 @@ class NodeResource(ChangeSetNode):
382
422
  self.requires_replacement = requires_replacement
383
423
  self.deletion_policy = deletion_policy
384
424
  self.update_replace_policy = update_replace_policy
425
+ self.fn_transform = fn_transform
385
426
 
386
427
 
387
428
  class NodeProperties(ChangeSetNode):
388
429
  properties: Final[list[NodeProperty]]
430
+ fn_transform: Final[Maybe[NodeIntrinsicFunctionFnTransform]]
389
431
 
390
- def __init__(self, scope: Scope, properties: list[NodeProperty]):
432
+ def __init__(
433
+ self,
434
+ scope: Scope,
435
+ properties: list[NodeProperty],
436
+ fn_transform: Maybe[NodeIntrinsicFunctionFnTransform],
437
+ ):
391
438
  change_type = parent_change_type_of(properties)
392
439
  super().__init__(scope=scope, change_type=change_type)
393
440
  self.properties = properties
441
+ self.fn_transform = fn_transform
394
442
 
395
443
 
396
444
  class NodeDependsOn(ChangeSetNode):
@@ -427,6 +475,40 @@ class NodeIntrinsicFunction(ChangeSetNode):
427
475
  self.arguments = arguments
428
476
 
429
477
 
478
+ class NodeIntrinsicFunctionFnTransform(NodeIntrinsicFunction):
479
+ def __init__(
480
+ self,
481
+ scope: Scope,
482
+ change_type: ChangeType,
483
+ intrinsic_function: str,
484
+ arguments: ChangeSetEntity,
485
+ before_siblings: list[Any],
486
+ after_siblings: list[Any],
487
+ ):
488
+ super().__init__(
489
+ scope=scope,
490
+ change_type=change_type,
491
+ intrinsic_function=intrinsic_function,
492
+ arguments=arguments,
493
+ )
494
+ self.before_siblings = before_siblings
495
+ self.after_siblings = after_siblings
496
+
497
+
498
+ class NodeForEach(ChangeSetNode):
499
+ def __init__(
500
+ self,
501
+ scope: Scope,
502
+ change_type: Final[ChangeType],
503
+ arguments: Final[ChangeSetEntity],
504
+ ):
505
+ super().__init__(
506
+ scope=scope,
507
+ change_type=change_type,
508
+ )
509
+ self.arguments = arguments
510
+
511
+
430
512
  class NodeObject(ChangeSetNode):
431
513
  bindings: Final[dict[str, ChangeSetEntity]]
432
514
 
@@ -598,6 +680,7 @@ class ChangeSetModel:
598
680
  arguments = self._visit_value(
599
681
  scope=arguments_scope, before_value=before_arguments, after_value=after_arguments
600
682
  )
683
+
601
684
  if is_created(before=before_arguments, after=after_arguments):
602
685
  change_type = ChangeType.CREATED
603
686
  elif is_removed(before=before_arguments, after=after_arguments):
@@ -611,15 +694,47 @@ class ChangeSetModel:
611
694
  change_type = resolve_function(arguments)
612
695
  else:
613
696
  change_type = arguments.change_type
614
- node_intrinsic_function = NodeIntrinsicFunction(
615
- scope=scope,
616
- change_type=change_type,
617
- intrinsic_function=intrinsic_function,
618
- arguments=arguments,
619
- )
697
+
698
+ if intrinsic_function == FnTransform:
699
+ if scope.count(FnTransform) > 1:
700
+ raise RuntimeError(
701
+ "Invalid: Fn::Transforms cannot be nested inside another Fn::Transform"
702
+ )
703
+
704
+ path = scope.parent.jsonpath
705
+ before_siblings = extract_jsonpath(self._before_template, path)
706
+ after_siblings = extract_jsonpath(self._after_template, path)
707
+
708
+ node_intrinsic_function = NodeIntrinsicFunctionFnTransform(
709
+ scope=scope,
710
+ change_type=change_type,
711
+ arguments=arguments,
712
+ intrinsic_function=intrinsic_function,
713
+ before_siblings=before_siblings,
714
+ after_siblings=after_siblings,
715
+ )
716
+ else:
717
+ node_intrinsic_function = NodeIntrinsicFunction(
718
+ scope=scope,
719
+ change_type=change_type,
720
+ intrinsic_function=intrinsic_function,
721
+ arguments=arguments,
722
+ )
620
723
  self._visited_scopes[scope] = node_intrinsic_function
621
724
  return node_intrinsic_function
622
725
 
726
+ def _visit_foreach(
727
+ self, scope: Scope, before_arguments: Maybe[list], after_arguments: Maybe[list]
728
+ ) -> NodeForEach:
729
+ node_foreach = self._visited_scopes.get(scope)
730
+ if isinstance(node_foreach, NodeForEach):
731
+ return node_foreach
732
+ arguments_scope = scope.open_scope("args")
733
+ arguments = self._visit_array(
734
+ arguments_scope, before_array=before_arguments, after_array=after_arguments
735
+ )
736
+ return NodeForEach(scope=scope, change_type=arguments.change_type, arguments=arguments)
737
+
623
738
  def _resolve_intrinsic_function_fn_sub(self, arguments: ChangeSetEntity) -> ChangeType:
624
739
  # TODO: This routine should instead export the implicit Ref and GetAtt calls within the first
625
740
  # string template parameter and compute the respective change set types. Currently,
@@ -667,6 +782,9 @@ class ChangeSetModel:
667
782
 
668
783
  logical_id = arguments.value
669
784
 
785
+ if isinstance(logical_id, str) and logical_id.startswith("AWS::"):
786
+ return arguments.change_type
787
+
670
788
  node_condition = self._retrieve_condition_if_exists(condition_name=logical_id)
671
789
  if isinstance(node_condition, NodeCondition):
672
790
  return node_condition.change_type
@@ -745,6 +863,7 @@ class ChangeSetModel:
745
863
  ) -> bool:
746
864
  # a bit hacky but we have to load the resource provider executor _and_ resource provider to get the schema
747
865
  # Note: we don't log the attempt to load the resource provider, we need to make sure this is only done once and we already do this in the executor
866
+
748
867
  resource_provider = ResourceProviderExecutor.try_load_resource_provider(resource_type.value)
749
868
  if not resource_provider:
750
869
  # if we don't support a resource, assume an in-place update for simplicity
@@ -889,10 +1008,18 @@ class ChangeSetModel:
889
1008
  return node_properties
890
1009
  property_names: list[str] = self._safe_keys_of(before_properties, after_properties)
891
1010
  properties: list[NodeProperty] = []
1011
+ fn_transform = Nothing
1012
+
892
1013
  for property_name in property_names:
893
1014
  property_scope, (before_property, after_property) = self._safe_access_in(
894
1015
  scope, property_name, before_properties, after_properties
895
1016
  )
1017
+ if property_name == FnTransform:
1018
+ fn_transform = self._visit_intrinsic_function(
1019
+ property_scope, FnTransform, before_property, after_property
1020
+ )
1021
+ continue
1022
+
896
1023
  property_ = self._visit_property(
897
1024
  scope=property_scope,
898
1025
  property_name=property_name,
@@ -900,7 +1027,10 @@ class ChangeSetModel:
900
1027
  after_property=after_property,
901
1028
  )
902
1029
  properties.append(property_)
903
- node_properties = NodeProperties(scope=scope, properties=properties)
1030
+
1031
+ node_properties = NodeProperties(
1032
+ scope=scope, properties=properties, fn_transform=fn_transform
1033
+ )
904
1034
  self._visited_scopes[scope] = node_properties
905
1035
  return node_properties
906
1036
 
@@ -1000,10 +1130,44 @@ class ChangeSetModel:
1000
1130
  after_update_replace_policy,
1001
1131
  )
1002
1132
 
1133
+ fn_transform = Nothing
1134
+ scope_fn_transform, (before_fn_transform_args, after_fn_transform_args) = (
1135
+ self._safe_access_in(scope, FnTransform, before_resource, after_resource)
1136
+ )
1137
+ if not is_nothing(before_fn_transform_args) or not is_nothing(after_fn_transform_args):
1138
+ if scope_fn_transform.count(FnTransform) > 1:
1139
+ raise RuntimeError(
1140
+ "Invalid: Fn::Transforms cannot be nested inside another Fn::Transform"
1141
+ )
1142
+ path = "$" + ".".join(scope_fn_transform.split("/")[:-1])
1143
+ before_siblings = extract_jsonpath(self._before_template, path)
1144
+ after_siblings = extract_jsonpath(self._after_template, path)
1145
+ arguments_scope = scope.open_scope("args")
1146
+ arguments = self._visit_value(
1147
+ scope=arguments_scope,
1148
+ before_value=before_fn_transform_args,
1149
+ after_value=after_fn_transform_args,
1150
+ )
1151
+ fn_transform = NodeIntrinsicFunctionFnTransform(
1152
+ scope=scope_fn_transform,
1153
+ change_type=ChangeType.MODIFIED, # TODO
1154
+ arguments=arguments, # TODO
1155
+ intrinsic_function=FnTransform,
1156
+ before_siblings=before_siblings,
1157
+ after_siblings=after_siblings,
1158
+ )
1159
+
1003
1160
  change_type = change_type_of(
1004
1161
  before_resource,
1005
1162
  after_resource,
1006
- [properties, condition_reference, depends_on, deletion_policy, update_replace_policy],
1163
+ [
1164
+ properties,
1165
+ condition_reference,
1166
+ depends_on,
1167
+ deletion_policy,
1168
+ update_replace_policy,
1169
+ fn_transform,
1170
+ ],
1007
1171
  )
1008
1172
  requires_replacement = self._resolve_requires_replacement(
1009
1173
  node_properties=properties, resource_type=terminal_value_type
@@ -1019,6 +1183,7 @@ class ChangeSetModel:
1019
1183
  requires_replacement=requires_replacement,
1020
1184
  deletion_policy=deletion_policy,
1021
1185
  update_replace_policy=update_replace_policy,
1186
+ fn_transform=fn_transform,
1022
1187
  )
1023
1188
  self._visited_scopes[scope] = node_resource
1024
1189
  return node_resource
@@ -1029,10 +1194,28 @@ class ChangeSetModel:
1029
1194
  # TODO: investigate type changes behavior.
1030
1195
  resources: list[NodeResource] = []
1031
1196
  resource_names = self._safe_keys_of(before_resources, after_resources)
1197
+ fn_transform = Nothing
1198
+ fn_foreaches = []
1032
1199
  for resource_name in resource_names:
1033
1200
  resource_scope, (before_resource, after_resource) = self._safe_access_in(
1034
1201
  scope, resource_name, before_resources, after_resources
1035
1202
  )
1203
+ if resource_name == FnTransform:
1204
+ fn_transform = self._visit_intrinsic_function(
1205
+ scope=resource_scope,
1206
+ intrinsic_function=resource_name,
1207
+ before_arguments=before_resource,
1208
+ after_arguments=after_resource,
1209
+ )
1210
+ continue
1211
+ elif resource_name.startswith("Fn::ForEach"):
1212
+ fn_for_each = self._visit_foreach(
1213
+ scope=resource_scope,
1214
+ before_arguments=before_resource,
1215
+ after_arguments=after_resource,
1216
+ )
1217
+ fn_foreaches.append(fn_for_each)
1218
+ continue
1036
1219
  resource = self._visit_resource(
1037
1220
  scope=resource_scope,
1038
1221
  resource_name=resource_name,
@@ -1040,7 +1223,12 @@ class ChangeSetModel:
1040
1223
  after_resource=after_resource,
1041
1224
  )
1042
1225
  resources.append(resource)
1043
- return NodeResources(scope=scope, resources=resources)
1226
+ return NodeResources(
1227
+ scope=scope,
1228
+ resources=resources,
1229
+ fn_transform=fn_transform,
1230
+ fn_foreaches=fn_foreaches,
1231
+ )
1044
1232
 
1045
1233
  def _visit_mapping(
1046
1234
  self, scope: Scope, name: str, before_mapping: Maybe[dict], after_mapping: Maybe[dict]
@@ -20,6 +20,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model_preproc impor
20
20
  PreprocResource,
21
21
  )
22
22
  from localstack.services.cloudformation.v2.entities import ChangeSet
23
+ from localstack.utils.numbers import is_number
23
24
 
24
25
  CHANGESET_KNOWN_AFTER_APPLY: Final[str] = "{{changeSet:KNOWN_AFTER_APPLY}}"
25
26
 
@@ -96,11 +97,27 @@ class ChangeSetModelDescriber(ChangeSetModelPreproc):
96
97
 
97
98
  return value
98
99
 
100
+ def visit_node_intrinsic_function(self, node_intrinsic_function: NodeIntrinsicFunction):
101
+ """
102
+ Intrinsic function results are always strings when referring to the describe output
103
+ """
104
+ # TODO: what about other places?
105
+ # TODO: should this be put in the preproc?
106
+ delta = super().visit_node_intrinsic_function(node_intrinsic_function)
107
+ if is_number(delta.before):
108
+ delta.before = str(delta.before)
109
+ if is_number(delta.after):
110
+ delta.after = str(delta.after)
111
+ return delta
112
+
99
113
  def visit_node_intrinsic_function_fn_join(
100
114
  self, node_intrinsic_function: NodeIntrinsicFunction
101
115
  ) -> PreprocEntityDelta:
102
- # TODO: investigate the behaviour and impact of this logic with the user defining
103
- # {{changeSet:KNOWN_AFTER_APPLY}} string literals as delimiters or arguments.
116
+ delta_args = super().visit(node_intrinsic_function.arguments)
117
+ if isinstance(delta_args.after, list) and CHANGESET_KNOWN_AFTER_APPLY in delta_args.after:
118
+ delta_args.after = CHANGESET_KNOWN_AFTER_APPLY
119
+ return delta_args
120
+
104
121
  delta = super().visit_node_intrinsic_function_fn_join(
105
122
  node_intrinsic_function=node_intrinsic_function
106
123
  )
@@ -112,6 +129,30 @@ class ChangeSetModelDescriber(ChangeSetModelPreproc):
112
129
  delta.after = CHANGESET_KNOWN_AFTER_APPLY
113
130
  return delta
114
131
 
132
+ def visit_node_intrinsic_function_fn_select(
133
+ self, node_intrinsic_function: NodeIntrinsicFunction
134
+ ):
135
+ # TODO: should this not _ALWAYS_ return CHANGESET_KNOWN_AFTER_APPLY?
136
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
137
+ delta = PreprocEntityDelta()
138
+ if not is_nothing(arguments_delta.before):
139
+ idx = arguments_delta.before[0]
140
+ arr = arguments_delta.before[1]
141
+ try:
142
+ delta.before = arr[int(idx)]
143
+ except Exception:
144
+ delta.before = CHANGESET_KNOWN_AFTER_APPLY
145
+
146
+ if not is_nothing(arguments_delta.after):
147
+ idx = arguments_delta.after[0]
148
+ arr = arguments_delta.after[1]
149
+ try:
150
+ delta.after = arr[int(idx)]
151
+ except Exception:
152
+ delta.after = CHANGESET_KNOWN_AFTER_APPLY
153
+
154
+ return delta
155
+
115
156
  def _register_resource_change(
116
157
  self,
117
158
  logical_id: str,
@@ -240,6 +281,14 @@ class ChangeSetModelDescriber(ChangeSetModelPreproc):
240
281
  export_name = node_intrinsic_function.arguments.value
241
282
 
242
283
  self._change_set.status_reason = f"[WARN] --include-property-values option can return incomplete ChangeSet data because: ChangeSet creation failed for resource [{resource_name}] because: No export named {export_name}"
243
- delta.after = "{{changeSet:KNOWN_AFTER_APPLY}}"
284
+ delta.after = CHANGESET_KNOWN_AFTER_APPLY
244
285
 
245
286
  return delta
287
+
288
+ def visit_node_intrinsic_function_fn_split(
289
+ self, node_intrinsic_function: NodeIntrinsicFunction
290
+ ) -> PreprocEntityDelta:
291
+ delta = super().visit_node_intrinsic_function_fn_split(node_intrinsic_function)
292
+ if isinstance(delta.after, list) and ":".join(delta.after) == CHANGESET_KNOWN_AFTER_APPLY:
293
+ delta.after = [CHANGESET_KNOWN_AFTER_APPLY]
294
+ return delta