Skip to content

Commit 5dc59d3

Browse files
authored
Change state machine schema based on query language (#4045)
1 parent bdec889 commit 5dc59d3

File tree

2 files changed

+280
-41
lines changed

2 files changed

+280
-41
lines changed

src/cfnlint/rules/resources/stepfunctions/StateMachineDefinition.py

+93-41
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
from __future__ import annotations
77

88
import json
9+
from collections import deque
10+
from copy import deepcopy
911
from typing import Any
1012

1113
import cfnlint.data.schemas.other.resources
1214
import cfnlint.data.schemas.other.step_functions
1315
import cfnlint.helpers
1416
from cfnlint.helpers import is_function
1517
from cfnlint.jsonschema import ValidationError, ValidationResult, Validator
18+
from cfnlint.rules.helpers import get_value_from_path
1619
from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails
1720
from cfnlint.schema.resolver import RefResolver
1821

@@ -40,11 +43,11 @@ def __init__(self):
4043
all_matches=True,
4144
)
4245

43-
store = {
46+
self.store = {
4447
"definition": self.schema,
4548
}
4649

47-
self.resolver = RefResolver.from_schema(self.schema, store=store)
50+
self.resolver = RefResolver.from_schema(self.schema, store=self.store)
4851

4952
def _fix_message(self, err: ValidationError) -> ValidationError:
5053
if len(err.path) > 1:
@@ -53,6 +56,66 @@ def _fix_message(self, err: ValidationError) -> ValidationError:
5356
err.context[i] = self._fix_message(c_err)
5457
return err
5558

59+
def _convert_schema_to_jsonata(self):
60+
schema = self.schema
61+
schema = deepcopy(schema)
62+
schema["definitions"]["common"] = deepcopy(schema["definitions"]["common"])
63+
schema["definitions"]["common"]["allOf"] = [
64+
{
65+
"properties": {
66+
"QueryLanguage": {"const": "JSONata"},
67+
"InputPath": False,
68+
"OutputPath": False,
69+
"Parameters": False,
70+
"ResultPath": False,
71+
"ResultSelector": False,
72+
},
73+
},
74+
]
75+
return schema
76+
77+
def _clean_schema(self, validator: Validator, instance: Any):
78+
for ql, ql_validator in get_value_from_path(
79+
validator, instance, deque(["QueryLanguage"])
80+
):
81+
if ql is None or ql == "JSONPath":
82+
yield self.schema, ql_validator
83+
84+
if ql == "JSONata":
85+
yield self._convert_schema_to_jsonata(), ql_validator
86+
87+
def _validate_step(
88+
self,
89+
validator: Validator,
90+
substitutions: Any,
91+
value: Any,
92+
add_path_to_message: bool,
93+
k: str,
94+
) -> ValidationResult:
95+
96+
for err in validator.iter_errors(value):
97+
if validator.is_type(err.instance, "string"):
98+
if (
99+
err.instance.replace("${", "").replace("}", "").strip()
100+
in substitutions
101+
):
102+
continue
103+
if add_path_to_message:
104+
err = self._fix_message(err)
105+
106+
err.path.appendleft(k)
107+
if err.schema in [True, False]:
108+
err.message = (
109+
f"Additional properties are not allowed ({err.path[-1]!r} "
110+
"was unexpected)"
111+
)
112+
if not err.validator or (
113+
not err.validator.startswith("fn_") and err.validator not in ["cfnLint"]
114+
):
115+
err.rule = self
116+
117+
yield self._clean_error(err)
118+
56119
def validate(
57120
self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
58121
) -> ValidationResult:
@@ -61,6 +124,11 @@ def validate(
61124
if not validator.cfn.has_serverless_transform():
62125
definition_keys.append("DefinitionString")
63126

127+
substitutions = []
128+
props_substitutions = instance.get("DefinitionSubstitutions", {})
129+
if validator.is_type(props_substitutions, "object"):
130+
substitutions = list(props_substitutions.keys())
131+
64132
# First time child rules are configured against the rule
65133
# so we can run this now
66134
for k in definition_keys:
@@ -74,48 +142,32 @@ def validate(
74142
add_path_to_message = False
75143
if validator.is_type(value, "string"):
76144
try:
77-
step_validator = validator.evolve(
78-
context=validator.context.evolve(
79-
functions=[],
80-
),
81-
resolver=self.resolver,
82-
schema=self.schema,
83-
)
84145
value = json.loads(value)
85146
add_path_to_message = True
147+
for schema, schema_validator in self._clean_schema(
148+
validator, value
149+
):
150+
resolver = RefResolver.from_schema(schema, store=self.store)
151+
step_validator = schema_validator.evolve(
152+
context=validator.context.evolve(
153+
functions=[],
154+
),
155+
resolver=resolver,
156+
schema=self.schema,
157+
)
158+
159+
yield from self._validate_step(
160+
step_validator, substitutions, value, add_path_to_message, k
161+
)
86162
except json.JSONDecodeError:
87163
return
88164
else:
89-
step_validator = validator.evolve(
90-
resolver=self.resolver,
91-
schema=self.schema,
92-
)
93-
94-
substitutions = []
95-
props_substitutions = instance.get("DefinitionSubstitutions", {})
96-
if validator.is_type(props_substitutions, "object"):
97-
substitutions = list(props_substitutions.keys())
98-
99-
for err in step_validator.iter_errors(value):
100-
if validator.is_type(err.instance, "string"):
101-
if (
102-
err.instance.replace("${", "").replace("}", "").strip()
103-
in substitutions
104-
):
105-
continue
106-
if add_path_to_message:
107-
err = self._fix_message(err)
108-
109-
err.path.appendleft(k)
110-
if err.schema in [True, False]:
111-
err.message = (
112-
f"Additional properties are not allowed ({err.path[-1]!r} "
113-
"was unexpected)"
165+
for schema, schema_validator in self._clean_schema(validator, value):
166+
resolver = RefResolver.from_schema(schema, store=self.store)
167+
step_validator = schema_validator.evolve(
168+
resolver=resolver,
169+
schema=schema,
170+
)
171+
yield from self._validate_step(
172+
step_validator, substitutions, value, add_path_to_message, k
114173
)
115-
if not err.validator or (
116-
not err.validator.startswith("fn_")
117-
and err.validator not in ["cfnLint"]
118-
):
119-
err.rule = self
120-
121-
yield self._clean_error(err)

test/unit/rules/resources/stepfunctions/test_state_machine_definition.py

+187
Original file line numberDiff line numberDiff line change
@@ -859,6 +859,193 @@ def rule():
859859
),
860860
],
861861
),
862+
(
863+
"JSONAta set at the top level",
864+
{
865+
"DefinitionSubstitutions": {"jobqueue": {"Ref": "MyJob"}},
866+
"Definition": {
867+
"Comment": (
868+
"An example of the Amazon States "
869+
"Language for notification on an "
870+
"AWS Batch job completion"
871+
),
872+
"StartAt": "Submit Batch Job",
873+
"TimeoutSeconds": 3600,
874+
"QueryLanguage": "JSONata",
875+
"States": {
876+
"Submit Batch Job 1": {
877+
"Type": "Task",
878+
"Resource": "${jobqueue}",
879+
"QueryLanguage": "JSONPath", # fails because QueryLanguage for JSONata can't be over written
880+
"Parameters": { # fails because JSONata doesn't support Parameters
881+
"JobName": "BatchJobNotification",
882+
"JobQueue": "arn:aws:batch:us-east-1:123456789012:job-queue/BatchJobQueue-7049d367474b4dd",
883+
"JobDefinition": "arn:aws:batch:us-east-1:123456789012:job-definition/BatchJobDefinition-74d55ec34c4643c:1",
884+
},
885+
"Next": "Notify Success",
886+
"Catch": [
887+
{
888+
"ErrorEquals": ["States.ALL"],
889+
"Next": "Notify Failure",
890+
}
891+
],
892+
},
893+
"Submit Batch Job 2": {
894+
"Type": "Task",
895+
"Resource": "${jobqueue}",
896+
"Arguments": {
897+
"JobName": "BatchJobNotification",
898+
"JobQueue": "arn:aws:batch:us-east-1:123456789012:job-queue/BatchJobQueue-7049d367474b4dd",
899+
"JobDefinition": "arn:aws:batch:us-east-1:123456789012:job-definition/BatchJobDefinition-74d55ec34c4643c:1",
900+
},
901+
"Next": "Notify Success",
902+
"Catch": [
903+
{
904+
"ErrorEquals": ["States.ALL"],
905+
"Next": "Notify Failure",
906+
}
907+
],
908+
"QueryLanguage": "JSONata",
909+
},
910+
"Submit Batch Job 3": {
911+
"Type": "Task",
912+
"Resource": "${jobqueue}",
913+
"Arguments": {
914+
"JobName": "BatchJobNotification",
915+
"JobQueue": "arn:aws:batch:us-east-1:123456789012:job-queue/BatchJobQueue-7049d367474b4dd",
916+
"JobDefinition": "arn:aws:batch:us-east-1:123456789012:job-definition/BatchJobDefinition-74d55ec34c4643c:1",
917+
},
918+
"Next": "Notify Success",
919+
"Catch": [
920+
{
921+
"ErrorEquals": ["States.ALL"],
922+
"Next": "Notify Failure",
923+
}
924+
],
925+
},
926+
"Notify Success": {
927+
"Type": "Task",
928+
"Resource": "arn:aws:states:::sns:publish",
929+
"Parameters": { # fails because the default is now JSONata
930+
"Message": "Batch job submitted through Step Functions succeeded",
931+
"TopicArn": "arn:aws:sns:us-east-1:123456789012:batchjobnotificatiointemplate-SNSTopic-1J757CVBQ2KHM",
932+
},
933+
"End": True,
934+
},
935+
"Notify Failure": {
936+
"Type": "Task",
937+
"Resource": "arn:aws:states:::sns:publish",
938+
"Parameters": { # fails because the default is now JSONata
939+
"Message": "Batch job submitted through Step Functions failed",
940+
"TopicArn": "arn:aws:sns:us-east-1:123456789012:batchjobnotificatiointemplate-SNSTopic-1J757CVBQ2KHM",
941+
},
942+
"End": True,
943+
},
944+
},
945+
},
946+
},
947+
[
948+
ValidationError(
949+
("'JSONata' was expected"),
950+
rule=StateMachineDefinition(),
951+
validator="const",
952+
schema_path=deque(
953+
[
954+
"properties",
955+
"States",
956+
"patternProperties",
957+
"^.{1,128}$",
958+
"allOf",
959+
5,
960+
"then",
961+
"allOf",
962+
0,
963+
"properties",
964+
"QueryLanguage",
965+
"const",
966+
]
967+
),
968+
path=deque(
969+
["Definition", "States", "Submit Batch Job 1", "QueryLanguage"]
970+
),
971+
),
972+
ValidationError(
973+
(
974+
"Additional properties are not allowed ('Parameters' was unexpected)"
975+
),
976+
rule=StateMachineDefinition(),
977+
validator=None,
978+
schema_path=deque(
979+
[
980+
"properties",
981+
"States",
982+
"patternProperties",
983+
"^.{1,128}$",
984+
"allOf",
985+
5,
986+
"then",
987+
"allOf",
988+
0,
989+
"properties",
990+
"Parameters",
991+
]
992+
),
993+
path=deque(
994+
["Definition", "States", "Submit Batch Job 1", "Parameters"]
995+
),
996+
),
997+
ValidationError(
998+
(
999+
"Additional properties are not allowed ('Parameters' was unexpected)"
1000+
),
1001+
rule=StateMachineDefinition(),
1002+
validator=None,
1003+
schema_path=deque(
1004+
[
1005+
"properties",
1006+
"States",
1007+
"patternProperties",
1008+
"^.{1,128}$",
1009+
"allOf",
1010+
5,
1011+
"then",
1012+
"allOf",
1013+
0,
1014+
"properties",
1015+
"Parameters",
1016+
]
1017+
),
1018+
path=deque(
1019+
["Definition", "States", "Notify Success", "Parameters"]
1020+
),
1021+
),
1022+
ValidationError(
1023+
(
1024+
"Additional properties are not allowed ('Parameters' was unexpected)"
1025+
),
1026+
rule=StateMachineDefinition(),
1027+
validator=None,
1028+
schema_path=deque(
1029+
[
1030+
"properties",
1031+
"States",
1032+
"patternProperties",
1033+
"^.{1,128}$",
1034+
"allOf",
1035+
5,
1036+
"then",
1037+
"allOf",
1038+
0,
1039+
"properties",
1040+
"Parameters",
1041+
]
1042+
),
1043+
path=deque(
1044+
["Definition", "States", "Notify Failure", "Parameters"]
1045+
),
1046+
),
1047+
],
1048+
),
8621049
],
8631050
)
8641051
def test_validate(

0 commit comments

Comments
 (0)