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
|
@@ -0,0 +1,138 @@
|
|
|
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 create_platform_endpoint_arn(
|
|
107
|
+
platform_application_arn: str,
|
|
108
|
+
) -> str:
|
|
109
|
+
# This is the format of an Endpoint Arn
|
|
110
|
+
# arn:aws:sns:us-west-2:1234567890:endpoint/GCM/MyApplication/12345678-abcd-9012-efgh-345678901234
|
|
111
|
+
return f"{platform_application_arn.replace('app', 'endpoint', 1)}/{uuid4()}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def encode_subscription_token_with_region(region: str) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Create a 64 characters Subscription Token with the region encoded
|
|
117
|
+
:param region:
|
|
118
|
+
:return: a subscription token with the region encoded
|
|
119
|
+
"""
|
|
120
|
+
return ((region.encode() + b"/").hex() + short_uid() * 8)[:64]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_next_page_token_from_arn(resource_arn: str) -> str:
|
|
124
|
+
return to_str(base64.b64encode(to_bytes(resource_arn)))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_region_from_subscription_token(token: str) -> str:
|
|
128
|
+
"""
|
|
129
|
+
Try to decode and return the region from a subscription token
|
|
130
|
+
:param token:
|
|
131
|
+
:return: the region if able to decode it
|
|
132
|
+
:raises: InvalidParameterException if the token is invalid
|
|
133
|
+
"""
|
|
134
|
+
try:
|
|
135
|
+
region = token.split("2f", maxsplit=1)[0]
|
|
136
|
+
return bytes.fromhex(region).decode("utf-8")
|
|
137
|
+
except (IndexError, ValueError, TypeError, UnicodeDecodeError):
|
|
138
|
+
raise InvalidParameterException("Invalid parameter: Token")
|
|
@@ -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
|
|
@@ -274,7 +314,8 @@ class SqsQueue:
|
|
|
274
314
|
purge_timestamp: float | None
|
|
275
315
|
|
|
276
316
|
delayed: set[SqsMessage]
|
|
277
|
-
|
|
317
|
+
# Simulating an ordered set in python. Only the keys are used and of interest.
|
|
318
|
+
inflight: dict[SqsMessage, None]
|
|
278
319
|
receipts: dict[str, SqsMessage]
|
|
279
320
|
|
|
280
321
|
def __init__(self, name: str, region: str, account_id: str, attributes=None, tags=None) -> None:
|
|
@@ -286,7 +327,7 @@ class SqsQueue:
|
|
|
286
327
|
self.tags = tags or {}
|
|
287
328
|
|
|
288
329
|
self.delayed = set()
|
|
289
|
-
self.inflight =
|
|
330
|
+
self.inflight = {}
|
|
290
331
|
self.receipts = {}
|
|
291
332
|
|
|
292
333
|
self.attributes = self.default_attributes()
|
|
@@ -473,7 +514,7 @@ class SqsQueue:
|
|
|
473
514
|
)
|
|
474
515
|
# Terminating the visibility timeout for a message
|
|
475
516
|
# https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html#terminating-message-visibility-timeout
|
|
476
|
-
self.inflight
|
|
517
|
+
del self.inflight[standard_message]
|
|
477
518
|
self._put_message(standard_message)
|
|
478
519
|
|
|
479
520
|
def remove(self, receipt_handle: str):
|
|
@@ -566,9 +607,17 @@ class SqsQueue:
|
|
|
566
607
|
standard_message,
|
|
567
608
|
self.arn,
|
|
568
609
|
)
|
|
569
|
-
self.inflight
|
|
610
|
+
del self.inflight[standard_message]
|
|
570
611
|
self._put_message(standard_message)
|
|
571
612
|
|
|
613
|
+
def add_inflight_message(self, message: SqsMessage):
|
|
614
|
+
"""
|
|
615
|
+
We are simulating an ordered set with a dict. When a value is added, it is added as key to the dict, which
|
|
616
|
+
is all we need. Hence all "values" in this ordered set are None
|
|
617
|
+
:param message: The message to put in flight
|
|
618
|
+
"""
|
|
619
|
+
self.inflight[message] = None
|
|
620
|
+
|
|
572
621
|
def enqueue_delayed_messages(self):
|
|
573
622
|
if not self.delayed:
|
|
574
623
|
return
|
|
@@ -739,7 +788,6 @@ class SqsQueue:
|
|
|
739
788
|
|
|
740
789
|
class StandardQueue(SqsQueue):
|
|
741
790
|
visible: InterruptiblePriorityQueue[SqsMessage]
|
|
742
|
-
inflight: set[SqsMessage]
|
|
743
791
|
|
|
744
792
|
def __init__(self, name: str, region: str, account_id: str, attributes=None, tags=None) -> None:
|
|
745
793
|
super().__init__(name, region, account_id, attributes, tags)
|
|
@@ -883,13 +931,13 @@ class StandardQueue(SqsQueue):
|
|
|
883
931
|
if message.visibility_timeout == 0:
|
|
884
932
|
self.visible.put_nowait(message)
|
|
885
933
|
else:
|
|
886
|
-
self.
|
|
934
|
+
self.add_inflight_message(message)
|
|
887
935
|
|
|
888
936
|
return result
|
|
889
937
|
|
|
890
938
|
def _on_remove_message(self, message: SqsMessage):
|
|
891
939
|
try:
|
|
892
|
-
self.inflight
|
|
940
|
+
del self.inflight[message]
|
|
893
941
|
except KeyError:
|
|
894
942
|
# this likely means the message was removed with an expired receipt handle unfortunately this
|
|
895
943
|
# means we need to scan the queue for the element and remove it from there, and then re-heapify
|
|
@@ -1013,8 +1061,7 @@ class FifoQueue(SqsQueue):
|
|
|
1013
1061
|
message.delay_seconds = value
|
|
1014
1062
|
|
|
1015
1063
|
def _pre_delete_checks(self, message: SqsMessage, receipt_handle: str) -> None:
|
|
1016
|
-
|
|
1017
|
-
if time.time() - float(last_received) > message.visibility_timeout:
|
|
1064
|
+
if message.is_visible:
|
|
1018
1065
|
raise InvalidParameterValueException(
|
|
1019
1066
|
f"Value {receipt_handle} for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired."
|
|
1020
1067
|
)
|
|
@@ -1110,6 +1157,26 @@ class FifoQueue(SqsQueue):
|
|
|
1110
1157
|
elif previously_empty:
|
|
1111
1158
|
self.message_group_queue.put_nowait(message_group)
|
|
1112
1159
|
|
|
1160
|
+
def requeue_inflight_messages(self):
|
|
1161
|
+
if not self.inflight:
|
|
1162
|
+
return
|
|
1163
|
+
|
|
1164
|
+
with self.mutex:
|
|
1165
|
+
messages = list(self.inflight)
|
|
1166
|
+
for standard_message in messages:
|
|
1167
|
+
# in fifo, an invisible message blocks potentially visible messages afterwards
|
|
1168
|
+
# this can happen for example if multiple message of the same group are received at once, then one
|
|
1169
|
+
# message of this batch has its visibility timeout extended
|
|
1170
|
+
if not standard_message.is_visible:
|
|
1171
|
+
return
|
|
1172
|
+
LOG.debug(
|
|
1173
|
+
"re-queueing inflight messages %s into queue %s",
|
|
1174
|
+
standard_message,
|
|
1175
|
+
self.arn,
|
|
1176
|
+
)
|
|
1177
|
+
del self.inflight[standard_message]
|
|
1178
|
+
self._put_message(standard_message)
|
|
1179
|
+
|
|
1113
1180
|
def remove_expired_messages(self):
|
|
1114
1181
|
with self.mutex:
|
|
1115
1182
|
retention_period = self.message_retention_period
|
|
@@ -1239,8 +1306,7 @@ class FifoQueue(SqsQueue):
|
|
|
1239
1306
|
if message.visibility_timeout == 0:
|
|
1240
1307
|
self._put_message(message)
|
|
1241
1308
|
else:
|
|
1242
|
-
self.
|
|
1243
|
-
|
|
1309
|
+
self.add_inflight_message(message)
|
|
1244
1310
|
return result
|
|
1245
1311
|
|
|
1246
1312
|
def _on_remove_message(self, message: SqsMessage):
|
|
@@ -1249,7 +1315,7 @@ class FifoQueue(SqsQueue):
|
|
|
1249
1315
|
|
|
1250
1316
|
with self.mutex:
|
|
1251
1317
|
try:
|
|
1252
|
-
self.inflight
|
|
1318
|
+
del self.inflight[message]
|
|
1253
1319
|
except KeyError:
|
|
1254
1320
|
# in FIFO queues, this should not happen, as expired receipt handles cannot be used to
|
|
1255
1321
|
# delete a message.
|