localstack-core 4.10.1.dev42__py3-none-any.whl → 4.12.1.dev18__py3-none-any.whl

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