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
@@ -1,24 +1,36 @@
1
1
  import copy
2
+ import json
2
3
  import logging
3
4
  import os
5
+ import re
4
6
  from typing import Any, Final, TypedDict
5
7
 
6
8
  import boto3
7
- from botocore.exceptions import ClientError
9
+ import jsonpath_ng
10
+ from botocore.exceptions import ClientError, ParamValidationError
8
11
  from samtranslator.translator.transform import transform as transform_sam
9
12
 
10
13
  from localstack.aws.connect import connect_to
14
+ from localstack.services.cloudformation.engine.parameters import StackParameter
11
15
  from localstack.services.cloudformation.engine.policy_loader import create_policy_loader
12
16
  from localstack.services.cloudformation.engine.template_preparer import parse_template
13
17
  from localstack.services.cloudformation.engine.transformers import (
14
18
  FailedTransformationException,
15
- execute_macro,
19
+ ResolveRefsRecursivelyContext,
20
+ apply_language_extensions_transform,
16
21
  )
17
22
  from localstack.services.cloudformation.engine.v2.change_set_model import (
18
23
  ChangeType,
24
+ FnTransform,
19
25
  Maybe,
26
+ NodeForEach,
20
27
  NodeGlobalTransform,
21
- NodeParameter,
28
+ NodeIntrinsicFunction,
29
+ NodeIntrinsicFunctionFnTransform,
30
+ NodeProperties,
31
+ NodeProperty,
32
+ NodeResource,
33
+ NodeResources,
22
34
  NodeTransform,
23
35
  Nothing,
24
36
  Scope,
@@ -27,10 +39,14 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
27
39
  from localstack.services.cloudformation.engine.v2.change_set_model_preproc import (
28
40
  ChangeSetModelPreproc,
29
41
  PreprocEntityDelta,
42
+ PreprocProperties,
30
43
  )
44
+ from localstack.services.cloudformation.engine.validations import ValidationError
31
45
  from localstack.services.cloudformation.stores import get_cloudformation_store
32
46
  from localstack.services.cloudformation.v2.entities import ChangeSet
47
+ from localstack.services.cloudformation.v2.types import EngineParameter, engine_parameter_value
33
48
  from localstack.utils import testutil
49
+ from localstack.utils.strings import long_uid
34
50
 
35
51
  LOG = logging.getLogger(__name__)
36
52
 
@@ -42,6 +58,20 @@ INCLUDE_TRANSFORM = "AWS::Include"
42
58
  _SCOPE_TRANSFORM_TEMPLATE_OUTCOME: Final[Scope] = Scope("TRANSFORM_TEMPLATE_OUTCOME")
43
59
 
44
60
 
61
+ def engine_parameters_to_stack_parameters(
62
+ engine_parameters: dict[str, EngineParameter],
63
+ ) -> dict[str, StackParameter]:
64
+ out = {}
65
+ for name, engine_param in engine_parameters.items():
66
+ out[name] = StackParameter(
67
+ ParameterKey=name,
68
+ ParameterValue=engine_parameter_value(engine_param),
69
+ ResolvedValue=engine_param.get("resolved_value"),
70
+ ParameterType=engine_param["type_"],
71
+ )
72
+ return out
73
+
74
+
45
75
  # TODO: evaluate the use of subtypes to represent and validate types of transforms
46
76
  class GlobalTransform:
47
77
  name: str
@@ -60,8 +90,8 @@ class TransformPreprocParameter(TypedDict):
60
90
 
61
91
 
62
92
  class ChangeSetModelTransform(ChangeSetModelPreproc):
63
- _before_parameters: Final[dict]
64
- _after_parameters: Final[dict]
93
+ _before_parameters: Final[dict[str, EngineParameter] | None]
94
+ _after_parameters: Final[dict[str, EngineParameter] | None]
65
95
  _before_template: Final[Maybe[dict]]
66
96
  _after_template: Final[Maybe[dict]]
67
97
 
@@ -79,44 +109,13 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
79
109
  self._before_template = before_template or Nothing
80
110
  self._after_template = after_template or Nothing
81
111
 
82
- def visit_node_parameter(
83
- self, node_parameter: NodeParameter
84
- ) -> PreprocEntityDelta[
85
- dict[str, TransformPreprocParameter], dict[str, TransformPreprocParameter]
86
- ]:
87
- # Enable compatability with v1 util.
88
- # TODO: port v1's SSM parameter resolution
89
-
90
- parameter_value_delta = super().visit_node_parameter(node_parameter=node_parameter)
91
- parameter_value_before = parameter_value_delta.before
92
- parameter_value_after = parameter_value_delta.after
93
-
94
- parameter_type_delta = self.visit(node_parameter.type_)
95
- parameter_type_before = parameter_type_delta.before
96
- parameter_type_after = parameter_type_delta.after
97
-
98
- parameter_key = node_parameter.name
99
-
100
- before = Nothing
101
- if not is_nothing(parameter_value_before):
102
- before = TransformPreprocParameter(
103
- ParameterKey=parameter_key,
104
- ParameterValue=parameter_value_before,
105
- ParameterType=parameter_type_before
106
- if not is_nothing(parameter_type_before)
107
- else None,
108
- )
109
- after = Nothing
110
- if not is_nothing(parameter_value_after):
111
- after = TransformPreprocParameter(
112
- ParameterKey=parameter_key,
113
- ParameterValue=parameter_value_after,
114
- ParameterType=parameter_type_after
115
- if not is_nothing(parameter_type_after)
116
- else None,
117
- )
112
+ def transform(self) -> tuple[dict, dict]:
113
+ self._setup_runtime_cache()
114
+ self._execute_local_transforms()
115
+ transformed_before_template, transformed_after_template = self._execute_global_transforms()
116
+ self._save_runtime_cache()
118
117
 
119
- return PreprocEntityDelta(before=before, after=after)
118
+ return transformed_before_template, transformed_after_template
120
119
 
121
120
  # Ported from v1:
122
121
  @staticmethod
@@ -143,102 +142,92 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
143
142
  if region_before is not None:
144
143
  os.environ["AWS_DEFAULT_REGION"] = region_before
145
144
 
146
- @staticmethod
147
- def _apply_global_include(
148
- global_transform: GlobalTransform, template: dict, parameters: dict, account_id, region_name
149
- ) -> dict:
150
- location = global_transform.parameters.get("Location")
145
+ def _compute_include_transform(self, parameters: dict, fragment: dict) -> dict:
146
+ location = parameters.get("Location")
151
147
  if not location or not location.startswith("s3://"):
152
148
  raise FailedTransformationException(
153
149
  transformation=INCLUDE_TRANSFORM,
154
- message="Unexpected Location parameter for AWS::Include transformer: %s" % location,
150
+ message=f"Unexpected Location parameter for AWS::Include transformer: {location}",
155
151
  )
156
152
 
157
- s3_client = connect_to(aws_access_key_id=account_id, region_name=region_name).s3
153
+ s3_client = connect_to(
154
+ aws_access_key_id=self._change_set.account_id, region_name=self._change_set.region_name
155
+ ).s3
158
156
  bucket, _, path = location.removeprefix("s3://").partition("/")
159
157
  try:
160
158
  content = testutil.download_s3_object(s3_client, bucket, path)
161
159
  except ClientError:
162
160
  raise FailedTransformationException(
163
161
  transformation=INCLUDE_TRANSFORM,
164
- message="Error downloading S3 object '%s/%s'" % (bucket, path),
162
+ message=f"Error downloading S3 object '{bucket}/{path}'",
165
163
  )
166
164
  try:
167
165
  template_to_include = parse_template(content)
168
166
  except Exception as e:
169
167
  raise FailedTransformationException(transformation=INCLUDE_TRANSFORM, message=str(e))
170
- return {**template, **template_to_include}
171
168
 
172
- @staticmethod
173
- def _apply_global_macro_transformation(
174
- account_id: str,
175
- region_name,
176
- global_transform: GlobalTransform,
177
- template: dict,
178
- parameters: dict,
179
- ) -> dict | None:
180
- macro_name = global_transform.name
181
- macros_store = get_cloudformation_store(
182
- account_id=account_id, region_name=region_name
183
- ).macros
184
- macro = macros_store.get(macro_name)
185
- if macro is None:
186
- raise RuntimeError(f"No definitions for global transform '{macro_name}'")
187
- transformation_parameters = global_transform.parameters or {}
188
- transformed_template = execute_macro(
189
- account_id,
190
- region_name,
191
- parsed_template=template,
192
- macro=macro,
193
- stack_parameters=parameters,
194
- transformation_parameters=transformation_parameters,
195
- )
196
- # The type annotation on the v1 util appears to be incorrect.
197
- return transformed_template # noqa
169
+ return {**fragment, **template_to_include}
198
170
 
199
171
  def _apply_global_transform(
200
- self, global_transform: GlobalTransform, template: dict, parameters: dict
172
+ self,
173
+ global_transform: GlobalTransform,
174
+ template: dict,
175
+ parameters: dict[str, EngineParameter],
201
176
  ) -> dict:
202
177
  transform_name = global_transform.name
203
178
  if transform_name == EXTENSIONS_TRANSFORM:
204
- # Applied lazily in downstream tasks (see ChangeSetModelPreproc).
205
- transformed_template = template
179
+ resources = template["Resources"]
180
+ mappings = template.get("Mappings", {})
181
+ conditions = template.get("Conditions", {})
182
+
183
+ resolve_context = ResolveRefsRecursivelyContext(
184
+ self._change_set.account_id,
185
+ self._change_set.region_name,
186
+ self._change_set.stack.stack_name,
187
+ resources,
188
+ mappings,
189
+ conditions,
190
+ parameters=engine_parameters_to_stack_parameters(parameters),
191
+ )
192
+ transformed_template = apply_language_extensions_transform(template, resolve_context)
206
193
  elif transform_name == SERVERLESS_TRANSFORM:
194
+ # serverless transform just requires the key/value pairs
195
+ serverless_parameters = {}
196
+ for name, param in parameters.items():
197
+ serverless_parameters[name] = param.get("resolved_value") or engine_parameter_value(
198
+ param
199
+ )
207
200
  transformed_template = self._apply_global_serverless_transformation(
208
201
  region_name=self._change_set.region_name,
209
202
  template=template,
210
- parameters=parameters,
203
+ parameters=serverless_parameters,
211
204
  )
212
205
  elif transform_name == SECRETSMANAGER_TRANSFORM:
213
206
  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/transform-aws-secretsmanager.html
214
207
  LOG.warning("%s is not yet supported. Ignoring.", SECRETSMANAGER_TRANSFORM)
215
208
  transformed_template = template
216
209
  elif transform_name == INCLUDE_TRANSFORM:
217
- transformed_template = self._apply_global_include(
218
- global_transform=global_transform,
219
- region_name=self._change_set.region_name,
220
- account_id=self._change_set.account_id,
221
- template=template,
222
- parameters=parameters,
210
+ transformed_template = self._compute_include_transform(
211
+ parameters=global_transform.parameters,
212
+ fragment=template,
223
213
  )
224
214
  else:
225
- transformed_template = self._apply_global_macro_transformation(
226
- account_id=self._change_set.account_id,
227
- region_name=self._change_set.region_name,
228
- global_transform=global_transform,
229
- template=template,
230
- parameters=parameters,
215
+ transformed_template = self._invoke_macro(
216
+ name=global_transform.name,
217
+ parameters=global_transform.parameters
218
+ if not is_nothing(global_transform.parameters)
219
+ else {},
220
+ fragment=template,
221
+ allow_string=False,
231
222
  )
232
223
  return transformed_template
233
224
 
234
- def transform(self) -> tuple[dict, dict]:
235
- self._setup_runtime_cache()
236
-
225
+ def _execute_local_transforms(self):
237
226
  node_template = self._change_set.update_model.node_template
227
+ self.visit_node_resources(node_template.resources)
238
228
 
239
- parameters_delta = self.visit_node_parameters(node_template.parameters)
240
- parameters_before = parameters_delta.before
241
- parameters_after = parameters_delta.after
229
+ def _execute_global_transforms(self) -> tuple[dict, dict]:
230
+ node_template = self._change_set.update_model.node_template
242
231
 
243
232
  transform_delta: PreprocEntityDelta[list[GlobalTransform], list[GlobalTransform]] = (
244
233
  self.visit_node_transform(node_template.transform)
@@ -248,33 +237,36 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
248
237
 
249
238
  transformed_before_template = self._before_template
250
239
  if transform_before and not is_nothing(self._before_template):
251
- transformed_before_template = self._before_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME)
252
- if not transformed_before_template:
253
- transformed_before_template = self._before_template
240
+ if _SCOPE_TRANSFORM_TEMPLATE_OUTCOME in self._before_cache:
241
+ transformed_before_template = self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME]
242
+ else:
254
243
  for before_global_transform in transform_before:
255
244
  if not is_nothing(before_global_transform.name):
256
245
  transformed_before_template = self._apply_global_transform(
257
246
  global_transform=before_global_transform,
258
- parameters=parameters_before,
247
+ parameters=self._before_parameters,
259
248
  template=transformed_before_template,
260
249
  )
250
+
251
+ # Macro transformations won't remove the transform from the template
252
+ if "Transform" in transformed_before_template:
253
+ transformed_before_template.pop("Transform")
261
254
  self._before_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_before_template
262
255
 
263
256
  transformed_after_template = self._after_template
264
257
  if transform_after and not is_nothing(self._after_template):
265
- transformed_after_template = self._after_cache.get(_SCOPE_TRANSFORM_TEMPLATE_OUTCOME)
266
- if not transformed_after_template:
267
- transformed_after_template = self._after_template
268
- for after_global_transform in transform_after:
269
- if not is_nothing(after_global_transform.name):
270
- transformed_after_template = self._apply_global_transform(
271
- global_transform=after_global_transform,
272
- parameters=parameters_after,
273
- template=transformed_after_template,
274
- )
275
- self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_after_template
276
-
277
- self._save_runtime_cache()
258
+ transformed_after_template = self._after_template
259
+ for after_global_transform in transform_after:
260
+ if not is_nothing(after_global_transform.name):
261
+ transformed_after_template = self._apply_global_transform(
262
+ global_transform=after_global_transform,
263
+ parameters=self._after_parameters,
264
+ template=transformed_after_template,
265
+ )
266
+ # Macro transformations won't remove the transform from the template
267
+ if "Transform" in transformed_after_template:
268
+ transformed_after_template.pop("Transform")
269
+ self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = transformed_after_template
278
270
 
279
271
  return transformed_before_template, transformed_after_template
280
272
 
@@ -301,6 +293,9 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
301
293
  before = [] if change_type != ChangeType.CREATED else Nothing
302
294
  after = [] if change_type != ChangeType.REMOVED else Nothing
303
295
  for change_set_entity in node_transform.global_transforms:
296
+ if not isinstance(change_set_entity.name.value, str):
297
+ raise ValidationError("Key Name of transform definition must be a string.")
298
+
304
299
  delta: PreprocEntityDelta[GlobalTransform, GlobalTransform] = self.visit(
305
300
  change_set_entity=change_set_entity
306
301
  )
@@ -311,3 +306,242 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
311
306
  if not is_nothing(after) and not is_nothing(delta_after):
312
307
  after.append(delta_after)
313
308
  return PreprocEntityDelta(before=before, after=after)
309
+
310
+ def _compute_fn_transform(
311
+ self, macro_definition: Any, siblings: Any, allow_string: False
312
+ ) -> Any:
313
+ def _normalize_transform(obj):
314
+ transforms = []
315
+
316
+ if isinstance(obj, str):
317
+ transforms.append({"Name": obj, "Parameters": {}})
318
+
319
+ if isinstance(obj, dict):
320
+ transforms.append(obj)
321
+
322
+ if isinstance(obj, list):
323
+ for v in obj:
324
+ if isinstance(v, str):
325
+ transforms.append({"Name": v, "Parameters": {}})
326
+
327
+ if isinstance(v, dict):
328
+ if not v.get("Parameters"):
329
+ v["Parameters"] = {}
330
+ transforms.append(v)
331
+
332
+ return transforms
333
+
334
+ normalized_transforms = _normalize_transform(macro_definition)
335
+ transform_output = copy.deepcopy(siblings)
336
+ for transform in normalized_transforms:
337
+ transform_name = transform["Name"]
338
+ if transform_name == INCLUDE_TRANSFORM:
339
+ transform_output = self._compute_include_transform(
340
+ parameters=transform["Parameters"], fragment=transform_output
341
+ )
342
+ else:
343
+ transform_output: dict | str = self._invoke_macro(
344
+ fragment=transform_output,
345
+ name=transform["Name"],
346
+ parameters=transform.get("Parameters", {}),
347
+ allow_string=allow_string,
348
+ )
349
+
350
+ if isinstance(transform_output, dict) and FnTransform in transform_output:
351
+ transform_output.pop(FnTransform)
352
+
353
+ return transform_output
354
+
355
+ def _replace_at_jsonpath(self, template: dict, path: str, result: Any):
356
+ pattern = jsonpath_ng.parse(path)
357
+ result_template = pattern.update(template, result)
358
+
359
+ return result_template
360
+
361
+ def visit_node_for_each(self, node_foreach: NodeForEach) -> PreprocEntityDelta:
362
+ return PreprocEntityDelta()
363
+
364
+ def visit_node_intrinsic_function_fn_transform(
365
+ self, node_intrinsic_function: NodeIntrinsicFunctionFnTransform
366
+ ) -> PreprocEntityDelta:
367
+ arguments_delta = self.visit(node_intrinsic_function.arguments)
368
+ parent_json_path = node_intrinsic_function.scope.parent.jsonpath
369
+
370
+ # Only when a FnTransform is used as Property value the macro function is allowed to return a str
371
+ property_value_regex = r"\.(Properties)"
372
+ allow_string = False
373
+ if re.search(property_value_regex, parent_json_path):
374
+ allow_string = True
375
+
376
+ if not is_nothing(arguments_delta.before):
377
+ before = self._compute_fn_transform(
378
+ arguments_delta.before,
379
+ node_intrinsic_function.before_siblings,
380
+ allow_string=allow_string,
381
+ )
382
+ updated_before_template = self._replace_at_jsonpath(
383
+ self._before_template, parent_json_path, before
384
+ )
385
+ self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_before_template
386
+ else:
387
+ before = Nothing
388
+
389
+ if not is_nothing(arguments_delta.after):
390
+ after = self._compute_fn_transform(
391
+ arguments_delta.after,
392
+ node_intrinsic_function.after_siblings,
393
+ allow_string=allow_string,
394
+ )
395
+ updated_after_template = self._replace_at_jsonpath(
396
+ self._after_template, parent_json_path, after
397
+ )
398
+ self._after_cache[_SCOPE_TRANSFORM_TEMPLATE_OUTCOME] = updated_after_template
399
+ else:
400
+ after = Nothing
401
+
402
+ self._save_runtime_cache()
403
+ return PreprocEntityDelta(before=before, after=after)
404
+
405
+ def visit_node_properties(
406
+ self, node_properties: NodeProperties
407
+ ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
408
+ if not is_nothing(node_properties.fn_transform):
409
+ self.visit_node_intrinsic_function_fn_transform(node_properties.fn_transform)
410
+
411
+ return super().visit_node_properties(node_properties=node_properties)
412
+
413
+ def visit_node_resource(self, node_resource: NodeResource) -> PreprocEntityDelta:
414
+ if not is_nothing(node_resource.fn_transform):
415
+ self.visit_node_intrinsic_function_fn_transform(
416
+ node_intrinsic_function=node_resource.fn_transform
417
+ )
418
+
419
+ try:
420
+ if delta := super().visit_node_resource(node_resource):
421
+ return delta
422
+ return super().visit_node_properties(node_resource.properties)
423
+ except RuntimeError:
424
+ return super().visit_node_properties(node_resource.properties)
425
+
426
+ def visit_node_resources(self, node_resources: NodeResources) -> PreprocEntityDelta:
427
+ if not is_nothing(node_resources.fn_transform):
428
+ self.visit_node_intrinsic_function_fn_transform(
429
+ node_intrinsic_function=node_resources.fn_transform
430
+ )
431
+
432
+ return super().visit_node_resources(node_resources=node_resources)
433
+
434
+ def _invoke_macro(self, name: str, parameters: dict, fragment: dict, allow_string=False):
435
+ account_id = self._change_set.account_id
436
+ region_name = self._change_set.region_name
437
+ macro_definition = get_cloudformation_store(
438
+ account_id=account_id, region_name=region_name
439
+ ).macros.get(name)
440
+
441
+ if not macro_definition:
442
+ raise FailedTransformationException(name, f"Transformation {name} is not supported.")
443
+
444
+ simplified_parameters = {}
445
+ if resolved_parameters := self._change_set.resolved_parameters:
446
+ for key, resolved_parameter in resolved_parameters.items():
447
+ final_value = engine_parameter_value(resolved_parameter)
448
+ simplified_parameters[key] = (
449
+ final_value.split(",")
450
+ if resolved_parameter["type_"] == "CommaDelimitedList"
451
+ else final_value
452
+ )
453
+
454
+ transformation_id = f"{account_id}::{name}"
455
+ event = {
456
+ "region": region_name,
457
+ "accountId": account_id,
458
+ "fragment": fragment,
459
+ "transformId": transformation_id,
460
+ "params": parameters,
461
+ "requestId": long_uid(),
462
+ "templateParameterValues": simplified_parameters,
463
+ }
464
+
465
+ client = connect_to(aws_access_key_id=account_id, region_name=region_name).lambda_
466
+ try:
467
+ invocation = client.invoke(
468
+ FunctionName=macro_definition["FunctionName"], Payload=json.dumps(event)
469
+ )
470
+ except ClientError:
471
+ LOG.error(
472
+ "client error executing lambda function '%s' with payload '%s'",
473
+ macro_definition["FunctionName"],
474
+ json.dumps(event),
475
+ )
476
+ raise
477
+ if invocation.get("StatusCode") != 200 or invocation.get("FunctionError") == "Unhandled":
478
+ raise FailedTransformationException(
479
+ transformation=name,
480
+ message=f"Received malformed response from transform {transformation_id}. Rollback requested by user.",
481
+ )
482
+ result = json.loads(invocation["Payload"].read())
483
+
484
+ if result.get("status") != "success":
485
+ error_message = result.get("errorMessage")
486
+ message = (
487
+ f"Transform {transformation_id} failed with: {error_message}. Rollback requested by user."
488
+ if error_message
489
+ else f"Transform {transformation_id} failed without an error message.. Rollback requested by user."
490
+ )
491
+ raise FailedTransformationException(transformation=name, message=message)
492
+
493
+ if not isinstance(result.get("fragment"), dict) and not allow_string:
494
+ raise FailedTransformationException(
495
+ transformation=name,
496
+ message="Template format error: unsupported structure.. Rollback requested by user.",
497
+ )
498
+
499
+ return result.get("fragment")
500
+
501
+ def visit_node_intrinsic_function_fn_get_att(
502
+ self, node_intrinsic_function: NodeIntrinsicFunction
503
+ ) -> PreprocEntityDelta:
504
+ try:
505
+ return super().visit_node_intrinsic_function_fn_get_att(node_intrinsic_function)
506
+ except RuntimeError:
507
+ return self.visit(node_intrinsic_function.arguments)
508
+
509
+ def visit_node_intrinsic_function_fn_sub(
510
+ self, node_intrinsic_function: NodeIntrinsicFunction
511
+ ) -> PreprocEntityDelta:
512
+ try:
513
+ # If an argument is a Parameter it should be resolved, any other case, ignore it
514
+ return super().visit_node_intrinsic_function_fn_sub(node_intrinsic_function)
515
+ except RuntimeError:
516
+ return self.visit(node_intrinsic_function.arguments)
517
+
518
+ def visit_node_intrinsic_function_fn_split(
519
+ self, node_intrinsic_function: NodeIntrinsicFunction
520
+ ) -> PreprocEntityDelta:
521
+ try:
522
+ # If an argument is a Parameter it should be resolved, any other case, ignore it
523
+ return super().visit_node_intrinsic_function_fn_split(node_intrinsic_function)
524
+ except RuntimeError:
525
+ return self.visit(node_intrinsic_function.arguments)
526
+
527
+ def visit_node_intrinsic_function_fn_select(
528
+ self, node_intrinsic_function: NodeIntrinsicFunction
529
+ ) -> PreprocEntityDelta:
530
+ try:
531
+ # If an argument is a Parameter it should be resolved, any other case, ignore it
532
+ return super().visit_node_intrinsic_function_fn_select(node_intrinsic_function)
533
+ except RuntimeError:
534
+ return self.visit(node_intrinsic_function.arguments)
535
+
536
+ def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta:
537
+ try:
538
+ return super().visit_node_property(node_property)
539
+ except ParamValidationError:
540
+ return self.visit(node_property.value)
541
+
542
+ # ignore errors from dynamic replacements
543
+ def _maybe_perform_dynamic_replacements(self, delta: PreprocEntityDelta) -> PreprocEntityDelta:
544
+ try:
545
+ return super()._maybe_perform_dynamic_replacements(delta)
546
+ except Exception:
547
+ return delta