Skip to content

Commit d878dfd

Browse files
committed
Merge remote-tracking branch 'origin/main' into cfn/v2/validation-2
2 parents 6c03e4c + 4464cd2 commit d878dfd

File tree

10 files changed

+1580
-55
lines changed

10 files changed

+1580
-55
lines changed

localstack-core/localstack/services/cdk/resource_providers/cdk_metadata.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,9 @@ def update(
8383
8484
"""
8585
model = request.desired_state
86+
result_model = {**model, "Id": request.previous_state["Id"]}
8687

8788
return ProgressEvent(
8889
status=OperationStatus.SUCCESS,
89-
resource_model=model,
90+
resource_model=result_model,
9091
)

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ def _resolve_intrinsic_function_fn_if(self, arguments: ChangeSetEntity) -> Chang
728728
)
729729
if not isinstance(node_condition, NodeCondition):
730730
raise RuntimeError()
731-
change_type = parent_change_type_of([node_condition, *arguments[1:]])
731+
change_type = parent_change_type_of([node_condition, *arguments.array[1:]])
732732
return change_type
733733

734734
def _resolve_requires_replacement(

localstack-core/localstack/services/cloudformation/engine/v2/change_set_model_preproc.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -564,21 +564,38 @@ def _compute_fn_equals(args: list[Any]) -> bool:
564564
def visit_node_intrinsic_function_fn_if(
565565
self, node_intrinsic_function: NodeIntrinsicFunction
566566
) -> PreprocEntityDelta:
567-
def _compute_delta_for_if_statement(args: list[Any]) -> PreprocEntityDelta:
568-
condition_name = args[0]
569-
boolean_expression_delta = self._resolve_condition(logical_id=condition_name)
570-
return PreprocEntityDelta(
571-
before=args[1] if boolean_expression_delta.before else args[2],
572-
after=args[1] if boolean_expression_delta.after else args[2],
567+
# `if` needs to be short-circuiting i.e. if the condition is True we don't evaluate the
568+
# False branch. If the condition is False, we don't evaluate the True branch.
569+
if len(node_intrinsic_function.arguments.array) != 3:
570+
raise ValueError(
571+
f"Incorrectly constructed Fn::If usage, expected 3 arguments, found {len(node_intrinsic_function.arguments.array)}"
573572
)
574573

575-
arguments_delta = self.visit(node_intrinsic_function.arguments)
576-
delta = self._cached_apply(
577-
scope=node_intrinsic_function.scope,
578-
arguments_delta=arguments_delta,
579-
resolver=_compute_delta_for_if_statement,
580-
)
581-
return delta
574+
condition_delta = self.visit(node_intrinsic_function.arguments.array[0])
575+
if_delta = PreprocEntityDelta()
576+
if not is_nothing(condition_delta.before):
577+
node_condition = self._get_node_condition_if_exists(
578+
condition_name=condition_delta.before
579+
)
580+
condition_value = self.visit(node_condition).before
581+
if condition_value:
582+
arg_delta = self.visit(node_intrinsic_function.arguments.array[1])
583+
else:
584+
arg_delta = self.visit(node_intrinsic_function.arguments.array[2])
585+
if_delta.before = arg_delta.before
586+
587+
if not is_nothing(condition_delta.after):
588+
node_condition = self._get_node_condition_if_exists(
589+
condition_name=condition_delta.after
590+
)
591+
condition_value = self.visit(node_condition).after
592+
if condition_value:
593+
arg_delta = self.visit(node_intrinsic_function.arguments.array[1])
594+
else:
595+
arg_delta = self.visit(node_intrinsic_function.arguments.array[2])
596+
if_delta.after = arg_delta.after
597+
598+
return if_delta
582599

583600
def visit_node_intrinsic_function_fn_and(
584601
self, node_intrinsic_function: NodeIntrinsicFunction

localstack-core/localstack/services/cloudformation/v2/provider.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -425,10 +425,17 @@ def create_change_set(
425425
# is needed for the update graph building, or only looked up in downstream tasks (metadata).
426426
request_parameters = request.get("Parameters", list())
427427
# TODO: handle parameter defaults and resolution
428-
after_parameters: dict[str, Any] = {
429-
parameter["ParameterKey"]: parameter["ParameterValue"]
430-
for parameter in request_parameters
431-
}
428+
after_parameters = {}
429+
for parameter in request_parameters:
430+
key = parameter["ParameterKey"]
431+
if parameter.get("UsePreviousValue", False):
432+
# todo: what if the parameter does not exist in the before parameters
433+
after_parameters[key] = before_parameters[key]
434+
continue
435+
436+
if "ParameterValue" in parameter:
437+
after_parameters[key] = parameter["ParameterValue"]
438+
continue
432439

433440
# TODO: update this logic to always pass the clean template object if one exists. The
434441
# current issue with relaying on stack.template_original is that this appears to have

localstack-core/localstack/testing/pytest/fixtures.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from werkzeug import Request, Response
2222

2323
from localstack import config
24+
from localstack.aws.api.cloudformation import CreateChangeSetInput, Parameter
2425
from localstack.aws.api.ec2 import CreateSecurityGroupRequest, CreateVpcEndpointRequest, VpcEndpoint
2526
from localstack.aws.connect import ServiceLevelClientFactory
2627
from localstack.services.stores import (
@@ -1095,6 +1096,7 @@ def _deploy(
10951096
max_wait: Optional[int] = None,
10961097
delay_between_polls: Optional[int] = 2,
10971098
custom_aws_client: Optional[ServiceLevelClientFactory] = None,
1099+
raw_parameters: Optional[list[Parameter]] = None,
10981100
) -> DeployResult:
10991101
if is_update:
11001102
assert stack_name
@@ -1110,20 +1112,21 @@ def _deploy(
11101112
raise RuntimeError(f"Could not find file {os.path.realpath(template_path)}")
11111113
template_rendered = render_template(template, **(template_mapping or {}))
11121114

1113-
kwargs = dict(
1115+
kwargs = CreateChangeSetInput(
11141116
StackName=stack_name,
11151117
ChangeSetName=change_set_name,
11161118
TemplateBody=template_rendered,
11171119
Capabilities=["CAPABILITY_AUTO_EXPAND", "CAPABILITY_IAM", "CAPABILITY_NAMED_IAM"],
11181120
ChangeSetType=("UPDATE" if is_update else "CREATE"),
1119-
Parameters=[
1120-
{
1121-
"ParameterKey": k,
1122-
"ParameterValue": v,
1123-
}
1124-
for (k, v) in (parameters or {}).items()
1125-
],
11261121
)
1122+
kwargs["Parameters"] = []
1123+
if parameters:
1124+
kwargs["Parameters"] = [
1125+
Parameter(ParameterKey=k, ParameterValue=v) for (k, v) in parameters.items()
1126+
]
1127+
elif raw_parameters:
1128+
kwargs["Parameters"] = raw_parameters
1129+
11271130
if role_arn is not None:
11281131
kwargs["RoleARN"] = role_arn
11291132

tests/aws/services/cloudformation/resources/test_cdk.py

Lines changed: 122 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,44 @@
11
import os
2+
from collections.abc import Callable
23

34
import pytest
45
from localstack_snapshot.snapshots.transformer import SortingTransformer
5-
from tests.aws.services.cloudformation.conftest import skip_if_v2_provider
6+
from tests.aws.services.cloudformation.conftest import skip_if_v1_provider
67

8+
from localstack.aws.api.cloudformation import Parameter
79
from localstack.testing.pytest import markers
810
from localstack.utils.files import load_file
911
from localstack.utils.strings import short_uid
1012

1113

1214
class TestCdkInit:
13-
@pytest.mark.parametrize("bootstrap_version", ["10", "11", "12"])
15+
@pytest.mark.parametrize(
16+
"bootstrap_version,parameters",
17+
[
18+
("10", {"FileAssetsBucketName": f"cdk-bootstrap-{short_uid()}"}),
19+
("11", {"FileAssetsBucketName": f"cdk-bootstrap-{short_uid()}"}),
20+
("12", {"FileAssetsBucketName": f"cdk-bootstrap-{short_uid()}"}),
21+
(
22+
"28",
23+
{
24+
"CloudFormationExecutionPolicies": "",
25+
"FileAssetsBucketKmsKeyId": "AWS_MANAGED_KEY",
26+
"PublicAccessBlockConfiguration": "true",
27+
"TrustedAccounts": "",
28+
"TrustedAccountsForLookup": "",
29+
},
30+
),
31+
],
32+
ids=["10", "11", "12", "28"],
33+
)
1434
@markers.aws.validated
15-
def test_cdk_bootstrap(self, deploy_cfn_template, bootstrap_version, aws_client):
35+
def test_cdk_bootstrap(self, deploy_cfn_template, aws_client, bootstrap_version, parameters):
1636
deploy_cfn_template(
1737
template_path=os.path.join(
1838
os.path.dirname(__file__),
1939
f"../../../templates/cdk_bootstrap_v{bootstrap_version}.yaml",
2040
),
21-
parameters={"FileAssetsBucketName": f"cdk-bootstrap-{short_uid()}"},
41+
parameters=parameters,
2242
)
2343
init_stack_result = deploy_cfn_template(
2444
template_path=os.path.join(
@@ -32,61 +52,136 @@ def test_cdk_bootstrap(self, deploy_cfn_template, bootstrap_version, aws_client)
3252
assert len(stack_res["StackResources"]) == 1
3353
assert stack_res["StackResources"][0]["LogicalResourceId"] == "CDKMetadata"
3454

35-
@skip_if_v2_provider(reason="CFNV2:Provider")
3655
@markers.aws.validated
37-
def test_cdk_bootstrap_redeploy(self, aws_client, cleanup_stacks, cleanup_changesets, cleanups):
56+
@pytest.mark.parametrize(
57+
"template,parameters_fn",
58+
[
59+
pytest.param(
60+
"cdk_bootstrap.yml",
61+
lambda qualifier: [
62+
{
63+
"ParameterKey": "BootstrapVariant",
64+
"ParameterValue": "AWS CDK: Default Resources",
65+
},
66+
{"ParameterKey": "TrustedAccounts", "ParameterValue": ""},
67+
{"ParameterKey": "TrustedAccountsForLookup", "ParameterValue": ""},
68+
{"ParameterKey": "CloudFormationExecutionPolicies", "ParameterValue": ""},
69+
{
70+
"ParameterKey": "FileAssetsBucketKmsKeyId",
71+
"ParameterValue": "AWS_MANAGED_KEY",
72+
},
73+
{
74+
"ParameterKey": "PublicAccessBlockConfiguration",
75+
"ParameterValue": "true",
76+
},
77+
{"ParameterKey": "Qualifier", "ParameterValue": qualifier},
78+
{
79+
"ParameterKey": "UseExamplePermissionsBoundary",
80+
"ParameterValue": "false",
81+
},
82+
],
83+
id="v20",
84+
),
85+
pytest.param(
86+
"cdk_bootstrap_v28.yaml",
87+
lambda qualifier: [
88+
{"ParameterKey": "CloudFormationExecutionPolicies", "ParameterValue": ""},
89+
{
90+
"ParameterKey": "FileAssetsBucketKmsKeyId",
91+
"ParameterValue": "AWS_MANAGED_KEY",
92+
},
93+
{
94+
"ParameterKey": "PublicAccessBlockConfiguration",
95+
"ParameterValue": "true",
96+
},
97+
{"ParameterKey": "Qualifier", "ParameterValue": qualifier},
98+
{"ParameterKey": "TrustedAccounts", "ParameterValue": ""},
99+
{"ParameterKey": "TrustedAccountsForLookup", "ParameterValue": ""},
100+
],
101+
id="v28",
102+
),
103+
],
104+
)
105+
@markers.snapshot.skip_snapshot_verify(
106+
paths=[
107+
# CFNV2:Provider
108+
"$..Description",
109+
# Wrong format, they are our internal parameter format
110+
"$..Parameters",
111+
# from the list of changes
112+
"$..Changes..Details",
113+
"$..Changes..LogicalResourceId",
114+
"$..Changes..ResourceType",
115+
"$..Changes..Scope",
116+
# provider
117+
"$..IncludeNestedStacks",
118+
"$..NotificationARNs",
119+
# CFNV2:Describe not supported yet
120+
"$..Outputs..ExportName",
121+
# mismatch between amazonaws.com and localhost.localstack.cloud
122+
"$..Outputs..OutputValue",
123+
]
124+
)
125+
@skip_if_v1_provider(reason="Changes array not in parity")
126+
def test_cdk_bootstrap_redeploy(
127+
self,
128+
aws_client,
129+
cleanup_stacks,
130+
cleanup_changesets,
131+
cleanups,
132+
snapshot,
133+
template,
134+
parameters_fn: Callable[[str], list[Parameter]],
135+
):
38136
"""Test that simulates a sequence of commands executed by CDK when running 'cdk bootstrap' twice"""
137+
snapshot.add_transformer(snapshot.transform.cloudformation_api())
138+
snapshot.add_transformer(SortingTransformer("Parameters", lambda p: p["ParameterKey"]))
139+
snapshot.add_transformer(SortingTransformer("Outputs", lambda p: p["OutputKey"]))
39140

40141
stack_name = f"CDKToolkit-{short_uid()}"
41142
change_set_name = f"cdk-deploy-change-set-{short_uid()}"
143+
qualifier = short_uid()
144+
snapshot.add_transformer(snapshot.transform.regex(qualifier, "<qualifier>"))
42145

43146
def clean_resources():
44147
cleanup_stacks([stack_name])
45148
cleanup_changesets([change_set_name])
46149

47150
cleanups.append(clean_resources)
48151

49-
template_body = load_file(
50-
os.path.join(os.path.dirname(__file__), "../../../templates/cdk_bootstrap.yml")
152+
template_path = os.path.realpath(
153+
os.path.join(os.path.dirname(__file__), f"../../../templates/{template}")
51154
)
155+
template_body = load_file(template_path)
156+
if template_body is None:
157+
raise RuntimeError(f"Template {template_path} not loaded")
158+
52159
aws_client.cloudformation.create_change_set(
53160
StackName=stack_name,
54161
ChangeSetName=change_set_name,
55162
TemplateBody=template_body,
56163
ChangeSetType="CREATE",
57164
Capabilities=["CAPABILITY_IAM", "CAPABILITY_NAMED_IAM", "CAPABILITY_AUTO_EXPAND"],
58165
Description="CDK Changeset for execution 731ed7da-8b2d-49c6-bca3-4698b6875954",
59-
Parameters=[
60-
{
61-
"ParameterKey": "BootstrapVariant",
62-
"ParameterValue": "AWS CDK: Default Resources",
63-
},
64-
{"ParameterKey": "TrustedAccounts", "ParameterValue": ""},
65-
{"ParameterKey": "TrustedAccountsForLookup", "ParameterValue": ""},
66-
{"ParameterKey": "CloudFormationExecutionPolicies", "ParameterValue": ""},
67-
{"ParameterKey": "FileAssetsBucketKmsKeyId", "ParameterValue": "AWS_MANAGED_KEY"},
68-
{"ParameterKey": "PublicAccessBlockConfiguration", "ParameterValue": "true"},
69-
{"ParameterKey": "Qualifier", "ParameterValue": "hnb659fds"},
70-
{"ParameterKey": "UseExamplePermissionsBoundary", "ParameterValue": "false"},
71-
],
166+
Parameters=parameters_fn(qualifier),
72167
)
73-
aws_client.cloudformation.describe_change_set(
168+
aws_client.cloudformation.get_waiter("change_set_create_complete").wait(
74169
StackName=stack_name, ChangeSetName=change_set_name
75170
)
76-
77-
aws_client.cloudformation.get_waiter("change_set_create_complete").wait(
171+
describe_change_set = aws_client.cloudformation.describe_change_set(
78172
StackName=stack_name, ChangeSetName=change_set_name
79173
)
174+
snapshot.match("describe-change-set", describe_change_set)
80175

81176
aws_client.cloudformation.execute_change_set(
82177
StackName=stack_name, ChangeSetName=change_set_name
83178
)
84179

85180
aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_name)
86-
aws_client.cloudformation.describe_stacks(StackName=stack_name)
181+
stacks = aws_client.cloudformation.describe_stacks(StackName=stack_name)["Stacks"][0]
182+
snapshot.match("describe-stacks", stacks)
87183

88-
# When CDK toolstrap command is executed again it just confirms that the template is the same
89-
aws_client.sts.get_caller_identity()
184+
# When CDK bootstrap command is executed again it just confirms that the template is the same
90185
aws_client.cloudformation.get_template(StackName=stack_name, TemplateStage="Original")
91186

92187
# TODO: create scenario where the template is different to catch cdk behavior

0 commit comments

Comments
 (0)