Skip to content

Commit

Permalink
Switch to helper functions
Browse files Browse the repository at this point in the history
  • Loading branch information
kddejong committed Jul 25, 2024
1 parent 22594cd commit 2d31429
Show file tree
Hide file tree
Showing 19 changed files with 990 additions and 652 deletions.
70 changes: 54 additions & 16 deletions src/cfnlint/conditions/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,23 @@ def build_scenarios(
# formatting or just the wrong condition name
return

def implies(self, scenarios: dict[str, bool], implies: str) -> bool:
def _build_cfn_implies(self, scenarios) -> And:
conditions = []
for condition_name, opt in scenarios.items():
if opt:
conditions.append(
self._conditions[condition_name].build_true_cnf(self._solver_params)
)
else:
conditions.append(
self._conditions[condition_name].build_false_cnf(
self._solver_params
)
)

return And(*conditions)

def can_imply(self, scenarios: dict[str, bool], implies: str) -> bool:
"""Based on a bunch of scenario conditions and their Truth/False value
determine if implies condition is True any time the scenarios are satisfied
solver, solver_params = self._build_solver(list(scenarios.keys()) + [implies])
Expand All @@ -260,23 +276,45 @@ def implies(self, scenarios: dict[str, bool], implies: str) -> bool:
if not scenarios.get(implies, True):
return False

conditions = []
for condition_name, opt in scenarios.items():
if opt:
conditions.append(
self._conditions[condition_name].build_true_cnf(
self._solver_params
)
)
else:
conditions.append(
self._conditions[condition_name].build_false_cnf(
self._solver_params
)
)
and_condition = self._build_cfn_implies(scenarios)
cnf.add_prop(and_condition)
implies_condition = self._conditions[implies].build_true_cnf(
self._solver_params
)
cnf.add_prop(Implies(and_condition, implies_condition))

and_condition = And(*conditions)
results = satisfiable(cnf)
if results:
return True

return False
except KeyError:
# KeyError is because the listed condition doesn't exist because of bad
# formatting or just the wrong condition name
return True

def has_to_imply(self, scenarios: dict[str, bool], implies: str) -> bool:
"""Based on a bunch of scenario conditions and their Truth/False value
determine if implies condition is True any time the scenarios are satisfied
solver, solver_params = self._build_solver(list(scenarios.keys()) + [implies])
Args:
scenarios (dict[str, bool]): A list of condition names
and if they are True or False implies: the condition name that
we are implying will also be True
Returns:
bool: if the implied condition will be True if the scenario is True
"""
try:
cnf = self._cnf.copy()
# if the implies condition has to be false in the scenarios we
# know it can never be true
if not scenarios.get(implies, True):
return False

and_condition = self._build_cfn_implies(scenarios)
cnf.add_prop(and_condition)
implies_condition = self._conditions[implies].build_true_cnf(
self._solver_params
)
Expand Down
1 change: 0 additions & 1 deletion src/cfnlint/rules/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,4 @@
from cfnlint.rules.jsonschema import CfnLintJsonSchema # type: ignore
from cfnlint.rules.jsonschema import CfnLintJsonSchemaRegional # type: ignore
from cfnlint.rules.jsonschema import CfnLintKeyword # type: ignore
from cfnlint.rules.jsonschema import CfnLintRelationship # type: ignore
from cfnlint.rules.jsonschema import SchemaDetails
12 changes: 12 additions & 0 deletions src/cfnlint/rules/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from cfnlint.rules.helpers.get_resource_by_name import get_resource_by_name
from cfnlint.rules.helpers.get_value_from_path import get_value_from_path

__all__ = [
"get_value_from_path",
"get_resource_by_name",
]
51 changes: 51 additions & 0 deletions src/cfnlint/rules/helpers/get_resource_by_name.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from __future__ import annotations

from collections import deque
from typing import Any, Sequence

from cfnlint.context import Path
from cfnlint.jsonschema import Validator


def get_resource_by_name(
validator: Validator, name: str, types: Sequence[str] | None = None
) -> tuple[Any, Validator]:

resource = validator.cfn.template.get("Resources", {}).get(name, {})
if not resource or not isinstance(resource, dict):
return None, validator

if types and resource.get("Type") not in types:
return None, validator

condition = resource.get("Condition", None)
if condition:
if not validator.cfn.conditions.can_imply(
validator.context.conditions.status, condition
):
return None, validator
validator = validator.evolve(
context=validator.context.evolve(
conditions=validator.context.conditions.evolve(
{
condition: True,
}
),
)
)

validator = validator.evolve(
context=validator.context.evolve(
path=Path(
path=deque(["Resources", name]),
cfn_path=deque(["Resources", resource.get("Type")]),
)
)
)

return resource, validator
121 changes: 121 additions & 0 deletions src/cfnlint/rules/helpers/get_value_from_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from __future__ import annotations

from collections import deque
from typing import Any, Iterator

from cfnlint.helpers import is_function
from cfnlint.jsonschema import Validator


def _get_relationship_fn_if(
validator: Validator, key: Any, value: Any, path: deque[str | int]
) -> Iterator[tuple[Any, Validator]]:
if not isinstance(value, list) or len(value) != 3:
return

Check warning on line 19 in src/cfnlint/rules/helpers/get_value_from_path.py

View check run for this annotation

Codecov / codecov/patch

src/cfnlint/rules/helpers/get_value_from_path.py#L19

Added line #L19 was not covered by tests
condition = value[0]
# True path
status = {k: set([v]) for k, v in validator.context.conditions.status.items()}
if condition not in status:
status[condition] = set([True, False])
for scenario in validator.cfn.conditions.build_scenarios(status):
if_validator = validator.evolve(
context=validator.context.evolve(
conditions=validator.context.conditions.evolve(
status=scenario,
)
)
)

i = 1 if scenario[condition] else 2
for r, v in get_value_from_path(
validator.evolve(
context=if_validator.context.evolve(
path=if_validator.context.path.descend(path=key).descend(path=i)
)
),
value[i],
path.copy(),
):
yield r, v


def _get_value_from_path_list(
validator: Validator, instance: Any, path: deque[str | int]
) -> Iterator[tuple[Any, Validator]]:
for i, v in enumerate(instance):
for r, v in get_value_from_path(
validator.evolve(
context=validator.context.evolve(
path=validator.context.path.descend(path=i)
),
),
v,
path.copy(),
):
yield r, v


def get_value_from_path(
validator: Validator, instance: Any, path: deque[str | int]
) -> Iterator[tuple[Any, Validator]]:
"""
Retrieve a value from a nested dictionary or list using a path.
Args:
validator (Validator): The validator instance
data (Any): The dictionary or list to search.
path (deque[str | int]): The path to the value.
Returns:
The value at the specified path, or None if the key doesn't exist.
Examples:
>>> data = {'a': {'b': {'c': 3}}}
>>> get_value_from_path(data, ['a', 'b', 'c'])
3
"""

fn_k, fn_v = is_function(instance)
if fn_k is not None:
if fn_k == "Fn::If":
yield from _get_relationship_fn_if(validator, fn_k, fn_v, path)
elif fn_k == "Ref" and fn_v == "AWS::NoValue":
yield None, validator.evolve(
context=validator.context.evolve(
path=validator.context.path.descend(path=fn_k)
)
)
elif not path:
yield instance, validator
return

if not path:
yield instance, validator
return

key = path.popleft()
if isinstance(instance, list) and key == "*":
yield from _get_value_from_path_list(validator, instance, path)
return

if not isinstance(instance, dict):
yield None, validator
return

for r, v in get_value_from_path(
validator.evolve(
context=validator.context.evolve(
path=validator.context.path.descend(path=key)
)
),
instance.get(key),
path.copy(),
):
yield r, v

return
Loading

0 comments on commit 2d31429

Please sign in to comment.