Repository: aws-cloudformation/cfn-lint

Issue:
False positive E3002 when using Fn to set non-primitive list property
*cfn-lint version:* 0.8.3

*Description of issue:* Using a function like `!GetAtt` or `!FindInMap` to set a list property generates an erroneous E3002 "Property is an object instead of List". 

This only occurs for properties that are specced to be lists of *non*-primitive types. Properties that expect lists of primitives (e.g., List of String) seem to work fine with functions.

Minimal example (using `!GetAtt`):
```yaml
Resources:
  Custom:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: arn.example

  LB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      # SecurityGroups: {Type: List, PrimitiveItemType: String}
      # cfn-lint allows this:
      SecurityGroups: !GetAtt Custom.ExampleListOfStrings
      # LoadBalancerAttributes: {Type: List, ItemType: LoadBalancerAttribute}
      # cfn-lint rejects this with "E3002 Property is an object instead of List":
      LoadBalancerAttributes: !GetAtt Custom.ExampleListOfKeyValuePairs
```

Results: E3002 Property is an object instead of List at Resources/LB/Properties/LoadBalancerAttributes

Expected: No errors

Notes:
* The example uses a CustomResource only because that supports arbitrary GetAtts, and LoadBalancer because that has both primitive and non-primitive list properties, making it easy to demonstrate the difference in handling. But it's also reproducible using (e.g.,) `!FindInMap` to set a property to a list of objects.
* Real-world case: setting `AWS::Route53::RecordSetGroup.RecordSets` from a custom resource  https://gitlab.com/medmunds/aws-cfn-ses-domain/blob/master/example-usage.cf.yaml#L46. (This template deploys properly, but shows the cfn-lint E3002 error.)
* It looks like #43 added support for `!If` conditions in this context, but didn't cover other functions that might be used for those property values.



Discussion:

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
@@ -119,8 +119,26 @@ def check_list_for_condition(self, text, prop, parenttype, resourcename, propspe
                         message = 'Invalid !If condition specified at %s' % ('/'.join(map(str, path)))
                         matches.append(RuleMatch(path, message))
                 else:
-                    message = 'Property is an object instead of List at %s' % ('/'.join(map(str, path)))
-                    matches.append(RuleMatch(path, message))
+                    # FindInMaps can be lists of objects so skip checking those
+                    if sub_key != 'Fn::FindInMap':
+                        # if its a GetAtt to a custom resource that custom resource
+                        # can return a list of objects so skip.
+                        if sub_key == 'Fn::GetAtt':
+                            resource_name = None
+                            if isinstance(sub_value, list):
+                                resource_name = sub_value[0]
+                            elif isinstance(sub_value, six.string_types):
+                                resource_name = sub_value.split('.')[0]
+                            if resource_name:
+                                resource_type = self.cfn.template.get('Resources', {}).get(resource_name, {}).get('Type')
+                                if not (resource_type == 'AWS::CloudFormation::CustomResource' or resource_type.startswith('Custom::')):
+                                    message = 'Property is an object instead of List at %s' % ('/'.join(map(str, path)))
+                                    matches.append(RuleMatch(path, message))
+                        else:
+                            message = 'Property is an object instead of List at %s' % ('/'.join(map(str, path)))
+                            matches.append(RuleMatch(path, message))
+                    else:
+                        self.logger.debug('Too much logic to handle whats actually in the map "%s" so skipping any more validation.', sub_value)
         else:
             message = 'Property is an object instead of List at %s' % ('/'.join(map(str, path)))
             matches.append(RuleMatch(path, message))


Test patch:
diff --git a/test/fixtures/templates/good/resource_properties.yaml b/test/fixtures/templates/good/resource_properties.yaml
--- a/test/fixtures/templates/good/resource_properties.yaml
+++ b/test/fixtures/templates/good/resource_properties.yaml
@@ -623,3 +623,24 @@ Resources:
       CacheNodeType: String
       Engine: String
       NumCacheNodes: 1
+  ### Testing relationships for lists to Custom and Maps
+  Custom:
+    Type: AWS::CloudFormation::CustomResource
+    Properties:
+      ServiceToken: arn.example
+
+  LB1:
+    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+    Properties:
+      SecurityGroups: !GetAtt Custom.ExampleListOfStrings
+      # LoadBalancerAttributes: {Type: List, ItemType: LoadBalancerAttribute}
+      # should be allowed as the custom resource can return a list of objects
+      LoadBalancerAttributes: !GetAtt Custom.ExampleListOfKeyValuePairs
+  LB2:
+    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
+    Properties:
+      SecurityGroups: !GetAtt Custom.ExampleListOfStrings
+      # LoadBalancerAttributes: {Type: List, ItemType: LoadBalancerAttribute}
+      # Should be allowed because mappings can have a list of objects
+      # This mapping isn't correct but validation of the actually mapping is more difficult
+      LoadBalancerAttributes: !FindInMap [ AWSInstanceType2Arch, c1.medium, Arch ]