Repository: aws-cloudformation/cfn-lint

Issue:
E1022 Fn::Join must use a list - fails with CloudFormation Custom Resource
*cfn-lint version: 0.21.4*

E1022 helpfully checks Fn::Join arguments for their type as a list; however, it is not able to recognize CloudFormation Custom Resource outputs as possible lists.

The following template launches successfully:
```
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
  CustomResourceFn:
    Type: "AWS::Serverless::Function"
    Properties:
      Handler: index.handler
      Runtime: python3.7
      InlineCode: |
        import cfnresponse
        def handler(event, context):
          outputs = {'ListOutput': ['joe', 'jill', 'jane', 'jim']}
          return cfnresponse.send(event, context, "SUCCESS", outputs)

  CustomResource:
    Type: "Custom::Resource"
    Properties:
      ServiceToken: !GetAtt CustomResourceFn.Arn

  Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      Tags:
        - Key: BucketUsers
          Value: !Join [":", !GetAtt CustomResource.ListOutput]
```

However, it raises the following error:
```
E1022 Fn::Join must use a list at Resources/Bucket/Properties/Tags/0/Value/Fn::Join
cfn/core/E1022.yml:25:11
```


Discussion:

Resolving patch:
diff --git a/src/cfnlint/__init__.py b/src/cfnlint/__init__.py
--- a/src/cfnlint/__init__.py
+++ b/src/cfnlint/__init__.py
@@ -229,16 +229,23 @@ def get_valid_getatts(self):
         results = {}
         resources = self.template.get('Resources', {})
 
-        astrik_types = (
-            'Custom::', 'AWS::CloudFormation::Stack',
+        astrik_string_types = (
+            'AWS::CloudFormation::Stack',
+        )
+        astrik_unknown_types = (
+            'Custom::',
             'AWS::Serverless::', 'AWS::CloudFormation::CustomResource'
         )
+
         for name, value in resources.items():
             if 'Type' in value:
                 valtype = value['Type']
-                if valtype.startswith(astrik_types):
+                if valtype.startswith(astrik_string_types):
                     LOGGER.debug('Cant build an appropriate getatt list from %s', valtype)
                     results[name] = {'*': {'PrimitiveItemType': 'String'}}
+                elif valtype.startswith(astrik_unknown_types):
+                    LOGGER.debug('Cant build an appropriate getatt list from %s', valtype)
+                    results[name] = {'*': {}}
                 else:
                     if value['Type'] in resourcetypes:
                         if 'Attributes' in resourcetypes[valtype]:
diff --git a/src/cfnlint/rules/functions/Join.py b/src/cfnlint/rules/functions/Join.py
--- a/src/cfnlint/rules/functions/Join.py
+++ b/src/cfnlint/rules/functions/Join.py
@@ -78,18 +78,22 @@ def _is_ref_a_list(self, parameter, template_parameters):
             return True
         return False
 
-
     def _is_getatt_a_list(self, parameter, get_atts):
         """ Is a GetAtt a List """
 
         for resource, attributes in get_atts.items():
             for attribute_name, attribute_values in attributes.items():
-                if resource == parameter[0] and attribute_name in ['*', parameter[1]]:
+                if resource == parameter[0] and attribute_name == '*':
+                    if attribute_values.get('PrimitiveItemType'):
+                        return 'FALSE'
                     if attribute_values.get('Type') == 'List':
-                        return True
-
-        return False
+                        return 'TRUE'
+                    return 'UNKNOWN'
+                if resource == parameter[0] and attribute_name == parameter[1]:
+                    if attribute_values.get('Type') == 'List':
+                        return 'TRUE'
 
+        return 'FALSE'
 
     def _match_string_objs(self, join_string_objs, cfn, path):
         """ Check join list """
@@ -112,7 +116,7 @@ def _match_string_objs(self, join_string_objs, cfn, path):
                             matches.append(RuleMatch(
                                 path, message.format('/'.join(map(str, path)))))
                     elif key in ['Fn::GetAtt']:
-                        if not self._is_getatt_a_list(self._normalize_getatt(value), get_atts):
+                        if self._is_getatt_a_list(self._normalize_getatt(value), get_atts) == 'FALSE':
                             message = 'Fn::Join must use a list at {0}'
                             matches.append(RuleMatch(
                                 path, message.format('/'.join(map(str, path)))))
@@ -139,7 +143,7 @@ def _match_string_objs(self, join_string_objs, cfn, path):
                                     matches.append(RuleMatch(
                                         path, message.format('/'.join(map(str, path)))))
                             elif key in ['Fn::GetAtt']:
-                                if self._is_getatt_a_list(self._normalize_getatt(value), get_atts):
+                                if self._is_getatt_a_list(self._normalize_getatt(value), get_atts) == 'TRUE':
                                     message = 'Fn::Join must not be a list at {0}'
                                     matches.append(RuleMatch(
                                         path, message.format('/'.join(map(str, path)))))


Test patch:
diff --git a/test/fixtures/templates/good/functions/join.yaml b/test/fixtures/templates/good/functions/join.yaml
--- a/test/fixtures/templates/good/functions/join.yaml
+++ b/test/fixtures/templates/good/functions/join.yaml
@@ -45,4 +45,26 @@ Resources:
             - Fn::Join:
               - ''
               - !Ref AWS::NotificationARNs
+  CustomResourceFn:
+    Type: "AWS::Serverless::Function"
+    Properties:
+      Handler: index.handler
+      Runtime: python3.7
+      InlineCode: |
+        import cfnresponse
+        def handler(event, context):
+          outputs = {'ListOutput': ['joe', 'jill', 'jane', 'jim']}
+          return cfnresponse.send(event, context, "SUCCESS", outputs)
+
+  CustomResource:
+    Type: "Custom::Resource"
+    Properties:
+      ServiceToken: !GetAtt CustomResourceFn.Arn
+
+  Bucket:
+    Type: "AWS::S3::Bucket"
+    Properties:
+      Tags:
+        - Key: BucketUsers
+          Value: !Join [":", !GetAtt CustomResource.ListOutput]