localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev42__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.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

Files changed (208) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +560 -559
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1041 -969
  5. localstack/aws/api/cloudwatch/__init__.py +408 -368
  6. localstack/aws/api/config/__init__.py +788 -786
  7. localstack/aws/api/core.py +4 -0
  8. localstack/aws/api/dynamodb/__init__.py +753 -759
  9. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  10. localstack/aws/api/ec2/__init__.py +9713 -8573
  11. localstack/aws/api/es/__init__.py +453 -453
  12. localstack/aws/api/events/__init__.py +552 -552
  13. localstack/aws/api/firehose/__init__.py +541 -543
  14. localstack/aws/api/iam/__init__.py +646 -572
  15. localstack/aws/api/kinesis/__init__.py +251 -144
  16. localstack/aws/api/kms/__init__.py +343 -333
  17. localstack/aws/api/lambda_/__init__.py +585 -571
  18. localstack/aws/api/logs/__init__.py +682 -666
  19. localstack/aws/api/opensearch/__init__.py +814 -785
  20. localstack/aws/api/pipes/__init__.py +336 -336
  21. localstack/aws/api/redshift/__init__.py +1192 -1164
  22. localstack/aws/api/resource_groups/__init__.py +175 -175
  23. localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
  24. localstack/aws/api/route53/__init__.py +256 -254
  25. localstack/aws/api/route53resolver/__init__.py +396 -396
  26. localstack/aws/api/s3/__init__.py +1358 -1345
  27. localstack/aws/api/s3control/__init__.py +616 -584
  28. localstack/aws/api/scheduler/__init__.py +118 -118
  29. localstack/aws/api/secretsmanager/__init__.py +193 -193
  30. localstack/aws/api/ses/__init__.py +227 -227
  31. localstack/aws/api/sns/__init__.py +115 -115
  32. localstack/aws/api/sqs/__init__.py +100 -100
  33. localstack/aws/api/ssm/__init__.py +1978 -1970
  34. localstack/aws/api/stepfunctions/__init__.py +323 -323
  35. localstack/aws/api/sts/__init__.py +90 -66
  36. localstack/aws/api/support/__init__.py +112 -112
  37. localstack/aws/api/swf/__init__.py +378 -386
  38. localstack/aws/api/transcribe/__init__.py +425 -425
  39. localstack/aws/client.py +7 -2
  40. localstack/aws/forwarder.py +52 -5
  41. localstack/aws/handlers/analytics.py +1 -1
  42. localstack/aws/handlers/logging.py +12 -2
  43. localstack/aws/handlers/metric_handler.py +41 -1
  44. localstack/aws/handlers/service.py +43 -10
  45. localstack/aws/protocol/parser.py +440 -21
  46. localstack/aws/protocol/serializer.py +684 -64
  47. localstack/aws/protocol/service_router.py +120 -20
  48. localstack/aws/scaffold.py +15 -17
  49. localstack/aws/skeleton.py +4 -2
  50. localstack/aws/spec-patches.json +58 -0
  51. localstack/aws/spec.py +33 -13
  52. localstack/cli/exceptions.py +1 -1
  53. localstack/cli/localstack.py +10 -5
  54. localstack/cli/lpm.py +3 -4
  55. localstack/cli/profiles.py +1 -2
  56. localstack/config.py +18 -12
  57. localstack/constants.py +4 -29
  58. localstack/dev/kubernetes/__main__.py +39 -4
  59. localstack/dev/run/paths.py +1 -1
  60. localstack/dns/plugins.py +5 -1
  61. localstack/dns/server.py +12 -3
  62. localstack/packages/api.py +9 -8
  63. localstack/packages/core.py +2 -2
  64. localstack/packages/plugins.py +0 -8
  65. localstack/runtime/init.py +1 -1
  66. localstack/services/apigateway/helpers.py +5 -9
  67. localstack/services/apigateway/legacy/provider.py +85 -12
  68. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  69. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  70. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  71. localstack/services/apigateway/next_gen/provider.py +5 -0
  72. localstack/services/apigateway/patches.py +0 -9
  73. localstack/services/cloudformation/engine/entities.py +12 -1
  74. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  75. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  76. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  77. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  78. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  79. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  80. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  81. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  82. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  83. localstack/services/cloudformation/provider.py +2 -2
  84. localstack/services/cloudformation/resource_provider.py +5 -1
  85. localstack/services/cloudformation/resources.py +24149 -0
  86. localstack/services/cloudformation/v2/entities.py +6 -3
  87. localstack/services/cloudformation/v2/provider.py +178 -33
  88. localstack/services/cloudformation/v2/types.py +8 -4
  89. localstack/services/cloudwatch/provider_v2.py +25 -28
  90. localstack/services/dynamodb/packages.py +2 -1
  91. localstack/services/dynamodb/provider.py +42 -0
  92. localstack/services/dynamodb/v2/provider.py +42 -0
  93. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  94. localstack/services/es/provider.py +2 -2
  95. localstack/services/events/event_rule_engine.py +31 -13
  96. localstack/services/events/models.py +4 -5
  97. localstack/services/events/target.py +17 -9
  98. localstack/services/iam/provider.py +11 -116
  99. localstack/services/iam/resources/policy_simulator.py +133 -0
  100. localstack/services/kinesis/models.py +15 -2
  101. localstack/services/kinesis/packages.py +1 -1
  102. localstack/services/kinesis/provider.py +77 -0
  103. localstack/services/kms/models.py +34 -4
  104. localstack/services/kms/provider.py +107 -21
  105. localstack/services/lambda_/api_utils.py +3 -1
  106. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  107. localstack/services/lambda_/packages.py +1 -1
  108. localstack/services/lambda_/provider.py +1 -1
  109. localstack/services/lambda_/runtimes.py +8 -3
  110. localstack/services/logs/provider.py +36 -19
  111. localstack/services/moto.py +2 -1
  112. localstack/services/opensearch/cluster.py +15 -7
  113. localstack/services/opensearch/packages.py +26 -7
  114. localstack/services/opensearch/provider.py +6 -1
  115. localstack/services/opensearch/versions.py +56 -7
  116. localstack/services/s3/constants.py +5 -2
  117. localstack/services/s3/cors.py +4 -4
  118. localstack/services/s3/notifications.py +1 -1
  119. localstack/services/s3/presigned_url.py +27 -43
  120. localstack/services/s3/provider.py +68 -12
  121. localstack/services/s3/utils.py +42 -11
  122. localstack/services/ses/provider.py +16 -7
  123. localstack/services/sns/constants.py +7 -1
  124. localstack/services/sns/v2/models.py +190 -0
  125. localstack/services/sns/v2/provider.py +992 -2
  126. localstack/services/sns/v2/utils.py +138 -0
  127. localstack/services/sqs/developer_api.py +205 -0
  128. localstack/services/sqs/models.py +79 -13
  129. localstack/services/sqs/provider.py +8 -309
  130. localstack/services/sqs/query_api.py +1 -1
  131. localstack/services/sqs/utils.py +121 -2
  132. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  133. localstack/testing/aws/cloudformation_utils.py +1 -1
  134. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  135. localstack/testing/pytest/container.py +4 -5
  136. localstack/testing/pytest/fixtures.py +20 -19
  137. localstack/testing/pytest/in_memory_localstack.py +0 -4
  138. localstack/testing/pytest/marking.py +13 -4
  139. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  140. localstack/testing/pytest/util.py +1 -1
  141. localstack/testing/pytest/validation_tracking.py +1 -2
  142. localstack/testing/snapshots/transformer_utility.py +7 -0
  143. localstack/testing/testselection/matching.py +0 -1
  144. localstack/utils/analytics/events.py +2 -2
  145. localstack/utils/analytics/metadata.py +1 -2
  146. localstack/utils/analytics/metrics/counter.py +6 -8
  147. localstack/utils/analytics/publisher.py +1 -2
  148. localstack/utils/analytics/service_request_aggregator.py +2 -2
  149. localstack/utils/archives.py +11 -11
  150. localstack/utils/aws/arns.py +17 -9
  151. localstack/utils/aws/aws_responses.py +7 -7
  152. localstack/utils/aws/aws_stack.py +2 -3
  153. localstack/utils/aws/client_types.py +0 -8
  154. localstack/utils/aws/message_forwarding.py +1 -2
  155. localstack/utils/aws/request_context.py +4 -5
  156. localstack/utils/batch_policy.py +3 -3
  157. localstack/utils/bootstrap.py +7 -7
  158. localstack/utils/catalog/catalog.py +139 -0
  159. localstack/utils/catalog/catalog_loader.py +119 -0
  160. localstack/utils/catalog/common.py +58 -0
  161. localstack/utils/catalog/plugins.py +28 -0
  162. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  163. localstack/utils/collections.py +7 -8
  164. localstack/utils/config_listener.py +1 -1
  165. localstack/utils/container_networking.py +2 -3
  166. localstack/utils/container_utils/container_client.py +115 -131
  167. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  168. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  169. localstack/utils/crypto.py +109 -0
  170. localstack/utils/diagnose.py +2 -3
  171. localstack/utils/docker_utils.py +3 -4
  172. localstack/utils/files.py +31 -7
  173. localstack/utils/functions.py +3 -2
  174. localstack/utils/http.py +4 -5
  175. localstack/utils/json.py +19 -5
  176. localstack/utils/kinesis/kinesis_connector.py +2 -1
  177. localstack/utils/net.py +6 -6
  178. localstack/utils/no_exit_argument_parser.py +2 -2
  179. localstack/utils/numbers.py +9 -2
  180. localstack/utils/objects.py +6 -5
  181. localstack/utils/patch.py +2 -1
  182. localstack/utils/run.py +10 -9
  183. localstack/utils/scheduler.py +11 -11
  184. localstack/utils/server/tcp_proxy.py +2 -2
  185. localstack/utils/serving.py +2 -3
  186. localstack/utils/strings.py +10 -11
  187. localstack/utils/sync.py +126 -1
  188. localstack/utils/tagging.py +1 -4
  189. localstack/utils/testutil.py +5 -4
  190. localstack/utils/threads.py +2 -2
  191. localstack/utils/time.py +11 -3
  192. localstack/utils/urls.py +1 -3
  193. localstack/version.py +2 -2
  194. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
  195. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
  196. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
  197. localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
  198. localstack/packages/terraform.py +0 -46
  199. localstack/services/cloudformation/deploy.html +0 -144
  200. localstack/services/cloudformation/deploy_ui.py +0 -47
  201. localstack/services/cloudformation/plugins.py +0 -12
  202. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  203. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
  204. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
  205. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
  206. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
  207. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
  208. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ from werkzeug.datastructures import Headers
8
8
  from localstack.aws.api.apigateway import Integration
9
9
 
10
10
  from ..context import EndpointResponse, IntegrationRequest, RestApiInvocationContext
11
- from ..gateway_response import ApiConfigurationError, IntegrationFailureError
11
+ from ..gateway_response import ApiConfigurationError, IntegrationFailureError, InternalServerError
12
12
  from ..header_utils import build_multi_value_headers
13
13
  from .core import RestApiIntegration
14
14
 
@@ -72,7 +72,7 @@ class RestApiHttpIntegration(BaseRestApiHttpIntegration):
72
72
  except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
73
73
  LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
74
74
  LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
75
- raise ApiConfigurationError("Internal server error") from e
75
+ raise InternalServerError("Internal server error") from e
76
76
 
77
77
  except (requests.exceptions.Timeout, requests.exceptions.SSLError) as e:
78
78
  # TODO make the exception catching more fine grained
@@ -127,7 +127,7 @@ class RestApiHttpProxyIntegration(BaseRestApiHttpIntegration):
127
127
  except (requests.exceptions.InvalidURL, requests.exceptions.InvalidSchema) as e:
128
128
  LOG.warning("Execution failed due to configuration error: Invalid endpoint address")
129
129
  LOG.debug("The URI specified for the HTTP/HTTP_PROXY integration is invalid: %s", uri)
130
- raise ApiConfigurationError("Internal server error") from e
130
+ raise InternalServerError("Internal server error") from e
131
131
 
132
132
  except (requests.exceptions.Timeout, requests.exceptions.SSLError):
133
133
  # TODO make the exception catching more fine grained
@@ -62,6 +62,17 @@ TEST_INVOKE_TEMPLATE_MOCK = """Execution log for request {request_id}
62
62
  {formatted_date} : Method completed with status: {method_response_status}
63
63
  """
64
64
 
65
+ TEST_INVOKE_TEMPLATE_FAILED = """Execution log for request {request_id}
66
+ {formatted_date} : Starting execution for request: {request_id}
67
+ {formatted_date} : HTTP Method: {request_method}, Resource Path: {resource_path}
68
+ {formatted_date} : Method request path: {method_request_path_parameters}
69
+ {formatted_date} : Method request query string: {method_request_query_string}
70
+ {formatted_date} : Method request headers: {method_request_headers}
71
+ {formatted_date} : Method request body before transformations: {method_request_body}
72
+ {formatted_date} : Execution failed due to {error_type}: {error_message}
73
+ {formatted_date} : Method completed with status: {method_response_status}
74
+ """
75
+
65
76
 
66
77
  def _dump_headers(headers: Headers) -> str:
67
78
  if not headers:
@@ -80,9 +91,9 @@ def log_template(invocation_context: RestApiInvocationContext, response_headers:
80
91
  formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
81
92
  request = invocation_context.invocation_request
82
93
  context_var = invocation_context.context_variables
83
- integration_req = invocation_context.integration_request
84
- endpoint_resp = invocation_context.endpoint_response
85
- method_resp = invocation_context.invocation_response
94
+ integration_req = invocation_context.integration_request or {}
95
+ endpoint_resp = invocation_context.endpoint_response or {}
96
+ method_resp = invocation_context.invocation_response or {}
86
97
  # TODO: if endpoint_uri is an ARN, it means it's an AWS_PROXY integration
87
98
  # this should be transformed to the true URL of a lambda invoke call
88
99
  endpoint_uri = integration_req.get("uri", "")
@@ -116,7 +127,7 @@ def log_mock_template(
116
127
  formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
117
128
  request = invocation_context.invocation_request
118
129
  context_var = invocation_context.context_variables
119
- method_resp = invocation_context.invocation_response
130
+ method_resp = invocation_context.invocation_response or {}
120
131
 
121
132
  return TEST_INVOKE_TEMPLATE_MOCK.format(
122
133
  formatted_date=formatted_date,
@@ -133,6 +144,29 @@ def log_mock_template(
133
144
  )
134
145
 
135
146
 
147
+ def log_failed_template(
148
+ invocation_context: RestApiInvocationContext, response_status_code: int
149
+ ) -> str:
150
+ formatted_date = datetime.datetime.now(tz=datetime.UTC).strftime("%a %b %d %H:%M:%S %Z %Y")
151
+ request = invocation_context.invocation_request
152
+ context_var = invocation_context.context_variables
153
+
154
+ return TEST_INVOKE_TEMPLATE_FAILED.format(
155
+ formatted_date=formatted_date,
156
+ request_id=context_var["requestId"],
157
+ resource_path=request["path"],
158
+ request_method=request["http_method"],
159
+ method_request_path_parameters=dict_to_string(request["path_parameters"]),
160
+ method_request_query_string=dict_to_string(request["query_string_parameters"]),
161
+ method_request_headers=_dump_headers(request.get("headers")),
162
+ method_request_body=to_str(request.get("body", "")),
163
+ method_response_status=response_status_code,
164
+ # TODO: fix the error message
165
+ error_type="",
166
+ error_message="",
167
+ )
168
+
169
+
136
170
  def create_test_chain() -> HandlerChain[RestApiInvocationContext]:
137
171
  return HandlerChain(
138
172
  request_handlers=[
@@ -216,7 +250,9 @@ def create_test_invocation_context(
216
250
  responseOverride=ContextVarsResponseOverride(header={}, status=0),
217
251
  )
218
252
  invocation_context.trace_id = parse_handler.populate_trace_id({})
219
- resource_method = resource["resourceMethods"][http_method]
253
+ resource_method = (
254
+ resource["resourceMethods"].get(http_method) or resource["resourceMethods"]["ANY"]
255
+ )
220
256
  invocation_context.resource = resource
221
257
  invocation_context.resource_method = resource_method
222
258
  invocation_context.integration = resource_method["methodIntegration"]
@@ -256,7 +292,15 @@ def run_test_invocation(
256
292
  # AWS does not return the Content-Length for TestInvokeMethod
257
293
  response_headers.remove("Content-Length")
258
294
 
259
- if is_mock_integration:
295
+ if not invocation_context.invocation_response:
296
+ # TODO: this is an heuristic to guess if we encounter an exception in the call
297
+ # in the future, we should attach the exception to the context so we could act on it and properly
298
+ # log as we go through the invocation, so that if we have an error we stop logging at the right moment
299
+ for header in ("Content-Type", "X-Amzn-Trace-Id"):
300
+ response_headers.remove(header)
301
+ log = log_failed_template(invocation_context, test_response.status_code)
302
+
303
+ elif is_mock_integration:
260
304
  # TODO: revisit how we're building the logs
261
305
  log = log_mock_template(invocation_context, response_headers)
262
306
  else:
@@ -429,6 +429,11 @@ class ApigatewayNextGenProvider(ApigatewayProvider):
429
429
  if not resource:
430
430
  raise NotFoundException("Invalid Resource identifier specified")
431
431
 
432
+ resource_methods = resource.resource_methods
433
+
434
+ if request["httpMethod"] not in resource_methods and "ANY" not in resource_methods:
435
+ raise NotFoundException("Invalid Method identifier specified")
436
+
432
437
  # test httpMethod
433
438
 
434
439
  rest_api_container = get_rest_api_container(context, rest_api_id=rest_api_id)
@@ -5,7 +5,6 @@ import logging
5
5
  from moto.apigateway import models as apigateway_models
6
6
  from moto.apigateway.exceptions import (
7
7
  DeploymentNotFoundException,
8
- NoIntegrationDefined,
9
8
  RestAPINotFound,
10
9
  StageStillActive,
11
10
  )
@@ -113,14 +112,6 @@ def apply_patches():
113
112
  )
114
113
  return result
115
114
 
116
- # patch integration error responses
117
- @patch(apigateway_models.Resource.get_integration)
118
- def apigateway_models_resource_get_integration(fn, self, method_type):
119
- resource_method = self.resource_methods.get(method_type, {})
120
- if not resource_method.method_integration:
121
- raise NoIntegrationDefined()
122
- return resource_method.method_integration
123
-
124
115
  @patch(apigateway_models.RestAPI.to_dict)
125
116
  def apigateway_models_rest_api_to_dict(fn, self):
126
117
  resp = fn(self)
@@ -14,7 +14,13 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
14
14
  )
15
15
  from localstack.utils.aws import arns
16
16
  from localstack.utils.collections import select_attributes
17
- from localstack.utils.id_generator import ExistingIds, ResourceIdentifier, Tags, generate_short_uid
17
+ from localstack.utils.id_generator import (
18
+ ExistingIds,
19
+ ResourceIdentifier,
20
+ Tags,
21
+ generate_short_uid,
22
+ generate_uid,
23
+ )
18
24
  from localstack.utils.json import clone_safe
19
25
  from localstack.utils.objects import recurse_object
20
26
  from localstack.utils.strings import long_uid, short_uid
@@ -75,6 +81,11 @@ class StackIdentifier(ResourceIdentifier):
75
81
  return generate_short_uid(resource_identifier=self, existing_ids=existing_ids, tags=tags)
76
82
 
77
83
 
84
+ class StackIdentifierV2(StackIdentifier):
85
+ def generate(self, existing_ids: ExistingIds = None, tags: Tags = None) -> str:
86
+ return generate_uid(resource_identifier=self, existing_ids=existing_ids, tags=tags)
87
+
88
+
78
89
  # TODO: remove metadata (flatten into individual fields)
79
90
  class Stack:
80
91
  change_sets: list["StackChangeSet"]
@@ -681,9 +681,6 @@ class ChangeSetModel:
681
681
  scope=arguments_scope, before_value=before_arguments, after_value=after_arguments
682
682
  )
683
683
 
684
- if intrinsic_function == "Ref" and arguments.value == "AWS::NoValue":
685
- arguments.value = Nothing
686
-
687
684
  if is_created(before=before_arguments, after=after_arguments):
688
685
  change_type = ChangeType.CREATED
689
686
  elif is_removed(before=before_arguments, after=after_arguments):
@@ -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,6 +97,19 @@ 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:
@@ -1,5 +1,6 @@
1
1
  import copy
2
2
  import logging
3
+ import os
3
4
  import re
4
5
  import uuid
5
6
  from collections.abc import Callable
@@ -105,14 +106,15 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
105
106
  except Exception as e:
106
107
  failure_message = str(e)
107
108
 
109
+ is_deletion = self._change_set.stack.status == StackStatus.DELETE_IN_PROGRESS
108
110
  if self._deferred_actions:
109
- if failure_message:
110
- # TODO: differentiate between update and create
111
- self._change_set.stack.set_stack_status(StackStatus.ROLLBACK_IN_PROGRESS)
112
- else:
111
+ if not is_deletion:
113
112
  # TODO: correct status
113
+ # TODO: differentiate between update and create
114
114
  self._change_set.stack.set_stack_status(
115
- StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
115
+ StackStatus.ROLLBACK_IN_PROGRESS
116
+ if failure_message
117
+ else StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
116
118
  )
117
119
 
118
120
  # perform all deferred actions such as deletions. These must happen in reverse from their
@@ -122,7 +124,7 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
122
124
  LOG.debug("executing deferred action: '%s'", deferred.name)
123
125
  deferred.action()
124
126
 
125
- if failure_message:
127
+ if failure_message and not is_deletion:
126
128
  # TODO: differentiate between update and create
127
129
  self._change_set.stack.set_stack_status(StackStatus.ROLLBACK_COMPLETE)
128
130
 
@@ -515,14 +517,11 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
515
517
  resource_type,
516
518
  f'No resource provider found for "{resource_type}"',
517
519
  )
518
- LOG.warning(
519
- "Deployment of resource type %s successful due to config CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES",
520
- resource_type,
521
- )
522
- LOG.warning(
523
- "Deployment of resource type %s will fail in upcoming LocalStack releases unless CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES is explicitly enabled.",
524
- resource_type,
525
- )
520
+ if "CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES" not in os.environ:
521
+ LOG.warning(
522
+ "Deployment of resource type %s succeeded, but will fail in upcoming LocalStack releases unless CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES is explicitly enabled.",
523
+ resource_type,
524
+ )
526
525
  event = ProgressEvent(
527
526
  OperationStatus.SUCCESS,
528
527
  resource_model={},
@@ -577,7 +576,6 @@ class ChangeSetModelExecutor(ChangeSetModelPreproc):
577
576
  )
578
577
  # TODO: do we actually need this line?
579
578
  resolved_resource.update(extra_resource_properties)
580
-
581
579
  case OperationStatus.FAILED:
582
580
  reason = event.message
583
581
  LOG.warning(
@@ -30,6 +30,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model import (
30
30
  NodeProperties,
31
31
  NodeProperty,
32
32
  NodeResource,
33
+ NodeResources,
33
34
  NodeTemplate,
34
35
  Nothing,
35
36
  NothingType,
@@ -45,6 +46,7 @@ from localstack.services.cloudformation.engine.v2.change_set_model_visitor impor
45
46
  ChangeSetModelVisitor,
46
47
  )
47
48
  from localstack.services.cloudformation.engine.v2.resolving import (
49
+ REGEX_DYNAMIC_REF,
48
50
  extract_dynamic_reference,
49
51
  perform_dynamic_reference_lookup,
50
52
  )
@@ -55,6 +57,7 @@ from localstack.services.cloudformation.stores import (
55
57
  from localstack.services.cloudformation.v2.entities import ChangeSet
56
58
  from localstack.services.cloudformation.v2.types import ResolvedResource
57
59
  from localstack.utils.aws.arns import get_partition
60
+ from localstack.utils.numbers import to_number
58
61
  from localstack.utils.objects import get_value_from_path
59
62
  from localstack.utils.run import to_str
60
63
  from localstack.utils.strings import to_bytes
@@ -224,6 +227,8 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
224
227
  def process(self) -> None:
225
228
  self._setup_runtime_cache()
226
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)
227
232
  self.visit(node_template)
228
233
  self._save_runtime_cache()
229
234
 
@@ -273,10 +278,10 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
273
278
  property_value: Any | None = get_value_from_path(properties, property_name)
274
279
 
275
280
  if property_value:
276
- if not isinstance(property_value, (str, list)):
277
- # TODO: is this correct? If there is a bug in the logic here, it's probably
278
- # better to know about it with a clear error message than to receive some form
279
- # 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.
280
285
  raise RuntimeError(
281
286
  f"Accessing property '{property_name}' from '{resource_logical_id}' resulted in a non-string value nor list"
282
287
  )
@@ -432,6 +437,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
432
437
  def _perform_dynamic_replacements(self, value: _T) -> _T:
433
438
  if not isinstance(value, str):
434
439
  return value
440
+
435
441
  if dynamic_ref := extract_dynamic_reference(value):
436
442
  new_value = perform_dynamic_reference_lookup(
437
443
  reference=dynamic_ref,
@@ -439,7 +445,11 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
439
445
  region_name=self._change_set.region_name,
440
446
  )
441
447
  if new_value:
442
- return 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)
443
453
 
444
454
  return value
445
455
 
@@ -559,13 +569,49 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
559
569
  arguments_list = arguments.split(".")
560
570
  else:
561
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
+
562
578
  logical_name_of_resource = arguments_list[0]
563
- attribute_name = arguments_list[1]
579
+ attribute_name = ".".join(arguments_list[1:])
564
580
 
565
581
  node_resource = self._get_node_resource_for(
566
582
  resource_name=logical_name_of_resource,
567
583
  node_template=self._change_set.update_model.node_template,
568
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
+
569
615
  node_property: NodeProperty | None = self._get_node_property_for(
570
616
  property_name=attribute_name, node_resource=node_resource
571
617
  )
@@ -573,19 +619,7 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
573
619
  # The property is statically defined in the template and its value can be computed.
574
620
  property_delta = self.visit(node_property)
575
621
  value = property_delta.before if select_before else property_delta.after
576
- else:
577
- # The property is not statically defined and must therefore be available in
578
- # the properties deployed set.
579
- if select_before:
580
- value = self._before_deployed_property_value_of(
581
- resource_logical_id=logical_name_of_resource,
582
- property_name=attribute_name,
583
- )
584
- else:
585
- value = self._after_deployed_property_value_of(
586
- resource_logical_id=logical_name_of_resource,
587
- property_name=attribute_name,
588
- )
622
+
589
623
  return value
590
624
 
591
625
  def visit_node_intrinsic_function_fn_get_att(
@@ -614,6 +648,12 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
614
648
  return args[0] == args[1]
615
649
 
616
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
+
617
657
  delta = self._cached_apply(
618
658
  scope=node_intrinsic_function.scope,
619
659
  arguments_delta=arguments_delta,
@@ -843,13 +883,27 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
843
883
  ):
844
884
  # TODO: add further support for schema validation
845
885
  def _compute_fn_select(args: list[Any]) -> Any:
846
- 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
+
847
891
  if not isinstance(values, list) or not values:
848
- 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
+
849
902
  values_len = len(values)
850
- index: int = int(args[0])
851
- if not isinstance(index, int) or index < 0 or index > values_len:
852
- 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
+ )
853
907
  selection = values[index]
854
908
  return selection
855
909
 
@@ -876,6 +930,17 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
876
930
  return split_string
877
931
 
878
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
+
879
944
  delta = self._cached_apply(
880
945
  scope=node_intrinsic_function.scope,
881
946
  arguments_delta=arguments_delta,
@@ -975,6 +1040,10 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
975
1040
  return PreprocEntityDelta(before=before_parameters, after=after_parameters)
976
1041
 
977
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
+ )
978
1047
  dynamic_value = node_parameter.dynamic_value
979
1048
  dynamic_delta = self.visit(dynamic_value)
980
1049
 
@@ -990,6 +1059,9 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
990
1059
  match type_:
991
1060
  case "List<String>" | "CommaDelimitedList":
992
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)
993
1065
  return value
994
1066
 
995
1067
  if not is_nothing(after):
@@ -1131,6 +1203,20 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1131
1203
  after = after_delta.after
1132
1204
  return PreprocEntityDelta(before=before, after=after)
1133
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
+
1134
1220
  def visit_node_resource(
1135
1221
  self, node_resource: NodeResource
1136
1222
  ) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
@@ -1241,6 +1327,14 @@ class ChangeSetModelPreproc(ChangeSetModelVisitor):
1241
1327
  before: list[PreprocOutput] = []
1242
1328
  after: list[PreprocOutput] = []
1243
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
+
1244
1338
  output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output)
1245
1339
  output_before = output_delta.before
1246
1340
  output_after = output_delta.after
@@ -501,7 +501,10 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
501
501
  def visit_node_intrinsic_function_fn_get_att(
502
502
  self, node_intrinsic_function: NodeIntrinsicFunction
503
503
  ) -> PreprocEntityDelta:
504
- return self.visit(node_intrinsic_function.arguments)
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)
505
508
 
506
509
  def visit_node_intrinsic_function_fn_sub(
507
510
  self, node_intrinsic_function: NodeIntrinsicFunction
@@ -4,7 +4,6 @@ from typing import Any
4
4
  from botocore.exceptions import ParamValidationError
5
5
 
6
6
  from localstack.services.cloudformation.engine.v2.change_set_model import (
7
- Maybe,
8
7
  NodeIntrinsicFunction,
9
8
  NodeProperty,
10
9
  NodeResource,
@@ -27,23 +26,15 @@ class ChangeSetModelValidator(ChangeSetModelPreproc):
27
26
  def visit_node_template(self, node_template: NodeTemplate):
28
27
  self.visit(node_template.mappings)
29
28
  self.visit(node_template.resources)
29
+ self.visit(node_template.parameters)
30
30
 
31
31
  def visit_node_intrinsic_function_fn_get_att(
32
32
  self, node_intrinsic_function: NodeIntrinsicFunction
33
33
  ) -> PreprocEntityDelta:
34
- arguments_delta = self.visit(node_intrinsic_function.arguments)
35
- before_arguments: Maybe[str | list[str]] = arguments_delta.before
36
- after_arguments: Maybe[str | list[str]] = arguments_delta.after
37
-
38
- before = self._before_cache.get(node_intrinsic_function.scope, Nothing)
39
- if is_nothing(before) and not is_nothing(before_arguments):
40
- before = ".".join(before_arguments)
41
-
42
- after = self._after_cache.get(node_intrinsic_function.scope, Nothing)
43
- if is_nothing(after) and not is_nothing(after_arguments):
44
- after = ".".join(after_arguments)
45
-
46
- return PreprocEntityDelta(before=before, after=after)
34
+ try:
35
+ return super().visit_node_intrinsic_function_fn_get_att(node_intrinsic_function)
36
+ except RuntimeError:
37
+ return self.visit(node_intrinsic_function.arguments)
47
38
 
48
39
  def visit_node_intrinsic_function_fn_sub(
49
40
  self, node_intrinsic_function: NodeIntrinsicFunction
@@ -54,6 +54,7 @@ class ChangeSetModelVisitor(abc.ABC):
54
54
  # entities (parameters, mappings, conditions, etc.). Then compute the output fields; computing
55
55
  # only the output fields would only result in the deployment logic of the referenced outputs
56
56
  # being evaluated, hence enforce the visiting of all the resources first.
57
+ self.visit(node_template.conditions)
57
58
  self.visit(node_template.resources)
58
59
  self.visit(node_template.outputs)
59
60
 
@@ -10,7 +10,9 @@ from localstack.aws.connect import connect_to
10
10
 
11
11
  LOG = logging.getLogger(__name__)
12
12
 
13
- REGEX_DYNAMIC_REF = re.compile(r"{{resolve:([^:]+):(.+)}}")
13
+ # CloudFormation allows using dynamic references in `Fn::Sub` expressions, so we must make sure
14
+ # we don't capture the parameter usage by excluding ${} characters
15
+ REGEX_DYNAMIC_REF = re.compile(r"{{resolve:([^:]+):([^${}]+)}}")
14
16
 
15
17
 
16
18
  @dataclass
@@ -21,7 +23,7 @@ class DynamicReference:
21
23
 
22
24
  def extract_dynamic_reference(value: Any) -> DynamicReference | None:
23
25
  if isinstance(value, str):
24
- if dynamic_ref_match := REGEX_DYNAMIC_REF.match(value):
26
+ if dynamic_ref_match := REGEX_DYNAMIC_REF.search(value):
25
27
  return DynamicReference(dynamic_ref_match[1], dynamic_ref_match[2])
26
28
  return None
27
29
 
@@ -90,9 +92,9 @@ def perform_dynamic_reference_lookup(
90
92
  raise RuntimeError(
91
93
  f"JSON value for {reference.service_name}.{reference.reference_key} not present"
92
94
  )
93
- return json_secret[json_key]
95
+ return str(json_secret[json_key])
94
96
  else:
95
- return secret_value
97
+ return str(secret_value)
96
98
 
97
99
  LOG.warning(
98
100
  "Unsupported service for dynamic parameter: service_name=%s", reference.service_name
@@ -1,5 +1,7 @@
1
1
  import yaml
2
2
 
3
+ from localstack.services.cloudformation.engine.validations import ValidationError
4
+
3
5
 
4
6
  def construct_raw(_, node):
5
7
  return node.value
@@ -60,5 +62,10 @@ customloader = NoDatesSafeLoader
60
62
  yaml.add_multi_constructor("!", shorthand_constructor, customloader)
61
63
 
62
64
 
63
- def parse_yaml(input_data: str):
64
- return yaml.load(input_data, customloader)
65
+ def parse_yaml(input_data: str) -> dict:
66
+ parsed = yaml.load(input_data, Loader=customloader)
67
+
68
+ if not isinstance(parsed, dict):
69
+ raise ValidationError("Template format error: unsupported structure.")
70
+
71
+ return parsed