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.
- localstack/aws/api/acm/__init__.py +122 -122
- localstack/aws/api/apigateway/__init__.py +560 -559
- localstack/aws/api/cloudcontrol/__init__.py +63 -63
- localstack/aws/api/cloudformation/__init__.py +1041 -969
- localstack/aws/api/cloudwatch/__init__.py +408 -368
- localstack/aws/api/config/__init__.py +788 -786
- localstack/aws/api/core.py +4 -0
- localstack/aws/api/dynamodb/__init__.py +753 -759
- localstack/aws/api/dynamodbstreams/__init__.py +74 -74
- localstack/aws/api/ec2/__init__.py +9713 -8573
- localstack/aws/api/es/__init__.py +453 -453
- localstack/aws/api/events/__init__.py +552 -552
- localstack/aws/api/firehose/__init__.py +541 -543
- localstack/aws/api/iam/__init__.py +646 -572
- localstack/aws/api/kinesis/__init__.py +251 -144
- localstack/aws/api/kms/__init__.py +343 -333
- localstack/aws/api/lambda_/__init__.py +585 -571
- localstack/aws/api/logs/__init__.py +682 -666
- localstack/aws/api/opensearch/__init__.py +814 -785
- localstack/aws/api/pipes/__init__.py +336 -336
- localstack/aws/api/redshift/__init__.py +1192 -1164
- localstack/aws/api/resource_groups/__init__.py +175 -175
- localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
- localstack/aws/api/route53/__init__.py +256 -254
- localstack/aws/api/route53resolver/__init__.py +396 -396
- localstack/aws/api/s3/__init__.py +1358 -1345
- localstack/aws/api/s3control/__init__.py +616 -584
- localstack/aws/api/scheduler/__init__.py +118 -118
- localstack/aws/api/secretsmanager/__init__.py +193 -193
- localstack/aws/api/ses/__init__.py +227 -227
- localstack/aws/api/sns/__init__.py +115 -115
- localstack/aws/api/sqs/__init__.py +100 -100
- localstack/aws/api/ssm/__init__.py +1978 -1970
- localstack/aws/api/stepfunctions/__init__.py +323 -323
- localstack/aws/api/sts/__init__.py +90 -66
- localstack/aws/api/support/__init__.py +112 -112
- localstack/aws/api/swf/__init__.py +378 -386
- localstack/aws/api/transcribe/__init__.py +425 -425
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +43 -10
- localstack/aws/protocol/parser.py +440 -21
- localstack/aws/protocol/serializer.py +684 -64
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/scaffold.py +15 -17
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +33 -13
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +10 -5
- localstack/cli/lpm.py +3 -4
- localstack/cli/profiles.py +1 -2
- localstack/config.py +18 -12
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +39 -4
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +12 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/init.py +1 -1
- localstack/services/apigateway/helpers.py +5 -9
- localstack/services/apigateway/legacy/provider.py +85 -12
- localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
- localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/apigateway/patches.py +0 -9
- localstack/services/cloudformation/engine/entities.py +12 -1
- localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +6 -4
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/provider.py +2 -2
- localstack/services/cloudformation/resource_provider.py +5 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/v2/entities.py +6 -3
- localstack/services/cloudformation/v2/provider.py +178 -33
- localstack/services/cloudformation/v2/types.py +8 -4
- localstack/services/cloudwatch/provider_v2.py +25 -28
- localstack/services/dynamodb/packages.py +2 -1
- localstack/services/dynamodb/provider.py +42 -0
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/es/provider.py +2 -2
- localstack/services/events/event_rule_engine.py +31 -13
- localstack/services/events/models.py +4 -5
- localstack/services/events/target.py +17 -9
- localstack/services/iam/provider.py +11 -116
- localstack/services/iam/resources/policy_simulator.py +133 -0
- localstack/services/kinesis/models.py +15 -2
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kinesis/provider.py +77 -0
- localstack/services/kms/models.py +34 -4
- localstack/services/kms/provider.py +107 -21
- localstack/services/lambda_/api_utils.py +3 -1
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/lambda_/provider.py +1 -1
- localstack/services/lambda_/runtimes.py +8 -3
- localstack/services/logs/provider.py +36 -19
- localstack/services/moto.py +2 -1
- localstack/services/opensearch/cluster.py +15 -7
- localstack/services/opensearch/packages.py +26 -7
- localstack/services/opensearch/provider.py +6 -1
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/notifications.py +1 -1
- localstack/services/s3/presigned_url.py +27 -43
- localstack/services/s3/provider.py +68 -12
- localstack/services/s3/utils.py +42 -11
- localstack/services/ses/provider.py +16 -7
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/v2/models.py +190 -0
- localstack/services/sns/v2/provider.py +992 -2
- localstack/services/sns/v2/utils.py +138 -0
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +79 -13
- localstack/services/sqs/provider.py +8 -309
- localstack/services/sqs/query_api.py +1 -1
- localstack/services/sqs/utils.py +121 -2
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +20 -19
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +13 -4
- localstack/testing/pytest/stepfunctions/utils.py +4 -3
- localstack/testing/pytest/util.py +1 -1
- localstack/testing/pytest/validation_tracking.py +1 -2
- localstack/testing/snapshots/transformer_utility.py +7 -0
- localstack/testing/testselection/matching.py +0 -1
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +1 -2
- localstack/utils/analytics/metrics/counter.py +6 -8
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/aws/arns.py +17 -9
- localstack/utils/aws/aws_responses.py +7 -7
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/client_types.py +0 -8
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +7 -7
- localstack/utils/catalog/catalog.py +139 -0
- localstack/utils/catalog/catalog_loader.py +119 -0
- localstack/utils/catalog/common.py +58 -0
- localstack/utils/catalog/plugins.py +28 -0
- localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
- localstack/utils/collections.py +7 -8
- localstack/utils/config_listener.py +1 -1
- localstack/utils/container_networking.py +2 -3
- localstack/utils/container_utils/container_client.py +115 -131
- localstack/utils/container_utils/docker_cmd_client.py +42 -42
- localstack/utils/container_utils/docker_sdk_client.py +63 -62
- localstack/utils/crypto.py +109 -0
- localstack/utils/diagnose.py +2 -3
- localstack/utils/docker_utils.py +3 -4
- localstack/utils/files.py +31 -7
- localstack/utils/functions.py +3 -2
- localstack/utils/http.py +4 -5
- localstack/utils/json.py +19 -5
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +6 -6
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +6 -5
- localstack/utils/patch.py +2 -1
- localstack/utils/run.py +10 -9
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +2 -3
- localstack/utils/strings.py +10 -11
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +1 -4
- localstack/utils/testutil.py +5 -4
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +11 -3
- localstack/utils/urls.py +1 -3
- localstack/version.py +2 -2
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
- localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
- localstack/packages/terraform.py +0 -46
- localstack/services/cloudformation/deploy.html +0 -144
- localstack/services/cloudformation/deploy_ui.py +0 -47
- localstack/services/cloudformation/plugins.py +0 -12
- localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
- {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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|