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
|
@@ -6,9 +6,11 @@ from werkzeug.exceptions import RequestEntityTooLarge
|
|
|
6
6
|
from werkzeug.http import parse_dict_header
|
|
7
7
|
|
|
8
8
|
from localstack.aws.spec import (
|
|
9
|
+
ProtocolName,
|
|
9
10
|
ServiceCatalog,
|
|
10
11
|
ServiceModelIdentifier,
|
|
11
12
|
get_service_catalog,
|
|
13
|
+
is_protocol_in_service_model_identifier,
|
|
12
14
|
)
|
|
13
15
|
from localstack.http import Request
|
|
14
16
|
from localstack.services.s3.utils import uses_host_addressing
|
|
@@ -17,6 +19,23 @@ from localstack.utils.strings import to_bytes
|
|
|
17
19
|
|
|
18
20
|
LOG = logging.getLogger(__name__)
|
|
19
21
|
|
|
22
|
+
_PROTOCOL_DETECTION_PRIORITY: list[ProtocolName] = [
|
|
23
|
+
"smithy-rpc-v2-cbor",
|
|
24
|
+
"json",
|
|
25
|
+
"query",
|
|
26
|
+
"ec2",
|
|
27
|
+
"rest-json",
|
|
28
|
+
"rest-xml",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProtocolError(Exception):
|
|
33
|
+
"""
|
|
34
|
+
Error which is thrown if we cannot detect the protocol for the request.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
pass
|
|
38
|
+
|
|
20
39
|
|
|
21
40
|
class _ServiceIndicators(NamedTuple):
|
|
22
41
|
"""
|
|
@@ -43,6 +62,7 @@ def _extract_service_indicators(request: Request) -> _ServiceIndicators:
|
|
|
43
62
|
"""Extracts all different fields that might indicate which service a request is targeting."""
|
|
44
63
|
x_amz_target = request.headers.get("x-amz-target")
|
|
45
64
|
authorization = request.headers.get("authorization")
|
|
65
|
+
is_rpc_v2 = "rpc-v2-cbor" in request.headers.get("Smithy-Protocol", "")
|
|
46
66
|
|
|
47
67
|
signing_name = None
|
|
48
68
|
if authorization:
|
|
@@ -55,7 +75,15 @@ def _extract_service_indicators(request: Request) -> _ServiceIndicators:
|
|
|
55
75
|
except (ValueError, KeyError):
|
|
56
76
|
LOG.debug("auth header could not be parsed for service routing: %s", authorization)
|
|
57
77
|
pass
|
|
58
|
-
if
|
|
78
|
+
if is_rpc_v2:
|
|
79
|
+
# https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html#requests
|
|
80
|
+
rpc_v2_params = request.path.lstrip("/").split("/")
|
|
81
|
+
if len(rpc_v2_params) >= 4:
|
|
82
|
+
*_, service_shape_name, __, operation = rpc_v2_params
|
|
83
|
+
target_prefix = service_shape_name.split("#")[-1]
|
|
84
|
+
else:
|
|
85
|
+
target_prefix, operation = None, None
|
|
86
|
+
elif x_amz_target:
|
|
59
87
|
if "." in x_amz_target:
|
|
60
88
|
target_prefix, operation = x_amz_target.split(".", 1)
|
|
61
89
|
else:
|
|
@@ -67,6 +95,48 @@ def _extract_service_indicators(request: Request) -> _ServiceIndicators:
|
|
|
67
95
|
return _ServiceIndicators(signing_name, target_prefix, operation, request.host, request.path)
|
|
68
96
|
|
|
69
97
|
|
|
98
|
+
def _matches_protocol(request: Request, protocol: ProtocolName) -> bool:
|
|
99
|
+
headers = request.headers
|
|
100
|
+
mimetype = request.mimetype.lower()
|
|
101
|
+
match protocol:
|
|
102
|
+
case "smithy-rpc-v2-cbor":
|
|
103
|
+
# Every request for the rpcv2Cbor protocol MUST contain a `Smithy-Protocol` header with the value
|
|
104
|
+
# of `rpc-v2-cbor`.
|
|
105
|
+
# https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
|
|
106
|
+
return headers.get("Smithy-Protocol", "") == "rpc-v2-cbor"
|
|
107
|
+
case "json":
|
|
108
|
+
return mimetype.startswith("application/x-amz-json")
|
|
109
|
+
case "query" | "ec2":
|
|
110
|
+
# https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#request-serialization
|
|
111
|
+
return (
|
|
112
|
+
mimetype.startswith("application/x-www-form-urlencoded") or "Action" in request.args
|
|
113
|
+
)
|
|
114
|
+
case "rest-xml" | "rest-json":
|
|
115
|
+
# `rest-json` and `rest-xml` can accept any kind of Content-Type, and it can be configured on the operation
|
|
116
|
+
# level.
|
|
117
|
+
# https://smithy.io/2.0/aws/protocols/aws-restjson1-protocol.html
|
|
118
|
+
return True
|
|
119
|
+
case _:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def match_available_protocols(
|
|
124
|
+
request: Request, available_protocols: list[ProtocolName]
|
|
125
|
+
) -> ProtocolName | None:
|
|
126
|
+
"""
|
|
127
|
+
Tries to match the current request and determine the protocol used amongst the available protocols given.
|
|
128
|
+
We use a priority order to try to determine the protocol, as some protocols are more permissive that others.
|
|
129
|
+
:param request: the incoming request
|
|
130
|
+
:param available_protocols: the available protocols of the Service the request is directed to
|
|
131
|
+
:return: the protocol matched, if any
|
|
132
|
+
"""
|
|
133
|
+
for protocol in _PROTOCOL_DETECTION_PRIORITY:
|
|
134
|
+
if protocol in available_protocols and _matches_protocol(request, protocol):
|
|
135
|
+
return protocol
|
|
136
|
+
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
70
140
|
signing_name_path_prefix_rules = {
|
|
71
141
|
# custom rules based on URI path prefixes that are not easily generalizable
|
|
72
142
|
"apigateway": {
|
|
@@ -248,10 +318,10 @@ def resolve_conflicts(
|
|
|
248
318
|
# The `application/x-amz-json-1.0` header is mandatory for requests targeting SQS with the `json` protocol. We
|
|
249
319
|
# can safely route them to the `sqs` JSON parser/serializer. If not present, route the request to the
|
|
250
320
|
# sqs-query protocol.
|
|
251
|
-
|
|
321
|
+
protocol = match_available_protocols(request, available_protocols=["json", "query"])
|
|
252
322
|
return (
|
|
253
323
|
ServiceModelIdentifier("sqs")
|
|
254
|
-
if
|
|
324
|
+
if protocol == "json"
|
|
255
325
|
else ServiceModelIdentifier("sqs", "query")
|
|
256
326
|
)
|
|
257
327
|
|
|
@@ -266,7 +336,25 @@ def determine_aws_service_model_for_data_plane(
|
|
|
266
336
|
custom_host_match = custom_host_addressing_rules(request.host)
|
|
267
337
|
if custom_host_match:
|
|
268
338
|
services = services or get_service_catalog()
|
|
269
|
-
return services.get(
|
|
339
|
+
return services.get(custom_host_match.name, custom_host_match.protocol)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def determine_aws_protocol(request: Request, service_model: ServiceModel) -> ProtocolName:
|
|
343
|
+
if not (protocols := service_model.metadata.get("protocols")):
|
|
344
|
+
# if the service does not define multiple protocols, return the `protocol` defined for the service
|
|
345
|
+
return service_model.protocol
|
|
346
|
+
|
|
347
|
+
if len(protocols) == 1:
|
|
348
|
+
return protocols[0]
|
|
349
|
+
|
|
350
|
+
if protocol := match_available_protocols(request, available_protocols=protocols):
|
|
351
|
+
return protocol
|
|
352
|
+
|
|
353
|
+
raise ProtocolError(
|
|
354
|
+
f"Could not determine the protocol for the request: "
|
|
355
|
+
f"{request.method} {request.path} for the service '{service_model.service_name}' "
|
|
356
|
+
f"(available protocols: {protocols})"
|
|
357
|
+
)
|
|
270
358
|
|
|
271
359
|
|
|
272
360
|
def determine_aws_service_model(
|
|
@@ -287,12 +375,13 @@ def determine_aws_service_model(
|
|
|
287
375
|
signing_name_candidates = services.by_signing_name(signing_name)
|
|
288
376
|
if len(signing_name_candidates) == 1:
|
|
289
377
|
# a unique signing-name -> service name mapping is the case for ~75% of service operations
|
|
290
|
-
|
|
378
|
+
candidate = signing_name_candidates[0]
|
|
379
|
+
return services.get(candidate.name, candidate.protocol)
|
|
291
380
|
|
|
292
381
|
# try to find a match with the custom signing name rules
|
|
293
382
|
custom_match = custom_signing_name_rules(signing_name, path)
|
|
294
383
|
if custom_match:
|
|
295
|
-
return services.get(
|
|
384
|
+
return services.get(custom_match.name, custom_match.protocol)
|
|
296
385
|
|
|
297
386
|
# still ambiguous - add the services to the list of candidates
|
|
298
387
|
candidates.update(signing_name_candidates)
|
|
@@ -302,33 +391,34 @@ def determine_aws_service_model(
|
|
|
302
391
|
target_candidates = services.by_target_prefix(target_prefix)
|
|
303
392
|
if len(target_candidates) == 1:
|
|
304
393
|
# a unique target prefix
|
|
305
|
-
|
|
394
|
+
candidate = target_candidates[0]
|
|
395
|
+
return services.get(candidate.name, candidate.protocol)
|
|
306
396
|
|
|
307
397
|
# still ambiguous - add the services to the list of candidates
|
|
308
398
|
candidates.update(target_candidates)
|
|
309
399
|
|
|
310
400
|
# exclude services where the operation is not contained in the service spec
|
|
311
401
|
for service_identifier in list(candidates):
|
|
312
|
-
service = services.get(
|
|
402
|
+
service = services.get(service_identifier.name, service_identifier.protocol)
|
|
313
403
|
if operation not in service.operation_names:
|
|
314
404
|
candidates.remove(service_identifier)
|
|
315
405
|
else:
|
|
316
406
|
# exclude services which have a target prefix (the current request does not have one)
|
|
317
407
|
for service_identifier in list(candidates):
|
|
318
|
-
service = services.get(
|
|
408
|
+
service = services.get(service_identifier.name, service_identifier.protocol)
|
|
319
409
|
if service.metadata.get("targetPrefix") is not None:
|
|
320
410
|
candidates.remove(service_identifier)
|
|
321
411
|
|
|
322
412
|
if len(candidates) == 1:
|
|
323
413
|
service_identifier = candidates.pop()
|
|
324
|
-
return services.get(
|
|
414
|
+
return services.get(service_identifier.name, service_identifier.protocol)
|
|
325
415
|
|
|
326
416
|
# 3. check the path if it is set and not a trivial root path
|
|
327
417
|
if path and path != "/":
|
|
328
418
|
# try to find a match with the custom path rules
|
|
329
419
|
custom_path_match = custom_path_addressing_rules(path)
|
|
330
420
|
if custom_path_match:
|
|
331
|
-
return services.get(
|
|
421
|
+
return services.get(custom_path_match.name, custom_path_match.protocol)
|
|
332
422
|
|
|
333
423
|
# 4. check the host (custom host addressing rules)
|
|
334
424
|
if host:
|
|
@@ -337,12 +427,14 @@ def determine_aws_service_model(
|
|
|
337
427
|
# this prevents a virtual host addressed bucket to be wrongly recognized
|
|
338
428
|
if host.startswith(f"{prefix}.") and ".s3." not in host:
|
|
339
429
|
if len(services_per_prefix) == 1:
|
|
340
|
-
|
|
430
|
+
candidate = services_per_prefix[0]
|
|
431
|
+
return services.get(candidate.name, candidate.protocol)
|
|
341
432
|
candidates.update(services_per_prefix)
|
|
342
433
|
|
|
343
434
|
custom_host_match = custom_host_addressing_rules(host)
|
|
344
435
|
if custom_host_match:
|
|
345
|
-
|
|
436
|
+
candidate = custom_host_match[0]
|
|
437
|
+
return services.get(candidate.name, candidate.protocol)
|
|
346
438
|
|
|
347
439
|
if request.shallow:
|
|
348
440
|
# from here on we would need access to the request body, which doesn't exist for shallow requests like
|
|
@@ -357,21 +449,28 @@ def determine_aws_service_model(
|
|
|
357
449
|
query_candidates = [
|
|
358
450
|
service
|
|
359
451
|
for service in services.by_operation(values["Action"])
|
|
360
|
-
if
|
|
452
|
+
if any(
|
|
453
|
+
is_protocol_in_service_model_identifier(protocol, service)
|
|
454
|
+
for protocol in ("ec2", "query")
|
|
455
|
+
)
|
|
361
456
|
]
|
|
362
457
|
|
|
363
458
|
if len(query_candidates) == 1:
|
|
364
|
-
|
|
459
|
+
candidate = query_candidates[0]
|
|
460
|
+
return services.get(candidate.name, candidate.protocol)
|
|
365
461
|
|
|
366
462
|
if "Version" in values:
|
|
367
463
|
for service_identifier in list(query_candidates):
|
|
368
|
-
service_model = services.get(
|
|
464
|
+
service_model = services.get(
|
|
465
|
+
service_identifier.name, service_identifier.protocol
|
|
466
|
+
)
|
|
369
467
|
if values["Version"] != service_model.api_version:
|
|
370
468
|
# the combination of Version and Action is not unique, add matches to the candidates
|
|
371
469
|
query_candidates.remove(service_identifier)
|
|
372
470
|
|
|
373
471
|
if len(query_candidates) == 1:
|
|
374
|
-
|
|
472
|
+
candidate = query_candidates[0]
|
|
473
|
+
return services.get(candidate.name, candidate.protocol)
|
|
375
474
|
|
|
376
475
|
candidates.update(query_candidates)
|
|
377
476
|
|
|
@@ -387,15 +486,16 @@ def determine_aws_service_model(
|
|
|
387
486
|
# 6. resolve service spec conflicts
|
|
388
487
|
resolved_conflict = resolve_conflicts(candidates, request)
|
|
389
488
|
if resolved_conflict:
|
|
390
|
-
return services.get(
|
|
489
|
+
return services.get(resolved_conflict.name, resolved_conflict.protocol)
|
|
391
490
|
|
|
392
491
|
# 7. check the legacy S3 rules in the end
|
|
393
492
|
legacy_match = legacy_s3_rules(request)
|
|
394
493
|
if legacy_match:
|
|
395
|
-
return services.get(
|
|
494
|
+
return services.get(legacy_match.name, legacy_match.protocol)
|
|
396
495
|
|
|
397
496
|
if signing_name:
|
|
398
497
|
return services.get(name=signing_name)
|
|
399
498
|
if candidates:
|
|
400
|
-
|
|
499
|
+
candidate = candidates.pop()
|
|
500
|
+
return services.get(candidate.name, candidate.protocol)
|
|
401
501
|
return None
|
|
@@ -159,7 +159,7 @@ class ParamValidator(BotocoreParamValidator):
|
|
|
159
159
|
if required_member in params and params[required_member] is None:
|
|
160
160
|
params.pop(required_member)
|
|
161
161
|
|
|
162
|
-
super(
|
|
162
|
+
super()._validate_structure(params, shape, errors, name)
|
|
163
163
|
|
|
164
164
|
|
|
165
165
|
def validate_request(operation: OperationModel, request: ServiceRequest) -> ValidationErrors:
|
localstack/aws/scaffold.py
CHANGED
|
@@ -221,7 +221,7 @@ class ShapeNode:
|
|
|
221
221
|
|
|
222
222
|
def _print_as_typed_dict(self, output, doc=True, quote_types=False):
|
|
223
223
|
name = to_valid_python_name(self.shape.name)
|
|
224
|
-
output.write('
|
|
224
|
+
output.write(f'{name} = TypedDict("{name}", {{\n')
|
|
225
225
|
for k, v in self.shape.members.items():
|
|
226
226
|
member_name = to_valid_python_name(v.name)
|
|
227
227
|
# check if the member name is the same as the type name (recursive types need to use forward references)
|
localstack/aws/skeleton.py
CHANGED
|
@@ -130,14 +130,16 @@ class Skeleton:
|
|
|
130
130
|
self.dispatch_table = create_dispatch_table(implementation)
|
|
131
131
|
|
|
132
132
|
def invoke(self, context: RequestContext) -> Response:
|
|
133
|
-
serializer = create_serializer(context.service)
|
|
133
|
+
serializer = create_serializer(context.service, context.protocol)
|
|
134
134
|
|
|
135
135
|
if context.operation and context.service_request:
|
|
136
136
|
# if the parsed request is already set in the context, re-use them
|
|
137
137
|
operation, instance = context.operation, context.service_request
|
|
138
138
|
else:
|
|
139
139
|
# otherwise, parse the incoming HTTPRequest
|
|
140
|
-
operation, instance = create_parser(context.service).parse(
|
|
140
|
+
operation, instance = create_parser(context.service, context.protocol).parse(
|
|
141
|
+
context.request
|
|
142
|
+
)
|
|
141
143
|
context.operation = operation
|
|
142
144
|
|
|
143
145
|
try:
|
localstack/aws/spec-patches.json
CHANGED
|
@@ -1329,6 +1329,26 @@
|
|
|
1329
1329
|
"documentation": "<p>The Content-MD5 you specified did not match what we received.</p>",
|
|
1330
1330
|
"exception": true
|
|
1331
1331
|
}
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
"op": "add",
|
|
1335
|
+
"path": "/shapes/AuthorizationHeaderMalformed",
|
|
1336
|
+
"value": {
|
|
1337
|
+
"type": "structure",
|
|
1338
|
+
"members": {
|
|
1339
|
+
"Region": {
|
|
1340
|
+
"shape": "BucketRegion"
|
|
1341
|
+
},
|
|
1342
|
+
"HostId": {
|
|
1343
|
+
"shape": "HostId"
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
"error": {
|
|
1347
|
+
"httpStatusCode": 400
|
|
1348
|
+
},
|
|
1349
|
+
"documentation": "<p>The authorization header is malformed.</p>",
|
|
1350
|
+
"exception": true
|
|
1351
|
+
}
|
|
1332
1352
|
}
|
|
1333
1353
|
],
|
|
1334
1354
|
"apigatewayv2/2018-11-29/service-2": [
|
|
@@ -1352,5 +1372,43 @@
|
|
|
1352
1372
|
"path": "/operations/CreateApiMapping/http/responseCode",
|
|
1353
1373
|
"value": 200
|
|
1354
1374
|
}
|
|
1375
|
+
],
|
|
1376
|
+
"cloudwatch/2010-08-01/service-2": [
|
|
1377
|
+
{
|
|
1378
|
+
"op": "add",
|
|
1379
|
+
"path": "/metadata/awsQueryCompatible",
|
|
1380
|
+
"value": {}
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
"op": "add",
|
|
1384
|
+
"path": "/metadata/jsonVersion",
|
|
1385
|
+
"value": "1.0"
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
"op": "add",
|
|
1389
|
+
"path": "/metadata/targetPrefix",
|
|
1390
|
+
"value": "GraniteServiceVersion20100801"
|
|
1391
|
+
},
|
|
1392
|
+
{
|
|
1393
|
+
"op": "replace",
|
|
1394
|
+
"path": "/metadata/protocol",
|
|
1395
|
+
"value": "smithy-rpc-v2-cbor"
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
"op": "replace",
|
|
1399
|
+
"path": "/metadata/protocols",
|
|
1400
|
+
"value": [
|
|
1401
|
+
"smithy-rpc-v2-cbor",
|
|
1402
|
+
"json",
|
|
1403
|
+
"query"
|
|
1404
|
+
]
|
|
1405
|
+
},
|
|
1406
|
+
{
|
|
1407
|
+
"op": "add",
|
|
1408
|
+
"path": "/shapes/ConflictException/error",
|
|
1409
|
+
"value": {
|
|
1410
|
+
"httpStatusCode": 409
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1355
1413
|
]
|
|
1356
1414
|
}
|
localstack/aws/spec.py
CHANGED
|
@@ -21,7 +21,7 @@ from localstack.utils.objects import singleton_factory
|
|
|
21
21
|
LOG = logging.getLogger(__name__)
|
|
22
22
|
|
|
23
23
|
ServiceName = str
|
|
24
|
-
ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2"]
|
|
24
|
+
ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2", "smithy-rpc-v2-cbor"]
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class ServiceModelIdentifier(NamedTuple):
|
|
@@ -33,6 +33,7 @@ class ServiceModelIdentifier(NamedTuple):
|
|
|
33
33
|
|
|
34
34
|
name: ServiceName
|
|
35
35
|
protocol: ProtocolName | None = None
|
|
36
|
+
protocols: tuple[ProtocolName] | None = None
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
spec_patches_json = os.path.join(os.path.dirname(__file__), "spec-patches.json")
|
|
@@ -69,7 +70,7 @@ class PatchingLoader(Loader):
|
|
|
69
70
|
|
|
70
71
|
@instance_cache
|
|
71
72
|
def load_data(self, name: str):
|
|
72
|
-
result = super(
|
|
73
|
+
result = super().load_data(name)
|
|
73
74
|
|
|
74
75
|
if patches := self.patches.get(name):
|
|
75
76
|
return jsonpatch.apply_patch(result, patches)
|
|
@@ -114,9 +115,13 @@ def load_service(
|
|
|
114
115
|
:raises: UnknownServiceProtocolError if the specific protocol of the service cannot be found
|
|
115
116
|
"""
|
|
116
117
|
service_description = loader.load_service_model(service, "service-2", version)
|
|
118
|
+
service_metadata = service_description.get("metadata", {})
|
|
119
|
+
service_protocols = {service_metadata.get("protocol")}
|
|
120
|
+
if protocols := service_metadata.get("protocols"):
|
|
121
|
+
service_protocols.update(protocols)
|
|
117
122
|
|
|
118
123
|
# check if the protocol is defined, and if so, if the loaded service defines this protocol
|
|
119
|
-
if protocol is not None and protocol
|
|
124
|
+
if protocol is not None and protocol not in service_protocols:
|
|
120
125
|
# if the protocol is defined, but not the one of the currently loaded service,
|
|
121
126
|
# check if we already loaded the custom spec based on the naming convention (<service>-<protocol>),
|
|
122
127
|
# f.e. "sqs-query"
|
|
@@ -132,7 +137,7 @@ def load_service(
|
|
|
132
137
|
|
|
133
138
|
# remove potential protocol names from the service name
|
|
134
139
|
# FIXME add more protocols here if we have to internalize more than just sqs-query
|
|
135
|
-
# TODO this should not contain specific internalized
|
|
140
|
+
# TODO this should not contain specific internalized service names
|
|
136
141
|
service = {"sqs-query": "sqs"}.get(service, service)
|
|
137
142
|
return ServiceModel(service_description, service)
|
|
138
143
|
|
|
@@ -149,6 +154,27 @@ def iterate_service_operations() -> Generator[tuple[ServiceModel, OperationModel
|
|
|
149
154
|
yield service, service.operation_model(op_name)
|
|
150
155
|
|
|
151
156
|
|
|
157
|
+
def is_protocol_in_service_model_identifier(
|
|
158
|
+
protocol: ProtocolName, service_model_identifier: ServiceModelIdentifier
|
|
159
|
+
) -> bool:
|
|
160
|
+
"""
|
|
161
|
+
:param protocol: the protocol name to check
|
|
162
|
+
:param service_model_identifier:
|
|
163
|
+
:return: boolean to indicate if the protocol is available for that service
|
|
164
|
+
"""
|
|
165
|
+
protocols = service_model_identifier.protocols or []
|
|
166
|
+
return protocol in protocols or protocol == service_model_identifier.protocol
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_service_model_identifier(service_model: ServiceModel) -> ServiceModelIdentifier:
|
|
170
|
+
protocols = service_model.metadata.get("protocols")
|
|
171
|
+
return ServiceModelIdentifier(
|
|
172
|
+
name=service_model.service_name,
|
|
173
|
+
protocol=service_model.protocol,
|
|
174
|
+
protocols=tuple(protocols) if protocols else None,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
152
178
|
@dataclasses.dataclass
|
|
153
179
|
class ServiceCatalogIndex:
|
|
154
180
|
"""
|
|
@@ -178,9 +204,7 @@ class LazyServiceCatalogIndex:
|
|
|
178
204
|
for service_model in service_models:
|
|
179
205
|
target_prefix = service_model.metadata.get("targetPrefix")
|
|
180
206
|
if target_prefix:
|
|
181
|
-
result[target_prefix].append(
|
|
182
|
-
ServiceModelIdentifier(service_model.service_name, service_model.protocol)
|
|
183
|
-
)
|
|
207
|
+
result[target_prefix].append(get_service_model_identifier(service_model))
|
|
184
208
|
return dict(result)
|
|
185
209
|
|
|
186
210
|
@cached_property
|
|
@@ -189,7 +213,7 @@ class LazyServiceCatalogIndex:
|
|
|
189
213
|
for service_models in self._services.values():
|
|
190
214
|
for service_model in service_models:
|
|
191
215
|
result[service_model.signing_name].append(
|
|
192
|
-
|
|
216
|
+
get_service_model_identifier(service_model)
|
|
193
217
|
)
|
|
194
218
|
return dict(result)
|
|
195
219
|
|
|
@@ -201,11 +225,7 @@ class LazyServiceCatalogIndex:
|
|
|
201
225
|
operations = service_model.operation_names
|
|
202
226
|
if operations:
|
|
203
227
|
for operation in operations:
|
|
204
|
-
result[operation].append(
|
|
205
|
-
ServiceModelIdentifier(
|
|
206
|
-
service_model.service_name, service_model.protocol
|
|
207
|
-
)
|
|
208
|
-
)
|
|
228
|
+
result[operation].append(get_service_model_identifier(service_model))
|
|
209
229
|
return dict(result)
|
|
210
230
|
|
|
211
231
|
@cached_property
|
|
@@ -214,7 +234,7 @@ class LazyServiceCatalogIndex:
|
|
|
214
234
|
for service_models in self._services.values():
|
|
215
235
|
for service_model in service_models:
|
|
216
236
|
result[service_model.endpoint_prefix].append(
|
|
217
|
-
|
|
237
|
+
get_service_model_identifier(service_model)
|
|
218
238
|
)
|
|
219
239
|
return dict(result)
|
|
220
240
|
|
|
@@ -343,8 +363,9 @@ def get_service_catalog() -> ServiceCatalog:
|
|
|
343
363
|
index = build_service_index_cache(cache_catalog_file)
|
|
344
364
|
return ServiceCatalog(index)
|
|
345
365
|
except Exception:
|
|
346
|
-
LOG.
|
|
347
|
-
"error while processing service catalog index cache, falling back to lazy-loaded index"
|
|
366
|
+
LOG.error(
|
|
367
|
+
"error while processing service catalog index cache, falling back to lazy-loaded index",
|
|
368
|
+
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
348
369
|
)
|
|
349
370
|
return ServiceCatalog()
|
|
350
371
|
|
localstack/cli/exceptions.py
CHANGED
|
@@ -12,7 +12,7 @@ class CLIError(ClickException):
|
|
|
12
12
|
def format_message(self) -> str:
|
|
13
13
|
return click.style(f"❌ Error: {self.message}", fg="red")
|
|
14
14
|
|
|
15
|
-
def show(self, file: t.
|
|
15
|
+
def show(self, file: t.IO[t.Any] | None = None) -> None:
|
|
16
16
|
if file is None:
|
|
17
17
|
file = get_text_stderr()
|
|
18
18
|
|
localstack/cli/localstack.py
CHANGED
|
@@ -3,7 +3,7 @@ import logging
|
|
|
3
3
|
import os
|
|
4
4
|
import sys
|
|
5
5
|
import traceback
|
|
6
|
-
from typing import
|
|
6
|
+
from typing import TypedDict
|
|
7
7
|
|
|
8
8
|
import click
|
|
9
9
|
import requests
|
|
@@ -47,7 +47,7 @@ class LocalStackCliGroup(click.Group):
|
|
|
47
47
|
|
|
48
48
|
def invoke(self, ctx: click.Context):
|
|
49
49
|
try:
|
|
50
|
-
return super(
|
|
50
|
+
return super().invoke(ctx)
|
|
51
51
|
except click.exceptions.Exit:
|
|
52
52
|
# raise Exit exceptions unmodified (e.g., raised on --help)
|
|
53
53
|
raise
|
|
@@ -320,8 +320,8 @@ class DockerStatus(TypedDict, total=False):
|
|
|
320
320
|
image_tag: str
|
|
321
321
|
image_id: str
|
|
322
322
|
image_created: str
|
|
323
|
-
container_name:
|
|
324
|
-
container_ip:
|
|
323
|
+
container_name: str | None
|
|
324
|
+
container_ip: str | None
|
|
325
325
|
|
|
326
326
|
|
|
327
327
|
def _print_docker_status(format_: str) -> None:
|
|
@@ -602,7 +602,7 @@ def cmd_stop() -> None:
|
|
|
602
602
|
|
|
603
603
|
try:
|
|
604
604
|
DOCKER_CLIENT.stop_container(container_name)
|
|
605
|
-
console.print("container stopped:
|
|
605
|
+
console.print(f"container stopped: {container_name}")
|
|
606
606
|
except NoSuchContainer:
|
|
607
607
|
raise CLIError(
|
|
608
608
|
f'Expected a running LocalStack container named "{container_name}", but found none'
|
|
@@ -699,7 +699,7 @@ def cmd_logs(follow: bool, tail: int) -> None:
|
|
|
699
699
|
metavar="N",
|
|
700
700
|
)
|
|
701
701
|
@publish_invocation
|
|
702
|
-
def cmd_wait(timeout:
|
|
702
|
+
def cmd_wait(timeout: float | None = None) -> None:
|
|
703
703
|
"""
|
|
704
704
|
Wait for the LocalStack runtime to be up and running.
|
|
705
705
|
|
localstack/cli/lpm.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import itertools
|
|
2
2
|
import logging
|
|
3
3
|
from multiprocessing.pool import ThreadPool
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
import click
|
|
7
6
|
from rich.console import Console
|
|
@@ -75,9 +74,9 @@ def _do_install_package(package: Package, version: str = None, target: InstallTa
|
|
|
75
74
|
)
|
|
76
75
|
def install(
|
|
77
76
|
package: list[str],
|
|
78
|
-
parallel:
|
|
79
|
-
version:
|
|
80
|
-
target:
|
|
77
|
+
parallel: int | None = 1,
|
|
78
|
+
version: str | None = None,
|
|
79
|
+
target: str | None = None,
|
|
81
80
|
):
|
|
82
81
|
"""Install one or more packages."""
|
|
83
82
|
try:
|
localstack/cli/plugins.py
CHANGED
localstack/cli/profiles.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import os
|
|
3
3
|
import sys
|
|
4
|
-
from typing import Optional
|
|
5
4
|
|
|
6
5
|
# important: this needs to be free of localstack imports
|
|
7
6
|
|
|
@@ -44,7 +43,7 @@ def set_and_remove_profile_from_sys_argv():
|
|
|
44
43
|
os.environ["CONFIG_PROFILE"] = profile.strip()
|
|
45
44
|
|
|
46
45
|
|
|
47
|
-
def parse_p_argument(args) ->
|
|
46
|
+
def parse_p_argument(args) -> str | None:
|
|
48
47
|
"""
|
|
49
48
|
Lightweight arg parsing to find the first occurrence of ``-p <config>``, or ``-p=<config>`` and return the value of
|
|
50
49
|
``<config>`` from the given arguments.
|