Skip to content

Commit

Permalink
Moving to new using schema structure for resource prop rules (#3236)
Browse files Browse the repository at this point in the history
* Moving to new using schema structure for resource prop rules
* Clean some tests for better coverage
  • Loading branch information
kddejong committed May 12, 2024
1 parent 41277c6 commit d394c83
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 169 deletions.
5 changes: 4 additions & 1 deletion src/cfnlint/rules/jsonschema/CfnLint.py
Expand Up @@ -28,4 +28,7 @@ def cfnLint(self, validator, keywords, instance, schema):

for rule_keyword in rule.keywords:
if rule_keyword == keyword:
yield from rule.validate(validator, keyword, instance, schema)
for err in rule.validate(validator, keyword, instance, schema):
if err.rule is None:
err.rule = rule
yield err
82 changes: 24 additions & 58 deletions src/cfnlint/rules/resources/route53/RecordSetName.py
Expand Up @@ -3,10 +3,13 @@
SPDX-License-Identifier: MIT-0
"""

from cfnlint.rules import CloudFormationLintRule, RuleMatch
from collections import deque

from cfnlint.jsonschema import ValidationError
from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword

class RecordSetName(CloudFormationLintRule):

class RecordSetName(CfnLintKeyword):
"""Check if a Route53 Resoruce Records Name is valid with a HostedZoneName"""

id = "E3041"
Expand All @@ -20,73 +23,36 @@ class RecordSetName(CloudFormationLintRule):

def __init__(self):
"""Init"""
super().__init__()
self.resource_property_types = ["AWS::Route53::RecordSet"]

def match_resource_properties(self, properties, _, path, cfn):
matches = []
super().__init__(["Resources/AWS::Route53::RecordSet/Properties"])

property_sets = cfn.get_object_without_conditions(
properties, ["Name", "HostedZoneName"]
def validate(self, validator, keywords, instance, schema):
property_sets = validator.cfn.get_object_without_conditions(
instance, ["Name", "HostedZoneName"]
)
for property_set in property_sets:
props = property_set.get("Object")
scenario = property_set.get("Scenario")
name = props.get("Name", None)
hz_name = props.get("HostedZoneName", None)
if isinstance(name, str) and isinstance(hz_name, str):
if hz_name[-1] != ".":
message = "HostedZoneName must end in a dot at {}"
if scenario is None:
matches.append(
RuleMatch(
path[:] + ["HostedZoneName"],
message.format("/".join(map(str, path))),
)
)
else:
scenario_text = " and ".join(
[
f'when condition "{k}" is {v}'
for (k, v) in scenario.items()
]
)
matches.append(
RuleMatch(
path[:] + ["HostedZoneName"],
message.format(
"/".join(map(str, path)) + " " + scenario_text
),
)
)
yield ValidationError(
f"{hz_name!r} must end in a dot",
path=deque(["HostedZoneName"]),
instance=props.get("HostedZoneName"),
)

if hz_name[-1] == ".":
hz_name = hz_name[:-1]
hz_name = f".{hz_name}"
if name[-1] == ".":
name = name[:-1]

if hz_name not in [name, name[-len(hz_name) :]]:
message = "Name must be a superdomain of HostedZoneName at {}"
if scenario is None:
matches.append(
RuleMatch(
path[:] + ["Name"],
message.format("/".join(map(str, path))),
)
)
else:
scenario_text = " and ".join(
[
f'when condition "{k}" is {v}'
for (k, v) in scenario.items()
]
)
matches.append(
RuleMatch(
path[:] + ["Name"],
message.format(
"/".join(map(str, path)) + " " + scenario_text
),
)
)

return matches
yield ValidationError(
(
f"{props.get('Name')!r} must be a subdomain "
f"of {props.get('HostedZoneName')!r}"
),
path=deque(["Name"]),
instance=props.get("Name"),
)
43 changes: 0 additions & 43 deletions test/fixtures/templates/bad/resources/route53/recordset_name.yaml

This file was deleted.

46 changes: 0 additions & 46 deletions test/fixtures/templates/good/resources/route53/recordset_name.yaml

This file was deleted.

91 changes: 91 additions & 0 deletions test/unit/rules/jsonschema/test_cfn_lint.py
@@ -0,0 +1,91 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

import pytest

from cfnlint.context import Context
from cfnlint.jsonschema import CfnTemplateValidator, ValidationError
from cfnlint.rules.jsonschema.CfnLint import CfnLint
from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword


class _FooError(CfnLintKeyword):
id = "EXXXX"
shortdesc = "A rule"
description = "A rule"

def __init__(self) -> None:
super().__init__(["Foo"])

def validate(self, validator, keywords, instance, schema):
yield ValidationError("No Foo")


class _BarError(CfnLintKeyword):
id = "EYYYY"
shortdesc = "A rule"
description = "A rule"

def __init__(self) -> None:
super().__init__(["Bar"])

def validate(self, validator, keywords, instance, schema):
yield ValidationError("No Bar", rule=self)


class _SubError(CfnLintKeyword):
def __init__(self) -> None:
super().__init__(["Bar"])


@pytest.fixture(scope="module")
def rule():
rule = CfnLint()
rule.child_rules["EXXXX"] = _FooError()
rule.child_rules["EYYYY"] = _BarError()
rule.child_rules["EZZZZ"] = _SubError()
yield rule


@pytest.mark.parametrize(
"name,keywords,expected_errs",
[
(
"When no error in exception add it",
["Foo"],
[
ValidationError(
message="No Foo",
rule=_FooError(),
),
],
),
(
"When no error in exception add it",
["Foo", "Bar"],
[
ValidationError(
message="No Foo",
rule=_FooError(),
),
ValidationError(
message="No Bar",
rule=_BarError(),
),
],
),
(
"When no error in exception add it",
["FooBar"],
[],
),
],
)
def test_cfn_schema(name, keywords, expected_errs, rule):
context = Context(regions=["us-east-1"])
validator = CfnTemplateValidator(schema={}, context=context)

errs = list(rule.cfnLint(validator, keywords, True, {}))
assert errs == expected_errs, f"{name} failed {errs} did not match {expected_errs}"

0 comments on commit d394c83

Please sign in to comment.