From d3f349c9e0e163d0994a5a1cd81eb7e8ef50ea90 Mon Sep 17 00:00:00 2001 From: Kevin DeJong Date: Mon, 23 Sep 2024 20:05:25 -0700 Subject: [PATCH] Switch FN support inside Rules --- src/cfnlint/rules/conditions/And.py | 20 +++-- src/cfnlint/rules/conditions/Not.py | 21 +++-- src/cfnlint/rules/conditions/Or.py | 20 +++-- test/unit/rules/conditions/test_and.py | 101 +++++++++++++++++++++++++ test/unit/rules/conditions/test_not.py | 101 +++++++++++++++++++++++++ test/unit/rules/conditions/test_or.py | 101 +++++++++++++++++++++++++ 6 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 test/unit/rules/conditions/test_and.py create mode 100644 test/unit/rules/conditions/test_not.py create mode 100644 test/unit/rules/conditions/test_or.py diff --git a/src/cfnlint/rules/conditions/And.py b/src/cfnlint/rules/conditions/And.py index aec7bb76c8..08e5829e2b 100644 --- a/src/cfnlint/rules/conditions/And.py +++ b/src/cfnlint/rules/conditions/And.py @@ -7,6 +7,7 @@ from typing import Any +from cfnlint.helpers import FUNCTION_RULES from cfnlint.jsonschema import Validator from cfnlint.rules.functions._BaseFn import BaseFn @@ -25,18 +26,23 @@ def __init__(self) -> None: self.fn_and = self.validate def schema(self, validator: Validator, instance: Any) -> dict[str, Any]: + if validator.context.path.path and validator.context.path.path[0] == "Rules": + functions = list(FUNCTION_RULES) + else: + functions = [ + "Condition", + "Fn::Equals", + "Fn::Not", + "Fn::And", + "Fn::Or", + ] + return { "type": "array", "minItems": 2, "maxItems": 10, "fn_items": { - "functions": [ - "Condition", - "Fn::Equals", - "Fn::Not", - "Fn::And", - "Fn::Or", - ], + "functions": functions, "schema": { "type": ["boolean"], }, diff --git a/src/cfnlint/rules/conditions/Not.py b/src/cfnlint/rules/conditions/Not.py index 391d51cf16..f6036fe4c5 100644 --- a/src/cfnlint/rules/conditions/Not.py +++ b/src/cfnlint/rules/conditions/Not.py @@ -7,6 +7,7 @@ from typing import Any +from cfnlint.helpers import FUNCTION_RULES from cfnlint.jsonschema import Validator from cfnlint.rules.functions._BaseFn import BaseFn @@ -25,18 +26,24 @@ def __init__(self) -> None: self.fn_not = self.validate def schema(self, validator: Validator, instance: Any) -> dict[str, Any]: + + if validator.context.path.path and validator.context.path.path[0] == "Rules": + functions = list(FUNCTION_RULES) + else: + functions = [ + "Condition", + "Fn::Equals", + "Fn::Not", + "Fn::And", + "Fn::Or", + ] + return { "type": "array", "maxItems": 1, "minItems": 1, "fn_items": { - "functions": [ - "Condition", - "Fn::Equals", - "Fn::Not", - "Fn::And", - "Fn::Or", - ], + "functions": functions, "schema": { "type": ["boolean"], }, diff --git a/src/cfnlint/rules/conditions/Or.py b/src/cfnlint/rules/conditions/Or.py index b102236520..d8dcfe43cb 100644 --- a/src/cfnlint/rules/conditions/Or.py +++ b/src/cfnlint/rules/conditions/Or.py @@ -7,6 +7,7 @@ from typing import Any +from cfnlint.helpers import FUNCTION_RULES from cfnlint.jsonschema import Validator from cfnlint.rules.functions._BaseFn import BaseFn @@ -25,18 +26,23 @@ def __init__(self) -> None: self.fn_or = self.validate def schema(self, validator: Validator, instance: Any) -> dict[str, Any]: + if validator.context.path.path and validator.context.path.path[0] == "Rules": + functions = list(FUNCTION_RULES) + else: + functions = [ + "Condition", + "Fn::Equals", + "Fn::Not", + "Fn::And", + "Fn::Or", + ] + return { "type": "array", "minItems": 2, "maxItems": 10, "fn_items": { - "functions": [ - "Condition", - "Fn::Equals", - "Fn::Not", - "Fn::And", - "Fn::Or", - ], + "functions": functions, "schema": { "type": ["boolean"], }, diff --git a/test/unit/rules/conditions/test_and.py b/test/unit/rules/conditions/test_and.py new file mode 100644 index 0000000000..beee37f9bf --- /dev/null +++ b/test/unit/rules/conditions/test_and.py @@ -0,0 +1,101 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.conditions.And import And + + +@pytest.fixture +def rule(): + rule = And() + yield rule + + +@pytest.fixture +def template(): + return { + "Parameters": { + "Environment": { + "Type": "String", + } + }, + "Conditions": { + "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]}, + "IsProduction": {"Fn::Equals": [{"Ref": "Environment"}, "Production"]}, + }, + } + + +@pytest.mark.parametrize( + "name,instance,path,errors", + [ + ( + "Valid and with other conditions", + {"Fn::And": [{"Condition": "IsUsEast1"}, {"Condition": "IsProduction"}]}, + {}, + [], + ), + ( + "Valid and with boolean types", + {"Fn::And": [True, False]}, + {}, + [], + ), + ( + "Invalid Type", + {"Fn::And": {}}, + {}, + [ + ValidationError( + "{} is not of type 'array'", + validator="fn_and", + schema_path=deque(["type"]), + path=deque(["Fn::And"]), + ), + ], + ), + ( + "Integer type", + {"Fn::And": ["a", True]}, + {}, + [ + ValidationError( + "'a' is not of type 'boolean'", + validator="fn_and", + schema_path=deque(["fn_items", "type"]), + path=deque(["Fn::And", 0]), + ) + ], + ), + ( + "Invalid functions in Conditions", + {"Fn::And": [True, {"Fn::Contains": []}]}, + {"path": deque(["Conditions", "Condition1"])}, + [ + ValidationError( + "{'Fn::Contains': []} is not of type 'boolean'", + validator="fn_and", + schema_path=deque(["fn_items", "type"]), + path=deque(["Fn::And", 1]), + ) + ], + ), + ( + "Valid functions in Rules", + {"Fn::And": [True, {"Fn::Contains": []}]}, + {"path": deque(["Rules", "Rule1"])}, + [], + ), + ], + indirect=["path"], +) +def test_condition(name, instance, errors, rule, validator): + errs = list(rule.validate(validator, {}, instance, {})) + + assert errs == errors, name diff --git a/test/unit/rules/conditions/test_not.py b/test/unit/rules/conditions/test_not.py new file mode 100644 index 0000000000..848f779daf --- /dev/null +++ b/test/unit/rules/conditions/test_not.py @@ -0,0 +1,101 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.conditions.Not import Not + + +@pytest.fixture +def rule(): + rule = Not() + yield rule + + +@pytest.fixture +def template(): + return { + "Parameters": { + "Environment": { + "Type": "String", + } + }, + "Conditions": { + "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]}, + "IsProduction": {"Fn::Equals": [{"Ref": "Environment"}, "Production"]}, + }, + } + + +@pytest.mark.parametrize( + "name,instance,path,errors", + [ + ( + "Valid or with other conditions", + {"Fn::Not": [{"Condition": "IsUsEast1"}]}, + {}, + [], + ), + ( + "Valid or with boolean types", + {"Fn::Not": [True]}, + {}, + [], + ), + ( + "Invalid Type", + {"Fn::Not": {}}, + {}, + [ + ValidationError( + "{} is not of type 'array'", + validator="fn_not", + schema_path=deque(["type"]), + path=deque(["Fn::Not"]), + ), + ], + ), + ( + "Integer type", + {"Fn::Not": ["a"]}, + {}, + [ + ValidationError( + "'a' is not of type 'boolean'", + validator="fn_not", + schema_path=deque(["fn_items", "type"]), + path=deque(["Fn::Not", 0]), + ) + ], + ), + ( + "Invalid functions in Conditions", + {"Fn::Not": [{"Fn::Contains": []}]}, + {"path": deque(["Conditions", "Condition1"])}, + [ + ValidationError( + "{'Fn::Contains': []} is not of type 'boolean'", + validator="fn_not", + schema_path=deque(["fn_items", "type"]), + path=deque(["Fn::Not", 0]), + ) + ], + ), + ( + "Valid functions in Rules", + {"Fn::Not": [{"Fn::Contains": []}]}, + {"path": deque(["Rules", "Rule1"])}, + [], + ), + ], + indirect=["path"], +) +def test_condition(name, instance, errors, rule, validator): + errs = list(rule.validate(validator, {}, instance, {})) + + assert errs == errors, name diff --git a/test/unit/rules/conditions/test_or.py b/test/unit/rules/conditions/test_or.py new file mode 100644 index 0000000000..3dfbedce53 --- /dev/null +++ b/test/unit/rules/conditions/test_or.py @@ -0,0 +1,101 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.conditions.Or import Or + + +@pytest.fixture +def rule(): + rule = Or() + yield rule + + +@pytest.fixture +def template(): + return { + "Parameters": { + "Environment": { + "Type": "String", + } + }, + "Conditions": { + "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]}, + "IsProduction": {"Fn::Equals": [{"Ref": "Environment"}, "Production"]}, + }, + } + + +@pytest.mark.parametrize( + "name,instance,path,errors", + [ + ( + "Valid or with other conditions", + {"Fn::Or": [{"Condition": "IsUsEast1"}, {"Condition": "IsProduction"}]}, + {}, + [], + ), + ( + "Valid or with boolean types", + {"Fn::Or": [True, False]}, + {}, + [], + ), + ( + "Invalid Type", + {"Fn::Or": {}}, + {}, + [ + ValidationError( + "{} is not of type 'array'", + validator="fn_or", + schema_path=deque(["type"]), + path=deque(["Fn::Or"]), + ), + ], + ), + ( + "Integer type", + {"Fn::Or": ["a", True]}, + {}, + [ + ValidationError( + "'a' is not of type 'boolean'", + validator="fn_or", + schema_path=deque(["fn_items", "type"]), + path=deque(["Fn::Or", 0]), + ) + ], + ), + ( + "Invalid functions in Conditions", + {"Fn::Or": [True, {"Fn::Contains": []}]}, + {"path": deque(["Conditions", "Condition1"])}, + [ + ValidationError( + "{'Fn::Contains': []} is not of type 'boolean'", + validator="fn_or", + schema_path=deque(["fn_items", "type"]), + path=deque(["Fn::Or", 1]), + ) + ], + ), + ( + "Valid functions in Rules", + {"Fn::Or": [True, {"Fn::Contains": []}]}, + {"path": deque(["Rules", "Rule1"])}, + [], + ), + ], + indirect=["path"], +) +def test_condition(name, instance, errors, rule, validator): + errs = list(rule.validate(validator, {}, instance, {})) + + assert errs == errors, name