From 29306336288912d67bc9855b735d68b0900fd349 Mon Sep 17 00:00:00 2001 From: Roger Zhang Date: Thu, 3 Sep 2026 14:40:29 -0700 Subject: [PATCH] fix(validate): pin cfn-lint below the release that drops the SAM transform cfn-lint 1.54.0 removed its dependency on the SAM transform and started validating AWS::Serverless resources against schemas it bundles itself. It has no schema for four of the resource types aws-sam-translator supports, so `sam validate --lint` fails a valid template with E3006 'Resource type ... does not exist': AWS::Serverless::CapacityProvider AWS::Serverless::MicrovmImage AWS::Serverless::NetworkConnector AWS::Serverless::WebSocketApi Reported upstream as aws-cloudformation/cfn-lint#4678; pin back to <1.54 until it is fixed. The floor moves to 1.52.0 because earlier versions have no schema for the resources the transform generates for MicrovmImage and NetworkConnector, so they cannot lint those templates either. Add a unit test that lints one of every SAM resource type, so a dependency bump that reintroduces this fails before merge. It fails if SAM gains a resource type the template does not cover. --- pyproject.toml | 3 +- requirements/reproducible-linux.txt | 10 +- requirements/reproducible-mac.txt | 10 +- requirements/reproducible-win.txt | 10 +- .../validate/test_lint_sam_resource_types.py | 128 ++++++++++++++++++ 5 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 tests/unit/commands/validate/test_lint_sam_resource_types.py 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}")