Repository: aws-cloudformation/cfn-lint

Issue:
Errors when using Fn::Transform
*cfn-lint version: (`0.20.2`)*

Given the following valid template, I see 2 errors:

```yaml
Parameters:
  ImageId:
    Type: AWS::EC2::Image::Id
  InstanceType:
    Type: String

Resources:
  LaunchConfiguration:
    Type: AWS::AutoScaling::LaunchConfiguration
    Properties:
      ImageId: !Ref ImageId
      InstanceType: !Ref InstanceType
      UserData:
        'Fn::Base64':
          'Fn::Sub':
            'Fn::Transform': # Returns a string that contains Fn::Sub tokens like ${AWS::Region}
              Name: DynamicUserData
```
* `E1019 Sub should be a string or array of 2 items for Resources/LaunchConfiguration/Properties/UserData/Fn::Base64/Fn::Sub` - `Fn::Transform` can return a string or a template, so `Fn::Sub` should be forgiving of it.
* `E0002 Unknown exception while processing rule W2001: expected string or bytes-like object` Same root cause, but fails in a different way due to assumption that it complies with E1019 in a specific way.





Discussion:
We will probably fail on higher level Transforms for a while as we need to be able to lint the transformed template.  However, in this case we shouldn't fail at all as this Transform is way down in the values of a properties section.

Resolving patch:
diff --git a/src/cfnlint/rules/functions/Sub.py b/src/cfnlint/rules/functions/Sub.py
--- a/src/cfnlint/rules/functions/Sub.py
+++ b/src/cfnlint/rules/functions/Sub.py
@@ -188,6 +188,17 @@ def match(self, cfn):
                     message = 'Sub should be an array of 2 for {0}'
                     matches.append(RuleMatch(
                         tree, message.format('/'.join(map(str, tree)))))
+            elif isinstance(sub_value_obj, dict):
+                if len(sub_value_obj) == 1:
+                    for key, _ in sub_value_obj.items():
+                        if not key == 'Fn::Transform':
+                            message = 'Sub should be a string or array of 2 items for {0}'
+                            matches.append(RuleMatch(
+                                tree, message.format('/'.join(map(str, tree)))))
+                else:
+                    message = 'Sub should be a string or array of 2 items for {0}'
+                    matches.append(RuleMatch(
+                        tree, message.format('/'.join(map(str, tree)))))
             else:
                 message = 'Sub should be a string or array of 2 items for {0}'
                 matches.append(RuleMatch(
diff --git a/src/cfnlint/rules/parameters/Used.py b/src/cfnlint/rules/parameters/Used.py
--- a/src/cfnlint/rules/parameters/Used.py
+++ b/src/cfnlint/rules/parameters/Used.py
@@ -57,7 +57,7 @@ def match(self, cfn):
         for subtree in subtrees:
             if isinstance(subtree[-1], list):
                 subs.extend(cfn.get_sub_parameters(subtree[-1][0]))
-            else:
+            elif isinstance(subtree[-1], six.string_types):
                 subs.extend(cfn.get_sub_parameters(subtree[-1]))
 
         for paramname, _ in cfn.get_parameters().items():


Test patch:
diff --git a/test/fixtures/templates/bad/functions/sub.yaml b/test/fixtures/templates/bad/functions/sub.yaml
--- a/test/fixtures/templates/bad/functions/sub.yaml
+++ b/test/fixtures/templates/bad/functions/sub.yaml
@@ -56,6 +56,26 @@ Resources:
         Fn::Sub:
         - "${myCidr}"
         - myCidr: !Ref CidrBlock
+  LaunchConfiguration:
+    Type: AWS::AutoScaling::LaunchConfiguration
+    Properties:
+      ImageId: ami-123456
+      InstanceType: t2.micro
+      UserData:
+        'Fn::Base64':
+          'Fn::Sub':
+            'Fn::Transform':  # Doesn't support an object of two items
+              Name: DynamicUserData
+            'Fn::GetAtt': myVPc2.CidrBlock
+  LaunchConfiguration2:
+    Type: AWS::AutoScaling::LaunchConfiguration
+    Properties:
+      ImageId: ami-123456
+      InstanceType: t2.micro
+      UserData:
+        'Fn::Base64':
+          'Fn::Sub':
+            'Fn::GetAtt': myVPc.CidrBlock
 Outputs:
   myOutput:
     Value: !Sub "${myInstance3.PrivateDnsName1}"
diff --git a/test/fixtures/templates/good/functions/sub.yaml b/test/fixtures/templates/good/functions/sub.yaml
--- a/test/fixtures/templates/good/functions/sub.yaml
+++ b/test/fixtures/templates/good/functions/sub.yaml
@@ -44,9 +44,18 @@ Resources:
     Type: Custom::Test
     Properties:
       ServiceToken: Arn
-
   myAlb:
     Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
     Properties:
       Name: !Sub ${AWS::StackName}-lb
       Subnets: !Ref mySubnets
+  LaunchConfiguration:
+    Type: AWS::AutoScaling::LaunchConfiguration
+    Properties:
+      ImageId: ami-123456
+      InstanceType: t2.micro
+      UserData:
+        'Fn::Base64':
+          'Fn::Sub':
+            'Fn::Transform':  # Doesn't fail on Transform
+              Name: DynamicUserData
diff --git a/test/fixtures/templates/good/parameters/used_transforms.yaml b/test/fixtures/templates/good/parameters/used_transforms.yaml
--- a/test/fixtures/templates/good/parameters/used_transforms.yaml
+++ b/test/fixtures/templates/good/parameters/used_transforms.yaml
@@ -17,3 +17,13 @@ Resources:
       Handler: com.SomeLambda::handleRequest
       Runtime: java8
       MemorySize: 256
+  LaunchConfiguration:
+    Type: AWS::AutoScaling::LaunchConfiguration
+    Properties:
+      ImageId: ami-123456
+      InstanceType: t2.micro
+      UserData:
+        'Fn::Base64':
+          'Fn::Sub':
+            'Fn::Transform':  # Doesn't fail on Transform
+              Name: DynamicUserData
diff --git a/test/rules/functions/test_sub.py b/test/rules/functions/test_sub.py
--- a/test/rules/functions/test_sub.py
+++ b/test/rules/functions/test_sub.py
@@ -34,4 +34,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('test/fixtures/templates/bad/functions/sub.yaml', 13)
+        self.helper_file_negative('test/fixtures/templates/bad/functions/sub.yaml', 15)