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
@@ -9,6 +9,7 @@ from typing import Any, Final, Generic, TypeVar
9
9
  from botocore.exceptions import ClientError
10
10
 
11
11
  from localstack import config
12
+ from localstack.aws.api.cloudformation import ResourceStatus
12
13
  from localstack.aws.api.ec2 import AvailabilityZoneList, DescribeAvailabilityZonesResult
13
14
  from localstack.aws.connect import connect_to
14
15
  from localstack.services.cloudformation.engine.v2.change_set_model import (
@@ -29,6 +30,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
29
30
  NodeProperties,
30
31
  NodeProperty,
31
32
  NodeResource,
33
+ NodeResources,
32
34
  NodeTemplate,
33
35
  Nothing,
34
36
  NothingType,
@@ -44,6 +46,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model_visitor impor
44
46
  ChangeSetModelVisitor,
45
47
  )
46
48
  from localstack.services.cloudformation.engine.v2.resolving import (
49
+ REGEX_DYNAMIC_REF,
47
50
  extract_dynamic_reference,
48
51
  perform_dynamic_reference_lookup,
49
52
  )
@@ -52,7 +55,9 @@ from localstack.services.cloudformation.stores import (
52
55
  exports_map,
53
56
  )
54
57
  from localstack.services.cloudformation.v2.entities import ChangeSet
58
+ from localstack.services.cloudformation.v2.types import ResolvedResource
55
59
  from localstack.utils.aws.arns import get_partition
60
+ from localstack.utils.numbers import to_number
56
61
  from localstack.utils.objects import get_value_from_path
57
62
  from localstack.utils.run import to_str
58
63
  from localstack.utils.strings import to_bytes
@@ -73,7 +78,11 @@ _PSEUDO_PARAMETERS: Final[set[str]] = {
73
78
 
74
79
  TBefore = TypeVar("TBefore")
75
80
  TAfter = TypeVar("TAfter")
81
+ _T = TypeVar("_T")
76
82
 
83
+ REGEX_OUTPUT_APIGATEWAY = re.compile(
84
+ rf"^(https?://.+\.execute-api\.)(?:[^-]+-){{2,3}}\d\.(amazonaws\.com|{_AWS_URL_SUFFIX})/?(.*)$"
85
+ )
77
86
  MOCKED_REFERENCE = "unknown"
78
87
 
79
88
  VALID_LOGICAL_RESOURCE_ID_RE = re.compile(r"^[A-Za-z0-9]+$")
@@ -113,6 +122,7 @@ class PreprocResource:
113
122
  properties: PreprocProperties
114
123
  depends_on: list[str] | None
115
124
  requires_replacement: bool
125
+ status: ResourceStatus | None
116
126
 
117
127
  def __init__(
118
128
  self,
@@ -123,6 +133,7 @@ class PreprocResource:
123
133
  properties: PreprocProperties,
124
134
  depends_on: list[str] | None,
125
135
  requires_replacement: bool,
136
+ status: ResourceStatus | None = None,
126
137
  ):
127
138
  self.logical_id = logical_id
128
139
  self.physical_resource_id = physical_resource_id
@@ -131,6 +142,7 @@ class PreprocResource:
131
142
  self.properties = properties
132
143
  self.depends_on = depends_on
133
144
  self.requires_replacement = requires_replacement
145
+ self.status = status
134
146
 
135
147
  @staticmethod
136
148
  def _compare_conditions(c1: bool, c2: bool):
@@ -215,6 +227,8 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
215
227
  def process(self) -> None:
216
228
  self._setup_runtime_cache()
217
229
  node_template = self._change_set.update_model.node_template
230
+ node_conditions = self._change_set.update_model.node_template.conditions
231
+ self.visit(node_conditions)
218
232
  self.visit(node_template)
219
233
  self._save_runtime_cache()
220
234
 
@@ -260,16 +274,16 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
260
274
  f"No deployed instances of resource '{resource_logical_id}' were found"
261
275
  )
262
276
  properties = resolved_resource.get("Properties", {})
263
- # support structured properties, e.g. NestedStack.Outputs.OutputName
277
+ # TODO support structured properties, e.g. NestedStack.Outputs.OutputName
264
278
  property_value: Any | None = get_value_from_path(properties, property_name)
265
279
 
266
280
  if property_value:
267
- if not isinstance(property_value, str):
268
- # TODO: is this correct? If there is a bug in the logic here, it's probably
269
- # better to know about it with a clear error message than to receive some form
270
- # of message about trying to use a dictionary in place of a string
281
+ if not isinstance(property_value, (str, list, dict)):
282
+ # Str: Standard expected type. TODO validate bools and numbers
283
+ # List: Multiple resource types can return a list of values e.g. AWS::EC2::VPC.
284
+ # Dict: Custom resources in CloudFormation can return arbitrary data structures.
271
285
  raise RuntimeError(
272
- f"Accessing property '{property_name}' from '{resource_logical_id}' resulted in a non-string value"
286
+ f"Accessing property '{property_name}' from '{resource_logical_id}' resulted in a non-string value nor list"
273
287
  )
274
288
  return property_value
275
289
  elif config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
@@ -395,10 +409,63 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
395
409
  return PreprocEntityDelta(before=before, after=after)
396
410
  delta = super().visit(change_set_entity=change_set_entity)
397
411
  if isinstance(delta, PreprocEntityDelta):
412
+ delta = self._maybe_perform_replacements(delta)
398
413
  self._before_cache[entity_scope] = delta.before
399
414
  self._after_cache[entity_scope] = delta.after
400
415
  return delta
401
416
 
417
+ def _maybe_perform_replacements(self, delta: PreprocEntityDelta) -> PreprocEntityDelta:
418
+ delta = self._maybe_perform_static_replacements(delta)
419
+ delta = self._maybe_perform_dynamic_replacements(delta)
420
+ return delta
421
+
422
+ def _maybe_perform_static_replacements(self, delta: PreprocEntityDelta) -> PreprocEntityDelta:
423
+ return self._maybe_perform_on_delta(delta, self._perform_static_replacements)
424
+
425
+ def _maybe_perform_dynamic_replacements(self, delta: PreprocEntityDelta) -> PreprocEntityDelta:
426
+ return self._maybe_perform_on_delta(delta, self._perform_dynamic_replacements)
427
+
428
+ def _maybe_perform_on_delta(
429
+ self, delta: PreprocEntityDelta | None, f: Callable[[_T], _T]
430
+ ) -> PreprocEntityDelta | None:
431
+ if isinstance(delta.before, str):
432
+ delta.before = f(delta.before)
433
+ if isinstance(delta.after, str):
434
+ delta.after = f(delta.after)
435
+ return delta
436
+
437
+ def _perform_dynamic_replacements(self, value: _T) -> _T:
438
+ if not isinstance(value, str):
439
+ return value
440
+
441
+ if dynamic_ref := extract_dynamic_reference(value):
442
+ new_value = perform_dynamic_reference_lookup(
443
+ reference=dynamic_ref,
444
+ account_id=self._change_set.account_id,
445
+ region_name=self._change_set.region_name,
446
+ )
447
+ if new_value:
448
+ # We need to use a function here, to avoid backslash processing by regex.
449
+ # From the regex sub documentation:
450
+ # repl can be a string or a function; if it is a string, any backslash escapes in it are processed.
451
+ # Using a function, we can avoid this processing.
452
+ return REGEX_DYNAMIC_REF.sub(lambda _: new_value, value)
453
+
454
+ return value
455
+
456
+ @staticmethod
457
+ def _perform_static_replacements(value: str) -> str:
458
+ api_match = REGEX_OUTPUT_APIGATEWAY.match(value)
459
+ if api_match and value not in config.CFN_STRING_REPLACEMENT_DENY_LIST:
460
+ prefix = api_match[1]
461
+ host = api_match[2]
462
+ path = api_match[3]
463
+ port = localstack_host().port
464
+ value = f"{prefix}{host}:{port}/{path}"
465
+ return value
466
+
467
+ return value
468
+
402
469
  def _cached_apply(
403
470
  self, scope: Scope, arguments_delta: PreprocEntityDelta, resolver: Callable[[Any], Any]
404
471
  ) -> PreprocEntityDelta:
@@ -447,6 +514,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
447
514
 
448
515
  return PreprocEntityDelta(before=before, after=after)
449
516
 
517
+ def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta:
518
+ return self.visit(node_property.value)
519
+
450
520
  def visit_terminal_value_modified(
451
521
  self, terminal_value_modified: TerminalValueModified
452
522
  ) -> PreprocEntityDelta:
@@ -486,9 +556,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
486
556
  delta: PreprocEntityDelta = self.visit(change_set_entity=change_set_entity)
487
557
  delta_before = delta.before
488
558
  delta_after = delta.after
489
- if not is_nothing(before) and not is_nothing(delta_before) and delta_before is not None:
559
+ if not is_nothing(before) and not is_nothing(delta_before):
490
560
  before[name] = delta_before
491
- if not is_nothing(after) and not is_nothing(delta_after) and delta_after is not None:
561
+ if not is_nothing(after) and not is_nothing(delta_after):
492
562
  after[name] = delta_after
493
563
  return PreprocEntityDelta(before=before, after=after)
494
564
 
@@ -499,13 +569,49 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
499
569
  arguments_list = arguments.split(".")
500
570
  else:
501
571
  arguments_list = arguments
572
+
573
+ if len(arguments_list) < 2:
574
+ raise ValidationError(
575
+ "Template error: every Fn::GetAtt object requires two non-empty parameters, the resource name and the resource attribute"
576
+ )
577
+
502
578
  logical_name_of_resource = arguments_list[0]
503
- attribute_name = arguments_list[1]
579
+ attribute_name = ".".join(arguments_list[1:])
504
580
 
505
581
  node_resource = self._get_node_resource_for(
506
582
  resource_name=logical_name_of_resource,
507
583
  node_template=self._change_set.update_model.node_template,
508
584
  )
585
+
586
+ if not is_nothing(node_resource.condition_reference):
587
+ condition = self._get_node_condition_if_exists(node_resource.condition_reference.value)
588
+ evaluation_result = self._resolve_condition(condition.name)
589
+
590
+ if select_before and not evaluation_result.before:
591
+ raise ValidationError(
592
+ f"Template format error: Unresolved resource dependencies [{logical_name_of_resource}] in the Resources block of the template"
593
+ )
594
+
595
+ if not select_before and not evaluation_result.after:
596
+ raise ValidationError(
597
+ f"Template format error: Unresolved resource dependencies [{logical_name_of_resource}] in the Resources block of the template"
598
+ )
599
+
600
+ # Custom Resources can mutate their definition
601
+ # So the preproc should search first in the resource values and then check the template
602
+ if select_before:
603
+ value = self._before_deployed_property_value_of(
604
+ resource_logical_id=logical_name_of_resource,
605
+ property_name=attribute_name,
606
+ )
607
+ else:
608
+ value = self._after_deployed_property_value_of(
609
+ resource_logical_id=logical_name_of_resource,
610
+ property_name=attribute_name,
611
+ )
612
+ if value is not None:
613
+ return value
614
+
509
615
  node_property: NodeProperty | None = self._get_node_property_for(
510
616
  property_name=attribute_name, node_resource=node_resource
511
617
  )
@@ -513,19 +619,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
513
619
  # The property is statically defined in the template and its value can be computed.
514
620
  property_delta = self.visit(node_property)
515
621
  value = property_delta.before if select_before else property_delta.after
516
- else:
517
- # The property is not statically defined and must therefore be available in
518
- # the properties deployed set.
519
- if select_before:
520
- value = self._before_deployed_property_value_of(
521
- resource_logical_id=logical_name_of_resource,
522
- property_name=attribute_name,
523
- )
524
- else:
525
- value = self._after_deployed_property_value_of(
526
- resource_logical_id=logical_name_of_resource,
527
- property_name=attribute_name,
528
- )
622
+
529
623
  return value
530
624
 
531
625
  def visit_node_intrinsic_function_fn_get_att(
@@ -554,6 +648,12 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
554
648
  return args[0] == args[1]
555
649
 
556
650
  arguments_delta = self.visit(node_intrinsic_function.arguments)
651
+
652
+ if isinstance(arguments_delta.after, list) and len(arguments_delta.after) != 2:
653
+ raise ValidationError(
654
+ "Template error: every Fn::Equals object requires a list of 2 string parameters."
655
+ )
656
+
557
657
  delta = self._cached_apply(
558
658
  scope=node_intrinsic_function.scope,
559
659
  arguments_delta=arguments_delta,
@@ -630,8 +730,12 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
630
730
  def visit_node_intrinsic_function_fn_not(
631
731
  self, node_intrinsic_function: NodeIntrinsicFunction
632
732
  ) -> PreprocEntityDelta:
633
- def _compute_fn_not(arg: bool) -> bool:
634
- return not arg
733
+ def _compute_fn_not(arg: list[bool] | bool) -> bool:
734
+ # Is the argument ever a lone boolean?
735
+ if isinstance(arg, list):
736
+ return not arg[0]
737
+ else:
738
+ return not arg
635
739
 
636
740
  arguments_delta = self.visit(node_intrinsic_function.arguments)
637
741
  delta = self._cached_apply(
@@ -779,13 +883,27 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
779
883
  ):
780
884
  # TODO: add further support for schema validation
781
885
  def _compute_fn_select(args: list[Any]) -> Any:
782
- values: list[Any] = args[1]
886
+ values = args[1]
887
+ # defer evaluation if the selection list contains unresolved elements (e.g., unresolved intrinsics)
888
+ if isinstance(values, list) and not all(isinstance(value, str) for value in values):
889
+ raise RuntimeError("Fn::Select list contains unresolved elements")
890
+
783
891
  if not isinstance(values, list) or not values:
784
- raise RuntimeError(f"Invalid arguments list value for Fn::Select: '{values}'")
892
+ raise ValidationError(
893
+ "Template error: Fn::Select requires a list argument with two elements: an integer index and a list"
894
+ )
895
+ try:
896
+ index: int = int(args[0])
897
+ except ValueError as e:
898
+ raise ValidationError(
899
+ "Template error: Fn::Select requires a list argument with two elements: an integer index and a list"
900
+ ) from e
901
+
785
902
  values_len = len(values)
786
- index: int = int(args[0])
787
- if not isinstance(index, int) or index < 0 or index > values_len:
788
- raise RuntimeError(f"Invalid or out of range index value for Fn::Select: '{index}'")
903
+ if index < 0 or index >= values_len:
904
+ raise ValidationError(
905
+ "Template error: Fn::Select requires a list argument with two elements: an integer index and a list"
906
+ )
789
907
  selection = values[index]
790
908
  return selection
791
909
 
@@ -812,6 +930,17 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
812
930
  return split_string
813
931
 
814
932
  arguments_delta = self.visit(node_intrinsic_function.arguments)
933
+
934
+ if not (
935
+ is_nothing(arguments_delta.after)
936
+ or isinstance(arguments_delta.after, list)
937
+ and len(arguments_delta.after) == 2
938
+ ):
939
+ raise ValidationError(
940
+ "Template error: every Fn::Split object requires two parameters, "
941
+ "(1) a string delimiter and (2) a string to be split or a function that returns a string to be split."
942
+ )
943
+
815
944
  delta = self._cached_apply(
816
945
  scope=node_intrinsic_function.scope,
817
946
  arguments_delta=arguments_delta,
@@ -911,6 +1040,10 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
911
1040
  return PreprocEntityDelta(before=before_parameters, after=after_parameters)
912
1041
 
913
1042
  def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta:
1043
+ if not VALID_LOGICAL_RESOURCE_ID_RE.match(node_parameter.name):
1044
+ raise ValidationError(
1045
+ f"Template format error: Parameter name {node_parameter.name} is non alphanumeric."
1046
+ )
914
1047
  dynamic_value = node_parameter.dynamic_value
915
1048
  dynamic_delta = self.visit(dynamic_value)
916
1049
 
@@ -924,8 +1057,11 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
924
1057
 
925
1058
  def _resolve_parameter_type(value: str, type_: str) -> Any:
926
1059
  match type_:
927
- case "List<String>":
1060
+ case "List<String>" | "CommaDelimitedList":
928
1061
  return [item.strip() for item in value.split(",")]
1062
+ case "Number":
1063
+ # TODO: validate the parameter type at template parse time (or whatever is in parity with AWS) so we know this cannot fail
1064
+ return to_number(value)
929
1065
  return value
930
1066
 
931
1067
  if not is_nothing(after):
@@ -942,11 +1078,17 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
942
1078
  return delta
943
1079
 
944
1080
  def _resource_physical_resource_id_from(
945
- self, logical_resource_id: str, resolved_resources: dict
946
- ) -> str:
1081
+ self, logical_resource_id: str, resolved_resources: dict[str, ResolvedResource]
1082
+ ) -> str | None:
947
1083
  # TODO: typing around resolved resources is needed and should be reflected here.
948
1084
  resolved_resource = resolved_resources.get(logical_resource_id, {})
949
- physical_resource_id: str | None = resolved_resource.get("PhysicalResourceId")
1085
+ if resolved_resource.get("ResourceStatus") not in {
1086
+ ResourceStatus.CREATE_COMPLETE,
1087
+ ResourceStatus.UPDATE_COMPLETE,
1088
+ }:
1089
+ return None
1090
+
1091
+ physical_resource_id = resolved_resource.get("PhysicalResourceId")
950
1092
  if not isinstance(physical_resource_id, str):
951
1093
  raise RuntimeError(f"No PhysicalResourceId found for resource '{logical_resource_id}'")
952
1094
  return physical_resource_id
@@ -965,6 +1107,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
965
1107
  self, node_intrinsic_function: NodeIntrinsicFunction
966
1108
  ) -> PreprocEntityDelta:
967
1109
  def _compute_fn_ref(logical_id: str) -> PreprocEntityDelta:
1110
+ if logical_id == "AWS::NoValue":
1111
+ return Nothing
1112
+
968
1113
  reference_delta: PreprocEntityDelta = self._resolve_reference(logical_id=logical_id)
969
1114
  if isinstance(before := reference_delta.before, PreprocResource):
970
1115
  reference_delta.before = before.physical_resource_id
@@ -1013,25 +1158,6 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1013
1158
  after.append(delta_after)
1014
1159
  return PreprocEntityDelta(before=before, after=after)
1015
1160
 
1016
- def visit_node_property(self, node_property: NodeProperty) -> PreprocEntityDelta:
1017
- # TODO: what about other positions?
1018
- value = self.visit(node_property.value)
1019
- if not is_nothing(value.before):
1020
- if dynamic_ref := extract_dynamic_reference(value.before):
1021
- value.before = perform_dynamic_reference_lookup(
1022
- reference=dynamic_ref,
1023
- account_id=self._change_set.account_id,
1024
- region_name=self._change_set.region_name,
1025
- )
1026
- if not is_nothing(value.after):
1027
- if dynamic_ref := extract_dynamic_reference(value.after):
1028
- value.after = perform_dynamic_reference_lookup(
1029
- reference=dynamic_ref,
1030
- account_id=self._change_set.account_id,
1031
- region_name=self._change_set.region_name,
1032
- )
1033
- return value
1034
-
1035
1161
  def visit_node_properties(
1036
1162
  self, node_properties: NodeProperties
1037
1163
  ) -> PreprocEntityDelta[PreprocProperties, PreprocProperties]:
@@ -1077,6 +1203,20 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1077
1203
  after = after_delta.after
1078
1204
  return PreprocEntityDelta(before=before, after=after)
1079
1205
 
1206
+ def visit_node_resources(self, node_resources: NodeResources):
1207
+ """
1208
+ Skip resources where they conditionally evaluate to False
1209
+ """
1210
+ for node_resource in node_resources.resources:
1211
+ if not is_nothing(node_resource.condition_reference):
1212
+ condition_delta = self._resolve_resource_condition_reference(
1213
+ node_resource.condition_reference
1214
+ )
1215
+ condition_after = condition_delta.after
1216
+ if condition_after is False:
1217
+ continue
1218
+ self.visit(node_resource)
1219
+
1080
1220
  def visit_node_resource(
1081
1221
  self, node_resource: NodeResource
1082
1222
  ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
@@ -1187,6 +1327,14 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1187
1327
  before: list[PreprocOutput] = []
1188
1328
  after: list[PreprocOutput] = []
1189
1329
  for node_output in node_outputs.outputs:
1330
+ if not is_nothing(node_output.condition_reference):
1331
+ condition_delta = self._resolve_resource_condition_reference(
1332
+ node_output.condition_reference
1333
+ )
1334
+ condition_after = condition_delta.after
1335
+ if condition_after is False:
1336
+ continue
1337
+
1190
1338
  output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output)
1191
1339
  output_before = output_delta.before
1192
1340
  output_after = output_delta.after
@@ -1216,3 +1364,8 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1216
1364
  resolver=_compute_fn_import_value,
1217
1365
  )
1218
1366
  return delta
1367
+
1368
+ def visit_node_intrinsic_function_fn_transform(
1369
+ self, node_intrinsic_function: NodeIntrinsicFunction
1370
+ ):
1371
+ raise RuntimeError("Fn::Transform should have been handled by the Transformer")