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
|
@@ -4,7 +4,6 @@ This module has utilities relating to creating/parsing AWS requests.
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
|
-
from typing import Optional
|
|
8
7
|
|
|
9
8
|
from rolo import Request as RoloRequest
|
|
10
9
|
|
|
@@ -30,7 +29,7 @@ def get_account_id_from_request(request: RoloRequest) -> str:
|
|
|
30
29
|
return get_account_id_from_access_key_id(access_key_id)
|
|
31
30
|
|
|
32
31
|
|
|
33
|
-
def extract_region_from_auth_header(headers) ->
|
|
32
|
+
def extract_region_from_auth_header(headers) -> str | None:
|
|
34
33
|
auth = headers.get("Authorization") or ""
|
|
35
34
|
region = re.sub(r".*Credential=[^/]+/[^/]+/([^/]+)/.*", r"\1", auth)
|
|
36
35
|
if region == auth:
|
|
@@ -38,12 +37,12 @@ def extract_region_from_auth_header(headers) -> Optional[str]:
|
|
|
38
37
|
return region
|
|
39
38
|
|
|
40
39
|
|
|
41
|
-
def extract_account_id_from_auth_header(headers) ->
|
|
40
|
+
def extract_account_id_from_auth_header(headers) -> str | None:
|
|
42
41
|
if access_key_id := extract_access_key_id_from_auth_header(headers):
|
|
43
42
|
return get_account_id_from_access_key_id(access_key_id)
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
def extract_access_key_id_from_auth_header(headers: dict[str, str]) ->
|
|
45
|
+
def extract_access_key_id_from_auth_header(headers: dict[str, str]) -> str | None:
|
|
47
46
|
auth = headers.get("Authorization") or ""
|
|
48
47
|
|
|
49
48
|
if auth.startswith("AWS4-"):
|
|
@@ -67,7 +66,7 @@ def extract_region_from_headers(headers) -> str:
|
|
|
67
66
|
return extract_region_from_auth_header(headers) or AWS_REGION_US_EAST_1
|
|
68
67
|
|
|
69
68
|
|
|
70
|
-
def extract_service_name_from_auth_header(headers: dict) ->
|
|
69
|
+
def extract_service_name_from_auth_header(headers: dict) -> str | None:
|
|
71
70
|
try:
|
|
72
71
|
auth_header = headers.get("authorization", "")
|
|
73
72
|
credential_scope = auth_header.split(",")[0].split()[1]
|
localstack/utils/batch_policy.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import time
|
|
3
|
-
from typing import Generic,
|
|
3
|
+
from typing import Generic, TypeVar, overload
|
|
4
4
|
|
|
5
5
|
from pydantic import Field
|
|
6
6
|
from pydantic.dataclasses import dataclass
|
|
@@ -47,8 +47,8 @@ class Batcher(Generic[T]):
|
|
|
47
47
|
assert batcher.flush() == ["item1", "item2", "item3", "item4"]
|
|
48
48
|
"""
|
|
49
49
|
|
|
50
|
-
max_count:
|
|
51
|
-
max_window:
|
|
50
|
+
max_count: int | None = Field(default=None, description="Maximum number of items", ge=0)
|
|
51
|
+
max_window: float | None = Field(
|
|
52
52
|
default=None, description="Maximum time window in seconds", ge=0
|
|
53
53
|
)
|
|
54
54
|
|
localstack/utils/bootstrap.py
CHANGED
|
@@ -9,9 +9,9 @@ import shlex
|
|
|
9
9
|
import signal
|
|
10
10
|
import threading
|
|
11
11
|
import time
|
|
12
|
-
from collections.abc import Iterable
|
|
12
|
+
from collections.abc import Callable, Iterable
|
|
13
13
|
from functools import wraps
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
16
|
from localstack import config, constants
|
|
17
17
|
from localstack.config import (
|
|
@@ -175,7 +175,7 @@ def get_docker_image_details(image_name: str = None) -> dict[str, str]:
|
|
|
175
175
|
return result
|
|
176
176
|
|
|
177
177
|
|
|
178
|
-
def get_image_environment_variable(env_name: str) ->
|
|
178
|
+
def get_image_environment_variable(env_name: str) -> str | None:
|
|
179
179
|
image_name = get_docker_image_to_start()
|
|
180
180
|
image_info = DOCKER_CLIENT.inspect_image(image_name)
|
|
181
181
|
image_envs = image_info["Config"]["Env"]
|
|
@@ -544,7 +544,7 @@ class ContainerConfigurators:
|
|
|
544
544
|
|
|
545
545
|
@staticmethod
|
|
546
546
|
def gateway_listen(
|
|
547
|
-
port:
|
|
547
|
+
port: int | Iterable[int] | HostAndPort | Iterable[HostAndPort],
|
|
548
548
|
):
|
|
549
549
|
"""
|
|
550
550
|
Uses the given ports to configure GATEWAY_LISTEN. For instance, ``gateway_listen([4566, 443])`` would
|
|
@@ -1000,7 +1000,7 @@ class RunningContainer:
|
|
|
1000
1000
|
return
|
|
1001
1001
|
raise
|
|
1002
1002
|
|
|
1003
|
-
def inspect(self) -> dict[str,
|
|
1003
|
+
def inspect(self) -> dict[str, dict | str]:
|
|
1004
1004
|
return self.container_client.inspect_container(container_name_or_id=self.id)
|
|
1005
1005
|
|
|
1006
1006
|
def attach(self):
|
|
@@ -1028,7 +1028,7 @@ class ContainerLogPrinter:
|
|
|
1028
1028
|
self.callback = callback
|
|
1029
1029
|
|
|
1030
1030
|
self._closed = threading.Event()
|
|
1031
|
-
self._stream:
|
|
1031
|
+
self._stream: CancellableStream | None = None
|
|
1032
1032
|
|
|
1033
1033
|
def _can_start_streaming(self):
|
|
1034
1034
|
if self._closed.is_set():
|
|
@@ -1338,7 +1338,7 @@ def start_infra_in_docker_detached(console, cli_params: dict[str, Any] = None):
|
|
|
1338
1338
|
console.log("detaching")
|
|
1339
1339
|
|
|
1340
1340
|
|
|
1341
|
-
def wait_container_is_ready(timeout:
|
|
1341
|
+
def wait_container_is_ready(timeout: float | None = None):
|
|
1342
1342
|
"""Blocks until the localstack main container is running and the ready marker has been printed."""
|
|
1343
1343
|
container_name = config.MAIN_CONTAINER_NAME
|
|
1344
1344
|
started = time.time()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
|
|
4
|
+
from plux import Plugin
|
|
5
|
+
|
|
6
|
+
from localstack.services.cloudformation.resource_provider import (
|
|
7
|
+
plugin_manager as cfn_plugin_manager,
|
|
8
|
+
)
|
|
9
|
+
from localstack.utils.catalog.catalog_loader import RemoteCatalogLoader
|
|
10
|
+
from localstack.utils.catalog.common import (
|
|
11
|
+
AwsServiceOperationsSupportInLatest,
|
|
12
|
+
AwsServicesSupportInLatest,
|
|
13
|
+
AwsServiceSupportAtRuntime,
|
|
14
|
+
CloudFormationResourcesSupportAtRuntime,
|
|
15
|
+
CloudFormationResourcesSupportInLatest,
|
|
16
|
+
LocalstackEmulatorType,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
ServiceName = str
|
|
20
|
+
ServiceOperations = set[str]
|
|
21
|
+
ProviderName = str
|
|
22
|
+
CfnResourceName = str
|
|
23
|
+
CfnResourceMethodName = str
|
|
24
|
+
AwsServicesSupportStatus = (
|
|
25
|
+
AwsServiceSupportAtRuntime | AwsServicesSupportInLatest | AwsServiceOperationsSupportInLatest
|
|
26
|
+
)
|
|
27
|
+
CfnResourceSupportStatus = (
|
|
28
|
+
CloudFormationResourcesSupportInLatest | CloudFormationResourcesSupportAtRuntime
|
|
29
|
+
)
|
|
30
|
+
CfnResourceCatalog = dict[LocalstackEmulatorType, dict[CfnResourceName, set[CfnResourceMethodName]]]
|
|
31
|
+
|
|
32
|
+
LOG = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CatalogPlugin(Plugin):
|
|
36
|
+
namespace = "localstack.utils.catalog"
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _get_cfn_resources_catalog(cloudformation_resources: dict) -> CfnResourceCatalog:
|
|
40
|
+
cfn_resources_catalog = {}
|
|
41
|
+
for emulator_type, resources in cloudformation_resources.items():
|
|
42
|
+
cfn_resources_catalog[emulator_type] = {}
|
|
43
|
+
for resource_name, resource in resources.items():
|
|
44
|
+
cfn_resources_catalog[emulator_type][resource_name] = set(resource.methods)
|
|
45
|
+
return cfn_resources_catalog
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def _get_services_at_runtime() -> set[ServiceName]:
|
|
49
|
+
from localstack.services.plugins import SERVICE_PLUGINS
|
|
50
|
+
|
|
51
|
+
return set(SERVICE_PLUGINS.list_available())
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def _get_cfn_resources_available_at_runtime() -> set[CfnResourceName]:
|
|
55
|
+
return set(cfn_plugin_manager.list_names())
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
def get_aws_service_status(
|
|
59
|
+
self, service_name: str, operation_name: str | None = None
|
|
60
|
+
) -> AwsServicesSupportStatus | None:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def get_cloudformation_resource_status(
|
|
65
|
+
self, resource_name: str, service_name: str, is_pro_resource: bool = False
|
|
66
|
+
) -> CfnResourceSupportStatus | AwsServicesSupportInLatest | None:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class AwsCatalogRuntimePlugin(CatalogPlugin):
|
|
71
|
+
name = "aws-catalog-runtime-only"
|
|
72
|
+
|
|
73
|
+
def get_aws_service_status(
|
|
74
|
+
self, service_name: str, operation_name: str | None = None
|
|
75
|
+
) -> AwsServicesSupportStatus | None:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def get_cloudformation_resource_status(
|
|
79
|
+
self, resource_name: str, service_name: str, is_pro_resource: bool = False
|
|
80
|
+
) -> CfnResourceSupportStatus | AwsServicesSupportInLatest | None:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AwsCatalogRemoteStatePlugin(CatalogPlugin):
|
|
85
|
+
name = "aws-catalog-remote-state"
|
|
86
|
+
current_emulator_type: LocalstackEmulatorType = LocalstackEmulatorType.COMMUNITY
|
|
87
|
+
services_in_latest: dict[ServiceName, dict[LocalstackEmulatorType, ServiceOperations]] = {}
|
|
88
|
+
services_at_runtime: set[ServiceName] = set()
|
|
89
|
+
cfn_resources_in_latest: CfnResourceCatalog = {}
|
|
90
|
+
cfn_resources_at_runtime: set[CfnResourceName] = set()
|
|
91
|
+
|
|
92
|
+
def __init__(self, remote_catalog_loader: RemoteCatalogLoader | None = None) -> None:
|
|
93
|
+
catalog_loader = remote_catalog_loader or RemoteCatalogLoader()
|
|
94
|
+
remote_catalog = catalog_loader.get_remote_catalog()
|
|
95
|
+
for service_name, emulators in remote_catalog.services.items():
|
|
96
|
+
for emulator_type, service_provider in emulators.items():
|
|
97
|
+
self.services_in_latest.setdefault(service_name, {})[emulator_type] = set(
|
|
98
|
+
service_provider.operations
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self.cfn_resources_in_latest = self._get_cfn_resources_catalog(
|
|
102
|
+
remote_catalog.cloudformation_resources
|
|
103
|
+
)
|
|
104
|
+
self.cfn_resources_at_runtime = self._get_cfn_resources_available_at_runtime()
|
|
105
|
+
self.services_at_runtime = self._get_services_at_runtime()
|
|
106
|
+
|
|
107
|
+
def get_aws_service_status(
|
|
108
|
+
self, service_name: str, operation_name: str | None = None
|
|
109
|
+
) -> AwsServicesSupportStatus | None:
|
|
110
|
+
if not self.services_in_latest:
|
|
111
|
+
return None
|
|
112
|
+
if service_name not in self.services_in_latest:
|
|
113
|
+
return AwsServicesSupportInLatest.NOT_SUPPORTED
|
|
114
|
+
if self.current_emulator_type not in self.services_in_latest[service_name]:
|
|
115
|
+
return AwsServicesSupportInLatest.SUPPORTED_WITH_LICENSE_UPGRADE
|
|
116
|
+
if not operation_name:
|
|
117
|
+
return AwsServicesSupportInLatest.SUPPORTED
|
|
118
|
+
if operation_name in self.services_in_latest[service_name][self.current_emulator_type]:
|
|
119
|
+
return AwsServiceOperationsSupportInLatest.SUPPORTED
|
|
120
|
+
for emulator_type in self.services_in_latest[service_name]:
|
|
121
|
+
if emulator_type is self.current_emulator_type:
|
|
122
|
+
continue
|
|
123
|
+
if operation_name in self.services_in_latest[service_name][emulator_type]:
|
|
124
|
+
return AwsServiceOperationsSupportInLatest.SUPPORTED_WITH_LICENSE_UPGRADE
|
|
125
|
+
return AwsServiceOperationsSupportInLatest.NOT_SUPPORTED
|
|
126
|
+
|
|
127
|
+
def get_cloudformation_resource_status(
|
|
128
|
+
self, resource_name: str, service_name: str, is_pro_resource: bool = False
|
|
129
|
+
) -> CfnResourceSupportStatus | AwsServicesSupportInLatest | None:
|
|
130
|
+
if resource_name in self.cfn_resources_at_runtime:
|
|
131
|
+
return CloudFormationResourcesSupportAtRuntime.AVAILABLE
|
|
132
|
+
if service_name in self.services_at_runtime:
|
|
133
|
+
if resource_name in self.cfn_resources_in_latest[self.current_emulator_type]:
|
|
134
|
+
return CloudFormationResourcesSupportInLatest.SUPPORTED
|
|
135
|
+
else:
|
|
136
|
+
return CloudFormationResourcesSupportInLatest.NOT_SUPPORTED
|
|
137
|
+
if service_name in self.services_in_latest:
|
|
138
|
+
return self.get_aws_service_status(service_name, operation_name=None)
|
|
139
|
+
return AwsServicesSupportInLatest.NOT_SUPPORTED
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from json import JSONDecodeError
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from localstack import config, constants
|
|
10
|
+
from localstack.utils.catalog.common import AwsRemoteCatalog
|
|
11
|
+
from localstack.utils.http import get_proxies
|
|
12
|
+
from localstack.utils.json import FileMappedDocument
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
AWS_CATALOG_FILE_NAME = "aws_catalog.json"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RemoteCatalogVersionResponse(BaseModel):
|
|
20
|
+
emulator_type: str
|
|
21
|
+
version: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AwsCatalogLoaderException(Exception):
|
|
25
|
+
def __init__(self, msg: str, *args):
|
|
26
|
+
super().__init__(msg, *args)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RemoteCatalogLoader:
|
|
30
|
+
supported_schema_version = "v1"
|
|
31
|
+
api_endpoint_catalog = f"{constants.API_ENDPOINT}/license/catalog"
|
|
32
|
+
catalog_file_path = Path(config.dirs.cache) / AWS_CATALOG_FILE_NAME
|
|
33
|
+
|
|
34
|
+
def get_remote_catalog(self) -> AwsRemoteCatalog:
|
|
35
|
+
catalog_doc = FileMappedDocument(self.catalog_file_path)
|
|
36
|
+
cached_catalog = AwsRemoteCatalog(**catalog_doc) if catalog_doc else None
|
|
37
|
+
if cached_catalog:
|
|
38
|
+
cached_catalog_version = cached_catalog.localstack.version
|
|
39
|
+
if not self._should_update_cached_catalog(cached_catalog_version):
|
|
40
|
+
return cached_catalog
|
|
41
|
+
catalog = self._get_catalog_from_platform()
|
|
42
|
+
self._save_catalog_to_cache(catalog_doc, catalog)
|
|
43
|
+
return catalog
|
|
44
|
+
|
|
45
|
+
def _get_latest_localstack_version(self) -> str:
|
|
46
|
+
try:
|
|
47
|
+
proxies = get_proxies()
|
|
48
|
+
response = requests.get(
|
|
49
|
+
f"{self.api_endpoint_catalog}/aws/version",
|
|
50
|
+
verify=not config.is_env_true("SSL_NO_VERIFY"),
|
|
51
|
+
proxies=proxies,
|
|
52
|
+
)
|
|
53
|
+
if response.ok:
|
|
54
|
+
return RemoteCatalogVersionResponse.model_validate(response.content).version
|
|
55
|
+
self._raise_server_error(response)
|
|
56
|
+
except requests.exceptions.RequestException as e:
|
|
57
|
+
raise AwsCatalogLoaderException(
|
|
58
|
+
f"An unexpected network error occurred when trying to fetch latest localstack version: {e}"
|
|
59
|
+
) from e
|
|
60
|
+
|
|
61
|
+
def _should_update_cached_catalog(self, current_catalog_version: str) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
latest_version = self._get_latest_localstack_version()
|
|
64
|
+
return latest_version != current_catalog_version
|
|
65
|
+
except Exception as e:
|
|
66
|
+
LOG.warning(
|
|
67
|
+
"Failed to retrieve the latest catalog version, cached catalog update skipped: %s",
|
|
68
|
+
e,
|
|
69
|
+
)
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def _save_catalog_to_cache(self, catalog_doc: FileMappedDocument, catalog: AwsRemoteCatalog):
|
|
73
|
+
catalog_doc.clear()
|
|
74
|
+
catalog_doc.update(catalog.model_dump())
|
|
75
|
+
catalog_doc.save()
|
|
76
|
+
|
|
77
|
+
def _get_catalog_from_platform(self) -> AwsRemoteCatalog:
|
|
78
|
+
try:
|
|
79
|
+
proxies = get_proxies()
|
|
80
|
+
response = requests.post(
|
|
81
|
+
self.api_endpoint_catalog,
|
|
82
|
+
verify=not config.is_env_true("SSL_NO_VERIFY"),
|
|
83
|
+
proxies=proxies,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if response.ok:
|
|
87
|
+
return self._parse_catalog(response.content)
|
|
88
|
+
self._raise_server_error(response)
|
|
89
|
+
except requests.exceptions.RequestException as e:
|
|
90
|
+
raise AwsCatalogLoaderException(
|
|
91
|
+
f"An unexpected network error occurred when trying to fetch remote catalog: {e}"
|
|
92
|
+
) from e
|
|
93
|
+
|
|
94
|
+
def _parse_catalog(self, document: bytes) -> AwsRemoteCatalog | None:
|
|
95
|
+
try:
|
|
96
|
+
catalog_json = json.loads(document)
|
|
97
|
+
except JSONDecodeError as e:
|
|
98
|
+
raise AwsCatalogLoaderException(f"Could not de-serialize json catalog: {e}") from e
|
|
99
|
+
remote_catalog = AwsRemoteCatalog.model_validate(catalog_json)
|
|
100
|
+
if remote_catalog.schema_version != self.supported_schema_version:
|
|
101
|
+
raise AwsCatalogLoaderException(
|
|
102
|
+
f"Unsupported schema version: '{remote_catalog.schema_version}'. Only '{self.supported_schema_version}' is supported"
|
|
103
|
+
)
|
|
104
|
+
return remote_catalog
|
|
105
|
+
|
|
106
|
+
def _raise_server_error(self, response: requests.Response):
|
|
107
|
+
try:
|
|
108
|
+
server_error = response.json()
|
|
109
|
+
if error_message := server_error.get("message"):
|
|
110
|
+
raise AwsCatalogLoaderException(
|
|
111
|
+
f"Unexpected AWS catalog server error: {response.text}"
|
|
112
|
+
)
|
|
113
|
+
raise AwsCatalogLoaderException(
|
|
114
|
+
f"A server error occurred while calling remote catalog API (HTTP {response.status_code}): {error_message}"
|
|
115
|
+
)
|
|
116
|
+
except Exception:
|
|
117
|
+
raise AwsCatalogLoaderException(
|
|
118
|
+
f"An unexpected server error occurred while calling remote catalog API (HTTP {response.status_code}): {response.text}"
|
|
119
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CloudFormationResource(BaseModel):
|
|
7
|
+
methods: list[str]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AwsServiceCatalog(BaseModel):
|
|
11
|
+
provider: str
|
|
12
|
+
operations: list[str]
|
|
13
|
+
plans: list[str]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class LocalStackMetadata(BaseModel):
|
|
17
|
+
version: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AwsRemoteCatalog(BaseModel):
|
|
21
|
+
schema_version: str
|
|
22
|
+
localstack: LocalStackMetadata
|
|
23
|
+
services: dict[str, dict[str, AwsServiceCatalog]]
|
|
24
|
+
cloudformation_resources: dict[str, dict[str, CloudFormationResource]]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class LocalstackEmulatorType(StrEnum):
|
|
28
|
+
COMMUNITY = "community"
|
|
29
|
+
PRO = "pro"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AwsServiceSupportAtRuntime(StrEnum):
|
|
33
|
+
AVAILABLE = "AVAILABLE"
|
|
34
|
+
AVAILABLE_WITH_LICENSE_UPGRADE = "AVAILABLE_WITH_LICENSE_UPGRADE"
|
|
35
|
+
NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AwsServicesSupportInLatest(StrEnum):
|
|
39
|
+
SUPPORTED = "SUPPORTED"
|
|
40
|
+
SUPPORTED_WITH_LICENSE_UPGRADE = "SUPPORTED_WITH_LICENSE_UPGRADE"
|
|
41
|
+
NOT_SUPPORTED = "NOT_SUPPORTED"
|
|
42
|
+
NON_DEFAULT_PROVIDER = "NON_DEFAULT_PROVIDER"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AwsServiceOperationsSupportInLatest(StrEnum):
|
|
46
|
+
SUPPORTED = "SUPPORTED"
|
|
47
|
+
SUPPORTED_WITH_LICENSE_UPGRADE = "SUPPORTED_WITH_LICENSE_UPGRADE"
|
|
48
|
+
NOT_SUPPORTED = "NOT_SUPPORTED"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CloudFormationResourcesSupportInLatest(StrEnum):
|
|
52
|
+
SUPPORTED = "SUPPORTED"
|
|
53
|
+
NOT_SUPPORTED = "NOT_SUPPORTED"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CloudFormationResourcesSupportAtRuntime(StrEnum):
|
|
57
|
+
AVAILABLE = "AVAILABLE"
|
|
58
|
+
NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from plux import PluginManager
|
|
4
|
+
|
|
5
|
+
from localstack.utils.catalog.catalog import CatalogPlugin
|
|
6
|
+
from localstack.utils.objects import singleton_factory
|
|
7
|
+
|
|
8
|
+
LOG = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@singleton_factory
|
|
12
|
+
def get_aws_catalog() -> CatalogPlugin:
|
|
13
|
+
plugin_manager = PluginManager(CatalogPlugin.namespace)
|
|
14
|
+
try:
|
|
15
|
+
plugin_name = "aws-catalog-remote-state-with-license"
|
|
16
|
+
if not plugin_manager.exists(plugin_name):
|
|
17
|
+
plugin_name = "aws-catalog-remote-state"
|
|
18
|
+
return plugin_manager.load(plugin_name)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
LOG.debug(
|
|
21
|
+
"Failed to load catalog plugin with the latest LocalStack services support data, falling back to catalog without remote state: %s",
|
|
22
|
+
e,
|
|
23
|
+
)
|
|
24
|
+
# Try to load runtime catalog from pro version first
|
|
25
|
+
fallback_plugin_name = "aws-catalog-runtime-only-with-license"
|
|
26
|
+
if not plugin_manager.exists(fallback_plugin_name):
|
|
27
|
+
fallback_plugin_name = "aws-catalog-runtime-only"
|
|
28
|
+
return plugin_manager.load(fallback_plugin_name)
|
|
@@ -2,7 +2,7 @@ import logging
|
|
|
2
2
|
import time
|
|
3
3
|
from datetime import datetime, timezone
|
|
4
4
|
from itertools import islice
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import TypedDict
|
|
6
6
|
|
|
7
7
|
from werkzeug import Response as WerkzeugResponse
|
|
8
8
|
|
|
@@ -20,8 +20,8 @@ LOG = logging.getLogger(__name__)
|
|
|
20
20
|
class SqsMetricBatchData(TypedDict, total=False):
|
|
21
21
|
MetricName: str
|
|
22
22
|
QueueName: str
|
|
23
|
-
Value:
|
|
24
|
-
Unit:
|
|
23
|
+
Value: int | None
|
|
24
|
+
Unit: str | None
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
def dimension_lambda(kwargs):
|
|
@@ -30,7 +30,7 @@ def dimension_lambda(kwargs):
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def publish_lambda_metric(
|
|
33
|
-
metric, value, kwargs, account_id:
|
|
33
|
+
metric, value, kwargs, account_id: str | None = None, region_name: str | None = None
|
|
34
34
|
):
|
|
35
35
|
# publish metric only if CloudWatch service is available
|
|
36
36
|
if not is_api_enabled("cloudwatch"):
|
|
@@ -155,7 +155,7 @@ def store_cloudwatch_logs(
|
|
|
155
155
|
log_stream_name,
|
|
156
156
|
log_output,
|
|
157
157
|
start_time=None,
|
|
158
|
-
auto_create_group:
|
|
158
|
+
auto_create_group: bool | None = True,
|
|
159
159
|
):
|
|
160
160
|
if not is_api_enabled("logs"):
|
|
161
161
|
return
|
localstack/utils/collections.py
CHANGED
|
@@ -5,10 +5,9 @@ and manipulate python collection (dicts, list, sets).
|
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
7
|
import re
|
|
8
|
-
from collections.abc import Iterable, Iterator, Mapping, Sized
|
|
8
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping, Sized
|
|
9
9
|
from typing import (
|
|
10
10
|
Any,
|
|
11
|
-
Callable,
|
|
12
11
|
Optional,
|
|
13
12
|
TypedDict,
|
|
14
13
|
TypeVar,
|
|
@@ -116,7 +115,7 @@ class PaginatedList(list[_ListType]):
|
|
|
116
115
|
next_token: str = None,
|
|
117
116
|
page_size: int = None,
|
|
118
117
|
filter_function: Callable[[_ListType], bool] = None,
|
|
119
|
-
) -> tuple[list[_ListType],
|
|
118
|
+
) -> tuple[list[_ListType], str | None]:
|
|
120
119
|
if filter_function is not None:
|
|
121
120
|
result_list = list(filter(filter_function, self))
|
|
122
121
|
else:
|
|
@@ -148,7 +147,7 @@ class PaginatedList(list[_ListType]):
|
|
|
148
147
|
class CustomExpiryTTLCache(cachetools.TTLCache):
|
|
149
148
|
"""TTLCache that allows to set custom expiry times for individual keys."""
|
|
150
149
|
|
|
151
|
-
def set_expiry(self, key: Any, ttl:
|
|
150
|
+
def set_expiry(self, key: Any, ttl: float | int) -> float:
|
|
152
151
|
"""Set the expiry of the given key in a TTLCache to (<current_time> + <ttl>)"""
|
|
153
152
|
with self.timer as time:
|
|
154
153
|
# note: need to access the internal dunder API here
|
|
@@ -315,7 +314,7 @@ def is_list_or_tuple(obj) -> bool:
|
|
|
315
314
|
return isinstance(obj, (list, tuple))
|
|
316
315
|
|
|
317
316
|
|
|
318
|
-
def ensure_list(obj: Any, wrap_none=False) ->
|
|
317
|
+
def ensure_list(obj: Any, wrap_none=False) -> list | None:
|
|
319
318
|
"""Wrap the given object in a list, or return the object itself if it already is a list."""
|
|
320
319
|
if obj is None and not wrap_none:
|
|
321
320
|
return obj
|
|
@@ -414,7 +413,7 @@ def items_equivalent(list1, list2, comparator):
|
|
|
414
413
|
return True
|
|
415
414
|
|
|
416
415
|
|
|
417
|
-
def is_none_or_empty(obj:
|
|
416
|
+
def is_none_or_empty(obj: str | None | list | None) -> bool:
|
|
418
417
|
return (
|
|
419
418
|
obj is None
|
|
420
419
|
or (isinstance(obj, str) and obj.strip() == "")
|
|
@@ -475,7 +474,7 @@ def convert_to_typed_dict(typed_dict: type[T], obj: dict, strict: bool = False)
|
|
|
475
474
|
return result
|
|
476
475
|
|
|
477
476
|
|
|
478
|
-
def dict_multi_values(elements:
|
|
477
|
+
def dict_multi_values(elements: list | dict) -> dict[str, list[Any]]:
|
|
479
478
|
"""
|
|
480
479
|
Return a dictionary with the original keys from the list of dictionary and the
|
|
481
480
|
values are the list of values of the original dictionary.
|
|
@@ -516,7 +515,7 @@ def split_list_by(
|
|
|
516
515
|
return truthy, falsy
|
|
517
516
|
|
|
518
517
|
|
|
519
|
-
def is_comma_delimited_list(string: str, item_regex:
|
|
518
|
+
def is_comma_delimited_list(string: str, item_regex: str | None = None) -> bool:
|
|
520
519
|
"""
|
|
521
520
|
Checks if the given string is a comma-delimited list of items.
|
|
522
521
|
The optional `item_regex` parameter specifies the regex pattern for each item in the list.
|
|
@@ -2,7 +2,6 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
from functools import lru_cache
|
|
5
|
-
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from localstack import config, constants
|
|
8
7
|
from localstack.utils.container_utils.container_client import ContainerException
|
|
@@ -13,7 +12,7 @@ LOG = logging.getLogger(__name__)
|
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
@lru_cache
|
|
16
|
-
def get_main_container_network() ->
|
|
15
|
+
def get_main_container_network() -> str | None:
|
|
17
16
|
"""
|
|
18
17
|
Gets the main network of the LocalStack container (if we run in one, bridge otherwise)
|
|
19
18
|
If there are multiple networks connected to the LocalStack container, we choose the first as "main" network
|
|
@@ -50,7 +49,7 @@ def get_main_container_network() -> Optional[str]:
|
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
@lru_cache
|
|
53
|
-
def get_endpoint_for_network(network:
|
|
52
|
+
def get_endpoint_for_network(network: str | None = None) -> str:
|
|
54
53
|
"""
|
|
55
54
|
Get the LocalStack endpoint (= IP address) on the given network.
|
|
56
55
|
If a network is given, it will return the IP address/hostname of LocalStack on that network
|