• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

localstack / localstack / 17265699519

27 Aug 2025 11:28AM UTC coverage: 86.827% (-0.01%) from 86.837%
17265699519

push

github

web-flow
Fix SQS tests failing due to missing snapshot update after #12957 (#13062)

67057 of 77231 relevant lines covered (86.83%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.0
/localstack-core/localstack/services/cloudformation/provider.py
1
import copy
1✔
2
import json
1✔
3
import logging
1✔
4
import re
1✔
5
from collections import defaultdict
1✔
6
from copy import deepcopy
1✔
7

8
from localstack.aws.api import CommonServiceException, RequestContext, handler
1✔
9
from localstack.aws.api.cloudformation import (
1✔
10
    AlreadyExistsException,
11
    CallAs,
12
    ChangeSetNameOrId,
13
    ChangeSetNotFoundException,
14
    ChangeSetType,
15
    ClientRequestToken,
16
    CloudformationApi,
17
    CreateChangeSetInput,
18
    CreateChangeSetOutput,
19
    CreateStackInput,
20
    CreateStackInstancesInput,
21
    CreateStackInstancesOutput,
22
    CreateStackOutput,
23
    CreateStackSetInput,
24
    CreateStackSetOutput,
25
    DeleteChangeSetOutput,
26
    DeleteStackInstancesInput,
27
    DeleteStackInstancesOutput,
28
    DeleteStackSetOutput,
29
    DeletionMode,
30
    DescribeChangeSetOutput,
31
    DescribeStackEventsOutput,
32
    DescribeStackResourceOutput,
33
    DescribeStackResourcesOutput,
34
    DescribeStackSetOperationOutput,
35
    DescribeStackSetOutput,
36
    DescribeStacksOutput,
37
    DisableRollback,
38
    EnableTerminationProtection,
39
    ExecuteChangeSetOutput,
40
    ExecutionStatus,
41
    ExportName,
42
    GetTemplateOutput,
43
    GetTemplateSummaryInput,
44
    GetTemplateSummaryOutput,
45
    IncludePropertyValues,
46
    InsufficientCapabilitiesException,
47
    InvalidChangeSetStatusException,
48
    ListChangeSetsOutput,
49
    ListExportsOutput,
50
    ListImportsOutput,
51
    ListStackInstancesInput,
52
    ListStackInstancesOutput,
53
    ListStackResourcesOutput,
54
    ListStackSetsInput,
55
    ListStackSetsOutput,
56
    ListStacksOutput,
57
    ListTypesInput,
58
    ListTypesOutput,
59
    LogicalResourceId,
60
    NextToken,
61
    Parameter,
62
    PhysicalResourceId,
63
    RegisterTypeInput,
64
    RegisterTypeOutput,
65
    RegistryType,
66
    RetainExceptOnCreate,
67
    RetainResources,
68
    RoleARN,
69
    StackName,
70
    StackNameOrId,
71
    StackSetName,
72
    StackStatus,
73
    StackStatusFilter,
74
    TemplateParameter,
75
    TemplateStage,
76
    TypeSummary,
77
    UpdateStackInput,
78
    UpdateStackOutput,
79
    UpdateStackSetInput,
80
    UpdateStackSetOutput,
81
    UpdateTerminationProtectionOutput,
82
    ValidateTemplateInput,
83
    ValidateTemplateOutput,
84
)
85
from localstack.aws.connect import connect_to
1✔
86
from localstack.services.cloudformation import api_utils
1✔
87
from localstack.services.cloudformation.engine import parameters as param_resolver
1✔
88
from localstack.services.cloudformation.engine import template_deployer, template_preparer
1✔
89
from localstack.services.cloudformation.engine.entities import (
1✔
90
    Stack,
91
    StackChangeSet,
92
    StackInstance,
93
    StackSet,
94
)
95
from localstack.services.cloudformation.engine.parameters import mask_no_echo, strip_parameter_type
1✔
96
from localstack.services.cloudformation.engine.resource_ordering import (
1✔
97
    NoResourceInStack,
98
    order_resources,
99
)
100
from localstack.services.cloudformation.engine.template_deployer import (
1✔
101
    NoStackUpdates,
102
)
103
from localstack.services.cloudformation.engine.template_utils import resolve_stack_conditions
1✔
104
from localstack.services.cloudformation.engine.transformers import (
1✔
105
    FailedTransformationException,
106
)
107
from localstack.services.cloudformation.engine.validations import (
1✔
108
    DEFAULT_TEMPLATE_VALIDATIONS,
109
    ValidationError,
110
)
111
from localstack.services.cloudformation.resource_provider import (
1✔
112
    PRO_RESOURCE_PROVIDERS,
113
    ResourceProvider,
114
)
115
from localstack.services.cloudformation.stores import (
1✔
116
    cloudformation_stores,
117
    find_active_stack_by_name_or_id,
118
    find_change_set,
119
    find_stack,
120
    find_stack_by_id,
121
    get_cloudformation_store,
122
)
123
from localstack.state import StateVisitor
1✔
124
from localstack.utils.collections import (
1✔
125
    remove_attributes,
126
    select_attributes,
127
    select_from_typed_dict,
128
)
129
from localstack.utils.json import clone
1✔
130
from localstack.utils.strings import long_uid, short_uid
1✔
131

132
LOG = logging.getLogger(__name__)
1✔
133

134
ARN_CHANGESET_REGEX = re.compile(
1✔
135
    r"arn:(aws|aws-us-gov|aws-cn):cloudformation:[-a-zA-Z0-9]+:\d{12}:changeSet/[a-zA-Z][-a-zA-Z0-9]*/[-a-zA-Z0-9:/._+]+"
136
)
137
ARN_STACK_REGEX = re.compile(
1✔
138
    r"arn:(aws|aws-us-gov|aws-cn):cloudformation:[-a-zA-Z0-9]+:\d{12}:stack/[a-zA-Z][-a-zA-Z0-9]*/[-a-zA-Z0-9:/._+]+"
139
)
140
ARN_STACK_SET_REGEX = re.compile(
1✔
141
    r"arn:(aws|aws-us-gov|aws-cn):cloudformation:[-a-zA-Z0-9]+:\d{12}:stack-set/[a-zA-Z][-a-zA-Z0-9]*/[-a-zA-Z0-9:/._+]+"
142
)
143

144

145
def clone_stack_params(stack_params):
1✔
146
    try:
1✔
147
        return clone(stack_params)
1✔
148
    except Exception as e:
×
149
        LOG.info("Unable to clone stack parameters: %s", e)
×
150
        return stack_params
×
151

152

153
def find_stack_instance(stack_set: StackSet, account: str, region: str):
1✔
154
    for instance in stack_set.stack_instances:
1✔
155
        if instance.metadata["Account"] == account and instance.metadata["Region"] == region:
1✔
156
            return instance
1✔
157
    return None
×
158

159

160
def stack_not_found_error(stack_name: str):
1✔
161
    # FIXME
162
    raise ValidationError(f"Stack with id {stack_name} does not exist")
×
163

164

165
def not_found_error(message: str):
1✔
166
    # FIXME
167
    raise ResourceNotFoundException(message)
×
168

169

170
class ResourceNotFoundException(CommonServiceException):
1✔
171
    def __init__(self, message=None):
1✔
172
        super().__init__("ResourceNotFoundException", status_code=404, message=message)
×
173

174

175
class InternalFailure(CommonServiceException):
1✔
176
    def __init__(self, message=None):
1✔
177
        super().__init__("InternalFailure", status_code=500, message=message, sender_fault=False)
×
178

179

180
class CloudformationProvider(CloudformationApi):
1✔
181
    def _stack_status_is_active(self, stack_status: str) -> bool:
1✔
182
        return stack_status not in [StackStatus.DELETE_COMPLETE]
1✔
183

184
    def accept_state_visitor(self, visitor: StateVisitor):
1✔
185
        visitor.visit(cloudformation_stores)
×
186

187
    @handler("CreateStack", expand=False)
1✔
188
    def create_stack(self, context: RequestContext, request: CreateStackInput) -> CreateStackOutput:
1✔
189
        # TODO: test what happens when both TemplateUrl and Body are specified
190
        state = get_cloudformation_store(context.account_id, context.region)
1✔
191

192
        stack_name = request.get("StackName")
1✔
193

194
        # get stacks by name
195
        active_stack_candidates = [
1✔
196
            s
197
            for s in state.stacks.values()
198
            if s.stack_name == stack_name and self._stack_status_is_active(s.status)
199
        ]
200

201
        # TODO: fix/implement this code path
202
        #   this needs more investigation how Cloudformation handles it (e.g. normal stack create or does it create a separate changeset?)
203
        # REVIEW_IN_PROGRESS is another special status
204
        # in this case existing changesets are set to obsolete and the stack is created
205
        # review_stack_candidates = [s for s in stack_candidates if s.status == StackStatus.REVIEW_IN_PROGRESS]
206
        # if review_stack_candidates:
207
        # set changesets to obsolete
208
        # for cs in review_stack_candidates[0].change_sets:
209
        #     cs.execution_status = ExecutionStatus.OBSOLETE
210

211
        if active_stack_candidates:
1✔
212
            raise AlreadyExistsException(f"Stack [{stack_name}] already exists")
1✔
213

214
        template_body = request.get("TemplateBody") or ""
1✔
215
        if len(template_body) > 51200:
1✔
216
            raise ValidationError(
1✔
217
                f"1 validation error detected: Value '{request['TemplateBody']}' at 'templateBody' "
218
                "failed to satisfy constraint: Member must have length less than or equal to 51200"
219
            )
220
        api_utils.prepare_template_body(request)  # TODO: avoid mutating request directly
1✔
221

222
        template = template_preparer.parse_template(request["TemplateBody"])
1✔
223

224
        stack_name = template["StackName"] = request.get("StackName")
1✔
225
        if api_utils.validate_stack_name(stack_name) is False:
1✔
226
            raise ValidationError(
×
227
                f"1 validation error detected: Value '{stack_name}' at 'stackName' failed to satisfy constraint:\
228
                Member must satisfy regular expression pattern: [a-zA-Z][-a-zA-Z0-9]*|arn:[-a-zA-Z0-9:/._+]*"
229
            )
230

231
        if (
1✔
232
            "CAPABILITY_AUTO_EXPAND" not in request.get("Capabilities", [])
233
            and "Transform" in template.keys()
234
        ):
235
            raise InsufficientCapabilitiesException(
1✔
236
                "Requires capabilities : [CAPABILITY_AUTO_EXPAND]"
237
            )
238

239
        # resolve stack parameters
240
        new_parameters = param_resolver.convert_stack_parameters_to_dict(request.get("Parameters"))
1✔
241
        parameter_declarations = param_resolver.extract_stack_parameter_declarations(template)
1✔
242
        resolved_parameters = param_resolver.resolve_parameters(
1✔
243
            account_id=context.account_id,
244
            region_name=context.region,
245
            parameter_declarations=parameter_declarations,
246
            new_parameters=new_parameters,
247
            old_parameters={},
248
        )
249

250
        stack = Stack(context.account_id, context.region, request, template)
1✔
251

252
        try:
1✔
253
            template = template_preparer.transform_template(
1✔
254
                context.account_id,
255
                context.region,
256
                template,
257
                stack.stack_name,
258
                stack.resources,
259
                stack.mappings,
260
                {},  # TODO
261
                resolved_parameters,
262
            )
263
        except FailedTransformationException as e:
1✔
264
            stack.add_stack_event(
1✔
265
                stack.stack_name,
266
                stack.stack_id,
267
                status="ROLLBACK_IN_PROGRESS",
268
                status_reason=e.message,
269
            )
270
            stack.set_stack_status("ROLLBACK_COMPLETE")
1✔
271
            state.stacks[stack.stack_id] = stack
1✔
272
            return CreateStackOutput(StackId=stack.stack_id)
1✔
273

274
        # HACK: recreate the stack (including all of its confusing processes in the __init__ method
275
        # to set the stack template to be the transformed template, rather than the untransformed
276
        # template
277
        stack = Stack(context.account_id, context.region, request, template)
1✔
278

279
        # perform basic static analysis on the template
280
        for validation_fn in DEFAULT_TEMPLATE_VALIDATIONS:
1✔
281
            validation_fn(template)
×
282

283
        # resolve conditions
284
        raw_conditions = template.get("Conditions", {})
1✔
285
        resolved_stack_conditions = resolve_stack_conditions(
1✔
286
            account_id=context.account_id,
287
            region_name=context.region,
288
            conditions=raw_conditions,
289
            parameters=resolved_parameters,
290
            mappings=stack.mappings,
291
            stack_name=stack_name,
292
        )
293
        stack.set_resolved_stack_conditions(resolved_stack_conditions)
1✔
294

295
        stack.set_resolved_parameters(resolved_parameters)
1✔
296
        stack.template_body = template_body
1✔
297
        state.stacks[stack.stack_id] = stack
1✔
298
        LOG.debug(
1✔
299
            'Creating stack "%s" with %s resources ...',
300
            stack.stack_name,
301
            len(stack.template_resources),
302
        )
303
        deployer = template_deployer.TemplateDeployer(context.account_id, context.region, stack)
1✔
304
        try:
1✔
305
            deployer.deploy_stack()
1✔
306
        except Exception as e:
×
307
            stack.set_stack_status("CREATE_FAILED")
×
308
            msg = f'Unable to create stack "{stack.stack_name}": {e}'
×
309
            LOG.error("%s", exc_info=LOG.isEnabledFor(logging.DEBUG))
×
310
            raise ValidationError(msg) from e
×
311

312
        return CreateStackOutput(StackId=stack.stack_id)
1✔
313

314
    @handler("DeleteStack")
1✔
315
    def delete_stack(
1✔
316
        self,
317
        context: RequestContext,
318
        stack_name: StackName,
319
        retain_resources: RetainResources = None,
320
        role_arn: RoleARN = None,
321
        client_request_token: ClientRequestToken = None,
322
        deletion_mode: DeletionMode = None,
323
        **kwargs,
324
    ) -> None:
325
        stack = find_active_stack_by_name_or_id(context.account_id, context.region, stack_name)
1✔
326
        if not stack:
1✔
327
            # aws will silently ignore invalid stack names - we should do the same
328
            return
1✔
329
        deployer = template_deployer.TemplateDeployer(context.account_id, context.region, stack)
1✔
330
        deployer.delete_stack()
1✔
331

332
    @handler("UpdateStack", expand=False)
1✔
333
    def update_stack(
1✔
334
        self,
335
        context: RequestContext,
336
        request: UpdateStackInput,
337
    ) -> UpdateStackOutput:
338
        stack_name = request.get("StackName")
1✔
339
        stack = find_stack(context.account_id, context.region, stack_name)
1✔
340
        if not stack:
1✔
341
            return not_found_error(f'Unable to update non-existing stack "{stack_name}"')
×
342

343
        api_utils.prepare_template_body(request)
1✔
344
        template = template_preparer.parse_template(request["TemplateBody"])
1✔
345

346
        if (
1✔
347
            "CAPABILITY_AUTO_EXPAND" not in request.get("Capabilities", [])
348
            and "Transform" in template.keys()
349
        ):
350
            raise InsufficientCapabilitiesException(
×
351
                "Requires capabilities : [CAPABILITY_AUTO_EXPAND]"
352
            )
353

354
        new_parameters: dict[str, Parameter] = param_resolver.convert_stack_parameters_to_dict(
1✔
355
            request.get("Parameters")
356
        )
357
        parameter_declarations = param_resolver.extract_stack_parameter_declarations(template)
1✔
358
        resolved_parameters = param_resolver.resolve_parameters(
1✔
359
            account_id=context.account_id,
360
            region_name=context.region,
361
            parameter_declarations=parameter_declarations,
362
            new_parameters=new_parameters,
363
            old_parameters=stack.resolved_parameters,
364
        )
365

366
        resolved_stack_conditions = resolve_stack_conditions(
1✔
367
            account_id=context.account_id,
368
            region_name=context.region,
369
            conditions=template.get("Conditions", {}),
370
            parameters=resolved_parameters,
371
            mappings=template.get("Mappings", {}),
372
            stack_name=stack_name,
373
        )
374

375
        raw_new_template = copy.deepcopy(template)
1✔
376
        try:
1✔
377
            template = template_preparer.transform_template(
1✔
378
                context.account_id,
379
                context.region,
380
                template,
381
                stack.stack_name,
382
                stack.resources,
383
                stack.mappings,
384
                resolved_stack_conditions,
385
                resolved_parameters,
386
            )
387
            processed_template = copy.deepcopy(
1✔
388
                template
389
            )  # copying it here since it's being mutated somewhere downstream
390
        except FailedTransformationException as e:
×
391
            stack.add_stack_event(
×
392
                stack.stack_name,
393
                stack.stack_id,
394
                status="ROLLBACK_IN_PROGRESS",
395
                status_reason=e.message,
396
            )
397
            stack.set_stack_status("ROLLBACK_COMPLETE")
×
398
            return CreateStackOutput(StackId=stack.stack_id)
×
399

400
        # perform basic static analysis on the template
401
        for validation_fn in DEFAULT_TEMPLATE_VALIDATIONS:
1✔
402
            validation_fn(template)
×
403

404
        # update the template
405
        stack.template_original = template
1✔
406

407
        deployer = template_deployer.TemplateDeployer(context.account_id, context.region, stack)
1✔
408
        # TODO: there shouldn't be a "new" stack on update
409
        new_stack = Stack(
1✔
410
            context.account_id, context.region, request, template, request["TemplateBody"]
411
        )
412
        new_stack.set_resolved_parameters(resolved_parameters)
1✔
413
        stack.set_resolved_parameters(resolved_parameters)
1✔
414
        stack.set_resolved_stack_conditions(resolved_stack_conditions)
1✔
415
        try:
1✔
416
            deployer.update_stack(new_stack)
1✔
417
        except NoStackUpdates as e:
1✔
418
            stack.set_stack_status("UPDATE_COMPLETE")
1✔
419
            if raw_new_template != processed_template:
1✔
420
                # processed templates seem to never return an exception here
421
                return UpdateStackOutput(StackId=stack.stack_id)
1✔
422
            raise ValidationError(str(e))
1✔
423
        except Exception as e:
×
424
            stack.set_stack_status("UPDATE_FAILED")
×
425
            msg = f'Unable to update stack "{stack_name}": {e}'
×
426
            LOG.error("%s", msg, exc_info=LOG.isEnabledFor(logging.DEBUG))
×
427
            raise ValidationError(msg) from e
×
428

429
        return UpdateStackOutput(StackId=stack.stack_id)
1✔
430

431
    @handler("DescribeStacks")
1✔
432
    def describe_stacks(
1✔
433
        self,
434
        context: RequestContext,
435
        stack_name: StackName = None,
436
        next_token: NextToken = None,
437
        **kwargs,
438
    ) -> DescribeStacksOutput:
439
        # TODO: test & implement pagination
440
        state = get_cloudformation_store(context.account_id, context.region)
1✔
441

442
        if stack_name:
1✔
443
            if ARN_STACK_REGEX.match(stack_name):
1✔
444
                # we can get the stack directly since we index the store by ARN/stackID
445
                stack = state.stacks.get(stack_name)
1✔
446
                stacks = [stack.describe_details()] if stack else []
1✔
447
            else:
448
                # otherwise we have to find the active stack with the given name
449
                stack_candidates: list[Stack] = [
1✔
450
                    s for stack_arn, s in state.stacks.items() if s.stack_name == stack_name
451
                ]
452
                active_stack_candidates = [
1✔
453
                    s for s in stack_candidates if self._stack_status_is_active(s.status)
454
                ]
455
                stacks = [s.describe_details() for s in active_stack_candidates]
1✔
456
        else:
457
            # return all active stacks
458
            stack_list = list(state.stacks.values())
1✔
459
            stacks = [
1✔
460
                s.describe_details() for s in stack_list if self._stack_status_is_active(s.status)
461
            ]
462

463
        if stack_name and not stacks:
1✔
464
            raise ValidationError(f"Stack with id {stack_name} does not exist")
1✔
465

466
        return DescribeStacksOutput(Stacks=stacks)
1✔
467

468
    @handler("ListStacks")
1✔
469
    def list_stacks(
1✔
470
        self,
471
        context: RequestContext,
472
        next_token: NextToken = None,
473
        stack_status_filter: StackStatusFilter = None,
474
        **kwargs,
475
    ) -> ListStacksOutput:
476
        state = get_cloudformation_store(context.account_id, context.region)
×
477

478
        stacks = [
×
479
            s.describe_details()
480
            for s in state.stacks.values()
481
            if not stack_status_filter or s.status in stack_status_filter
482
        ]
483

484
        attrs = [
×
485
            "StackId",
486
            "StackName",
487
            "TemplateDescription",
488
            "CreationTime",
489
            "LastUpdatedTime",
490
            "DeletionTime",
491
            "StackStatus",
492
            "StackStatusReason",
493
            "ParentId",
494
            "RootId",
495
            "DriftInformation",
496
        ]
497
        stacks = [select_attributes(stack, attrs) for stack in stacks]
×
498
        return ListStacksOutput(StackSummaries=stacks)
×
499

500
    @handler("GetTemplate")
1✔
501
    def get_template(
1✔
502
        self,
503
        context: RequestContext,
504
        stack_name: StackName = None,
505
        change_set_name: ChangeSetNameOrId = None,
506
        template_stage: TemplateStage = None,
507
        **kwargs,
508
    ) -> GetTemplateOutput:
509
        if change_set_name:
1✔
510
            stack = find_change_set(
×
511
                context.account_id, context.region, stack_name=stack_name, cs_name=change_set_name
512
            )
513
        else:
514
            stack = find_stack(context.account_id, context.region, stack_name)
1✔
515
        if not stack:
1✔
516
            return stack_not_found_error(stack_name)
×
517

518
        if template_stage == TemplateStage.Processed and "Transform" in stack.template_body:
1✔
519
            copy_template = clone(stack.template_original)
1✔
520
            for key in [
1✔
521
                "ChangeSetName",
522
                "StackName",
523
                "StackId",
524
                "Transform",
525
                "Conditions",
526
                "Mappings",
527
            ]:
528
                copy_template.pop(key, None)
1✔
529
            for key in ["Parameters", "Outputs"]:
1✔
530
                if key in copy_template and not copy_template[key]:
1✔
531
                    copy_template.pop(key)
1✔
532
            for resource in copy_template.get("Resources", {}).values():
1✔
533
                resource.pop("LogicalResourceId", None)
1✔
534
            template_body = json.dumps(copy_template)
1✔
535
        else:
536
            template_body = stack.template_body
1✔
537

538
        return GetTemplateOutput(
1✔
539
            TemplateBody=template_body,
540
            StagesAvailable=[TemplateStage.Original, TemplateStage.Processed],
541
        )
542

543
    @handler("GetTemplateSummary", expand=False)
1✔
544
    def get_template_summary(
1✔
545
        self,
546
        context: RequestContext,
547
        request: GetTemplateSummaryInput,
548
    ) -> GetTemplateSummaryOutput:
549
        stack_name = request.get("StackName")
1✔
550

551
        if stack_name:
1✔
552
            stack = find_stack(context.account_id, context.region, stack_name)
1✔
553
            if not stack:
1✔
554
                return stack_not_found_error(stack_name)
×
555
            template = stack.template
1✔
556
        else:
557
            api_utils.prepare_template_body(request)
1✔
558
            template = template_preparer.parse_template(request["TemplateBody"])
1✔
559
            request["StackName"] = "tmp-stack"
1✔
560
            stack = Stack(context.account_id, context.region, request, template)
1✔
561

562
        result: GetTemplateSummaryOutput = stack.describe_details()
1✔
563

564
        # build parameter declarations
565
        result["Parameters"] = list(
1✔
566
            param_resolver.extract_stack_parameter_declarations(template).values()
567
        )
568

569
        id_summaries = defaultdict(list)
1✔
570
        for resource_id, resource in stack.template_resources.items():
1✔
571
            res_type = resource["Type"]
1✔
572
            id_summaries[res_type].append(resource_id)
1✔
573

574
        result["ResourceTypes"] = list(id_summaries.keys())
1✔
575
        result["ResourceIdentifierSummaries"] = [
1✔
576
            {"ResourceType": key, "LogicalResourceIds": values}
577
            for key, values in id_summaries.items()
578
        ]
579
        result["Metadata"] = stack.template.get("Metadata")
1✔
580
        result["Version"] = stack.template.get("AWSTemplateFormatVersion", "2010-09-09")
1✔
581
        # these do not appear in the output
582
        result.pop("Capabilities", None)
1✔
583

584
        return select_from_typed_dict(GetTemplateSummaryOutput, result)
1✔
585

586
    def update_termination_protection(
1✔
587
        self,
588
        context: RequestContext,
589
        enable_termination_protection: EnableTerminationProtection,
590
        stack_name: StackNameOrId,
591
        **kwargs,
592
    ) -> UpdateTerminationProtectionOutput:
593
        stack = find_stack(context.account_id, context.region, stack_name)
1✔
594
        if not stack:
1✔
595
            raise ValidationError(f"Stack '{stack_name}' does not exist.")
×
596
        stack.metadata["EnableTerminationProtection"] = enable_termination_protection
1✔
597
        return UpdateTerminationProtectionOutput(StackId=stack.stack_id)
1✔
598

599
    @handler("CreateChangeSet", expand=False)
1✔
600
    def create_change_set(
1✔
601
        self, context: RequestContext, request: CreateChangeSetInput
602
    ) -> CreateChangeSetOutput:
603
        state = get_cloudformation_store(context.account_id, context.region)
1✔
604

605
        req_params = request
1✔
606
        change_set_type = req_params.get("ChangeSetType", "UPDATE")
1✔
607
        stack_name = req_params.get("StackName")
1✔
608
        if not stack_name:
1✔
609
            raise ValidationError("Member must have length greater than or equal to 1")
1✔
610
        change_set_name = req_params.get("ChangeSetName")
1✔
611
        template_body = req_params.get("TemplateBody")
1✔
612
        # s3 or secretsmanager url
613
        template_url = req_params.get("TemplateURL")
1✔
614

615
        # validate and resolve template
616
        if template_body and template_url:
1✔
617
            raise ValidationError(
×
618
                "Specify exactly one of 'TemplateBody' or 'TemplateUrl'"
619
            )  # TODO: check proper message
620

621
        if not template_body and not template_url:
1✔
622
            raise ValidationError(
×
623
                "Specify exactly one of 'TemplateBody' or 'TemplateUrl'"
624
            )  # TODO: check proper message
625

626
        api_utils.prepare_template_body(
1✔
627
            req_params
628
        )  # TODO: function has too many unclear responsibilities
629
        if not template_body:
1✔
630
            template_body = req_params[
×
631
                "TemplateBody"
632
            ]  # should then have been set by prepare_template_body
633
        template = template_preparer.parse_template(req_params["TemplateBody"])
1✔
634

635
        del req_params["TemplateBody"]  # TODO: stop mutating req_params
1✔
636
        template["StackName"] = stack_name
1✔
637
        # TODO: validate with AWS what this is actually doing?
638
        template["ChangeSetName"] = change_set_name
1✔
639

640
        # this is intentionally not in a util yet. Let's first see how the different operations deal with these before generalizing
641
        # handle ARN stack_name here (not valid for initial CREATE, since stack doesn't exist yet)
642
        if ARN_STACK_REGEX.match(stack_name):
1✔
643
            if not (stack := state.stacks.get(stack_name)):
1✔
644
                raise ValidationError(f"Stack '{stack_name}' does not exist.")
×
645
        else:
646
            # stack name specified, so fetch the stack by name
647
            stack_candidates: list[Stack] = [
1✔
648
                s for stack_arn, s in state.stacks.items() if s.stack_name == stack_name
649
            ]
650
            active_stack_candidates = [
1✔
651
                s for s in stack_candidates if self._stack_status_is_active(s.status)
652
            ]
653

654
            # on a CREATE an empty Stack should be generated if we didn't find an active one
655
            if not active_stack_candidates and change_set_type == ChangeSetType.CREATE:
1✔
656
                empty_stack_template = dict(template)
1✔
657
                empty_stack_template["Resources"] = {}
1✔
658
                req_params_copy = clone_stack_params(req_params)
1✔
659
                stack = Stack(
1✔
660
                    context.account_id,
661
                    context.region,
662
                    req_params_copy,
663
                    empty_stack_template,
664
                    template_body=template_body,
665
                )
666
                state.stacks[stack.stack_id] = stack
1✔
667
                stack.set_stack_status("REVIEW_IN_PROGRESS")
1✔
668
            else:
669
                if not active_stack_candidates:
1✔
670
                    raise ValidationError(f"Stack '{stack_name}' does not exist.")
1✔
671
                stack = active_stack_candidates[0]
1✔
672

673
        # TODO: test if rollback status is allowed as well
674
        if (
1✔
675
            change_set_type == ChangeSetType.CREATE
676
            and stack.status != StackStatus.REVIEW_IN_PROGRESS
677
        ):
678
            raise ValidationError(
1✔
679
                f"Stack [{stack_name}] already exists and cannot be created again with the changeSet [{change_set_name}]."
680
            )
681

682
        old_parameters: dict[str, Parameter] = {}
1✔
683
        match change_set_type:
1✔
684
            case ChangeSetType.UPDATE:
1✔
685
                # add changeset to existing stack
686
                old_parameters = {
1✔
687
                    k: mask_no_echo(strip_parameter_type(v))
688
                    for k, v in stack.resolved_parameters.items()
689
                }
690
            case ChangeSetType.IMPORT:
1✔
691
                raise NotImplementedError()  # TODO: implement importing resources
692
            case ChangeSetType.CREATE:
1✔
693
                pass
1✔
694
            case _:
×
695
                msg = (
×
696
                    f"1 validation error detected: Value '{change_set_type}' at 'changeSetType' failed to satisfy "
697
                    f"constraint: Member must satisfy enum value set: [IMPORT, UPDATE, CREATE] "
698
                )
699
                raise ValidationError(msg)
×
700

701
        # resolve parameters
702
        new_parameters: dict[str, Parameter] = param_resolver.convert_stack_parameters_to_dict(
1✔
703
            request.get("Parameters")
704
        )
705
        parameter_declarations = param_resolver.extract_stack_parameter_declarations(template)
1✔
706
        resolved_parameters = param_resolver.resolve_parameters(
1✔
707
            account_id=context.account_id,
708
            region_name=context.region,
709
            parameter_declarations=parameter_declarations,
710
            new_parameters=new_parameters,
711
            old_parameters=old_parameters,
712
        )
713

714
        # TODO: remove this when fixing Stack.resources and transformation order
715
        #   currently we need to create a stack with existing resources + parameters so that resolve refs recursively in here will work.
716
        #   The correct way to do it would be at a later stage anyway just like a normal intrinsic function
717
        req_params_copy = clone_stack_params(req_params)
1✔
718
        temp_stack = Stack(context.account_id, context.region, req_params_copy, template)
1✔
719
        temp_stack.set_resolved_parameters(resolved_parameters)
1✔
720

721
        # TODO: everything below should be async
722
        # apply template transformations
723
        transformed_template = template_preparer.transform_template(
1✔
724
            context.account_id,
725
            context.region,
726
            template,
727
            stack_name=temp_stack.stack_name,
728
            resources=temp_stack.resources,
729
            mappings=temp_stack.mappings,
730
            conditions={},  # TODO: we don't have any resolved conditions yet at this point but we need the conditions because of the samtranslator...
731
            resolved_parameters=resolved_parameters,
732
        )
733

734
        # perform basic static analysis on the template
735
        for validation_fn in DEFAULT_TEMPLATE_VALIDATIONS:
1✔
736
            validation_fn(template)
×
737

738
        # create change set for the stack and apply changes
739
        change_set = StackChangeSet(
1✔
740
            context.account_id, context.region, stack, req_params, transformed_template
741
        )
742
        # only set parameters for the changeset, then switch to stack on execute_change_set
743
        change_set.set_resolved_parameters(resolved_parameters)
1✔
744
        change_set.template_body = template_body
1✔
745

746
        # TODO: evaluate conditions
747
        raw_conditions = transformed_template.get("Conditions", {})
1✔
748
        resolved_stack_conditions = resolve_stack_conditions(
1✔
749
            account_id=context.account_id,
750
            region_name=context.region,
751
            conditions=raw_conditions,
752
            parameters=resolved_parameters,
753
            mappings=temp_stack.mappings,
754
            stack_name=stack_name,
755
        )
756
        change_set.set_resolved_stack_conditions(resolved_stack_conditions)
1✔
757

758
        # a bit gross but use the template ordering to validate missing resources
759
        try:
1✔
760
            order_resources(
1✔
761
                transformed_template["Resources"],
762
                resolved_parameters=resolved_parameters,
763
                resolved_conditions=resolved_stack_conditions,
764
            )
765
        except NoResourceInStack as e:
1✔
766
            raise ValidationError(str(e)) from e
1✔
767

768
        deployer = template_deployer.TemplateDeployer(
1✔
769
            context.account_id, context.region, change_set
770
        )
771
        changes = deployer.construct_changes(
1✔
772
            stack,
773
            change_set,
774
            change_set_id=change_set.change_set_id,
775
            append_to_changeset=True,
776
            filter_unchanged_resources=True,
777
        )
778
        stack.change_sets.append(change_set)
1✔
779
        if not changes:
1✔
780
            change_set.metadata["Status"] = "FAILED"
1✔
781
            change_set.metadata["ExecutionStatus"] = "UNAVAILABLE"
1✔
782
            change_set.metadata["StatusReason"] = (
1✔
783
                "The submitted information didn't contain changes. Submit different information to create a change set."
784
            )
785
        else:
786
            change_set.metadata["Status"] = (
1✔
787
                "CREATE_COMPLETE"  # technically for some time this should first be CREATE_PENDING
788
            )
789
            change_set.metadata["ExecutionStatus"] = (
1✔
790
                "AVAILABLE"  # technically for some time this should first be UNAVAILABLE
791
            )
792

793
        return CreateChangeSetOutput(StackId=change_set.stack_id, Id=change_set.change_set_id)
1✔
794

795
    @handler("DescribeChangeSet")
1✔
796
    def describe_change_set(
1✔
797
        self,
798
        context: RequestContext,
799
        change_set_name: ChangeSetNameOrId,
800
        stack_name: StackNameOrId = None,
801
        next_token: NextToken = None,
802
        include_property_values: IncludePropertyValues = None,
803
        **kwargs,
804
    ) -> DescribeChangeSetOutput:
805
        # TODO add support for include_property_values
806
        # only relevant if change_set_name isn't an ARN
807
        if not ARN_CHANGESET_REGEX.match(change_set_name):
1✔
808
            if not stack_name:
1✔
809
                raise ValidationError(
×
810
                    "StackName must be specified if ChangeSetName is not specified as an ARN."
811
                )
812

813
            stack = find_stack(context.account_id, context.region, stack_name)
1✔
814
            if not stack:
1✔
815
                raise ValidationError(f"Stack [{stack_name}] does not exist")
1✔
816

817
        change_set = find_change_set(
1✔
818
            context.account_id, context.region, change_set_name, stack_name=stack_name
819
        )
820
        if not change_set:
1✔
821
            raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
1✔
822

823
        attrs = [
1✔
824
            "ChangeSetType",
825
            "StackStatus",
826
            "LastUpdatedTime",
827
            "DisableRollback",
828
            "EnableTerminationProtection",
829
            "Transform",
830
        ]
831
        result = remove_attributes(deepcopy(change_set.metadata), attrs)
1✔
832
        # TODO: replace this patch with a better solution
833
        result["Parameters"] = [
1✔
834
            mask_no_echo(strip_parameter_type(p)) for p in result.get("Parameters", [])
835
        ]
836
        return result
1✔
837

838
    @handler("DeleteChangeSet")
1✔
839
    def delete_change_set(
1✔
840
        self,
841
        context: RequestContext,
842
        change_set_name: ChangeSetNameOrId,
843
        stack_name: StackNameOrId = None,
844
        **kwargs,
845
    ) -> DeleteChangeSetOutput:
846
        # only relevant if change_set_name isn't an ARN
847
        if not ARN_CHANGESET_REGEX.match(change_set_name):
1✔
848
            if not stack_name:
1✔
849
                raise ValidationError(
1✔
850
                    "StackName must be specified if ChangeSetName is not specified as an ARN."
851
                )
852

853
            stack = find_stack(context.account_id, context.region, stack_name)
1✔
854
            if not stack:
1✔
855
                raise ValidationError(f"Stack [{stack_name}] does not exist")
1✔
856

857
        change_set = find_change_set(
1✔
858
            context.account_id, context.region, change_set_name, stack_name=stack_name
859
        )
860
        if not change_set:
1✔
861
            raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
×
862
        change_set.stack.change_sets = [
1✔
863
            cs
864
            for cs in change_set.stack.change_sets
865
            if change_set_name not in (cs.change_set_name, cs.change_set_id)
866
        ]
867
        return DeleteChangeSetOutput()
1✔
868

869
    @handler("ExecuteChangeSet")
1✔
870
    def execute_change_set(
1✔
871
        self,
872
        context: RequestContext,
873
        change_set_name: ChangeSetNameOrId,
874
        stack_name: StackNameOrId = None,
875
        client_request_token: ClientRequestToken = None,
876
        disable_rollback: DisableRollback = None,
877
        retain_except_on_create: RetainExceptOnCreate = None,
878
        **kwargs,
879
    ) -> ExecuteChangeSetOutput:
880
        change_set = find_change_set(
1✔
881
            context.account_id,
882
            context.region,
883
            change_set_name,
884
            stack_name=stack_name,
885
            active_only=True,
886
        )
887
        if not change_set:
1✔
888
            raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
×
889
        if change_set.metadata.get("ExecutionStatus") != ExecutionStatus.AVAILABLE:
1✔
890
            LOG.debug("Change set %s not in execution status 'AVAILABLE'", change_set_name)
1✔
891
            raise InvalidChangeSetStatusException(
1✔
892
                f"ChangeSet [{change_set.metadata['ChangeSetId']}] cannot be executed in its current status of [{change_set.metadata.get('Status')}]"
893
            )
894
        stack_name = change_set.stack.stack_name
1✔
895
        LOG.debug(
1✔
896
            'Executing change set "%s" for stack "%s" with %s resources ...',
897
            change_set_name,
898
            stack_name,
899
            len(change_set.template_resources),
900
        )
901
        deployer = template_deployer.TemplateDeployer(
1✔
902
            context.account_id, context.region, change_set.stack
903
        )
904
        try:
1✔
905
            deployer.apply_change_set(change_set)
1✔
906
            change_set.stack.metadata["ChangeSetId"] = change_set.change_set_id
1✔
907
        except NoStackUpdates:
×
908
            # TODO: parity-check if this exception should be re-raised or swallowed
909
            raise ValidationError("No updates to be performed for stack change set")
×
910

911
        return ExecuteChangeSetOutput()
1✔
912

913
    @handler("ListChangeSets")
1✔
914
    def list_change_sets(
1✔
915
        self,
916
        context: RequestContext,
917
        stack_name: StackNameOrId,
918
        next_token: NextToken = None,
919
        **kwargs,
920
    ) -> ListChangeSetsOutput:
921
        stack = find_stack(context.account_id, context.region, stack_name)
×
922
        if not stack:
×
923
            return not_found_error(f'Unable to find stack "{stack_name}"')
×
924
        result = [cs.metadata for cs in stack.change_sets]
×
925
        return ListChangeSetsOutput(Summaries=result)
×
926

927
    @handler("ListExports")
1✔
928
    def list_exports(
1✔
929
        self, context: RequestContext, next_token: NextToken = None, **kwargs
930
    ) -> ListExportsOutput:
931
        state = get_cloudformation_store(context.account_id, context.region)
1✔
932
        return ListExportsOutput(Exports=state.exports.values())
1✔
933

934
    @handler("ListImports")
1✔
935
    def list_imports(
1✔
936
        self,
937
        context: RequestContext,
938
        export_name: ExportName,
939
        next_token: NextToken = None,
940
        **kwargs,
941
    ) -> ListImportsOutput:
942
        state = get_cloudformation_store(context.account_id, context.region)
×
943

944
        importing_stack_names = []
×
945
        for stack in state.stacks.values():
×
946
            if export_name in stack.imports:
×
947
                importing_stack_names.append(stack.stack_name)
×
948

949
        return ListImportsOutput(Imports=importing_stack_names)
×
950

951
    @handler("DescribeStackEvents")
1✔
952
    def describe_stack_events(
1✔
953
        self,
954
        context: RequestContext,
955
        stack_name: StackName = None,
956
        next_token: NextToken = None,
957
        **kwargs,
958
    ) -> DescribeStackEventsOutput:
959
        if stack_name is None:
1✔
960
            raise ValidationError(
1✔
961
                "1 validation error detected: Value null at 'stackName' failed to satisfy constraint: Member must not be null"
962
            )
963

964
        stack = find_active_stack_by_name_or_id(context.account_id, context.region, stack_name)
1✔
965
        if not stack:
1✔
966
            stack = find_stack_by_id(
1✔
967
                account_id=context.account_id, region_name=context.region, stack_id=stack_name
968
            )
969
        if not stack:
1✔
970
            raise ValidationError(f"Stack [{stack_name}] does not exist")
1✔
971
        return DescribeStackEventsOutput(StackEvents=stack.events)
1✔
972

973
    @handler("DescribeStackResource")
1✔
974
    def describe_stack_resource(
1✔
975
        self,
976
        context: RequestContext,
977
        stack_name: StackName,
978
        logical_resource_id: LogicalResourceId,
979
        **kwargs,
980
    ) -> DescribeStackResourceOutput:
981
        stack = find_stack(context.account_id, context.region, stack_name)
1✔
982

983
        if not stack:
1✔
984
            return stack_not_found_error(stack_name)
×
985

986
        try:
1✔
987
            details = stack.resource_status(logical_resource_id)
1✔
988
        except Exception as e:
1✔
989
            if "Unable to find details" in str(e):
1✔
990
                raise ValidationError(
1✔
991
                    f"Resource {logical_resource_id} does not exist for stack {stack_name}"
992
                )
993
            raise
×
994

995
        return DescribeStackResourceOutput(StackResourceDetail=details)
1✔
996

997
    @handler("DescribeStackResources")
1✔
998
    def describe_stack_resources(
1✔
999
        self,
1000
        context: RequestContext,
1001
        stack_name: StackName = None,
1002
        logical_resource_id: LogicalResourceId = None,
1003
        physical_resource_id: PhysicalResourceId = None,
1004
        **kwargs,
1005
    ) -> DescribeStackResourcesOutput:
1006
        if physical_resource_id and stack_name:
1✔
1007
            raise ValidationError("Cannot specify both StackName and PhysicalResourceId")
×
1008
        # TODO: filter stack by PhysicalResourceId!
1009
        stack = find_stack(context.account_id, context.region, stack_name)
1✔
1010
        if not stack:
1✔
1011
            return stack_not_found_error(stack_name)
×
1012
        statuses = [
1✔
1013
            res_status
1014
            for res_id, res_status in stack.resource_states.items()
1015
            if logical_resource_id in [res_id, None]
1016
        ]
1017
        for status in statuses:
1✔
1018
            status.setdefault("DriftInformation", {"StackResourceDriftStatus": "NOT_CHECKED"})
1✔
1019
        return DescribeStackResourcesOutput(StackResources=statuses)
1✔
1020

1021
    @handler("ListStackResources")
1✔
1022
    def list_stack_resources(
1✔
1023
        self, context: RequestContext, stack_name: StackName, next_token: NextToken = None, **kwargs
1024
    ) -> ListStackResourcesOutput:
1025
        result = self.describe_stack_resources(context, stack_name)
1✔
1026

1027
        resources = deepcopy(result.get("StackResources", []))
1✔
1028
        for resource in resources:
1✔
1029
            attrs = ["StackName", "StackId", "Timestamp", "PreviousResourceStatus"]
1✔
1030
            remove_attributes(resource, attrs)
1✔
1031

1032
        return ListStackResourcesOutput(StackResourceSummaries=resources)
1✔
1033

1034
    @handler("ValidateTemplate", expand=False)
1✔
1035
    def validate_template(
1✔
1036
        self, context: RequestContext, request: ValidateTemplateInput
1037
    ) -> ValidateTemplateOutput:
1038
        try:
1✔
1039
            # TODO implement actual validation logic
1040
            template_body = api_utils.get_template_body(request)
1✔
1041
            valid_template = json.loads(template_preparer.template_to_json(template_body))
1✔
1042

1043
            parameters = [
1✔
1044
                TemplateParameter(
1045
                    ParameterKey=k,
1046
                    DefaultValue=v.get("Default", ""),
1047
                    NoEcho=v.get("NoEcho", False),
1048
                    Description=v.get("Description", ""),
1049
                )
1050
                for k, v in valid_template.get("Parameters", {}).items()
1051
            ]
1052

1053
            return ValidateTemplateOutput(
1✔
1054
                Description=valid_template.get("Description"), Parameters=parameters
1055
            )
1056
        except Exception as e:
1✔
1057
            LOG.error("Error validating template", exc_info=LOG.isEnabledFor(logging.DEBUG))
1✔
1058
            raise ValidationError("Template Validation Error") from e
1✔
1059

1060
    # =======================================
1061
    # =============  Stack Set  =============
1062
    # =======================================
1063

1064
    @handler("CreateStackSet", expand=False)
1✔
1065
    def create_stack_set(
1✔
1066
        self, context: RequestContext, request: CreateStackSetInput
1067
    ) -> CreateStackSetOutput:
1068
        state = get_cloudformation_store(context.account_id, context.region)
1✔
1069
        stack_set = StackSet(request)
1✔
1070
        stack_set_id = f"{stack_set.stack_set_name}:{long_uid()}"
1✔
1071
        stack_set.metadata["StackSetId"] = stack_set_id
1✔
1072
        state.stack_sets[stack_set_id] = stack_set
1✔
1073

1074
        return CreateStackSetOutput(StackSetId=stack_set_id)
1✔
1075

1076
    @handler("DescribeStackSetOperation")
1✔
1077
    def describe_stack_set_operation(
1✔
1078
        self,
1079
        context: RequestContext,
1080
        stack_set_name: StackSetName,
1081
        operation_id: ClientRequestToken,
1082
        call_as: CallAs = None,
1083
        **kwargs,
1084
    ) -> DescribeStackSetOperationOutput:
1085
        state = get_cloudformation_store(context.account_id, context.region)
1✔
1086

1087
        set_name = stack_set_name
1✔
1088

1089
        stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]
1✔
1090
        if not stack_set:
1✔
1091
            return not_found_error(f'Unable to find stack set "{set_name}"')
×
1092
        stack_set = stack_set[0]
1✔
1093
        result = stack_set.operations.get(operation_id)
1✔
1094
        if not result:
1✔
1095
            LOG.debug(
×
1096
                'Unable to find operation ID "%s" for stack set "%s" in list: %s',
1097
                operation_id,
1098
                set_name,
1099
                list(stack_set.operations.keys()),
1100
            )
1101
            return not_found_error(
×
1102
                f'Unable to find operation ID "{operation_id}" for stack set "{set_name}"'
1103
            )
1104

1105
        return DescribeStackSetOperationOutput(StackSetOperation=result)
1✔
1106

1107
    @handler("DescribeStackSet")
1✔
1108
    def describe_stack_set(
1✔
1109
        self,
1110
        context: RequestContext,
1111
        stack_set_name: StackSetName,
1112
        call_as: CallAs = None,
1113
        **kwargs,
1114
    ) -> DescribeStackSetOutput:
1115
        state = get_cloudformation_store(context.account_id, context.region)
×
1116
        result = [
×
1117
            sset.metadata
1118
            for sset in state.stack_sets.values()
1119
            if sset.stack_set_name == stack_set_name
1120
        ]
1121
        if not result:
×
1122
            return not_found_error(f'Unable to find stack set "{stack_set_name}"')
×
1123

1124
        return DescribeStackSetOutput(StackSet=result[0])
×
1125

1126
    @handler("ListStackSets", expand=False)
1✔
1127
    def list_stack_sets(
1✔
1128
        self, context: RequestContext, request: ListStackSetsInput
1129
    ) -> ListStackSetsOutput:
1130
        state = get_cloudformation_store(context.account_id, context.region)
×
1131
        result = [sset.metadata for sset in state.stack_sets.values()]
×
1132
        return ListStackSetsOutput(Summaries=result)
×
1133

1134
    @handler("UpdateStackSet", expand=False)
1✔
1135
    def update_stack_set(
1✔
1136
        self, context: RequestContext, request: UpdateStackSetInput
1137
    ) -> UpdateStackSetOutput:
1138
        state = get_cloudformation_store(context.account_id, context.region)
×
1139
        set_name = request.get("StackSetName")
×
1140
        stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]
×
1141
        if not stack_set:
×
1142
            return not_found_error(f'Stack set named "{set_name}" does not exist')
×
1143
        stack_set = stack_set[0]
×
1144
        stack_set.metadata.update(request)
×
1145
        op_id = request.get("OperationId") or short_uid()
×
1146
        operation = {
×
1147
            "OperationId": op_id,
1148
            "StackSetId": stack_set.metadata["StackSetId"],
1149
            "Action": "UPDATE",
1150
            "Status": "SUCCEEDED",
1151
        }
1152
        stack_set.operations[op_id] = operation
×
1153
        return UpdateStackSetOutput(OperationId=op_id)
×
1154

1155
    @handler("DeleteStackSet")
1✔
1156
    def delete_stack_set(
1✔
1157
        self,
1158
        context: RequestContext,
1159
        stack_set_name: StackSetName,
1160
        call_as: CallAs = None,
1161
        **kwargs,
1162
    ) -> DeleteStackSetOutput:
1163
        state = get_cloudformation_store(context.account_id, context.region)
1✔
1164
        stack_set = [
1✔
1165
            sset for sset in state.stack_sets.values() if sset.stack_set_name == stack_set_name
1166
        ]
1167

1168
        if not stack_set:
1✔
1169
            return not_found_error(f'Stack set named "{stack_set_name}" does not exist')
×
1170

1171
        # TODO: add a check for remaining stack instances
1172

1173
        for instance in stack_set[0].stack_instances:
1✔
1174
            deployer = template_deployer.TemplateDeployer(
×
1175
                context.account_id, context.region, instance.stack
1176
            )
1177
            deployer.delete_stack()
×
1178
        return DeleteStackSetOutput()
1✔
1179

1180
    @handler("CreateStackInstances", expand=False)
1✔
1181
    def create_stack_instances(
1✔
1182
        self,
1183
        context: RequestContext,
1184
        request: CreateStackInstancesInput,
1185
    ) -> CreateStackInstancesOutput:
1186
        state = get_cloudformation_store(context.account_id, context.region)
1✔
1187

1188
        set_name = request.get("StackSetName")
1✔
1189
        stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]
1✔
1190

1191
        if not stack_set:
1✔
1192
            return not_found_error(f'Stack set named "{set_name}" does not exist')
×
1193

1194
        stack_set = stack_set[0]
1✔
1195
        op_id = request.get("OperationId") or short_uid()
1✔
1196
        sset_meta = stack_set.metadata
1✔
1197
        accounts = request["Accounts"]
1✔
1198
        regions = request["Regions"]
1✔
1199

1200
        stacks_to_await = []
1✔
1201
        for account in accounts:
1✔
1202
            for region in regions:
1✔
1203
                # deploy new stack
1204
                LOG.debug(
1✔
1205
                    'Deploying instance for stack set "%s" in account: %s region %s',
1206
                    set_name,
1207
                    account,
1208
                    region,
1209
                )
1210
                cf_client = connect_to(aws_access_key_id=account, region_name=region).cloudformation
1✔
1211
                kwargs = select_attributes(sset_meta, ["TemplateBody"]) or select_attributes(
1✔
1212
                    sset_meta, ["TemplateURL"]
1213
                )
1214
                stack_name = f"sset-{set_name}-{account}"
1✔
1215

1216
                # skip creation of existing stacks
1217
                if find_stack(context.account_id, context.region, stack_name):
1✔
1218
                    continue
1✔
1219

1220
                result = cf_client.create_stack(StackName=stack_name, **kwargs)
1✔
1221
                stacks_to_await.append((stack_name, account, region))
1✔
1222
                # store stack instance
1223
                instance = {
1✔
1224
                    "StackSetId": sset_meta["StackSetId"],
1225
                    "OperationId": op_id,
1226
                    "Account": account,
1227
                    "Region": region,
1228
                    "StackId": result["StackId"],
1229
                    "Status": "CURRENT",
1230
                    "StackInstanceStatus": {"DetailedStatus": "SUCCEEDED"},
1231
                }
1232
                instance = StackInstance(instance)
1✔
1233
                stack_set.stack_instances.append(instance)
1✔
1234

1235
        # wait for completion of stack
1236
        for stack_name, account_id, region_name in stacks_to_await:
1✔
1237
            client = connect_to(
1✔
1238
                aws_access_key_id=account_id, region_name=region_name
1239
            ).cloudformation
1240
            client.get_waiter("stack_create_complete").wait(StackName=stack_name)
1✔
1241

1242
        # record operation
1243
        operation = {
1✔
1244
            "OperationId": op_id,
1245
            "StackSetId": stack_set.metadata["StackSetId"],
1246
            "Action": "CREATE",
1247
            "Status": "SUCCEEDED",
1248
        }
1249
        stack_set.operations[op_id] = operation
1✔
1250

1251
        return CreateStackInstancesOutput(OperationId=op_id)
1✔
1252

1253
    @handler("ListStackInstances", expand=False)
1✔
1254
    def list_stack_instances(
1✔
1255
        self,
1256
        context: RequestContext,
1257
        request: ListStackInstancesInput,
1258
    ) -> ListStackInstancesOutput:
1259
        set_name = request.get("StackSetName")
×
1260
        state = get_cloudformation_store(context.account_id, context.region)
×
1261
        stack_set = [sset for sset in state.stack_sets.values() if sset.stack_set_name == set_name]
×
1262
        if not stack_set:
×
1263
            return not_found_error(f'Stack set named "{set_name}" does not exist')
×
1264

1265
        stack_set = stack_set[0]
×
1266
        result = [inst.metadata for inst in stack_set.stack_instances]
×
1267
        return ListStackInstancesOutput(Summaries=result)
×
1268

1269
    @handler("DeleteStackInstances", expand=False)
1✔
1270
    def delete_stack_instances(
1✔
1271
        self,
1272
        context: RequestContext,
1273
        request: DeleteStackInstancesInput,
1274
    ) -> DeleteStackInstancesOutput:
1275
        op_id = request.get("OperationId") or short_uid()
1✔
1276

1277
        accounts = request["Accounts"]
1✔
1278
        regions = request["Regions"]
1✔
1279

1280
        state = get_cloudformation_store(context.account_id, context.region)
1✔
1281
        stack_sets = state.stack_sets.values()
1✔
1282

1283
        set_name = request.get("StackSetName")
1✔
1284
        stack_set = next((sset for sset in stack_sets if sset.stack_set_name == set_name), None)
1✔
1285

1286
        if not stack_set:
1✔
1287
            return not_found_error(f'Stack set named "{set_name}" does not exist')
×
1288

1289
        for account in accounts:
1✔
1290
            for region in regions:
1✔
1291
                instance = find_stack_instance(stack_set, account, region)
1✔
1292
                if instance:
1✔
1293
                    stack_set.stack_instances.remove(instance)
1✔
1294

1295
        # record operation
1296
        operation = {
1✔
1297
            "OperationId": op_id,
1298
            "StackSetId": stack_set.metadata["StackSetId"],
1299
            "Action": "DELETE",
1300
            "Status": "SUCCEEDED",
1301
        }
1302
        stack_set.operations[op_id] = operation
1✔
1303

1304
        return DeleteStackInstancesOutput(OperationId=op_id)
1✔
1305

1306
    @handler("RegisterType", expand=False)
1✔
1307
    def register_type(
1✔
1308
        self,
1309
        context: RequestContext,
1310
        request: RegisterTypeInput,
1311
    ) -> RegisterTypeOutput:
1312
        return RegisterTypeOutput()
×
1313

1314
    def list_types(
1✔
1315
        self, context: RequestContext, request: ListTypesInput, **kwargs
1316
    ) -> ListTypesOutput:
1317
        def is_list_overridden(child_class, parent_class):
×
1318
            if hasattr(child_class, "list"):
×
1319
                import inspect
×
1320

1321
                child_method = child_class.list
×
1322
                parent_method = parent_class.list
×
1323
                return inspect.unwrap(child_method) is not inspect.unwrap(parent_method)
×
1324
            return False
×
1325

1326
        def get_listable_types_summaries(plugin_manager):
×
1327
            plugins = plugin_manager.list_names()
×
1328
            type_summaries = []
×
1329
            for plugin in plugins:
×
1330
                type_summary = TypeSummary(
×
1331
                    Type=RegistryType.RESOURCE,
1332
                    TypeName=plugin,
1333
                )
1334
                provider = plugin_manager.load(plugin)
×
1335
                if is_list_overridden(provider.factory, ResourceProvider):
×
1336
                    type_summaries.append(type_summary)
×
1337
            return type_summaries
×
1338

1339
        from localstack.services.cloudformation.resource_provider import (
×
1340
            plugin_manager,
1341
        )
1342

1343
        type_summaries = get_listable_types_summaries(plugin_manager)
×
1344
        if PRO_RESOURCE_PROVIDERS:
×
1345
            from localstack.services.cloudformation.resource_provider import (
×
1346
                pro_plugin_manager,
1347
            )
1348

1349
            type_summaries.extend(get_listable_types_summaries(pro_plugin_manager))
×
1350

1351
        return ListTypesOutput(TypeSummaries=type_summaries)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc