Skip to content

Commit 8096cdf

Browse files
authored
CFn: correctly skip conditionally disabled resources (#13238)
1 parent ccbd18a commit 8096cdf

File tree

5 files changed

+274
-1
lines changed

5 files changed

+274
-1
lines changed

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
NodeProperties,
3131
NodeProperty,
3232
NodeResource,
33+
NodeResources,
3334
NodeTemplate,
3435
Nothing,
3536
NothingType,
@@ -1174,6 +1175,20 @@ def _resolve_resource_condition_reference(self, reference: TerminalValue) -> Pre
11741175
after = after_delta.after
11751176
return PreprocEntityDelta(before=before, after=after)
11761177

1178+
def visit_node_resources(self, node_resources: NodeResources):
1179+
"""
1180+
Skip resources where they conditionally evaluate to False
1181+
"""
1182+
for node_resource in node_resources.resources:
1183+
if not is_nothing(node_resource.condition_reference):
1184+
condition_delta = self._resolve_resource_condition_reference(
1185+
node_resource.condition_reference
1186+
)
1187+
condition_after = condition_delta.after
1188+
if condition_after is False:
1189+
continue
1190+
self.visit(node_resource)
1191+
11771192
def visit_node_resource(
11781193
self, node_resource: NodeResource
11791194
) -> PreprocEntityDelta[PreprocResource, PreprocResource]:
@@ -1284,6 +1299,14 @@ def visit_node_outputs(
12841299
before: list[PreprocOutput] = []
12851300
after: list[PreprocOutput] = []
12861301
for node_output in node_outputs.outputs:
1302+
if not is_nothing(node_output.condition_reference):
1303+
condition_delta = self._resolve_resource_condition_reference(
1304+
node_output.condition_reference
1305+
)
1306+
condition_after = condition_delta.after
1307+
if condition_after is False:
1308+
continue
1309+
12871310
output_delta: PreprocEntityDelta[PreprocOutput, PreprocOutput] = self.visit(node_output)
12881311
output_before = output_delta.before
12891312
output_after = output_delta.after

tests/aws/services/cloudformation/engine/test_conditions.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os.path
22

33
import pytest
4+
from localstack_snapshot.snapshots.transformer import SortingTransformer
45
from tests.aws.services.cloudformation.conftest import skip_if_legacy_engine
56

67
from localstack.services.cloudformation.v2.utils import is_v2_engine
@@ -153,7 +154,7 @@ def test_dependent_get_att(self, aws_client, snapshot):
153154
snapshot.match("dependent_ref_exc", e.value.response)
154155

155156
@markers.aws.validated
156-
@pytest.mark.skipif(condition=not is_aws_cloud(), reason="not supported yet")
157+
@skip_if_legacy_engine()
157158
def test_dependent_ref_intrinsic_fn_condition(self, aws_client, deploy_cfn_template):
158159
"""
159160
Checks behavior of un-refable resources
@@ -515,3 +516,37 @@ def test_update_conditions(self, deploy_cfn_template, aws_client):
515516
assert aws_client.s3.head_bucket(Bucket=bucket_1)
516517
with pytest.raises(aws_client.s3.exceptions.ClientError):
517518
aws_client.s3.head_bucket(Bucket=bucket_2)
519+
520+
@markers.aws.validated
521+
@pytest.mark.parametrize("should_deploy", ["yes", "no"])
522+
@skip_if_legacy_engine()
523+
def test_references_to_disabled_resources(
524+
self, deploy_cfn_template, aws_client, should_deploy, snapshot
525+
):
526+
snapshot.add_transformer(
527+
SortingTransformer("StackResources", lambda e: e["LogicalResourceId"])
528+
)
529+
snapshot.add_transformer(SortingTransformer("Parameters", lambda e: e["ParameterKey"]))
530+
snapshot.add_transformer(snapshot.transform.key_value("PhysicalResourceId"))
531+
snapshot.add_transformer(snapshot.transform.cloudformation_api())
532+
533+
parameter_value = short_uid()
534+
snapshot.add_transformer(snapshot.transform.regex(parameter_value, "<parameter-value>"))
535+
536+
stack = deploy_cfn_template(
537+
template_path=os.path.join(
538+
os.path.dirname(__file__), "../../../templates/references_to_conditions.yml"
539+
),
540+
parameters={
541+
"Toggle": should_deploy,
542+
"ParameterValue": parameter_value,
543+
},
544+
)
545+
546+
describe_stack_res = aws_client.cloudformation.describe_stacks(StackName=stack.stack_id)
547+
snapshot.match("stack-description", describe_stack_res)
548+
549+
describe_resources = aws_client.cloudformation.describe_stack_resources(
550+
StackName=stack.stack_id
551+
)
552+
snapshot.match("resources-description", describe_resources)

tests/aws/services/cloudformation/engine/test_conditions.snapshot.json

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,5 +775,163 @@
775775
}
776776
}
777777
}
778+
},
779+
"tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_references_to_disabled_resources[yes]": {
780+
"recorded-date": "07-10-2025, 21:21:23",
781+
"recorded-content": {
782+
"stack-description": {
783+
"Stacks": [
784+
{
785+
"Capabilities": [
786+
"CAPABILITY_AUTO_EXPAND",
787+
"CAPABILITY_IAM",
788+
"CAPABILITY_NAMED_IAM"
789+
],
790+
"ChangeSetId": "arn:<partition>:cloudformation:<region>:111111111111:changeSet/<resource:1>",
791+
"CreationTime": "datetime",
792+
"DisableRollback": false,
793+
"DriftInformation": {
794+
"StackDriftStatus": "NOT_CHECKED"
795+
},
796+
"EnableTerminationProtection": false,
797+
"LastUpdatedTime": "datetime",
798+
"NotificationARNs": [],
799+
"Outputs": [
800+
{
801+
"OutputKey": "ConditionalParameterValue",
802+
"OutputValue": "<parameter-value>"
803+
}
804+
],
805+
"Parameters": [
806+
{
807+
"ParameterKey": "ParameterValue",
808+
"ParameterValue": "<parameter-value>"
809+
},
810+
{
811+
"ParameterKey": "Toggle",
812+
"ParameterValue": "yes"
813+
}
814+
],
815+
"RollbackConfiguration": {},
816+
"StackId": "arn:<partition>:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:2>",
817+
"StackName": "<stack-name:1>",
818+
"StackStatus": "CREATE_COMPLETE",
819+
"Tags": []
820+
}
821+
],
822+
"ResponseMetadata": {
823+
"HTTPHeaders": {},
824+
"HTTPStatusCode": 200
825+
}
826+
},
827+
"resources-description": {
828+
"StackResources": [
829+
{
830+
"DriftInformation": {
831+
"StackResourceDriftStatus": "NOT_CHECKED"
832+
},
833+
"LogicalResourceId": "AnotherResource",
834+
"PhysicalResourceId": "<physical-resource-id:1>",
835+
"ResourceStatus": "CREATE_COMPLETE",
836+
"ResourceType": "AWS::SSM::Parameter",
837+
"StackId": "arn:<partition>:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:2>",
838+
"StackName": "<stack-name:1>",
839+
"Timestamp": "timestamp"
840+
},
841+
{
842+
"DriftInformation": {
843+
"StackResourceDriftStatus": "NOT_CHECKED"
844+
},
845+
"LogicalResourceId": "BaseParameter",
846+
"PhysicalResourceId": "<physical-resource-id:2>",
847+
"ResourceStatus": "CREATE_COMPLETE",
848+
"ResourceType": "AWS::SSM::Parameter",
849+
"StackId": "arn:<partition>:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:2>",
850+
"StackName": "<stack-name:1>",
851+
"Timestamp": "timestamp"
852+
},
853+
{
854+
"DriftInformation": {
855+
"StackResourceDriftStatus": "NOT_CHECKED"
856+
},
857+
"LogicalResourceId": "ConditionalParameter",
858+
"PhysicalResourceId": "<physical-resource-id:3>",
859+
"ResourceStatus": "CREATE_COMPLETE",
860+
"ResourceType": "AWS::SSM::Parameter",
861+
"StackId": "arn:<partition>:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:2>",
862+
"StackName": "<stack-name:1>",
863+
"Timestamp": "timestamp"
864+
}
865+
],
866+
"ResponseMetadata": {
867+
"HTTPHeaders": {},
868+
"HTTPStatusCode": 200
869+
}
870+
}
871+
}
872+
},
873+
"tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_references_to_disabled_resources[no]": {
874+
"recorded-date": "07-10-2025, 21:21:37",
875+
"recorded-content": {
876+
"stack-description": {
877+
"Stacks": [
878+
{
879+
"Capabilities": [
880+
"CAPABILITY_AUTO_EXPAND",
881+
"CAPABILITY_IAM",
882+
"CAPABILITY_NAMED_IAM"
883+
],
884+
"ChangeSetId": "arn:<partition>:cloudformation:<region>:111111111111:changeSet/<resource:1>",
885+
"CreationTime": "datetime",
886+
"DisableRollback": false,
887+
"DriftInformation": {
888+
"StackDriftStatus": "NOT_CHECKED"
889+
},
890+
"EnableTerminationProtection": false,
891+
"LastUpdatedTime": "datetime",
892+
"NotificationARNs": [],
893+
"Parameters": [
894+
{
895+
"ParameterKey": "ParameterValue",
896+
"ParameterValue": "<parameter-value>"
897+
},
898+
{
899+
"ParameterKey": "Toggle",
900+
"ParameterValue": "no"
901+
}
902+
],
903+
"RollbackConfiguration": {},
904+
"StackId": "arn:<partition>:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:2>",
905+
"StackName": "<stack-name:1>",
906+
"StackStatus": "CREATE_COMPLETE",
907+
"Tags": []
908+
}
909+
],
910+
"ResponseMetadata": {
911+
"HTTPHeaders": {},
912+
"HTTPStatusCode": 200
913+
}
914+
},
915+
"resources-description": {
916+
"StackResources": [
917+
{
918+
"DriftInformation": {
919+
"StackResourceDriftStatus": "NOT_CHECKED"
920+
},
921+
"LogicalResourceId": "BaseParameter",
922+
"PhysicalResourceId": "<physical-resource-id:1>",
923+
"ResourceStatus": "CREATE_COMPLETE",
924+
"ResourceType": "AWS::SSM::Parameter",
925+
"StackId": "arn:<partition>:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:2>",
926+
"StackName": "<stack-name:1>",
927+
"Timestamp": "timestamp"
928+
}
929+
],
930+
"ResponseMetadata": {
931+
"HTTPHeaders": {},
932+
"HTTPStatusCode": 200
933+
}
934+
}
935+
}
778936
}
779937
}

tests/aws/services/cloudformation/engine/test_conditions.validation.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@
3232
"tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_output_reference_to_skipped_resource": {
3333
"last_validated_date": "2023-06-26T22:43:18+00:00"
3434
},
35+
"tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_references_to_disabled_resources[no]": {
36+
"last_validated_date": "2025-10-07T21:21:41+00:00",
37+
"durations_in_seconds": {
38+
"setup": 0.0,
39+
"call": 7.01,
40+
"teardown": 4.39,
41+
"total": 11.4
42+
}
43+
},
44+
"tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_references_to_disabled_resources[yes]": {
45+
"last_validated_date": "2025-10-07T21:21:30+00:00",
46+
"durations_in_seconds": {
47+
"setup": 0.89,
48+
"call": 10.41,
49+
"teardown": 6.43,
50+
"total": 17.73
51+
}
52+
},
3553
"tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_update_conditions": {
3654
"last_validated_date": "2024-06-18T19:43:43+00:00"
3755
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
Parameters:
2+
Toggle:
3+
Type: String
4+
5+
ParameterValue:
6+
Type: String
7+
8+
Conditions:
9+
ShouldDeploy:
10+
Fn::Equals:
11+
- !Ref Toggle
12+
- "yes"
13+
14+
Resources:
15+
BaseParameter:
16+
# We need a single resource to deploy
17+
Type: AWS::SSM::Parameter
18+
Properties:
19+
Type: String
20+
Value: !Ref ParameterValue
21+
22+
ConditionalParameter:
23+
Type: AWS::SSM::Parameter
24+
Condition: ShouldDeploy
25+
Properties:
26+
Type: String
27+
Value: !Ref ParameterValue
28+
29+
AnotherResource:
30+
Type: AWS::SSM::Parameter
31+
Condition: ShouldDeploy
32+
Properties:
33+
Type: String
34+
Value: !GetAtt ConditionalParameter.Value
35+
36+
Outputs:
37+
ConditionalParameterValue:
38+
Condition: ShouldDeploy
39+
Value: !GetAtt ConditionalParameter.Value

0 commit comments

Comments
 (0)