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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from botocore.utils import InvalidArnException
|
|
6
|
+
|
|
7
|
+
from localstack.aws.api.sns import InvalidParameterException
|
|
8
|
+
from localstack.services.sns.constants import E164_REGEX, VALID_SUBSCRIPTION_ATTR_NAME
|
|
9
|
+
from localstack.utils.aws.arns import ArnData, parse_arn
|
|
10
|
+
from localstack.utils.strings import short_uid, to_bytes, to_str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_and_validate_topic_arn(topic_arn: str | None) -> ArnData:
|
|
14
|
+
return _parse_and_validate_arn(topic_arn, "Topic")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_and_validate_platform_application_arn(platform_application_arn: str | None) -> ArnData:
|
|
18
|
+
return _parse_and_validate_arn(platform_application_arn, "PlatformApplication")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_and_validate_arn(arn: str | None, resource_type: str) -> ArnData:
|
|
22
|
+
arn = arn or ""
|
|
23
|
+
try:
|
|
24
|
+
return parse_arn(arn)
|
|
25
|
+
except InvalidArnException:
|
|
26
|
+
count = len(arn.split(":"))
|
|
27
|
+
raise InvalidParameterException(
|
|
28
|
+
f"Invalid parameter: {resource_type}Arn Reason: An ARN must have at least 6 elements, not {count}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def is_valid_e164_number(number: str) -> bool:
|
|
33
|
+
return E164_REGEX.match(number) is not None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def validate_subscription_attribute(
|
|
37
|
+
attribute_name: str,
|
|
38
|
+
attribute_value: str,
|
|
39
|
+
topic_arn: str,
|
|
40
|
+
endpoint: str,
|
|
41
|
+
is_subscribe_call: bool = False,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Validate the subscription attribute to be set. See:
|
|
45
|
+
https://docs.aws.amazon.com/sns/latest/api/API_SetSubscriptionAttributes.html
|
|
46
|
+
:param attribute_name: the subscription attribute name, must be in VALID_SUBSCRIPTION_ATTR_NAME
|
|
47
|
+
:param attribute_value: the subscription attribute value
|
|
48
|
+
:param topic_arn: the topic_arn of the subscription, needed to know if it is FIFO
|
|
49
|
+
:param endpoint: the subscription endpoint (like an SQS queue ARN)
|
|
50
|
+
:param is_subscribe_call: the error message is different if called from Subscribe or SetSubscriptionAttributes
|
|
51
|
+
:raises InvalidParameterException
|
|
52
|
+
:return:
|
|
53
|
+
"""
|
|
54
|
+
error_prefix = (
|
|
55
|
+
"Invalid parameter: Attributes Reason: " if is_subscribe_call else "Invalid parameter: "
|
|
56
|
+
)
|
|
57
|
+
if attribute_name not in VALID_SUBSCRIPTION_ATTR_NAME:
|
|
58
|
+
raise InvalidParameterException(f"{error_prefix}AttributeName")
|
|
59
|
+
|
|
60
|
+
if attribute_name == "FilterPolicy":
|
|
61
|
+
try:
|
|
62
|
+
json.loads(attribute_value or "{}")
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
raise InvalidParameterException(f"{error_prefix}FilterPolicy: failed to parse JSON.")
|
|
65
|
+
elif attribute_name == "FilterPolicyScope":
|
|
66
|
+
if attribute_value not in ("MessageAttributes", "MessageBody"):
|
|
67
|
+
raise InvalidParameterException(
|
|
68
|
+
f"{error_prefix}FilterPolicyScope: Invalid value [{attribute_value}]. "
|
|
69
|
+
f"Please use either MessageBody or MessageAttributes"
|
|
70
|
+
)
|
|
71
|
+
elif attribute_name == "RawMessageDelivery":
|
|
72
|
+
# TODO: only for SQS and https(s) subs, + firehose
|
|
73
|
+
if attribute_value.lower() not in ("true", "false"):
|
|
74
|
+
raise InvalidParameterException(
|
|
75
|
+
f"{error_prefix}RawMessageDelivery: Invalid value [{attribute_value}]. "
|
|
76
|
+
f"Must be true or false."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
elif attribute_name == "RedrivePolicy":
|
|
80
|
+
try:
|
|
81
|
+
dlq_target_arn = json.loads(attribute_value).get("deadLetterTargetArn", "")
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
raise InvalidParameterException(f"{error_prefix}RedrivePolicy: failed to parse JSON.")
|
|
84
|
+
try:
|
|
85
|
+
parsed_arn = parse_arn(dlq_target_arn)
|
|
86
|
+
except InvalidArnException:
|
|
87
|
+
raise InvalidParameterException(
|
|
88
|
+
f"{error_prefix}RedrivePolicy: deadLetterTargetArn is an invalid arn"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if topic_arn.endswith(".fifo"):
|
|
92
|
+
if endpoint.endswith(".fifo") and (
|
|
93
|
+
not parsed_arn["resource"].endswith(".fifo") or "sqs" not in parsed_arn["service"]
|
|
94
|
+
):
|
|
95
|
+
raise InvalidParameterException(
|
|
96
|
+
f"{error_prefix}RedrivePolicy: must use a FIFO queue as DLQ for a FIFO Subscription to a FIFO Topic."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def create_subscription_arn(topic_arn: str) -> str:
|
|
101
|
+
# This is the format of a Subscription ARN
|
|
102
|
+
# arn:aws:sns:us-west-2:123456789012:my-topic:8a21d249-4329-4871-acc6-7be709c6ea7f
|
|
103
|
+
return f"{topic_arn}:{uuid4()}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def encode_subscription_token_with_region(region: str) -> str:
|
|
107
|
+
"""
|
|
108
|
+
Create a 64 characters Subscription Token with the region encoded
|
|
109
|
+
:param region:
|
|
110
|
+
:return: a subscription token with the region encoded
|
|
111
|
+
"""
|
|
112
|
+
return ((region.encode() + b"/").hex() + short_uid() * 8)[:64]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_next_page_token_from_arn(resource_arn: str) -> str:
|
|
116
|
+
return to_str(base64.b64encode(to_bytes(resource_arn)))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def get_region_from_subscription_token(token: str) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Try to decode and return the region from a subscription token
|
|
122
|
+
:param token:
|
|
123
|
+
:return: the region if able to decode it
|
|
124
|
+
:raises: InvalidParameterException if the token is invalid
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
region = token.split("2f", maxsplit=1)[0]
|
|
128
|
+
return bytes.fromhex(region).decode("utf-8")
|
|
129
|
+
except (IndexError, ValueError, TypeError, UnicodeDecodeError):
|
|
130
|
+
raise InvalidParameterException("Invalid parameter: Token")
|
|
@@ -18,7 +18,7 @@ DEDUPLICATION_INTERVAL_IN_SEC = 5 * 60
|
|
|
18
18
|
RECENTLY_DELETED_TIMEOUT = 60
|
|
19
19
|
|
|
20
20
|
# the default maximum message size in SQS
|
|
21
|
-
DEFAULT_MAXIMUM_MESSAGE_SIZE =
|
|
21
|
+
DEFAULT_MAXIMUM_MESSAGE_SIZE = 1048576
|
|
22
22
|
INTERNAL_QUEUE_ATTRIBUTES = [
|
|
23
23
|
# these attributes cannot be changed by set_queue_attributes and should
|
|
24
24
|
# therefore be ignored when comparing queue attributes for create_queue
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from werkzeug import Request as WerkzeugRequest
|
|
5
|
+
|
|
6
|
+
from localstack.aws.api import CommonServiceException, ServiceException
|
|
7
|
+
from localstack.aws.api.sqs import (
|
|
8
|
+
Message,
|
|
9
|
+
QueueAttributeName,
|
|
10
|
+
QueueDoesNotExist,
|
|
11
|
+
ReceiveMessageResult,
|
|
12
|
+
)
|
|
13
|
+
from localstack.aws.protocol.parser import create_parser
|
|
14
|
+
from localstack.aws.protocol.serializer import aws_response_serializer
|
|
15
|
+
from localstack.aws.spec import load_service
|
|
16
|
+
from localstack.http import Request, route
|
|
17
|
+
from localstack.services.sqs.models import (
|
|
18
|
+
FifoQueue,
|
|
19
|
+
SqsMessage,
|
|
20
|
+
SqsQueue,
|
|
21
|
+
StandardQueue,
|
|
22
|
+
sqs_stores,
|
|
23
|
+
to_sqs_api_message,
|
|
24
|
+
)
|
|
25
|
+
from localstack.services.sqs.utils import (
|
|
26
|
+
parse_queue_url,
|
|
27
|
+
)
|
|
28
|
+
from localstack.utils.aws.request_context import extract_region_from_headers
|
|
29
|
+
|
|
30
|
+
LOG = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InvalidAddress(ServiceException):
|
|
34
|
+
code = "InvalidAddress"
|
|
35
|
+
message = "The address https://queue.amazonaws.com/ is not valid for this endpoint."
|
|
36
|
+
sender_fault = True
|
|
37
|
+
status_code = 404
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_sqs_protocol(request: Request) -> Literal["query", "json"]:
|
|
41
|
+
content_type = request.headers.get("Content-Type")
|
|
42
|
+
return "json" if content_type == "application/x-amz-json-1.0" else "query"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def sqs_auto_protocol_aws_response_serializer(service_name: str, operation: str):
|
|
46
|
+
def _decorate(fn):
|
|
47
|
+
def _proxy(*args, **kwargs):
|
|
48
|
+
# extract request from function invocation (decorator can be used for methods as well as for functions).
|
|
49
|
+
if len(args) > 0 and isinstance(args[0], WerkzeugRequest):
|
|
50
|
+
# function
|
|
51
|
+
request = args[0]
|
|
52
|
+
elif len(args) > 1 and isinstance(args[1], WerkzeugRequest):
|
|
53
|
+
# method (arg[0] == self)
|
|
54
|
+
request = args[1]
|
|
55
|
+
elif "request" in kwargs:
|
|
56
|
+
request = kwargs["request"]
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"could not find Request in signature of function {fn}")
|
|
59
|
+
|
|
60
|
+
protocol = get_sqs_protocol(request)
|
|
61
|
+
return aws_response_serializer(service_name, operation, protocol)(fn)(*args, **kwargs)
|
|
62
|
+
|
|
63
|
+
return _proxy
|
|
64
|
+
|
|
65
|
+
return _decorate
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class SqsDeveloperApi:
|
|
69
|
+
"""
|
|
70
|
+
A set of SQS developer tool endpoints:
|
|
71
|
+
|
|
72
|
+
- ``/_aws/sqs/messages``: list SQS messages without side effects, compatible with ``ReceiveMessage``.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, stores=None):
|
|
76
|
+
self.stores = stores or sqs_stores
|
|
77
|
+
|
|
78
|
+
@route("/_aws/sqs/messages", methods=["GET", "POST"])
|
|
79
|
+
@sqs_auto_protocol_aws_response_serializer("sqs", "ReceiveMessage")
|
|
80
|
+
def list_messages(self, request: Request) -> ReceiveMessageResult:
|
|
81
|
+
"""
|
|
82
|
+
This endpoint expects a ``QueueUrl`` request parameter (either as query arg or form parameter), similar to
|
|
83
|
+
the ``ReceiveMessage`` operation. It will parse the Queue URL generated by one of the SQS endpoint strategies.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
if "x-amz-" in request.mimetype or "x-www-form-urlencoded" in request.mimetype:
|
|
87
|
+
# only parse the request using a parser if it comes from an AWS client
|
|
88
|
+
protocol = get_sqs_protocol(request)
|
|
89
|
+
operation, service_request = create_parser(
|
|
90
|
+
load_service("sqs", protocol=protocol)
|
|
91
|
+
).parse(request)
|
|
92
|
+
if operation.name != "ReceiveMessage":
|
|
93
|
+
raise CommonServiceException(
|
|
94
|
+
"InvalidRequest", "This endpoint only accepts ReceiveMessage calls"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
service_request = dict(request.values)
|
|
98
|
+
|
|
99
|
+
if not service_request.get("QueueUrl"):
|
|
100
|
+
raise QueueDoesNotExist()
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
account_id, region, queue_name = parse_queue_url(service_request.get("QueueUrl"))
|
|
104
|
+
except ValueError:
|
|
105
|
+
LOG.error(
|
|
106
|
+
"Error while parsing Queue URL from request values: %s",
|
|
107
|
+
service_request.get,
|
|
108
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
109
|
+
)
|
|
110
|
+
raise InvalidAddress()
|
|
111
|
+
|
|
112
|
+
if not region:
|
|
113
|
+
region = extract_region_from_headers(request.headers)
|
|
114
|
+
|
|
115
|
+
return self._get_and_serialize_messages(request, region, account_id, queue_name)
|
|
116
|
+
|
|
117
|
+
@route("/_aws/sqs/messages/<region>/<account_id>/<queue_name>")
|
|
118
|
+
@sqs_auto_protocol_aws_response_serializer("sqs", "ReceiveMessage")
|
|
119
|
+
def list_messages_for_queue_url(
|
|
120
|
+
self, request: Request, region: str, account_id: str, queue_name: str
|
|
121
|
+
) -> ReceiveMessageResult:
|
|
122
|
+
"""
|
|
123
|
+
This endpoint extracts the region, account_id, and queue_name directly from the URL rather than requiring the
|
|
124
|
+
QueueUrl as parameter.
|
|
125
|
+
"""
|
|
126
|
+
return self._get_and_serialize_messages(request, region, account_id, queue_name)
|
|
127
|
+
|
|
128
|
+
def _get_and_serialize_messages(
|
|
129
|
+
self,
|
|
130
|
+
request: Request,
|
|
131
|
+
region: str,
|
|
132
|
+
account_id: str,
|
|
133
|
+
queue_name: str,
|
|
134
|
+
) -> ReceiveMessageResult:
|
|
135
|
+
show_invisible = request.values.get("ShowInvisible", "").lower() in ["true", "1"]
|
|
136
|
+
show_delayed = request.values.get("ShowDelayed", "").lower() in ["true", "1"]
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
store = self.stores[account_id][region]
|
|
140
|
+
queue = store.queues[queue_name]
|
|
141
|
+
except KeyError:
|
|
142
|
+
LOG.info(
|
|
143
|
+
"no queue named %s in region %s and account %s", queue_name, region, account_id
|
|
144
|
+
)
|
|
145
|
+
raise QueueDoesNotExist()
|
|
146
|
+
|
|
147
|
+
messages = self._collect_messages(
|
|
148
|
+
queue, show_invisible=show_invisible, show_delayed=show_delayed
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return ReceiveMessageResult(Messages=messages)
|
|
152
|
+
|
|
153
|
+
def _collect_messages(
|
|
154
|
+
self, queue: SqsQueue, show_invisible: bool = False, show_delayed: bool = False
|
|
155
|
+
) -> list[Message]:
|
|
156
|
+
"""
|
|
157
|
+
Retrieves from a given SqsQueue all visible messages without causing any side effects (not setting any
|
|
158
|
+
receive timestamps, receive counts, or visibility state).
|
|
159
|
+
|
|
160
|
+
:param queue: the queue
|
|
161
|
+
:param show_invisible: show invisible messages as well
|
|
162
|
+
:param show_delayed: show delayed messages as well
|
|
163
|
+
:return: a list of messages
|
|
164
|
+
"""
|
|
165
|
+
receipt_handle = "SQS/BACKDOOR/ACCESS" # dummy receipt handle
|
|
166
|
+
|
|
167
|
+
sqs_messages: list[SqsMessage] = []
|
|
168
|
+
|
|
169
|
+
if show_invisible:
|
|
170
|
+
sqs_messages.extend(queue.inflight)
|
|
171
|
+
|
|
172
|
+
if isinstance(queue, StandardQueue):
|
|
173
|
+
sqs_messages.extend(queue.visible.queue)
|
|
174
|
+
elif isinstance(queue, FifoQueue):
|
|
175
|
+
if show_invisible:
|
|
176
|
+
for inflight_group in queue.inflight_groups:
|
|
177
|
+
# messages that have been received are held in ``queue.inflight``, even for FIFO queues. however,
|
|
178
|
+
# for fifo queues, messages that are in the same message group as messages that have been
|
|
179
|
+
# received, are also considered invisible, and are held here in ``inflight_group.messages``.
|
|
180
|
+
for sqs_message in inflight_group.messages:
|
|
181
|
+
sqs_messages.append(sqs_message)
|
|
182
|
+
|
|
183
|
+
for message_group in queue.message_group_queue.queue:
|
|
184
|
+
# these are all messages of message groups that are visible
|
|
185
|
+
for sqs_message in message_group.messages:
|
|
186
|
+
sqs_messages.append(sqs_message)
|
|
187
|
+
else:
|
|
188
|
+
raise ValueError(f"unknown queue type {type(queue)}")
|
|
189
|
+
|
|
190
|
+
if show_delayed:
|
|
191
|
+
sqs_messages.extend(queue.delayed)
|
|
192
|
+
|
|
193
|
+
messages = []
|
|
194
|
+
|
|
195
|
+
for sqs_message in sqs_messages:
|
|
196
|
+
message: Message = to_sqs_api_message(sqs_message, [QueueAttributeName.All], ["All"])
|
|
197
|
+
# these are all non-standard fields so we squelch the linter
|
|
198
|
+
if show_invisible:
|
|
199
|
+
message["Attributes"]["IsVisible"] = str(sqs_message.is_visible).lower() # noqa
|
|
200
|
+
if show_delayed:
|
|
201
|
+
message["Attributes"]["IsDelayed"] = str(sqs_message.is_delayed).lower() # noqa
|
|
202
|
+
messages.append(message)
|
|
203
|
+
message["ReceiptHandle"] = receipt_handle
|
|
204
|
+
|
|
205
|
+
return messages
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import hashlib
|
|
2
3
|
import heapq
|
|
3
4
|
import inspect
|
|
@@ -15,6 +16,7 @@ from localstack.aws.api.sqs import (
|
|
|
15
16
|
AttributeNameList,
|
|
16
17
|
InvalidAttributeName,
|
|
17
18
|
Message,
|
|
19
|
+
MessageAttributeNameList,
|
|
18
20
|
MessageSystemAttributeName,
|
|
19
21
|
QueueAttributeMap,
|
|
20
22
|
QueueAttributeName,
|
|
@@ -29,12 +31,15 @@ from localstack.services.sqs.exceptions import (
|
|
|
29
31
|
)
|
|
30
32
|
from localstack.services.sqs.queue import InterruptiblePriorityQueue, InterruptibleQueue
|
|
31
33
|
from localstack.services.sqs.utils import (
|
|
34
|
+
create_message_attribute_hash,
|
|
32
35
|
encode_move_task_handle,
|
|
33
36
|
encode_receipt_handle,
|
|
34
37
|
extract_receipt_handle_info,
|
|
35
38
|
global_message_sequence,
|
|
36
39
|
guess_endpoint_strategy_and_host,
|
|
37
40
|
is_message_deduplication_id_required,
|
|
41
|
+
message_filter_attributes,
|
|
42
|
+
message_filter_message_attributes,
|
|
38
43
|
)
|
|
39
44
|
from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
|
|
40
45
|
from localstack.utils.aws.arns import get_partition
|
|
@@ -153,7 +158,7 @@ class SqsMessage:
|
|
|
153
158
|
"""
|
|
154
159
|
Returns false if the message has a visibility deadline that is in the future.
|
|
155
160
|
|
|
156
|
-
:return: whether the message is
|
|
161
|
+
:return: whether the message is visible or not.
|
|
157
162
|
"""
|
|
158
163
|
if self.visibility_deadline is None:
|
|
159
164
|
return True
|
|
@@ -190,6 +195,41 @@ class SqsMessage:
|
|
|
190
195
|
return f"SqsMessage(id={self.message_id},group={self.message_group_id})"
|
|
191
196
|
|
|
192
197
|
|
|
198
|
+
def to_sqs_api_message(
|
|
199
|
+
standard_message: SqsMessage,
|
|
200
|
+
attribute_names: AttributeNameList = None,
|
|
201
|
+
message_attribute_names: MessageAttributeNameList = None,
|
|
202
|
+
) -> Message:
|
|
203
|
+
"""
|
|
204
|
+
Utility function to convert an SQS message from LocalStack's internal representation to the AWS API
|
|
205
|
+
concept 'Message', which is the format returned by the ``ReceiveMessage`` operation.
|
|
206
|
+
|
|
207
|
+
:param standard_message: A LocalStack SQS message
|
|
208
|
+
:param attribute_names: the attribute name list to filter
|
|
209
|
+
:param message_attribute_names: the message attribute names to filter
|
|
210
|
+
:return: a copy of the original Message with updated message attributes and MD5 attribute hash sums
|
|
211
|
+
"""
|
|
212
|
+
# prepare message for receiver
|
|
213
|
+
message = copy.deepcopy(standard_message.message)
|
|
214
|
+
|
|
215
|
+
# update system attributes of the message copy
|
|
216
|
+
message["Attributes"][MessageSystemAttributeName.ApproximateFirstReceiveTimestamp] = str(
|
|
217
|
+
int((standard_message.first_received or 0) * 1000)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# filter attributes for receiver
|
|
221
|
+
message_filter_attributes(message, attribute_names)
|
|
222
|
+
message_filter_message_attributes(message, message_attribute_names)
|
|
223
|
+
if message.get("MessageAttributes"):
|
|
224
|
+
message["MD5OfMessageAttributes"] = create_message_attribute_hash(
|
|
225
|
+
message["MessageAttributes"]
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
# delete the value that was computed when creating the message
|
|
229
|
+
message.pop("MD5OfMessageAttributes", None)
|
|
230
|
+
return message
|
|
231
|
+
|
|
232
|
+
|
|
193
233
|
class ReceiveMessageResult:
|
|
194
234
|
"""
|
|
195
235
|
Object to communicate the result of a "receive messages" operation between the SqsProvider and
|
|
@@ -771,7 +811,11 @@ class StandardQueue(SqsQueue):
|
|
|
771
811
|
f"request includes a parameter that is not valid for this queue type."
|
|
772
812
|
)
|
|
773
813
|
|
|
774
|
-
standard_message = SqsMessage(
|
|
814
|
+
standard_message = SqsMessage(
|
|
815
|
+
time.time(),
|
|
816
|
+
message,
|
|
817
|
+
message_group_id=message_group_id,
|
|
818
|
+
)
|
|
775
819
|
|
|
776
820
|
if visibility_timeout is not None:
|
|
777
821
|
standard_message.visibility_timeout = visibility_timeout
|
|
@@ -1004,13 +1048,12 @@ class FifoQueue(SqsQueue):
|
|
|
1004
1048
|
}
|
|
1005
1049
|
|
|
1006
1050
|
def update_delay_seconds(self, value: int):
|
|
1007
|
-
super(
|
|
1051
|
+
super().update_delay_seconds(value)
|
|
1008
1052
|
for message in self.delayed:
|
|
1009
1053
|
message.delay_seconds = value
|
|
1010
1054
|
|
|
1011
1055
|
def _pre_delete_checks(self, message: SqsMessage, receipt_handle: str) -> None:
|
|
1012
|
-
|
|
1013
|
-
if time.time() - float(last_received) > message.visibility_timeout:
|
|
1056
|
+
if message.is_visible:
|
|
1014
1057
|
raise InvalidParameterValueException(
|
|
1015
1058
|
f"Value {receipt_handle} for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired."
|
|
1016
1059
|
)
|