localstack-core 4.7.1.dev49__py3-none-any.whl → 4.10.1.dev12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. localstack/aws/api/cloudformation/__init__.py +18 -4
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +6 -2
  5. localstack/aws/api/dynamodb/__init__.py +30 -0
  6. localstack/aws/api/ec2/__init__.py +1522 -65
  7. localstack/aws/api/iam/__init__.py +7 -0
  8. localstack/aws/api/kinesis/__init__.py +19 -0
  9. localstack/aws/api/kms/__init__.py +6 -0
  10. localstack/aws/api/lambda_/__init__.py +13 -0
  11. localstack/aws/api/logs/__init__.py +15 -0
  12. localstack/aws/api/redshift/__init__.py +9 -3
  13. localstack/aws/api/route53/__init__.py +5 -0
  14. localstack/aws/api/s3/__init__.py +12 -0
  15. localstack/aws/api/s3control/__init__.py +54 -0
  16. localstack/aws/api/ssm/__init__.py +2 -0
  17. localstack/aws/api/transcribe/__init__.py +17 -0
  18. localstack/aws/client.py +7 -2
  19. localstack/aws/forwarder.py +52 -5
  20. localstack/aws/handlers/analytics.py +1 -1
  21. localstack/aws/handlers/internal_requests.py +6 -1
  22. localstack/aws/handlers/logging.py +12 -2
  23. localstack/aws/handlers/metric_handler.py +41 -1
  24. localstack/aws/handlers/service.py +40 -20
  25. localstack/aws/mocking.py +2 -2
  26. localstack/aws/patches.py +2 -2
  27. localstack/aws/protocol/parser.py +459 -32
  28. localstack/aws/protocol/serializer.py +689 -69
  29. localstack/aws/protocol/service_router.py +120 -20
  30. localstack/aws/protocol/validate.py +1 -1
  31. localstack/aws/scaffold.py +1 -1
  32. localstack/aws/skeleton.py +4 -2
  33. localstack/aws/spec-patches.json +58 -0
  34. localstack/aws/spec.py +37 -16
  35. localstack/cli/exceptions.py +1 -1
  36. localstack/cli/localstack.py +6 -6
  37. localstack/cli/lpm.py +3 -4
  38. localstack/cli/plugins.py +1 -1
  39. localstack/cli/profiles.py +1 -2
  40. localstack/config.py +25 -18
  41. localstack/constants.py +4 -29
  42. localstack/dev/kubernetes/__main__.py +130 -7
  43. localstack/dev/run/configurators.py +1 -4
  44. localstack/dev/run/paths.py +1 -1
  45. localstack/dns/plugins.py +5 -1
  46. localstack/dns/server.py +13 -4
  47. localstack/logging/format.py +3 -3
  48. localstack/packages/api.py +9 -8
  49. localstack/packages/core.py +2 -2
  50. localstack/packages/plugins.py +0 -8
  51. localstack/runtime/analytics.py +3 -0
  52. localstack/runtime/hooks.py +1 -1
  53. localstack/runtime/init.py +2 -2
  54. localstack/runtime/main.py +5 -5
  55. localstack/runtime/patches.py +2 -2
  56. localstack/services/apigateway/helpers.py +1 -4
  57. localstack/services/apigateway/legacy/helpers.py +7 -8
  58. localstack/services/apigateway/legacy/integration.py +4 -3
  59. localstack/services/apigateway/legacy/invocations.py +6 -5
  60. localstack/services/apigateway/legacy/provider.py +148 -68
  61. localstack/services/apigateway/legacy/templates.py +1 -1
  62. localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
  63. localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
  64. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  65. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  66. localstack/services/apigateway/next_gen/execute_api/template_mapping.py +2 -2
  67. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
  68. localstack/services/apigateway/next_gen/provider.py +5 -0
  69. localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
  70. localstack/services/cloudformation/api_utils.py +4 -8
  71. localstack/services/cloudformation/cfn_utils.py +1 -1
  72. localstack/services/cloudformation/engine/entities.py +14 -4
  73. localstack/services/cloudformation/engine/template_deployer.py +6 -4
  74. localstack/services/cloudformation/engine/transformers.py +6 -4
  75. localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
  76. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
  77. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
  78. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
  79. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
  80. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
  81. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  82. localstack/services/cloudformation/engine/v2/resolving.py +7 -5
  83. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  84. localstack/services/cloudformation/provider.py +7 -5
  85. localstack/services/cloudformation/resource_provider.py +7 -1
  86. localstack/services/cloudformation/resources.py +24149 -0
  87. localstack/services/cloudformation/service_models.py +2 -2
  88. localstack/services/cloudformation/v2/entities.py +19 -9
  89. localstack/services/cloudformation/v2/provider.py +336 -106
  90. localstack/services/cloudformation/v2/types.py +13 -7
  91. localstack/services/cloudformation/v2/utils.py +4 -1
  92. localstack/services/cloudwatch/alarm_scheduler.py +4 -1
  93. localstack/services/cloudwatch/provider.py +18 -13
  94. localstack/services/cloudwatch/provider_v2.py +25 -28
  95. localstack/services/dynamodb/packages.py +2 -1
  96. localstack/services/dynamodb/provider.py +42 -0
  97. localstack/services/dynamodb/server.py +2 -2
  98. localstack/services/dynamodb/v2/provider.py +42 -0
  99. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  100. localstack/services/edge.py +1 -1
  101. localstack/services/es/provider.py +2 -2
  102. localstack/services/events/event_rule_engine.py +31 -13
  103. localstack/services/events/models.py +4 -5
  104. localstack/services/events/provider.py +17 -14
  105. localstack/services/events/target.py +17 -9
  106. localstack/services/events/v1/provider.py +5 -5
  107. localstack/services/firehose/provider.py +14 -4
  108. localstack/services/iam/provider.py +11 -116
  109. localstack/services/iam/resources/policy_simulator.py +133 -0
  110. localstack/services/kinesis/models.py +15 -2
  111. localstack/services/kinesis/provider.py +86 -3
  112. localstack/services/kms/provider.py +14 -5
  113. localstack/services/lambda_/api_utils.py +6 -3
  114. localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
  115. localstack/services/lambda_/invocation/event_manager.py +1 -1
  116. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  117. localstack/services/lambda_/invocation/lambda_models.py +10 -7
  118. localstack/services/lambda_/invocation/lambda_service.py +5 -1
  119. localstack/services/lambda_/packages.py +1 -1
  120. localstack/services/lambda_/provider.py +4 -3
  121. localstack/services/lambda_/provider_utils.py +1 -1
  122. localstack/services/logs/provider.py +36 -19
  123. localstack/services/moto.py +2 -1
  124. localstack/services/opensearch/cluster.py +15 -7
  125. localstack/services/opensearch/packages.py +26 -7
  126. localstack/services/opensearch/provider.py +8 -2
  127. localstack/services/opensearch/versions.py +56 -7
  128. localstack/services/plugins.py +11 -7
  129. localstack/services/providers.py +10 -2
  130. localstack/services/redshift/provider.py +0 -21
  131. localstack/services/s3/constants.py +5 -2
  132. localstack/services/s3/cors.py +4 -4
  133. localstack/services/s3/models.py +1 -1
  134. localstack/services/s3/notifications.py +55 -39
  135. localstack/services/s3/presigned_url.py +35 -54
  136. localstack/services/s3/provider.py +73 -15
  137. localstack/services/s3/utils.py +42 -22
  138. localstack/services/s3/validation.py +46 -32
  139. localstack/services/s3/website_hosting.py +4 -2
  140. localstack/services/ses/provider.py +18 -8
  141. localstack/services/sns/constants.py +7 -1
  142. localstack/services/sns/executor.py +9 -2
  143. localstack/services/sns/provider.py +8 -5
  144. localstack/services/sns/publisher.py +31 -16
  145. localstack/services/sns/v2/models.py +167 -0
  146. localstack/services/sns/v2/provider.py +867 -0
  147. localstack/services/sns/v2/utils.py +130 -0
  148. localstack/services/sqs/constants.py +1 -1
  149. localstack/services/sqs/developer_api.py +205 -0
  150. localstack/services/sqs/models.py +48 -5
  151. localstack/services/sqs/provider.py +38 -311
  152. localstack/services/sqs/query_api.py +6 -2
  153. localstack/services/sqs/utils.py +121 -2
  154. localstack/services/ssm/provider.py +1 -1
  155. localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
  156. localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
  157. localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
  158. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
  159. localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
  160. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
  161. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
  162. localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
  163. localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
  164. localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
  165. localstack/services/stepfunctions/asl/eval/environment.py +1 -1
  166. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  167. localstack/services/stepfunctions/backend/execution.py +2 -1
  168. localstack/services/stores.py +1 -1
  169. localstack/services/transcribe/provider.py +6 -1
  170. localstack/state/codecs.py +61 -0
  171. localstack/state/core.py +11 -5
  172. localstack/state/pickle.py +10 -49
  173. localstack/testing/aws/cloudformation_utils.py +1 -1
  174. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  175. localstack/testing/pytest/cloudformation/transformers.py +0 -0
  176. localstack/testing/pytest/container.py +4 -5
  177. localstack/testing/pytest/fixtures.py +33 -31
  178. localstack/testing/pytest/in_memory_localstack.py +0 -4
  179. localstack/testing/pytest/marking.py +38 -11
  180. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  181. localstack/testing/pytest/util.py +1 -1
  182. localstack/testing/pytest/validation_tracking.py +1 -2
  183. localstack/testing/snapshots/transformer_utility.py +6 -1
  184. localstack/utils/analytics/events.py +2 -2
  185. localstack/utils/analytics/metadata.py +6 -4
  186. localstack/utils/analytics/metrics/counter.py +8 -15
  187. localstack/utils/analytics/publisher.py +1 -2
  188. localstack/utils/analytics/service_providers.py +19 -0
  189. localstack/utils/analytics/service_request_aggregator.py +2 -2
  190. localstack/utils/archives.py +11 -11
  191. localstack/utils/asyncio.py +2 -2
  192. localstack/utils/aws/arns.py +24 -29
  193. localstack/utils/aws/aws_responses.py +8 -8
  194. localstack/utils/aws/aws_stack.py +2 -3
  195. localstack/utils/aws/dead_letter_queue.py +1 -5
  196. localstack/utils/aws/message_forwarding.py +1 -2
  197. localstack/utils/aws/request_context.py +4 -5
  198. localstack/utils/aws/resources.py +1 -1
  199. localstack/utils/aws/templating.py +1 -1
  200. localstack/utils/batch_policy.py +3 -3
  201. localstack/utils/bootstrap.py +21 -13
  202. localstack/utils/catalog/catalog.py +139 -0
  203. localstack/utils/catalog/catalog_loader.py +119 -0
  204. localstack/utils/catalog/common.py +58 -0
  205. localstack/utils/catalog/plugins.py +28 -0
  206. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  207. localstack/utils/collections.py +7 -8
  208. localstack/utils/config_listener.py +1 -1
  209. localstack/utils/container_networking.py +2 -3
  210. localstack/utils/container_utils/container_client.py +135 -136
  211. localstack/utils/container_utils/docker_cmd_client.py +85 -69
  212. localstack/utils/container_utils/docker_sdk_client.py +69 -66
  213. localstack/utils/crypto.py +10 -10
  214. localstack/utils/diagnose.py +3 -4
  215. localstack/utils/docker_utils.py +9 -5
  216. localstack/utils/files.py +33 -13
  217. localstack/utils/functions.py +4 -3
  218. localstack/utils/http.py +11 -11
  219. localstack/utils/json.py +20 -6
  220. localstack/utils/kinesis/kinesis_connector.py +2 -1
  221. localstack/utils/net.py +15 -9
  222. localstack/utils/no_exit_argument_parser.py +2 -2
  223. localstack/utils/numbers.py +9 -2
  224. localstack/utils/objects.py +7 -6
  225. localstack/utils/patch.py +10 -3
  226. localstack/utils/run.py +12 -11
  227. localstack/utils/scheduler.py +11 -11
  228. localstack/utils/server/tcp_proxy.py +2 -2
  229. localstack/utils/serving.py +3 -4
  230. localstack/utils/strings.py +15 -16
  231. localstack/utils/sync.py +126 -1
  232. localstack/utils/tagging.py +8 -6
  233. localstack/utils/testutil.py +8 -8
  234. localstack/utils/threads.py +2 -2
  235. localstack/utils/time.py +12 -4
  236. localstack/utils/urls.py +1 -3
  237. localstack/utils/xray/traceid.py +1 -1
  238. localstack/version.py +16 -3
  239. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
  240. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
  241. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
  242. localstack_core-4.10.1.dev12.dist-info/plux.json +1 -0
  243. localstack/packages/terraform.py +0 -46
  244. localstack/services/cloudformation/deploy.html +0 -144
  245. localstack/services/cloudformation/deploy_ui.py +0 -47
  246. localstack/services/cloudformation/plugins.py +0 -12
  247. localstack_core-4.7.1.dev49.dist-info/plux.json +0 -1
  248. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
  249. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
  250. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
  251. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
  252. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
  253. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
@@ -1,8 +1,10 @@
1
1
  import copy
2
2
  import json
3
3
  import logging
4
+ import re
4
5
  from collections import defaultdict
5
6
  from datetime import UTC, datetime
7
+ from urllib.parse import urlencode
6
8
 
7
9
  from localstack import config
8
10
  from localstack.aws.api import RequestContext, handler
@@ -13,6 +15,7 @@ from localstack.aws.api.cloudformation import (
13
15
  ChangeSetNameOrId,
14
16
  ChangeSetNotFoundException,
15
17
  ChangeSetStatus,
18
+ ChangeSetSummary,
16
19
  ChangeSetType,
17
20
  ClientRequestToken,
18
21
  CreateChangeSetInput,
@@ -44,6 +47,7 @@ from localstack.aws.api.cloudformation import (
44
47
  IncludePropertyValues,
45
48
  InsufficientCapabilitiesException,
46
49
  InvalidChangeSetStatusException,
50
+ ListChangeSetsOutput,
47
51
  ListExportsOutput,
48
52
  ListStackResourcesOutput,
49
53
  ListStacksOutput,
@@ -51,6 +55,7 @@ from localstack.aws.api.cloudformation import (
51
55
  NextToken,
52
56
  Parameter,
53
57
  PhysicalResourceId,
58
+ ResourceStatus,
54
59
  RetainExceptOnCreate,
55
60
  RetainResources,
56
61
  RoleARN,
@@ -80,6 +85,7 @@ from localstack.aws.connect import connect_to
80
85
  from localstack.services.cloudformation import api_utils
81
86
  from localstack.services.cloudformation.engine import template_preparer
82
87
  from localstack.services.cloudformation.engine.parameters import resolve_ssm_parameter
88
+ from localstack.services.cloudformation.engine.transformers import FailedTransformationException
83
89
  from localstack.services.cloudformation.engine.v2.change_set_model import (
84
90
  ChangeSetModel,
85
91
  ChangeType,
@@ -114,13 +120,19 @@ from localstack.services.cloudformation.v2.entities import (
114
120
  StackInstance,
115
121
  StackSet,
116
122
  )
117
- from localstack.services.cloudformation.v2.types import EngineParameter
123
+ from localstack.services.cloudformation.v2.types import EngineParameter, engine_parameter_value
124
+ from localstack.services.plugins import ServiceLifecycleHook
118
125
  from localstack.utils.collections import select_attributes
126
+ from localstack.utils.numbers import is_number
119
127
  from localstack.utils.strings import short_uid
120
128
  from localstack.utils.threads import start_worker_thread
121
129
 
122
130
  LOG = logging.getLogger(__name__)
123
131
 
132
+ SSM_PARAMETER_TYPE_RE = re.compile(
133
+ r"^AWS::SSM::Parameter::Value<(?P<listtype>List<)?(?P<innertype>[^>]+)>?>$"
134
+ )
135
+
124
136
 
125
137
  def is_stack_arn(stack_name_or_id: str) -> bool:
126
138
  return ARN_STACK_REGEX.match(stack_name_or_id) is not None
@@ -208,10 +220,35 @@ def find_stack_instance(stack_set: StackSet, account: str, region: str) -> Stack
208
220
  return None
209
221
 
210
222
 
211
- class CloudformationProviderV2(CloudformationProvider):
223
+ class CloudformationProviderV2(CloudformationProvider, ServiceLifecycleHook):
224
+ def on_before_start(self):
225
+ base = "https://github.com/localstack/localstack/issues/new"
226
+ query_args = {
227
+ "template": "bug-report.yml",
228
+ "labels": ",".join(
229
+ [
230
+ "aws:cloudformation:v2",
231
+ "status: triage needed",
232
+ "type: bug",
233
+ ]
234
+ ),
235
+ "title": "CFNV2: ",
236
+ }
237
+ issue_url = "?".join([base, urlencode(query_args)])
238
+ LOG.info(
239
+ "You have opted in to the new CloudFormation deployment engine. "
240
+ "You can opt in to using the old engine by setting PROVIDER_OVERRIDE_CLOUDFORMATION=engine-legacy. "
241
+ "If you experience issues, please submit a bug report at this URL: %s",
242
+ issue_url,
243
+ )
244
+
212
245
  @staticmethod
213
246
  def _resolve_parameters(
214
- 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,
215
252
  ) -> dict[str, EngineParameter]:
216
253
  template_parameters = template.get("Parameters", {})
217
254
  resolved_parameters = {}
@@ -220,19 +257,57 @@ class CloudformationProviderV2(CloudformationProvider):
220
257
  given_value = parameters.get(name)
221
258
  default_value = parameter.get("Default")
222
259
  resolved_parameter = EngineParameter(
223
- type_=parameter["Type"], given_value=given_value, default_value=default_value
260
+ type_=parameter["Type"],
261
+ given_value=given_value,
262
+ default_value=default_value,
263
+ no_echo=parameter.get("NoEcho"),
224
264
  )
225
265
 
226
- if parameter["Type"] == "AWS::SSM::Parameter::Value<String>":
227
- # TODO: support other parameter types
228
- try:
229
- resolved_parameter["resolved_value"] = resolve_ssm_parameter(
230
- account_id, region_name, given_value or default_value
231
- )
232
- except Exception:
233
- raise ValidationError(
234
- f"Parameter {name} should either have input value or default value"
235
- )
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
+
272
+ # TODO: support other parameter types
273
+ if match := SSM_PARAMETER_TYPE_RE.match(parameter["Type"]):
274
+ inner_type = match.group("innertype")
275
+ is_list_type = match.group("listtype") is not None
276
+ if is_list_type or inner_type == "CommaDelimitedList":
277
+ # list types
278
+ try:
279
+ resolved_value = resolve_ssm_parameter(
280
+ account_id, region_name, given_value or default_value
281
+ )
282
+ resolved_parameter["resolved_value"] = resolved_value.split(",")
283
+ except Exception:
284
+ raise ValidationError(
285
+ f"Parameter {name} should either have input value or default value"
286
+ )
287
+ else:
288
+ try:
289
+ resolved_parameter["resolved_value"] = resolve_ssm_parameter(
290
+ account_id, region_name, given_value or default_value
291
+ )
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
236
311
  elif given_value is None and default_value is None:
237
312
  invalid_parameters.append(name)
238
313
  continue
@@ -262,7 +337,7 @@ class CloudformationProviderV2(CloudformationProvider):
262
337
  after_template: dict | None,
263
338
  before_parameters: dict | None,
264
339
  after_parameters: dict | None,
265
- previous_update_model: UpdateModel | None,
340
+ previous_update_model: UpdateModel | None = None,
266
341
  ):
267
342
  resolved_parameters = None
268
343
  if after_parameters is not None:
@@ -271,6 +346,7 @@ class CloudformationProviderV2(CloudformationProvider):
271
346
  after_parameters,
272
347
  change_set.stack.account_id,
273
348
  change_set.stack.region_name,
349
+ before_parameters,
274
350
  )
275
351
 
276
352
  change_set.resolved_parameters = resolved_parameters
@@ -299,9 +375,18 @@ class CloudformationProviderV2(CloudformationProvider):
299
375
  before_template=before_template,
300
376
  after_template=after_template,
301
377
  )
302
- transformed_before_template, transformed_after_template = (
303
- change_set_model_transform.transform()
304
- )
378
+ try:
379
+ transformed_before_template, transformed_after_template = (
380
+ change_set_model_transform.transform()
381
+ )
382
+ except FailedTransformationException as e:
383
+ change_set.status = ChangeSetStatus.FAILED
384
+ change_set.status_reason = e.message
385
+ change_set.stack.set_stack_status(
386
+ status=StackStatus.ROLLBACK_IN_PROGRESS, reason=e.message
387
+ )
388
+ change_set.stack.set_stack_status(status=StackStatus.CREATE_FAILED)
389
+ return
305
390
 
306
391
  # Remodel the update graph after the applying the global transforms.
307
392
  change_set_model = ChangeSetModel(
@@ -317,6 +402,7 @@ class CloudformationProviderV2(CloudformationProvider):
317
402
  # the transformations.
318
403
  update_model.before_runtime_cache.update(raw_update_model.before_runtime_cache)
319
404
  update_model.after_runtime_cache.update(raw_update_model.after_runtime_cache)
405
+ change_set.set_update_model(update_model)
320
406
 
321
407
  # perform validations
322
408
  validator = ChangeSetModelValidator(
@@ -324,16 +410,19 @@ class CloudformationProviderV2(CloudformationProvider):
324
410
  )
325
411
  validator.validate()
326
412
 
327
- change_set.set_update_model(update_model)
413
+ # hacky
414
+ if transform := raw_update_model.node_template.transform:
415
+ if transform.global_transforms:
416
+ # global transforms should always be considered "MODIFIED"
417
+ update_model.node_template.change_type = ChangeType.MODIFIED
328
418
  change_set.processed_template = transformed_after_template
329
419
 
330
420
  @handler("CreateChangeSet", expand=False)
331
421
  def create_change_set(
332
422
  self, context: RequestContext, request: CreateChangeSetInput
333
423
  ) -> CreateChangeSetOutput:
334
- try:
335
- stack_name = request["StackName"]
336
- except KeyError:
424
+ stack_name = request.get("StackName")
425
+ if not stack_name:
337
426
  # TODO: proper exception
338
427
  raise ValidationError("StackName must be specified")
339
428
  try:
@@ -363,12 +452,19 @@ class CloudformationProviderV2(CloudformationProvider):
363
452
  template_body = api_utils.extract_template_body(request)
364
453
  structured_template = template_preparer.parse_template(template_body)
365
454
 
455
+ if len(template_body) > 51200 and not template_url:
456
+ raise ValidationError(
457
+ f"1 validation error detected: Value '{template_body}' at 'templateBody' "
458
+ "failed to satisfy constraint: Member must have length less than or equal to 51200"
459
+ )
460
+
366
461
  # this is intentionally not in a util yet. Let's first see how the different operations deal with these before generalizing
367
462
  # handle ARN stack_name here (not valid for initial CREATE, since stack doesn't exist yet)
368
463
  if is_stack_arn(stack_name):
369
464
  stack = state.stacks_v2.get(stack_name)
370
465
  if not stack:
371
466
  raise ValidationError(f"Stack '{stack_name}' does not exist.")
467
+ stack.capabilities = request.get("Capabilities") or []
372
468
  else:
373
469
  # stack name specified, so fetch the stack by name
374
470
  stack_candidates: list[Stack] = [
@@ -389,6 +485,8 @@ class CloudformationProviderV2(CloudformationProvider):
389
485
  if not active_stack_candidates:
390
486
  raise ValidationError(f"Stack '{stack_name}' does not exist.")
391
487
  stack = active_stack_candidates[0]
488
+ # propagate capabilities from create change set request
489
+ stack.capabilities = request.get("Capabilities") or []
392
490
 
393
491
  # TODO: test if rollback status is allowed as well
394
492
  if (
@@ -399,6 +497,14 @@ class CloudformationProviderV2(CloudformationProvider):
399
497
  f"Stack [{stack_name}] already exists and cannot be created again with the changeSet [{change_set_name}]."
400
498
  )
401
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
+
402
508
  before_parameters: dict[str, Parameter] | None = None
403
509
  match change_set_type:
404
510
  case ChangeSetType.UPDATE:
@@ -445,7 +551,12 @@ class CloudformationProviderV2(CloudformationProvider):
445
551
  pass
446
552
 
447
553
  # create change set for the stack and apply changes
448
- change_set = ChangeSet(stack, request, template=after_template, template_body=template_body)
554
+ change_set = ChangeSet(
555
+ stack,
556
+ request,
557
+ template=after_template,
558
+ template_body=template_body,
559
+ )
449
560
  self._setup_change_set_model(
450
561
  change_set=change_set,
451
562
  before_template=before_template,
@@ -454,23 +565,21 @@ class CloudformationProviderV2(CloudformationProvider):
454
565
  after_parameters=after_parameters,
455
566
  previous_update_model=previous_update_model,
456
567
  )
457
-
458
- # TODO: handle the empty change set case
459
- if not change_set.has_changes():
460
- change_set.set_change_set_status(ChangeSetStatus.FAILED)
568
+ if change_set.status == ChangeSetStatus.FAILED:
461
569
  change_set.set_execution_status(ExecutionStatus.UNAVAILABLE)
462
- change_set.status_reason = "The submitted information didn't contain changes. Submit different information to create a change set."
463
570
  else:
464
- if stack.status in [StackStatus.CREATE_COMPLETE, StackStatus.UPDATE_COMPLETE]:
465
- stack.set_stack_status(StackStatus.UPDATE_IN_PROGRESS)
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."
466
575
  else:
467
- stack.set_stack_status(StackStatus.REVIEW_IN_PROGRESS)
576
+ if stack.status not in [StackStatus.CREATE_COMPLETE, StackStatus.UPDATE_COMPLETE]:
577
+ stack.set_stack_status(StackStatus.REVIEW_IN_PROGRESS, "User Initiated")
468
578
 
469
- change_set.set_change_set_status(ChangeSetStatus.CREATE_COMPLETE)
579
+ change_set.set_change_set_status(ChangeSetStatus.CREATE_COMPLETE)
470
580
 
471
- stack.change_set_ids.append(change_set.change_set_id)
581
+ stack.change_set_ids.add(change_set.change_set_id)
472
582
  state.change_sets[change_set.change_set_id] = change_set
473
-
474
583
  return CreateChangeSetOutput(StackId=stack.stack_id, Id=change_set.change_set_id)
475
584
 
476
585
  @handler("ExecuteChangeSet")
@@ -505,6 +614,8 @@ class CloudformationProviderV2(CloudformationProvider):
505
614
  raise RuntimeError("Programming error: no update graph found for change set")
506
615
 
507
616
  change_set.set_execution_status(ExecutionStatus.EXECUTE_IN_PROGRESS)
617
+ # propagate the tags as this is done during execution
618
+ change_set.stack.tags = change_set.tags
508
619
  change_set.stack.set_stack_status(
509
620
  StackStatus.UPDATE_IN_PROGRESS
510
621
  if change_set.change_set_type == ChangeSetType.UPDATE
@@ -516,15 +627,22 @@ class CloudformationProviderV2(CloudformationProvider):
516
627
  )
517
628
 
518
629
  def _run(*args):
519
- try:
520
- result = change_set_executor.execute()
630
+ # TODO: should this be cleared before or after execution?
631
+ change_set.stack.status_reason = None
632
+ result = change_set_executor.execute()
633
+ change_set.stack.resolved_parameters = change_set.resolved_parameters
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
+
640
+ if not result.failure_message:
521
641
  new_stack_status = StackStatus.UPDATE_COMPLETE
522
642
  if change_set.change_set_type == ChangeSetType.CREATE:
523
643
  new_stack_status = StackStatus.CREATE_COMPLETE
524
644
  change_set.stack.set_stack_status(new_stack_status)
525
645
  change_set.set_execution_status(ExecutionStatus.EXECUTE_COMPLETE)
526
- change_set.stack.resolved_resources = result.resources
527
- change_set.stack.resolved_parameters = change_set.resolved_parameters
528
646
  change_set.stack.resolved_outputs = result.outputs
529
647
 
530
648
  change_set.stack.resolved_exports = {}
@@ -533,36 +651,80 @@ class CloudformationProviderV2(CloudformationProvider):
533
651
  change_set.stack.resolved_exports[export_name] = output["OutputValue"]
534
652
 
535
653
  change_set.stack.change_set_id = change_set.change_set_id
536
- change_set.stack.change_set_ids.append(change_set.change_set_id)
537
-
538
- # if the deployment succeeded, update the stack's template representation to that
539
- # which was just deployed
540
- change_set.stack.template = change_set.template
541
- change_set.stack.description = change_set.template.get("Description")
542
- change_set.stack.processed_template = change_set.processed_template
543
- change_set.stack.template_body = change_set.template_body
544
- except Exception as e:
654
+ else:
545
655
  LOG.error(
546
656
  "Execute change set failed: %s",
547
- e,
657
+ result.failure_message,
548
658
  exc_info=LOG.isEnabledFor(logging.DEBUG) and config.CFN_VERBOSE_ERRORS,
549
659
  )
550
- new_stack_status = StackStatus.UPDATE_FAILED
551
- if change_set.change_set_type == ChangeSetType.CREATE:
552
- new_stack_status = StackStatus.CREATE_FAILED
553
-
554
- change_set.stack.set_stack_status(new_stack_status)
660
+ # stack status is taken care of in the executor
555
661
  change_set.set_execution_status(ExecutionStatus.EXECUTE_FAILED)
556
- change_set.stack.change_set_id = change_set.change_set_id
557
- change_set.stack.change_set_ids.append(change_set.change_set_id)
662
+ change_set.stack.deletion_time = datetime.now(tz=UTC)
558
663
 
559
664
  start_worker_thread(_run)
560
665
 
561
666
  return ExecuteChangeSetOutput()
562
667
 
563
- def _describe_change_set(
564
- self, change_set: ChangeSet, include_property_values: bool
668
+ @staticmethod
669
+ def _render_resolved_parameters(
670
+ resolved_parameters: dict[str, EngineParameter],
671
+ ) -> list[Parameter]:
672
+ result = []
673
+ for name, resolved_parameter in resolved_parameters.items():
674
+ parameter = Parameter(
675
+ ParameterKey=name,
676
+ ParameterValue=resolved_parameter.get("given_value")
677
+ or resolved_parameter.get("default_value"),
678
+ )
679
+ if resolved_value := resolved_parameter.get("resolved_value"):
680
+ parameter["ResolvedValue"] = resolved_value
681
+
682
+ # TODO :what happens to the resolved value?
683
+ if resolved_parameter.get("no_echo", False):
684
+ parameter["ParameterValue"] = "****"
685
+ result.append(parameter)
686
+
687
+ return result
688
+
689
+ @handler("DescribeChangeSet")
690
+ def describe_change_set(
691
+ self,
692
+ context: RequestContext,
693
+ change_set_name: ChangeSetNameOrId,
694
+ stack_name: StackNameOrId | None = None,
695
+ next_token: NextToken | None = None,
696
+ include_property_values: IncludePropertyValues | None = None,
697
+ **kwargs,
565
698
  ) -> DescribeChangeSetOutput:
699
+ # TODO add support for include_property_values
700
+ # only relevant if change_set_name isn't an ARN
701
+ state = get_cloudformation_store(context.account_id, context.region)
702
+ change_set = find_change_set_v2(state, change_set_name, stack_name)
703
+
704
+ if not change_set:
705
+ raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
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
+
566
728
  # TODO: The ChangeSetModelDescriber currently matches AWS behavior by listing
567
729
  # resource changes in the order they appear in the template. However, when
568
730
  # a resource change is triggered indirectly (e.g., via Ref or GetAtt), the
@@ -591,36 +753,49 @@ class CloudformationProviderV2(CloudformationProvider):
591
753
  # TODO: static information
592
754
  IncludeNestedStacks=False,
593
755
  NotificationARNs=[],
756
+ Tags=change_set.tags or None,
594
757
  )
595
758
  if change_set.resolved_parameters:
596
- result["Parameters"] = [
597
- # TODO: add masking support.
598
- Parameter(ParameterKey=key, ParameterValue=value)
599
- for (key, value) in change_set.resolved_parameters.items()
600
- ]
759
+ result["Parameters"] = self._render_resolved_parameters(change_set.resolved_parameters)
601
760
  return result
602
761
 
603
- @handler("DescribeChangeSet")
604
- def describe_change_set(
762
+ @handler("ListChangeSets")
763
+ def list_change_sets(
605
764
  self,
606
765
  context: RequestContext,
607
- change_set_name: ChangeSetNameOrId,
608
- stack_name: StackNameOrId | None = None,
609
- next_token: NextToken | None = None,
610
- include_property_values: IncludePropertyValues | None = None,
766
+ stack_name: StackNameOrId,
767
+ next_token: NextToken = None,
611
768
  **kwargs,
612
- ) -> DescribeChangeSetOutput:
613
- # TODO add support for include_property_values
614
- # only relevant if change_set_name isn't an ARN
615
- state = get_cloudformation_store(context.account_id, context.region)
616
- change_set = find_change_set_v2(state, change_set_name, stack_name)
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
617
782
 
618
- if not change_set:
619
- raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
620
- result = self._describe_change_set(
621
- change_set=change_set, include_property_values=include_property_values or False
622
- )
623
- return result
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)
624
799
 
625
800
  @handler("DeleteChangeSet")
626
801
  def delete_change_set(
@@ -635,8 +810,22 @@ class CloudformationProviderV2(CloudformationProvider):
635
810
  if not change_set:
636
811
  return DeleteChangeSetOutput()
637
812
 
638
- change_set.stack.change_set_ids.remove(change_set.change_set_id)
639
- 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
+ )
640
829
 
641
830
  return DeleteChangeSetOutput()
642
831
 
@@ -688,6 +877,12 @@ class CloudformationProviderV2(CloudformationProvider):
688
877
  template_body = api_utils.extract_template_body(request)
689
878
  structured_template = template_preparer.parse_template(template_body)
690
879
 
880
+ if len(template_body) > 51200 and not template_url:
881
+ raise ValidationError(
882
+ f"1 validation error detected: Value '{template_body}' at 'templateBody' "
883
+ "failed to satisfy constraint: Member must have length less than or equal to 51200"
884
+ )
885
+
691
886
  if "CAPABILITY_AUTO_EXPAND" not in request.get("Capabilities", []) and (
692
887
  "Transform" in structured_template.keys() or "Fn::Transform" in template_body
693
888
  ):
@@ -699,6 +894,7 @@ class CloudformationProviderV2(CloudformationProvider):
699
894
  account_id=context.account_id,
700
895
  region_name=context.region,
701
896
  request_payload=request,
897
+ tags=request.get("Tags"),
702
898
  )
703
899
  # TODO: what is the correct initial status?
704
900
  state.stacks_v2[stack.stack_id] = stack
@@ -727,6 +923,10 @@ class CloudformationProviderV2(CloudformationProvider):
727
923
  after_parameters=after_parameters,
728
924
  previous_update_model=None,
729
925
  )
926
+ if change_set.status == ChangeSetStatus.FAILED:
927
+ return CreateStackOutput(StackId=stack.stack_id)
928
+
929
+ stack.processed_template = change_set.processed_template
730
930
 
731
931
  # deployment process
732
932
  stack.set_stack_status(StackStatus.CREATE_IN_PROGRESS)
@@ -735,19 +935,26 @@ class CloudformationProviderV2(CloudformationProvider):
735
935
  def _run(*args):
736
936
  try:
737
937
  result = change_set_executor.execute()
738
- stack.set_stack_status(StackStatus.CREATE_COMPLETE)
739
938
  stack.resolved_resources = result.resources
740
939
  stack.resolved_outputs = result.outputs
940
+ if all(
941
+ resource["ResourceStatus"] == ResourceStatus.CREATE_COMPLETE
942
+ for resource in stack.resolved_resources.values()
943
+ ):
944
+ stack.set_stack_status(StackStatus.CREATE_COMPLETE)
945
+ else:
946
+ stack.set_stack_status(StackStatus.CREATE_FAILED)
947
+
741
948
  # if the deployment succeeded, update the stack's template representation to that
742
949
  # which was just deployed
743
950
  stack.template = change_set.template
744
951
  stack.template_body = change_set.template_body
952
+ stack.processed_template = change_set.processed_template
745
953
  stack.resolved_parameters = change_set.resolved_parameters
746
954
  stack.resolved_exports = {}
747
955
  for output in result.outputs:
748
956
  if export_name := output.get("ExportName"):
749
957
  stack.resolved_exports[export_name] = output["OutputValue"]
750
- stack.processed_template = change_set.processed_template
751
958
  except Exception as e:
752
959
  LOG.error(
753
960
  "Create Stack set failed: %s",
@@ -793,12 +1000,10 @@ class CloudformationProviderV2(CloudformationProvider):
793
1000
 
794
1001
  return DescribeStacksOutput(Stacks=describe_stack_output)
795
1002
 
796
- @staticmethod
797
- def _describe_stack(stack: Stack) -> ApiStack:
1003
+ def _describe_stack(self, stack: Stack) -> ApiStack:
798
1004
  stack_description = ApiStack(
799
1005
  Description=stack.description,
800
1006
  CreationTime=stack.creation_time,
801
- DeletionTime=stack.deletion_time,
802
1007
  StackId=stack.stack_id,
803
1008
  StackName=stack.stack_name,
804
1009
  StackStatus=stack.status,
@@ -808,13 +1013,14 @@ class CloudformationProviderV2(CloudformationProvider):
808
1013
  DriftInformation=StackDriftInformation(StackDriftStatus=StackDriftStatus.NOT_CHECKED),
809
1014
  EnableTerminationProtection=stack.enable_termination_protection,
810
1015
  RollbackConfiguration=RollbackConfiguration(),
811
- Tags=[],
1016
+ Tags=stack.tags,
812
1017
  NotificationARNs=[],
813
- # "Parameters": stack.resolved_parameters,
814
1018
  )
815
1019
  if stack.status != StackStatus.REVIEW_IN_PROGRESS:
816
1020
  # TODO: actually track updated time
817
1021
  stack_description["LastUpdatedTime"] = stack.creation_time
1022
+ if stack.deletion_time:
1023
+ stack_description["DeletionTime"] = stack.deletion_time
818
1024
  if stack.capabilities:
819
1025
  stack_description["Capabilities"] = stack.capabilities
820
1026
  # TODO: confirm the logic for this
@@ -822,16 +1028,9 @@ class CloudformationProviderV2(CloudformationProvider):
822
1028
  stack_description["ChangeSetId"] = change_set_id
823
1029
 
824
1030
  if stack.resolved_parameters:
825
- stack_description["Parameters"] = []
826
- for name, resolved_parameter in stack.resolved_parameters.items():
827
- parameter = Parameter(
828
- ParameterKey=name,
829
- ParameterValue=resolved_parameter.get("given_value")
830
- or resolved_parameter.get("default_value"),
831
- )
832
- if resolved_value := resolved_parameter.get("resolved_value"):
833
- parameter["ResolvedValue"] = resolved_value
834
- stack_description["Parameters"].append(parameter)
1031
+ stack_description["Parameters"] = self._render_resolved_parameters(
1032
+ stack.resolved_parameters
1033
+ )
835
1034
 
836
1035
  if stack.resolved_outputs:
837
1036
  stack_description["Outputs"] = stack.resolved_outputs
@@ -910,6 +1109,12 @@ class CloudformationProviderV2(CloudformationProvider):
910
1109
 
911
1110
  try:
912
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
913
1118
  except KeyError:
914
1119
  raise ValidationError(
915
1120
  f"Resource {logical_resource_id} does not exist for stack {stack_name}"
@@ -923,6 +1128,7 @@ class CloudformationProviderV2(CloudformationProvider):
923
1128
  ResourceType=resource["Type"],
924
1129
  LastUpdatedTimestamp=resource["LastUpdatedTimestamp"],
925
1130
  ResourceStatus=resource["ResourceStatus"],
1131
+ DriftInformation={"StackResourceDriftStatus": "NOT_CHECKED"},
926
1132
  )
927
1133
  return DescribeStackResourceOutput(StackResourceDetail=resource_detail)
928
1134
 
@@ -1168,10 +1374,19 @@ class CloudformationProviderV2(CloudformationProvider):
1168
1374
  ) -> GetTemplateOutput:
1169
1375
  state = get_cloudformation_store(context.account_id, context.region)
1170
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
+
1171
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")
1172
1383
  stack = change_set.stack
1173
1384
  elif stack_name:
1174
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
+ )
1175
1390
  else:
1176
1391
  raise StackNotFoundError(stack_name)
1177
1392
 
@@ -1198,6 +1413,12 @@ class CloudformationProviderV2(CloudformationProvider):
1198
1413
  stack = find_stack_v2(state, stack_name)
1199
1414
  if not stack:
1200
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
+
1201
1422
  template = stack.template
1202
1423
  else:
1203
1424
  template_body = request.get("TemplateBody")
@@ -1219,6 +1440,11 @@ class CloudformationProviderV2(CloudformationProvider):
1219
1440
  template = template_preparer.parse_template(template_body)
1220
1441
 
1221
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
+
1222
1448
  for resource_id, resource in template["Resources"].items():
1223
1449
  res_type = resource["Type"]
1224
1450
  id_summaries[res_type].append(resource_id)
@@ -1322,6 +1548,14 @@ class CloudformationProviderV2(CloudformationProvider):
1322
1548
  raise RuntimeError("Multiple stacks matched, update matching logic")
1323
1549
  stack = active_stack_candidates[0]
1324
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
+
1325
1559
  # TODO: proper status modeling
1326
1560
  before_parameters = stack.resolved_parameters
1327
1561
  # TODO: reconsider the way parameters are modelled in the update graph process.
@@ -1358,7 +1592,7 @@ class CloudformationProviderV2(CloudformationProvider):
1358
1592
  # TODO: some changes are only detectable at runtime; consider using
1359
1593
  # the ChangeSetModelDescriber, or a new custom visitors, to
1360
1594
  # pick-up on runtime changes.
1361
- if change_set.update_model.node_template.change_type == ChangeType.UNCHANGED:
1595
+ if not change_set.has_changes():
1362
1596
  raise ValidationError("No updates are to be performed.")
1363
1597
 
1364
1598
  stack.set_stack_status(StackStatus.UPDATE_IN_PROGRESS)
@@ -1438,10 +1672,7 @@ class CloudformationProviderV2(CloudformationProvider):
1438
1672
  stack.deletion_time = datetime.now(tz=UTC)
1439
1673
  return
1440
1674
 
1441
- previous_update_model = None
1442
- if stack.change_set_id:
1443
- if previous_change_set := find_change_set_v2(state, stack.change_set_id):
1444
- previous_update_model = previous_change_set.update_model
1675
+ stack.set_stack_status(StackStatus.DELETE_IN_PROGRESS)
1445
1676
 
1446
1677
  # create a dummy change set
1447
1678
  change_set = ChangeSet(
@@ -1449,18 +1680,16 @@ class CloudformationProviderV2(CloudformationProvider):
1449
1680
  ) # noqa
1450
1681
  self._setup_change_set_model(
1451
1682
  change_set=change_set,
1452
- before_template=stack.template,
1683
+ before_template=stack.processed_template,
1453
1684
  after_template=None,
1454
1685
  before_parameters=stack.resolved_parameters,
1455
1686
  after_parameters=None,
1456
- previous_update_model=previous_update_model,
1457
1687
  )
1458
1688
 
1459
1689
  change_set_executor = ChangeSetModelExecutor(change_set)
1460
1690
 
1461
1691
  def _run(*args):
1462
1692
  try:
1463
- stack.set_stack_status(StackStatus.DELETE_IN_PROGRESS)
1464
1693
  change_set_executor.execute()
1465
1694
  stack.set_stack_status(StackStatus.DELETE_COMPLETE)
1466
1695
  stack.deletion_time = datetime.now(tz=UTC)
@@ -1474,6 +1703,7 @@ class CloudformationProviderV2(CloudformationProvider):
1474
1703
  stack.set_stack_status(StackStatus.DELETE_FAILED)
1475
1704
 
1476
1705
  start_worker_thread(_run)
1706
+ return ExecuteChangeSetOutput()
1477
1707
 
1478
1708
  @handler("ListExports")
1479
1709
  def list_exports(