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.
- localstack/aws/api/cloudformation/__init__.py +18 -4
- localstack/aws/api/cloudwatch/__init__.py +41 -1
- localstack/aws/api/config/__init__.py +4 -0
- localstack/aws/api/core.py +6 -2
- localstack/aws/api/dynamodb/__init__.py +30 -0
- localstack/aws/api/ec2/__init__.py +1522 -65
- localstack/aws/api/iam/__init__.py +7 -0
- localstack/aws/api/kinesis/__init__.py +19 -0
- localstack/aws/api/kms/__init__.py +6 -0
- localstack/aws/api/lambda_/__init__.py +13 -0
- localstack/aws/api/logs/__init__.py +15 -0
- localstack/aws/api/redshift/__init__.py +9 -3
- localstack/aws/api/route53/__init__.py +5 -0
- localstack/aws/api/s3/__init__.py +12 -0
- localstack/aws/api/s3control/__init__.py +54 -0
- localstack/aws/api/ssm/__init__.py +2 -0
- localstack/aws/api/transcribe/__init__.py +17 -0
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/internal_requests.py +6 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +40 -20
- localstack/aws/mocking.py +2 -2
- localstack/aws/patches.py +2 -2
- localstack/aws/protocol/parser.py +459 -32
- localstack/aws/protocol/serializer.py +689 -69
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/protocol/validate.py +1 -1
- localstack/aws/scaffold.py +1 -1
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +37 -16
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +6 -6
- localstack/cli/lpm.py +3 -4
- localstack/cli/plugins.py +1 -1
- localstack/cli/profiles.py +1 -2
- localstack/config.py +25 -18
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +130 -7
- localstack/dev/run/configurators.py +1 -4
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +13 -4
- localstack/logging/format.py +3 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/analytics.py +3 -0
- localstack/runtime/hooks.py +1 -1
- localstack/runtime/init.py +2 -2
- localstack/runtime/main.py +5 -5
- localstack/runtime/patches.py +2 -2
- localstack/services/apigateway/helpers.py +1 -4
- localstack/services/apigateway/legacy/helpers.py +7 -8
- localstack/services/apigateway/legacy/integration.py +4 -3
- localstack/services/apigateway/legacy/invocations.py +6 -5
- localstack/services/apigateway/legacy/provider.py +148 -68
- localstack/services/apigateway/legacy/templates.py +1 -1
- localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
- localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
- 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/template_mapping.py +2 -2
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
- localstack/services/cloudformation/api_utils.py +4 -8
- localstack/services/cloudformation/cfn_utils.py +1 -1
- localstack/services/cloudformation/engine/entities.py +14 -4
- localstack/services/cloudformation/engine/template_deployer.py +6 -4
- localstack/services/cloudformation/engine/transformers.py +6 -4
- localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +7 -5
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/provider.py +7 -5
- localstack/services/cloudformation/resource_provider.py +7 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/service_models.py +2 -2
- localstack/services/cloudformation/v2/entities.py +19 -9
- localstack/services/cloudformation/v2/provider.py +336 -106
- localstack/services/cloudformation/v2/types.py +13 -7
- localstack/services/cloudformation/v2/utils.py +4 -1
- localstack/services/cloudwatch/alarm_scheduler.py +4 -1
- localstack/services/cloudwatch/provider.py +18 -13
- 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/server.py +2 -2
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/edge.py +1 -1
- 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/provider.py +17 -14
- localstack/services/events/target.py +17 -9
- localstack/services/events/v1/provider.py +5 -5
- localstack/services/firehose/provider.py +14 -4
- 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/provider.py +86 -3
- localstack/services/kms/provider.py +14 -5
- localstack/services/lambda_/api_utils.py +6 -3
- localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
- localstack/services/lambda_/invocation/event_manager.py +1 -1
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/invocation/lambda_models.py +10 -7
- localstack/services/lambda_/invocation/lambda_service.py +5 -1
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/lambda_/provider.py +4 -3
- localstack/services/lambda_/provider_utils.py +1 -1
- 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 +8 -2
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/plugins.py +11 -7
- localstack/services/providers.py +10 -2
- localstack/services/redshift/provider.py +0 -21
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/models.py +1 -1
- localstack/services/s3/notifications.py +55 -39
- localstack/services/s3/presigned_url.py +35 -54
- localstack/services/s3/provider.py +73 -15
- localstack/services/s3/utils.py +42 -22
- localstack/services/s3/validation.py +46 -32
- localstack/services/s3/website_hosting.py +4 -2
- localstack/services/ses/provider.py +18 -8
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/executor.py +9 -2
- localstack/services/sns/provider.py +8 -5
- localstack/services/sns/publisher.py +31 -16
- localstack/services/sns/v2/models.py +167 -0
- localstack/services/sns/v2/provider.py +867 -0
- localstack/services/sns/v2/utils.py +130 -0
- localstack/services/sqs/constants.py +1 -1
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +48 -5
- localstack/services/sqs/provider.py +38 -311
- localstack/services/sqs/query_api.py +6 -2
- localstack/services/sqs/utils.py +121 -2
- localstack/services/ssm/provider.py +1 -1
- localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
- localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
- localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
- localstack/services/stepfunctions/asl/eval/environment.py +1 -1
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/services/stepfunctions/backend/execution.py +2 -1
- localstack/services/stores.py +1 -1
- localstack/services/transcribe/provider.py +6 -1
- localstack/state/codecs.py +61 -0
- localstack/state/core.py +11 -5
- localstack/state/pickle.py +10 -49
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/cloudformation/transformers.py +0 -0
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +33 -31
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +38 -11
- 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 +6 -1
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +6 -4
- localstack/utils/analytics/metrics/counter.py +8 -15
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_providers.py +19 -0
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/asyncio.py +2 -2
- localstack/utils/aws/arns.py +24 -29
- localstack/utils/aws/aws_responses.py +8 -8
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/dead_letter_queue.py +1 -5
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/aws/resources.py +1 -1
- localstack/utils/aws/templating.py +1 -1
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +21 -13
- 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 +135 -136
- localstack/utils/container_utils/docker_cmd_client.py +85 -69
- localstack/utils/container_utils/docker_sdk_client.py +69 -66
- localstack/utils/crypto.py +10 -10
- localstack/utils/diagnose.py +3 -4
- localstack/utils/docker_utils.py +9 -5
- localstack/utils/files.py +33 -13
- localstack/utils/functions.py +4 -3
- localstack/utils/http.py +11 -11
- localstack/utils/json.py +20 -6
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +15 -9
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +7 -6
- localstack/utils/patch.py +10 -3
- localstack/utils/run.py +12 -11
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +3 -4
- localstack/utils/strings.py +15 -16
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +8 -6
- localstack/utils/testutil.py +8 -8
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +12 -4
- localstack/utils/urls.py +1 -3
- localstack/utils/xray/traceid.py +1 -1
- localstack/version.py +16 -3
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
- localstack_core-4.10.1.dev12.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.dev49.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
|
@@ -38,7 +38,6 @@ from localstack.aws.api.s3 import Type as GranteeType
|
|
|
38
38
|
from localstack.services.s3 import constants as s3_constants
|
|
39
39
|
from localstack.services.s3.exceptions import InvalidRequest, MalformedACLError, MalformedXML
|
|
40
40
|
from localstack.services.s3.utils import (
|
|
41
|
-
_create_invalid_argument_exc,
|
|
42
41
|
get_class_attrs_from_spec_class,
|
|
43
42
|
get_permission_header_name,
|
|
44
43
|
is_bucket_name_valid,
|
|
@@ -87,8 +86,11 @@ def validate_canned_acl(canned_acl: str) -> None:
|
|
|
87
86
|
Validate the canned ACL value, or raise an Exception
|
|
88
87
|
"""
|
|
89
88
|
if canned_acl and canned_acl not in VALID_CANNED_ACLS:
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
raise InvalidArgument(
|
|
90
|
+
None,
|
|
91
|
+
ArgumentName="x-amz-acl",
|
|
92
|
+
ArgumentValue=canned_acl,
|
|
93
|
+
)
|
|
92
94
|
|
|
93
95
|
|
|
94
96
|
def parse_grants_in_headers(permission: Permission, grantees: str) -> Grants:
|
|
@@ -98,16 +100,18 @@ def parse_grants_in_headers(permission: Permission, grantees: str) -> Grants:
|
|
|
98
100
|
grantee_type, grantee_id = seralized_grantee.split("=")
|
|
99
101
|
grantee_id = grantee_id.strip('"')
|
|
100
102
|
if grantee_type not in ("uri", "id", "emailAddress"):
|
|
101
|
-
|
|
103
|
+
raise InvalidArgument(
|
|
102
104
|
"Argument format not recognized",
|
|
103
|
-
get_permission_header_name(permission),
|
|
104
|
-
seralized_grantee,
|
|
105
|
+
ArgumentName=get_permission_header_name(permission),
|
|
106
|
+
ArgumentValue=seralized_grantee,
|
|
105
107
|
)
|
|
106
|
-
raise ex
|
|
107
108
|
elif grantee_type == "uri":
|
|
108
109
|
if grantee_id not in s3_constants.VALID_ACL_PREDEFINED_GROUPS:
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
raise InvalidArgument(
|
|
111
|
+
"Invalid group uri",
|
|
112
|
+
ArgumentName="uri",
|
|
113
|
+
ArgumentValue=grantee_id,
|
|
114
|
+
)
|
|
111
115
|
grantee = Grantee(
|
|
112
116
|
Type=GranteeType.Group,
|
|
113
117
|
URI=grantee_id,
|
|
@@ -115,8 +119,11 @@ def parse_grants_in_headers(permission: Permission, grantees: str) -> Grants:
|
|
|
115
119
|
|
|
116
120
|
elif grantee_type == "id":
|
|
117
121
|
if not is_valid_canonical_id(grantee_id):
|
|
118
|
-
|
|
119
|
-
|
|
122
|
+
raise InvalidArgument(
|
|
123
|
+
"Invalid id",
|
|
124
|
+
ArgumentName="id",
|
|
125
|
+
ArgumentValue=grantee_id,
|
|
126
|
+
)
|
|
120
127
|
grantee = Grantee(
|
|
121
128
|
Type=GranteeType.CanonicalUser,
|
|
122
129
|
ID=grantee_id,
|
|
@@ -141,8 +148,11 @@ def validate_acl_acp(acp: AccessControlPolicy) -> None:
|
|
|
141
148
|
)
|
|
142
149
|
|
|
143
150
|
if not is_valid_canonical_id(owner_id := acp["Owner"].get("ID", "")):
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
raise InvalidArgument(
|
|
152
|
+
"Invalid id",
|
|
153
|
+
ArgumentName="CanonicalUser/ID",
|
|
154
|
+
ArgumentValue=owner_id,
|
|
155
|
+
)
|
|
146
156
|
|
|
147
157
|
for grant in acp["Grants"]:
|
|
148
158
|
if grant.get("Permission") not in s3_constants.VALID_GRANTEE_PERMISSIONS:
|
|
@@ -165,8 +175,11 @@ def validate_acl_acp(acp: AccessControlPolicy) -> None:
|
|
|
165
175
|
and (grant_uri := grantee.get("URI", ""))
|
|
166
176
|
not in s3_constants.VALID_ACL_PREDEFINED_GROUPS
|
|
167
177
|
):
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
raise InvalidArgument(
|
|
179
|
+
"Invalid group uri",
|
|
180
|
+
ArgumentName="Group/URI",
|
|
181
|
+
ArgumentValue=grant_uri,
|
|
182
|
+
)
|
|
170
183
|
|
|
171
184
|
elif grant_type == GranteeType.AmazonCustomerByEmail:
|
|
172
185
|
# TODO: add validation here
|
|
@@ -175,8 +188,11 @@ def validate_acl_acp(acp: AccessControlPolicy) -> None:
|
|
|
175
188
|
elif grant_type == GranteeType.CanonicalUser and not is_valid_canonical_id(
|
|
176
189
|
grantee_id := grantee.get("ID", "")
|
|
177
190
|
):
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
raise InvalidArgument(
|
|
192
|
+
"Invalid id",
|
|
193
|
+
ArgumentName="CanonicalUser/ID",
|
|
194
|
+
ArgumentValue=grantee_id,
|
|
195
|
+
)
|
|
180
196
|
|
|
181
197
|
|
|
182
198
|
def validate_lifecycle_configuration(lifecycle_conf: BucketLifecycleConfiguration) -> None:
|
|
@@ -242,12 +258,12 @@ def validate_website_configuration(website_config: WebsiteConfiguration) -> None
|
|
|
242
258
|
"""
|
|
243
259
|
if redirect_all_req := website_config.get("RedirectAllRequestsTo", {}):
|
|
244
260
|
if len(website_config) > 1:
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
261
|
+
raise InvalidArgument(
|
|
262
|
+
"RedirectAllRequestsTo cannot be provided in conjunction with other Routing Rules.",
|
|
263
|
+
ArgumentName="RedirectAllRequestsTo",
|
|
264
|
+
ArgumentValue="not null",
|
|
249
265
|
)
|
|
250
|
-
|
|
266
|
+
|
|
251
267
|
if "HostName" not in redirect_all_req:
|
|
252
268
|
raise MalformedXML()
|
|
253
269
|
|
|
@@ -261,20 +277,18 @@ def validate_website_configuration(website_config: WebsiteConfiguration) -> None
|
|
|
261
277
|
# required
|
|
262
278
|
# https://docs.aws.amazon.com/AmazonS3/latest/API/API_IndexDocument.html
|
|
263
279
|
if not (index_configuration := website_config.get("IndexDocument")):
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
280
|
+
raise InvalidArgument(
|
|
281
|
+
"A value for IndexDocument Suffix must be provided if RedirectAllRequestsTo is empty",
|
|
282
|
+
ArgumentName="IndexDocument",
|
|
283
|
+
ArgumentValue="null",
|
|
268
284
|
)
|
|
269
|
-
raise ex
|
|
270
285
|
|
|
271
286
|
if not (index_suffix := index_configuration.get("Suffix")) or "/" in index_suffix:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
287
|
+
raise InvalidArgument(
|
|
288
|
+
"The IndexDocument Suffix is not well formed",
|
|
289
|
+
ArgumentName="IndexDocument",
|
|
290
|
+
ArgumentValue=index_suffix or None,
|
|
276
291
|
)
|
|
277
|
-
raise ex
|
|
278
292
|
|
|
279
293
|
if "ErrorDocument" in website_config and not website_config.get("ErrorDocument", {}).get("Key"):
|
|
280
294
|
raise MalformedXML()
|
|
@@ -93,8 +93,10 @@ class S3WebsiteHostingHandler:
|
|
|
93
93
|
return Response(response_body, status=e.response["ResponseMetadata"]["HTTPStatusCode"])
|
|
94
94
|
|
|
95
95
|
except Exception:
|
|
96
|
-
LOG.
|
|
97
|
-
"Exception encountered while trying to serve s3-website at %s",
|
|
96
|
+
LOG.error(
|
|
97
|
+
"Exception encountered while trying to serve s3-website at %s",
|
|
98
|
+
request.url,
|
|
99
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
98
100
|
)
|
|
99
101
|
return Response(_create_500_error_string(), status=500)
|
|
100
102
|
|
|
@@ -182,6 +182,8 @@ class SesProvider(SesApi, ServiceLifecycleHook):
|
|
|
182
182
|
#
|
|
183
183
|
|
|
184
184
|
def on_after_init(self):
|
|
185
|
+
self._apply_patches()
|
|
186
|
+
|
|
185
187
|
# Allow sent emails to be retrieved from the SES emails endpoint
|
|
186
188
|
register_ses_api_resource()
|
|
187
189
|
|
|
@@ -197,6 +199,12 @@ class SesProvider(SesApi, ServiceLifecycleHook):
|
|
|
197
199
|
return entity.replace("From:", "").strip()
|
|
198
200
|
return None
|
|
199
201
|
|
|
202
|
+
def _apply_patches(self) -> None:
|
|
203
|
+
# Suppress Moto's validation of receipt rule actions. These validations use Moto's implementation of S3, Lambda
|
|
204
|
+
# and SQS, which fail because these services have been internalised in LocalStack.
|
|
205
|
+
# Besides, AWS does not run the same validations as evidenced by our AWS-validated tests.
|
|
206
|
+
SESBackend._validate_receipt_rule_actions = lambda *_: None
|
|
207
|
+
|
|
200
208
|
#
|
|
201
209
|
# Implementations for SES operations
|
|
202
210
|
#
|
|
@@ -517,8 +525,10 @@ class SesProvider(SesApi, ServiceLifecycleHook):
|
|
|
517
525
|
backend.create_receipt_rule_set(rule_set_name)
|
|
518
526
|
original_rule_set = backend.describe_receipt_rule_set(original_rule_set_name)
|
|
519
527
|
|
|
520
|
-
|
|
521
|
-
|
|
528
|
+
after = None
|
|
529
|
+
for rule in original_rule_set.rules:
|
|
530
|
+
backend.create_receipt_rule(rule_set_name, rule, after)
|
|
531
|
+
after = rule["Name"]
|
|
522
532
|
|
|
523
533
|
return CloneReceiptRuleSetResponse()
|
|
524
534
|
|
|
@@ -547,7 +557,7 @@ class SesProvider(SesApi, ServiceLifecycleHook):
|
|
|
547
557
|
)
|
|
548
558
|
|
|
549
559
|
backend = get_ses_backend(context)
|
|
550
|
-
if identity not in backend.
|
|
560
|
+
if identity not in backend.email_identities:
|
|
551
561
|
raise MessageRejected(f"Identity {identity} is not verified or does not exist.")
|
|
552
562
|
|
|
553
563
|
# Store the setting in the backend
|
|
@@ -626,7 +636,7 @@ class SNSEmitter:
|
|
|
626
636
|
Subject="Amazon SES Email Event Notification",
|
|
627
637
|
)
|
|
628
638
|
except ClientError:
|
|
629
|
-
LOGGER.
|
|
639
|
+
LOGGER.error("sending SNS message", exc_info=LOGGER.isEnabledFor(logging.DEBUG))
|
|
630
640
|
|
|
631
641
|
def emit_delivery_event(self, payload: EventDestinationPayload, sns_topic_arn: str):
|
|
632
642
|
now = datetime.now(tz=UTC)
|
|
@@ -659,7 +669,7 @@ class SNSEmitter:
|
|
|
659
669
|
Subject="Amazon SES Email Event Notification",
|
|
660
670
|
)
|
|
661
671
|
except ClientError:
|
|
662
|
-
LOGGER.
|
|
672
|
+
LOGGER.error("sending SNS message", exc_info=LOGGER.isEnabledFor(logging.DEBUG))
|
|
663
673
|
|
|
664
674
|
@staticmethod
|
|
665
675
|
def _client_for_topic(topic_arn: str) -> "SNSClient":
|
|
@@ -677,7 +687,7 @@ class SNSEmitter:
|
|
|
677
687
|
def notify_event_destinations(
|
|
678
688
|
context: RequestContext,
|
|
679
689
|
# FIXME: Moto stores the Event Destinations as a single value when it should be a list
|
|
680
|
-
event_destinations:
|
|
690
|
+
event_destinations: EventDestination | list[EventDestination],
|
|
681
691
|
payload: EventDestinationPayload,
|
|
682
692
|
email_type: EmailType,
|
|
683
693
|
):
|
|
@@ -690,11 +700,11 @@ def notify_event_destinations(
|
|
|
690
700
|
if not event_destination["Enabled"]:
|
|
691
701
|
continue
|
|
692
702
|
|
|
693
|
-
sns_destination_arn = event_destination.get("SNSDestination")
|
|
703
|
+
sns_destination_arn = event_destination.get("SNSDestination", {}).get("TopicARN")
|
|
694
704
|
if not sns_destination_arn:
|
|
695
705
|
continue
|
|
696
706
|
|
|
697
|
-
matching_event_types = event_destination.get("
|
|
707
|
+
matching_event_types = event_destination.get("MatchingEventTypes") or []
|
|
698
708
|
if EventType.send in matching_event_types:
|
|
699
709
|
emitter.emit_send_event(
|
|
700
710
|
payload, sns_destination_arn, emit_source_arn=email_type != EmailType.TEMPLATED
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from string import ascii_letters, digits
|
|
3
|
+
from typing import get_args
|
|
4
|
+
|
|
5
|
+
from localstack.services.sns.v2.models import SnsApplicationPlatforms
|
|
3
6
|
|
|
4
7
|
SNS_PROTOCOLS = [
|
|
5
8
|
"http",
|
|
@@ -13,7 +16,7 @@ SNS_PROTOCOLS = [
|
|
|
13
16
|
"firehose",
|
|
14
17
|
]
|
|
15
18
|
|
|
16
|
-
VALID_SUBSCRIPTION_ATTR_NAME = [
|
|
19
|
+
VALID_SUBSCRIPTION_ATTR_NAME: list[str] = [
|
|
17
20
|
"DeliveryPolicy",
|
|
18
21
|
"FilterPolicy",
|
|
19
22
|
"FilterPolicyScope",
|
|
@@ -39,3 +42,6 @@ SUBSCRIPTION_TOKENS_ENDPOINT = "/_aws/sns/subscription-tokens"
|
|
|
39
42
|
SNS_CERT_ENDPOINT = "/_aws/sns/SimpleNotificationService-6c6f63616c737461636b69736e696365.pem"
|
|
40
43
|
|
|
41
44
|
DUMMY_SUBSCRIPTION_PRINCIPAL = "arn:{partition}:iam::{account_id}:user/DummySNSPrincipal"
|
|
45
|
+
E164_REGEX = re.compile(r"^\+?[1-9]\d{1,14}$")
|
|
46
|
+
|
|
47
|
+
VALID_APPLICATION_PLATFORMS = list(get_args(SnsApplicationPlatforms))
|
|
@@ -18,7 +18,10 @@ def _worker(work_queue: queue.Queue):
|
|
|
18
18
|
del work_item
|
|
19
19
|
|
|
20
20
|
except Exception:
|
|
21
|
-
LOG.
|
|
21
|
+
LOG.error(
|
|
22
|
+
"Exception in worker",
|
|
23
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
24
|
+
)
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class _WorkItem:
|
|
@@ -31,7 +34,11 @@ class _WorkItem:
|
|
|
31
34
|
try:
|
|
32
35
|
self.fn(*self.args, **self.kwargs)
|
|
33
36
|
except Exception:
|
|
34
|
-
LOG.
|
|
37
|
+
LOG.error(
|
|
38
|
+
"Unhandled Exception in while running %s",
|
|
39
|
+
self.fn.__name__,
|
|
40
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
41
|
+
)
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
class TopicPartitionedThreadPoolExecutor:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
import contextlib
|
|
2
3
|
import copy
|
|
3
4
|
import functools
|
|
4
5
|
import json
|
|
@@ -419,6 +420,10 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
|
|
|
419
420
|
def unsubscribe(
|
|
420
421
|
self, context: RequestContext, subscription_arn: subscriptionARN, **kwargs
|
|
421
422
|
) -> None:
|
|
423
|
+
if subscription_arn is None:
|
|
424
|
+
raise InvalidParameterException(
|
|
425
|
+
"Invalid parameter: SubscriptionArn Reason: no value for required parameter",
|
|
426
|
+
)
|
|
422
427
|
count = len(subscription_arn.split(":"))
|
|
423
428
|
try:
|
|
424
429
|
parsed_arn = parse_arn(subscription_arn)
|
|
@@ -469,7 +474,8 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
|
|
|
469
474
|
subscription_arn=subscription_arn,
|
|
470
475
|
)
|
|
471
476
|
|
|
472
|
-
|
|
477
|
+
with contextlib.suppress(ValueError):
|
|
478
|
+
store.topic_subscriptions[subscription["TopicArn"]].remove(subscription_arn)
|
|
473
479
|
store.subscription_filter_policy.pop(subscription_arn, None)
|
|
474
480
|
store.subscriptions.pop(subscription_arn, None)
|
|
475
481
|
|
|
@@ -583,10 +589,7 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
|
|
|
583
589
|
raise InvalidParameterException(
|
|
584
590
|
"Invalid parameter: MessageDeduplicationId Reason: The request includes MessageDeduplicationId parameter that is not valid for this topic type"
|
|
585
591
|
)
|
|
586
|
-
|
|
587
|
-
raise InvalidParameterException(
|
|
588
|
-
"Invalid parameter: MessageGroupId Reason: The request includes MessageGroupId parameter that is not valid for this topic type"
|
|
589
|
-
)
|
|
592
|
+
|
|
590
593
|
is_endpoint_publish = target_arn and ":endpoint/" in target_arn
|
|
591
594
|
if message_structure == "json":
|
|
592
595
|
try:
|
|
@@ -89,9 +89,10 @@ class TopicPublisher(abc.ABC):
|
|
|
89
89
|
try:
|
|
90
90
|
self._publish(context=context, subscriber=subscriber)
|
|
91
91
|
except Exception:
|
|
92
|
-
LOG.
|
|
92
|
+
LOG.error(
|
|
93
93
|
"An internal error occurred while trying to send the SNS message %s",
|
|
94
94
|
context.message,
|
|
95
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
95
96
|
)
|
|
96
97
|
return
|
|
97
98
|
|
|
@@ -147,9 +148,10 @@ class EndpointPublisher(abc.ABC):
|
|
|
147
148
|
try:
|
|
148
149
|
self._publish(context=context, endpoint=endpoint)
|
|
149
150
|
except Exception:
|
|
150
|
-
LOG.
|
|
151
|
+
LOG.error(
|
|
151
152
|
"An internal error occurred while trying to send the SNS message %s",
|
|
152
153
|
context.message,
|
|
154
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
153
155
|
)
|
|
154
156
|
return
|
|
155
157
|
|
|
@@ -295,7 +297,10 @@ class SqsTopicPublisher(TopicPublisher):
|
|
|
295
297
|
)
|
|
296
298
|
kwargs = self.get_sqs_kwargs(msg_context=message_context, subscriber=subscriber)
|
|
297
299
|
except Exception:
|
|
298
|
-
LOG.
|
|
300
|
+
LOG.error(
|
|
301
|
+
"An internal error occurred while trying to format the message for SQS",
|
|
302
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
303
|
+
)
|
|
299
304
|
return
|
|
300
305
|
try:
|
|
301
306
|
queue_url: str = sqs_queue_url_for_arn(subscriber["Endpoint"])
|
|
@@ -335,19 +340,29 @@ class SqsTopicPublisher(TopicPublisher):
|
|
|
335
340
|
|
|
336
341
|
# SNS now allows regular non-fifo subscriptions to FIFO topics. Validate that the subscription target is fifo
|
|
337
342
|
# before passing the FIFO-only parameters
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
343
|
+
|
|
344
|
+
# SNS will only forward the `MessageGroupId` for Fair Queues in some scenarios:
|
|
345
|
+
# - non-FIFO SNS topic to Fair Queue
|
|
346
|
+
# - FIFO topic to FIFO queue
|
|
347
|
+
# It will NOT forward it with FIFO topic to regular Queue (possibly used for internal grouping without relying
|
|
348
|
+
# on SQS capabilities)
|
|
349
|
+
if subscriber["TopicArn"].endswith(".fifo"):
|
|
350
|
+
if subscriber["Endpoint"].endswith(".fifo"):
|
|
351
|
+
if msg_context.message_group_id:
|
|
352
|
+
kwargs["MessageGroupId"] = msg_context.message_group_id
|
|
353
|
+
if msg_context.message_deduplication_id:
|
|
354
|
+
kwargs["MessageDeduplicationId"] = msg_context.message_deduplication_id
|
|
355
|
+
else:
|
|
356
|
+
# SNS uses the message body provided to generate a unique hash value to use as the deduplication ID
|
|
357
|
+
# for each message, so you don't need to set a deduplication ID when you send each message.
|
|
358
|
+
# https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html
|
|
359
|
+
content = msg_context.message_content("sqs")
|
|
360
|
+
kwargs["MessageDeduplicationId"] = hashlib.sha256(
|
|
361
|
+
content.encode("utf-8")
|
|
362
|
+
).hexdigest()
|
|
363
|
+
|
|
364
|
+
elif msg_context.message_group_id:
|
|
365
|
+
kwargs["MessageGroupId"] = msg_context.message_group_id
|
|
351
366
|
|
|
352
367
|
# TODO: for message deduplication, we are using the underlying features of the SQS queue
|
|
353
368
|
# however, SQS queue only deduplicate at the Queue level, where the SNS topic deduplicate on the topic level
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import time
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
from localstack.aws.api.sns import (
|
|
8
|
+
MessageAttributeMap,
|
|
9
|
+
PlatformApplication,
|
|
10
|
+
PublishBatchRequestEntry,
|
|
11
|
+
TopicAttributesMap,
|
|
12
|
+
subscriptionARN,
|
|
13
|
+
topicARN,
|
|
14
|
+
)
|
|
15
|
+
from localstack.services.stores import (
|
|
16
|
+
AccountRegionBundle,
|
|
17
|
+
BaseStore,
|
|
18
|
+
CrossRegionAttribute,
|
|
19
|
+
LocalAttribute,
|
|
20
|
+
)
|
|
21
|
+
from localstack.utils.objects import singleton_factory
|
|
22
|
+
from localstack.utils.strings import long_uid
|
|
23
|
+
from localstack.utils.tagging import TaggingService
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Topic(TypedDict, total=True):
|
|
27
|
+
arn: str
|
|
28
|
+
name: str
|
|
29
|
+
attributes: TopicAttributesMap
|
|
30
|
+
subscriptions: list[str]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
SnsProtocols = Literal[
|
|
34
|
+
"http", "https", "email", "email-json", "sms", "sqs", "application", "lambda", "firehose"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
SnsApplicationPlatforms = Literal[
|
|
38
|
+
"APNS", "APNS_SANDBOX", "ADM", "FCM", "Baidu", "GCM", "MPNS", "WNS"
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
SMS_ATTRIBUTE_NAMES = [
|
|
43
|
+
"DeliveryStatusIAMRole",
|
|
44
|
+
"DeliveryStatusSuccessSamplingRate",
|
|
45
|
+
"DefaultSenderID",
|
|
46
|
+
"DefaultSMSType",
|
|
47
|
+
"UsageReportS3Bucket",
|
|
48
|
+
]
|
|
49
|
+
SMS_TYPES = ["Promotional", "Transactional"]
|
|
50
|
+
SMS_DEFAULT_SENDER_REGEX = r"^(?=[A-Za-z0-9]{1,11}$)(?=.*[A-Za-z])[A-Za-z0-9]+$"
|
|
51
|
+
SnsMessageProtocols = Literal[SnsProtocols, SnsApplicationPlatforms]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SnsSubscription(TypedDict, total=False):
|
|
55
|
+
"""
|
|
56
|
+
In SNS, Subscription can be represented with only TopicArn, Endpoint, Protocol, SubscriptionArn and Owner, for
|
|
57
|
+
example in ListSubscriptions. However, when getting a subscription with GetSubscriptionAttributes, it will return
|
|
58
|
+
the Subscription object merged with its own attributes.
|
|
59
|
+
This represents this merged object, for internal use and in GetSubscriptionAttributes
|
|
60
|
+
https://docs.aws.amazon.com/cli/latest/reference/sns/get-subscription-attributes.html
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
TopicArn: topicARN
|
|
64
|
+
Endpoint: str
|
|
65
|
+
Protocol: SnsProtocols
|
|
66
|
+
SubscriptionArn: subscriptionARN
|
|
67
|
+
PendingConfirmation: Literal["true", "false"]
|
|
68
|
+
Owner: str | None
|
|
69
|
+
SubscriptionPrincipal: str | None
|
|
70
|
+
FilterPolicy: str | None
|
|
71
|
+
FilterPolicyScope: Literal["MessageAttributes", "MessageBody"]
|
|
72
|
+
RawMessageDelivery: Literal["true", "false"]
|
|
73
|
+
ConfirmationWasAuthenticated: Literal["true", "false"]
|
|
74
|
+
SubscriptionRoleArn: str | None
|
|
75
|
+
DeliveryPolicy: str | None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@singleton_factory
|
|
79
|
+
def global_sns_message_sequence():
|
|
80
|
+
# creates a 20-digit number used as the start for the global sequence, adds 100 for it to be different from SQS's
|
|
81
|
+
# mostly for testing purpose, both global sequence would be initialized at the same and be identical
|
|
82
|
+
start = int(time.time() + 100) << 33
|
|
83
|
+
# itertools.count is thread safe over the GIL since its getAndIncrement operation is a single python bytecode op
|
|
84
|
+
return itertools.count(start)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_next_sequence_number():
|
|
88
|
+
return next(global_sns_message_sequence())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SnsMessageType(StrEnum):
|
|
92
|
+
Notification = "Notification"
|
|
93
|
+
SubscriptionConfirmation = "SubscriptionConfirmation"
|
|
94
|
+
UnsubscribeConfirmation = "UnsubscribeConfirmation"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class SnsMessage:
|
|
99
|
+
type: SnsMessageType
|
|
100
|
+
message: (
|
|
101
|
+
str | dict
|
|
102
|
+
) # can be Dict if after being JSON decoded for validation if structure is `json`
|
|
103
|
+
message_attributes: MessageAttributeMap | None = None
|
|
104
|
+
message_structure: str | None = None
|
|
105
|
+
subject: str | None = None
|
|
106
|
+
message_deduplication_id: str | None = None
|
|
107
|
+
message_group_id: str | None = None
|
|
108
|
+
token: str | None = None
|
|
109
|
+
message_id: str = field(default_factory=long_uid)
|
|
110
|
+
is_fifo: bool | None = False
|
|
111
|
+
sequencer_number: str | None = None
|
|
112
|
+
|
|
113
|
+
def __post_init__(self):
|
|
114
|
+
if self.message_attributes is None:
|
|
115
|
+
self.message_attributes = {}
|
|
116
|
+
if self.is_fifo:
|
|
117
|
+
self.sequencer_number = str(get_next_sequence_number())
|
|
118
|
+
|
|
119
|
+
def message_content(self, protocol: SnsMessageProtocols) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Helper function to retrieve the message content for the right protocol if the StructureMessage is `json`
|
|
122
|
+
See https://docs.aws.amazon.com/sns/latest/dg/sns-send-custom-platform-specific-payloads-mobile-devices.html
|
|
123
|
+
https://docs.aws.amazon.com/sns/latest/dg/example_sns_Publish_section.html
|
|
124
|
+
:param protocol:
|
|
125
|
+
:return: message content as string
|
|
126
|
+
"""
|
|
127
|
+
if self.message_structure == "json":
|
|
128
|
+
return self.message.get(protocol, self.message.get("default"))
|
|
129
|
+
|
|
130
|
+
return self.message
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_batch_entry(cls, entry: PublishBatchRequestEntry, is_fifo=False) -> "SnsMessage":
|
|
134
|
+
return cls(
|
|
135
|
+
type=SnsMessageType.Notification,
|
|
136
|
+
message=entry["Message"],
|
|
137
|
+
subject=entry.get("Subject"),
|
|
138
|
+
message_structure=entry.get("MessageStructure"),
|
|
139
|
+
message_attributes=entry.get("MessageAttributes"),
|
|
140
|
+
message_deduplication_id=entry.get("MessageDeduplicationId"),
|
|
141
|
+
message_group_id=entry.get("MessageGroupId"),
|
|
142
|
+
is_fifo=is_fifo,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class SnsStore(BaseStore):
|
|
147
|
+
topics: dict[str, Topic] = LocalAttribute(default=dict)
|
|
148
|
+
|
|
149
|
+
# maps subscription ARN to SnsSubscription
|
|
150
|
+
subscriptions: dict[str, SnsSubscription] = LocalAttribute(default=dict)
|
|
151
|
+
|
|
152
|
+
# filter policy are stored as JSON string in subscriptions, store the decoded result Dict
|
|
153
|
+
subscription_filter_policy: dict[subscriptionARN, dict] = LocalAttribute(default=dict)
|
|
154
|
+
|
|
155
|
+
# maps confirmation token to subscription ARN
|
|
156
|
+
subscription_tokens: dict[str, str] = LocalAttribute(default=dict)
|
|
157
|
+
|
|
158
|
+
# maps platform application arns to platform applications
|
|
159
|
+
platform_applications: dict[str, PlatformApplication] = LocalAttribute(default=dict)
|
|
160
|
+
|
|
161
|
+
# topic/subscription independent default values for sending sms messages
|
|
162
|
+
sms_attributes: dict[str, str] = LocalAttribute(default=dict)
|
|
163
|
+
|
|
164
|
+
TAGS: TaggingService = CrossRegionAttribute(default=TaggingService)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
sns_stores = AccountRegionBundle("sns", SnsStore)
|