Skip to content

Commit 7fee6bd

Browse files
authored
Add rule to validate mixing API body definitions (#3989)
* Add rule W3660 to validate mixing API body definitions
1 parent 0bf1182 commit 7fee6bd

File tree

2 files changed

+237
-0
lines changed

2 files changed

+237
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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+
from typing import Any
9+
10+
from cfnlint.jsonschema import ValidationError, ValidationResult, Validator
11+
from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword
12+
13+
14+
class RestApiMixingDefinitions(CfnLintKeyword):
15+
id = "W3660"
16+
shortdesc = "Validate if multiple resources are modifying a Rest API definition"
17+
description = (
18+
"When using AWS::ApiGateway::RestApi with 'Body' or 'BodyS3Location' "
19+
"the resource handler will use PutRestApi with mode overwrite. "
20+
"Depending on how resources are updated the IaC template will "
21+
"drift and create orphaned resources."
22+
)
23+
tags = ["resources", "apigateway"]
24+
25+
def __init__(self) -> None:
26+
super().__init__(
27+
keywords=[
28+
"Resources/AWS::ApiGateway::RestApi/Properties/Body",
29+
"Resources/AWS::ApiGateway::RestApi/Properties/BodyS3Location",
30+
],
31+
)
32+
self._mix_types = [
33+
"AWS::ApiGateway::Method",
34+
"AWS::ApiGateway::Model",
35+
"AWS::ApiGateway::Resource",
36+
"AWS::ApiGateway::GatewayResponse",
37+
"AWS::ApiGateway::RequestValidator",
38+
"AWS::ApiGateway::Authorizer",
39+
]
40+
41+
def validate(
42+
self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
43+
) -> ValidationResult:
44+
45+
if validator.cfn.graph is None: # pragma: no cover
46+
return # pragma: no cover
47+
48+
if not len(validator.context.path.path) > 3:
49+
return
50+
51+
resource_name = validator.context.path.path[1]
52+
key = validator.context.path.path[3]
53+
54+
unique_sources: set[str] = set()
55+
for source, _ in validator.cfn.graph.graph.in_edges(resource_name):
56+
if source not in validator.context.resources:
57+
continue
58+
if validator.context.resources[source].type in self._mix_types:
59+
if source not in unique_sources:
60+
yield ValidationError(
61+
message=(
62+
f"Defining {key!r} with a relation to resource {source!r} "
63+
f"of type {validator.context.resources[source].type!r} "
64+
"may result in drift and orphaned resources"
65+
),
66+
rule=self,
67+
)
68+
unique_sources.add(source)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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.apigateway.RestApiMixingDefinitions import (
12+
RestApiMixingDefinitions,
13+
)
14+
15+
16+
@pytest.fixture(scope="module")
17+
def rule():
18+
rule = RestApiMixingDefinitions()
19+
yield rule
20+
21+
22+
_rest_api = {
23+
"Type": "AWS::ApiGateway::RestApi",
24+
"Properties": {},
25+
}
26+
27+
_api_resource = {
28+
"Type": "AWS::ApiGateway::Resource",
29+
"Properties": {
30+
"RestApiId": {"Ref": "RestApi"},
31+
"ParentId": {"Fn::GetAtt": "RestApi.RootResourceId"},
32+
"PathPart": "test",
33+
},
34+
}
35+
36+
_api_model = {
37+
"Type": "AWS::ApiGateway::Model",
38+
"Properties": {
39+
"RestApiId": {"Ref": "RestApi"},
40+
"ContentType": "application/json",
41+
"Description": "Schema for Pets example",
42+
"Name": "PetsModelNoFlatten",
43+
"Schema": {
44+
"$schema": "http://json-schema.org/draft-04/schema#",
45+
"title": "PetsModelNoFlatten",
46+
"type": "array",
47+
"items": {
48+
"type": "object",
49+
"properties": {
50+
"number": {"type": "integer"},
51+
"class": {"type": "string"},
52+
"salesPrice": {"type": "number"},
53+
},
54+
},
55+
},
56+
},
57+
}
58+
59+
_api_stage = {
60+
"Type": "AWS::ApiGateway::Stage",
61+
"Properties": {
62+
"StageName": "Prod",
63+
"Description": "Prod Stage",
64+
"RestApiId": {"Ref": "RestApi"},
65+
},
66+
}
67+
68+
69+
@pytest.mark.parametrize(
70+
"template,path,expected",
71+
[
72+
(
73+
{
74+
"Resources": {
75+
"RestApi": _rest_api,
76+
}
77+
},
78+
{
79+
"path": deque(["Resources", "RestApi", "Properties", "Body"]),
80+
},
81+
[],
82+
),
83+
(
84+
{
85+
"Resources": {
86+
"RestApi": _rest_api,
87+
"ProdStage": _api_stage,
88+
},
89+
"Outputs": {"RestApiId": {"Value": {"Ref": "RestApi"}}},
90+
},
91+
{
92+
"path": deque(["Resources", "RestApi", "Properties", "Body"]),
93+
},
94+
[],
95+
),
96+
(
97+
{
98+
"Resources": {
99+
"RestApi": _rest_api,
100+
"RootResource": _api_resource,
101+
}
102+
},
103+
{
104+
"path": deque(["Resources", "RestApi", "Properties", "Body"]),
105+
},
106+
[
107+
ValidationError(
108+
(
109+
"Defining 'Body' with a relation to "
110+
"resource 'RootResource' of type "
111+
"'AWS::ApiGateway::Resource' may result "
112+
"in drift and orphaned resources"
113+
),
114+
rule=RestApiMixingDefinitions(),
115+
path=deque([]),
116+
)
117+
],
118+
),
119+
(
120+
{
121+
"Resources": {
122+
"RestApi": _rest_api,
123+
"RootResource": _api_resource,
124+
}
125+
},
126+
{
127+
"path": deque(["Resources", "RestApi"]),
128+
},
129+
[],
130+
),
131+
(
132+
{
133+
"Resources": {
134+
"RestApi": _rest_api,
135+
"RootResource": _api_resource,
136+
"Model": _api_model,
137+
}
138+
},
139+
{
140+
"path": deque(["Resources", "RestApi", "Properties", "Body"]),
141+
},
142+
[
143+
ValidationError(
144+
(
145+
"Defining 'Body' with a relation to "
146+
"resource 'RootResource' of type "
147+
"'AWS::ApiGateway::Resource' may result "
148+
"in drift and orphaned resources"
149+
),
150+
rule=RestApiMixingDefinitions(),
151+
),
152+
ValidationError(
153+
(
154+
"Defining 'Body' with a relation to "
155+
"resource 'Model' of type "
156+
"'AWS::ApiGateway::Model' may result "
157+
"in drift and orphaned resources"
158+
),
159+
rule=RestApiMixingDefinitions(),
160+
),
161+
],
162+
),
163+
],
164+
indirect=["template", "path"],
165+
)
166+
def test_validate(template, path, expected, rule, validator):
167+
errs = list(rule.validate(validator, "", "", {}))
168+
169+
assert errs == expected, f"Expected {expected} got {errs}"

0 commit comments

Comments
 (0)