Repository: aws-cloudformation/cfn-lint

Issue:
Linter doesn't handle templated code correctly
*cfn-lint version: cfn-lint 0.4.1

*Description of issue.*

The linter doesn't handle templated code [see reference](https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html) where `Code: ./sample.zip` is accepted as a valid template but errors in the linter.

* Template linting issues:

![image](https://user-images.githubusercontent.com/379176/43136521-5831750a-8f8c-11e8-8ad5-67f6d4268cc4.png)

  * Please provide a CloudFormation sample that generated the issue.

```
  SampleLambda:
    Type: "AWS::Lambda::Function"
    Properties:
      Role:
        Fn::ImportValue:
          !Sub "${BuildSystemIAMStackName}-SampleLambdaRoleArn"

      Runtime: python2.7
      Handler: lambda_sample.handler
      Code: ./sample.zip
      Timeout: 300
```

  * If present, please add links to the (official) documentation for clarification.
Above

What is the guidance on template files being interpreted as CFN yaml files? Should this be picked up by the linter?


Discussion:
Yea this is a good one.  We probably shouldn't be giving you an error on this.  The bummer (for us) here is that we are using the CloudFormation resource spec which requires Code to be an object.   Since there is a separate aws cli command that can take that template, do some work on your behalf, and then output a "valid" template we are going to have to adjust for this case.  @cmmeyer as much as I don't like exceptions I'm guessing we should make an exception for this one.  Thoughts?

I would have to check more but I think we are good with all the Transform ones as @fatbasstard has already done some great work in that category.
Any progress on this? We'd like to enforce linting in our Codebuild pipeline and this is a pain to work with

Resolving patch:
diff --git a/src/cfnlint/rules/resources/properties/Properties.py b/src/cfnlint/rules/resources/properties/Properties.py
--- a/src/cfnlint/rules/resources/properties/Properties.py
+++ b/src/cfnlint/rules/resources/properties/Properties.py
@@ -14,6 +14,7 @@
   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
+import six
 from cfnlint import CloudFormationLintRule
 from cfnlint import RuleMatch
 import cfnlint.helpers
@@ -123,6 +124,24 @@ def check_list_for_condition(self, text, prop, parenttype, resourcename, propspe
 
         return matches
 
+    def check_exceptions(self, parenttype, proptype, text):
+        """
+            Checks for exceptions to the spec
+            - Start with handling exceptions for templated code.
+        """
+        templated_exceptions = {
+            'AWS::ApiGateway::RestApi': ['BodyS3Location'],
+            'AWS::Lambda::Function': ['Code'],
+            'AWS::ElasticBeanstalk::ApplicationVersion': ['SourceBundle'],
+        }
+
+        exceptions = templated_exceptions.get(parenttype, [])
+        if proptype in exceptions:
+            if isinstance(text, six.string_types):
+                return True
+
+        return False
+
     def propertycheck(self, text, proptype, parenttype, resourcename, path, root):
         """Check individual properties"""
 
@@ -149,8 +168,9 @@ def propertycheck(self, text, proptype, parenttype, resourcename, path, root):
         if text == 'AWS::NoValue':
             return matches
         if not isinstance(text, dict):
-            message = 'Expecting an object at %s' % ('/'.join(map(str, path)))
-            matches.append(RuleMatch(path, message))
+            if not self.check_exceptions(parenttype, proptype, text):
+                message = 'Expecting an object at %s' % ('/'.join(map(str, path)))
+                matches.append(RuleMatch(path, message))
             return matches
 
         for prop in text:
diff --git a/src/cfnlint/rules/resources/properties/PropertiesTemplated.py b/src/cfnlint/rules/resources/properties/PropertiesTemplated.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/rules/resources/properties/PropertiesTemplated.py
@@ -0,0 +1,65 @@
+"""
+  Copyright 2018 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.
+"""
+import six
+from cfnlint import CloudFormationLintRule
+from cfnlint import RuleMatch
+
+
+class PropertiesTemplated(CloudFormationLintRule):
+    """Check Base Resource Configuration"""
+    id = 'W3002'
+    shortdesc = 'Warn when properties are configured to only work with the package command'
+    description = 'Some properties can be configured to only work with the CloudFormation' \
+                  'package command. Warn when this is the case so user is aware.'
+    source_url = 'https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html'
+    tags = ['resources']
+
+    def __init__(self):
+        self.resource_property_types.extend([
+            'AWS::ApiGateway::RestApi',
+            'AWS::Lambda::Function',
+            'AWS::ElasticBeanstalk::ApplicationVersion',
+        ])
+
+    def check_value(self, value, path):
+        """ Check the value """
+        matches = list()
+        if isinstance(value, six.string_types):
+            message = 'This code may only work with `package` cli command as the property (%s) is a string' % ('/'.join(map(str, path)))
+            matches.append(RuleMatch(path, message))
+
+        return matches
+
+    def match_resource_properties(self, properties, resourcetype, path, cfn):
+        """Check CloudFormation Properties"""
+        matches = list()
+
+        templated_exceptions = {
+            'AWS::ApiGateway::RestApi': ['BodyS3Location'],
+            'AWS::Lambda::Function': ['Code'],
+            'AWS::ElasticBeanstalk::ApplicationVersion': ['SourceBundle'],
+        }
+
+        for key in templated_exceptions.get(resourcetype, []):
+            matches.extend(
+                cfn.check_value(
+                    obj=properties, key=key,
+                    path=path[:],
+                    check_value=self.check_value
+                ))
+
+        return matches


Test patch:
diff --git a/test/fixtures/templates/bad/resources/properties/templated_code.yaml b/test/fixtures/templates/bad/resources/properties/templated_code.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/resources/properties/templated_code.yaml
@@ -0,0 +1,13 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Resources:
+  SampleLambda:
+    Type: AWS::Lambda::Function
+    Properties:
+      Role: role:arn
+      Runtime: python2.7
+      Handler: lambda_sample.handler
+      Code: ./sample.zip
+      Timeout: 300
+      # Still finds errors when objects aren't templatable
+      DeadLetterConfig: ./test.zip
diff --git a/test/fixtures/templates/good/resources/properties/templated_code.yaml b/test/fixtures/templates/good/resources/properties/templated_code.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/good/resources/properties/templated_code.yaml
@@ -0,0 +1,12 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Resources:
+  SampleLambda:
+    Type: AWS::Lambda::Function
+    Properties:
+      Role: role:arn
+      Runtime: python2.7
+      Handler: lambda_sample.handler
+      # Templatable property
+      Code: ./sample.zip
+      Timeout: 300
diff --git a/test/rules/resources/properties/test_properties.py b/test/rules/resources/properties/test_properties.py
--- a/test/rules/resources/properties/test_properties.py
+++ b/test/rules/resources/properties/test_properties.py
@@ -25,7 +25,8 @@ def setUp(self):
         super(TestResourceProperties, self).setUp()
         self.collection.register(Properties())
         self.success_templates = [
-            'fixtures/templates/good/resource_properties.yaml'
+            'fixtures/templates/good/resource_properties.yaml',
+            'fixtures/templates/good/resources/properties/templated_code.yaml',
         ]
 
     def test_file_positive(self):
diff --git a/test/rules/resources/properties/test_properties_templated.py b/test/rules/resources/properties/test_properties_templated.py
new file mode 100644
--- /dev/null
+++ b/test/rules/resources/properties/test_properties_templated.py
@@ -0,0 +1,34 @@
+"""
+  Copyright 2018 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.rules.resources.properties.PropertiesTemplated import PropertiesTemplated  # pylint: disable=E0401
+from ... import BaseRuleTestCase
+
+
+class TestPropertiesTemplated(BaseRuleTestCase):
+    """Test Resource Properties"""
+    def setUp(self):
+        """Setup"""
+        super(TestPropertiesTemplated, self).setUp()
+        self.collection.register(PropertiesTemplated())
+
+    def test_file_positive(self):
+        """Test Positive"""
+        self.helper_file_positive()
+
+    def test_file_negative_4(self):
+        """Failure test"""
+        self.helper_file_negative('fixtures/templates/bad/resources/properties/templated_code.yaml', 1)