Skip to content

Commit

Permalink
Add rule E3505 to validate lambda,sqs timeouts
Browse files Browse the repository at this point in the history
  • Loading branch information
kddejong committed Mar 5, 2025
1 parent 7fee6bd commit 9f89d1a
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 0 deletions.
96 changes: 96 additions & 0 deletions src/cfnlint/rules/resources/lmbd/EventSourceMappingToSqsTimeout.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from __future__ import annotations

from collections import deque
from typing import Any

from cfnlint.jsonschema import ValidationError, ValidationResult, Validator
from cfnlint.rules.helpers import get_value_from_path
from cfnlint.rules.jsonschema.CfnLintKeyword import CfnLintKeyword


class EventSourceMappingToSqsTimeout(CfnLintKeyword):

id = "E3505"
shortdesc = (
"Validate SQS 'VisibilityTimeout' is greater than a function's 'Timeout'"
)
description = (
"When attaching a Lambda function to a SQS queue to a Lambda function the "
"SQS 'VisibilityTimeout' has to be greater than or equal to "
" the lambda functions's 'Timeout'"
)
source_url = "https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html"
tags = ["resources", "lambda", "sqs"]

def __init__(self):
"""Init"""
super().__init__(["Resources/AWS::Lambda::Function/Properties/Timeout"])

def validate(
self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
) -> ValidationResult:

if validator.is_type(instance, "string"):
try:
instance = int(instance)
except: # noqa: E722
return

if not validator.is_type(instance, "integer"):
return

if validator.cfn.graph is None: # pragma: no cover
return # pragma: no cover

if not len(validator.context.path.path) >= 2:
return

resource_name = validator.context.path.path[1]
for child_1, _ in validator.cfn.graph.graph.in_edges(resource_name):
if child_1 not in validator.context.resources:
continue

if (
validator.context.resources[child_1].type
== "AWS::Lambda::EventSourceMapping"
):
for _, child_2 in validator.cfn.graph.graph.out_edges(child_1):
if child_2 not in validator.context.resources:
continue

Check warning on line 64 in src/cfnlint/rules/resources/lmbd/EventSourceMappingToSqsTimeout.py

View check run for this annotation

Codecov / codecov/patch

src/cfnlint/rules/resources/lmbd/EventSourceMappingToSqsTimeout.py#L64

Added line #L64 was not covered by tests
if validator.context.resources[child_2].type == "AWS::SQS::Queue":
for visibility_timeout, _ in get_value_from_path(
validator,
validator.cfn.template,
deque(
[
"Resources",
child_2,
"Properties",
"VisibilityTimeout",
]
),
):
if validator.is_type(visibility_timeout, "string"):
try:
visibility_timeout = int(visibility_timeout)
except: # noqa: E722
continue

if not validator.is_type(visibility_timeout, "integer"):
continue

if visibility_timeout < instance:
yield ValidationError(
message=(
f"Queue visibility timeout "
f"({visibility_timeout!r}) "
"is less than Function timeout "
f"({instance!r}) seconds"
),
rule=self,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: MIT-0
"""

from collections import deque

import jsonpatch
import pytest

from cfnlint.jsonschema import ValidationError
from cfnlint.rules.resources.lmbd.EventSourceMappingToSqsTimeout import (
EventSourceMappingToSqsTimeout,
)


@pytest.fixture(scope="module")
def rule():
rule = EventSourceMappingToSqsTimeout()
yield rule


_template = {
"Parameters": {
"BatchSize": {"Type": "String"},
},
"Resources": {
"MyFifoQueue": {
"Type": "AWS::SQS::Queue",
"Properties": {
"VisibilityTimeout": 300,
"MessageRetentionPeriod": 7200,
},
},
"SQSBatch": {
"Type": "AWS::Lambda::EventSourceMapping",
"Properties": {
"BatchSize": {"Ref": "BatchSize"},
"Enabled": True,
"EventSourceArn": {"Fn::GetAtt": "MyFifoQueue.Arn"},
"FunctionName": {"Ref": "Lambda"},
},
},
"Lambda": {
"Type": "AWS::Lambda::Function",
"Properties": {"Role": {"Fn::GetAtt": "Role.Arn"}},
},
"CustomResource": {
"Type": "AWS::CloudFormation::CustomResource",
"Properties": {"Key": {"Fn::GetAtt": "Lambda.Arn"}},
},
},
"Outputs": {
"LambdaArn": {"Value": {"Fn::GetAtt": "Lambda.Arn"}},
"SourceMapping": {"Value": {"Ref": "SQSBatch"}},
},
}


@pytest.mark.parametrize(
"instance,template,path,expected",
[
(
"100",
_template,
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
[],
),
(
"a",
_template,
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
[],
),
(
{"Ref": "AWS::Region"},
_template,
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
[],
),
(
"100",
jsonpatch.apply_patch(
_template,
[
{
"op": "add",
"path": (
"/Resources/MyFifoQueue/" "Properties/VisibilityTimeout"
),
"value": "300",
},
],
),
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
[],
),
(
"600",
jsonpatch.apply_patch(
_template,
[
{
"op": "add",
"path": (
"/Resources/MyFifoQueue/" "Properties/VisibilityTimeout"
),
"value": {"Ref": "AWS::Region"},
},
],
),
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
[],
),
(
"600",
jsonpatch.apply_patch(
_template,
[
{
"op": "add",
"path": (
"/Resources/MyFifoQueue/" "Properties/VisibilityTimeout"
),
"value": "a",
},
],
),
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
[],
),
(
"600",
_template,
{"path": deque(["Resources"])},
[],
),
(
"600",
_template,
{"path": deque(["Resources", "Lambda", "Properties", "Timeout"])},
[
ValidationError(
(
"Queue visibility timeout (300) is less "
"than Function timeout (600) seconds"
),
rule=EventSourceMappingToSqsTimeout(),
)
],
),
],
indirect=["template", "path"],
)
def test_lambda_runtime(instance, template, path, expected, rule, validator):
errs = list(rule.validate(validator, "", instance, {}))
assert errs == expected, f"Expected {expected} got {errs}"

0 comments on commit 9f89d1a

Please sign in to comment.