Skip to content

Commit

Permalink
add region checks into SnapStart rules (#2973)
Browse files Browse the repository at this point in the history
  • Loading branch information
kddejong authored Dec 13, 2023
1 parent e833967 commit 3a2a873
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 41 deletions.
19 changes: 14 additions & 5 deletions src/cfnlint/conditions/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import itertools
import logging
import traceback
from typing import Any, Dict, Generator, Iterator, List, Tuple
from typing import Any, Dict, Iterator, List, Tuple

from sympy import And, Implies, Not, Symbol
from sympy.assumptions.cnf import EncodedCNF
Expand Down Expand Up @@ -152,7 +152,9 @@ def _build_cnf(

return (cnf, equal_vars)

def build_scenarios(self, condition_names: List[str]) -> Iterator[Dict[str, bool]]:
def build_scenarios(
self, condition_names: List[str], region: None = None
) -> Iterator[Dict[str, bool]]:
"""Given a list of condition names this function will yield scenarios that represent
those conditions and there result (True/False)
Expand All @@ -169,7 +171,14 @@ def build_scenarios(self, condition_names: List[str]) -> Iterator[Dict[str, bool
try:
# build a large matric of True/False options based on the provided conditions
scenarios_returned = 0
for p in itertools.product([True, False], repeat=len(condition_names)):
if region:
products = itertools.starmap(
self.build_scenerios_on_region,
itertools.product(condition_names, [region]),
)
else:
products = itertools.product([True, False], repeat=len(condition_names))
for p in products:
cnf = self._cnf.copy()
params = dict(zip(condition_names, p))
for condition_name, opt in params.items():
Expand Down Expand Up @@ -256,8 +265,8 @@ def check_implies(self, scenarios: Dict[str, bool], implies: str) -> bool:

def build_scenerios_on_region(
self, condition_name: str, region: str
) -> Generator[bool, None, None]:
"""Based on a region validate if the condition_name coudle be true
) -> Iterator[bool]:
"""Based on a region validate if the condition_name could be true
Args:
condition_name (str): The name of the condition we are validating against
Expand Down
5 changes: 4 additions & 1 deletion src/cfnlint/rules/resources/lmbd/SnapStartEnabled.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ def __init__(self):
super().__init__()
self.resource_property_types.append("AWS::Lambda::Function")

def validate(self, runtime, path):
def validate(self, runtime, path, region, regions):
if not isinstance(runtime, str):
return []

if region not in regions:
return []

if not (runtime.startswith("java")) or runtime in ["java8.al2", "java8"]:
return []

Expand Down
85 changes: 62 additions & 23 deletions src/cfnlint/rules/resources/lmbd/SnapStartSupported.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,77 @@ def __init__(self):
super().__init__()
self.resource_property_types.append("AWS::Lambda::Function")
self.child_rules = {"I2530": None}
self.regions = [
"us-east-2",
"us-east-1",
"us-west-1",
"us-west-2",
"af-south-1",
"ap-east-1",
"ap-southeast-3",
"ap-south-1",
"ap-northeast-2",
"ap-northeast-3",
"ap-southeast-1",
"ap-southeast-2",
"ap-northeast-1",
"ca-central-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"eu-south-1",
"eu-west-3",
"eu-north-1",
"me-south-1",
"sa-east-1",
]

def match_resource_properties(self, properties, _, path, cfn):
"""Check CloudFormation Properties"""
matches = []

for scenario in cfn.get_object_without_nested_conditions(properties, path):
props = scenario.get("Object")
for region in cfn.regions:
for scenario in cfn.get_object_without_conditions(
properties, ["Runtime", "SnapStart"], region
):
props = scenario.get("Object")

runtime = props.get("Runtime")
snap_start = props.get("SnapStart")
if not snap_start:
if self.child_rules["I2530"]:
matches.extend(
self.child_rules["I2530"].validate(
runtime, path, region, self.regions
)
)
continue

runtime = props.get("Runtime")
snap_start = props.get("SnapStart")
if not snap_start:
if self.child_rules["I2530"]:
matches.extend(self.child_rules["I2530"].validate(runtime, path))
continue
if region not in self.regions:
matches.append(
RuleMatch(
path + ["SnapStart"],
f"Region {region} does not support SnapStart enabled functions",
)
)

if snap_start.get("ApplyOn") != "PublishedVersions":
continue
if snap_start.get("ApplyOn") != "PublishedVersions":
continue

# Validate runtime is a string before using startswith
if not isinstance(runtime, str):
continue
# Validate runtime is a string before using startswith
if not isinstance(runtime, str):
continue

if (
runtime
and (not runtime.startswith("java"))
and runtime not in ["java8.al2", "java8"]
):
matches.append(
RuleMatch(
path + ["SnapStart", "ApplyOn"],
f"{runtime} is not supported for SnapStart enabled functions",
if (
runtime
and (not runtime.startswith("java"))
and runtime not in ["java8.al2", "java8"]
):
matches.append(
RuleMatch(
path + ["SnapStart", "ApplyOn"],
f"{runtime} is not supported for SnapStart enabled functions",
)
)
)

return matches
18 changes: 10 additions & 8 deletions src/cfnlint/template/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,13 +872,13 @@ def is_resource_available(self, path, resource):
# if resource condition isn't available then the resource is available
return results

def get_object_without_nested_conditions(self, obj, path):
def get_object_without_nested_conditions(self, obj, path, region=None):
"""
Get a list of object values without conditions included.
Evaluates deep into the object removing any nested conditions as well
"""
results = []
scenarios = self.get_condition_scenarios_below_path(path)
scenarios = self.get_condition_scenarios_below_path(path, False, region)
if not isinstance(obj, (dict, list)):
return results

Expand Down Expand Up @@ -978,7 +978,7 @@ def get_value(value, scenario): # pylint: disable=R0911

return result

def get_object_without_conditions(self, obj, property_names=None):
def get_object_without_conditions(self, obj, property_names=None, region=None):
"""
Gets a list of object values without conditions included
Input:
Expand Down Expand Up @@ -1018,7 +1018,7 @@ def get_object_without_conditions(self, obj, property_names=None):
o = deepcopy(obj)
results = []

scenarios = self.get_conditions_scenarios_from_object([o])
scenarios = self.get_conditions_scenarios_from_object([o], region)
if not isinstance(obj, dict):
return results

Expand All @@ -1036,7 +1036,9 @@ def get_object_without_conditions(self, obj, property_names=None):

return results

def get_condition_scenarios_below_path(self, path, include_if_in_function=False):
def get_condition_scenarios_below_path(
self, path, include_if_in_function=False, region=None
):
"""
get Condition Scenarios from below path
"""
Expand All @@ -1055,9 +1057,9 @@ def get_condition_scenarios_below_path(self, path, include_if_in_function=False)
else:
results[condition_name] = condition_values

return list(self.conditions.build_scenarios(list(results.keys())))
return list(self.conditions.build_scenarios(list(results.keys()), region))

def get_conditions_scenarios_from_object(self, objs):
def get_conditions_scenarios_from_object(self, objs, region=None):
"""
Get condition from objects
"""
Expand Down Expand Up @@ -1105,7 +1107,7 @@ def get_conditions_from_property(value):
else:
con = con.union(get_conditions_from_property(v))

return list(self.conditions.build_scenarios(list(con)))
return list(self.conditions.build_scenarios(list(con), region))

def get_conditions_from_path(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ Resources:
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole

FunctionJava17:
Type: AWS::Lambda::Function
Properties:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Conditions:
IsMeCentral1: !Equals [!Ref AWS::Region, "me-central-1"]
Resources:
LambdaRole:
Type: AWS::IAM::Role
Expand All @@ -21,7 +23,10 @@ Resources:
S3Key: function.zip
Runtime: java17
SnapStart:
ApplyOn: PublishedVersions
Fn::If:
- IsMeCentral1
- !Ref AWS::NoValue
- ApplyOn: PublishedVersions
TracingConfig:
Mode: Active

Expand All @@ -36,7 +41,10 @@ Resources:
S3Key: function.zip
Runtime: java11
SnapStart:
ApplyOn: None
Fn::If:
- IsMeCentral1
- !Ref AWS::NoValue
- ApplyOn: None
TracingConfig:
Mode: Active

Expand Down
4 changes: 3 additions & 1 deletion test/unit/rules/resources/lmbd/test_snapstart_supported.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ def test_file_positive(self):
def test_file_negative(self):
"""Test failure"""
self.helper_file_negative(
"test/fixtures/templates/bad/resources/lambda/snapstart-supported.yaml", 1
"test/fixtures/templates/bad/resources/lambda/snapstart-supported.yaml",
2,
regions=["us-east-1", "me-central-1"],
)

0 comments on commit 3a2a873

Please sign in to comment.