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
|
@@ -13,20 +13,26 @@ class EngineParameter(TypedDict):
|
|
|
13
13
|
given_value: NotRequired[str | None]
|
|
14
14
|
resolved_value: NotRequired[str | None]
|
|
15
15
|
default_value: NotRequired[str | None]
|
|
16
|
+
no_echo: NotRequired[bool | None]
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def engine_parameter_value(parameter: EngineParameter) -> str:
|
|
19
|
-
|
|
20
|
-
if
|
|
21
|
-
|
|
20
|
+
given_value = parameter.get("given_value")
|
|
21
|
+
if given_value is not None:
|
|
22
|
+
return given_value
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
default_value = parameter.get("default_value")
|
|
25
|
+
if default_value is not None:
|
|
26
|
+
return default_value
|
|
27
|
+
|
|
28
|
+
raise RuntimeError("Parameter value is None")
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
class ResolvedResource(TypedDict):
|
|
27
32
|
LogicalResourceId: str
|
|
28
33
|
Type: str
|
|
29
34
|
Properties: dict
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
LastUpdatedTimestamp: datetime
|
|
36
|
+
ResourceStatus: NotRequired[ResourceStatus]
|
|
37
|
+
PhysicalResourceId: NotRequired[str]
|
|
38
|
+
ResourceStatusReason: NotRequired[str]
|
|
@@ -2,4 +2,7 @@ from localstack import config
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def is_v2_engine() -> bool:
|
|
5
|
-
return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation")
|
|
5
|
+
return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") not in {
|
|
6
|
+
"engine-legacy",
|
|
7
|
+
"engine-legacy_pro",
|
|
8
|
+
}
|
|
@@ -69,7 +69,10 @@ class AlarmScheduler:
|
|
|
69
69
|
schedule_period = evaluation_periods * period
|
|
70
70
|
|
|
71
71
|
def on_error(e):
|
|
72
|
-
LOG.
|
|
72
|
+
if LOG.isEnabledFor(logging.DEBUG):
|
|
73
|
+
LOG.exception("Error executing scheduled alarm", exc_info=e)
|
|
74
|
+
else:
|
|
75
|
+
LOG.error("Error executing scheduled alarm")
|
|
73
76
|
|
|
74
77
|
task = self.scheduler.schedule(
|
|
75
78
|
func=calculate_alarm_state,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import uuid
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
from typing import Any
|
|
5
|
-
from xml.sax.saxutils import escape
|
|
6
6
|
|
|
7
7
|
from moto.cloudwatch import cloudwatch_backends
|
|
8
|
-
from moto.cloudwatch.models import
|
|
8
|
+
from moto.cloudwatch.models import Alarm, CloudWatchBackend, MetricDatum
|
|
9
9
|
|
|
10
10
|
from localstack.aws.accounts import get_account_id_from_access_key_id
|
|
11
11
|
from localstack.aws.api import CommonServiceException, RequestContext, handler
|
|
@@ -54,7 +54,7 @@ MOTO_INITIAL_UNCHECKED_REASON = "Unchecked: Initial alarm creation"
|
|
|
54
54
|
LOG = logging.getLogger(__name__)
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
@patch(target=
|
|
57
|
+
@patch(target=Alarm.update_state)
|
|
58
58
|
def update_state(target, self, reason, reason_data, state_value):
|
|
59
59
|
if reason_data is None:
|
|
60
60
|
reason_data = ""
|
|
@@ -127,9 +127,7 @@ def put_metric_alarm(
|
|
|
127
127
|
threshold_metric_id: str | None = None,
|
|
128
128
|
rule: str | None = None,
|
|
129
129
|
tags: list[dict[str, str]] | None = None,
|
|
130
|
-
) ->
|
|
131
|
-
if description:
|
|
132
|
-
description = escape(description)
|
|
130
|
+
) -> Alarm:
|
|
133
131
|
return target(
|
|
134
132
|
self,
|
|
135
133
|
name,
|
|
@@ -158,7 +156,7 @@ def put_metric_alarm(
|
|
|
158
156
|
)
|
|
159
157
|
|
|
160
158
|
|
|
161
|
-
def create_metric_data_query_from_alarm(alarm:
|
|
159
|
+
def create_metric_data_query_from_alarm(alarm: Alarm):
|
|
162
160
|
# TODO may need to be adapted for other use cases
|
|
163
161
|
# verified return value with a snapshot test
|
|
164
162
|
return [
|
|
@@ -179,7 +177,7 @@ def create_metric_data_query_from_alarm(alarm: FakeAlarm):
|
|
|
179
177
|
|
|
180
178
|
|
|
181
179
|
def create_message_response_update_state_lambda(
|
|
182
|
-
alarm:
|
|
180
|
+
alarm: Alarm, old_state, old_state_reason, old_state_timestamp
|
|
183
181
|
):
|
|
184
182
|
response = {
|
|
185
183
|
"accountId": extract_account_id_from_arn(alarm.alarm_arn),
|
|
@@ -189,12 +187,12 @@ def create_message_response_update_state_lambda(
|
|
|
189
187
|
"state": {
|
|
190
188
|
"value": alarm.state_value,
|
|
191
189
|
"reason": alarm.state_reason,
|
|
192
|
-
"timestamp": alarm.state_updated_timestamp,
|
|
190
|
+
"timestamp": _to_iso_8601_datetime_with_nanoseconds(alarm.state_updated_timestamp),
|
|
193
191
|
},
|
|
194
192
|
"previousState": {
|
|
195
193
|
"value": old_state,
|
|
196
194
|
"reason": old_state_reason,
|
|
197
|
-
"timestamp": old_state_timestamp,
|
|
195
|
+
"timestamp": _to_iso_8601_datetime_with_nanoseconds(old_state_timestamp),
|
|
198
196
|
},
|
|
199
197
|
"configuration": {
|
|
200
198
|
"description": alarm.description or "",
|
|
@@ -204,7 +202,7 @@ def create_message_response_update_state_lambda(
|
|
|
204
202
|
), # TODO: add test with metric_data_queries
|
|
205
203
|
},
|
|
206
204
|
},
|
|
207
|
-
"time": alarm.state_updated_timestamp,
|
|
205
|
+
"time": _to_iso_8601_datetime_with_nanoseconds(alarm.state_updated_timestamp),
|
|
208
206
|
"region": alarm.region_name,
|
|
209
207
|
"source": "aws.cloudwatch",
|
|
210
208
|
}
|
|
@@ -217,10 +215,12 @@ def create_message_response_update_state_sns(alarm, old_state):
|
|
|
217
215
|
"OldStateValue": old_state,
|
|
218
216
|
"AlarmName": alarm.name,
|
|
219
217
|
"AlarmDescription": alarm.description or "",
|
|
220
|
-
"AlarmConfigurationUpdatedTimestamp":
|
|
218
|
+
"AlarmConfigurationUpdatedTimestamp": _to_iso_8601_datetime_with_nanoseconds(
|
|
219
|
+
alarm.configuration_updated_timestamp
|
|
220
|
+
),
|
|
221
221
|
"NewStateValue": alarm.state_value,
|
|
222
222
|
"NewStateReason": alarm.state_reason,
|
|
223
|
-
"StateChangeTime": alarm.state_updated_timestamp,
|
|
223
|
+
"StateChangeTime": _to_iso_8601_datetime_with_nanoseconds(alarm.state_updated_timestamp),
|
|
224
224
|
# the long-name for 'region' should be used - as we don't have it, we use the short name
|
|
225
225
|
# which needs to be slightly changed to make snapshot tests work
|
|
226
226
|
"Region": alarm.region_name.replace("-", " ").capitalize(),
|
|
@@ -268,6 +268,11 @@ class ValidationError(CommonServiceException):
|
|
|
268
268
|
super().__init__("ValidationError", message, 400, True)
|
|
269
269
|
|
|
270
270
|
|
|
271
|
+
def _to_iso_8601_datetime_with_nanoseconds(date: datetime | None) -> str | None:
|
|
272
|
+
if date is not None:
|
|
273
|
+
return date.strftime("%Y-%m-%dT%H:%M:%S.%f000Z")
|
|
274
|
+
|
|
275
|
+
|
|
271
276
|
def _set_alarm_actions(context, alarm_names, enabled):
|
|
272
277
|
backend = cloudwatch_backends[context.account_id][context.region]
|
|
273
278
|
for name in alarm_names:
|
|
@@ -15,6 +15,7 @@ from localstack.aws.api.cloudwatch import (
|
|
|
15
15
|
AlarmTypes,
|
|
16
16
|
AmazonResourceName,
|
|
17
17
|
CloudwatchApi,
|
|
18
|
+
ContributorId,
|
|
18
19
|
DashboardBody,
|
|
19
20
|
DashboardName,
|
|
20
21
|
DashboardNamePrefix,
|
|
@@ -107,17 +108,11 @@ _STORE_LOCK = threading.RLock()
|
|
|
107
108
|
AWS_MAX_DATAPOINTS_ACCEPTED: int = 1440
|
|
108
109
|
|
|
109
110
|
|
|
110
|
-
class
|
|
111
|
-
# TODO: check this error against AWS (doesn't exist in the API)
|
|
111
|
+
class ValidationException(CommonServiceException):
|
|
112
112
|
def __init__(self, message: str):
|
|
113
113
|
super().__init__("ValidationError", message, 400, True)
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
class InvalidParameterCombination(CommonServiceException):
|
|
117
|
-
def __init__(self, message: str):
|
|
118
|
-
super().__init__("InvalidParameterCombination", message, 400, True)
|
|
119
|
-
|
|
120
|
-
|
|
121
116
|
def _validate_parameters_for_put_metric_data(metric_data: MetricData) -> None:
|
|
122
117
|
for index, metric_item in enumerate(metric_data):
|
|
123
118
|
indexplusone = index + 1
|
|
@@ -245,7 +240,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
245
240
|
results: list[MetricDataResult] = []
|
|
246
241
|
limit = max_datapoints or 100_800
|
|
247
242
|
messages: MetricDataResultMessages = []
|
|
248
|
-
nxt = None
|
|
243
|
+
nxt: str | None = None
|
|
249
244
|
label_additions = []
|
|
250
245
|
|
|
251
246
|
for diff in LABEL_DIFFERENTIATORS:
|
|
@@ -279,14 +274,14 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
279
274
|
timestamp_value_dicts = [
|
|
280
275
|
{
|
|
281
276
|
"Timestamp": timestamp,
|
|
282
|
-
"Value": value,
|
|
277
|
+
"Value": float(value),
|
|
283
278
|
}
|
|
284
279
|
for timestamp, value in zip(timestamps, values, strict=False)
|
|
285
280
|
]
|
|
286
281
|
|
|
287
282
|
pagination = PaginatedList(timestamp_value_dicts)
|
|
288
283
|
timestamp_page, nxt = pagination.get_page(
|
|
289
|
-
lambda item: item.get("Timestamp"),
|
|
284
|
+
lambda item: str(item.get("Timestamp")),
|
|
290
285
|
next_token=next_token,
|
|
291
286
|
page_size=limit,
|
|
292
287
|
)
|
|
@@ -314,6 +309,11 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
314
309
|
state_reason_data: StateReasonData = None,
|
|
315
310
|
**kwargs,
|
|
316
311
|
) -> None:
|
|
312
|
+
if state_value not in ("OK", "ALARM", "INSUFFICIENT_DATA"):
|
|
313
|
+
raise ValidationException(
|
|
314
|
+
f"1 validation error detected: Value '{state_value}' at 'stateValue' failed to satisfy constraint: Member must satisfy enum value set: [INSUFFICIENT_DATA, ALARM, OK]"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
317
|
try:
|
|
318
318
|
if state_reason_data:
|
|
319
319
|
state_reason_data = json.loads(state_reason_data)
|
|
@@ -332,10 +332,6 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
332
332
|
raise ResourceNotFound()
|
|
333
333
|
|
|
334
334
|
old_state = alarm.alarm["StateValue"]
|
|
335
|
-
if state_value not in ("OK", "ALARM", "INSUFFICIENT_DATA"):
|
|
336
|
-
raise ValidationError(
|
|
337
|
-
f"1 validation error detected: Value '{state_value}' at 'stateValue' failed to satisfy constraint: Member must satisfy enum value set: [INSUFFICIENT_DATA, ALARM, OK]"
|
|
338
|
-
)
|
|
339
335
|
|
|
340
336
|
old_state_reason = alarm.alarm["StateReason"]
|
|
341
337
|
old_state_update_timestamp = alarm.alarm["StateUpdatedTimestamp"]
|
|
@@ -415,7 +411,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
415
411
|
"ignore",
|
|
416
412
|
"missing",
|
|
417
413
|
]:
|
|
418
|
-
raise
|
|
414
|
+
raise ValidationException(
|
|
419
415
|
f"The value {request['TreatMissingData']} is not supported for TreatMissingData parameter. Supported values are [breaching, notBreaching, ignore, missing]."
|
|
420
416
|
)
|
|
421
417
|
# do some sanity checks:
|
|
@@ -424,7 +420,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
424
420
|
value = request.get("Period")
|
|
425
421
|
if value not in (10, 30):
|
|
426
422
|
if value % 60 != 0:
|
|
427
|
-
raise
|
|
423
|
+
raise ValidationException("Period must be 10, 30 or a multiple of 60")
|
|
428
424
|
if request.get("Statistic"):
|
|
429
425
|
if request.get("Statistic") not in [
|
|
430
426
|
"SampleCount",
|
|
@@ -433,7 +429,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
433
429
|
"Minimum",
|
|
434
430
|
"Maximum",
|
|
435
431
|
]:
|
|
436
|
-
raise
|
|
432
|
+
raise ValidationException(
|
|
437
433
|
f"Value '{request.get('Statistic')}' at 'statistic' failed to satisfy constraint: Member must satisfy enum value set: [Maximum, SampleCount, Sum, Minimum, Average]"
|
|
438
434
|
)
|
|
439
435
|
|
|
@@ -447,7 +443,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
447
443
|
"evaluate",
|
|
448
444
|
"ignore",
|
|
449
445
|
):
|
|
450
|
-
raise
|
|
446
|
+
raise ValidationException(
|
|
451
447
|
f"Option {evaluate_low_sample_count_percentile} is not supported. "
|
|
452
448
|
"Supported options for parameter EvaluateLowSampleCountPercentile are evaluate and ignore."
|
|
453
449
|
)
|
|
@@ -690,7 +686,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
690
686
|
expected_datapoints = (end_time_unix - start_time_unix) / period
|
|
691
687
|
|
|
692
688
|
if expected_datapoints > AWS_MAX_DATAPOINTS_ACCEPTED:
|
|
693
|
-
raise
|
|
689
|
+
raise InvalidParameterCombinationException(
|
|
694
690
|
f"You have requested up to {int(expected_datapoints)} datapoints, which exceeds the limit of {AWS_MAX_DATAPOINTS_ACCEPTED}. "
|
|
695
691
|
f"You may reduce the datapoints requested by increasing Period, or decreasing the time range."
|
|
696
692
|
)
|
|
@@ -737,7 +733,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
737
733
|
for i, timestamp in enumerate(timestamps):
|
|
738
734
|
stat_datapoints.setdefault(selected_unit, {})
|
|
739
735
|
stat_datapoints[selected_unit].setdefault(timestamp, {})
|
|
740
|
-
stat_datapoints[selected_unit][timestamp][stat] = values[i]
|
|
736
|
+
stat_datapoints[selected_unit][timestamp][stat] = float(values[i])
|
|
741
737
|
stat_datapoints[selected_unit][timestamp]["Unit"] = selected_unit
|
|
742
738
|
|
|
743
739
|
datapoints: list[Datapoint] = []
|
|
@@ -822,14 +818,15 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
|
|
|
822
818
|
def describe_alarm_history(
|
|
823
819
|
self,
|
|
824
820
|
context: RequestContext,
|
|
825
|
-
alarm_name: AlarmName = None,
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
821
|
+
alarm_name: AlarmName | None = None,
|
|
822
|
+
alarm_contributor_id: ContributorId | None = None,
|
|
823
|
+
alarm_types: AlarmTypes | None = None,
|
|
824
|
+
history_item_type: HistoryItemType | None = None,
|
|
825
|
+
start_date: Timestamp | None = None,
|
|
826
|
+
end_date: Timestamp | None = None,
|
|
827
|
+
max_records: MaxRecords | None = None,
|
|
828
|
+
next_token: NextToken | None = None,
|
|
829
|
+
scan_by: ScanBy | None = None,
|
|
833
830
|
**kwargs,
|
|
834
831
|
) -> DescribeAlarmHistoryOutput:
|
|
835
832
|
store = self.get_store(context.account_id, context.region)
|
|
@@ -17,7 +17,8 @@ from localstack.utils.run import run
|
|
|
17
17
|
DDB_AGENT_JAR_URL = f"{ARTIFACTS_REPO}/raw/e4e8c8e294b1fcda90c678ff6af5d5ebe1f091eb/dynamodb-local-patch/target/ddb-local-loader-0.2.jar"
|
|
18
18
|
JAVASSIST_JAR_URL = f"{MAVEN_REPO_URL}/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar"
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# URL points to 2.x here - however the latest 3.x builds are available under this URL
|
|
21
|
+
DDBLOCAL_URL = "https://d1ni2b6xgvw0s0.cloudfront.net/v2.x/dynamodb_local_latest.zip"
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class DynamoDBLocalPackage(Package):
|
|
@@ -47,6 +47,8 @@ from localstack.aws.api.dynamodb import (
|
|
|
47
47
|
DeleteRequest,
|
|
48
48
|
DeleteTableOutput,
|
|
49
49
|
DescribeContinuousBackupsOutput,
|
|
50
|
+
DescribeContributorInsightsInput,
|
|
51
|
+
DescribeContributorInsightsOutput,
|
|
50
52
|
DescribeGlobalTableOutput,
|
|
51
53
|
DescribeKinesisStreamingDestinationOutput,
|
|
52
54
|
DescribeTableOutput,
|
|
@@ -746,6 +748,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
746
748
|
if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
|
|
747
749
|
table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
|
|
748
750
|
|
|
751
|
+
if "WarmThroughput" in table_description:
|
|
752
|
+
table_description["WarmThroughput"]["Status"] = "UPDATING"
|
|
753
|
+
|
|
749
754
|
tags = table_definitions.pop("Tags", [])
|
|
750
755
|
if tags:
|
|
751
756
|
get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
|
|
@@ -763,6 +768,13 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
763
768
|
) -> DeleteTableOutput:
|
|
764
769
|
global_table_region = self.get_global_table_region(context, table_name)
|
|
765
770
|
|
|
771
|
+
self.ensure_table_exists(
|
|
772
|
+
context.account_id,
|
|
773
|
+
global_table_region,
|
|
774
|
+
table_name,
|
|
775
|
+
error_message=f"Requested resource not found: Table: {table_name} not found",
|
|
776
|
+
)
|
|
777
|
+
|
|
766
778
|
# Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
|
|
767
779
|
# This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
|
|
768
780
|
|
|
@@ -823,6 +835,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
823
835
|
table_description["TableClassSummary"] = {
|
|
824
836
|
"TableClass": table_definitions["TableClass"]
|
|
825
837
|
}
|
|
838
|
+
if warm_throughput := table_definitions.get("WarmThroughput"):
|
|
839
|
+
table_description["WarmThroughput"] = warm_throughput.copy()
|
|
840
|
+
table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
|
|
826
841
|
|
|
827
842
|
if "GlobalSecondaryIndexes" in table_description:
|
|
828
843
|
for gsi in table_description["GlobalSecondaryIndexes"]:
|
|
@@ -835,6 +850,17 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
835
850
|
# Terraform depends on this parity for update operations
|
|
836
851
|
gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
|
|
837
852
|
|
|
853
|
+
# Set defaults for warm throughput
|
|
854
|
+
if "WarmThroughput" not in table_description:
|
|
855
|
+
billing_mode = table_definitions.get("BillingMode") if table_definitions else None
|
|
856
|
+
table_description["WarmThroughput"] = {
|
|
857
|
+
"ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
|
|
858
|
+
"WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
|
|
859
|
+
}
|
|
860
|
+
table_description["WarmThroughput"]["Status"] = (
|
|
861
|
+
table_description.get("TableStatus") or "ACTIVE"
|
|
862
|
+
)
|
|
863
|
+
|
|
838
864
|
return DescribeTableOutput(
|
|
839
865
|
Table=select_from_typed_dict(TableDescription, table_description)
|
|
840
866
|
)
|
|
@@ -955,6 +981,22 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
955
981
|
|
|
956
982
|
return response
|
|
957
983
|
|
|
984
|
+
#
|
|
985
|
+
# Contributor Insights
|
|
986
|
+
#
|
|
987
|
+
|
|
988
|
+
@handler("DescribeContributorInsights", expand=False)
|
|
989
|
+
def describe_contributor_insights(
|
|
990
|
+
self,
|
|
991
|
+
context: RequestContext,
|
|
992
|
+
describe_contributor_insights_input: DescribeContributorInsightsInput,
|
|
993
|
+
) -> DescribeContributorInsightsOutput:
|
|
994
|
+
return DescribeContributorInsightsOutput(
|
|
995
|
+
TableName=describe_contributor_insights_input["TableName"],
|
|
996
|
+
IndexName=describe_contributor_insights_input.get("IndexName"),
|
|
997
|
+
ContributorInsightsStatus="DISABLED",
|
|
998
|
+
)
|
|
999
|
+
|
|
958
1000
|
#
|
|
959
1001
|
# Item ops
|
|
960
1002
|
#
|
|
@@ -162,7 +162,7 @@ class DynamodbServer(Server):
|
|
|
162
162
|
cmd = [
|
|
163
163
|
"java",
|
|
164
164
|
*self._get_java_vm_options(),
|
|
165
|
-
"-Xmx
|
|
165
|
+
f"-Xmx{self.heap_size}",
|
|
166
166
|
f"-javaagent:{dynamodblocal_package.get_installer().get_ddb_agent_jar_path()}",
|
|
167
167
|
f"-Djava.library.path={self.library_path}",
|
|
168
168
|
"-jar",
|
|
@@ -219,7 +219,7 @@ class DynamodbServer(Server):
|
|
|
219
219
|
aws_secret_access_key=DEFAULT_AWS_ACCOUNT_ID,
|
|
220
220
|
).dynamodb.list_tables()
|
|
221
221
|
except Exception:
|
|
222
|
-
LOG.
|
|
222
|
+
LOG.error("DynamoDB health check failed", exc_info=LOG.isEnabledFor(logging.DEBUG))
|
|
223
223
|
if expect_shutdown:
|
|
224
224
|
assert out is None
|
|
225
225
|
else:
|
|
@@ -40,6 +40,8 @@ from localstack.aws.api.dynamodb import (
|
|
|
40
40
|
DeleteRequest,
|
|
41
41
|
DeleteTableOutput,
|
|
42
42
|
DescribeContinuousBackupsOutput,
|
|
43
|
+
DescribeContributorInsightsInput,
|
|
44
|
+
DescribeContributorInsightsOutput,
|
|
43
45
|
DescribeGlobalTableOutput,
|
|
44
46
|
DescribeKinesisStreamingDestinationOutput,
|
|
45
47
|
DescribeTableOutput,
|
|
@@ -558,6 +560,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
558
560
|
if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
|
|
559
561
|
table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
|
|
560
562
|
|
|
563
|
+
if "WarmThroughput" in table_description:
|
|
564
|
+
table_description["WarmThroughput"]["Status"] = "UPDATING"
|
|
565
|
+
|
|
561
566
|
tags = table_definitions.pop("Tags", [])
|
|
562
567
|
if tags:
|
|
563
568
|
get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
|
|
@@ -575,6 +580,13 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
575
580
|
) -> DeleteTableOutput:
|
|
576
581
|
global_table_region = self.get_global_table_region(context, table_name)
|
|
577
582
|
|
|
583
|
+
self.ensure_table_exists(
|
|
584
|
+
context.account_id,
|
|
585
|
+
global_table_region,
|
|
586
|
+
table_name,
|
|
587
|
+
error_message=f"Requested resource not found: Table: {table_name} not found",
|
|
588
|
+
)
|
|
589
|
+
|
|
578
590
|
# Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
|
|
579
591
|
# This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
|
|
580
592
|
|
|
@@ -634,6 +646,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
634
646
|
table_description["TableClassSummary"] = {
|
|
635
647
|
"TableClass": table_definitions["TableClass"]
|
|
636
648
|
}
|
|
649
|
+
if warm_throughput := table_definitions.get("WarmThroughput"):
|
|
650
|
+
table_description["WarmThroughput"] = warm_throughput.copy()
|
|
651
|
+
table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
|
|
637
652
|
|
|
638
653
|
if "GlobalSecondaryIndexes" in table_description:
|
|
639
654
|
for gsi in table_description["GlobalSecondaryIndexes"]:
|
|
@@ -646,6 +661,17 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
646
661
|
# Terraform depends on this parity for update operations
|
|
647
662
|
gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
|
|
648
663
|
|
|
664
|
+
# Set defaults for warm throughput
|
|
665
|
+
if "WarmThroughput" not in table_description:
|
|
666
|
+
billing_mode = table_definitions.get("BillingMode") if table_definitions else None
|
|
667
|
+
table_description["WarmThroughput"] = {
|
|
668
|
+
"ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
|
|
669
|
+
"WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
|
|
670
|
+
}
|
|
671
|
+
table_description["WarmThroughput"]["Status"] = (
|
|
672
|
+
table_description.get("TableStatus") or "ACTIVE"
|
|
673
|
+
)
|
|
674
|
+
|
|
649
675
|
return DescribeTableOutput(
|
|
650
676
|
Table=select_from_typed_dict(TableDescription, table_description)
|
|
651
677
|
)
|
|
@@ -757,6 +783,22 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
|
|
|
757
783
|
|
|
758
784
|
return response
|
|
759
785
|
|
|
786
|
+
#
|
|
787
|
+
# Contributor Insights
|
|
788
|
+
#
|
|
789
|
+
|
|
790
|
+
@handler("DescribeContributorInsights", expand=False)
|
|
791
|
+
def describe_contributor_insights(
|
|
792
|
+
self,
|
|
793
|
+
context: RequestContext,
|
|
794
|
+
describe_contributor_insights_input: DescribeContributorInsightsInput,
|
|
795
|
+
) -> DescribeContributorInsightsOutput:
|
|
796
|
+
return DescribeContributorInsightsOutput(
|
|
797
|
+
TableName=describe_contributor_insights_input["TableName"],
|
|
798
|
+
IndexName=describe_contributor_insights_input.get("IndexName"),
|
|
799
|
+
ContributorInsightsStatus="DISABLED",
|
|
800
|
+
)
|
|
801
|
+
|
|
760
802
|
#
|
|
761
803
|
# Item ops
|
|
762
804
|
#
|
|
@@ -6,7 +6,6 @@ from pathlib import Path
|
|
|
6
6
|
from typing import TypedDict
|
|
7
7
|
|
|
8
8
|
import localstack.services.cloudformation.provider_utils as util
|
|
9
|
-
from localstack.constants import AWS_REGION_US_EAST_1, DEFAULT_AWS_ACCOUNT_ID
|
|
10
9
|
from localstack.services.cloudformation.resource_provider import (
|
|
11
10
|
OperationStatus,
|
|
12
11
|
ProgressEvent,
|
|
@@ -90,6 +89,10 @@ class ECRRepositoryProvider(ResourceProvider[ECRRepositoryProperties]):
|
|
|
90
89
|
|
|
91
90
|
"""
|
|
92
91
|
model = request.desired_state
|
|
92
|
+
model["RepositoryName"] = (
|
|
93
|
+
model.get("RepositoryName")
|
|
94
|
+
or util.generate_default_name(request.stack_name, request.logical_resource_id).lower()
|
|
95
|
+
)
|
|
93
96
|
|
|
94
97
|
default_repos_per_stack[request.stack_name] = model["RepositoryName"]
|
|
95
98
|
LOG.warning(
|
|
@@ -98,7 +101,7 @@ class ECRRepositoryProvider(ResourceProvider[ECRRepositoryProperties]):
|
|
|
98
101
|
model.update(
|
|
99
102
|
{
|
|
100
103
|
"Arn": arns.ecr_repository_arn(
|
|
101
|
-
model["RepositoryName"],
|
|
104
|
+
model["RepositoryName"], request.account_id, request.region_name
|
|
102
105
|
),
|
|
103
106
|
"RepositoryUri": "http://localhost:4566",
|
|
104
107
|
"ImageTagMutability": "MUTABLE",
|
localstack/services/edge.py
CHANGED
|
@@ -72,7 +72,7 @@ def start_component(
|
|
|
72
72
|
default_port=constants.DEFAULT_PORT_EDGE,
|
|
73
73
|
),
|
|
74
74
|
)
|
|
75
|
-
raise Exception("Unexpected component name '
|
|
75
|
+
raise Exception(f"Unexpected component name '{component}' received during start up")
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
def start_proxy(
|
|
@@ -3,7 +3,6 @@ from typing import cast
|
|
|
3
3
|
|
|
4
4
|
from botocore.exceptions import ClientError
|
|
5
5
|
|
|
6
|
-
from localstack import constants
|
|
7
6
|
from localstack.aws.api import RequestContext, handler
|
|
8
7
|
from localstack.aws.api.es import (
|
|
9
8
|
ARN,
|
|
@@ -68,6 +67,7 @@ from localstack.aws.api.opensearch import (
|
|
|
68
67
|
VersionString,
|
|
69
68
|
)
|
|
70
69
|
from localstack.aws.connect import connect_to
|
|
70
|
+
from localstack.services.opensearch.packages import ELASTICSEARCH_DEFAULT_VERSION
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
def _version_to_opensearch(
|
|
@@ -236,7 +236,7 @@ class EsProvider(EsApi):
|
|
|
236
236
|
engine_version = (
|
|
237
237
|
_version_to_opensearch(elasticsearch_version)
|
|
238
238
|
if elasticsearch_version
|
|
239
|
-
else
|
|
239
|
+
else ELASTICSEARCH_DEFAULT_VERSION
|
|
240
240
|
)
|
|
241
241
|
kwargs = {
|
|
242
242
|
"DomainName": domain_name,
|
|
@@ -59,14 +59,21 @@ class EventRuleEngine:
|
|
|
59
59
|
for flat_pattern in flat_pattern_conditions
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
def _evaluate_condition(self, value, condition, field_exists: bool):
|
|
62
|
+
def _evaluate_condition(self, value: t.Any, condition: t.Any, field_exists: bool) -> bool:
|
|
63
63
|
if not isinstance(condition, dict):
|
|
64
64
|
return field_exists and value == condition
|
|
65
|
+
|
|
65
66
|
elif (must_exist := condition.get("exists")) is not None:
|
|
66
67
|
# if must_exists is True then field_exists must be True
|
|
67
68
|
# if must_exists is False then fields_exists must be False
|
|
68
69
|
return must_exist == field_exists
|
|
70
|
+
|
|
69
71
|
elif (anything_but := condition.get("anything-but")) is not None:
|
|
72
|
+
if not field_exists:
|
|
73
|
+
# anything-but can handle None `value`, but it needs to differentiate between user-set `null` and
|
|
74
|
+
# missing value
|
|
75
|
+
return False
|
|
76
|
+
|
|
70
77
|
if isinstance(anything_but, dict):
|
|
71
78
|
if (not_condition := anything_but.get("prefix")) is not None:
|
|
72
79
|
predicate = self._evaluate_prefix
|
|
@@ -95,6 +102,7 @@ class EventRuleEngine:
|
|
|
95
102
|
elif value is None:
|
|
96
103
|
# the remaining conditions require the value to not be None
|
|
97
104
|
return False
|
|
105
|
+
|
|
98
106
|
elif (prefix := condition.get("prefix")) is not None:
|
|
99
107
|
if isinstance(prefix, dict):
|
|
100
108
|
if (prefix_equal_ignore_case := prefix.get("equals-ignore-case")) is not None:
|
|
@@ -104,7 +112,7 @@ class EventRuleEngine:
|
|
|
104
112
|
|
|
105
113
|
elif (suffix := condition.get("suffix")) is not None:
|
|
106
114
|
if isinstance(suffix, dict):
|
|
107
|
-
if suffix_equal_ignore_case := suffix.get("equals-ignore-case"):
|
|
115
|
+
if (suffix_equal_ignore_case := suffix.get("equals-ignore-case")) is not None:
|
|
108
116
|
return self._evaluate_suffix(suffix_equal_ignore_case.lower(), value.lower())
|
|
109
117
|
else:
|
|
110
118
|
return self._evaluate_suffix(suffix, value)
|
|
@@ -126,19 +134,19 @@ class EventRuleEngine:
|
|
|
126
134
|
return False
|
|
127
135
|
|
|
128
136
|
@staticmethod
|
|
129
|
-
def _evaluate_prefix(condition: str | list, value:
|
|
130
|
-
return value.startswith(condition)
|
|
137
|
+
def _evaluate_prefix(condition: str | list, value: t.Any) -> bool:
|
|
138
|
+
return isinstance(value, str) and value.startswith(condition)
|
|
131
139
|
|
|
132
140
|
@staticmethod
|
|
133
|
-
def _evaluate_suffix(condition: str | list, value:
|
|
134
|
-
return value.endswith(condition)
|
|
141
|
+
def _evaluate_suffix(condition: str | list, value: t.Any) -> bool:
|
|
142
|
+
return isinstance(value, str) and value.endswith(condition)
|
|
135
143
|
|
|
136
144
|
@staticmethod
|
|
137
|
-
def _evaluate_equal_ignore_case(condition: str, value:
|
|
138
|
-
return condition.lower() == value.lower()
|
|
145
|
+
def _evaluate_equal_ignore_case(condition: str, value: t.Any) -> bool:
|
|
146
|
+
return isinstance(value, str) and condition.lower() == value.lower()
|
|
139
147
|
|
|
140
148
|
@staticmethod
|
|
141
|
-
def _evaluate_cidr(condition: str, value:
|
|
149
|
+
def _evaluate_cidr(condition: str, value: t.Any) -> bool:
|
|
142
150
|
try:
|
|
143
151
|
ip = ipaddress.ip_address(value)
|
|
144
152
|
return ip in ipaddress.ip_network(condition)
|
|
@@ -146,8 +154,10 @@ class EventRuleEngine:
|
|
|
146
154
|
return False
|
|
147
155
|
|
|
148
156
|
@staticmethod
|
|
149
|
-
def _evaluate_wildcard(condition: str, value:
|
|
150
|
-
return
|
|
157
|
+
def _evaluate_wildcard(condition: str, value: t.Any) -> bool:
|
|
158
|
+
return isinstance(value, str) and bool(
|
|
159
|
+
re.match(re.escape(condition).replace("\\*", ".+") + "$", value)
|
|
160
|
+
)
|
|
151
161
|
|
|
152
162
|
@staticmethod
|
|
153
163
|
def _evaluate_numeric_condition(conditions: list, value: t.Any) -> bool:
|
|
@@ -457,10 +467,18 @@ class EventPatternCompiler:
|
|
|
457
467
|
return
|
|
458
468
|
|
|
459
469
|
elif operator == "anything-but":
|
|
460
|
-
# anything-but can actually contain any kind of simple rule (str, number, and list)
|
|
470
|
+
# anything-but can actually contain any kind of simple rule (str, number, and list) except Null
|
|
471
|
+
if value is None:
|
|
472
|
+
raise InvalidEventPatternException(
|
|
473
|
+
f"{self.error_prefix}Value of anything-but must be an array or single string/number value."
|
|
474
|
+
)
|
|
461
475
|
if isinstance(value, list):
|
|
462
476
|
for v in value:
|
|
463
|
-
|
|
477
|
+
if v is None:
|
|
478
|
+
raise InvalidEventPatternException(
|
|
479
|
+
f"{self.error_prefix}Inside anything but list, start|null|boolean is not supported."
|
|
480
|
+
)
|
|
481
|
+
self._validate_rule(v, from_="anything-but")
|
|
464
482
|
|
|
465
483
|
return
|
|
466
484
|
|