Repository: aws-cloudformation/cfn-lint

Issue:
Support resource metadata for ignoring checks
*cfn-lint version: any*

*Description of issue.*

This is a feature request for using the metadata section of resources. Declaring `ignore_checks` at the resource level would allow for ignoring a specific instance of a check while keeping that check active for the rest of a template.

Possibly related: #113



Discussion:
I think this is the first iteration to #113.  Let me look into what it will take to get this done.
Thanks! Happy to contribute where I can.

Resolving patch:
diff --git a/src/cfnlint/__init__.py b/src/cfnlint/__init__.py
--- a/src/cfnlint/__init__.py
+++ b/src/cfnlint/__init__.py
@@ -565,6 +565,22 @@ def get_valid_getatts(self):
 
         return results
 
+    def get_directives(self):
+        """ Get Directives"""
+        results = {}
+        for _, resource_values in self.template.get('Resources', {}).items():
+            ignore_rule_ids = resource_values.get('Metadata', {}).get('cfn-lint', {}).get('config', {}).get('ignore_checks', [])
+            for ignore_rule_id in ignore_rule_ids:
+                if ignore_rule_id not in results:
+                    results[ignore_rule_id] = []
+                location = self._loc(resource_values)
+                results[ignore_rule_id].append({
+                    'start': location[0],
+                    'end': location[2]
+                })
+
+        return results
+
     def _get_sub_resource_properties(self, keys, properties, path):
         """Used for recursive handling of properties in the keys"""
         LOGGER.debug('Get Sub Resource Properties from %s', keys)
@@ -1310,11 +1326,20 @@ def run(self):
                 self.rules.run(
                     self.filename, self.cfn))
 
-        # uniq the list of incidents
+        # uniq the list of incidents and filter out exceptions from the template
+        directives = self.cfn.get_directives()
         return_matches = []
-        for _, match in enumerate(matches):
+        for match in matches:
             if not any(match == u for u in return_matches):
-                return_matches.append(match)
+                if match.rule.id not in directives:
+                    return_matches.append(match)
+                else:
+                    exception = False
+                    for directive in directives.get(match.rule.id):
+                        if directive.get('start') <= match.linenumber <= directive.get('end'):
+                            exception = True
+                    if not exception:
+                        return_matches.append(match)
         return return_matches
 
 


Test patch:
diff --git a/test/fixtures/templates/bad/core/directives.yaml b/test/fixtures/templates/bad/core/directives.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/core/directives.yaml
@@ -0,0 +1,37 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+
+Resources:
+  myBucketPass:
+    Type: AWS::S3::Bucket
+    Metadata:
+      cfn-lint:
+        config:
+          ignore_checks:
+          - E3002
+    Properties:
+      BucketName1: String
+  myBucketFail:
+    Type: AWS::S3::Bucket
+    Properties:
+      BucketName1: String
+  myBucketFirstAndLastPass:
+    BadProperty: Test
+    Type: AWS::S3::Bucket
+    Metadata:
+      cfn-lint:
+        config:
+          ignore_checks:
+          - E3030
+          - E3001
+    Properties:
+      BadKey: Value  # other errors still show up
+      VersioningConfiguration:
+        Status: Enabled1
+  myBucketFirstAndLastFail:
+    BadProperty: Test
+    Type: AWS::S3::Bucket
+    Properties:
+      BadKey: Value  # other errors still show up
+      VersioningConfiguration:
+        Status: Enabled1
diff --git a/test/integration/test_directives.py b/test/integration/test_directives.py
new file mode 100644
--- /dev/null
+++ b/test/integration/test_directives.py
@@ -0,0 +1,44 @@
+"""
+  Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+from cfnlint import Template, RulesCollection, Runner  # pylint: disable=E0401
+from cfnlint.core import DEFAULT_RULESDIR  # pylint: disable=E0401
+import cfnlint.decode.cfn_yaml  # pylint: disable=E0401
+from testlib.testcase import BaseTestCase
+
+
+class TestDirectives(BaseTestCase):
+    """Test Directives """
+    def setUp(self):
+        """ SetUp template object"""
+        self.rules = RulesCollection(include_rules=['I'])
+        rulesdirs = [DEFAULT_RULESDIR]
+        for rulesdir in rulesdirs:
+            self.rules.extend(
+                RulesCollection.create_from_directory(rulesdir))
+
+    def test_templates(self):
+        """Test ignoring certain rules"""
+        filename = 'test/fixtures/templates/bad/core/directives.yaml'
+        failures = 5
+
+        template = cfnlint.decode.cfn_yaml.load(filename)
+        runner = Runner(self.rules, filename, template, ['us-east-1'])
+        matches = []
+        matches.extend(runner.transform())
+        if not matches:
+            matches.extend(runner.run())
+        assert len(matches) == failures, 'Expected {} failures, got {} on {}'.format(failures, len(matches), filename)
diff --git a/test/rules/__init__.py b/test/rules/__init__.py
--- a/test/rules/__init__.py
+++ b/test/rules/__init__.py
@@ -25,7 +25,7 @@ class BaseRuleTestCase(BaseTestCase):
 
     def setUp(self):
         """Setup"""
-        self.collection = RulesCollection(include_rules=['I'],include_experimental=True)
+        self.collection = RulesCollection(include_rules=['I'], include_experimental=True)
 
     def helper_file_positive(self):
         """Success test"""