Skip to content

Commit cf66b46

Browse files
authored
Add rules for Rules section (#3703)
* Add rules for Rules section
1 parent af75136 commit cf66b46

File tree

14 files changed

+407
-4
lines changed

14 files changed

+407
-4
lines changed

src/cfnlint/data/schemas/other/rules/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"additionalProperties": false,
3+
"definitions": {
4+
"assertion": {
5+
"properties": {
6+
"Assert": {},
7+
"AssertDescription": {
8+
"type": "string"
9+
}
10+
},
11+
"type": "object"
12+
},
13+
"rule": {
14+
"properties": {
15+
"Assertions": {
16+
"items": {
17+
"$ref": "#/definitions/assertion"
18+
},
19+
"type": "array"
20+
},
21+
"RuleCondition": {}
22+
},
23+
"required": [
24+
"Assertions"
25+
],
26+
"type": "object"
27+
}
28+
},
29+
"patternProperties": {
30+
"^[A-Za-z0-9]+$": {
31+
"$ref": "#/definitions/rule"
32+
}
33+
},
34+
"type": "object"
35+
}

src/cfnlint/helpers.py

+21
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,33 @@
185185
FUNCTION_NOT = "Fn::Not"
186186
FUNCTION_EQUALS = "Fn::Equals"
187187
FUNCTION_BASE64 = "Fn::Base64"
188+
FUNCTION_CONTAINS = "Fn::Contains"
189+
FUNCTION_EACH_MEMBER_EQUALS = "Fn::EachMemberEquals"
190+
FUNCTION_EACH_MEMBER_IN = "Fn::EachMemberIn"
191+
FUNCTION_REF_ALL = "Fn::RefAll"
192+
FUNCTION_VALUE_OF = "Fn::ValueOf"
193+
FUNCTION_VALUE_OF_ALL = "Fn::ValueOfAll"
188194
FUNCTION_FOR_EACH = re.compile(r"^Fn::ForEach::[a-zA-Z0-9]+$")
189195

190196
FUNCTION_CONDITIONS = frozenset(
191197
[FUNCTION_AND, FUNCTION_OR, FUNCTION_NOT, FUNCTION_EQUALS]
192198
)
193199

200+
FUNCTION_RULES = frozenset(
201+
[
202+
FUNCTION_AND,
203+
FUNCTION_OR,
204+
FUNCTION_NOT,
205+
FUNCTION_EQUALS,
206+
FUNCTION_CONTAINS,
207+
FUNCTION_EACH_MEMBER_EQUALS,
208+
FUNCTION_EACH_MEMBER_IN,
209+
FUNCTION_REF_ALL,
210+
FUNCTION_VALUE_OF,
211+
FUNCTION_VALUE_OF_ALL,
212+
]
213+
)
214+
194215
FUNCTIONS_ALL = frozenset.union(
195216
*[FUNCTIONS, FUNCTION_CONDITIONS, frozenset(["Condition"])]
196217
)

src/cfnlint/rules/resources/ectwo/RouteTableAssociation.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def get_values(self, subnetid, resource_condition, property_condition):
3636
if isinstance(subnetid, dict):
3737
if len(subnetid) == 1:
3838
for key, value in subnetid.items():
39-
if key in cfnlint.helpers.CONDITION_FUNCTIONS:
39+
if key == cfnlint.helpers.FUNCTION_IF:
4040
if isinstance(value, list):
4141
if len(value) == 3:
4242
property_condition = value[0]

src/cfnlint/rules/rules/Assert.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any
9+
10+
from cfnlint.helpers import FUNCTION_RULES
11+
from cfnlint.jsonschema import ValidationResult, Validator
12+
from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema
13+
14+
15+
class Assert(CfnLintJsonSchema):
16+
id = "E1701"
17+
shortdesc = "Validate the configuration of Assertions"
18+
description = "Make sure the Assert value in a Rule is properly configured"
19+
source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/rules-section-structure.html"
20+
tags = ["rules"]
21+
22+
def __init__(self):
23+
super().__init__(
24+
keywords=["Rules/*/Assertions/*/Assert"],
25+
all_matches=True,
26+
)
27+
28+
def validate(
29+
self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
30+
) -> ValidationResult:
31+
validator = validator.evolve(
32+
context=validator.context.evolve(
33+
functions=list(FUNCTION_RULES) + ["Condition"],
34+
),
35+
function_filter=validator.function_filter.evolve(
36+
add_cfn_lint_keyword=False,
37+
),
38+
schema={"type": "boolean"},
39+
)
40+
41+
yield from self._iter_errors(validator, instance)
+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any
9+
10+
import cfnlint.data.schemas.other.rules
11+
from cfnlint.jsonschema import Validator
12+
from cfnlint.jsonschema._keywords import patternProperties
13+
from cfnlint.jsonschema._keywords_cfn import cfn_type
14+
from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails
15+
16+
17+
class Configuration(CfnLintJsonSchema):
18+
id = "E1700"
19+
shortdesc = "Rules have the appropriate configuration"
20+
description = "Making sure the Rules section is properly configured"
21+
source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/rules-section-structure.html"
22+
tags = ["rules"]
23+
24+
def __init__(self):
25+
super().__init__(
26+
keywords=["Rules"],
27+
schema_details=SchemaDetails(
28+
cfnlint.data.schemas.other.rules, "configuration.json"
29+
),
30+
all_matches=True,
31+
)
32+
self.validators = {
33+
"type": cfn_type,
34+
"patternProperties": self._pattern_properties,
35+
}
36+
37+
def _pattern_properties(
38+
self, validator: Validator, aP: Any, instance: Any, schema: Any
39+
):
40+
# We have to rework pattern properties
41+
# to re-add the keyword or we will have an
42+
# infinite loop
43+
validator = validator.evolve(
44+
function_filter=validator.function_filter.evolve(
45+
add_cfn_lint_keyword=True,
46+
)
47+
)
48+
yield from patternProperties(validator, aP, instance, schema)
49+
50+
def validate(
51+
self, validator: Validator, conditions: Any, instance: Any, schema: Any
52+
):
53+
rule_validator = self.extend_validator(
54+
validator=validator,
55+
schema=self.schema,
56+
context=validator.context.evolve(
57+
resources={},
58+
strict_types=False,
59+
),
60+
).evolve(
61+
context=validator.context.evolve(strict_types=False),
62+
function_filter=validator.function_filter.evolve(
63+
add_cfn_lint_keyword=False,
64+
),
65+
)
66+
67+
yield from super()._iter_errors(rule_validator, instance)
+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import Any
9+
10+
from cfnlint.helpers import FUNCTION_RULES
11+
from cfnlint.jsonschema import ValidationResult, Validator
12+
from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema
13+
14+
15+
class RuleCondition(CfnLintJsonSchema):
16+
id = "E1702"
17+
shortdesc = "Validate the configuration of Rules RuleCondition"
18+
description = "Make sure the RuleCondition in a Rule is properly configured"
19+
source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/rules-section-structure.html"
20+
tags = ["rules"]
21+
22+
def __init__(self):
23+
super().__init__(
24+
keywords=["Rules/*/RuleCondition"],
25+
all_matches=True,
26+
)
27+
28+
def validate(
29+
self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
30+
) -> ValidationResult:
31+
validator = validator.evolve(
32+
context=validator.context.evolve(
33+
functions=list(FUNCTION_RULES) + ["Condition"],
34+
),
35+
function_filter=validator.function_filter.evolve(
36+
add_cfn_lint_keyword=False,
37+
),
38+
schema={"type": "boolean"},
39+
)
40+
41+
yield from self._iter_errors(validator, instance)

src/cfnlint/rules/rules/__init__.py

Whitespace-only changes.

src/cfnlint/template/template.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ def get_condition_values(self, template, path: Path | None) -> list[dict[str, An
462462
# Checking for conditions inside of conditions
463463
if isinstance(item, dict):
464464
for sub_key, sub_value in item.items():
465-
if sub_key in cfnlint.helpers.CONDITION_FUNCTIONS:
465+
if sub_key == cfnlint.helpers.FUNCTION_IF:
466466
results = self.get_condition_values(
467467
sub_value, result["Path"] + [sub_key]
468468
)
@@ -521,7 +521,7 @@ def get_values(self, obj, key, path: Path | None = None):
521521
is_condition = False
522522
is_no_value = False
523523
for obj_key, obj_value in value.items():
524-
if obj_key in cfnlint.helpers.CONDITION_FUNCTIONS:
524+
if obj_key == cfnlint.helpers.FUNCTION_IF:
525525
is_condition = True
526526
results = self.get_condition_values(
527527
obj_value, path[:] + [obj_key]
@@ -552,7 +552,7 @@ def get_values(self, obj, key, path: Path | None = None):
552552
is_condition = False
553553
is_no_value = False
554554
for obj_key, obj_value in list_value.items():
555-
if obj_key in cfnlint.helpers.CONDITION_FUNCTIONS:
555+
if obj_key == cfnlint.helpers.FUNCTION_IF:
556556
is_condition = True
557557
results = self.get_condition_values(
558558
obj_value, path[:] + [list_index, obj_key]

test/integration/test_schema_files.py

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class TestSchemaFiles(TestCase):
5252
"Resources/*/Type",
5353
"Resources/*/UpdatePolicy",
5454
"Resources/*/UpdateReplacePolicy",
55+
"Rules",
56+
"Rules/*/Assertions/*/Assert",
57+
"Rules/*/RuleCondition",
5558
"Transform",
5659
]
5760

test/unit/rules/rules/__init__.py

Whitespace-only changes.

test/unit/rules/rules/test_assert.py

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from collections import deque
7+
8+
import pytest
9+
10+
from cfnlint.jsonschema import ValidationError
11+
from cfnlint.rules.rules.Assert import Assert
12+
13+
14+
@pytest.fixture(scope="module")
15+
def rule():
16+
rule = Assert()
17+
yield rule
18+
19+
20+
@pytest.mark.parametrize(
21+
"name,instance,expected",
22+
[
23+
(
24+
"boolean is okay",
25+
True,
26+
[],
27+
),
28+
(
29+
"wrong type",
30+
[],
31+
[
32+
ValidationError(
33+
"[] is not of type 'boolean'",
34+
validator="type",
35+
schema_path=deque(["type"]),
36+
rule=Assert(),
37+
)
38+
],
39+
),
40+
(
41+
"functions are okay",
42+
{"Fn::Equals": ["a", "b"]},
43+
[],
44+
),
45+
],
46+
)
47+
def test_validate(name, instance, expected, rule, validator):
48+
errs = list(rule.validate(validator, {}, instance, {}))
49+
50+
assert errs == expected, f"Test {name!r} got {errs!r}"

0 commit comments

Comments
 (0)