diff --git a/Makefile b/Makefile index dc2cc11778..5083326961 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ black-check: black --check setup.py samcli tests schema format: black - ruff samcli --fix + ruff check samcli --fix schema: python -m schema.make_schema diff --git a/mypy.ini b/mypy.ini index ba4e3fa9d1..68ba89944e 100644 --- a/mypy.ini +++ b/mypy.ini @@ -49,6 +49,9 @@ ignore_missing_imports=True [mypy-watchdog,watchdog.*] ignore_missing_imports=True +[mypy-cfnlint,cfnlint.*] +ignore_missing_imports=True + # progressive add typechecks and these modules already complete the process, let's keep them clean [mypy-samcli.lib.iac.plugins_interfaces,samcli.commands.build,samcli.lib.build.*,samcli.commands.local.cli_common.invoke_context,samcli.commands.local.lib.local_lambda,samcli.lib.providers.*,samcli.lib.utils.git_repo.py,samcli.lib.cookiecutter.*,samcli.lib.pipeline.*,samcli.commands.pipeline.*] disallow_untyped_defs=True diff --git a/requirements/base.txt b/requirements/base.txt index efe044eee2..032bf6ea8b 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -28,7 +28,7 @@ regex!=2021.10.8 tzlocal==5.2 #Adding cfn-lint dependency for SAM validate -cfn-lint~=0.87.7 +cfn-lint~=1.9.2 # Type checking boto3 objects boto3-stubs[apigateway,cloudformation,ecr,iam,lambda,s3,schemas,secretsmanager,signer,stepfunctions,sts,xray,sqs,kinesis]==1.34.149 diff --git a/requirements/reproducible-linux.txt b/requirements/reproducible-linux.txt index bb6d821ee2..074e487a72 100644 --- a/requirements/reproducible-linux.txt +++ b/requirements/reproducible-linux.txt @@ -16,10 +16,8 @@ attrs==23.2.0 \ --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via - # jschema-to-python # jsonschema # referencing - # sarif-om aws-lambda-builders==1.50.0 \ --hash=sha256:40a613ecb19fbf0b64a47bae14bd252ea5da32ea71fde9808d596e2dbc011baf \ --hash=sha256:ad95ed55359c399872f5825582896500dfc1c5564eccf2a6ab8d0e9f6c1ae385 @@ -116,9 +114,9 @@ cffi==1.16.0 \ --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 # via cryptography -cfn-lint==0.87.7 \ - --hash=sha256:85f6b7f32cf155a74d670d53f86b39f99cfc282b02158d98fdab9fc1dba0809e \ - --hash=sha256:ac6ac86dde1ba3d0fb0e217a4d329239a98f00af7862e4fa5ace6c416c4e056c +cfn-lint==1.9.2 \ + --hash=sha256:6920bd71875e76b36f244bd2ac70975a843e4e859a390625c60d16eaa0e3f3f3 \ + --hash=sha256:cc540882762e1545c37a08b23849f13f2034acfe5ae46079a8815c740bbdcfa2 # via aws-sam-cli (setup.py) chardet==5.2.0 \ --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ @@ -298,18 +296,10 @@ jmespath==1.0.1 \ # aws-sam-cli (setup.py) # boto3 # botocore -jschema-to-python==1.2.3 \ - --hash=sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91 \ - --hash=sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05 - # via cfn-lint jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via cfn-lint -jsonpickle==3.2.2 \ - --hash=sha256:87cd82d237fd72c5a34970e7222dddc0accc13fddf49af84111887ed9a9445aa \ - --hash=sha256:d425fd2b8afe9f5d7d57205153403fbf897782204437882a477e8eed60930f8c - # via jschema-to-python jsonpointer==3.0.0 \ --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef @@ -320,15 +310,10 @@ jsonschema==4.23.0 \ # via # aws-sam-cli (setup.py) # aws-sam-translator - # cfn-lint jsonschema-specifications==2023.12.1 \ --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c # via jsonschema -junit-xml==1.9 \ - --hash=sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f \ - --hash=sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732 - # via cfn-lint markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb @@ -465,12 +450,6 @@ networkx==3.3 \ --hash=sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9 \ --hash=sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2 # via cfn-lint -pbr==6.0.0 \ - --hash=sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda \ - --hash=sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9 - # via - # jschema-to-python - # sarif-om pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc @@ -908,16 +887,10 @@ s3transfer==0.10.2 \ --hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \ --hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69 # via boto3 -sarif-om==1.0.4 \ - --hash=sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911 \ - --hash=sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98 - # via cfn-lint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via - # junit-xml - # python-dateutil + # via python-dateutil sympy==1.13.1 \ --hash=sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f \ --hash=sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8 @@ -949,6 +922,7 @@ typing-extensions==4.12.2 \ # aws-sam-cli (setup.py) # aws-sam-translator # boto3-stubs + # cfn-lint # mypy-boto3-apigateway # mypy-boto3-cloudformation # mypy-boto3-ecr diff --git a/requirements/reproducible-mac.txt b/requirements/reproducible-mac.txt index eb55c70c74..9cc6f94714 100644 --- a/requirements/reproducible-mac.txt +++ b/requirements/reproducible-mac.txt @@ -16,10 +16,8 @@ attrs==23.2.0 \ --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via - # jschema-to-python # jsonschema # referencing - # sarif-om aws-lambda-builders==1.50.0 \ --hash=sha256:40a613ecb19fbf0b64a47bae14bd252ea5da32ea71fde9808d596e2dbc011baf \ --hash=sha256:ad95ed55359c399872f5825582896500dfc1c5564eccf2a6ab8d0e9f6c1ae385 @@ -134,9 +132,9 @@ cffi==1.16.0 \ --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 # via cryptography -cfn-lint==0.87.7 \ - --hash=sha256:85f6b7f32cf155a74d670d53f86b39f99cfc282b02158d98fdab9fc1dba0809e \ - --hash=sha256:ac6ac86dde1ba3d0fb0e217a4d329239a98f00af7862e4fa5ace6c416c4e056c +cfn-lint==1.9.2 \ + --hash=sha256:6920bd71875e76b36f244bd2ac70975a843e4e859a390625c60d16eaa0e3f3f3 \ + --hash=sha256:cc540882762e1545c37a08b23849f13f2034acfe5ae46079a8815c740bbdcfa2 # via aws-sam-cli (setup.py) chardet==5.2.0 \ --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ @@ -326,18 +324,10 @@ jmespath==1.0.1 \ # aws-sam-cli (setup.py) # boto3 # botocore -jschema-to-python==1.2.3 \ - --hash=sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91 \ - --hash=sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05 - # via cfn-lint jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via cfn-lint -jsonpickle==3.2.2 \ - --hash=sha256:87cd82d237fd72c5a34970e7222dddc0accc13fddf49af84111887ed9a9445aa \ - --hash=sha256:d425fd2b8afe9f5d7d57205153403fbf897782204437882a477e8eed60930f8c - # via jschema-to-python jsonpointer==3.0.0 \ --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef @@ -348,15 +338,10 @@ jsonschema==4.23.0 \ # via # aws-sam-cli (setup.py) # aws-sam-translator - # cfn-lint jsonschema-specifications==2023.12.1 \ --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c # via jsonschema -junit-xml==1.9 \ - --hash=sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f \ - --hash=sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732 - # via cfn-lint markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb @@ -493,12 +478,6 @@ networkx==3.1 \ --hash=sha256:4f33f68cb2afcf86f28a45f43efc27a9386b535d567d2127f8f61d51dec58d36 \ --hash=sha256:de346335408f84de0eada6ff9fafafff9bcda11f0a0dfaa931133debb146ab61 # via cfn-lint -pbr==6.0.0 \ - --hash=sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda \ - --hash=sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9 - # via - # jschema-to-python - # sarif-om pkgutil-resolve-name==1.3.10 \ --hash=sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174 \ --hash=sha256:ca27cc078d25c5ad71a9de0a7a330146c4e014c2462d9af19c6b828280649c5e @@ -940,16 +919,10 @@ s3transfer==0.10.2 \ --hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \ --hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69 # via boto3 -sarif-om==1.0.4 \ - --hash=sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911 \ - --hash=sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98 - # via cfn-lint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via - # junit-xml - # python-dateutil + # via python-dateutil sympy==1.13.1 \ --hash=sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f \ --hash=sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8 @@ -983,6 +956,7 @@ typing-extensions==4.12.2 \ # aws-sam-translator # boto3-stubs # botocore-stubs + # cfn-lint # mypy-boto3-apigateway # mypy-boto3-cloudformation # mypy-boto3-ecr diff --git a/requirements/reproducible-win.txt b/requirements/reproducible-win.txt index 0f857a3acd..3ef106398a 100644 --- a/requirements/reproducible-win.txt +++ b/requirements/reproducible-win.txt @@ -16,10 +16,8 @@ attrs==23.2.0 \ --hash=sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30 \ --hash=sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1 # via - # jschema-to-python # jsonschema # referencing - # sarif-om aws-lambda-builders==1.50.0 \ --hash=sha256:40a613ecb19fbf0b64a47bae14bd252ea5da32ea71fde9808d596e2dbc011baf \ --hash=sha256:ad95ed55359c399872f5825582896500dfc1c5564eccf2a6ab8d0e9f6c1ae385 @@ -116,9 +114,9 @@ cffi==1.16.0 \ --hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \ --hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357 # via cryptography -cfn-lint==0.87.7 \ - --hash=sha256:85f6b7f32cf155a74d670d53f86b39f99cfc282b02158d98fdab9fc1dba0809e \ - --hash=sha256:ac6ac86dde1ba3d0fb0e217a4d329239a98f00af7862e4fa5ace6c416c4e056c +cfn-lint==1.9.2 \ + --hash=sha256:6920bd71875e76b36f244bd2ac70975a843e4e859a390625c60d16eaa0e3f3f3 \ + --hash=sha256:cc540882762e1545c37a08b23849f13f2034acfe5ae46079a8815c740bbdcfa2 # via aws-sam-cli (setup.py) chardet==5.2.0 \ --hash=sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7 \ @@ -302,18 +300,10 @@ jmespath==1.0.1 \ # aws-sam-cli (setup.py) # boto3 # botocore -jschema-to-python==1.2.3 \ - --hash=sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91 \ - --hash=sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05 - # via cfn-lint jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via cfn-lint -jsonpickle==3.2.2 \ - --hash=sha256:87cd82d237fd72c5a34970e7222dddc0accc13fddf49af84111887ed9a9445aa \ - --hash=sha256:d425fd2b8afe9f5d7d57205153403fbf897782204437882a477e8eed60930f8c - # via jschema-to-python jsonpointer==3.0.0 \ --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef @@ -324,15 +314,10 @@ jsonschema==4.23.0 \ # via # aws-sam-cli (setup.py) # aws-sam-translator - # cfn-lint jsonschema-specifications==2023.12.1 \ --hash=sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc \ --hash=sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c # via jsonschema -junit-xml==1.9 \ - --hash=sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f \ - --hash=sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732 - # via cfn-lint markdown-it-py==3.0.0 \ --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb @@ -469,12 +454,6 @@ networkx==3.3 \ --hash=sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9 \ --hash=sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2 # via cfn-lint -pbr==6.0.0 \ - --hash=sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda \ - --hash=sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9 - # via - # jschema-to-python - # sarif-om pycparser==2.22 \ --hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \ --hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc @@ -928,16 +907,10 @@ s3transfer==0.10.2 \ --hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \ --hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69 # via boto3 -sarif-om==1.0.4 \ - --hash=sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911 \ - --hash=sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98 - # via cfn-lint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 - # via - # junit-xml - # python-dateutil + # via python-dateutil sympy==1.13.1 \ --hash=sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f \ --hash=sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8 @@ -969,6 +942,7 @@ typing-extensions==4.12.2 \ # aws-sam-cli (setup.py) # aws-sam-translator # boto3-stubs + # cfn-lint # mypy-boto3-apigateway # mypy-boto3-cloudformation # mypy-boto3-ecr diff --git a/samcli/commands/validate/validate.py b/samcli/commands/validate/validate.py index db0a130b24..cf317b8bfe 100644 --- a/samcli/commands/validate/validate.py +++ b/samcli/commands/validate/validate.py @@ -2,7 +2,9 @@ CLI Command for Validating a SAM Template """ +import logging import os +from dataclasses import dataclass import boto3 import click @@ -16,7 +18,7 @@ from samcli.commands._utils.cdk_support_decorators import unsupported_command_cdk from samcli.commands._utils.command_exception_handler import command_exception_handler from samcli.commands._utils.options import template_option_without_build -from samcli.commands.exceptions import LinterRuleMatchedException +from samcli.commands.exceptions import LinterRuleMatchedException, UserException from samcli.commands.validate.core.command import ValidateCommand from samcli.lib.telemetry.event import EventTracker from samcli.lib.telemetry.metric import track_command @@ -26,6 +28,14 @@ Verify and Lint an AWS SAM Template being valid. """ +CNT_LINT_LOGGER_NAME = "cfnlint" + + +@dataclass +class SamTemplate: + serialized: str + deserialized: dict + @click.command( "validate", @@ -71,14 +81,14 @@ def do_cli(ctx, template, lint): from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException from samcli.lib.translate.sam_template_validator import SamTemplateValidator + sam_template = _read_sam_file(template) + if lint: - _lint(ctx, template) + _lint(ctx, sam_template.serialized, template) else: - sam_template = _read_sam_file(template) - iam_client = boto3.client("iam") validator = SamTemplateValidator( - sam_template, ManagedPolicyLoader(iam_client), profile=ctx.profile, region=ctx.region + sam_template.deserialized, ManagedPolicyLoader(iam_client), profile=ctx.profile, region=ctx.region ) try: @@ -103,7 +113,7 @@ def do_cli(ctx, template, lint): ) -def _read_sam_file(template): +def _read_sam_file(template) -> SamTemplate: """ Reads the file (json and yaml supported) provided and returns the dictionary representation of the file. @@ -120,12 +130,13 @@ def _read_sam_file(template): raise SamTemplateNotFoundException("Template at {} is not found".format(template)) with click.open_file(template, "r", encoding="utf-8") as sam_file: - sam_template = yaml_parse(sam_file.read()) + template_string = sam_file.read() + sam_template = yaml_parse(template_string) - return sam_template + return SamTemplate(serialized=template_string, deserialized=sam_template) -def _lint(ctx: Context, template: str) -> None: +def _lint(ctx: Context, template: str, template_path: str) -> None: """ Parses provided SAM template and maps errors from CloudFormation template back to SAM template. @@ -139,52 +150,40 @@ def _lint(ctx: Context, template: str) -> None: ctx Click context object template - Path to the template file - + Contents of sam template as a string + template_path + Path to the sam template """ - import logging - - import cfnlint.core # type: ignore - - from samcli.commands.exceptions import UserException + from cfnlint.api import ManualArgs, lint + from cfnlint.runner import InvalidRegionException - cfn_lint_logger = logging.getLogger("cfnlint") + cfn_lint_logger = logging.getLogger(CNT_LINT_LOGGER_NAME) cfn_lint_logger.propagate = False + EventTracker.track_event("UsedFeature", "CFNLint") - try: - lint_args = [template] - if ctx.debug: - lint_args.append("--debug") - if ctx.region: - lint_args.append("--region") - lint_args.append(ctx.region) - - (args, filenames, formatter) = cfnlint.core.get_args_filenames(lint_args) - cfn_lint_logger.setLevel(logging.WARNING) - matches = list(cfnlint.core.get_matches(filenames, args)) - if not matches: - click.secho("{} is a valid SAM Template".format(template), fg="green") - return - - rules = cfnlint.core.get_used_rules() - matches_output = formatter.print_matches(matches, rules, filenames) - - if matches_output: - click.secho(matches_output) - - raise LinterRuleMatchedException( - "Linting failed. At least one linting rule was matched to the provided template." - ) + linter_config = {} + if ctx.region: + linter_config["regions"] = [ctx.region] + if ctx.debug: + cfn_lint_logger.propagate = True + cfn_lint_logger.setLevel(logging.DEBUG) - except cfnlint.core.InvalidRegionException as e: - raise UserException( - "AWS Region was not found. Please configure your region through the --region option", - wrapped_from=e.__class__.__name__, - ) from e - except cfnlint.core.CfnLintExitException as lint_error: + config = ManualArgs(**linter_config) + + try: + matches = lint(template, config=config) + except InvalidRegionException as ex: raise UserException( - lint_error, - wrapped_from=lint_error.__class__.__name__, - ) from lint_error + f"AWS Region was not found. Please configure your region through the --region option.\n{ex}", + wrapped_from=ex.__class__.__name__, + ) from ex + + if not matches: + click.secho("{} is a valid SAM Template".format(template_path), fg="green") + return + + click.secho(matches) + + raise LinterRuleMatchedException("Linting failed. At least one linting rule was matched to the provided template.") diff --git a/tests/integration/validate/test_validate_command.py b/tests/integration/validate/test_validate_command.py index b74ded8f56..50624a2965 100644 --- a/tests/integration/validate/test_validate_command.py +++ b/tests/integration/validate/test_validate_command.py @@ -9,7 +9,7 @@ from enum import Enum, auto from pathlib import Path from typing import List, Optional -from unittest import TestCase +from unittest import TestCase, skip from unittest.case import skipIf from parameterized import parameterized @@ -155,7 +155,11 @@ def test_lint_deprecated_runtimes(self, runtime): output = command_result.stdout.decode("utf-8") self.assertEqual(command_result.process.returncode, 1) - self.assertRegex(output, f"W2531 Runtime \\({runtime}\\)") + self.assertRegex( + output, + f"\\[\\[W2531: Check if EOL Lambda Function Runtimes are used] " + f"\\(Runtime \\'{runtime}'\\ was deprecated on.*", + ) def test_lint_supported_runtimes(self): template = { @@ -223,7 +227,10 @@ def test_lint_error_invalid_region(self): command_result = run_command(self.command_list(lint=True, region="us-north-5", template_file=template_path)) output = command_result.stderr.decode("utf-8") - error_message = f"Error: AWS Region was not found. Please configure your region through the --region option" + error_message = ( + f"Error: AWS Region was not found. Please configure your region through the --region option.\n" + f"Regions ['us-north-5'] are unsupported. Supported regions are" + ) self.assertIn(error_message, output) @@ -237,10 +244,10 @@ def test_lint_invalid_template(self): output = output.replace("\r", "") warning_message = ( - 'E0000 Duplicate found "HelloWorldFunction" (line 5)\n' - f'{os.path.join(test_data_path, "templateError.yaml")}:5:3\n\n' - 'E0000 Duplicate found "HelloWorldFunction" (line 12)\n' - f'{os.path.join(test_data_path, "templateError.yaml")}:12:3\n\n' + "[[E0000: Parsing error found when parsing the template] " + '(Duplicate found "HelloWorldFunction" (line 5)) matched 5, ' + "[E0000: Parsing error found when parsing the template] " + '(Duplicate found "HelloWorldFunction" (line 12)) matched 12]\n' ) self.assertIn(warning_message, output) diff --git a/tests/unit/commands/validate/test_cli.py b/tests/unit/commands/validate/test_cli.py index c19f5d0377..f77a4711e9 100644 --- a/tests/unit/commands/validate/test_cli.py +++ b/tests/unit/commands/validate/test_cli.py @@ -4,12 +4,10 @@ from botocore.exceptions import NoCredentialsError -from cfnlint.core import CfnLintExitException, InvalidRegionException # type: ignore - from samcli.commands.exceptions import UserException, LinterRuleMatchedException from samcli.commands.local.cli_common.user_exceptions import SamTemplateNotFoundException, InvalidSamTemplateException from samcli.commands.validate.lib.exceptions import InvalidSamDocumentException -from samcli.commands.validate.validate import do_cli, _read_sam_file, _lint +from samcli.commands.validate.validate import do_cli, _read_sam_file, _lint, SamTemplate ctx_mock = namedtuple("ctx_mock", ["profile", "region"]) ctx_lint_mock = namedtuple("ctx_lint_mock", ["debug", "region"]) @@ -38,19 +36,19 @@ def test_file_parsed(self, path_exists_patch, click_patch, yaml_parse_patch): actual_template = _read_sam_file(template_path) - self.assertEqual(actual_template, {"a": "b"}) + self.assertEqual(actual_template.deserialized, {"a": "b"}) @patch("samcli.lib.translate.sam_template_validator.SamTemplateValidator") @patch("samcli.commands.validate.validate.click") @patch("samcli.commands.validate.validate._read_sam_file") @patch("boto3.client") - def test_template_fails_validation(self, patched_boto, read_sam_file_patch, click_patch, template_valiadator): + def test_template_fails_validation(self, patched_boto, read_sam_file_patch, click_patch, template_validator): template_path = "path_to_template" - read_sam_file_patch.return_value = {"a": "b"} + read_sam_file_patch.return_value = SamTemplate(deserialized={"a": "b"}, serialized="") get_translated_template_if_valid_mock = Mock() get_translated_template_if_valid_mock.get_translated_template_if_valid.side_effect = InvalidSamDocumentException - template_valiadator.return_value = get_translated_template_if_valid_mock + template_validator.return_value = get_translated_template_if_valid_mock with self.assertRaises(InvalidSamTemplateException): do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) @@ -59,13 +57,13 @@ def test_template_fails_validation(self, patched_boto, read_sam_file_patch, clic @patch("samcli.commands.validate.validate.click") @patch("samcli.commands.validate.validate._read_sam_file") @patch("boto3.client") - def test_no_credentials_provided(self, patched_boto, read_sam_file_patch, click_patch, template_valiadator): + def test_no_credentials_provided(self, patched_boto, read_sam_file_patch, click_patch, template_validator): template_path = "path_to_template" - read_sam_file_patch.return_value = {"a": "b"} + read_sam_file_patch.return_value = SamTemplate(deserialized={"a": "b"}, serialized="") get_translated_template_if_valid_mock = Mock() get_translated_template_if_valid_mock.get_translated_template_if_valid.side_effect = NoCredentialsError - template_valiadator.return_value = get_translated_template_if_valid_mock + template_validator.return_value = get_translated_template_if_valid_mock with self.assertRaises(UserException): do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) @@ -74,69 +72,55 @@ def test_no_credentials_provided(self, patched_boto, read_sam_file_patch, click_ @patch("samcli.commands.validate.validate.click") @patch("samcli.commands.validate.validate._read_sam_file") @patch("boto3.client") - def test_template_passes_validation(self, patched_boto, read_sam_file_patch, click_patch, template_valiadator): + def test_template_passes_validation(self, patched_boto, read_sam_file_patch, click_patch, template_validator): template_path = "path_to_template" - read_sam_file_patch.return_value = {"a": "b"} + read_sam_file_patch.return_value = SamTemplate(deserialized={"a": "b"}, serialized="") get_translated_template_if_valid_mock = Mock() get_translated_template_if_valid_mock.get_translated_template_if_valid.return_value = True - template_valiadator.return_value = get_translated_template_if_valid_mock + template_validator.return_value = get_translated_template_if_valid_mock do_cli(ctx=ctx_mock(profile="profile", region="region"), template=template_path, lint=False) + @patch("samcli.commands.validate.validate._read_sam_file") @patch("samcli.commands.validate.validate.click") @patch("samcli.commands.validate.validate._lint") - def test_lint_template_passes(self, click_patch, lint_patch): + def test_lint_template_passes(self, click_patch, lint_patch, read_sam_file_patch): template_path = "path_to_template" + read_sam_file_patch.return_value = SamTemplate(serialized="{}", deserialized={}) lint_patch.return_value = True do_cli(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path, lint=True) - @patch("cfnlint.core.get_args_filenames") - @patch("cfnlint.core.get_matches") - @patch("samcli.commands.validate.validate.click") - def test_lint_invalid_region_argument_fails(self, click_patch, matches_patch, args_patch): - template_path = "path_to_template" - - args_patch.return_value = ("A", "B", "C") - - matches_patch.side_effect = InvalidRegionException - - with self.assertRaises(UserException): - _lint(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path) - - @patch("cfnlint.core.get_args_filenames") - @patch("cfnlint.core.get_matches") + @patch("cfnlint.api.lint") @patch("samcli.commands.validate.validate.click") - def test_lint_exception_fails(self, click_patch, matches_patch, args_patch): - template_path = "path_to_template" - - args_patch.return_value = ("A", "B", "C") - - matches_patch.side_effect = CfnLintExitException - - with self.assertRaises(UserException): - _lint(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path) - - @patch("samcli.commands.validate.validate.click") - def test_lint_event_recorded(self, click_patch): + def test_lint_event_recorded(self, click_patch, lint_patch): template_path = "path_to_template" + template_contents = "{}" with patch("samcli.lib.telemetry.event.EventTracker.track_event") as track_patch: with self.assertRaises(LinterRuleMatchedException): - _lint(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path) + _lint( + ctx=ctx_lint_mock(debug=False, region="region"), + template=template_contents, + template_path=template_path, + ) track_patch.assert_called_with("UsedFeature", "CFNLint") - @patch("cfnlint.core.get_args_filenames") - @patch("cfnlint.core.get_matches") + @patch("cfnlint.api.lint") @patch("samcli.commands.validate.validate.click") - def test_linter_raises_exception_if_matches_found(self, click_patch, matches_patch, args_patch): + def test_linter_raises_exception_if_matches_found(self, click_patch, lint_patch): template_path = "path_to_template" - args_patch.return_value = ("A", "B", Mock()) - matches_patch.return_value = ["Failed rule A", "Failed rule B"] + template_contents = "{}" + + lint_patch.return_value = ["Failed rule A", "Failed rule B"] + with self.assertRaises(LinterRuleMatchedException) as ex: - _lint(ctx=ctx_lint_mock(debug=False, region="region"), template=template_path) + _lint( + ctx=ctx_lint_mock(debug=False, region="region"), template=template_contents, template_path=template_path + ) + self.assertEqual( ex.exception.message, "Linting failed. At least one linting rule was matched to the provided template." )