Skip to content

Commit ef15a85

Browse files
authored
Add rule E3505 to validate lambda,sqs timeouts (#3990)
1 parent 7fee6bd commit ef15a85

File tree

2 files changed

+253
-0
lines changed

2 files changed

+253
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 collections import deque
9+
from typing import Any
10+
11+
from cfnlint.jsonschema import ValidationError, ValidationResult, Validator
12+
from cfnlint.rules.helpers import get_value_from_path
13+
from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword
14+
15+
16+
class EventSourceMappingToSqsTimeout(CfnLintKeyword):
17+
18+
id = "E3505"
19+
shortdesc = (
20+
"Validate SQS 'VisibilityTimeout' is greater than a function's 'Timeout'"
21+
)
22+
description = (
23+
"When attaching a Lambda function to a SQS queue to a Lambda function the "
24+
"SQS 'VisibilityTimeout' has to be greater than or equal to "
25+
" the lambda functions's 'Timeout'"
26+
)
27+
source_url = "https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html"
28+
tags = ["resources", "lambda", "sqs"]
29+
30+
def __init__(self):
31+
"""Init"""
32+
super().__init__(["Resources/AWS::Lambda::Function/Properties/Timeout"])
33+
34+
def validate(
35+
self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
36+
) -> ValidationResult:
37+
38+
if validator.is_type(instance, "string"):
39+
try:
40+
instance = int(instance)
41+
except: # noqa: E722
42+
return
43+
44+
if not validator.is_type(instance, "integer"):
45+
return
46+
47+
if validator.cfn.graph is None: # pragma: no cover
48+
return # pragma: no cover
49+
50+
if not len(validator.context.path.path) >= 2:
51+
return
52+
53+
resource_name = validator.context.path.path[1]
54+
for child_1, _ in validator.cfn.graph.graph.in_edges(resource_name):
55+
if child_1 not in validator.context.resources:
56+
continue
57+
58+
if (
59+
validator.context.resources[child_1].type
60+
== "AWS::Lambda::EventSourceMapping"
61+
):
62+
for _, child_2 in validator.cfn.graph.graph.out_edges(child_1):
63+
if child_2 not in validator.context.resources:
64+
continue
65+
if validator.context.resources[child_2].type == "AWS::SQS::Queue":
66+
for visibility_timeout, _ in get_value_from_path(
67+
validator,
68+
validator.cfn.template,
69+
deque(
70+
[
71+
"Resources",
72+
child_2,
73+
"Properties",
74+
"VisibilityTimeout",
75+
]
76+
),
77+
):
78+
if validator.is_type(visibility_timeout, "string"):
79+
try:
80+
visibility_timeout = int(visibility_timeout)
81+
except: # noqa: E722
82+
continue
83+
84+
if not validator.is_type(visibility_timeout, "integer"):
85+
continue
86+
87+
if visibility_timeout < instance:
88+
yield ValidationError(
89+
message=(
90+
f"Queue visibility timeout "
91+
f"({visibility_timeout!r}) "
92+
"is less than Function timeout "
93+
f"({instance!r}) seconds"
94+
),
95+
rule=self,
96+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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 jsonpatch
9+
import pytest
10+
11+
from cfnlint.jsonschema import ValidationError
12+
from cfnlint.rules.resources.lmbd.EventSourceMappingToSqsTimeout import (
13+
EventSourceMappingToSqsTimeout,
14+
)
15+
16+
17+
@pytest.fixture(scope="module")
18+
def rule():
19+
rule = EventSourceMappingToSqsTimeout()
20+
yield rule
21+
22+
23+
_template = {
24+
"Parameters": {
25+
"BatchSize": {"Type": "String"},
26+
},
27+
"Resources": {
28+
"MyFifoQueue": {
29+
"Type": "AWS::SQS::Queue",
30+
"Properties": {
31+
"VisibilityTimeout": 300,
32+
"MessageRetentionPeriod": 7200,
33+
},
34+
},
35+
"SQSBatch": {
36+
"Type": "AWS::Lambda::EventSourceMapping",
37+
"Properties": {
38+
"BatchSize": {"Ref": "BatchSize"},
39+
"Enabled": True,
40+
"EventSourceArn": {"Fn::GetAtt": "MyFifoQueue.Arn"},
41+
"FunctionName": {"Ref": "Lambda"},
42+
},
43+
},
44+
"Lambda": {
45+
"Type": "AWS::Lambda::Function",
46+
"Properties": {"Role": {"Fn::GetAtt": "Role.Arn"}},
47+
},
48+
"CustomResource": {
49+
"Type": "AWS::CloudFormation::CustomResource",
50+
"Properties": {"Key": {"Fn::GetAtt": "Lambda.Arn"}},
51+
},
52+
},
53+
"Outputs": {
54+
"LambdaArn": {"Value": {"Fn::GetAtt": "Lambda.Arn"}},
55+
"SourceMapping": {"Value": {"Ref": "SQSBatch"}},
56+
},
57+
}
58+
59+
60+
@pytest.mark.parametrize(
61+
"instance,template,path,expected",
62+
[
63+
(
64+
"100",
65+
_template,
66+
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
67+
[],
68+
),
69+
(
70+
"a",
71+
_template,
72+
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
73+
[],
74+
),
75+
(
76+
{"Ref": "AWS::Region"},
77+
_template,
78+
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
79+
[],
80+
),
81+
(
82+
"100",
83+
jsonpatch.apply_patch(
84+
_template,
85+
[
86+
{
87+
"op": "add",
88+
"path": (
89+
"/Resources/MyFifoQueue/" "Properties/VisibilityTimeout"
90+
),
91+
"value": "300",
92+
},
93+
],
94+
),
95+
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
96+
[],
97+
),
98+
(
99+
"600",
100+
jsonpatch.apply_patch(
101+
_template,
102+
[
103+
{
104+
"op": "add",
105+
"path": (
106+
"/Resources/MyFifoQueue/" "Properties/VisibilityTimeout"
107+
),
108+
"value": {"Ref": "AWS::Region"},
109+
},
110+
],
111+
),
112+
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
113+
[],
114+
),
115+
(
116+
"600",
117+
jsonpatch.apply_patch(
118+
_template,
119+
[
120+
{
121+
"op": "add",
122+
"path": (
123+
"/Resources/MyFifoQueue/" "Properties/VisibilityTimeout"
124+
),
125+
"value": "a",
126+
},
127+
],
128+
),
129+
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
130+
[],
131+
),
132+
(
133+
"600",
134+
_template,
135+
{"path": deque(["Resources"])},
136+
[],
137+
),
138+
(
139+
"600",
140+
_template,
141+
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
142+
[
143+
ValidationError(
144+
(
145+
"Queue visibility timeout (300) is less "
146+
"than Function timeout (600) seconds"
147+
),
148+
rule=EventSourceMappingToSqsTimeout(),
149+
)
150+
],
151+
),
152+
],
153+
indirect=["template", "path"],
154+
)
155+
def test_lambda_runtime(instance, template, path, expected, rule, validator):
156+
errs = list(rule.validate(validator, "", instance, {}))
157+
assert errs == expected, f"Expected {expected} got {errs}"

0 commit comments

Comments
 (0)