Skip to content

Commit fb4a457

Browse files
committed
Use schema cache to know if we need to validate props again
1 parent 66df358 commit fb4a457

File tree

6 files changed

+83
-19
lines changed

6 files changed

+83
-19
lines changed

src/cfnlint/rules/resources/properties/JsonSchema.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
FN_PREFIX,
1212
PSEUDOPARAMS,
1313
REGEX_DYN_REF,
14+
REGION_PRIMARY,
1415
REGISTRY_SCHEMAS,
1516
UNCONVERTED_SUFFIXES,
1617
load_resource,
@@ -195,18 +196,22 @@ def match(self, cfn):
195196
if t.startswith("Custom::"):
196197
t = "AWS::CloudFormation::CustomResource"
197198
if t:
199+
cached_validation_run = []
198200
for region in cfn.regions:
199201
self.region = region
200202
schema = {}
201203
try:
202-
schema = PROVIDER_SCHEMA_MANAGER.get_resource_schema(
203-
region, t
204-
).json_schema()
204+
schema = PROVIDER_SCHEMA_MANAGER.get_resource_schema(region, t)
205205
except ResourceNotFoundError as e:
206206
LOGGER.info(e)
207207
continue
208-
if schema:
209-
cfn_validator = self.validator(schema)
208+
if schema.json_schema():
209+
if t in cached_validation_run or region == REGION_PRIMARY:
210+
if schema.is_cached:
211+
# if its cached we already ran the same validation lets not run it again
212+
continue
213+
cached_validation_run.append(t)
214+
cfn_validator = self.validator(schema.json_schema())
210215
path = ["Resources", n, "Properties"]
211216
for scenario in cfn.get_object_without_nested_conditions(
212217
p, path

src/cfnlint/rules/resources/properties/StringSize.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"""
55
import datetime
66
import json
7+
from typing import Any
78

89
import regex as re
910

@@ -24,12 +25,10 @@ class StringSize(CloudFormationLintRule):
2425
def _serialize_date(self, obj):
2526
if isinstance(obj, datetime.date):
2627
return obj.isoformat()
27-
raise TypeError(
28-
f"Object of type {obj.__class__.__name__} is not JSON serializable"
29-
)
28+
return json.JSONEncoder.default(self, o=obj)
3029

3130
# pylint: disable=too-many-return-statements
32-
def _remove_functions(self, obj):
31+
def _remove_functions(self, obj: Any) -> Any:
3332
"""Replaces intrinsic functions with string"""
3433
if isinstance(obj, dict):
3534
new_obj = {}
@@ -70,14 +69,20 @@ def _non_string_min_length(self, instance, mL):
7069

7170
# pylint: disable=unused-argument
7271
def maxLength(self, validator, mL, instance, schema):
73-
if validator.is_type(instance, "object"):
72+
if (
73+
validator.is_type(instance, "object")
74+
and validator.schema.get("type") == "object"
75+
):
7476
yield from self._non_string_max_length(instance, mL)
7577
elif validator.is_type(instance, "string") and len(instance) > mL:
7678
yield ValidationError(f"{instance!r} is too long")
7779

7880
# pylint: disable=unused-argument
7981
def minLength(self, validator, mL, instance, schema):
80-
if validator.is_type(instance, "object"):
82+
if (
83+
validator.is_type(instance, "object")
84+
and validator.schema.get("type") == "object"
85+
):
8186
yield from self._non_string_min_length(instance, mL)
8287
elif validator.is_type(instance, "string") and len(instance) < mL:
8388
yield ValidationError(f"{instance!r} is too short")

src/cfnlint/schema/manager.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import shutil
1313
import sys
1414
import zipfile
15+
from copy import copy
1516
from typing import Any, Dict, List, Sequence, Union
1617

1718
import jsonpatch
@@ -45,7 +46,7 @@ def __init__(self, path: List[str]):
4546

4647

4748
class ProviderSchemaManager:
48-
_schemas: Dict[str, Schema] = {}
49+
_schemas: Dict[str, Dict[str, Schema]] = {}
4950
_provider_schema_modules: Dict[str, Any] = {}
5051
_cache: Dict[str, Union[Sequence, str]] = {}
5152

@@ -74,7 +75,7 @@ def reset(self):
7475
Important function when processing many templates
7576
and using spec patching
7677
"""
77-
self._schemas: Dict[str, Schema] = {}
78+
self._schemas: Dict[str, Dict[str, Schema]] = {}
7879
self._cache["ResourceTypes"] = {}
7980
self._cache["GetAtts"] = {}
8081
self._cache["RemovedTypes"] = []
@@ -107,10 +108,14 @@ def get_resource_schema(self, region: str, resource_type: str) -> Schema:
107108
)
108109
# load the schema
109110
if f"{rt.provider}.json" in self._provider_schema_modules[reg.name].cached:
110-
self._schemas[reg.name][rt.name] = self.get_resource_schema(
111-
region=self._region_primary.name,
112-
resource_type=rt.name,
111+
schema_cached = copy(
112+
self.get_resource_schema(
113+
region=self._region_primary.name,
114+
resource_type=rt.name,
115+
)
113116
)
117+
schema_cached.is_cached = True
118+
self._schemas[reg.name][rt.name] = schema_cached
114119
return self._schemas[reg.name][rt.name]
115120
try:
116121
self._schemas[reg.name][rt.name] = Schema(
@@ -151,7 +156,7 @@ def get_resource_types(self, region: str) -> List[str]:
151156
resource_type = schema.get("typeName")
152157
if resource_type in self._cache["RemovedTypes"]:
153158
continue
154-
self._schemas[reg.name][resource_type] = Schema(schema)
159+
self._schemas[reg.name][resource_type] = Schema(schema, True)
155160
self._cache["ResourceTypes"][region].append(resource_type)
156161
for filename in resource_listdir(
157162
"cfnlint", resource_name=f"{'/'.join(self._root.path)}/{reg.py}"

src/cfnlint/schema/schema.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@
1616
class Schema:
1717
_json_schema: Dict
1818

19-
def __init__(self, schema) -> None:
19+
def __init__(self, schema: Dict, is_cached: bool = False) -> None:
20+
self.is_cached = is_cached
2021
self.schema = deepcopy(schema)
21-
self._json_schema = self._cleanse_schema(schema=schema)
22+
self._json_schema = self._cleanse_schema(schema=deepcopy(schema))
2223
self.type_name = schema["typeName"]
2324
self._getatts = GetAtts(self.schema)
2425

test/unit/module/schema/test_manager.py

+17
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,20 @@ def test_patch_value_error(self):
246246
self.manager.patch("bad", regions=["us-east-1"])
247247
self.assertEqual(mock_exit.type, SystemExit)
248248
self.assertEqual(mock_exit.value.code == 1)
249+
250+
251+
class TestManagerCaching(BaseTestCase):
252+
"""Test caching and getting cached templates"""
253+
254+
def setUp(self) -> None:
255+
super().setUp()
256+
257+
self.manager = ProviderSchemaManager()
258+
259+
def test_getting_cached_schema(self):
260+
rt = "AWS::EC2::VPC"
261+
262+
self.manager.get_resource_schema("us-east-1", rt)
263+
schema = self.manager.get_resource_schema("us-west-2", rt)
264+
265+
self.assertTrue(schema.is_cached)

test/unit/rules/resources/properties/test_string_size.py

+31
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
33
SPDX-License-Identifier: MIT-0
44
"""
5+
import datetime
56
from collections import deque
67
from test.unit.rules import BaseRuleTestCase
78

@@ -31,6 +32,16 @@ def test_max_length(self):
3132
)
3233
self.assertEqual(list(rule.maxLength(validator, 3, "a", {})), [])
3334
self.assertEqual(len(list(rule.maxLength(validator, 3, "abcd", {}))), 1)
35+
36+
def test_max_object_length(self):
37+
"""Test object max length"""
38+
rule = StringSize()
39+
validator = Draft7Validator(
40+
{
41+
"type": "object",
42+
"maxLength": 3,
43+
}
44+
)
3445
self.assertEqual(len(list(rule.maxLength(validator, 10, {"a": "b"}, {}))), 0)
3546
self.assertEqual(len(list(rule.maxLength(validator, 3, {"a": "bcd"}, {}))), 1)
3647
self.assertEqual(
@@ -42,6 +53,16 @@ def test_max_length(self):
4253
self.assertEqual(
4354
len(list(rule.maxLength(validator, 3, {"Fn::Sub": ["abcd", {}]}, {}))), 1
4455
)
56+
self.assertEqual(
57+
len(
58+
list(rule.maxLength(validator, 100, {"now": datetime.date.today()}, {}))
59+
),
60+
0,
61+
)
62+
self.assertEqual(
63+
len(list(rule.maxLength(validator, 100, {"now": {"Ref": "date"}}, {}))),
64+
0,
65+
)
4566

4667
with self.assertRaises(TypeError):
4768
list(rule.maxLength(validator, 10, {"foo": Unserializable()}, {}))
@@ -57,6 +78,16 @@ def test_min_length(self):
5778
)
5879
self.assertEqual(list(rule.minLength(validator, 3, "abcde", {})), [])
5980
self.assertEqual(len(list(rule.minLength(validator, 3, "ab", {}))), 1)
81+
82+
def test_min_object_length(self):
83+
"""Test min object length"""
84+
rule = StringSize()
85+
validator = Draft7Validator(
86+
{
87+
"type": "object",
88+
"minLength": 3,
89+
}
90+
)
6091
self.assertEqual(len(list(rule.minLength(validator, 5, {"a": "b"}, {}))), 0)
6192
self.assertEqual(len(list(rule.minLength(validator, 12, {"a": "bcd"}, {}))), 1)
6293
self.assertEqual(

0 commit comments

Comments
 (0)