localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev7__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 +1 -0
- localstack/aws/api/cloudwatch/__init__.py +41 -1
- localstack/aws/api/config/__init__.py +4 -0
- localstack/aws/api/core.py +4 -0
- localstack/aws/api/ec2/__init__.py +1113 -56
- 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 +2 -0
- localstack/aws/api/s3/__init__.py +12 -0
- localstack/aws/api/s3control/__init__.py +32 -0
- localstack/aws/api/ssm/__init__.py +2 -0
- 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 +32 -9
- localstack/aws/protocol/parser.py +440 -21
- localstack/aws/protocol/serializer.py +684 -64
- localstack/aws/protocol/service_router.py +120 -20
- 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 +4 -4
- 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 +1 -1
- 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/legacy/provider.py +53 -3
- 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/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/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 +172 -27
- 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/provider.py +77 -0
- localstack/services/kms/provider.py +14 -5
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/logs/provider.py +1 -1
- 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 +67 -11
- 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 +167 -0
- localstack/services/sns/v2/provider.py +860 -2
- localstack/services/sns/v2/utils.py +130 -0
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +42 -3
- 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 +5 -0
- 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/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 +11 -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/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.dev7.dist-info}/METADATA +17 -12
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
- localstack_core-4.10.1.dev7.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.dev7.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/top_level.txt +0 -0
|
@@ -25,7 +25,7 @@ from localstack.aws.api.cloudformation import (
|
|
|
25
25
|
Parameter as ApiParameter,
|
|
26
26
|
)
|
|
27
27
|
from localstack.services.cloudformation.engine.entities import (
|
|
28
|
-
|
|
28
|
+
StackIdentifierV2,
|
|
29
29
|
)
|
|
30
30
|
from localstack.services.cloudformation.engine.v2.change_set_model import (
|
|
31
31
|
ChangeType,
|
|
@@ -41,6 +41,7 @@ class Stack:
|
|
|
41
41
|
description: str | None
|
|
42
42
|
parameters: list[ApiParameter]
|
|
43
43
|
change_set_id: str | None
|
|
44
|
+
change_set_ids: set[str]
|
|
44
45
|
status: StackStatus
|
|
45
46
|
status_reason: StackStatusReason | None
|
|
46
47
|
stack_id: str
|
|
@@ -49,6 +50,7 @@ class Stack:
|
|
|
49
50
|
events: list[StackEvent]
|
|
50
51
|
capabilities: list[Capability]
|
|
51
52
|
enable_termination_protection: bool
|
|
53
|
+
template: dict | None
|
|
52
54
|
processed_template: dict | None
|
|
53
55
|
template_body: str | None
|
|
54
56
|
tags: list[Tag]
|
|
@@ -72,11 +74,12 @@ class Stack:
|
|
|
72
74
|
self.region_name = region_name
|
|
73
75
|
self.status = initial_status
|
|
74
76
|
self.status_reason = None
|
|
75
|
-
self.change_set_ids =
|
|
77
|
+
self.change_set_ids = set()
|
|
76
78
|
self.creation_time = datetime.now(tz=UTC)
|
|
77
79
|
self.deletion_time = None
|
|
78
80
|
self.change_set_id = None
|
|
79
81
|
self.enable_termination_protection = False
|
|
82
|
+
self.template = None
|
|
80
83
|
self.processed_template = None
|
|
81
84
|
self.template_body = None
|
|
82
85
|
self.tags = tags or []
|
|
@@ -85,7 +88,7 @@ class Stack:
|
|
|
85
88
|
self.parameters = request_payload.get("Parameters", [])
|
|
86
89
|
self.stack_id = arns.cloudformation_stack_arn(
|
|
87
90
|
self.stack_name,
|
|
88
|
-
stack_id=
|
|
91
|
+
stack_id=StackIdentifierV2(
|
|
89
92
|
account_id=self.account_id, region=self.region_name, stack_name=self.stack_name
|
|
90
93
|
).generate(tags=request_payload.get("Tags")),
|
|
91
94
|
account_id=self.account_id,
|
|
@@ -15,6 +15,7 @@ from localstack.aws.api.cloudformation import (
|
|
|
15
15
|
ChangeSetNameOrId,
|
|
16
16
|
ChangeSetNotFoundException,
|
|
17
17
|
ChangeSetStatus,
|
|
18
|
+
ChangeSetSummary,
|
|
18
19
|
ChangeSetType,
|
|
19
20
|
ClientRequestToken,
|
|
20
21
|
CreateChangeSetInput,
|
|
@@ -46,6 +47,7 @@ from localstack.aws.api.cloudformation import (
|
|
|
46
47
|
IncludePropertyValues,
|
|
47
48
|
InsufficientCapabilitiesException,
|
|
48
49
|
InvalidChangeSetStatusException,
|
|
50
|
+
ListChangeSetsOutput,
|
|
49
51
|
ListExportsOutput,
|
|
50
52
|
ListStackResourcesOutput,
|
|
51
53
|
ListStacksOutput,
|
|
@@ -118,9 +120,10 @@ from localstack.services.cloudformation.v2.entities import (
|
|
|
118
120
|
StackInstance,
|
|
119
121
|
StackSet,
|
|
120
122
|
)
|
|
121
|
-
from localstack.services.cloudformation.v2.types import EngineParameter
|
|
123
|
+
from localstack.services.cloudformation.v2.types import EngineParameter, engine_parameter_value
|
|
122
124
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
123
125
|
from localstack.utils.collections import select_attributes
|
|
126
|
+
from localstack.utils.numbers import is_number
|
|
124
127
|
from localstack.utils.strings import short_uid
|
|
125
128
|
from localstack.utils.threads import start_worker_thread
|
|
126
129
|
|
|
@@ -234,14 +237,18 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
234
237
|
issue_url = "?".join([base, urlencode(query_args)])
|
|
235
238
|
LOG.info(
|
|
236
239
|
"You have opted in to the new CloudFormation deployment engine. "
|
|
237
|
-
"You can opt in to using the old engine by setting PROVIDER_OVERRIDE_CLOUDFORMATION=legacy. "
|
|
240
|
+
"You can opt in to using the old engine by setting PROVIDER_OVERRIDE_CLOUDFORMATION=engine-legacy. "
|
|
238
241
|
"If you experience issues, please submit a bug report at this URL: %s",
|
|
239
242
|
issue_url,
|
|
240
243
|
)
|
|
241
244
|
|
|
242
245
|
@staticmethod
|
|
243
246
|
def _resolve_parameters(
|
|
244
|
-
template: dict | None,
|
|
247
|
+
template: dict | None,
|
|
248
|
+
parameters: dict | None,
|
|
249
|
+
account_id: str,
|
|
250
|
+
region_name: str,
|
|
251
|
+
before_parameters: dict | None,
|
|
245
252
|
) -> dict[str, EngineParameter]:
|
|
246
253
|
template_parameters = template.get("Parameters", {})
|
|
247
254
|
resolved_parameters = {}
|
|
@@ -256,6 +263,12 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
256
263
|
no_echo=parameter.get("NoEcho"),
|
|
257
264
|
)
|
|
258
265
|
|
|
266
|
+
# validate the type
|
|
267
|
+
if parameter["Type"] == "Number" and not is_number(
|
|
268
|
+
engine_parameter_value(resolved_parameter)
|
|
269
|
+
):
|
|
270
|
+
raise ValidationError(f"Parameter '{name}' must be a number.")
|
|
271
|
+
|
|
259
272
|
# TODO: support other parameter types
|
|
260
273
|
if match := SSM_PARAMETER_TYPE_RE.match(parameter["Type"]):
|
|
261
274
|
inner_type = match.group("innertype")
|
|
@@ -276,10 +289,25 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
276
289
|
resolved_parameter["resolved_value"] = resolve_ssm_parameter(
|
|
277
290
|
account_id, region_name, given_value or default_value
|
|
278
291
|
)
|
|
279
|
-
except Exception:
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
except Exception as e:
|
|
293
|
+
# we could not find the parameter however CDK provides the resolved value rather than the
|
|
294
|
+
# parameter name again so try to look up the value in the previous parameters
|
|
295
|
+
if (
|
|
296
|
+
before_parameters
|
|
297
|
+
and (before_param := before_parameters.get(name))
|
|
298
|
+
and isinstance(before_param, dict)
|
|
299
|
+
and (resolved_value := before_param.get("resolved_value"))
|
|
300
|
+
):
|
|
301
|
+
LOG.debug(
|
|
302
|
+
"Parameter %s could not be resolved, using previous value of %s",
|
|
303
|
+
name,
|
|
304
|
+
resolved_value,
|
|
305
|
+
)
|
|
306
|
+
resolved_parameter["resolved_value"] = resolved_value
|
|
307
|
+
else:
|
|
308
|
+
raise ValidationError(
|
|
309
|
+
f"Parameter {name} should either have input value or default value"
|
|
310
|
+
) from e
|
|
283
311
|
elif given_value is None and default_value is None:
|
|
284
312
|
invalid_parameters.append(name)
|
|
285
313
|
continue
|
|
@@ -318,6 +346,7 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
318
346
|
after_parameters,
|
|
319
347
|
change_set.stack.account_id,
|
|
320
348
|
change_set.stack.region_name,
|
|
349
|
+
before_parameters,
|
|
321
350
|
)
|
|
322
351
|
|
|
323
352
|
change_set.resolved_parameters = resolved_parameters
|
|
@@ -435,6 +464,7 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
435
464
|
stack = state.stacks_v2.get(stack_name)
|
|
436
465
|
if not stack:
|
|
437
466
|
raise ValidationError(f"Stack '{stack_name}' does not exist.")
|
|
467
|
+
stack.capabilities = request.get("Capabilities") or []
|
|
438
468
|
else:
|
|
439
469
|
# stack name specified, so fetch the stack by name
|
|
440
470
|
stack_candidates: list[Stack] = [
|
|
@@ -455,6 +485,8 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
455
485
|
if not active_stack_candidates:
|
|
456
486
|
raise ValidationError(f"Stack '{stack_name}' does not exist.")
|
|
457
487
|
stack = active_stack_candidates[0]
|
|
488
|
+
# propagate capabilities from create change set request
|
|
489
|
+
stack.capabilities = request.get("Capabilities") or []
|
|
458
490
|
|
|
459
491
|
# TODO: test if rollback status is allowed as well
|
|
460
492
|
if (
|
|
@@ -465,6 +497,14 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
465
497
|
f"Stack [{stack_name}] already exists and cannot be created again with the changeSet [{change_set_name}]."
|
|
466
498
|
)
|
|
467
499
|
|
|
500
|
+
if change_set_type == ChangeSetType.UPDATE and (
|
|
501
|
+
stack.status == StackStatus.DELETE_COMPLETE
|
|
502
|
+
or stack.status == StackStatus.DELETE_IN_PROGRESS
|
|
503
|
+
):
|
|
504
|
+
raise ValidationError(
|
|
505
|
+
f"Stack:{stack.stack_id} is in {stack.status} state and can not be updated."
|
|
506
|
+
)
|
|
507
|
+
|
|
468
508
|
before_parameters: dict[str, Parameter] | None = None
|
|
469
509
|
match change_set_type:
|
|
470
510
|
case ChangeSetType.UPDATE:
|
|
@@ -525,21 +565,21 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
525
565
|
after_parameters=after_parameters,
|
|
526
566
|
previous_update_model=previous_update_model,
|
|
527
567
|
)
|
|
528
|
-
|
|
529
|
-
# TODO: handle the empty change set case
|
|
530
|
-
if not change_set.has_changes():
|
|
531
|
-
change_set.set_change_set_status(ChangeSetStatus.FAILED)
|
|
568
|
+
if change_set.status == ChangeSetStatus.FAILED:
|
|
532
569
|
change_set.set_execution_status(ExecutionStatus.UNAVAILABLE)
|
|
533
|
-
change_set.status_reason = "The submitted information didn't contain changes. Submit different information to create a change set."
|
|
534
570
|
else:
|
|
535
|
-
if
|
|
536
|
-
|
|
571
|
+
if not change_set.has_changes():
|
|
572
|
+
change_set.set_change_set_status(ChangeSetStatus.FAILED)
|
|
573
|
+
change_set.set_execution_status(ExecutionStatus.UNAVAILABLE)
|
|
574
|
+
change_set.status_reason = "The submitted information didn't contain changes. Submit different information to create a change set."
|
|
575
|
+
else:
|
|
576
|
+
if stack.status not in [StackStatus.CREATE_COMPLETE, StackStatus.UPDATE_COMPLETE]:
|
|
577
|
+
stack.set_stack_status(StackStatus.REVIEW_IN_PROGRESS, "User Initiated")
|
|
537
578
|
|
|
538
|
-
|
|
579
|
+
change_set.set_change_set_status(ChangeSetStatus.CREATE_COMPLETE)
|
|
539
580
|
|
|
540
|
-
stack.change_set_ids.
|
|
581
|
+
stack.change_set_ids.add(change_set.change_set_id)
|
|
541
582
|
state.change_sets[change_set.change_set_id] = change_set
|
|
542
|
-
|
|
543
583
|
return CreateChangeSetOutput(StackId=stack.stack_id, Id=change_set.change_set_id)
|
|
544
584
|
|
|
545
585
|
@handler("ExecuteChangeSet")
|
|
@@ -592,6 +632,11 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
592
632
|
result = change_set_executor.execute()
|
|
593
633
|
change_set.stack.resolved_parameters = change_set.resolved_parameters
|
|
594
634
|
change_set.stack.resolved_resources = result.resources
|
|
635
|
+
change_set.stack.template = change_set.template
|
|
636
|
+
change_set.stack.processed_template = change_set.processed_template
|
|
637
|
+
change_set.stack.template_body = change_set.template_body
|
|
638
|
+
change_set.stack.description = change_set.template.get("Description")
|
|
639
|
+
|
|
595
640
|
if not result.failure_message:
|
|
596
641
|
new_stack_status = StackStatus.UPDATE_COMPLETE
|
|
597
642
|
if change_set.change_set_type == ChangeSetType.CREATE:
|
|
@@ -606,14 +651,6 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
606
651
|
change_set.stack.resolved_exports[export_name] = output["OutputValue"]
|
|
607
652
|
|
|
608
653
|
change_set.stack.change_set_id = change_set.change_set_id
|
|
609
|
-
change_set.stack.change_set_ids.append(change_set.change_set_id)
|
|
610
|
-
|
|
611
|
-
# if the deployment succeeded, update the stack's template representation to that
|
|
612
|
-
# which was just deployed
|
|
613
|
-
change_set.stack.template = change_set.template
|
|
614
|
-
change_set.stack.description = change_set.template.get("Description")
|
|
615
|
-
change_set.stack.processed_template = change_set.processed_template
|
|
616
|
-
change_set.stack.template_body = change_set.template_body
|
|
617
654
|
else:
|
|
618
655
|
LOG.error(
|
|
619
656
|
"Execute change set failed: %s",
|
|
@@ -667,6 +704,27 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
667
704
|
if not change_set:
|
|
668
705
|
raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
|
|
669
706
|
|
|
707
|
+
# if the change set failed to create, then we can return a blank response
|
|
708
|
+
if change_set.status == ChangeSetStatus.FAILED:
|
|
709
|
+
return DescribeChangeSetOutput(
|
|
710
|
+
Status=change_set.status,
|
|
711
|
+
ChangeSetId=change_set.change_set_id,
|
|
712
|
+
ChangeSetName=change_set.change_set_name,
|
|
713
|
+
ExecutionStatus=change_set.execution_status,
|
|
714
|
+
RollbackConfiguration=RollbackConfiguration(),
|
|
715
|
+
StackId=change_set.stack.stack_id,
|
|
716
|
+
StackName=change_set.stack.stack_name,
|
|
717
|
+
CreationTime=change_set.creation_time,
|
|
718
|
+
Changes=[],
|
|
719
|
+
Capabilities=change_set.stack.capabilities,
|
|
720
|
+
StatusReason=change_set.status_reason,
|
|
721
|
+
Description=change_set.description,
|
|
722
|
+
# TODO: static information
|
|
723
|
+
IncludeNestedStacks=False,
|
|
724
|
+
NotificationARNs=[],
|
|
725
|
+
Tags=change_set.tags or None,
|
|
726
|
+
)
|
|
727
|
+
|
|
670
728
|
# TODO: The ChangeSetModelDescriber currently matches AWS behavior by listing
|
|
671
729
|
# resource changes in the order they appear in the template. However, when
|
|
672
730
|
# a resource change is triggered indirectly (e.g., via Ref or GetAtt), the
|
|
@@ -701,6 +759,44 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
701
759
|
result["Parameters"] = self._render_resolved_parameters(change_set.resolved_parameters)
|
|
702
760
|
return result
|
|
703
761
|
|
|
762
|
+
@handler("ListChangeSets")
|
|
763
|
+
def list_change_sets(
|
|
764
|
+
self,
|
|
765
|
+
context: RequestContext,
|
|
766
|
+
stack_name: StackNameOrId,
|
|
767
|
+
next_token: NextToken = None,
|
|
768
|
+
**kwargs,
|
|
769
|
+
) -> ListChangeSetsOutput:
|
|
770
|
+
store = get_cloudformation_store(account_id=context.account_id, region_name=context.region)
|
|
771
|
+
stack = find_stack_v2(store, stack_name)
|
|
772
|
+
if not stack:
|
|
773
|
+
raise StackNotFoundError(stack_name)
|
|
774
|
+
summaries = []
|
|
775
|
+
for change_set_id in stack.change_set_ids:
|
|
776
|
+
change_set = store.change_sets[change_set_id]
|
|
777
|
+
if (
|
|
778
|
+
change_set.status != ChangeSetStatus.CREATE_COMPLETE
|
|
779
|
+
or change_set.execution_status != ExecutionStatus.AVAILABLE
|
|
780
|
+
):
|
|
781
|
+
continue
|
|
782
|
+
|
|
783
|
+
summaries.append(
|
|
784
|
+
ChangeSetSummary(
|
|
785
|
+
StackId=change_set.stack.stack_id,
|
|
786
|
+
StackName=change_set.stack.stack_name,
|
|
787
|
+
ChangeSetId=change_set_id,
|
|
788
|
+
ChangeSetName=change_set.change_set_name,
|
|
789
|
+
ExecutionStatus=change_set.execution_status,
|
|
790
|
+
Status=change_set.status,
|
|
791
|
+
StatusReason=change_set.status_reason,
|
|
792
|
+
CreationTime=change_set.creation_time,
|
|
793
|
+
# mocked information
|
|
794
|
+
IncludeNestedStacks=False,
|
|
795
|
+
)
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
return ListChangeSetsOutput(Summaries=summaries)
|
|
799
|
+
|
|
704
800
|
@handler("DeleteChangeSet")
|
|
705
801
|
def delete_change_set(
|
|
706
802
|
self,
|
|
@@ -714,8 +810,22 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
714
810
|
if not change_set:
|
|
715
811
|
return DeleteChangeSetOutput()
|
|
716
812
|
|
|
717
|
-
|
|
718
|
-
|
|
813
|
+
try:
|
|
814
|
+
change_set.stack.change_set_ids.remove(change_set.change_set_id)
|
|
815
|
+
except KeyError:
|
|
816
|
+
LOG.warning(
|
|
817
|
+
"Could not disassociatei change set '%s' from stack '%s', it does not seem to be associated",
|
|
818
|
+
change_set.change_set_id,
|
|
819
|
+
change_set.stack.stack_id,
|
|
820
|
+
)
|
|
821
|
+
try:
|
|
822
|
+
state.change_sets.pop(change_set.change_set_id)
|
|
823
|
+
except KeyError:
|
|
824
|
+
# This _should_ never fail since if we cannot find the change set in the store (using
|
|
825
|
+
# `find_change_set_v2`) then we early return from this function
|
|
826
|
+
LOG.warning(
|
|
827
|
+
"Could not delete change set '%s', it does not exist", change_set.change_set_id
|
|
828
|
+
)
|
|
719
829
|
|
|
720
830
|
return DeleteChangeSetOutput()
|
|
721
831
|
|
|
@@ -999,6 +1109,12 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
999
1109
|
|
|
1000
1110
|
try:
|
|
1001
1111
|
resource = stack.resolved_resources[logical_resource_id]
|
|
1112
|
+
if resource.get("ResourceStatus") not in [
|
|
1113
|
+
StackStatus.CREATE_COMPLETE,
|
|
1114
|
+
StackStatus.UPDATE_COMPLETE,
|
|
1115
|
+
StackStatus.ROLLBACK_COMPLETE,
|
|
1116
|
+
]:
|
|
1117
|
+
raise KeyError
|
|
1002
1118
|
except KeyError:
|
|
1003
1119
|
raise ValidationError(
|
|
1004
1120
|
f"Resource {logical_resource_id} does not exist for stack {stack_name}"
|
|
@@ -1258,10 +1374,19 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
1258
1374
|
) -> GetTemplateOutput:
|
|
1259
1375
|
state = get_cloudformation_store(context.account_id, context.region)
|
|
1260
1376
|
if change_set_name:
|
|
1377
|
+
if not is_changeset_arn(change_set_name) and not stack_name:
|
|
1378
|
+
raise ValidationError("StackName is a required parameter.")
|
|
1379
|
+
|
|
1261
1380
|
change_set = find_change_set_v2(state, change_set_name, stack_name=stack_name)
|
|
1381
|
+
if not change_set:
|
|
1382
|
+
raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
|
|
1262
1383
|
stack = change_set.stack
|
|
1263
1384
|
elif stack_name:
|
|
1264
1385
|
stack = find_stack_v2(state, stack_name)
|
|
1386
|
+
if not stack:
|
|
1387
|
+
raise StackNotFoundError(
|
|
1388
|
+
stack_name, message_override=f"Stack with id {stack_name} does not exist"
|
|
1389
|
+
)
|
|
1265
1390
|
else:
|
|
1266
1391
|
raise StackNotFoundError(stack_name)
|
|
1267
1392
|
|
|
@@ -1288,6 +1413,12 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
1288
1413
|
stack = find_stack_v2(state, stack_name)
|
|
1289
1414
|
if not stack:
|
|
1290
1415
|
raise StackNotFoundError(stack_name)
|
|
1416
|
+
|
|
1417
|
+
if stack.status == StackStatus.REVIEW_IN_PROGRESS:
|
|
1418
|
+
raise ValidationError(
|
|
1419
|
+
"GetTemplateSummary cannot be called on REVIEW_IN_PROGRESS stacks."
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1291
1422
|
template = stack.template
|
|
1292
1423
|
else:
|
|
1293
1424
|
template_body = request.get("TemplateBody")
|
|
@@ -1309,6 +1440,11 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
1309
1440
|
template = template_preparer.parse_template(template_body)
|
|
1310
1441
|
|
|
1311
1442
|
id_summaries = defaultdict(list)
|
|
1443
|
+
if "Resources" not in template:
|
|
1444
|
+
raise ValidationError(
|
|
1445
|
+
"Template format error: At least one Resources member must be defined."
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1312
1448
|
for resource_id, resource in template["Resources"].items():
|
|
1313
1449
|
res_type = resource["Type"]
|
|
1314
1450
|
id_summaries[res_type].append(resource_id)
|
|
@@ -1412,6 +1548,14 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
1412
1548
|
raise RuntimeError("Multiple stacks matched, update matching logic")
|
|
1413
1549
|
stack = active_stack_candidates[0]
|
|
1414
1550
|
|
|
1551
|
+
if (
|
|
1552
|
+
stack.status == StackStatus.DELETE_COMPLETE
|
|
1553
|
+
or stack.status == StackStatus.DELETE_IN_PROGRESS
|
|
1554
|
+
):
|
|
1555
|
+
raise ValidationError(
|
|
1556
|
+
f"Stack:{stack.stack_id} is in {stack.status} state and can not be updated."
|
|
1557
|
+
)
|
|
1558
|
+
|
|
1415
1559
|
# TODO: proper status modeling
|
|
1416
1560
|
before_parameters = stack.resolved_parameters
|
|
1417
1561
|
# TODO: reconsider the way parameters are modelled in the update graph process.
|
|
@@ -1559,6 +1703,7 @@ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
|
|
|
1559
1703
|
stack.set_stack_status(StackStatus.DELETE_FAILED)
|
|
1560
1704
|
|
|
1561
1705
|
start_worker_thread(_run)
|
|
1706
|
+
return ExecuteChangeSetOutput()
|
|
1562
1707
|
|
|
1563
1708
|
@handler("ListExports")
|
|
1564
1709
|
def list_exports(
|
|
@@ -17,11 +17,15 @@ class EngineParameter(TypedDict):
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def engine_parameter_value(parameter: EngineParameter) -> str:
|
|
20
|
-
|
|
21
|
-
if
|
|
22
|
-
|
|
20
|
+
given_value = parameter.get("given_value")
|
|
21
|
+
if given_value is not None:
|
|
22
|
+
return given_value
|
|
23
23
|
|
|
24
|
-
|
|
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")
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
class ResolvedResource(TypedDict):
|
|
@@ -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
|
#
|