localstack-core 4.10.1.dev42__py3-none-any.whl → 4.12.1.dev18__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/apigateway/__init__.py +42 -0
- localstack/aws/api/cloudformation/__init__.py +161 -0
- localstack/aws/api/ec2/__init__.py +1178 -12
- localstack/aws/api/iam/__init__.py +228 -0
- localstack/aws/api/kms/__init__.py +1 -0
- localstack/aws/api/lambda_/__init__.py +1034 -66
- localstack/aws/api/logs/__init__.py +500 -0
- localstack/aws/api/opensearch/__init__.py +100 -0
- localstack/aws/api/redshift/__init__.py +69 -0
- localstack/aws/api/resourcegroupstaggingapi/__init__.py +36 -0
- localstack/aws/api/route53/__init__.py +45 -0
- localstack/aws/api/route53resolver/__init__.py +1 -0
- localstack/aws/api/s3/__init__.py +64 -0
- localstack/aws/api/s3control/__init__.py +19 -0
- localstack/aws/api/secretsmanager/__init__.py +37 -23
- localstack/aws/api/stepfunctions/__init__.py +52 -10
- localstack/aws/api/sts/__init__.py +52 -0
- localstack/aws/connect.py +35 -15
- localstack/aws/handlers/logging.py +8 -4
- localstack/aws/handlers/service.py +11 -2
- localstack/aws/protocol/serializer.py +1 -1
- localstack/config.py +8 -0
- localstack/constants.py +3 -0
- localstack/deprecations.py +0 -6
- localstack/dev/kubernetes/__main__.py +39 -14
- localstack/runtime/analytics.py +11 -0
- localstack/services/acm/provider.py +17 -1
- localstack/services/apigateway/legacy/provider.py +28 -15
- localstack/services/cloudformation/engine/template_preparer.py +6 -2
- localstack/services/cloudformation/engine/v2/change_set_model.py +9 -0
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +15 -1
- localstack/services/cloudformation/engine/v2/change_set_resource_support_checker.py +114 -0
- localstack/services/cloudformation/provider.py +26 -1
- localstack/services/cloudformation/provider_utils.py +20 -0
- localstack/services/cloudformation/resource_provider.py +5 -4
- localstack/services/cloudformation/scaffolding/__main__.py +94 -22
- localstack/services/cloudformation/v2/provider.py +41 -0
- localstack/services/cloudwatch/provider.py +10 -3
- localstack/services/cloudwatch/provider_v2.py +6 -3
- localstack/services/configservice/provider.py +5 -1
- localstack/services/dynamodb/provider.py +1 -0
- localstack/services/dynamodb/v2/provider.py +1 -0
- localstack/services/dynamodbstreams/provider.py +6 -0
- localstack/services/dynamodbstreams/v2/provider.py +6 -0
- localstack/services/ec2/provider.py +6 -0
- localstack/services/es/provider.py +6 -0
- localstack/services/events/provider.py +4 -0
- localstack/services/events/v1/provider.py +9 -0
- localstack/services/firehose/provider.py +5 -0
- localstack/services/iam/provider.py +4 -0
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kms/models.py +16 -22
- localstack/services/kms/provider.py +4 -0
- localstack/services/lambda_/analytics.py +11 -2
- localstack/services/lambda_/api_utils.py +37 -20
- localstack/services/lambda_/event_source_mapping/pollers/stream_poller.py +1 -1
- localstack/services/lambda_/invocation/assignment.py +4 -1
- localstack/services/lambda_/invocation/event_manager.py +15 -11
- localstack/services/lambda_/invocation/execution_environment.py +21 -2
- localstack/services/lambda_/invocation/lambda_models.py +31 -2
- localstack/services/lambda_/invocation/lambda_service.py +62 -3
- localstack/services/lambda_/invocation/models.py +9 -1
- localstack/services/lambda_/invocation/version_manager.py +18 -3
- localstack/services/lambda_/provider.py +307 -106
- localstack/services/lambda_/resource_providers/aws_lambda_function.py +33 -1
- localstack/services/lambda_/runtimes.py +3 -1
- localstack/services/logs/provider.py +9 -0
- localstack/services/opensearch/packages.py +34 -20
- localstack/services/opensearch/provider.py +53 -3
- localstack/services/resource_groups/provider.py +5 -1
- localstack/services/resourcegroupstaggingapi/provider.py +6 -1
- localstack/services/route53/provider.py +7 -0
- localstack/services/route53resolver/provider.py +5 -0
- localstack/services/s3/constants.py +5 -0
- localstack/services/s3/exceptions.py +9 -0
- localstack/services/s3/models.py +9 -1
- localstack/services/s3/provider.py +51 -43
- localstack/services/s3/utils.py +81 -15
- localstack/services/s3control/provider.py +107 -2
- localstack/services/s3control/validation.py +50 -0
- localstack/services/scheduler/provider.py +4 -2
- localstack/services/secretsmanager/provider.py +4 -0
- localstack/services/ses/provider.py +4 -0
- localstack/services/sns/constants.py +16 -1
- localstack/services/sns/provider.py +5 -0
- localstack/services/sns/publisher.py +15 -6
- localstack/services/sns/v2/models.py +9 -0
- localstack/services/sns/v2/provider.py +750 -19
- localstack/services/sns/v2/utils.py +12 -0
- localstack/services/sqs/constants.py +6 -0
- localstack/services/sqs/provider.py +9 -1
- localstack/services/sqs/resource_providers/aws_sqs_queue.py +61 -46
- localstack/services/ssm/provider.py +6 -0
- localstack/services/stepfunctions/asl/component/common/path/result_path.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/execute_state.py +0 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +0 -1
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/lambda_eval_utils.py +8 -8
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/{mock_eval_utils.py → local_mock_eval_utils.py} +13 -9
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service.py +6 -6
- localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +1 -1
- localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +4 -0
- localstack/services/stepfunctions/asl/component/test_state/state/base_mock.py +118 -0
- localstack/services/stepfunctions/asl/component/test_state/state/common.py +82 -0
- localstack/services/stepfunctions/asl/component/test_state/state/execution.py +139 -0
- localstack/services/stepfunctions/asl/component/test_state/state/map.py +77 -0
- localstack/services/stepfunctions/asl/component/test_state/state/task.py +44 -0
- localstack/services/stepfunctions/asl/eval/environment.py +30 -22
- localstack/services/stepfunctions/asl/eval/states.py +1 -1
- localstack/services/stepfunctions/asl/eval/test_state/environment.py +49 -9
- localstack/services/stepfunctions/asl/eval/test_state/program_state.py +22 -0
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +5 -1
- localstack/services/stepfunctions/asl/parse/preprocessor.py +67 -24
- localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py +5 -4
- localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py +222 -31
- localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py +256 -22
- localstack/services/stepfunctions/backend/execution.py +10 -11
- localstack/services/stepfunctions/backend/execution_worker.py +5 -5
- localstack/services/stepfunctions/backend/test_state/execution.py +36 -0
- localstack/services/stepfunctions/backend/test_state/execution_worker.py +33 -1
- localstack/services/stepfunctions/backend/test_state/test_state_mock.py +127 -0
- localstack/services/stepfunctions/local_mocking/__init__.py +9 -0
- localstack/services/stepfunctions/{mocking → local_mocking}/mock_config.py +24 -17
- localstack/services/stepfunctions/provider.py +83 -25
- localstack/services/stepfunctions/test_state/mock_config.py +47 -0
- localstack/services/sts/provider.py +7 -0
- localstack/services/support/provider.py +5 -1
- localstack/services/swf/provider.py +5 -1
- localstack/services/transcribe/provider.py +7 -0
- localstack/testing/aws/lambda_utils.py +1 -1
- localstack/testing/aws/util.py +2 -1
- localstack/testing/config.py +1 -0
- localstack/testing/pytest/fixtures.py +28 -0
- localstack/testing/snapshots/transformer_utility.py +5 -0
- localstack/utils/analytics/publisher.py +37 -155
- localstack/utils/analytics/service_request_aggregator.py +6 -4
- localstack/utils/aws/arns.py +7 -0
- localstack/utils/aws/client_types.py +2 -4
- localstack/utils/batching.py +258 -0
- localstack/utils/bootstrap.py +2 -2
- localstack/utils/catalog/catalog.py +3 -2
- localstack/utils/collections.py +23 -11
- localstack/utils/container_utils/container_client.py +22 -13
- localstack/utils/container_utils/docker_cmd_client.py +6 -6
- localstack/version.py +2 -2
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/METADATA +7 -7
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/RECORD +155 -146
- localstack_core-4.12.1.dev18.dist-info/plux.json +1 -0
- localstack/services/stepfunctions/mocking/__init__.py +0 -0
- localstack/utils/batch_policy.py +0 -124
- localstack_core-4.10.1.dev42.dist-info/plux.json +0 -1
- /localstack/services/stepfunctions/{mocking → local_mocking}/mock_config_file.py +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.10.1.dev42.data → localstack_core-4.12.1.dev18.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/WHEEL +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.10.1.dev42.dist-info → localstack_core-4.12.1.dev18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from localstack.services.cloudformation.engine.v2.change_set_model import (
|
|
2
|
+
NodeResource,
|
|
3
|
+
)
|
|
4
|
+
from localstack.services.cloudformation.engine.v2.change_set_model_visitor import (
|
|
5
|
+
ChangeSetModelVisitor,
|
|
6
|
+
)
|
|
7
|
+
from localstack.services.cloudformation.resources import AWS_AVAILABLE_CFN_RESOURCES
|
|
8
|
+
from localstack.utils.catalog.catalog import (
|
|
9
|
+
AwsServicesSupportStatus,
|
|
10
|
+
CatalogPlugin,
|
|
11
|
+
CfnResourceSupportStatus,
|
|
12
|
+
)
|
|
13
|
+
from localstack.utils.catalog.common import (
|
|
14
|
+
AwsServicesSupportInLatest,
|
|
15
|
+
AwsServiceSupportAtRuntime,
|
|
16
|
+
CloudFormationResourcesSupportAtRuntime,
|
|
17
|
+
CloudFormationResourcesSupportInLatest,
|
|
18
|
+
)
|
|
19
|
+
from localstack.utils.catalog.plugins import get_aws_catalog
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# TODO handle all available resource types
|
|
23
|
+
def _get_service_name(resource_type: str) -> str | None:
|
|
24
|
+
parts = resource_type.split("::")
|
|
25
|
+
if len(parts) == 1:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
match parts:
|
|
29
|
+
case _ if "Cognito::IdentityPool" in resource_type:
|
|
30
|
+
return "cognito-identity"
|
|
31
|
+
case [*_, "Cognito", "UserPool"]:
|
|
32
|
+
return "cognito-idp"
|
|
33
|
+
case [*_, "Cognito", _]:
|
|
34
|
+
return "cognito-idp"
|
|
35
|
+
case [*_, "Elasticsearch", _]:
|
|
36
|
+
return "es"
|
|
37
|
+
case [*_, "OpenSearchService", _]:
|
|
38
|
+
return "opensearch"
|
|
39
|
+
case [*_, "KinesisFirehose", _]:
|
|
40
|
+
return "firehose"
|
|
41
|
+
case [*_, "ResourceGroups", _]:
|
|
42
|
+
return "resource-groups"
|
|
43
|
+
case [*_, "CertificateManager", _]:
|
|
44
|
+
return "acm"
|
|
45
|
+
case _ if "ElasticLoadBalancing::" in resource_type:
|
|
46
|
+
return "elb"
|
|
47
|
+
case _ if "ElasticLoadBalancingV2::" in resource_type:
|
|
48
|
+
return "elbv2"
|
|
49
|
+
case _ if "ApplicationAutoScaling::" in resource_type:
|
|
50
|
+
return "application-autoscaling"
|
|
51
|
+
case _ if "MSK::" in resource_type:
|
|
52
|
+
return "kafka"
|
|
53
|
+
case _ if "Timestream::" in resource_type:
|
|
54
|
+
return "timestream-write"
|
|
55
|
+
case [_, service, *_]:
|
|
56
|
+
return service.lower()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_resource_failure_message(
|
|
60
|
+
resource_type: str, status: AwsServicesSupportStatus | CfnResourceSupportStatus
|
|
61
|
+
) -> str:
|
|
62
|
+
service_name = _get_service_name(resource_type) or "malformed"
|
|
63
|
+
template = "Sorry, the {resource} resource in the {service} service is not supported."
|
|
64
|
+
match status:
|
|
65
|
+
case CloudFormationResourcesSupportAtRuntime.NOT_IMPLEMENTED:
|
|
66
|
+
template = "Sorry, the {resource} resource (from the {service} service) is not supported by this version of LocalStack, but is available in the latest version."
|
|
67
|
+
case CloudFormationResourcesSupportInLatest.NOT_SUPPORTED:
|
|
68
|
+
template = "Sorry, the {resource} resource (from the {service} service) is not currently supported by LocalStack."
|
|
69
|
+
case AwsServiceSupportAtRuntime.AVAILABLE_WITH_LICENSE_UPGRADE:
|
|
70
|
+
template = "Sorry, the {service} service (for the {resource} resource) is not included within your LocalStack license, but is available in an upgraded license."
|
|
71
|
+
case AwsServiceSupportAtRuntime.NOT_IMPLEMENTED:
|
|
72
|
+
template = "The API for service {service} (for the {resource} resource) is either not included in your current license plan or has not yet been emulated by LocalStack."
|
|
73
|
+
case AwsServicesSupportInLatest.NOT_SUPPORTED:
|
|
74
|
+
template = "Sorry, the {service} (for the {resource} resource) service is not currently supported by LocalStack."
|
|
75
|
+
case AwsServicesSupportInLatest.SUPPORTED_WITH_LICENSE_UPGRADE:
|
|
76
|
+
template = "Sorry, the {service} service (for the {resource} resource) is not supported by this version of LocalStack, but is available in the latest version if you upgrade to the latest stable version."
|
|
77
|
+
return template.format(
|
|
78
|
+
resource=resource_type,
|
|
79
|
+
service=service_name,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ChangeSetResourceSupportChecker(ChangeSetModelVisitor):
|
|
84
|
+
catalog: CatalogPlugin
|
|
85
|
+
|
|
86
|
+
TITLE_MESSAGE = "Unsupported resources detected:"
|
|
87
|
+
|
|
88
|
+
def __init__(self):
|
|
89
|
+
self._resource_failure_messages: dict[str, str] = {}
|
|
90
|
+
self.catalog = get_aws_catalog()
|
|
91
|
+
|
|
92
|
+
def visit_node_resource(self, node_resource: NodeResource):
|
|
93
|
+
resource_type = node_resource.type_.value
|
|
94
|
+
if resource_type not in self._resource_failure_messages:
|
|
95
|
+
if resource_type not in AWS_AVAILABLE_CFN_RESOURCES:
|
|
96
|
+
# Ignore non-AWS resources
|
|
97
|
+
pass
|
|
98
|
+
support_status = self._resource_support_status(resource_type)
|
|
99
|
+
if support_status == CloudFormationResourcesSupportAtRuntime.AVAILABLE:
|
|
100
|
+
pass
|
|
101
|
+
else:
|
|
102
|
+
failure_message = _build_resource_failure_message(resource_type, support_status)
|
|
103
|
+
self._resource_failure_messages[resource_type] = failure_message
|
|
104
|
+
super().visit_node_resource(node_resource)
|
|
105
|
+
|
|
106
|
+
def _resource_support_status(
|
|
107
|
+
self, resource_type: str
|
|
108
|
+
) -> AwsServicesSupportStatus | CfnResourceSupportStatus:
|
|
109
|
+
service_name = _get_service_name(resource_type)
|
|
110
|
+
return self.catalog.get_cloudformation_resource_status(resource_type, service_name, True)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def failure_messages(self) -> list[str]:
|
|
114
|
+
return list(self._resource_failure_messages.values())
|
|
@@ -5,6 +5,7 @@ import re
|
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from copy import deepcopy
|
|
7
7
|
|
|
8
|
+
from localstack import config
|
|
8
9
|
from localstack.aws.api import CommonServiceException, RequestContext, handler
|
|
9
10
|
from localstack.aws.api.cloudformation import (
|
|
10
11
|
AlreadyExistsException,
|
|
@@ -120,6 +121,7 @@ from localstack.services.cloudformation.stores import (
|
|
|
120
121
|
find_stack_by_id,
|
|
121
122
|
get_cloudformation_store,
|
|
122
123
|
)
|
|
124
|
+
from localstack.services.plugins import ServiceLifecycleHook
|
|
123
125
|
from localstack.state import StateVisitor
|
|
124
126
|
from localstack.utils.collections import (
|
|
125
127
|
remove_attributes,
|
|
@@ -177,7 +179,30 @@ class InternalFailure(CommonServiceException):
|
|
|
177
179
|
super().__init__("InternalFailure", status_code=500, message=message, sender_fault=False)
|
|
178
180
|
|
|
179
181
|
|
|
180
|
-
class CloudformationProvider(CloudformationApi):
|
|
182
|
+
class CloudformationProvider(CloudformationApi, ServiceLifecycleHook):
|
|
183
|
+
def on_before_start(self):
|
|
184
|
+
self._validate_config()
|
|
185
|
+
|
|
186
|
+
def _validate_config(self):
|
|
187
|
+
no_wait_value: int = 5
|
|
188
|
+
try:
|
|
189
|
+
no_wait_value = int(config.CFN_NO_WAIT_ITERATIONS or 5)
|
|
190
|
+
except (TypeError, ValueError):
|
|
191
|
+
LOG.warning(
|
|
192
|
+
"You have set CFN_NO_WAIT_ITERATIONS to an invalid value: '%s'. It must be an integer greater or equal to 0. Using the default of 5",
|
|
193
|
+
config.CFN_NO_WAIT_ITERATIONS,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if no_wait_value < 0:
|
|
197
|
+
LOG.warning(
|
|
198
|
+
"You have set CFN_NO_WAIT_ITERATIONS to an invalid value: '%s'. It must be an integer greater or equal to 0. Using the default of 5",
|
|
199
|
+
config.CFN_NO_WAIT_ITERATIONS,
|
|
200
|
+
)
|
|
201
|
+
no_wait_value = 5
|
|
202
|
+
|
|
203
|
+
# Set the configuration back
|
|
204
|
+
config.CFN_NO_WAIT_ITERATIONS = no_wait_value
|
|
205
|
+
|
|
181
206
|
def _stack_status_is_active(self, stack_status: str) -> bool:
|
|
182
207
|
return stack_status not in [StackStatus.DELETE_COMPLETE]
|
|
183
208
|
|
|
@@ -275,6 +275,26 @@ def convert_values_to_numbers(input_dict: dict, keys_to_skip: list[str] | None =
|
|
|
275
275
|
return recursive_convert(input_dict)
|
|
276
276
|
|
|
277
277
|
|
|
278
|
+
def resource_tags_to_remove_or_update(
|
|
279
|
+
prev_tags: list[dict], new_tags: list[dict]
|
|
280
|
+
) -> tuple[list[str], dict[str, str]]:
|
|
281
|
+
"""
|
|
282
|
+
When updating resources that have tags, we need to determine which tags to remove and which to add/update,
|
|
283
|
+
as these are typically done in separate API calls. The format of prev_tags and new_tags is expected to
|
|
284
|
+
be [{ "Key": tagName, "Value": tagValue }, ...]. The return value will be a tuple of (tags_to_remove, tags_to_update),
|
|
285
|
+
where:
|
|
286
|
+
- tags_to_remove is a list of tag keys that are present in prev_tags but not in new_tags.
|
|
287
|
+
- tags_to_update is a dict of tags to add or update, with the format: { tagName: tagValue, ... }.
|
|
288
|
+
"""
|
|
289
|
+
prev_tag_keys = [tag["Key"] for tag in prev_tags]
|
|
290
|
+
new_tag_keys = [tag["Key"] for tag in new_tags]
|
|
291
|
+
tags_to_remove = list(set(prev_tag_keys) - set(new_tag_keys))
|
|
292
|
+
|
|
293
|
+
# convert from list of dicts, to a single dict because that's what tag_queue APIs expect.
|
|
294
|
+
tags_to_update = {tag["Key"]: tag["Value"] for tag in new_tags}
|
|
295
|
+
return (tags_to_remove, tags_to_update)
|
|
296
|
+
|
|
297
|
+
|
|
278
298
|
# LocalStack specific utilities
|
|
279
299
|
def get_schema_path(file_path: Path) -> dict:
|
|
280
300
|
file_name_base = file_path.name.removesuffix(".py").removesuffix(".py.enc")
|
|
@@ -436,11 +436,11 @@ class ResourceProviderExecutor:
|
|
|
436
436
|
resource: dict,
|
|
437
437
|
raw_payload: ResourceProviderPayload,
|
|
438
438
|
max_timeout: int = config.CFN_PER_RESOURCE_TIMEOUT,
|
|
439
|
-
sleep_time: float =
|
|
439
|
+
sleep_time: float = 1,
|
|
440
440
|
) -> ProgressEvent[Properties]:
|
|
441
441
|
payload = copy.deepcopy(raw_payload)
|
|
442
442
|
|
|
443
|
-
max_iterations = max(ceil(max_timeout / sleep_time),
|
|
443
|
+
max_iterations = max(ceil(max_timeout / sleep_time), 10)
|
|
444
444
|
|
|
445
445
|
for current_iteration in range(max_iterations):
|
|
446
446
|
resource_type = get_resource_type({"Type": raw_payload["resourceType"]})
|
|
@@ -486,10 +486,11 @@ class ResourceProviderExecutor:
|
|
|
486
486
|
payload["requestData"]["resourceProperties"] = event.resource_model
|
|
487
487
|
resource["Properties"] = event.resource_model
|
|
488
488
|
|
|
489
|
-
if current_iteration
|
|
490
|
-
|
|
489
|
+
if current_iteration < config.CFN_NO_WAIT_ITERATIONS:
|
|
490
|
+
pass
|
|
491
491
|
else:
|
|
492
492
|
time.sleep(sleep_time)
|
|
493
|
+
|
|
493
494
|
case OperationStatus.PENDING:
|
|
494
495
|
# come back to this resource in another iteration
|
|
495
496
|
return event
|
|
@@ -10,6 +10,7 @@ from functools import reduce
|
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
from typing import Any, Literal, TypedDict, TypeVar
|
|
12
12
|
|
|
13
|
+
import boto3
|
|
13
14
|
import click
|
|
14
15
|
from jinja2 import Environment, FileSystemLoader
|
|
15
16
|
from yaml import safe_dump
|
|
@@ -140,14 +141,76 @@ class SchemaProvider:
|
|
|
140
141
|
) from e
|
|
141
142
|
|
|
142
143
|
|
|
144
|
+
class LiveSchemaProvider:
|
|
145
|
+
"""
|
|
146
|
+
Provides CloudFormation resource schemas by fetching them from the live AWS CloudFormation service, rather than
|
|
147
|
+
a local zip file.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, cfn_client):
|
|
151
|
+
self.cfn_client = cfn_client
|
|
152
|
+
|
|
153
|
+
def available_schemas(self, pattern: str) -> list[str]:
|
|
154
|
+
"""
|
|
155
|
+
Return the names of available CloudFormation resource types. `pattern` should be something like
|
|
156
|
+
AWS::S3::Bucket or AWS::S3::*, depending on whether you want all resources for a service or a specific one.
|
|
157
|
+
The result is a list of matching resource type names (e.g. [AWS::S3::Bucket, AWS::S3::Object, ...])
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
is_wildcard = pattern.endswith("*")
|
|
161
|
+
pattern = pattern[:-1] if is_wildcard else pattern
|
|
162
|
+
matching_names = []
|
|
163
|
+
|
|
164
|
+
params = {
|
|
165
|
+
"Visibility": "PUBLIC",
|
|
166
|
+
"Type": "RESOURCE",
|
|
167
|
+
"DeprecatedStatus": "LIVE",
|
|
168
|
+
"Filters": {"Category": "AWS_TYPES", "TypeNamePrefix": pattern},
|
|
169
|
+
}
|
|
170
|
+
next_token: str | None = None
|
|
171
|
+
|
|
172
|
+
# Note: pagination is necessary since list_types requires multiple calls even to get a single result.
|
|
173
|
+
while True:
|
|
174
|
+
if next_token:
|
|
175
|
+
params["NextToken"] = next_token
|
|
176
|
+
response = self.cfn_client.list_types(**params)
|
|
177
|
+
|
|
178
|
+
# collect any matching type names (if wildcard, all; else exact match only)
|
|
179
|
+
matching_names.extend(
|
|
180
|
+
[
|
|
181
|
+
type_summary["TypeName"]
|
|
182
|
+
for type_summary in response.get("TypeSummaries", [])
|
|
183
|
+
if (is_wildcard or type_summary["TypeName"] == pattern)
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
next_token = response.get("NextToken")
|
|
188
|
+
if not next_token:
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
return matching_names
|
|
192
|
+
|
|
193
|
+
def schema(self, type_name: ResourceName) -> ResourceSchema:
|
|
194
|
+
"""
|
|
195
|
+
Given a CloudFormation ResourceName (representing something like "AWS::S3::Bucket"), return the resource
|
|
196
|
+
schema as dict.
|
|
197
|
+
"""
|
|
198
|
+
response = self.cfn_client.describe_type(
|
|
199
|
+
Type="RESOURCE",
|
|
200
|
+
TypeName=type_name.full_name,
|
|
201
|
+
)
|
|
202
|
+
schema_str = response.get("Schema")
|
|
203
|
+
if not schema_str:
|
|
204
|
+
raise click.ClickException(
|
|
205
|
+
f"Could not fetch schema for CloudFormation resource type: {type_name}"
|
|
206
|
+
)
|
|
207
|
+
return json.loads(schema_str)
|
|
208
|
+
|
|
209
|
+
|
|
143
210
|
LOCALSTACK_ROOT_DIR = Path(__file__).parent.joinpath("../../../../..").resolve()
|
|
144
211
|
LOCALSTACK_PRO_ROOT_DIR = LOCALSTACK_ROOT_DIR.joinpath("../localstack-pro").resolve()
|
|
145
|
-
TESTS_ROOT_DIR = LOCALSTACK_ROOT_DIR.joinpath(
|
|
146
|
-
|
|
147
|
-
)
|
|
148
|
-
TESTS_PRO_ROOT_DIR = LOCALSTACK_PRO_ROOT_DIR.joinpath(
|
|
149
|
-
"localstack-pro-core/tests/aws/services/cloudformation/resource_providers"
|
|
150
|
-
)
|
|
212
|
+
TESTS_ROOT_DIR = LOCALSTACK_ROOT_DIR.joinpath("tests/aws/services")
|
|
213
|
+
TESTS_PRO_ROOT_DIR = LOCALSTACK_PRO_ROOT_DIR.joinpath("localstack-pro-core/tests/aws/services")
|
|
151
214
|
|
|
152
215
|
assert LOCALSTACK_ROOT_DIR.is_dir(), f"{LOCALSTACK_ROOT_DIR} does not exist"
|
|
153
216
|
assert LOCALSTACK_PRO_ROOT_DIR.is_dir(), f"{LOCALSTACK_PRO_ROOT_DIR} does not exist"
|
|
@@ -193,7 +256,7 @@ def template_path(
|
|
|
193
256
|
output_path = (
|
|
194
257
|
tests_root_dir(pro)
|
|
195
258
|
.joinpath(
|
|
196
|
-
f"{resource_name.python_compatible_service_name.lower()}/
|
|
259
|
+
f"{resource_name.python_compatible_service_name.lower()}/resource_providers/templates/{stub}"
|
|
197
260
|
)
|
|
198
261
|
.resolve()
|
|
199
262
|
)
|
|
@@ -202,7 +265,7 @@ def template_path(
|
|
|
202
265
|
test_path = (
|
|
203
266
|
root_dir(pro)
|
|
204
267
|
.joinpath(
|
|
205
|
-
f"tests/aws/
|
|
268
|
+
f"tests/aws/{resource_name.python_compatible_service_name.lower()}/resource_providers/templates"
|
|
206
269
|
)
|
|
207
270
|
.resolve()
|
|
208
271
|
)
|
|
@@ -276,7 +339,7 @@ class TemplateRenderer:
|
|
|
276
339
|
# e.g. .../resource_providers/aws_iam_role/test_X.py vs. .../resource_providers/iam/test_X.py
|
|
277
340
|
# add extra parameters
|
|
278
341
|
tests_output_path = root_dir(self.pro).joinpath(
|
|
279
|
-
f"tests/aws/
|
|
342
|
+
f"tests/aws/{resource_name.python_compatible_service_name.lower()}/resource_providers/templates"
|
|
280
343
|
)
|
|
281
344
|
match file_type:
|
|
282
345
|
case FileType.getatt_test:
|
|
@@ -284,7 +347,9 @@ class TemplateRenderer:
|
|
|
284
347
|
kwargs["service"] = resource_name.service.lower()
|
|
285
348
|
kwargs["resource"] = resource_name.resource.lower()
|
|
286
349
|
kwargs["template_path"] = str(
|
|
287
|
-
template_path(
|
|
350
|
+
template_path(
|
|
351
|
+
resource_name, FileType.attribute_template, tests_output_path, pro=self.pro
|
|
352
|
+
)
|
|
288
353
|
)
|
|
289
354
|
case FileType.provider:
|
|
290
355
|
property_ir = generate_ir_for_type(
|
|
@@ -318,17 +383,25 @@ class TemplateRenderer:
|
|
|
318
383
|
kwargs["pro"] = self.pro
|
|
319
384
|
case FileType.integration_test:
|
|
320
385
|
kwargs["black_box_template_path"] = str(
|
|
321
|
-
template_path(
|
|
386
|
+
template_path(
|
|
387
|
+
resource_name, FileType.minimal_template, tests_output_path, pro=self.pro
|
|
388
|
+
)
|
|
322
389
|
)
|
|
323
390
|
kwargs["update_template_path"] = str(
|
|
324
391
|
template_path(
|
|
325
392
|
resource_name,
|
|
326
393
|
FileType.update_without_replacement_template,
|
|
327
394
|
tests_output_path,
|
|
395
|
+
pro=self.pro,
|
|
328
396
|
)
|
|
329
397
|
)
|
|
330
398
|
kwargs["autogenerated_template_path"] = str(
|
|
331
|
-
template_path(
|
|
399
|
+
template_path(
|
|
400
|
+
resource_name,
|
|
401
|
+
FileType.autogenerated_template,
|
|
402
|
+
tests_output_path,
|
|
403
|
+
pro=self.pro,
|
|
404
|
+
)
|
|
332
405
|
)
|
|
333
406
|
# case FileType.cloudcontrol_test:
|
|
334
407
|
case FileType.parity_test:
|
|
@@ -531,20 +604,24 @@ class FileWriter:
|
|
|
531
604
|
),
|
|
532
605
|
FileType.integration_test: tests_root_dir(self.pro).joinpath(
|
|
533
606
|
self.resource_name.python_compatible_service_name.lower(),
|
|
607
|
+
"resource_providers",
|
|
534
608
|
self.resource_name.path_compatible_full_name(),
|
|
535
609
|
"test_basic.py",
|
|
536
610
|
),
|
|
537
611
|
FileType.getatt_test: tests_root_dir(self.pro).joinpath(
|
|
538
612
|
self.resource_name.python_compatible_service_name.lower(),
|
|
613
|
+
"resource_providers",
|
|
539
614
|
self.resource_name.path_compatible_full_name(),
|
|
540
615
|
"test_exploration.py",
|
|
541
616
|
),
|
|
542
617
|
# FileType.cloudcontrol_test: tests_root_dir(self.pro).joinpath(
|
|
543
618
|
# self.resource_name.python_compatible_service_name.lower(),
|
|
619
|
+
# "resource_providers",
|
|
544
620
|
# f"test_aws_{self.resource_name.service.lower()}_{self.resource_name.resource.lower()}_cloudcontrol.py",
|
|
545
621
|
# ),
|
|
546
622
|
FileType.parity_test: tests_root_dir(self.pro).joinpath(
|
|
547
623
|
self.resource_name.python_compatible_service_name.lower(),
|
|
624
|
+
"resource_providers",
|
|
548
625
|
self.resource_name.path_compatible_full_name(),
|
|
549
626
|
"test_parity.py",
|
|
550
627
|
),
|
|
@@ -558,7 +635,9 @@ class FileWriter:
|
|
|
558
635
|
FileType.autogenerated_template,
|
|
559
636
|
]
|
|
560
637
|
for template_type in templates:
|
|
561
|
-
self.destination_files[template_type] = template_path(
|
|
638
|
+
self.destination_files[template_type] = template_path(
|
|
639
|
+
self.resource_name, template_type, pro=self.pro
|
|
640
|
+
)
|
|
562
641
|
|
|
563
642
|
def write(self, file_type: FileType, contents: str):
|
|
564
643
|
file_destination = self.destination_files[file_type]
|
|
@@ -763,21 +842,14 @@ def generate(
|
|
|
763
842
|
console = Console()
|
|
764
843
|
console.rule(title=resource_type)
|
|
765
844
|
|
|
766
|
-
schema_provider =
|
|
767
|
-
zipfile_path=Path(__file__).parent.joinpath("CloudformationSchema.zip")
|
|
768
|
-
)
|
|
845
|
+
schema_provider = LiveSchemaProvider(boto3.client("cloudformation"))
|
|
769
846
|
|
|
770
847
|
template_root = Path(__file__).parent.joinpath("templates")
|
|
771
848
|
env = Environment(
|
|
772
849
|
loader=FileSystemLoader(template_root),
|
|
773
850
|
)
|
|
774
851
|
|
|
775
|
-
|
|
776
|
-
if parts[-1] == "*":
|
|
777
|
-
# generate all resource types for that service
|
|
778
|
-
matching_resources = [x for x in schema_provider.schemas.keys() if x.startswith(parts[0])]
|
|
779
|
-
else:
|
|
780
|
-
matching_resources = [resource_type]
|
|
852
|
+
matching_resources = schema_provider.available_schemas(resource_type)
|
|
781
853
|
|
|
782
854
|
for matching_resource in matching_resources:
|
|
783
855
|
console.rule(title=matching_resource)
|
|
@@ -103,6 +103,9 @@ from localstack.services.cloudformation.engine.v2.change_set_model_transform imp
|
|
|
103
103
|
from localstack.services.cloudformation.engine.v2.change_set_model_validator import (
|
|
104
104
|
ChangeSetModelValidator,
|
|
105
105
|
)
|
|
106
|
+
from localstack.services.cloudformation.engine.v2.change_set_resource_support_checker import (
|
|
107
|
+
ChangeSetResourceSupportChecker,
|
|
108
|
+
)
|
|
106
109
|
from localstack.services.cloudformation.engine.validations import ValidationError
|
|
107
110
|
from localstack.services.cloudformation.provider import (
|
|
108
111
|
ARN_CHANGESET_REGEX,
|
|
@@ -222,6 +225,12 @@ def find_stack_instance(stack_set: StackSet, account: str, region: str) -> Stack
|
|
|
222
225
|
|
|
223
226
|
class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
224
227
|
def on_before_start(self):
|
|
228
|
+
# TODO: make sure to bring `_validate_config` from the base class when removing it
|
|
229
|
+
# as this ensures we have a valid CFN_NO_WAIT_ITERATIONS value
|
|
230
|
+
super().on_before_start()
|
|
231
|
+
self._log_create_issue_info()
|
|
232
|
+
|
|
233
|
+
def _log_create_issue_info(self):
|
|
225
234
|
base = "https://github.com/localstack/localstack/issues/new"
|
|
226
235
|
query_args = {
|
|
227
236
|
"template": "bug-report.yml",
|
|
@@ -417,6 +426,38 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
417
426
|
update_model.node_template.change_type = ChangeType.MODIFIED
|
|
418
427
|
change_set.processed_template = transformed_after_template
|
|
419
428
|
|
|
429
|
+
if not config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
|
|
430
|
+
support_visitor = ChangeSetResourceSupportChecker()
|
|
431
|
+
support_visitor.visit(change_set.update_model.node_template)
|
|
432
|
+
failure_messages = support_visitor.failure_messages
|
|
433
|
+
if failure_messages:
|
|
434
|
+
reason_suffix = ", ".join(failure_messages)
|
|
435
|
+
status_reason = f"{ChangeSetResourceSupportChecker.TITLE_MESSAGE} {reason_suffix}"
|
|
436
|
+
|
|
437
|
+
change_set.status_reason = status_reason
|
|
438
|
+
change_set.set_change_set_status(ChangeSetStatus.FAILED)
|
|
439
|
+
failure_transitions = {
|
|
440
|
+
ChangeSetType.CREATE: (
|
|
441
|
+
StackStatus.ROLLBACK_IN_PROGRESS,
|
|
442
|
+
StackStatus.CREATE_FAILED,
|
|
443
|
+
),
|
|
444
|
+
ChangeSetType.UPDATE: (
|
|
445
|
+
StackStatus.UPDATE_ROLLBACK_IN_PROGRESS,
|
|
446
|
+
StackStatus.UPDATE_ROLLBACK_FAILED,
|
|
447
|
+
),
|
|
448
|
+
ChangeSetType.IMPORT: (
|
|
449
|
+
StackStatus.IMPORT_ROLLBACK_IN_PROGRESS,
|
|
450
|
+
StackStatus.IMPORT_ROLLBACK_FAILED,
|
|
451
|
+
),
|
|
452
|
+
}
|
|
453
|
+
transitions = failure_transitions.get(change_set.change_set_type)
|
|
454
|
+
if transitions:
|
|
455
|
+
first_status, *remaining_statuses = transitions
|
|
456
|
+
change_set.stack.set_stack_status(first_status, status_reason)
|
|
457
|
+
for status in remaining_statuses:
|
|
458
|
+
change_set.stack.set_stack_status(status)
|
|
459
|
+
return
|
|
460
|
+
|
|
420
461
|
@handler("CreateChangeSet", expand=False)
|
|
421
462
|
def create_change_set(
|
|
422
463
|
self, context: RequestContext, request: CreateChangeSetInput
|
|
@@ -35,6 +35,7 @@ from localstack.services import moto
|
|
|
35
35
|
from localstack.services.cloudwatch.alarm_scheduler import AlarmScheduler
|
|
36
36
|
from localstack.services.edge import ROUTER
|
|
37
37
|
from localstack.services.plugins import SERVICE_PLUGINS, ServiceLifecycleHook
|
|
38
|
+
from localstack.state import StateVisitor
|
|
38
39
|
from localstack.utils.aws import arns
|
|
39
40
|
from localstack.utils.aws.arns import extract_account_id_from_arn, lambda_function_name
|
|
40
41
|
from localstack.utils.aws.request_context import (
|
|
@@ -306,8 +307,13 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
306
307
|
self.tags = TaggingService()
|
|
307
308
|
self.alarm_scheduler = None
|
|
308
309
|
|
|
310
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
311
|
+
visitor.visit(cloudwatch_backends)
|
|
312
|
+
|
|
309
313
|
def on_after_init(self):
|
|
310
314
|
ROUTER.add(PATH_GET_RAW_METRICS, self.get_raw_metrics)
|
|
315
|
+
|
|
316
|
+
def on_before_start(self):
|
|
311
317
|
self.start_alarm_scheduler()
|
|
312
318
|
|
|
313
319
|
def on_before_state_reset(self):
|
|
@@ -337,9 +343,10 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
337
343
|
self.alarm_scheduler = AlarmScheduler()
|
|
338
344
|
|
|
339
345
|
def shutdown_alarm_scheduler(self):
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
346
|
+
if self.alarm_scheduler:
|
|
347
|
+
LOG.debug("stopping cloudwatch scheduler")
|
|
348
|
+
self.alarm_scheduler.shutdown_scheduler()
|
|
349
|
+
self.alarm_scheduler = None
|
|
343
350
|
|
|
344
351
|
def delete_alarms(self, context: RequestContext, alarm_names: AlarmNames, **kwargs) -> None:
|
|
345
352
|
moto.call_moto(context)
|
|
@@ -161,6 +161,8 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
161
161
|
|
|
162
162
|
def on_after_init(self):
|
|
163
163
|
ROUTER.add(PATH_GET_RAW_METRICS, self.get_raw_metrics)
|
|
164
|
+
|
|
165
|
+
def on_before_start(self):
|
|
164
166
|
self.start_alarm_scheduler()
|
|
165
167
|
|
|
166
168
|
def on_before_state_reset(self):
|
|
@@ -192,9 +194,10 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
192
194
|
self.alarm_scheduler = AlarmScheduler()
|
|
193
195
|
|
|
194
196
|
def shutdown_alarm_scheduler(self):
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
197
|
+
if self.alarm_scheduler:
|
|
198
|
+
LOG.debug("stopping cloudwatch scheduler")
|
|
199
|
+
self.alarm_scheduler.shutdown_scheduler()
|
|
200
|
+
self.alarm_scheduler = None
|
|
198
201
|
|
|
199
202
|
def delete_alarms(self, context: RequestContext, alarm_names: AlarmNames, **kwargs) -> None:
|
|
200
203
|
"""
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
from localstack.aws.api.config import ConfigApi
|
|
2
|
+
from localstack.state import StateVisitor
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class ConfigProvider(ConfigApi):
|
|
5
|
-
|
|
6
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
7
|
+
from moto.config.models import config_backends
|
|
8
|
+
|
|
9
|
+
visitor.visit(config_backends)
|
|
@@ -535,6 +535,7 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
535
535
|
self.server = self._new_dynamodb_server()
|
|
536
536
|
self._expired_items_worker = ExpiredItemsWorker()
|
|
537
537
|
self._router_rules = []
|
|
538
|
+
# TODO: fix _event_forwarder to have lazy instantiation of the ThreadPoolExecutor
|
|
538
539
|
self._event_forwarder = EventForwarder()
|
|
539
540
|
|
|
540
541
|
def on_before_start(self):
|
|
@@ -392,6 +392,7 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
392
392
|
|
|
393
393
|
def accept_state_visitor(self, visitor: StateVisitor):
|
|
394
394
|
visitor.visit(dynamodb_stores)
|
|
395
|
+
# FIXME: DDB v2 does not depend on dynamodbstreams_stores as DynamoDB Local holds all the state
|
|
395
396
|
visitor.visit(dynamodbstreams_stores)
|
|
396
397
|
visitor.visit(AssetDirectory(self.service, os.path.join(config.dirs.data, self.service)))
|
|
397
398
|
|
|
@@ -37,6 +37,7 @@ from localstack.services.dynamodbstreams.dynamodbstreams_api import (
|
|
|
37
37
|
table_name_from_stream_arn,
|
|
38
38
|
)
|
|
39
39
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
40
|
+
from localstack.state import StateVisitor
|
|
40
41
|
from localstack.utils.collections import select_from_typed_dict
|
|
41
42
|
|
|
42
43
|
LOG = logging.getLogger(__name__)
|
|
@@ -57,6 +58,11 @@ class DynamoDBStreamsProvider(DynamodbstreamsApi, ServiceLifecycleHook):
|
|
|
57
58
|
def __init__(self):
|
|
58
59
|
self.shard_to_region = {}
|
|
59
60
|
|
|
61
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
62
|
+
from localstack.services.dynamodbstreams.models import dynamodbstreams_stores
|
|
63
|
+
|
|
64
|
+
visitor.visit(dynamodbstreams_stores)
|
|
65
|
+
|
|
60
66
|
def describe_stream(
|
|
61
67
|
self,
|
|
62
68
|
context: RequestContext,
|
|
@@ -18,6 +18,7 @@ from localstack.services.dynamodb.utils import modify_ddblocal_arns
|
|
|
18
18
|
from localstack.services.dynamodb.v2.provider import DynamoDBProvider, modify_context_region
|
|
19
19
|
from localstack.services.dynamodbstreams.dynamodbstreams_api import get_original_region
|
|
20
20
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
21
|
+
from localstack.state import StateVisitor
|
|
21
22
|
from localstack.utils.aws.arns import parse_arn
|
|
22
23
|
|
|
23
24
|
LOG = logging.getLogger(__name__)
|
|
@@ -32,6 +33,11 @@ class DynamoDBStreamsProvider(DynamodbstreamsApi, ServiceLifecycleHook):
|
|
|
32
33
|
self.server = DynamodbServer.get()
|
|
33
34
|
self.shard_to_region = {}
|
|
34
35
|
|
|
36
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
37
|
+
# DynamoDB Streams state is entirely dependent on DynamoDB Local state, and does not hold state itself
|
|
38
|
+
# the DynamoDB provider is responsible for the persistence of DDB Streams
|
|
39
|
+
pass
|
|
40
|
+
|
|
35
41
|
def on_after_init(self):
|
|
36
42
|
# add response processor specific to ddblocal
|
|
37
43
|
handlers.modify_service_response.append(self.service, modify_ddblocal_arns)
|
|
@@ -94,6 +94,7 @@ from localstack.services.ec2.models import get_ec2_backend
|
|
|
94
94
|
from localstack.services.ec2.patches import apply_patches
|
|
95
95
|
from localstack.services.moto import call_moto, call_moto_with_request
|
|
96
96
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
97
|
+
from localstack.state import StateVisitor
|
|
97
98
|
from localstack.utils.patch import patch
|
|
98
99
|
from localstack.utils.strings import first_char_to_upper, long_uid, short_uid
|
|
99
100
|
|
|
@@ -107,6 +108,11 @@ class Ec2Provider(Ec2Api, ABC, ServiceLifecycleHook):
|
|
|
107
108
|
def on_after_init(self):
|
|
108
109
|
apply_patches()
|
|
109
110
|
|
|
111
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
112
|
+
from moto.ec2.models import ec2_backends
|
|
113
|
+
|
|
114
|
+
visitor.visit(ec2_backends)
|
|
115
|
+
|
|
110
116
|
@handler("DescribeAvailabilityZones", expand=False)
|
|
111
117
|
def describe_availability_zones(
|
|
112
118
|
self,
|
|
@@ -68,6 +68,7 @@ from localstack.aws.api.opensearch import (
|
|
|
68
68
|
)
|
|
69
69
|
from localstack.aws.connect import connect_to
|
|
70
70
|
from localstack.services.opensearch.packages import ELASTICSEARCH_DEFAULT_VERSION
|
|
71
|
+
from localstack.state import StateVisitor
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
def _version_to_opensearch(
|
|
@@ -208,6 +209,11 @@ def exception_mapper():
|
|
|
208
209
|
|
|
209
210
|
|
|
210
211
|
class EsProvider(EsApi):
|
|
212
|
+
def accept_state_visitor(self, visitor: StateVisitor):
|
|
213
|
+
# ES state entirely depends on `opensearch`, and delegates its entire state to it
|
|
214
|
+
# we do not need to manage state in ES
|
|
215
|
+
pass
|
|
216
|
+
|
|
211
217
|
def create_elasticsearch_domain(
|
|
212
218
|
self,
|
|
213
219
|
context: RequestContext,
|