Repository: aws-cloudformation/cfn-lint

Issue:
Best way to handle parameters from nested stacks?
This project is awesome and super useful!

I can imagine the complexity of linting nested stacks is super high and wouldn't expect that, but how are folks handling situations where Outputs from nested stacks are being used?

For example, given this code:
```
            /etc/aws-signing-proxy.yml:
              content: !Sub |
                listen-address: 127.0.0.1
                port: 9200
                target: https://${ElasticsearchStack.Outputs.DomainEndpoint}
                region: ${AWS::Region}
```

always throws:
```
E1019 String parameter ElasticsearchStack.Outputs.DomainEndpoint not found in string for Resources/ServerLaunchConfig/Metadata/AWS::CloudFormation::Init/es_proxy_setup/files//etc/aws-signing-proxy.yml/content/Fn::Sub
```

I don't want to disable all E1019 checks because they're useful.  Would it maybe be right, for now, to exclude all parameters that have `/.Outputs./` in their name?


Discussion:
Let me check on that and add tests for this going forward.  I thought I had this working but I'm guessing I forgot to write the test to make sure that change stayed permanent.  Now its been a while.  

The best I had at the time was to ignore GetAtts to resources that that are of type AWS::CloudFormation::Stack.  The same applies to custom:: resources.  Would that make sense to you?

Resolving patch:
diff --git a/src/cfnlint/__init__.py b/src/cfnlint/__init__.py
--- a/src/cfnlint/__init__.py
+++ b/src/cfnlint/__init__.py
@@ -482,10 +482,15 @@ def get_valid_getatts(self):
         resourcetypes = cfnlint.helpers.RESOURCE_SPECS['us-east-1'].get('ResourceTypes')
         results = {}
         resources = self.template.get('Resources', {})
+
+        astrik_types = (
+            'Custom::', 'AWS::CloudFormation::Stack',
+            'AWS::Serverless::', 'AWS::CloudFormation::CustomResource'
+        )
         for name, value in resources.items():
             if 'Type' in value:
                 valtype = value['Type']
-                if valtype.startswith(('Custom::', 'AWS::CloudFormation::Stack', 'AWS::Serverless::')):
+                if valtype.startswith(astrik_types):
                     LOGGER.debug('Cant build an appropriate getatt list from %s', valtype)
                     results[name] = {'*': {'PrimitiveItemType': 'String'}}
                 else:
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
@@ -51,17 +51,23 @@ def _test_string(self, cfn, sub_string, parameters, tree):
         valid_params.extend(cfn.get_resource_names())
         for key, _ in parameters.items():
             valid_params.append(key)
-        for resource, attributes in get_atts.items():
-            for attribute_name, _ in attributes.items():
-                valid_params.append('%s.%s' % (resource, attribute_name))
 
         for string_param in string_params:
             string_param = string_param[2:-1]
-            if isinstance(string_param, six.string_types):
+            if isinstance(string_param, (six.string_types, six.text_type)):
                 if string_param not in valid_params:
-                    message = 'String parameter {0} not found in string for {1}'
-                    matches.append(RuleMatch(
-                        tree, message.format(string_param, '/'.join(map(str, tree)))))
+                    found = False
+                    for resource, attributes in get_atts.items():
+                        for attribute_name, _ in attributes.items():
+                            if resource == string_param.split('.')[0] and attribute_name == '*':
+                                found = True
+                            elif (resource == string_param.split('.')[0] and
+                                  attribute_name == '.'.join(string_param.split('.')[1:])):
+                                found = True
+                    if not found:
+                        message = 'String parameter {0} not found in string for {1}'
+                        matches.append(RuleMatch(
+                            tree, message.format(string_param, '/'.join(map(str, tree)))))
 
         return matches
 


Test patch:
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
@@ -31,4 +31,4 @@ def test_file_positive(self):
 
     def test_file_negative(self):
         """Test failure"""
-        self.helper_file_negative('templates/bad/functions_sub.yaml', 4)
+        self.helper_file_negative('templates/bad/functions_sub.yaml', 7)
diff --git a/test/templates/bad/functions_sub.yaml b/test/templates/bad/functions_sub.yaml
--- a/test/templates/bad/functions_sub.yaml
+++ b/test/templates/bad/functions_sub.yaml
@@ -5,6 +5,18 @@ Description: >
 Resources:
   myInstance:
     Type: AWS::EC2::Instance
+    Metadata:
+      AWS::CloudFormation::Init:
+        config:
+          files:
+            /etc/aws-signing-proxy.yml:
+              content: !Sub |
+                listen-address: 127.0.0.1
+                port: 9200
+                target: https://${mySubStack.Outputs.DomainEndpoint}
+                region: ${AWS::Region}
+                loadbalancer: ${myInstance3.PublicDnsName1}
+                badresource: ${myInstance5.PublicDnsName}
     Properties:
       ImageId: ami-asdfef
       UserData:
diff --git a/test/templates/good/functions_sub.yaml b/test/templates/good/functions_sub.yaml
--- a/test/templates/good/functions_sub.yaml
+++ b/test/templates/good/functions_sub.yaml
@@ -11,9 +11,22 @@ Parameters:
     Type: String
     Default: java
     Description: App Package
+  mySubnets:
+    Type: List<AWS::EC2::Subnet::Id>
 Resources:
   myInstance:
     Type: AWS::EC2::Instance
+    Metadata:
+      AWS::CloudFormation::Init:
+        config:
+          files:
+            /etc/aws-signing-proxy.yml:
+              content: !Sub |
+                listen-address: 127.0.0.1
+                port: 9200
+                target: https://${mySubStack.Outputs.DomainEndpoint}
+                region: ${AWS::Region}
+                loadbalancer: ${myAlb.DNSName}
     Properties:
       ImageId: String
       UserData:
@@ -21,4 +34,19 @@ Resources:
           Fn::Sub:
           - yum install ${myPackage}
             yum install ${package}
+            yum install ${myCustomResource.Package}
           - package: !Ref myAppPackage
+  mySubStack:
+    Type: AWS::CloudFormation::Stack
+    Properties:
+      TemplateURL: String
+  myCustomResource:
+    Type: Custom::Test
+    Properties:
+      ServiceToken: Arn
+
+  myAlb:
+    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
+    Properties:
+      Name: !Sub ${AWS::StackName}-lb
+      Subnets: !Ref mySubnets