Repository: aws-cloudformation/cfn-lint

Issue:
Environment Variables as Integer on AWS SAM template
*cfn-lint version: 0.28.4*

*Description of issue.*
When using Integer as Envrironment Variable for Serverless (AWS SAM), we get an error saying it should be String.

E3012 Property Resources/MyResource/Properties/Environment/Variables/MAIL_PORT should be of type String


Please provide as much information as possible:
* Template linting issues:
  * Please provide a CloudFormation sample that generated the issue.

```
Description: My Lambda Repository
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Environment:
      Variables:
        MAIL_PORT: 587

Resources:
  MyLambda:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: my-lambda
      CodeUri: .
      Handler: pipeline.php
      Timeout: 300
      MemorySize: 512
      Environment:
        Variables:
          TENANT_QUEUE: !Ref TenantQueue
      Role: !GetAtt LambdaExecutionRole.Arn
      Layers: [!Sub "arn:aws:lambda:${AWS::Region}:209497400698:layer:php-73:18"]
      Runtime: provided
      VpcConfig:
        SecurityGroupIds: [!ImportValue AllowAllAddressesContainerSecurityGroup]
        SubnetIds: !Split [',', !ImportValue PrivateSubnets]
```

  * If present, please add links to the (official) documentation for clarification.
  * Validate if the issue still exists with the latest version of `cfn-lint` and/or the latest Spec files
* Feature request:
  * Please provide argumentation about the missing feature. Context is key!


Cfn-lint uses the [CloudFormation Resource Specifications](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-resource-specification.html) as the base to do validation. These files are included as part of the application version. Please update to the latest version of `cfn-lint` or update the spec files manually (`cfn-lint -u`)



Discussion:
Translated Template.
```
{
  "Description": "My Lambda Repository",
  "Resources": {
    "MyLambda": {
      "Properties": {
        "Code": {
          "S3Bucket": "bucket",
          "S3Key": "value"
        },
        "Environment": {
          "Variables": {
            "MAIL_PORT": 587,
            "TENANT_QUEUE": {
              "Ref": "TenantQueue"
            }
          }
        },
        "FunctionName": "my-lambda",
        "Handler": "pipeline.php",
        "Layers": [
          {
            "Fn::Sub": "arn:aws:lambda:${AWS::Region}:209497400698:layer:php-73:18"
          }
        ],
        "MemorySize": 512,
        "Role": {
          "Fn::GetAtt": [
            "LambdaExecutionRole",
            "Arn"
          ]
        },
        "Runtime": "provided",
        "Tags": [
          {
            "Key": "lambda:createdBy",
            "Value": "SAM"
          }
        ],
        "Timeout": 300,
        "VpcConfig": {
          "SecurityGroupIds": [
            {
              "Fn::ImportValue": "AllowAllAddressesContainerSecurityGroup"
            }
          ],
          "SubnetIds": {
            "Fn::Split": [
              ",",
              {
                "Fn::ImportValue": "PrivateSubnets"
              }
            ]
          }
        }
      },
      "Type": "AWS::Lambda::Function"
    }
  }
}
```

As the spec has it.
```
"Variables": {
          "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-environment.html#cfn-lambda-function-environment-variables",
          "DuplicatesAllowed": false,
          "PrimitiveItemType": "String",
          "Required": false,
          "Type": "Map",
          "UpdateType": "Mutable"
        }
```

This isn't really a spec bug.  By default we do strict checking of the types based on the spec. 
Two options:
1. We make an exception like we did for `AWS::CloudFormation::Stack` `Parameters`.
2. You turn off strict checking in E3012.  Documentation is here https://github.com/aws-cloudformation/cfn-python-lint#configure-rules
I was hoping this could work similarly to how ECS Task Definition environment variable does: I can have number values there.

Resolving patch:
diff --git a/src/cfnlint/rules/resources/properties/ValuePrimitiveType.py b/src/cfnlint/rules/resources/properties/ValuePrimitiveType.py
--- a/src/cfnlint/rules/resources/properties/ValuePrimitiveType.py
+++ b/src/cfnlint/rules/resources/properties/ValuePrimitiveType.py
@@ -18,6 +18,15 @@ class ValuePrimitiveType(CloudFormationLintRule):
     source_url = 'https://github.com/aws-cloudformation/cfn-python-lint/blob/master/docs/cfn-resource-specification.md#valueprimitivetype'
     tags = ['resources']
 
+    strict_exceptions = {
+        'AWS::CloudFormation::Stack': [
+            'Parameters'
+        ],
+        'AWS::Lambda::Function.Environment': [
+            'Variables'
+        ]
+    }
+
     def __init__(self):
         """Init"""
         super(ValuePrimitiveType, self).__init__()
@@ -41,10 +50,10 @@ def initialize(self, cfn):
         for property_spec in self.property_specs:
             self.resource_sub_property_types.append(property_spec)
 
-    def _value_check(self, value, path, item_type, extra_args):
+    def _value_check(self, value, path, item_type, strict_check, extra_args):
         """ Checks non strict """
         matches = []
-        if not self.config['strict']:
+        if not strict_check:
             try:
                 if item_type in ['String']:
                     str(value)
@@ -81,7 +90,7 @@ def _value_check(self, value, path, item_type, extra_args):
 
         return matches
 
-    def check_primitive_type(self, value, item_type, path):
+    def check_primitive_type(self, value, item_type, path, strict_check):
         """Chec item type"""
         matches = []
         if isinstance(value, dict) and item_type == 'Json':
@@ -89,20 +98,20 @@ def check_primitive_type(self, value, item_type, path):
         if item_type in ['String']:
             if not isinstance(value, (six.string_types)):
                 extra_args = {'actual_type': type(value).__name__, 'expected_type': str.__name__}
-                matches.extend(self._value_check(value, path, item_type, extra_args))
+                matches.extend(self._value_check(value, path, item_type, strict_check, extra_args))
         elif item_type in ['Boolean']:
             if not isinstance(value, (bool)):
                 extra_args = {'actual_type': type(value).__name__, 'expected_type': bool.__name__}
-                matches.extend(self._value_check(value, path, item_type, extra_args))
+                matches.extend(self._value_check(value, path, item_type, strict_check, extra_args))
         elif item_type in ['Double']:
             if not isinstance(value, (float, int)):
                 extra_args = {'actual_type': type(value).__name__, 'expected_type': [
                     float.__name__, int.__name__]}
-                matches.extend(self._value_check(value, path, item_type, extra_args))
+                matches.extend(self._value_check(value, path, item_type, strict_check, extra_args))
         elif item_type in ['Integer']:
             if not isinstance(value, (int)):
                 extra_args = {'actual_type': type(value).__name__, 'expected_type': int.__name__}
-                matches.extend(self._value_check(value, path, item_type, extra_args))
+                matches.extend(self._value_check(value, path, item_type, strict_check, extra_args))
         elif item_type in ['Long']:
             if sys.version_info < (3,):
                 integer_types = (int, long,)  # pylint: disable=undefined-variable
@@ -111,7 +120,7 @@ def check_primitive_type(self, value, item_type, path):
             if not isinstance(value, integer_types):
                 extra_args = {'actual_type': type(value).__name__, 'expected_type': ' or '.join([
                     x.__name__ for x in integer_types])}
-                matches.extend(self._value_check(value, path, item_type, extra_args))
+                matches.extend(self._value_check(value, path, item_type, strict_check, extra_args))
         elif isinstance(value, list):
             message = 'Property should be of type %s at %s' % (item_type, '/'.join(map(str, path)))
             extra_args = {'actual_type': type(value).__name__, 'expected_type': list.__name__}
@@ -124,18 +133,19 @@ def check_value(self, value, path, **kwargs):
         matches = []
         primitive_type = kwargs.get('primitive_type', {})
         item_type = kwargs.get('item_type', {})
+        strict_check = kwargs.get('non_strict', self.config['strict'])
         if item_type in ['Map']:
             if isinstance(value, dict):
                 for map_key, map_value in value.items():
                     if not isinstance(map_value, dict):
                         matches.extend(self.check_primitive_type(
-                            map_value, primitive_type, path + [map_key]))
+                            map_value, primitive_type, path + [map_key], strict_check))
         else:
             # some properties support primitive types and objects
             # skip in the case it could be an object and the value is a object
             if (item_type or primitive_type) and isinstance(value, dict):
                 return matches
-            matches.extend(self.check_primitive_type(value, primitive_type, path))
+            matches.extend(self.check_primitive_type(value, primitive_type, path, strict_check))
 
         return matches
 
@@ -153,15 +163,19 @@ def check(self, cfn, properties, specs, spec_type, path):
                 else:
                     item_type = None
                 if primitive_type:
-                    if not(spec_type == 'AWS::CloudFormation::Stack' and prop == 'Parameters'):
-                        matches.extend(
-                            cfn.check_value(
-                                properties, prop, path,
-                                check_value=self.check_value,
-                                primitive_type=primitive_type,
-                                item_type=item_type
-                            )
+                    strict_check = self.config['strict']
+                    if spec_type in self.strict_exceptions:
+                        if prop in self.strict_exceptions[spec_type]:
+                            strict_check = False
+                    matches.extend(
+                        cfn.check_value(
+                            properties, prop, path,
+                            check_value=self.check_value,
+                            primitive_type=primitive_type,
+                            item_type=item_type,
+                            non_strict=strict_check,
                         )
+                    )
 
         return matches
 


Test patch:
diff --git a/test/unit/rules/resources/properties/test_value_primitive_type.py b/test/unit/rules/resources/properties/test_value_primitive_type.py
--- a/test/unit/rules/resources/properties/test_value_primitive_type.py
+++ b/test/unit/rules/resources/properties/test_value_primitive_type.py
@@ -63,33 +63,34 @@ def setUp(self):
     def test_file_positive(self):
         """Test Positive"""
         # Test Booleans
-        self.assertEqual(len(self.rule._value_check('True', ['test'], 'Boolean', {})), 0)
-        self.assertEqual(len(self.rule._value_check('False', ['test'], 'Boolean', {})), 0)
-        self.assertEqual(len(self.rule._value_check(1, ['test'], 'Boolean', {})), 1)
+        self.assertEqual(len(self.rule._value_check('True', ['test'], 'Boolean', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check('False', ['test'], 'Boolean', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check(1, ['test'], 'Boolean', False, {})), 1)
         # Test Strings
-        self.assertEqual(len(self.rule._value_check(1, ['test'], 'String', {})), 0)
-        self.assertEqual(len(self.rule._value_check(2, ['test'], 'String', {})), 0)
-        self.assertEqual(len(self.rule._value_check(True, ['test'], 'String', {})), 0)
+        self.assertEqual(len(self.rule._value_check(1, ['test'], 'String', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check(2, ['test'], 'String', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check(True, ['test'], 'String', False, {})), 0)
         # Test Integer
-        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Integer', {})), 0)
-        self.assertEqual(len(self.rule._value_check('1.2', ['test'], 'Integer', {})), 1)
-        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Integer', {})), 1)
-        self.assertEqual(len(self.rule._value_check('test', ['test'], 'Integer', {})), 1)
+        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Integer', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check('1.2', ['test'], 'Integer', False, {})), 1)
+        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Integer', False, {})), 1)
+        self.assertEqual(len(self.rule._value_check('test', ['test'], 'Integer', False, {})), 1)
         # Test Double
-        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Double', {})), 0)
-        self.assertEqual(len(self.rule._value_check('1.2', ['test'], 'Double', {})), 0)
-        self.assertEqual(len(self.rule._value_check(1, ['test'], 'Double', {})), 0)
-        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Double', {})), 1)
-        self.assertEqual(len(self.rule._value_check('test', ['test'], 'Double', {})), 1)
+        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Double', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check('1.2', ['test'], 'Double', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check(1, ['test'], 'Double', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Double', False, {})), 1)
+        self.assertEqual(len(self.rule._value_check('test', ['test'], 'Double', False, {})), 1)
         # Test Long
-        self.assertEqual(len(self.rule._value_check(str(65536 * 65536), ['test'], 'Long', {})), 0)
-        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Long', {})), 0)
-        self.assertEqual(len(self.rule._value_check('1.2', ['test'], 'Long', {})), 1)
-        self.assertEqual(len(self.rule._value_check(1.2, ['test'], 'Long', {})), 1)
-        self.assertEqual(len(self.rule._value_check(65536 * 65536, ['test'], 'Long', {})), 0)
-        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Long', {})), 1)
-        self.assertEqual(len(self.rule._value_check('test', ['test'], 'Long', {})), 1)
+        self.assertEqual(len(self.rule._value_check(
+            str(65536 * 65536), ['test'], 'Long', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Long', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check('1.2', ['test'], 'Long', False, {})), 1)
+        self.assertEqual(len(self.rule._value_check(1.2, ['test'], 'Long', False, {})), 1)
+        self.assertEqual(len(self.rule._value_check(65536 * 65536, ['test'], 'Long', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Long', False, {})), 1)
+        self.assertEqual(len(self.rule._value_check('test', ['test'], 'Long', False, {})), 1)
         # Test Unknown type doesn't return error
-        self.assertEqual(len(self.rule._value_check(1, ['test'], 'Unknown', {})), 0)
-        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Unknown', {})), 0)
-        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Unknown', {})), 0)
+        self.assertEqual(len(self.rule._value_check(1, ['test'], 'Unknown', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check('1', ['test'], 'Unknown', False, {})), 0)
+        self.assertEqual(len(self.rule._value_check(True, ['test'], 'Unknown', False, {})), 0)