Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -681,9 +681,6 @@ def _visit_intrinsic_function(
scope=arguments_scope, before_value=before_arguments, after_value=after_arguments
)

if intrinsic_function == "Ref" and arguments.value == "AWS::NoValue":
arguments.value = Nothing

if is_created(before=before_arguments, after=after_arguments):
change_type = ChangeType.CREATED
elif is_removed(before=before_arguments, after=after_arguments):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,10 @@ def visit_node_parameters(
return PreprocEntityDelta(before=before_parameters, after=after_parameters)

def visit_node_parameter(self, node_parameter: NodeParameter) -> PreprocEntityDelta:
if not VALID_LOGICAL_RESOURCE_ID_RE.match(node_parameter.name):
raise ValidationError(
f"Template format error: Parameter name {node_parameter.name} is non alphanumeric."
)
dynamic_value = node_parameter.dynamic_value
dynamic_delta = self.visit(dynamic_value)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def validate(self):
def visit_node_template(self, node_template: NodeTemplate):
self.visit(node_template.mappings)
self.visit(node_template.resources)
self.visit(node_template.parameters)

def visit_node_intrinsic_function_fn_get_att(
self, node_intrinsic_function: NodeIntrinsicFunction
Expand Down
168 changes: 168 additions & 0 deletions tests/aws/services/cloudformation/api/test_changesets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import json
import os.path
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

import pytest
from botocore.config import Config
Expand Down Expand Up @@ -1246,6 +1248,126 @@ def test_describe_change_set_with_similarly_named_stacks(deploy_cfn_template, aw
)


@dataclass
class NoValueScenario:
name: str
template: dict[str, Any]
should_fail: bool


cases = [
NoValueScenario(
name="parameter",
template={
"Parameters": {
"AWS::NoValue": {
"Type": "String",
"Default": "foo",
},
},
"Resources": {
"MyParameter": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Type": "String",
"Value": {
"Ref": "AWS::NoValue",
},
},
},
},
},
should_fail=True,
),
NoValueScenario(
name="resource",
template={
"Resources": {
"AWS::NoValue": {
"Type": "AWS::SSM::Parameter",
"Properties": {
"Type": "String",
"Value": short_uid(),
},
},
},
},
should_fail=True,
),
NoValueScenario(
name="condition",
template={
"Conditions": {
"AWS::NoValue": {
"Fn::Equals": [
"a",
"a",
],
},
},
"Resources": {
"MyParameter": {
"Type": "AWS::SSM::Parameter",
"Condition": "AWS::NoValue",
"Properties": {
"Type": "String",
"Value": short_uid(),
},
},
},
},
should_fail=False,
),
]


@skip_if_v1_provider("Unsupported in V1 engine")
@markers.aws.validated
@pytest.mark.parametrize(
"case",
cases,
ids=[case.name for case in cases],
)
def test_using_pseudoparameters_in_places(aws_client, snapshot, cleanups, case):
"""
Test that AWS pseudoparameters (particularly AWS::NoValue) can
only be used in value positions of the template
"""

stack_name = f"stack-{short_uid()}"
change_set_name = f"change-set-{short_uid()}"

cleanups.append(
lambda: aws_client.cloudformation.delete_change_set(
ChangeSetName=change_set_name,
StackName=stack_name,
)
)

if case.should_fail:
with pytest.raises(ClientError) as exc_info:
aws_client.cloudformation.create_change_set(
ChangeSetName=change_set_name,
StackName=stack_name,
TemplateBody=json.dumps(case.template),
ChangeSetType="CREATE",
)

snapshot.match("error", exc_info.value.response)

else:
aws_client.cloudformation.create_change_set(
ChangeSetName=change_set_name,
StackName=stack_name,
TemplateBody=json.dumps(case.template),
ChangeSetType="CREATE",
)
aws_client.cloudformation.get_waiter("change_set_create_complete").wait(
ChangeSetName=change_set_name,
StackName=stack_name,
)


@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(
paths=[
Expand Down Expand Up @@ -1319,3 +1441,49 @@ def test_describe_changeset_after_delete(aws_client, cleanups, snapshot):
)

snapshot.match("describe-2", describe_res)


@markers.aws.validated
@skip_if_v1_provider("Unsupported in V1 engine")
def test_update_change_set_with_aws_novalue_repro(aws_client, cleanups):
"""
Fix a bug with trying to access falsy conditions when updating
"""
stack_name = f"stack-{short_uid()}"
create_cs_name = f"cs-create-{short_uid()}"
update_cs_name = f"cs-update-{short_uid()}"
template_path = os.path.join(os.path.dirname(__file__), "../../../templates/aws_novalue.yml")
template_body = load_template_raw(template_path)
fallback_bucket = f"my-bucket-{short_uid()}"

create_resp = aws_client.cloudformation.create_change_set(
StackName=stack_name,
ChangeSetName=create_cs_name,
TemplateBody=template_body,
ChangeSetType="CREATE",
Parameters=[
{"ParameterKey": "SetBucketName", "ParameterValue": "no"},
{"ParameterKey": "FallbackBucketName", "ParameterValue": fallback_bucket},
],
)
create_cs_id = create_resp["Id"]
stack_id = create_resp["StackId"]

cleanups.append(lambda: aws_client.cloudformation.delete_stack(StackName=stack_id))

aws_client.cloudformation.get_waiter("change_set_create_complete").wait(
ChangeSetName=create_cs_id
)
aws_client.cloudformation.execute_change_set(ChangeSetName=create_cs_id)
aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack_id)

aws_client.cloudformation.create_change_set(
StackName=stack_name,
ChangeSetName=update_cs_name,
TemplateBody=template_body,
ChangeSetType="UPDATE",
Parameters=[
{"ParameterKey": "SetBucketName", "ParameterValue": "no"},
{"ParameterKey": "FallbackBucketName", "ParameterValue": fallback_bucket},
],
)
Original file line number Diff line number Diff line change
Expand Up @@ -619,5 +619,41 @@
}
}
}
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_using_pseudoparameters_in_places[resource]": {
"recorded-date": "11-09-2025, 22:15:52",
"recorded-content": {
"error": {
"Error": {
"Code": "ValidationError",
"Message": "Template format error: Resource name AWS::NoValue is non alphanumeric.",
"Type": "Sender"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
}
}
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_using_pseudoparameters_in_places[condition]": {
"recorded-date": "11-09-2025, 22:15:55",
"recorded-content": {}
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_using_pseudoparameters_in_places[parameter]": {
"recorded-date": "11-09-2025, 22:15:51",
"recorded-content": {
"error": {
"Error": {
"Code": "ValidationError",
"Message": "Template format error: Parameter name AWS::NoValue is non alphanumeric.",
"Type": "Sender"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,41 @@
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_name_conflicts": {
"last_validated_date": "2023-11-22T09:58:04+00:00"
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_update_change_set_with_aws_novalue_repro": {
"last_validated_date": "2025-09-11T15:25:28+00:00",
"durations_in_seconds": {
"setup": 0.98,
"call": 21.48,
"teardown": 0.19,
"total": 22.65
}
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_using_pseudoparameters_in_places[condition]": {
"last_validated_date": "2025-09-11T22:15:55+00:00",
"durations_in_seconds": {
"setup": 0.0,
"call": 3.64,
"teardown": 0.09,
"total": 3.73
}
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_using_pseudoparameters_in_places[parameter]": {
"last_validated_date": "2025-09-11T22:15:51+00:00",
"durations_in_seconds": {
"setup": 1.03,
"call": 0.32,
"teardown": 0.13,
"total": 1.48
}
},
"tests/aws/services/cloudformation/api/test_changesets.py::test_using_pseudoparameters_in_places[resource]": {
"last_validated_date": "2025-09-11T22:15:52+00:00",
"durations_in_seconds": {
"setup": 0.0,
"call": 0.16,
"teardown": 0.07,
"total": 0.23
}
}
}
Loading