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.
Files changed (173) hide show
  1. localstack/aws/api/cloudformation/__init__.py +1 -0
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +4 -0
  5. localstack/aws/api/ec2/__init__.py +1113 -56
  6. localstack/aws/api/iam/__init__.py +7 -0
  7. localstack/aws/api/kinesis/__init__.py +19 -0
  8. localstack/aws/api/kms/__init__.py +6 -0
  9. localstack/aws/api/lambda_/__init__.py +13 -0
  10. localstack/aws/api/logs/__init__.py +15 -0
  11. localstack/aws/api/redshift/__init__.py +9 -3
  12. localstack/aws/api/route53/__init__.py +2 -0
  13. localstack/aws/api/s3/__init__.py +12 -0
  14. localstack/aws/api/s3control/__init__.py +32 -0
  15. localstack/aws/api/ssm/__init__.py +2 -0
  16. localstack/aws/client.py +7 -2
  17. localstack/aws/forwarder.py +52 -5
  18. localstack/aws/handlers/analytics.py +1 -1
  19. localstack/aws/handlers/logging.py +12 -2
  20. localstack/aws/handlers/metric_handler.py +41 -1
  21. localstack/aws/handlers/service.py +32 -9
  22. localstack/aws/protocol/parser.py +440 -21
  23. localstack/aws/protocol/serializer.py +684 -64
  24. localstack/aws/protocol/service_router.py +120 -20
  25. localstack/aws/skeleton.py +4 -2
  26. localstack/aws/spec-patches.json +58 -0
  27. localstack/aws/spec.py +33 -13
  28. localstack/cli/exceptions.py +1 -1
  29. localstack/cli/localstack.py +4 -4
  30. localstack/cli/lpm.py +3 -4
  31. localstack/cli/profiles.py +1 -2
  32. localstack/config.py +18 -12
  33. localstack/constants.py +4 -29
  34. localstack/dev/kubernetes/__main__.py +1 -1
  35. localstack/dev/run/paths.py +1 -1
  36. localstack/dns/plugins.py +5 -1
  37. localstack/dns/server.py +12 -3
  38. localstack/packages/api.py +9 -8
  39. localstack/packages/core.py +2 -2
  40. localstack/packages/plugins.py +0 -8
  41. localstack/runtime/init.py +1 -1
  42. localstack/services/apigateway/legacy/provider.py +53 -3
  43. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  44. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  45. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  46. localstack/services/apigateway/next_gen/provider.py +5 -0
  47. localstack/services/cloudformation/engine/entities.py +12 -1
  48. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  49. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  50. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  52. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  53. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  54. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  55. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  56. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  57. localstack/services/cloudformation/resource_provider.py +5 -1
  58. localstack/services/cloudformation/resources.py +24149 -0
  59. localstack/services/cloudformation/v2/entities.py +6 -3
  60. localstack/services/cloudformation/v2/provider.py +172 -27
  61. localstack/services/cloudformation/v2/types.py +8 -4
  62. localstack/services/cloudwatch/provider_v2.py +25 -28
  63. localstack/services/dynamodb/packages.py +2 -1
  64. localstack/services/dynamodb/provider.py +42 -0
  65. localstack/services/dynamodb/v2/provider.py +42 -0
  66. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  67. localstack/services/es/provider.py +2 -2
  68. localstack/services/events/event_rule_engine.py +31 -13
  69. localstack/services/events/models.py +4 -5
  70. localstack/services/events/target.py +17 -9
  71. localstack/services/iam/provider.py +11 -116
  72. localstack/services/iam/resources/policy_simulator.py +133 -0
  73. localstack/services/kinesis/models.py +15 -2
  74. localstack/services/kinesis/provider.py +77 -0
  75. localstack/services/kms/provider.py +14 -5
  76. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  77. localstack/services/lambda_/packages.py +1 -1
  78. localstack/services/logs/provider.py +1 -1
  79. localstack/services/moto.py +2 -1
  80. localstack/services/opensearch/cluster.py +15 -7
  81. localstack/services/opensearch/packages.py +26 -7
  82. localstack/services/opensearch/provider.py +6 -1
  83. localstack/services/opensearch/versions.py +56 -7
  84. localstack/services/s3/constants.py +5 -2
  85. localstack/services/s3/cors.py +4 -4
  86. localstack/services/s3/notifications.py +1 -1
  87. localstack/services/s3/presigned_url.py +27 -43
  88. localstack/services/s3/provider.py +67 -11
  89. localstack/services/s3/utils.py +42 -11
  90. localstack/services/ses/provider.py +16 -7
  91. localstack/services/sns/constants.py +7 -1
  92. localstack/services/sns/v2/models.py +167 -0
  93. localstack/services/sns/v2/provider.py +860 -2
  94. localstack/services/sns/v2/utils.py +130 -0
  95. localstack/services/sqs/developer_api.py +205 -0
  96. localstack/services/sqs/models.py +42 -3
  97. localstack/services/sqs/provider.py +8 -309
  98. localstack/services/sqs/query_api.py +1 -1
  99. localstack/services/sqs/utils.py +121 -2
  100. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  101. localstack/testing/aws/cloudformation_utils.py +1 -1
  102. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  103. localstack/testing/pytest/container.py +4 -5
  104. localstack/testing/pytest/fixtures.py +20 -19
  105. localstack/testing/pytest/in_memory_localstack.py +0 -4
  106. localstack/testing/pytest/marking.py +13 -4
  107. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  108. localstack/testing/pytest/util.py +1 -1
  109. localstack/testing/pytest/validation_tracking.py +1 -2
  110. localstack/testing/snapshots/transformer_utility.py +5 -0
  111. localstack/utils/analytics/events.py +2 -2
  112. localstack/utils/analytics/metadata.py +1 -2
  113. localstack/utils/analytics/metrics/counter.py +6 -8
  114. localstack/utils/analytics/publisher.py +1 -2
  115. localstack/utils/analytics/service_request_aggregator.py +2 -2
  116. localstack/utils/archives.py +11 -11
  117. localstack/utils/aws/arns.py +17 -9
  118. localstack/utils/aws/aws_responses.py +7 -7
  119. localstack/utils/aws/aws_stack.py +2 -3
  120. localstack/utils/aws/message_forwarding.py +1 -2
  121. localstack/utils/aws/request_context.py +4 -5
  122. localstack/utils/batch_policy.py +3 -3
  123. localstack/utils/bootstrap.py +7 -7
  124. localstack/utils/catalog/catalog.py +139 -0
  125. localstack/utils/catalog/catalog_loader.py +11 -0
  126. localstack/utils/catalog/common.py +58 -0
  127. localstack/utils/catalog/plugins.py +28 -0
  128. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  129. localstack/utils/collections.py +7 -8
  130. localstack/utils/config_listener.py +1 -1
  131. localstack/utils/container_networking.py +2 -3
  132. localstack/utils/container_utils/container_client.py +115 -131
  133. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  134. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  135. localstack/utils/diagnose.py +2 -3
  136. localstack/utils/docker_utils.py +3 -4
  137. localstack/utils/files.py +31 -7
  138. localstack/utils/functions.py +3 -2
  139. localstack/utils/http.py +4 -5
  140. localstack/utils/json.py +19 -5
  141. localstack/utils/kinesis/kinesis_connector.py +2 -1
  142. localstack/utils/net.py +6 -6
  143. localstack/utils/no_exit_argument_parser.py +2 -2
  144. localstack/utils/numbers.py +9 -2
  145. localstack/utils/objects.py +6 -5
  146. localstack/utils/patch.py +2 -1
  147. localstack/utils/run.py +10 -9
  148. localstack/utils/scheduler.py +11 -11
  149. localstack/utils/server/tcp_proxy.py +2 -2
  150. localstack/utils/serving.py +2 -3
  151. localstack/utils/strings.py +10 -11
  152. localstack/utils/sync.py +126 -1
  153. localstack/utils/tagging.py +1 -4
  154. localstack/utils/testutil.py +5 -4
  155. localstack/utils/threads.py +2 -2
  156. localstack/utils/time.py +11 -3
  157. localstack/utils/urls.py +1 -3
  158. localstack/version.py +2 -2
  159. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
  160. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
  161. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
  162. localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
  163. localstack/packages/terraform.py +0 -46
  164. localstack/services/cloudformation/deploy.html +0 -144
  165. localstack/services/cloudformation/deploy_ui.py +0 -47
  166. localstack/services/cloudformation/plugins.py +0 -12
  167. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  168. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
  169. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
  170. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
  171. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
  172. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
  173. {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
- StackIdentifier,
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=StackIdentifier(
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, parameters: dict | None, account_id: str, region_name: str
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
- raise ValidationError(
281
- f"Parameter {name} should either have input value or default value"
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 stack.status not in [StackStatus.CREATE_COMPLETE, StackStatus.UPDATE_COMPLETE]:
536
- stack.set_stack_status(StackStatus.REVIEW_IN_PROGRESS, "User Initiated")
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
- change_set.set_change_set_status(ChangeSetStatus.CREATE_COMPLETE)
579
+ change_set.set_change_set_status(ChangeSetStatus.CREATE_COMPLETE)
539
580
 
540
- stack.change_set_ids.append(change_set.change_set_id)
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
- change_set.stack.change_set_ids.remove(change_set.change_set_id)
718
- state.change_sets.pop(change_set.change_set_id)
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
- value = parameter.get("given_value") or parameter.get("default_value")
21
- if value is None:
22
- raise RuntimeError("Parameter value is None")
20
+ given_value = parameter.get("given_value")
21
+ if given_value is not None:
22
+ return given_value
23
23
 
24
- return value
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 ValidationError(CommonServiceException):
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 ValidationError(
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 ValidationError("Period must be 10, 30 or a multiple of 60")
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 ValidationError(
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 ValidationError(
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 InvalidParameterCombination(
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
- alarm_types: AlarmTypes = None,
827
- history_item_type: HistoryItemType = None,
828
- start_date: Timestamp = None,
829
- end_date: Timestamp = None,
830
- max_records: MaxRecords = None,
831
- next_token: NextToken = None,
832
- scan_by: ScanBy = None,
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
- DDBLOCAL_URL = "https://d1ni2b6xgvw0s0.cloudfront.net/v3.x/dynamodb_local_latest.zip"
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
  #