Skip to content

Commit

Permalink
Update mappings to support transforms at the root level (#3439)
Browse files Browse the repository at this point in the history
  • Loading branch information
kddejong authored Jun 28, 2024
1 parent 625d38a commit 57a0070
Show file tree
Hide file tree
Showing 9 changed files with 198 additions and 104 deletions.
100 changes: 100 additions & 0 deletions src/cfnlint/context/_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from typing import Any, Iterator

LOGGER = logging.getLogger(__name__)


@dataclass(frozen=True)
class Mappings:
"""
This class holds a mapping
"""

maps: dict[str, Map] = field(init=True, default_factory=dict)
is_transform: bool = field(init=True, default=False)

@classmethod
def create_from_dict(cls, instance: Any) -> Mappings:

if not isinstance(instance, dict):
return cls({})
try:
result = {}
is_transform = False
for k, v in instance.items():
if k == "Fn::Transform":
is_transform = True
else:
result[k] = Map.create_from_dict(v)
return cls(result, is_transform)
except (ValueError, AttributeError) as e:
LOGGER.debug(e, exc_info=True)
return cls({})


@dataclass(frozen=True)
class _MappingSecondaryKey:
"""
This class holds a mapping value
"""

keys: dict[str, list[Any] | str | int | float] = field(
init=True, default_factory=dict
)
is_transform: bool = field(init=True, default=False)

def value(self, secondary_key: str):
if secondary_key not in self.keys:
raise KeyError(secondary_key)
return self.keys[secondary_key]

@classmethod
def create_from_dict(cls, instance: Any) -> _MappingSecondaryKey:
if not isinstance(instance, dict):
return cls({})
is_transform = False
keys = {}
for k, v in instance.items():
if k == "Fn::Transform":
is_transform = True
elif isinstance(v, (str, list, int, float)):
keys[k] = v
else:
continue
return cls(keys, is_transform)


@dataclass(frozen=True)
class Map:
"""
This class holds a mapping
"""

keys: dict[str, _MappingSecondaryKey] = field(init=True, default_factory=dict)
is_transform: bool = field(init=True, default=False)

def find_in_map(self, top_key: str, secondary_key: str) -> Iterator[Any]:
if top_key not in self.keys:
raise KeyError(top_key)
yield self.keys[top_key].value(secondary_key)

@classmethod
def create_from_dict(cls, instance: Any) -> Map:
if not isinstance(instance, dict):
return cls({})
is_transform = False
keys = {}
for k, v in instance.items():
if k == "Fn::Transform":
is_transform = True
else:
keys[k] = _MappingSecondaryKey.create_from_dict(v)
return cls(keys, is_transform)
77 changes: 3 additions & 74 deletions src/cfnlint/context/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from typing import Any, Deque, Iterator, Sequence, Set, Tuple

from cfnlint.context._conditions import Conditions
from cfnlint.context._mappings import Mappings
from cfnlint.helpers import (
BOOLEAN_STRINGS_TRUE,
FUNCTIONS,
Expand Down Expand Up @@ -138,7 +139,7 @@ class Context:
parameters: dict[str, "Parameter"] = field(init=True, default_factory=dict)
resources: dict[str, "Resource"] = field(init=True, default_factory=dict)
conditions: Conditions = field(init=True, default_factory=Conditions)
mappings: dict[str, "Map"] = field(init=True, default_factory=dict)
mappings: Mappings = field(init=True, default_factory=Mappings)

strict_types: bool = field(init=True, default=True)

Expand Down Expand Up @@ -374,61 +375,6 @@ def ref(self, context: Context) -> Iterator[Any]:
yield


@dataclass
class _MappingSecondaryKey:
"""
This class holds a mapping value
"""

keys: dict[str, list[Any] | str | int | float] = field(
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:
raise ValueError("Third keys must not be an object")

def value(self, secondary_key: str):
if secondary_key not in self.keys:
raise KeyError(secondary_key)
return self.keys[secondary_key]


@dataclass
class Map:
"""
This class holds a mapping
"""

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():
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:
raise KeyError(top_key)
yield self.keys[top_key].value(secondary_key)


def _init_parameters(parameters: Any) -> dict[str, Parameter]:
obj = {}
if not isinstance(parameters, dict):
Expand Down Expand Up @@ -460,19 +406,6 @@ def _init_transforms(transforms: Any) -> Transforms:
return Transforms([])


def _init_mappings(mappings: Any) -> dict[str, Map]:
obj = {}
if not isinstance(mappings, dict):
raise ValueError("Mappings must be a object")
for k, v in mappings.items():
try:
obj[k] = Map(v)
except ValueError:
pass

return obj


def create_context_for_template(cfn):
parameters = {}
try:
Expand All @@ -494,11 +427,7 @@ def create_context_for_template(cfn):
except (ValueError, AttributeError):
pass

mappings = {}
try:
mappings = _init_mappings(cfn.template.get("Mappings", {}))
except (ValueError, AttributeError):
pass
mappings = Mappings.create_from_dict(cfn.template.get("Mappings", {}))

return Context(
parameters=parameters,
Expand Down
10 changes: 6 additions & 4 deletions src/cfnlint/jsonschema/_resolvers_cfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
), None
default_value_found = True

if not default_value_found and not validator.context.mappings:
if not default_value_found and not validator.context.mappings.maps:
if validator.context.mappings.is_transform:
return
yield None, validator, ValidationError(
(
f"{instance[0]!r} is not one of "
f"{list(validator.context.mappings.keys())!r}"
f"{list(validator.context.mappings.maps.keys())!r}"
),
path=deque([0]),
)
Expand All @@ -71,13 +73,13 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
)
and validator.is_type(instance[2], "string")
):
map = validator.context.mappings.get(instance[0])
map = validator.context.mappings.maps.get(instance[0])
if map is None:
if not default_value_found:
yield None, validator, ValidationError(
(
f"{instance[0]!r} is not one of "
f"{list(validator.context.mappings.keys())!r}"
f"{list(validator.context.mappings.maps.keys())!r}"
),
path=deque([0]),
)
Expand Down
7 changes: 7 additions & 0 deletions src/cfnlint/rules/mappings/Used.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""

from cfnlint._typing import RuleMatches
from cfnlint.helpers import is_function
from cfnlint.rules import CloudFormationLintRule, RuleMatch
from cfnlint.template import Template

Expand All @@ -22,6 +23,12 @@ def match(self, cfn: Template) -> RuleMatches:
findinmap_mappings = []

mappings = cfn.template.get("Mappings", {})
k, _ = is_function(mappings)
if k == "Fn::Transform":
self.logger.debug(
(f"Mapping Name has a transform. Disabling check {self.id!r}"),
)
return matches

if mappings:
# Get all "FindInMaps" that reference a Mapping
Expand Down
15 changes: 10 additions & 5 deletions test/unit/module/context/test_create_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
},
},
},
_Counts(resources=1, parameters=1, conditions=0, mappings=1),
_Counts(resources=1, parameters=1, conditions=0, mappings=2),
),
(
"Invalid mapping second key",
Expand All @@ -75,7 +75,7 @@
"Map": {"us-east-1": {"foo": "bar"}},
},
},
_Counts(resources=0, parameters=0, conditions=0, mappings=1),
_Counts(resources=0, parameters=0, conditions=0, mappings=2),
),
(
"Invalid mapping third key",
Expand All @@ -89,7 +89,7 @@
"Map": {"us-east-1": {"foo": "bar"}},
},
},
_Counts(resources=0, parameters=0, conditions=0, mappings=1),
_Counts(resources=0, parameters=0, conditions=0, mappings=2),
),
],
)
Expand All @@ -101,10 +101,15 @@ def test_create_context(name, instance, counts):
if i == "conditions":
assert len(context.conditions.conditions) == getattr(counts, i), (
f"Test {name} has {i} {len(getattr(context, i))} "
"and expected {getattr(counts, i)}"
f"and expected {getattr(counts, i)}"
)
elif i == "mappings":
assert len(context.mappings.maps) == getattr(counts, i), (
f"Test {name} has {i} {len(context.mappings.maps)} "
f"and expected {getattr(counts, i)}"
)
else:
assert len(getattr(context, i)) == getattr(counts, i), (
f"Test {name} has {i} {len(getattr(context, i))} "
"and expected {getattr(counts, i)}"
f"and expected {getattr(counts, i)}"
)
62 changes: 58 additions & 4 deletions test/unit/module/context/test_mappings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import pytest

from cfnlint.context.context import Map
from cfnlint.context._mappings import Map, Mappings, _MappingSecondaryKey


@pytest.mark.parametrize(
Expand All @@ -17,7 +17,7 @@
],
)
def test_mapping_value(name, get_key_1, get_key_2, expected):
mapping = Map({"A": {"B": "C"}})
mapping = Map.create_from_dict({"A": {"B": "C"}})

if isinstance(expected, Exception):
with pytest.raises(type(expected)):
Expand All @@ -27,10 +27,64 @@ def test_mapping_value(name, get_key_1, get_key_2, expected):


def test_transforms():
mapping = Map({"A": {"Fn::Transform": "C"}})
mapping = Map.create_from_dict({"A": {"Fn::Transform": "C"}})

assert mapping.keys.get("A").is_transform is True

mapping = Map({"Fn::Transform": {"B": "C"}})
mapping = Map.create_from_dict({"Fn::Transform": {"B": "C"}})

assert mapping.is_transform is True

mapping = Mappings.create_from_dict({"Fn::Transform": {"B": {"C": "D"}}})

assert mapping.is_transform is True


@pytest.mark.parametrize(
"name,mappings,expected",
[
(
"Valid mappings",
{
"A": {"B": {"C": "D"}},
"1": {"2": {"3": "4"}},
"Z": [],
"9": {"8": []},
"M": {"N": {"O": {"P": "Q"}}},
},
Mappings(
{
"A": Map({"B": _MappingSecondaryKey({"C": "D"})}),
"1": Map({"2": _MappingSecondaryKey({"3": "4"})}),
"Z": Map({}),
"9": Map({"8": _MappingSecondaryKey({})}),
"M": Map({"N": _MappingSecondaryKey({})}),
}
),
),
(
"Valid mappings with transforms",
{
"A": {"Fn::Transform": "MyTransform"},
"1": {"2": {"Fn::Transform": "MyTransform"}},
},
Mappings(
{
"A": Map({}, True),
"1": Map({"2": _MappingSecondaryKey({}, True)}),
}
),
),
(
"Valid mappings with transforms for mappings",
{
"Fn::Transform": "MyTransform",
},
Mappings({}, True),
),
],
)
def test_mapping_creation(name, mappings, expected):
results = Mappings.create_from_dict(mappings)

assert results == expected, f"{name!r} failed got {results!r}"
Loading

0 comments on commit 57a0070

Please sign in to comment.