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

localstack / localstack / 287d2d5f-77c6-4688-a935-5247d99c1a92

18 Mar 2025 09:15PM UTC coverage: 86.828% (-0.09%) from 86.915%
287d2d5f-77c6-4688-a935-5247d99c1a92

push

circleci

web-flow
APIGW: migrate to new Counter type for REST API analytics (#12383)

8 of 8 new or added lines in 1 file covered. (100.0%)

129 existing lines in 10 files now uncovered.

62766 of 72288 relevant lines covered (86.83%)

0.87 hits per line

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

18.52
/localstack-core/localstack/services/cloudformation/v2/provider.py
1
from copy import deepcopy
1✔
2

3
from localstack.aws.api import RequestContext, handler
1✔
4
from localstack.aws.api.cloudformation import (
1✔
5
    ChangeSetNameOrId,
6
    ChangeSetNotFoundException,
7
    ChangeSetType,
8
    CreateChangeSetInput,
9
    CreateChangeSetOutput,
10
    DescribeChangeSetOutput,
11
    IncludePropertyValues,
12
    NextToken,
13
    Parameter,
14
    StackNameOrId,
15
    StackStatus,
16
)
17
from localstack.services.cloudformation import api_utils
1✔
18
from localstack.services.cloudformation.engine import parameters as param_resolver
1✔
19
from localstack.services.cloudformation.engine import template_deployer, template_preparer
1✔
20
from localstack.services.cloudformation.engine.entities import Stack, StackChangeSet
1✔
21
from localstack.services.cloudformation.engine.parameters import mask_no_echo, strip_parameter_type
1✔
22
from localstack.services.cloudformation.engine.resource_ordering import (
1✔
23
    NoResourceInStack,
24
    order_resources,
25
)
26
from localstack.services.cloudformation.engine.template_utils import resolve_stack_conditions
1✔
27
from localstack.services.cloudformation.engine.v2.change_set_model_describer import (
1✔
28
    ChangeSetModelDescriber,
29
)
30
from localstack.services.cloudformation.engine.validations import ValidationError
1✔
31
from localstack.services.cloudformation.provider import (
1✔
32
    ARN_CHANGESET_REGEX,
33
    ARN_STACK_REGEX,
34
    CloudformationProvider,
35
    clone_stack_params,
36
)
37
from localstack.services.cloudformation.stores import (
1✔
38
    find_change_set,
39
    find_stack,
40
    get_cloudformation_store,
41
)
42
from localstack.utils.collections import remove_attributes
1✔
43

44

45
class CloudformationProviderV2(CloudformationProvider):
1✔
46
    @handler("CreateChangeSet", expand=False)
1✔
47
    def create_change_set(
1✔
48
        self, context: RequestContext, request: CreateChangeSetInput
49
    ) -> CreateChangeSetOutput:
UNCOV
50
        state = get_cloudformation_store(context.account_id, context.region)
×
51

UNCOV
52
        req_params = request
×
UNCOV
53
        change_set_type = req_params.get("ChangeSetType", "UPDATE")
×
UNCOV
54
        stack_name = req_params.get("StackName")
×
UNCOV
55
        change_set_name = req_params.get("ChangeSetName")
×
UNCOV
56
        template_body = req_params.get("TemplateBody")
×
57
        # s3 or secretsmanager url
UNCOV
58
        template_url = req_params.get("TemplateURL")
×
59

60
        # validate and resolve template
UNCOV
61
        if template_body and template_url:
×
UNCOV
62
            raise ValidationError(
×
63
                "Specify exactly one of 'TemplateBody' or 'TemplateUrl'"
64
            )  # TODO: check proper message
65

UNCOV
66
        if not template_body and not template_url:
×
UNCOV
67
            raise ValidationError(
×
68
                "Specify exactly one of 'TemplateBody' or 'TemplateUrl'"
69
            )  # TODO: check proper message
70

UNCOV
71
        api_utils.prepare_template_body(
×
72
            req_params
73
        )  # TODO: function has too many unclear responsibilities
UNCOV
74
        if not template_body:
×
UNCOV
75
            template_body = req_params[
×
76
                "TemplateBody"
77
            ]  # should then have been set by prepare_template_body
UNCOV
78
        template = template_preparer.parse_template(req_params["TemplateBody"])
×
79

UNCOV
80
        del req_params["TemplateBody"]  # TODO: stop mutating req_params
×
UNCOV
81
        template["StackName"] = stack_name
×
82
        # TODO: validate with AWS what this is actually doing?
UNCOV
83
        template["ChangeSetName"] = change_set_name
×
84

85
        # this is intentionally not in a util yet. Let's first see how the different operations deal with these before generalizing
86
        # handle ARN stack_name here (not valid for initial CREATE, since stack doesn't exist yet)
UNCOV
87
        if ARN_STACK_REGEX.match(stack_name):
×
UNCOV
88
            if not (stack := state.stacks.get(stack_name)):
×
UNCOV
89
                raise ValidationError(f"Stack '{stack_name}' does not exist.")
×
90
        else:
91
            # stack name specified, so fetch the stack by name
UNCOV
92
            stack_candidates: list[Stack] = [
×
93
                s for stack_arn, s in state.stacks.items() if s.stack_name == stack_name
94
            ]
UNCOV
95
            active_stack_candidates = [
×
96
                s for s in stack_candidates if self._stack_status_is_active(s.status)
97
            ]
98

99
            # on a CREATE an empty Stack should be generated if we didn't find an active one
UNCOV
100
            if not active_stack_candidates and change_set_type == ChangeSetType.CREATE:
×
UNCOV
101
                empty_stack_template = dict(template)
×
UNCOV
102
                empty_stack_template["Resources"] = {}
×
UNCOV
103
                req_params_copy = clone_stack_params(req_params)
×
UNCOV
104
                stack = Stack(
×
105
                    context.account_id,
106
                    context.region,
107
                    req_params_copy,
108
                    empty_stack_template,
109
                    template_body=template_body,
110
                )
UNCOV
111
                state.stacks[stack.stack_id] = stack
×
UNCOV
112
                stack.set_stack_status("REVIEW_IN_PROGRESS")
×
113
            else:
UNCOV
114
                if not active_stack_candidates:
×
UNCOV
115
                    raise ValidationError(f"Stack '{stack_name}' does not exist.")
×
UNCOV
116
                stack = active_stack_candidates[0]
×
117

118
        # TODO: test if rollback status is allowed as well
UNCOV
119
        if (
×
120
            change_set_type == ChangeSetType.CREATE
121
            and stack.status != StackStatus.REVIEW_IN_PROGRESS
122
        ):
UNCOV
123
            raise ValidationError(
×
124
                f"Stack [{stack_name}] already exists and cannot be created again with the changeSet [{change_set_name}]."
125
            )
126

UNCOV
127
        old_parameters: dict[str, Parameter] = {}
×
UNCOV
128
        match change_set_type:
×
UNCOV
129
            case ChangeSetType.UPDATE:
×
130
                # add changeset to existing stack
UNCOV
131
                old_parameters = {
×
132
                    k: mask_no_echo(strip_parameter_type(v))
133
                    for k, v in stack.resolved_parameters.items()
134
                }
UNCOV
135
            case ChangeSetType.IMPORT:
×
136
                raise NotImplementedError()  # TODO: implement importing resources
UNCOV
137
            case ChangeSetType.CREATE:
×
UNCOV
138
                pass
×
UNCOV
139
            case _:
×
UNCOV
140
                msg = (
×
141
                    f"1 validation error detected: Value '{change_set_type}' at 'changeSetType' failed to satisfy "
142
                    f"constraint: Member must satisfy enum value set: [IMPORT, UPDATE, CREATE] "
143
                )
UNCOV
144
                raise ValidationError(msg)
×
145

146
        # resolve parameters
UNCOV
147
        new_parameters: dict[str, Parameter] = param_resolver.convert_stack_parameters_to_dict(
×
148
            request.get("Parameters")
149
        )
UNCOV
150
        parameter_declarations = param_resolver.extract_stack_parameter_declarations(template)
×
UNCOV
151
        resolved_parameters = param_resolver.resolve_parameters(
×
152
            account_id=context.account_id,
153
            region_name=context.region,
154
            parameter_declarations=parameter_declarations,
155
            new_parameters=new_parameters,
156
            old_parameters=old_parameters,
157
        )
158

159
        # TODO: remove this when fixing Stack.resources and transformation order
160
        #   currently we need to create a stack with existing resources + parameters so that resolve refs recursively in here will work.
161
        #   The correct way to do it would be at a later stage anyway just like a normal intrinsic function
UNCOV
162
        req_params_copy = clone_stack_params(req_params)
×
UNCOV
163
        temp_stack = Stack(context.account_id, context.region, req_params_copy, template)
×
UNCOV
164
        temp_stack.set_resolved_parameters(resolved_parameters)
×
165

166
        # TODO: everything below should be async
167
        # apply template transformations
UNCOV
168
        transformed_template = template_preparer.transform_template(
×
169
            context.account_id,
170
            context.region,
171
            template,
172
            stack_name=temp_stack.stack_name,
173
            resources=temp_stack.resources,
174
            mappings=temp_stack.mappings,
175
            conditions={},  # TODO: we don't have any resolved conditions yet at this point but we need the conditions because of the samtranslator...
176
            resolved_parameters=resolved_parameters,
177
        )
178

179
        # create change set for the stack and apply changes
UNCOV
180
        change_set = StackChangeSet(
×
181
            context.account_id, context.region, stack, req_params, transformed_template
182
        )
183
        # only set parameters for the changeset, then switch to stack on execute_change_set
UNCOV
184
        change_set.template_body = template_body
×
UNCOV
185
        change_set.populate_update_graph(stack.template, transformed_template)
×
186

187
        # TODO: evaluate conditions
UNCOV
188
        raw_conditions = transformed_template.get("Conditions", {})
×
UNCOV
189
        resolved_stack_conditions = resolve_stack_conditions(
×
190
            account_id=context.account_id,
191
            region_name=context.region,
192
            conditions=raw_conditions,
193
            parameters=resolved_parameters,
194
            mappings=temp_stack.mappings,
195
            stack_name=stack_name,
196
        )
UNCOV
197
        change_set.set_resolved_stack_conditions(resolved_stack_conditions)
×
198

199
        # a bit gross but use the template ordering to validate missing resources
UNCOV
200
        try:
×
UNCOV
201
            order_resources(
×
202
                transformed_template["Resources"],
203
                resolved_parameters=resolved_parameters,
204
                resolved_conditions=resolved_stack_conditions,
205
            )
UNCOV
206
        except NoResourceInStack as e:
×
UNCOV
207
            raise ValidationError(str(e)) from e
×
208

UNCOV
209
        deployer = template_deployer.TemplateDeployer(
×
210
            context.account_id, context.region, change_set
211
        )
UNCOV
212
        changes = deployer.construct_changes(
×
213
            stack,
214
            change_set,
215
            change_set_id=change_set.change_set_id,
216
            append_to_changeset=True,
217
            filter_unchanged_resources=True,
218
        )
UNCOV
219
        stack.change_sets.append(change_set)
×
UNCOV
220
        if not changes:
×
UNCOV
221
            change_set.metadata["Status"] = "FAILED"
×
UNCOV
222
            change_set.metadata["ExecutionStatus"] = "UNAVAILABLE"
×
UNCOV
223
            change_set.metadata["StatusReason"] = (
×
224
                "The submitted information didn't contain changes. Submit different information to create a change set."
225
            )
226
        else:
UNCOV
227
            change_set.metadata["Status"] = (
×
228
                "CREATE_COMPLETE"  # technically for some time this should first be CREATE_PENDING
229
            )
UNCOV
230
            change_set.metadata["ExecutionStatus"] = (
×
231
                "AVAILABLE"  # technically for some time this should first be UNAVAILABLE
232
            )
233

UNCOV
234
        return CreateChangeSetOutput(StackId=change_set.stack_id, Id=change_set.change_set_id)
×
235

236
    @handler("DescribeChangeSet")
1✔
237
    def describe_change_set(
1✔
238
        self,
239
        context: RequestContext,
240
        change_set_name: ChangeSetNameOrId,
241
        stack_name: StackNameOrId = None,
242
        next_token: NextToken = None,
243
        include_property_values: IncludePropertyValues = None,
244
        **kwargs,
245
    ) -> DescribeChangeSetOutput:
246
        # TODO add support for include_property_values
247
        # only relevant if change_set_name isn't an ARN
UNCOV
248
        if not ARN_CHANGESET_REGEX.match(change_set_name):
×
UNCOV
249
            if not stack_name:
×
UNCOV
250
                raise ValidationError(
×
251
                    "StackName must be specified if ChangeSetName is not specified as an ARN."
252
                )
253

UNCOV
254
            stack = find_stack(context.account_id, context.region, stack_name)
×
UNCOV
255
            if not stack:
×
UNCOV
256
                raise ValidationError(f"Stack [{stack_name}] does not exist")
×
257

UNCOV
258
        change_set = find_change_set(
×
259
            context.account_id, context.region, change_set_name, stack_name=stack_name
260
        )
UNCOV
261
        if not change_set:
×
UNCOV
262
            raise ChangeSetNotFoundException(f"ChangeSet [{change_set_name}] does not exist")
×
263

UNCOV
264
        change_set_describer = ChangeSetModelDescriber(node_template=change_set.update_graph)
×
UNCOV
265
        resource_changes = change_set_describer.get_resource_changes()
×
266

UNCOV
267
        attrs = [
×
268
            "ChangeSetType",
269
            "StackStatus",
270
            "LastUpdatedTime",
271
            "DisableRollback",
272
            "EnableTerminationProtection",
273
            "Transform",
274
        ]
UNCOV
275
        result = remove_attributes(deepcopy(change_set.metadata), attrs)
×
276
        # TODO: replace this patch with a better solution
UNCOV
277
        result["Parameters"] = [
×
278
            mask_no_echo(strip_parameter_type(p)) for p in result.get("Parameters", [])
279
        ]
UNCOV
280
        result["Changes"] = resource_changes
×
UNCOV
281
        return result
×
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