diff --git a/pyproject.toml b/pyproject.toml index 47760cca152..2b6a4adbd3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,8 @@ dependencies = [ # regex is not a direct dependency of SAM CLI, exclude version 2021.10.8 due to not working on M1 Mac "regex!=2021.10.8", "tzlocal==5.4.4", - "cfn-lint>=1.51.3,<1.56", + # Bounded to the versions that lint every SAM resource type, see #9249 + "cfn-lint>=1.52.0,<1.54", "boto3-stubs[apigateway,cloudformation,ecr,iam,lambda,s3,schemas,secretsmanager,signer,stepfunctions,sts,xray,sqs,kinesis]>=1.41.0", "python-dotenv>=1.0,<1.3", ] diff --git a/requirements/reproducible-linux.txt b/requirements/reproducible-linux.txt index abec254db51..e1f5a46bf38 100644 --- a/requirements/reproducible-linux.txt +++ b/requirements/reproducible-linux.txt @@ -21,7 +21,9 @@ aws-lambda-builders==1.67.0 \ aws-sam-translator==1.113.0 \ --hash=sha256:3a24b3c5bab9c24b6389cdf18a7a745d338363c57803364e028718fb078259e5 \ --hash=sha256:b2db7e4f7c6ee8fafa89e718a82bfa33b8e97c4e64be643aa3595b27cd7a2e2b - # via aws-sam-cli (pyproject.toml) + # via + # aws-sam-cli (pyproject.toml) + # cfn-lint awscrt==0.36.0 \ --hash=sha256:01a55a4de4d3d915714590bd50cd3cc430e8f5bce78a4c6b6308c6a7f513ca12 \ --hash=sha256:0ef852c7bd977402f2c82959d9b6c68e39c74bca885d4578cec86e6a3b60c864 \ @@ -206,9 +208,9 @@ cffi==2.1.1 \ --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 # via cryptography -cfn-lint==1.55.1 \ - --hash=sha256:1e870ec3dfad17bf2cf164decba961551aed00a3eb3e51182eca6987eddb5562 \ - --hash=sha256:4de4ced80c898ce0753b64fc5707bebf011601eb2d027d95e2ab3f91692b99d8 +cfn-lint==1.53.3 \ + --hash=sha256:2ed701460d68314e905165a9ceed4a9ddf874a03f5022a1070b96e3869b1a931 \ + --hash=sha256:739bd8294f07d184b32b2768caa63c18db17b1160d3dd2842abf4efc2980dbe3 # via aws-sam-cli (pyproject.toml) charset-normalizer==3.5.1 \ --hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \ diff --git a/requirements/reproducible-mac.txt b/requirements/reproducible-mac.txt index f3566907470..772dab92989 100644 --- a/requirements/reproducible-mac.txt +++ b/requirements/reproducible-mac.txt @@ -21,7 +21,9 @@ aws-lambda-builders==1.67.0 \ aws-sam-translator==1.113.0 \ --hash=sha256:3a24b3c5bab9c24b6389cdf18a7a745d338363c57803364e028718fb078259e5 \ --hash=sha256:b2db7e4f7c6ee8fafa89e718a82bfa33b8e97c4e64be643aa3595b27cd7a2e2b - # via aws-sam-cli (pyproject.toml) + # via + # aws-sam-cli (pyproject.toml) + # cfn-lint awscrt==0.36.0 \ --hash=sha256:01a55a4de4d3d915714590bd50cd3cc430e8f5bce78a4c6b6308c6a7f513ca12 \ --hash=sha256:0ef852c7bd977402f2c82959d9b6c68e39c74bca885d4578cec86e6a3b60c864 \ @@ -206,9 +208,9 @@ cffi==2.1.1 \ --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 # via cryptography -cfn-lint==1.55.1 \ - --hash=sha256:1e870ec3dfad17bf2cf164decba961551aed00a3eb3e51182eca6987eddb5562 \ - --hash=sha256:4de4ced80c898ce0753b64fc5707bebf011601eb2d027d95e2ab3f91692b99d8 +cfn-lint==1.53.3 \ + --hash=sha256:2ed701460d68314e905165a9ceed4a9ddf874a03f5022a1070b96e3869b1a931 \ + --hash=sha256:739bd8294f07d184b32b2768caa63c18db17b1160d3dd2842abf4efc2980dbe3 # via aws-sam-cli (pyproject.toml) charset-normalizer==3.5.1 \ --hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \ diff --git a/requirements/reproducible-win.txt b/requirements/reproducible-win.txt index 42dd27405c8..ff6999dd3a2 100644 --- a/requirements/reproducible-win.txt +++ b/requirements/reproducible-win.txt @@ -21,7 +21,9 @@ aws-lambda-builders==1.67.0 \ aws-sam-translator==1.113.0 \ --hash=sha256:3a24b3c5bab9c24b6389cdf18a7a745d338363c57803364e028718fb078259e5 \ --hash=sha256:b2db7e4f7c6ee8fafa89e718a82bfa33b8e97c4e64be643aa3595b27cd7a2e2b - # via aws-sam-cli (pyproject.toml) + # via + # aws-sam-cli (pyproject.toml) + # cfn-lint awscrt==0.36.0 \ --hash=sha256:01a55a4de4d3d915714590bd50cd3cc430e8f5bce78a4c6b6308c6a7f513ca12 \ --hash=sha256:0ef852c7bd977402f2c82959d9b6c68e39c74bca885d4578cec86e6a3b60c864 \ @@ -206,9 +208,9 @@ cffi==2.1.1 \ --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 # via cryptography -cfn-lint==1.55.1 \ - --hash=sha256:1e870ec3dfad17bf2cf164decba961551aed00a3eb3e51182eca6987eddb5562 \ - --hash=sha256:4de4ced80c898ce0753b64fc5707bebf011601eb2d027d95e2ab3f91692b99d8 +cfn-lint==1.53.3 \ + --hash=sha256:2ed701460d68314e905165a9ceed4a9ddf874a03f5022a1070b96e3869b1a931 \ + --hash=sha256:739bd8294f07d184b32b2768caa63c18db17b1160d3dd2842abf4efc2980dbe3 # via aws-sam-cli (pyproject.toml) charset-normalizer==3.5.1 \ --hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \ diff --git a/tests/unit/commands/validate/test_lint_sam_resource_types.py b/tests/unit/commands/validate/test_lint_sam_resource_types.py new file mode 100644 index 00000000000..4ec96c01e8b --- /dev/null +++ b/tests/unit/commands/validate/test_lint_sam_resource_types.py @@ -0,0 +1,128 @@ +""" +Guards `sam validate --lint` against a cfn-lint that does not cover every SAM resource type. + +cfn-lint 1.54.0 dropped the SAM transform and started validating AWS::Serverless resources against +schemas it bundles itself, so a resource type it has no schema for fails a valid template with +E3006. See aws-cloudformation/cfn-lint#4678 and the cfn-lint bound in pyproject.toml. +""" + +import inspect +import json +from typing import Any, Dict, Set +from unittest import TestCase + +from cfnlint.api import ManualArgs, lint +from samtranslator.model import sam_resources + +REGION = "us-east-1" + + +def get_sam_resource_types() -> Set[str]: + """Every AWS::Serverless resource type the installed SAM translator can expand.""" + resource_types = set() + for member in vars(sam_resources).values(): + resource_type = getattr(member, "resource_type", None) if inspect.isclass(member) else None + if isinstance(resource_type, str) and resource_type.startswith("AWS::Serverless::"): + resource_types.add(resource_type) + return resource_types + + +def get_all_sam_resource_types_template() -> Dict[str, Any]: + """A valid template holding one of every SAM resource type, so linting it must report nothing.""" + return { + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Resources": { + "Api": { + "Type": "AWS::Serverless::Api", + "Properties": {"StageName": "prod", "DefinitionUri": "s3://bucket/api.yaml"}, + }, + "Application": { + "Type": "AWS::Serverless::Application", + "Properties": {"Location": "s3://bucket/app.yaml"}, + }, + "CapacityProvider": { + "Type": "AWS::Serverless::CapacityProvider", + "Properties": { + "VpcConfig": { + "SubnetIds": ["subnet-0123456789abcdef0"], + "SecurityGroupIds": ["sg-0123456789abcdef0"], + } + }, + }, + "Connector": { + "Type": "AWS::Serverless::Connector", + "Properties": { + "Source": {"Id": "Function"}, + "Destination": {"Id": "SimpleTable"}, + "Permissions": ["Read"], + }, + }, + "Function": { + "Type": "AWS::Serverless::Function", + "Properties": { + "InlineCode": "def handler(event, context): pass", + "Handler": "index.handler", + "Runtime": "python3.13", + }, + }, + "GraphQLApi": { + "Type": "AWS::Serverless::GraphQLApi", + "Properties": {"Auth": {"Type": "AWS_IAM"}, "SchemaInline": "type Query { hello: String }"}, + }, + "HttpApi": {"Type": "AWS::Serverless::HttpApi", "Properties": {}}, + "LayerVersion": { + "Type": "AWS::Serverless::LayerVersion", + "Properties": {"ContentUri": "s3://bucket/layer.zip"}, + }, + "MicrovmImage": { + "Type": "AWS::Serverless::MicrovmImage", + "Properties": { + "Name": "image", + "CodeUri": "s3://bucket/code.zip", + "BaseImageArn": "arn:aws:lambda:us-east-1:123456789012:microvm-base-image/base", + "BaseImageVersion": "1", + }, + }, + "NetworkConnector": { + "Type": "AWS::Serverless::NetworkConnector", + "Properties": { + "VpcConfig": { + "SubnetIds": ["subnet-0123456789abcdef0"], + "SecurityGroupIds": ["sg-0123456789abcdef0"], + "NetworkProtocol": "IPv4", + } + }, + }, + "SimpleTable": {"Type": "AWS::Serverless::SimpleTable", "Properties": {}}, + "StateMachine": { + "Type": "AWS::Serverless::StateMachine", + "Properties": {"Definition": {"StartAt": "Done", "States": {"Done": {"Type": "Succeed"}}}}, + }, + "WebSocketApi": { + "Type": "AWS::Serverless::WebSocketApi", + "Properties": { + "RouteSelectionExpression": "$request.body.action", + "Routes": {"$connect": {"FunctionArn": {"Fn::GetAtt": ["Function", "Arn"]}}}, + }, + }, + }, + } + + +class TestLintEverySamResourceType(TestCase): + def test_lint_every_sam_resource_type(self): + # A resource type cfn-lint does not cover fails a valid template, so lint one of each. + template = get_all_sam_resource_types_template() + + # Fails when SAM gains a resource type the template does not cover. + self.assertEqual( + {resource["Type"] for resource in template["Resources"].values()}, + get_sam_resource_types(), + "Add the new SAM resource type to get_all_sam_resource_types_template", + ) + + # Reaches cfn-lint the same way samcli.commands.validate.validate._lint does. + matches = lint(json.dumps(template), config=ManualArgs(regions=[REGION])) + + self.assertEqual(matches, [], f"cfn-lint rejected a valid SAM template: {matches}")