diff --git a/src/cfnlint/context/context.py b/src/cfnlint/context/context.py index 7a095d8eea..99f818c435 100644 --- a/src/cfnlint/context/context.py +++ b/src/cfnlint/context/context.py @@ -384,11 +384,15 @@ class _MappingSecondaryKey: init=False, default_factory=dict ) instance: InitVar[Any] + is_transform: bool = field(init=False, default=False) def __post_init__(self, instance) -> None: if not isinstance(instance, dict): raise ValueError("Secondary keys must be a object") for k, v in instance.items(): + if k == "Fn::Transform": + self.is_transform = True + continue if isinstance(v, (str, list, int, float)): self.keys[k] = v else: @@ -408,12 +412,16 @@ class Map: keys: dict[str, _MappingSecondaryKey] = field(init=False, default_factory=dict) resource: InitVar[Any] + is_transform: bool = field(init=False, default=False) def __post_init__(self, mapping) -> None: if not isinstance(mapping, dict): raise ValueError("Mapping must be a object") for k, v in mapping.items(): - self.keys[k] = _MappingSecondaryKey(v) + if k == "Fn::Transform": + self.is_transform = True + else: + self.keys[k] = _MappingSecondaryKey(v) def find_in_map(self, top_key: str, secondary_key: str) -> Iterator[Any]: if top_key not in self.keys: diff --git a/src/cfnlint/jsonschema/_resolvers_cfn.py b/src/cfnlint/jsonschema/_resolvers_cfn.py index 623cddca05..77edff1b6b 100644 --- a/src/cfnlint/jsonschema/_resolvers_cfn.py +++ b/src/cfnlint/jsonschema/_resolvers_cfn.py @@ -85,6 +85,8 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult: top_key = map.keys.get(instance[1]) if top_key is None: + if map.is_transform: + return if not default_value_found: yield None, validator, ValidationError( ( @@ -98,6 +100,8 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult: value = top_key.keys.get(instance[2]) if value is None: + if top_key.is_transform: + return if not default_value_found: yield value, validator, ValidationError( ( diff --git a/test/unit/module/context/test_mappings.py b/test/unit/module/context/test_mappings.py index 14b36cff6c..edf67b30f3 100644 --- a/test/unit/module/context/test_mappings.py +++ b/test/unit/module/context/test_mappings.py @@ -24,3 +24,13 @@ def test_mapping_value(name, get_key_1, get_key_2, expected): list(mapping.find_in_map(get_key_1, get_key_2)) else: assert list(mapping.find_in_map(get_key_1, get_key_2)) == expected + + +def test_transforms(): + mapping = Map({"A": {"Fn::Transform": "C"}}) + + assert mapping.keys.get("A").is_transform is True + + mapping = Map({"Fn::Transform": {"B": "C"}}) + + assert mapping.is_transform is True diff --git a/test/unit/module/jsonschema/test_resolvers_cfn.py b/test/unit/module/jsonschema/test_resolvers_cfn.py index 93fa0409fb..4d08460123 100644 --- a/test/unit/module/jsonschema/test_resolvers_cfn.py +++ b/test/unit/module/jsonschema/test_resolvers_cfn.py @@ -240,7 +240,10 @@ def test_invalid_functions(name, instance, response): None, deque([]), ValidationError( - ("'bar' is not one of ['foo']"), + ( + "'bar' is not one of ['foo', " + "'transformFirstKey', 'transformSecondKey']" + ), path=deque(["Fn::FindInMap", 0]), ), ) @@ -304,6 +307,16 @@ def test_invalid_functions(name, instance, response): {"Fn::FindInMap": ["foo", "first", "third", {"DefaultValue": "default"}]}, [("default", deque([4, "DefaultValue"]), None)], ), + ( + "Valid FindInMap with a transform on first key", + {"Fn::FindInMap": ["transformFirstKey", "first", "third"]}, + [], + ), + ( + "Valid FindInMap with a transform on second key", + {"Fn::FindInMap": ["transformSecondKey", "first", "third"]}, + [], + ), ( "Valid Sub with a resolvable values", {"Fn::Sub": ["${a}-${b}", {"a": "foo", "b": "bar"}]}, @@ -319,6 +332,8 @@ def test_invalid_functions(name, instance, response): def test_valid_functions(name, instance, response): context = Context() context.mappings["foo"] = Map({"first": {"second": "bar"}}) + context.mappings["transformFirstKey"] = Map({"Fn::Transform": {"second": "bar"}}) + context.mappings["transformSecondKey"] = Map({"first": {"Fn::Transform": "bar"}}) _resolve(name, instance, response, context=context)