Skip to content

Commit d5c3da9

Browse files
authored
Create rule E3695 to validate cache cluster engines (#3824)
1 parent 4b21477 commit d5c3da9

File tree

4 files changed

+332
-5
lines changed

4 files changed

+332
-5
lines changed

scripts/update_schemas_from_aws_api.py

+84-5
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
session = boto3.session.Session()
1919
config = Config(retries={"max_attempts": 10})
20-
client = session.client("rds", region_name="us-east-1", config=config)
20+
rds_client = session.client("rds", region_name="us-east-1", config=config)
21+
elasticache_client = session.client(
22+
"elasticache", region_name="us-east-1", config=config
23+
)
2124

2225

2326
def configure_logging():
@@ -180,12 +183,65 @@ def write_db_instance(results):
180183
write_output("aws_rds_dbinstance", "engine_version", schema)
181184

182185

183-
def main():
184-
"""main function"""
185-
configure_logging()
186+
def write_elasticache_engines(results):
187+
schema = {"allOf": []}
188+
189+
engines = [
190+
"memcached",
191+
"redis",
192+
"valkey",
193+
]
194+
195+
schema["allOf"].append(
196+
{
197+
"if": {
198+
"properties": {
199+
"Engine": {
200+
"type": "string",
201+
}
202+
},
203+
"required": ["Engine"],
204+
},
205+
"then": {
206+
"properties": {
207+
"Engine": {
208+
"enum": sorted(engines),
209+
}
210+
}
211+
},
212+
}
213+
)
214+
215+
for engine in engines:
216+
if not results.get(engine):
217+
continue
186218

219+
engine_versions = sorted(results.get(engine))
220+
schema["allOf"].append(
221+
{
222+
"if": {
223+
"properties": {
224+
"Engine": {
225+
"const": engine,
226+
},
227+
"EngineVersion": {
228+
"type": ["string", "number"],
229+
},
230+
},
231+
"required": ["Engine", "EngineVersion"],
232+
},
233+
"then": {
234+
"properties": {"EngineVersion": {"enum": sorted(engine_versions)}}
235+
},
236+
}
237+
)
238+
239+
write_output("aws_elasticache_cachecluster", "engine_version", schema)
240+
241+
242+
def rds_api():
187243
results = {}
188-
for page in client.get_paginator("describe_db_engine_versions").paginate():
244+
for page in rds_client.get_paginator("describe_db_engine_versions").paginate():
189245
for version in page.get("DBEngineVersions"):
190246
engine = version.get("Engine")
191247
engine_version = version.get("EngineVersion")
@@ -197,6 +253,29 @@ def main():
197253
write_db_instance(results)
198254

199255

256+
def elasticache_api():
257+
results = {}
258+
for page in elasticache_client.get_paginator(
259+
"describe_cache_engine_versions"
260+
).paginate():
261+
print(page)
262+
for version in page.get("CacheEngineVersions"):
263+
engine = version.get("Engine")
264+
engine_version = version.get("EngineVersion")
265+
if engine not in results:
266+
results[engine] = []
267+
results[engine].append(engine_version)
268+
269+
write_elasticache_engines(results)
270+
271+
272+
def main():
273+
"""main function"""
274+
configure_logging()
275+
rds_api()
276+
elasticache_api()
277+
278+
200279
if __name__ == "__main__":
201280
try:
202281
main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
{
2+
"_description": [
3+
"Automatically updated using aws api"
4+
],
5+
"allOf": [
6+
{
7+
"if": {
8+
"properties": {
9+
"Engine": {
10+
"type": "string"
11+
}
12+
},
13+
"required": [
14+
"Engine"
15+
]
16+
},
17+
"then": {
18+
"properties": {
19+
"Engine": {
20+
"enum": [
21+
"memcached",
22+
"redis",
23+
"valkey"
24+
]
25+
}
26+
}
27+
}
28+
},
29+
{
30+
"if": {
31+
"properties": {
32+
"Engine": {
33+
"const": "memcached"
34+
},
35+
"EngineVersion": {
36+
"type": [
37+
"string",
38+
"number"
39+
]
40+
}
41+
},
42+
"required": [
43+
"Engine",
44+
"EngineVersion"
45+
]
46+
},
47+
"then": {
48+
"properties": {
49+
"EngineVersion": {
50+
"enum": [
51+
"1.4.14",
52+
"1.4.24",
53+
"1.4.33",
54+
"1.4.34",
55+
"1.4.5",
56+
"1.5.10",
57+
"1.5.16",
58+
"1.6.12",
59+
"1.6.17",
60+
"1.6.22",
61+
"1.6.6"
62+
]
63+
}
64+
}
65+
}
66+
},
67+
{
68+
"if": {
69+
"properties": {
70+
"Engine": {
71+
"const": "redis"
72+
},
73+
"EngineVersion": {
74+
"type": [
75+
"string",
76+
"number"
77+
]
78+
}
79+
},
80+
"required": [
81+
"Engine",
82+
"EngineVersion"
83+
]
84+
},
85+
"then": {
86+
"properties": {
87+
"EngineVersion": {
88+
"enum": [
89+
"4.0.10",
90+
"5.0.6",
91+
"6.0",
92+
"6.2",
93+
"7.0",
94+
"7.1"
95+
]
96+
}
97+
}
98+
}
99+
},
100+
{
101+
"if": {
102+
"properties": {
103+
"Engine": {
104+
"const": "valkey"
105+
},
106+
"EngineVersion": {
107+
"type": [
108+
"string",
109+
"number"
110+
]
111+
}
112+
},
113+
"required": [
114+
"Engine",
115+
"EngineVersion"
116+
]
117+
},
118+
"then": {
119+
"properties": {
120+
"EngineVersion": {
121+
"enum": [
122+
"7.2"
123+
]
124+
}
125+
}
126+
}
127+
}
128+
]
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from __future__ import annotations
7+
8+
import cfnlint.data.schemas.extensions.aws_elasticache_cachecluster
9+
from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails
10+
11+
12+
class CacheClusterEngine(CfnLintJsonSchema):
13+
id = "E3695"
14+
shortdesc = "Validate Elasticache Cluster Engine and Engine Version"
15+
description = (
16+
"Validate the Elasticache cluster engine along with the engine version"
17+
)
18+
tags = ["resources"]
19+
source_url = "https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/supported-engine-versions.html"
20+
21+
def __init__(self) -> None:
22+
super().__init__(
23+
keywords=["Resources/AWS::ElastiCache::CacheCluster/Properties"],
24+
schema_details=SchemaDetails(
25+
module=cfnlint.data.schemas.extensions.aws_elasticache_cachecluster,
26+
filename="engine_version.json",
27+
),
28+
all_matches=True,
29+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"""
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: MIT-0
4+
"""
5+
6+
from collections import deque
7+
8+
import pytest
9+
10+
from cfnlint.jsonschema import ValidationError
11+
from cfnlint.rules.resources.elasticache.CacheClusterEngine import CacheClusterEngine
12+
13+
14+
@pytest.fixture(scope="module")
15+
def rule():
16+
rule = CacheClusterEngine()
17+
yield rule
18+
19+
20+
@pytest.mark.parametrize(
21+
"instance,expected",
22+
[
23+
(
24+
{"Engine": "redis", "EngineVersion": 7.1},
25+
[],
26+
),
27+
(
28+
{
29+
"Engine": "redis",
30+
},
31+
[],
32+
),
33+
(
34+
{},
35+
[],
36+
),
37+
(
38+
{
39+
"Engine": "redis",
40+
"EngineVersion": "7.1.0",
41+
},
42+
[
43+
ValidationError(
44+
(
45+
"'7.1.0' is not one of "
46+
"['4.0.10', '5.0.6', "
47+
"'6.0', '6.2', '7.0', '7.1']"
48+
),
49+
validator="enum",
50+
path=deque(["EngineVersion"]),
51+
schema_path=[
52+
"allOf",
53+
2,
54+
"then",
55+
"properties",
56+
"EngineVersion",
57+
"enum",
58+
],
59+
rule=CacheClusterEngine(),
60+
)
61+
],
62+
),
63+
(
64+
{
65+
"Engine": "oss-redis",
66+
"EngineVersion": "7.1",
67+
},
68+
[
69+
ValidationError(
70+
("'oss-redis' is not one of " "['memcached', 'redis', 'valkey']"),
71+
validator="enum",
72+
path=deque(["Engine"]),
73+
schema_path=["allOf", 0, "then", "properties", "Engine", "enum"],
74+
rule=CacheClusterEngine(),
75+
)
76+
],
77+
),
78+
(
79+
{
80+
"Engine": "redis",
81+
"EngineVersion": {"Ref": "AWS::AccountId"},
82+
},
83+
[],
84+
),
85+
],
86+
)
87+
def test_validate(instance, expected, rule, validator):
88+
errs = list(rule.validate(validator, "", instance, {}))
89+
90+
assert errs == expected, f"Expected {expected} got {errs}"

0 commit comments

Comments
 (0)