Repository: getmoto/moto
Pull request #7784 (2024-06-20 10:24:52)

Fix cfn-lint breaking changes
[cfn-lint 1.3.0](https://pypi.org/project/cfn-lint/1.3.0) was released on 2024-06-19.

Currently Moto pins it as `cfn-lint>=0.40.0` which causes the latest version to be picked up and integration to break.

This PR fixes the breaking changes and updates its usage.

bblommers: Hi @viren-nadkarni, thanks for the PR!

V1 only came out recently, so I think we want to support both v0 and v1 for now.
I'm sure there are plenty of people who want to continue using v0 for now, and I don't want to force anyone to upgrade.

I'd be happy to make the necessary changes to support both versions, we already do that for a few other dependencies (see moto/core/versions py) - I just don't know when I'll have time for that.

------

Related announcement from AWS, for future reference:
https://aws.amazon.com/blogs/devops/aws-cloudformation-linter-v1/

Patch:
diff --git a/moto/cloudformation/utils.py b/moto/cloudformation/utils.py
index 3a18868f9..af47a4068 100644
--- a/moto/cloudformation/utils.py
+++ b/moto/cloudformation/utils.py
@@ -59,35 +59,58 @@ def yaml_tag_constructor(loader: Any, tag: Any, node: Any) -> Any:
 
 def validate_template_cfn_lint(template: str) -> List[Any]:
     # Importing cfnlint adds a significant overhead, so we keep it local
-    from cfnlint import core, decode
 
-    # Save the template to a temporary file -- cfn-lint requires a file
-    filename = "file.tmp"
-    with open(filename, "w") as file:
-        file.write(template)
-    abs_filename = os.path.abspath(filename)
-
-    # decode handles both yaml and json
     try:
-        template, matches = decode.decode(abs_filename, False)
-    except TypeError:
-        # As of cfn-lint 0.39.0, the second argument (ignore_bad_template) was dropped
-        # https://github.com/aws-cloudformation/cfn-python-lint/pull/1580
-        template, matches = decode.decode(abs_filename)
+        # Compatibility for cfn-lint 0.x
+        # Fail fast with `cfnlint.core.configure_logging` which is removed in cfn-lint 1.x
+
+        from cfnlint.core import configure_logging, get_rules, run_checks
+        from cfnlint.decode import decode
+
+        # Save the template to a temporary file -- cfn-lint requires a file
+        filename = "file.tmp"
+        with open(filename, "w") as file:
+            file.write(template)
+        abs_filename = os.path.abspath(filename)
+
+        # decode handles both yaml and json
+        try:
+            template, matches = decode(abs_filename, False)
+        except TypeError:
+            # As of cfn-lint 0.39.0, the second argument (ignore_bad_template) was dropped
+            # https://github.com/aws-cloudformation/cfn-python-lint/pull/1580
+            template, matches = decode(abs_filename)
+
+        # Set cfn-lint to info
+        configure_logging(None)
+
+        # Initialize the ruleset to be applied (no overrules, no excludes)
+        rules = get_rules([], [], [])
+
+        # Use us-east-1 region (spec file) for validation
+        regions = ["us-east-1"]
+
+        # Process all the rules and gather the errors
+        return run_checks(abs_filename, template, rules, regions)
+
+    except ImportError:
+        # Compatibility for cfn-lint 1.x
 
-    # Set cfn-lint to info
-    core.configure_logging(None)
+        from cfnlint.api import lint
+        from cfnlint.config import configure_logging
+        from cfnlint.core import get_rules
 
-    # Initialize the ruleset to be applied (no overrules, no excludes)
-    rules = core.get_rules([], [], [])
+        # Set cfn-lint to info
+        configure_logging(None, False)
 
-    # Use us-east-1 region (spec file) for validation
-    regions = ["us-east-1"]
+        # Initialize the ruleset to be applied (no overrules, no excludes)
+        rules = get_rules([], [], [])
 
-    # Process all the rules and gather the errors
-    matches = core.run_checks(abs_filename, template, rules, regions)
+        # Use us-east-1 region (spec file) for validation
+        regions = ["us-east-1"]
 
-    return matches
+        # Process all the rules and gather the errors
+        return lint(template, rules, regions)
 
 
 def get_stack_from_s3_url(template_url: str, account_id: str, partition: str) -> str:


Test patch:
diff --git a/tests/test_cloudformation/test_validate.py b/tests/test_cloudformation/test_validate.py
index ffdde144b..b9f009c2e 100644
--- a/tests/test_cloudformation/test_validate.py
+++ b/tests/test_cloudformation/test_validate.py
@@ -69,7 +69,10 @@ def test_boto3_json_invalid_missing_resource():
     err = exc.value.response["Error"]
     assert (
         err["Message"]
-        == "Stack with id Missing top level template section Resources does not exist"
+        in (
+            "Stack with id Missing top level template section Resources does not exist",  # cfn-lint 0.x
+            "Stack with id 'Resources' is a required property does not exist",  # cfn-lint 1.x
+        )
     )
 
 
@@ -124,5 +127,8 @@ def test_boto3_yaml_invalid_missing_resource():
     err = exc.value.response["Error"]
     assert (
         err["Message"]
-        == "Stack with id Missing top level template section Resources does not exist"
+        in (
+            "Stack with id Missing top level template section Resources does not exist",  # cfn-lint 0.x
+            "Stack with id 'Resources' is a required property does not exist",  # cfn-lint 1.x
+        )
     )