Skip to content

Commit c8560af

Browse files
authored
Add Condition logic for template Rules (#3634)
* Add rule logic to v0 condition logic
1 parent 44b7aa1 commit c8560af

File tree

8 files changed

+477
-5
lines changed

8 files changed

+477
-5
lines changed

src/cfnlint/conditions/_rule.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""
2+
Copyright 2019 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, Dict
9+
10+
from sympy import And, Implies, Symbol
11+
from sympy.logic.boolalg import BooleanFunction
12+
13+
from cfnlint.conditions._condition import (
14+
ConditionAnd,
15+
ConditionList,
16+
ConditionNamed,
17+
ConditionNot,
18+
ConditionOr,
19+
)
20+
from cfnlint.conditions._equals import Equal
21+
from cfnlint.helpers import FUNCTION_CONDITIONS
22+
23+
# we leave the type hinting here
24+
_RULE = Dict[str, Any]
25+
26+
27+
class _Assertion:
28+
def __init__(self, condition: Any, all_conditions: dict[str, dict]) -> None:
29+
self._fn_equals: Equal | None = None
30+
self._condition: ConditionList | ConditionNamed | None = None
31+
32+
if len(condition) == 1:
33+
for k, v in condition.items():
34+
if k in FUNCTION_CONDITIONS:
35+
if not isinstance(v, list):
36+
raise ValueError(f"{k} value should be an array")
37+
if k == "Fn::Equals":
38+
self._fn_equals = Equal(v)
39+
elif k == "Fn::And":
40+
self._condition = ConditionAnd(v, all_conditions)
41+
elif k == "Fn::Or":
42+
self._condition = ConditionOr(v, all_conditions)
43+
elif k == "Fn::Not":
44+
self._condition = ConditionNot(v, all_conditions)
45+
elif k == "Condition":
46+
if not isinstance(v, str):
47+
raise ValueError(f"Condition value {v!r} must be a string")
48+
self._condition = ConditionNamed(v, all_conditions)
49+
else:
50+
raise ValueError(f"Unknown key ({k}) in condition")
51+
else:
52+
raise ValueError("Condition value must be an object of length 1")
53+
54+
def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction | Symbol | None:
55+
if self._fn_equals:
56+
return self._fn_equals.hash
57+
58+
if self._condition:
59+
return self._condition.build_cnf(params)
60+
61+
return None
62+
63+
@property
64+
def equals(self) -> list[Equal]:
65+
if self._fn_equals:
66+
return [self._fn_equals]
67+
if self._condition:
68+
return self._condition.equals
69+
return []
70+
71+
72+
class _Assertions:
73+
def __init__(self, assertions: list[dict], all_conditions: dict[str, dict]) -> None:
74+
self._assertions: list[_Assertion] = []
75+
for assertion in assertions:
76+
assert_ = assertion.get("Assert", {})
77+
self._assertions.append(_Assertion(assert_, all_conditions))
78+
79+
def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction | Symbol | None:
80+
81+
assertions = []
82+
for assertion in self._assertions:
83+
assertions.append(assertion.build_cnf(params))
84+
85+
return And(*assertions)
86+
87+
@property
88+
def equals(self) -> list[Equal]:
89+
90+
results = []
91+
for assertion in self._assertions:
92+
results.extend(assertion.equals)
93+
return results
94+
95+
96+
class Rule:
97+
98+
def __init__(self, rule: _RULE, all_conditions: dict[str, dict]) -> None:
99+
self._condition: _Assertion | None = None
100+
self._assertions: _Assertions | None = None
101+
self._init_rule(rule, all_conditions)
102+
103+
def _init_rule(
104+
self,
105+
rule: _RULE,
106+
all_conditions: dict[str, dict],
107+
) -> None:
108+
condition = rule.get("RuleCondition")
109+
if condition:
110+
self._condition = _Assertion(condition, all_conditions)
111+
112+
assertions = rule.get("Assertions")
113+
if not assertions:
114+
raise ValueError("Rule must have Assertions")
115+
self._assertions = _Assertions(assertions, all_conditions)
116+
117+
@property
118+
def equals(self) -> list[Equal]:
119+
result = []
120+
if self._condition:
121+
result.extend(self._condition.equals)
122+
if self._assertions:
123+
result.extend(self._assertions.equals)
124+
return result
125+
126+
def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction | Symbol | None:
127+
128+
if self._assertions:
129+
if self._condition:
130+
return Implies(
131+
self._condition.build_cnf(params),
132+
self._assertions.build_cnf(params),
133+
)
134+
return self._assertions.build_cnf(params)
135+
return None

src/cfnlint/conditions/conditions.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from cfnlint.conditions._condition import ConditionNamed
2020
from cfnlint.conditions._equals import Equal, EqualParameter
2121
from cfnlint.conditions._errors import UnknownSatisfisfaction
22+
from cfnlint.conditions._rule import Rule
2223
from cfnlint.conditions._utils import get_hash
2324

2425
LOGGER = logging.getLogger(__name__)
@@ -32,8 +33,10 @@ class Conditions:
3233
def __init__(self, cfn) -> None:
3334
self._conditions: dict[str, ConditionNamed] = {}
3435
self._parameters: dict[str, list[str]] = {}
36+
self._rules: list[Rule] = []
3537
self._init_conditions(cfn=cfn)
3638
self._init_parameters(cfn=cfn)
39+
self._init_rules(cfn=cfn)
3740
self._cnf, self._solver_params = self._build_cnf(list(self._conditions.keys()))
3841

3942
def _init_conditions(self, cfn):
@@ -74,6 +77,29 @@ def _init_parameters(self, cfn: Any) -> None:
7477
if isinstance(allowed_value, (str, int, float, bool)):
7578
self._parameters[param_hash].append(get_hash(str(allowed_value)))
7679

80+
def _init_rules(self, cfn: Any) -> None:
81+
rules = cfn.template.get("Rules")
82+
conditions = cfn.template.get("Conditions")
83+
if not isinstance(rules, dict) or not isinstance(conditions, dict):
84+
return
85+
for k, v in rules.items():
86+
if not isinstance(rules, dict):
87+
continue
88+
try:
89+
self._rules.append(Rule(v, conditions))
90+
except ValueError as e:
91+
LOGGER.debug("Captured error while building rule %s: %s", k, str(e))
92+
except Exception as e: # pylint: disable=broad-exception-caught
93+
if LOGGER.getEffectiveLevel() == logging.DEBUG:
94+
error_message = traceback.format_exc()
95+
else:
96+
error_message = str(e)
97+
LOGGER.debug(
98+
"Captured unknown error while building rule %s: %s",
99+
k,
100+
error_message,
101+
)
102+
77103
def get(self, name: str, default: Any = None) -> ConditionNamed:
78104
"""Return the conditions"""
79105
return self._conditions.get(name, default)
@@ -155,6 +181,9 @@ def _build_cnf(
155181
if prop is not None:
156182
cnf.add_prop(Not(prop))
157183

184+
for rule in self._rules:
185+
cnf.add_prop(rule.build_cnf(equal_vars))
186+
158187
return (cnf, equal_vars)
159188

160189
def build_scenarios(

src/cfnlint/context/conditions/_conditions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class Conditions:
3434

3535
@classmethod
3636
def create_from_instance(
37-
cls, conditions: Any, parameters: dict[str, "Parameter"]
37+
cls, conditions: Any, rules: dict[str, dict], parameters: dict[str, "Parameter"]
3838
) -> "Conditions":
3939
obj: dict[str, Condition] = {}
4040
if not isinstance(conditions, dict):

src/cfnlint/context/context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,10 +431,12 @@ def create_context_for_template(cfn):
431431

432432
try:
433433
conditions = Conditions.create_from_instance(
434-
cfn.template.get("Conditions", {}), parameters
434+
cfn.template.get("Conditions", {}),
435+
cfn.template.get("Rules", {}),
436+
parameters,
435437
)
436438
except (ValueError, AttributeError):
437-
conditions = Conditions.create_from_instance({}, {})
439+
conditions = Conditions.create_from_instance({}, {}, {})
438440

439441
mappings = Mappings.create_from_dict(cfn.template.get("Mappings", {}))
440442

test/integration/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ def run_module_integration_scenarios(self, config):
113113

114114
runner = Runner(scenario_config)
115115

116-
print(f"Running test for {filename!r}")
117116
with patch("sys.exit") as exit:
118117
with patch("sys.stdout", new=StringIO()) as out:
119118
runner.cli()

0 commit comments

Comments
 (0)