localstack-core 4.14.1.dev48__py3-none-any.whl → 4.14.1.dev50__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.
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +3 -3
- localstack/services/cloudformation/engine/v2/resolving.py +95 -0
- localstack/services/cloudformation/v2/entities.py +148 -5
- localstack/services/cloudformation/v2/provider.py +32 -259
- localstack/services/iam/models.py +215 -0
- localstack/services/iam/policy_validation.py +561 -0
- localstack/services/iam/provider.py +5358 -448
- localstack/services/iam/resources/aws_managed_policies.json +143872 -0
- localstack/services/iam/resources/policy_simulator.py +33 -43
- localstack/services/iam/utils.py +244 -0
- localstack/services/providers.py +2 -4
- localstack/services/s3/presigned_url.py +5 -12
- localstack/services/sts/models.py +52 -8
- localstack/services/sts/provider.py +460 -51
- localstack/testing/pytest/fixtures.py +146 -1
- localstack/testing/snapshots/transformer_utility.py +4 -0
- localstack/version.py +2 -2
- {localstack_core-4.14.1.dev48.dist-info → localstack_core-4.14.1.dev50.dist-info}/METADATA +2 -1
- {localstack_core-4.14.1.dev48.dist-info → localstack_core-4.14.1.dev50.dist-info}/RECORD +24 -21
- localstack/services/iam/iam_patches.py +0 -163
- {localstack_core-4.14.1.dev48.data → localstack_core-4.14.1.dev50.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.14.1.dev48.dist-info → localstack_core-4.14.1.dev50.dist-info}/WHEEL +0 -0
- {localstack_core-4.14.1.dev48.dist-info → localstack_core-4.14.1.dev50.dist-info}/entry_points.txt +0 -0
- {localstack_core-4.14.1.dev48.dist-info → localstack_core-4.14.1.dev50.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.14.1.dev48.dist-info → localstack_core-4.14.1.dev50.dist-info}/top_level.txt +0 -0
|
@@ -97,9 +97,9 @@ class ChangeSetModelTransform(ChangeSetModelPreproc):
|
|
|
97
97
|
|
|
98
98
|
def __init__(
|
|
99
99
|
self,
|
|
100
|
-
change_set: ChangeSet,
|
|
101
|
-
before_parameters: dict,
|
|
102
|
-
after_parameters: dict,
|
|
100
|
+
change_set: "ChangeSet",
|
|
101
|
+
before_parameters: dict | None,
|
|
102
|
+
after_parameters: dict | None,
|
|
103
103
|
before_template: dict | None,
|
|
104
104
|
after_template: dict | None,
|
|
105
105
|
):
|
|
@@ -7,6 +7,10 @@ from typing import Any
|
|
|
7
7
|
from botocore.exceptions import ClientError
|
|
8
8
|
|
|
9
9
|
from localstack.aws.connect import connect_to
|
|
10
|
+
from localstack.services.cloudformation.engine.parameters import resolve_ssm_parameter
|
|
11
|
+
from localstack.services.cloudformation.engine.validations import ValidationError
|
|
12
|
+
from localstack.services.cloudformation.v2.types import EngineParameter, engine_parameter_value
|
|
13
|
+
from localstack.utils.numbers import is_number
|
|
10
14
|
|
|
11
15
|
LOG = logging.getLogger(__name__)
|
|
12
16
|
|
|
@@ -14,6 +18,10 @@ LOG = logging.getLogger(__name__)
|
|
|
14
18
|
# we don't capture the parameter usage by excluding ${} characters
|
|
15
19
|
REGEX_DYNAMIC_REF = re.compile(r"{{resolve:([^:]+):([^${}]+)}}")
|
|
16
20
|
|
|
21
|
+
SSM_PARAMETER_TYPE_RE = re.compile(
|
|
22
|
+
r"^AWS::SSM::Parameter::Value<(?P<listtype>List<)?(?P<innertype>[^>]+)>?>$"
|
|
23
|
+
)
|
|
24
|
+
|
|
17
25
|
|
|
18
26
|
@dataclass
|
|
19
27
|
class DynamicReference:
|
|
@@ -100,3 +108,90 @@ def perform_dynamic_reference_lookup(
|
|
|
100
108
|
"Unsupported service for dynamic parameter: service_name=%s", reference.service_name
|
|
101
109
|
)
|
|
102
110
|
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def resolve_parameters(
|
|
114
|
+
template: dict | None,
|
|
115
|
+
parameters: dict | None,
|
|
116
|
+
account_id: str,
|
|
117
|
+
region_name: str,
|
|
118
|
+
before_parameters: dict | None,
|
|
119
|
+
) -> dict[str, EngineParameter]:
|
|
120
|
+
template_parameters = template.get("Parameters", {})
|
|
121
|
+
resolved_parameters = {}
|
|
122
|
+
invalid_parameters = []
|
|
123
|
+
for name, parameter in template_parameters.items():
|
|
124
|
+
given_value = parameters.get(name)
|
|
125
|
+
default_value = parameter.get("Default")
|
|
126
|
+
resolved_parameter = EngineParameter(
|
|
127
|
+
type_=parameter["Type"],
|
|
128
|
+
given_value=given_value,
|
|
129
|
+
default_value=default_value,
|
|
130
|
+
no_echo=parameter.get("NoEcho"),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# validate the type
|
|
134
|
+
if parameter["Type"] == "Number" and not is_number(
|
|
135
|
+
engine_parameter_value(resolved_parameter)
|
|
136
|
+
):
|
|
137
|
+
raise ValidationError(f"Parameter '{name}' must be a number.")
|
|
138
|
+
|
|
139
|
+
# TODO: support other parameter types
|
|
140
|
+
if match := SSM_PARAMETER_TYPE_RE.match(parameter["Type"]):
|
|
141
|
+
inner_type = match.group("innertype")
|
|
142
|
+
is_list_type = match.group("listtype") is not None
|
|
143
|
+
if is_list_type or inner_type == "CommaDelimitedList":
|
|
144
|
+
# list types
|
|
145
|
+
try:
|
|
146
|
+
resolved_value = resolve_ssm_parameter(
|
|
147
|
+
account_id, region_name, given_value or default_value
|
|
148
|
+
)
|
|
149
|
+
resolved_parameter["resolved_value"] = resolved_value.split(",")
|
|
150
|
+
except Exception:
|
|
151
|
+
raise ValidationError(
|
|
152
|
+
f"Parameter {name} should either have input value or default value"
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
try:
|
|
156
|
+
resolved_parameter["resolved_value"] = resolve_ssm_parameter(
|
|
157
|
+
account_id, region_name, given_value or default_value
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
# we could not find the parameter however CDK provides the resolved value rather than the
|
|
161
|
+
# parameter name again so try to look up the value in the previous parameters
|
|
162
|
+
if (
|
|
163
|
+
before_parameters
|
|
164
|
+
and (before_param := before_parameters.get(name))
|
|
165
|
+
and isinstance(before_param, dict)
|
|
166
|
+
and (resolved_value := before_param.get("resolved_value"))
|
|
167
|
+
):
|
|
168
|
+
LOG.debug(
|
|
169
|
+
"Parameter %s could not be resolved, using previous value of %s",
|
|
170
|
+
name,
|
|
171
|
+
resolved_value,
|
|
172
|
+
)
|
|
173
|
+
resolved_parameter["resolved_value"] = resolved_value
|
|
174
|
+
else:
|
|
175
|
+
raise ValidationError(
|
|
176
|
+
f"Parameter {name} should either have input value or default value"
|
|
177
|
+
) from e
|
|
178
|
+
elif given_value is None and default_value is None:
|
|
179
|
+
invalid_parameters.append(name)
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
resolved_parameters[name] = resolved_parameter
|
|
183
|
+
|
|
184
|
+
if invalid_parameters:
|
|
185
|
+
raise ValidationError(f"Parameters: [{','.join(invalid_parameters)}] must have values")
|
|
186
|
+
|
|
187
|
+
for name, parameter in resolved_parameters.items():
|
|
188
|
+
if (
|
|
189
|
+
parameter.get("resolved_value") is None
|
|
190
|
+
and parameter.get("given_value") is None
|
|
191
|
+
and parameter.get("default_value") is None
|
|
192
|
+
):
|
|
193
|
+
raise ValidationError(
|
|
194
|
+
f"Parameter {name} should either have input value or default value"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return resolved_parameters
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from datetime import UTC, datetime
|
|
2
3
|
from typing import NotRequired, TypedDict
|
|
3
4
|
|
|
5
|
+
from localstack import config
|
|
4
6
|
from localstack.aws.api.cloudformation import (
|
|
5
7
|
Capability,
|
|
6
8
|
ChangeSetStatus,
|
|
@@ -28,9 +30,14 @@ from localstack.services.cloudformation.engine.entities import (
|
|
|
28
30
|
StackIdentifierV2,
|
|
29
31
|
)
|
|
30
32
|
from localstack.services.cloudformation.engine.v2.change_set_model import (
|
|
33
|
+
ChangeSetModel,
|
|
31
34
|
ChangeType,
|
|
32
35
|
UpdateModel,
|
|
33
36
|
)
|
|
37
|
+
from localstack.services.cloudformation.engine.v2.change_set_resource_support_checker import (
|
|
38
|
+
ChangeSetResourceSupportChecker,
|
|
39
|
+
)
|
|
40
|
+
from localstack.services.cloudformation.engine.v2.resolving import resolve_parameters
|
|
34
41
|
from localstack.services.cloudformation.v2.types import EngineParameter, ResolvedResource
|
|
35
42
|
from localstack.utils.aws import arns
|
|
36
43
|
from localstack.utils.strings import long_uid, short_uid
|
|
@@ -190,11 +197,19 @@ class ChangeSetRequestPayload(TypedDict, total=False):
|
|
|
190
197
|
ChangeSetType: NotRequired[ChangeSetType]
|
|
191
198
|
|
|
192
199
|
|
|
200
|
+
@dataclass
|
|
201
|
+
class UpdateModelInputs:
|
|
202
|
+
before_template: dict | None
|
|
203
|
+
after_template: dict | None
|
|
204
|
+
before_parameters: dict | None
|
|
205
|
+
after_parameters: dict | None
|
|
206
|
+
previous_update_model: UpdateModel | None = None
|
|
207
|
+
|
|
208
|
+
|
|
193
209
|
class ChangeSet:
|
|
194
210
|
change_set_name: str
|
|
195
211
|
change_set_id: str
|
|
196
212
|
change_set_type: ChangeSetType
|
|
197
|
-
update_model: UpdateModel | None
|
|
198
213
|
status: ChangeSetStatus
|
|
199
214
|
status_reason: str | None
|
|
200
215
|
execution_status: ExecutionStatus
|
|
@@ -217,7 +232,6 @@ class ChangeSet:
|
|
|
217
232
|
self.status = ChangeSetStatus.CREATE_IN_PROGRESS
|
|
218
233
|
self.status_reason = None
|
|
219
234
|
self.execution_status = ExecutionStatus.AVAILABLE
|
|
220
|
-
self.update_model = None
|
|
221
235
|
self.creation_time = datetime.now(tz=UTC)
|
|
222
236
|
self.resolved_parameters = {}
|
|
223
237
|
self.tags = request_payload.get("Tags") or []
|
|
@@ -232,9 +246,9 @@ class ChangeSet:
|
|
|
232
246
|
region_name=self.stack.region_name,
|
|
233
247
|
)
|
|
234
248
|
self.processed_template = None
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
self.
|
|
249
|
+
# Do not add the `_cached_update_model` attribute to the type annotations of the class. This is a runtime-only
|
|
250
|
+
# attribute that cannot be serialized by our persistence framework.
|
|
251
|
+
self._cached_update_model = None
|
|
238
252
|
|
|
239
253
|
def set_change_set_status(self, status: ChangeSetStatus):
|
|
240
254
|
self.status = status
|
|
@@ -243,6 +257,8 @@ class ChangeSet:
|
|
|
243
257
|
self.execution_status = execution_status
|
|
244
258
|
|
|
245
259
|
def has_changes(self) -> bool:
|
|
260
|
+
if self.update_model is None:
|
|
261
|
+
raise ValueError("update model has not been computed")
|
|
246
262
|
return self.update_model.node_template.change_type != ChangeType.UNCHANGED
|
|
247
263
|
|
|
248
264
|
@property
|
|
@@ -253,6 +269,133 @@ class ChangeSet:
|
|
|
253
269
|
def region_name(self) -> str:
|
|
254
270
|
return self.stack.region_name
|
|
255
271
|
|
|
272
|
+
@property
|
|
273
|
+
def update_model(self) -> UpdateModel | None:
|
|
274
|
+
# non-persisted state, runtime cache
|
|
275
|
+
# TODO: maybe move out of the `ChangeSet` class into the provider
|
|
276
|
+
return self._cached_update_model
|
|
277
|
+
|
|
278
|
+
def compute_update_model(self, inputs: UpdateModelInputs):
|
|
279
|
+
from localstack.services.cloudformation.engine.transformers import (
|
|
280
|
+
FailedTransformationException,
|
|
281
|
+
)
|
|
282
|
+
from localstack.services.cloudformation.engine.v2.change_set_model_transform import (
|
|
283
|
+
ChangeSetModelTransform,
|
|
284
|
+
)
|
|
285
|
+
from localstack.services.cloudformation.engine.v2.change_set_model_validator import (
|
|
286
|
+
ChangeSetModelValidator,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
resolved_parameters = None
|
|
290
|
+
if inputs.after_parameters is not None:
|
|
291
|
+
resolved_parameters = resolve_parameters(
|
|
292
|
+
inputs.after_template,
|
|
293
|
+
inputs.after_parameters,
|
|
294
|
+
self.account_id,
|
|
295
|
+
self.region_name,
|
|
296
|
+
inputs.before_parameters,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
self.resolved_parameters = resolved_parameters or {}
|
|
300
|
+
|
|
301
|
+
# Create and preprocess the update graph for this template update.
|
|
302
|
+
change_set_model = ChangeSetModel(
|
|
303
|
+
before_template=inputs.before_template,
|
|
304
|
+
after_template=inputs.after_template,
|
|
305
|
+
before_parameters=inputs.before_parameters,
|
|
306
|
+
after_parameters=resolved_parameters,
|
|
307
|
+
)
|
|
308
|
+
raw_update_model: UpdateModel = change_set_model.get_update_model()
|
|
309
|
+
# If there exists an update model which operated in the 'before' version of this change set,
|
|
310
|
+
# port the runtime values computed for the before version into this latest update model.
|
|
311
|
+
if inputs.previous_update_model:
|
|
312
|
+
raw_update_model.before_runtime_cache.clear()
|
|
313
|
+
raw_update_model.before_runtime_cache.update(
|
|
314
|
+
inputs.previous_update_model.after_runtime_cache
|
|
315
|
+
)
|
|
316
|
+
self._cached_update_model = raw_update_model
|
|
317
|
+
|
|
318
|
+
# Apply global transforms.
|
|
319
|
+
# TODO: skip this process iff both versions of the template don't specify transform blocks.
|
|
320
|
+
change_set_model_transform = ChangeSetModelTransform(
|
|
321
|
+
change_set=self,
|
|
322
|
+
before_parameters=inputs.before_parameters,
|
|
323
|
+
after_parameters=resolved_parameters,
|
|
324
|
+
before_template=inputs.before_template,
|
|
325
|
+
after_template=inputs.after_template,
|
|
326
|
+
)
|
|
327
|
+
try:
|
|
328
|
+
transformed_before_template, transformed_after_template = (
|
|
329
|
+
change_set_model_transform.transform()
|
|
330
|
+
)
|
|
331
|
+
except FailedTransformationException as e:
|
|
332
|
+
self.status = ChangeSetStatus.FAILED
|
|
333
|
+
self.status_reason = e.message
|
|
334
|
+
self.stack.set_stack_status(status=StackStatus.ROLLBACK_IN_PROGRESS, reason=e.message)
|
|
335
|
+
self.stack.set_stack_status(status=StackStatus.CREATE_FAILED)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Remodel the update graph after the applying the global transforms.
|
|
339
|
+
change_set_model = ChangeSetModel(
|
|
340
|
+
before_template=transformed_before_template,
|
|
341
|
+
after_template=transformed_after_template,
|
|
342
|
+
before_parameters=inputs.before_parameters,
|
|
343
|
+
after_parameters=resolved_parameters,
|
|
344
|
+
)
|
|
345
|
+
update_model = change_set_model.get_update_model()
|
|
346
|
+
# Bring the cache for the previous operations forward in the update graph for this version
|
|
347
|
+
# of the templates. This enables downstream update graph visitors to access runtime
|
|
348
|
+
# information computed whilst evaluating the previous version of this template, and during
|
|
349
|
+
# the transformations.
|
|
350
|
+
update_model.before_runtime_cache.update(raw_update_model.before_runtime_cache)
|
|
351
|
+
update_model.after_runtime_cache.update(raw_update_model.after_runtime_cache)
|
|
352
|
+
self._cached_update_model = update_model
|
|
353
|
+
|
|
354
|
+
# perform validations
|
|
355
|
+
validator = ChangeSetModelValidator(
|
|
356
|
+
change_set=self,
|
|
357
|
+
)
|
|
358
|
+
validator.validate()
|
|
359
|
+
|
|
360
|
+
# hacky
|
|
361
|
+
if transform := raw_update_model.node_template.transform:
|
|
362
|
+
if transform.global_transforms:
|
|
363
|
+
# global transforms should always be considered "MODIFIED"
|
|
364
|
+
update_model.node_template.change_type = ChangeType.MODIFIED
|
|
365
|
+
self.processed_template = transformed_after_template
|
|
366
|
+
|
|
367
|
+
if not config.CFN_IGNORE_UNSUPPORTED_RESOURCE_TYPES:
|
|
368
|
+
support_visitor = ChangeSetResourceSupportChecker(change_set_type=self.change_set_type)
|
|
369
|
+
support_visitor.visit(self._cached_update_model.node_template)
|
|
370
|
+
failure_messages = support_visitor.failure_messages
|
|
371
|
+
if failure_messages:
|
|
372
|
+
reason_suffix = ", ".join(failure_messages)
|
|
373
|
+
status_reason = f"{ChangeSetResourceSupportChecker.TITLE_MESSAGE} {reason_suffix}"
|
|
374
|
+
|
|
375
|
+
self.status_reason = status_reason
|
|
376
|
+
self.set_change_set_status(ChangeSetStatus.FAILED)
|
|
377
|
+
failure_transitions = {
|
|
378
|
+
ChangeSetType.CREATE: (
|
|
379
|
+
StackStatus.ROLLBACK_IN_PROGRESS,
|
|
380
|
+
StackStatus.CREATE_FAILED,
|
|
381
|
+
),
|
|
382
|
+
ChangeSetType.UPDATE: (
|
|
383
|
+
StackStatus.UPDATE_ROLLBACK_IN_PROGRESS,
|
|
384
|
+
StackStatus.UPDATE_ROLLBACK_FAILED,
|
|
385
|
+
),
|
|
386
|
+
ChangeSetType.IMPORT: (
|
|
387
|
+
StackStatus.IMPORT_ROLLBACK_IN_PROGRESS,
|
|
388
|
+
StackStatus.IMPORT_ROLLBACK_FAILED,
|
|
389
|
+
),
|
|
390
|
+
}
|
|
391
|
+
transitions = failure_transitions.get(self.change_set_type)
|
|
392
|
+
if transitions:
|
|
393
|
+
first_status, *remaining_statuses = transitions
|
|
394
|
+
self.stack.set_stack_status(first_status, status_reason)
|
|
395
|
+
for status in remaining_statuses:
|
|
396
|
+
self.stack.set_stack_status(status)
|
|
397
|
+
return
|
|
398
|
+
|
|
256
399
|
|
|
257
400
|
class StackInstance:
|
|
258
401
|
def __init__(
|