Skip to content

Commit cc27d5f

Browse files
authored
Make intrinsic functions configurable (#4129)
* Make intrinsic functions configurable * Add jsonschema keyword to cfnContext to flip context information * Add jsonschema keyword to dynamicValidation to validate values against dynamic information
1 parent da5163c commit cc27d5f

File tree

92 files changed

+2766
-1035
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+2766
-1035
lines changed

docs/cfn-schema-specification.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,80 @@ is equivalent to the JSON schema
214214
}
215215
}
216216
```
217+
218+
### CloudFormation Context-Aware Validation
219+
220+
To support CloudFormation's unique validation requirements, cfn-lint extends JSON Schema with context-aware validation capabilities.
221+
222+
#### cfnContext
223+
224+
_cfnContext_ provides a way to specify which CloudFormation intrinsic functions are allowed in a specific context and define the schema for validating the value.
225+
226+
```json
227+
{
228+
"cfnContext": {
229+
"functions": ["Ref", "Fn::GetAtt"],
230+
"schema": {
231+
"type": "string"
232+
}
233+
}
234+
}
235+
```
236+
237+
The `functions` array specifies which intrinsic functions are allowed in this context. The `schema` object defines the validation rules for the value.
238+
239+
For example, to specify that only `Ref` is allowed in a parameter reference:
240+
241+
```json
242+
{
243+
"cfnContext": {
244+
"functions": ["Ref"],
245+
"schema": {
246+
"type": "string"
247+
}
248+
}
249+
}
250+
```
251+
252+
#### dynamicValidation
253+
254+
_dynamicValidation_ enables validation against dynamic sources from the template context, such as parameter names, condition names, or resource IDs.
255+
256+
```json
257+
{
258+
"dynamicValidation": {
259+
"context": "conditions"
260+
}
261+
}
262+
```
263+
264+
This validates that the value exists in the specified context. Available contexts include:
265+
- `conditions`: Condition names defined in the template
266+
- `mappings`: Mapping names defined in the template
267+
- `refs`: CloudFormation valid refs
268+
269+
_dynamicValidation_ can also check if a specific transform is present in the template:
270+
271+
```json
272+
{
273+
"dynamicValidation": {
274+
"transformCheck": "AWS::LanguageExtensions"
275+
}
276+
}
277+
```
278+
279+
This will validate that the specified transform is included in the template.
280+
281+
_dynamicValidation_ can also validate based on the current path in the template:
282+
283+
```json
284+
{
285+
"dynamicValidation": {
286+
"pathCheck": "Resources/MyResource/Properties"
287+
}
288+
}
289+
```
290+
291+
This checks if the current path in the template matches the specified pattern.
292+
293+
These context-aware validation features allow for more precise validation of CloudFormation templates, ensuring that references are valid and that template elements are used in the appropriate contexts.

scripts/update_schemas_from_aws_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def configure_logging():
6464

6565
def write_output(resource, filename, obj):
6666
filename = f"src/cfnlint/data/schemas/extensions/{resource}/{filename}.json"
67-
obj["_description"] = ("Automatically updated using aws api",)
67+
obj["description"] = "Automatically updated using aws api"
6868

6969
with open(filename, "w+", encoding="utf-8") as f:
7070
json.dump(obj, f, indent=1, sort_keys=True, separators=(",", ": "))

scripts/update_specs_from_pricing.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,14 +237,12 @@ def get_rds_pricing():
237237
specs = {}
238238
cluster_specs = {}
239239
for product_region, product_values in rds_details.items():
240-
if product_region not in specs:
241-
specs[product_region] = {"allOf": []}
242-
if product_region not in cluster_specs:
243-
cluster_specs[product_region] = {"allOf": []}
244240
for deployment_option, deployment_values in product_values.items():
245241
if deployment_option in ["Single-AZ", "Multi-AZ"]:
246242
for license_name, license_values in deployment_values.items():
247243
for product_name, instance_types in license_values.items():
244+
if product_region not in specs:
245+
specs[product_region] = {"allOf": []}
248246
if license_name == "general-public-license":
249247
specs[product_region]["allOf"].append(
250248
{
@@ -297,6 +295,8 @@ def get_rds_pricing():
297295
else:
298296
for license_name, license_values in deployment_values.items():
299297
for product_name, instance_types in license_values.items():
298+
if product_region not in cluster_specs:
299+
cluster_specs[product_region] = {"allOf": []}
300300
cluster_specs[product_region]["allOf"].append(
301301
{
302302
"if": {
@@ -364,7 +364,7 @@ def get_results(service, product_families, default=None):
364364
def write_output(resource, filename, obj):
365365
filename = f"src/cfnlint/data/schemas/extensions/{resource}/{filename}.json"
366366
output = {
367-
"_description": "Automatically updated using update_specs_from_pricing",
367+
"description": "Automatically updated using update_specs_from_pricing",
368368
}
369369
for region, values in obj.items():
370370
output[region] = {"enum": sorted(list(values))}

src/cfnlint/config.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ def __call__(self, parser, namespace, values, option_string=None):
515515
advanced.add_argument(
516516
"-s",
517517
"--registry-schemas",
518+
default=[],
518519
help="one or more directories of CloudFormation Registry Schemas",
519520
action="extend",
520521
type=comma_separated_arg,
@@ -616,6 +617,35 @@ class ManualArgs(TypedDict, total=False):
616617
non_zero_exit_code: str
617618
output_file: str
618619
regions: list
620+
registry_schemas: list[str]
621+
622+
623+
def _merge_configs(
624+
cli_value: Any, template_value: Any, file_value: Any, manual_value: Any
625+
) -> Any:
626+
# the CLI will always have an empty list when the item is a list
627+
# we will use that to evaluate if we need to merge the lists
628+
if isinstance(cli_value, list):
629+
merged_list = cli_value.copy()
630+
if isinstance(template_value, list):
631+
merged_list.extend(template_value)
632+
if isinstance(file_value, list):
633+
merged_list.extend(file_value)
634+
if isinstance(manual_value, list):
635+
merged_list.extend(manual_value)
636+
return merged_list
637+
638+
elif isinstance(cli_value, dict):
639+
merged_dict = cli_value.copy()
640+
if isinstance(template_value, dict):
641+
merged_dict.update(template_value)
642+
if isinstance(file_value, dict):
643+
merged_dict.update(file_value)
644+
if isinstance(manual_value, dict):
645+
merged_dict.update(manual_value)
646+
return merged_dict
647+
648+
return None
619649

620650

621651
# pylint: disable=too-many-public-methods
@@ -639,6 +669,7 @@ def __repr__(self):
639669
"mandatory_checks": self.mandatory_checks,
640670
"include_experimental": self.include_experimental,
641671
"configure_rules": self.configure_rules,
672+
"registry_schemas": self.registry_schemas,
642673
"regions": self.regions,
643674
"ignore_bad_template": self.ignore_bad_template,
644675
"debug": self.debug,
@@ -651,34 +682,28 @@ def __repr__(self):
651682
"config_file": self.config_file,
652683
"merge_configs": self.merge_configs,
653684
"non_zero_exit_code": self.non_zero_exit_code,
685+
"patch_specs": self.patch_specs,
654686
}
655687
)
656688

657689
def _get_argument_value(self, arg_name, is_template, is_config_file):
658690
cli_value = getattr(self.cli_args, arg_name)
659691
template_value = self.template_args.get(arg_name)
660692
file_value = self.file_args.get(arg_name)
693+
manual_value = self._manual_args.get(arg_name)
661694

662695
# merge list configurations
663696
# make sure we don't do an infinite loop so skip this check for merge_configs
664697
if arg_name != "merge_configs":
665698
if self.merge_configs:
666-
# the CLI will always have an empty list when the item is a list
667-
# we will use that to evaluate if we need to merge the lists
668-
if isinstance(cli_value, list):
669-
# Use a copy here, otherwise we will
670-
# accumulate template level config
671-
# into the cli_value which will persist between template files
672-
result = cli_value.copy()
673-
if isinstance(template_value, list):
674-
result.extend(template_value)
675-
if isinstance(file_value, list):
676-
result.extend(file_value)
677-
return result
699+
if isinstance(cli_value, (list, dict)):
700+
return _merge_configs(
701+
cli_value, template_value, file_value, manual_value
702+
)
678703

679704
# return individual items
680-
if arg_name in self._manual_args:
681-
return self._manual_args[arg_name]
705+
if manual_value:
706+
return manual_value
682707
if cli_value:
683708
return cli_value
684709
if template_value and is_template:

src/cfnlint/context/context.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,16 @@
3333
@dataclass
3434
class Transforms:
3535
# Template level parameters
36-
transforms: InitVar[str | list[str] | None]
36+
obj: InitVar[str | list[str] | None]
3737
_transforms: list[str] = field(init=False, default_factory=list)
3838

39-
def __post_init__(self, transforms) -> None:
40-
if transforms is None:
39+
def __post_init__(self, obj) -> None:
40+
if obj is None:
4141
return
42-
if not isinstance(transforms, list):
43-
transforms = [transforms]
42+
if not isinstance(obj, list):
43+
obj = [obj]
4444

45-
for transform in transforms:
45+
for transform in obj:
4646
if not isinstance(transform, str):
4747
continue
4848
self._transforms.append(transform)
@@ -52,6 +52,10 @@ def __post_init__(self, transforms) -> None:
5252
self.has_language_extensions_transform
5353
)
5454

55+
@property
56+
def transforms(self):
57+
return self._transforms
58+
5559
def has_language_extensions_transform(self):
5660
return bool(TRANSFORM_LANGUAGE_EXTENSION in self._transforms)
5761

src/cfnlint/data/schemas/extensions/aws_amazonmq_broker/instancetype_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"af-south-1": {
43
"enum": [
54
"mq.m5.2xlarge",
@@ -177,6 +176,7 @@
177176
"mq.t3.micro"
178177
]
179178
},
179+
"description": "Automatically updated using update_specs_from_pricing",
180180
"eu-central-1": {
181181
"enum": [
182182
"mq.m4.large",

src/cfnlint/data/schemas/extensions/aws_appstream_fleet/instancetype_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"ap-northeast-1": {
43
"enum": [
54
"stream.compute.2xlarge",
@@ -270,6 +269,7 @@
270269
"stream.standard.xlarge"
271270
]
272271
},
272+
"description": "Automatically updated using update_specs_from_pricing",
273273
"eu-central-1": {
274274
"enum": [
275275
"stream.compute.2xlarge",

src/cfnlint/data/schemas/extensions/aws_dax_cluster/nodetype_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"ap-northeast-1": {
43
"enum": [
54
"dax.r3.2xlarge",
@@ -146,6 +145,7 @@
146145
"dax.t3.small"
147146
]
148147
},
148+
"description": "Automatically updated using update_specs_from_pricing",
149149
"eu-central-1": {
150150
"enum": [
151151
"dax.r4.16xlarge",

src/cfnlint/data/schemas/extensions/aws_docdb_dbinstance/dbinstanceclass_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"af-south-1": {
43
"enum": [
54
"db.r4.16xlarge",
@@ -363,6 +362,7 @@
363362
"db.t4g.medium"
364363
]
365364
},
365+
"description": "Automatically updated using update_specs_from_pricing",
366366
"eu-central-1": {
367367
"enum": [
368368
"db.r4.16xlarge",

src/cfnlint/data/schemas/extensions/aws_ec2_instance/instancetype_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"af-south-1": {
43
"enum": [
54
"a1.2xlarge",
@@ -15929,6 +15928,7 @@
1592915928
"z1d.xlarge"
1593015929
]
1593115930
},
15931+
"description": "Automatically updated using update_specs_from_pricing",
1593215932
"eu-central-1": {
1593315933
"enum": [
1593415934
"a1.2xlarge",

src/cfnlint/data/schemas/extensions/aws_elasticache_cachecluster/cachenodetype_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"af-south-1": {
43
"enum": [
54
"cache.c1.xlarge",
@@ -1615,6 +1614,7 @@
16151614
"cache.t4g.small"
16161615
]
16171616
},
1617+
"description": "Automatically updated using update_specs_from_pricing",
16181618
"eu-central-1": {
16191619
"enum": [
16201620
"cache.c1.xlarge",

src/cfnlint/data/schemas/extensions/aws_elasticache_cachecluster/engine_version.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
{
2-
"_description": [
3-
"Automatically updated using aws api"
4-
],
52
"allOf": [
63
{
74
"if": {
@@ -126,5 +123,6 @@
126123
}
127124
}
128125
}
129-
]
126+
],
127+
"description": "Automatically updated using aws api"
130128
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing"
2+
"description": "Automatically updated using update_specs_from_pricing"
33
}

src/cfnlint/data/schemas/extensions/aws_emr_cluster/instancetypeconfig_instancetype_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"af-south-1": {
43
"enum": [
54
"c1.medium",
@@ -10591,6 +10590,7 @@
1059110590
"z1d.xlarge"
1059210591
]
1059310592
},
10593+
"description": "Automatically updated using update_specs_from_pricing",
1059410594
"eu-central-1": {
1059510595
"enum": [
1059610596
"c1.medium",

src/cfnlint/data/schemas/extensions/aws_gamelift_fleet/ec2instancetype_enum.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
{
2-
"_description": "Automatically updated using update_specs_from_pricing",
32
"af-south-1": {
43
"enum": [
54
"c3.2xlarge",
@@ -6060,6 +6059,7 @@
60606059
"r8g.xlarge"
60616060
]
60626061
},
6062+
"description": "Automatically updated using update_specs_from_pricing",
60636063
"eu-central-1": {
60646064
"enum": [
60656065
"c3.2xlarge",

0 commit comments

Comments
 (0)