Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update findinmap resolv to handle Ref psedueparams #3785

Merged
merged 3 commits into from
Oct 23, 2024
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
70 changes: 67 additions & 3 deletions src/cfnlint/jsonschema/_resolvers_cfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
AVAILABILITY_ZONES,
PSEUDOPARAMS,
REGEX_SUB_PARAMETERS,
REGIONS,
is_function,
)
from cfnlint.jsonschema import ValidationError, Validator
Expand Down Expand Up @@ -98,9 +99,75 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
if validator.context.mappings.maps[map_name].is_transform:
continue

k, v = is_function(instance[2])
if k == "Ref" and v in PSEUDOPARAMS:
continue

k, v = is_function(instance[1])
if k == "Ref" and v in PSEUDOPARAMS:
if isinstance(instance[2], str):
found_top_level_key = False
found_second_key = False
for top_level_key, top_values in validator.context.mappings.maps[
map_name
].keys.items():
if v == "AWS::AccountId":
if not re.match("^[0-9]{12}$", top_level_key):
continue
elif v == "AWS::Region":
if top_level_key not in REGIONS:
continue
found_top_level_key = True
for second_level_key, second_v, _ in validator.resolve_value(
instance[2]
):
if second_level_key in top_values.keys:
for value in validator.context.mappings.maps[
map_name
].find_in_map(
top_level_key,
second_level_key,
):
found_second_key = True
yield (
value,
validator.evolve(
context=validator.context.evolve(
path=validator.context.path.evolve(
value_path=deque(
[
"Mappings",
map_name,
top_level_key,
second_level_key,
]
)
)
)
),
None,
)

if not found_top_level_key:
yield None, validator, ValidationError(
(
f"{instance[1]!r} is not a "
f"first level key for mapping {map_name!r}"
),
path=deque([1]),
)
elif not found_second_key:
yield None, validator, ValidationError(
(
f"{instance[2]!r} is not a "
"second level key when "
f"{instance[1]!r} is resolved "
f"for mapping {map_name!r}"
),
path=deque([2]),
)
continue

for top_level_key, top_v, _ in validator.resolve_value(instance[1]):
if validator.is_type(top_level_key, "integer"):
top_level_key = str(top_level_key)
Expand Down Expand Up @@ -134,9 +201,6 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
):
continue

k, v = is_function(instance[2])
if k == "Ref" and v in PSEUDOPARAMS:
continue
for second_level_key, second_v, err in validator.resolve_value(instance[2]):
if validator.is_type(second_level_key, "integer"):
second_level_key = str(second_level_key)
Expand Down
175 changes: 170 additions & 5 deletions test/unit/module/jsonschema/test_resolvers_cfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest

from cfnlint.context._mappings import Mappings
from cfnlint.context.context import Context
from cfnlint.context.context import Context, Parameter
from cfnlint.jsonschema import ValidationError
from cfnlint.jsonschema.validators import CfnTemplateValidator

Expand Down Expand Up @@ -247,7 +247,7 @@ def test_invalid_functions(name, instance, response):
(
"'bar' is not one of ['foo', "
"'transformFirstKey', 'transformSecondKey', "
"'integers']"
"'integers', 'accounts', 'environments']"
),
path=deque(["Fn::FindInMap", 0]),
),
Expand Down Expand Up @@ -296,9 +296,75 @@ def test_invalid_functions(name, instance, response):
[],
),
(
"Valid FindInMap with an top level key that is a Ref to pseudo param",
"Valid FindInMap with an top level key that is a Ref to an account",
{"Fn::FindInMap": ["accounts", {"Ref": "AWS::AccountId"}, "dev"]},
[
(
"bar",
deque(["Mappings", "accounts", "123456789012", "dev"]),
None,
)
],
),
(
(
"Valid FindInMap with an top level key "
"that is a Ref to non account non region"
),
{"Fn::FindInMap": ["accounts", {"Ref": "AWS::StackName"}, "dev"]},
[
(
"bar",
deque(["Mappings", "accounts", "123456789012", "dev"]),
None,
)
],
),
(
"Valid FindInMap with an top level key that is a Ref to a region",
{"Fn::FindInMap": ["accounts", {"Ref": "AWS::Region"}, "bar"]},
[
(
"foo",
deque(["Mappings", "accounts", "us-east-1", "bar"]),
None,
)
],
),
(
"Invalid FindInMap with an top level key that is a Ref to an account",
{"Fn::FindInMap": ["accounts", {"Ref": "AWS::AccountId"}, "bar"]},
[
(
None,
deque([]),
ValidationError(
(
"'bar' is not a second level key "
"when {'Ref': 'AWS::AccountId'} is "
"resolved for mapping 'accounts'"
),
path=deque(["Fn::FindInMap", 2]),
),
)
],
),
(
"Invalid FindInMap with an top level key that is a Ref to pseudo param",
{"Fn::FindInMap": ["foo", {"Ref": "AWS::AccountId"}, "second"]},
[],
[
(
None,
deque([]),
ValidationError(
(
"{'Ref': 'AWS::AccountId'} is not a "
"first level key for mapping 'foo'"
),
path=deque(["Fn::FindInMap", 1]),
),
)
],
),
(
"Valid FindInMap with a second level key that is a Ref to pseudo param",
Expand Down Expand Up @@ -327,6 +393,78 @@ def test_invalid_functions(name, instance, response):
{"Fn::FindInMap": ["integers", 1, 2]},
[("Value", deque(["Mappings", "integers", "1", "2"]), None)],
),
(
(
"Valid FindInMap with a Ref to a parameter "
"with allowed values for top level key"
),
{"Fn::FindInMap": ["environments", {"Ref": "Environment"}, "foo"]},
[
("one", deque(["Mappings", "environments", "dev", "foo"]), None),
("two", deque(["Mappings", "environments", "test", "foo"]), None),
],
),
(
"Valid FindInMap with a Ref to a parameter for top level key",
{"Fn::FindInMap": ["environments", {"Ref": "RandomString"}, "foo"]},
[],
),
(
(
"Valid FindInMap with a Ref to accounts for top level "
"key Ref to a ramom string for second level key"
),
{
"Fn::FindInMap": [
"accounts",
{"Ref": "AWS::AccountId"},
{"Ref": "RandomString"},
]
},
[],
),
(
(
"Valid FindInMap with a Ref to accounts for top level "
"key Ref to an allowed value parameter for second level key"
),
{
"Fn::FindInMap": [
"accounts",
{"Ref": "AWS::AccountId"},
{"Ref": "Environment"},
]
},
[],
),
(
(
"Valid FindInMap with a Ref to a parameter "
"with allowed values for second level key"
),
{"Fn::FindInMap": ["environments", "lion", {"Ref": "Environment"}]},
[
("one", deque(["Mappings", "environments", "lion", "dev"]), None),
("two", deque(["Mappings", "environments", "lion", "test"]), None),
("three", deque(["Mappings", "environments", "lion", "prod"]), None),
],
),
(
"Valid FindInMap with a Ref to a parameter for top level key",
{"Fn::FindInMap": ["environments", {"Ref": "RandomString"}, "foo"]},
[],
),
(
"Valid FindInMap with a Ref to a parameter and a Ref to pseudo parameter",
{
"Fn::FindInMap": [
"accounts",
{"Ref": "AWS::AccountId"},
{"Ref": "RandomString"},
]
},
[],
),
(
"Valid FindInMap with a bad second key and default",
{"Fn::FindInMap": ["foo", "first", "third", {"DefaultValue": "default"}]},
Expand Down Expand Up @@ -356,14 +494,41 @@ def test_invalid_functions(name, instance, response):
)
def test_valid_functions(name, instance, response):
context = Context(
parameters={
"RandomString": Parameter(
{
"Type": "String",
}
),
"Environment": Parameter(
{
"Type": "String",
"AllowedValues": ["dev", "test", "prod"],
}
),
},
mappings=Mappings.create_from_dict(
{
"foo": {"first": {"second": "bar"}},
"transformFirstKey": {"Fn::Transform": {"second": "bar"}},
"transformSecondKey": {"first": {"Fn::Transform": "bar"}},
"integers": {"1": {"2": "Value"}},
"accounts": {
"123456789012": {"dev": "bar"},
"us-east-1": {"bar": "foo"},
},
"environments": {
"dev": {"foo": "one"},
"test": {"foo": "two"},
"prod": {"bar": "three"},
"lion": {
"dev": "one",
"test": "two",
"prod": "three",
},
},
}
)
),
)
_resolve(name, instance, response, context=context)

Expand Down
Loading