Repository: aws-cloudformation/cfn-lint

Issue:
Template format error: Every AllowedValues member must be a list
cfn-lint 0.26.2

Checking .yaml file, block 
```yaml
Parameters:
  Env:
    Type: "String"
    Description: "Environment"
    Default: "staging"
    AllowedValues:
            staging
            mock
            production
            DEVOPS
```
Lint says no errors, but when run aws cloudformation:
An error occurred (ValidationError) when calling the CreateStack operation: Template format error: Every AllowedValues member must be a list.


Discussion:
Should be an easy one to do.  Thanks for submitting this. 

Resolving patch:
diff --git a/src/cfnlint/rules/parameters/Configuration.py b/src/cfnlint/rules/parameters/Configuration.py
--- a/src/cfnlint/rules/parameters/Configuration.py
+++ b/src/cfnlint/rules/parameters/Configuration.py
@@ -14,37 +14,120 @@ class Configuration(CloudFormationLintRule):
     source_url = 'https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html'
     tags = ['parameters']
 
-    valid_keys = [
-        'AllowedPattern',
-        'AllowedValues',
-        'ConstraintDescription',
-        'Default',
-        'Description',
-        'MaxLength',
-        'MaxValue',
-        'MinLength',
-        'MinValue',
-        'NoEcho',
-        'Type',
-    ]
+    valid_keys = {
+        'AllowedPattern': {
+            'Type': 'String'
+        },
+        'AllowedValues': {
+            'Type': 'List',
+            'ItemType': 'String',
+        },
+        'ConstraintDescription': {
+            'Type': 'String'
+        },
+        'Default': {
+            'Type': 'String'
+        },
+        'Description': {
+            'Type': 'String'
+        },
+        'MaxLength': {
+            'Type': 'Integer',
+            'ValidForTypes': ['String']
+        },
+        'MaxValue': {
+            'Type': 'Integer',
+            'ValidForTypes': ['Number']
+        },
+        'MinLength': {
+            'Type': 'Integer',
+            'ValidForTypes': ['String']
+        },
+        'MinValue': {
+            'Type': 'Integer',
+            'ValidForTypes': ['Number']
+        },
+        'NoEcho': {
+            'Type': 'Boolean'
+        },
+        'Type': {
+            'Type': 'String'
+        }
+    }
 
     required_keys = [
         'Type'
     ]
 
+    def check_type(self, value, path, props):
+        """ Check the type and handle recursion with lists """
+        results = []
+        prop_type = props.get('Type')
+        try:
+            if prop_type in ['List']:
+                if isinstance(value, list):
+                    for i, item in enumerate(value):
+                        results.extend(self.check_type(item, path[:] + [i], {
+                            'Type': props.get('ItemType')
+                        }))
+                else:
+                    message = 'Property %s should be of type %s' % (
+                        '/'.join(map(str, path)), prop_type)
+                    results.append(RuleMatch(path, message))
+            if prop_type in ['String']:
+                if isinstance(value, (dict, list)):
+                    message = 'Property %s should be of type %s' % (
+                        '/'.join(map(str, path)), prop_type)
+                    results.append(RuleMatch(path, message))
+                str(value)
+            elif prop_type in ['Boolean']:
+                if not isinstance(value, bool):
+                    if value not in ['True', 'true', 'False', 'false']:
+                        message = 'Property %s should be of type %s' % (
+                            '/'.join(map(str, path)), prop_type)
+                        results.append(RuleMatch(path, message))
+            elif prop_type in ['Integer']:
+                if isinstance(value, bool):
+                    message = 'Property %s should be of type %s' % (
+                        '/'.join(map(str, path)), prop_type)
+                    results.append(RuleMatch(path, message))
+                else:  # has to be a Double
+                    int(value)
+        except Exception:  # pylint: disable=W0703
+            message = 'Property %s should be of type %s' % (
+                '/'.join(map(str, path)), prop_type)
+            results.append(RuleMatch(path, message,))
+
+        return results
+
     def match(self, cfn):
         """Check CloudFormation Parameters"""
 
         matches = []
 
         for paramname, paramvalue in cfn.get_parameters().items():
-            for propname, _ in paramvalue.items():
+            for propname, propvalue in paramvalue.items():
                 if propname not in self.valid_keys:
                     message = 'Parameter {0} has invalid property {1}'
                     matches.append(RuleMatch(
                         ['Parameters', paramname, propname],
                         message.format(paramname, propname)
                     ))
+                else:
+                    props = self.valid_keys.get(propname)
+                    prop_path = ['Parameters', paramname, propname]
+                    matches.extend(self.check_type(
+                        propvalue, prop_path, props))
+                    # Check that the property is needed for the current type
+                    valid_for = props.get('ValidForTypes')
+                    if valid_for is not None and paramvalue.get('Type'):
+                        if paramvalue.get('Type') not in valid_for:
+                            message = 'Parameter {0} has property {1} which is only valid for {2}'
+                            matches.append(RuleMatch(
+                                ['Parameters', paramname, propname],
+                                message.format(paramname, propname, valid_for)
+                            ))
+
             for reqname in self.required_keys:
                 if reqname not in paramvalue.keys():
                     message = 'Parameter {0} is missing required property {1}'


Test patch:
diff --git a/test/fixtures/templates/bad/parameters.yaml b/test/fixtures/templates/bad/parameters.yaml
deleted file mode 100644
--- a/test/fixtures/templates/bad/parameters.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
----
-AWSTemplateFormatVersion: '2010-09-09'
-Parameters:
-  myParameter:
-    # Type not string
-    Type: NotString
-    Default: Value
-    Description: Type not String
-  myInvalidParameter:
-    # invalid Property
-    NotType: String
-  mySsmParam:
-    Type: AWS::SSM::Parameter::Value<Test>
-Resources:
-  # Helps tests map resource types
-  IamPipeline:
-    Type: "AWS::CloudFormation::Stack"
-    Properties:
-      TemplateURL: !Sub 'https://s3.${AWS::Region}.amazonaws.com/bucket-dne-${AWS::Region}/${AWS::AccountId}/pipeline.yaml'
-      Parameters:
-        DeploymentName: iam-pipeline
-        Deploy: 'auto'
diff --git a/test/fixtures/templates/bad/parameters/configuration.yaml b/test/fixtures/templates/bad/parameters/configuration.yaml
new file mode 100644
--- /dev/null
+++ b/test/fixtures/templates/bad/parameters/configuration.yaml
@@ -0,0 +1,38 @@
+---
+AWSTemplateFormatVersion: "2010-09-09"
+Parameters:
+  allowedValuesNotAList:
+    Type: String
+    AllowedValues: vpc!@#$
+  allowedValuesAListofBadTypes:
+    Type: String
+    AllowedValues:
+      - key: value
+  minValueIsNotNumber:
+    Type: Number
+    MinValue: a
+  maxLengthIsNotString:
+    Type: Number
+    MaxLength: 10
+  notBooleanEcho:
+    Type: String
+    NoEcho: 123
+  myParameter:
+    # Type not string
+    Type: NotString
+    Default: Value
+    Description: Type not String
+  myInvalidParameter:
+    # invalid Property
+    NotType: String
+  mySsmParam:
+    Type: AWS::SSM::Parameter::Value<Test>
+Resources:
+  # Helps tests map resource types
+  IamPipeline:
+    Type: "AWS::CloudFormation::Stack"
+    Properties:
+      TemplateURL: !Sub "https://s3.${AWS::Region}.amazonaws.com/bucket-dne-${AWS::Region}/${AWS::AccountId}/pipeline.yaml"
+      Parameters:
+        DeploymentName: iam-pipeline
+        Deploy: "auto"
diff --git a/test/unit/rules/parameters/test_configuration.py b/test/unit/rules/parameters/test_configuration.py
--- a/test/unit/rules/parameters/test_configuration.py
+++ b/test/unit/rules/parameters/test_configuration.py
@@ -20,4 +20,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/parameters.yaml', 2)
+        self.helper_file_negative('test/fixtures/templates/bad/parameters/configuration.yaml', 7)
diff --git a/test/unit/rules/parameters/test_types.py b/test/unit/rules/parameters/test_types.py
--- a/test/unit/rules/parameters/test_types.py
+++ b/test/unit/rules/parameters/test_types.py
@@ -20,4 +20,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/parameters.yaml', 1)
+        self.helper_file_negative('test/fixtures/templates/bad/parameters/configuration.yaml', 1)
diff --git a/test/unit/rules/parameters/test_used.py b/test/unit/rules/parameters/test_used.py
--- a/test/unit/rules/parameters/test_used.py
+++ b/test/unit/rules/parameters/test_used.py
@@ -24,7 +24,7 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/parameters.yaml', 3)
+        self.helper_file_negative('test/fixtures/templates/bad/parameters/configuration.yaml', 8)
 
     def test_file_negative_removed(self):
         """Test failure"""