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