Repository: localstack/localstack

Issue:
CloudFormation does not expose ExportName
Given a CF template that defines outputs as named exports, I expect the "describe-stack" endpoint to include the exported names. With localstack, that is currently not the case.

Example template:
```yaml
  Outputs:
    VPCId:
      Value: {Ref: MyVPC}
      Export:
        Name: My-VPCId

  Resources:
    MyVPC:
      Properties:
        CidrBlock: 10.0.0.0/16
      Type: AWS::EC2::VPC

```

AWS:
```
$ aws cloudformation describe-stacks
...
                {
                    "OutputKey": "VPCId",
                    "OutputValue": "vpc-0123456789",
                    "ExportName": "My-VPCId"
                },

```

localstack:
```
$ aws cloudformation --endpoint-url=http://localhost:4581/ describe-stacks
...
                {
                    "OutputKey": "VPCId",
                    "OutputValue": "vpc-0123456789"
                },
...
```


Discussion:
Did you manage to resolve this issue?

 My serverless scripts depend on ExportName and fails to deploy.
> Did you manage to resolve this issue?
> 
> My serverless scripts depend on ExportName and fails to deploy.

I haven't worked on this. It's (one of) the blocker(s) to using localstack in one of my projects.

Resolving patch:
diff --git a/localstack/services/cloudformation/cloudformation_starter.py b/localstack/services/cloudformation/cloudformation_starter.py
--- a/localstack/services/cloudformation/cloudformation_starter.py
+++ b/localstack/services/cloudformation/cloudformation_starter.py
@@ -146,6 +146,10 @@ def apply_patches():
     """ Apply patches to make LocalStack seamlessly interact with the moto backend.
         TODO: Eventually, these patches should be contributed to the upstream repo! """
 
+    # add model mappings to moto
+
+    parsing.MODEL_MAP.update(MODEL_MAP)
+
     # Patch S3Backend.get_key method in moto to use S3 API from LocalStack
 
     def get_key(self, bucket_name, key_name, version_id=None):
@@ -173,10 +177,6 @@ def clean_json(resource_json, resources_map):
     clean_json_orig = parsing.clean_json
     parsing.clean_json = clean_json
 
-    # add model mappings to moto
-
-    parsing.MODEL_MAP.update(MODEL_MAP)
-
     # Patch parse_and_create_resource method in moto to deploy resources in LocalStack
 
     def parse_and_create_resource(logical_id, resource_json, resources_map, region_name, force_create=False):
@@ -377,17 +377,31 @@ def update_resource_id(resource, new_id, props, region_name):
 
     def parse_output(output_logical_id, output_json, resources_map):
         try:
-            return parse_output_orig(output_logical_id, output_json, resources_map)
+            result = parse_output_orig(output_logical_id, output_json, resources_map)
         except KeyError:
-            output = Output()
-            output.key = output_logical_id
-            output.value = None
-            output.description = output_json.get('Description')
-            return output
+            result = Output()
+            result.key = output_logical_id
+            result.value = None
+            result.description = output_json.get('Description')
+        # Make sure output includes export name
+        if not hasattr(result, 'export_name'):
+            result.export_name = output_json.get('Export', {}).get('Name')
+        return result
 
     parse_output_orig = parsing.parse_output
     parsing.parse_output = parse_output
 
+    # Make sure the export name is returned for stack outputs
+
+    if '<ExportName>' not in responses.DESCRIBE_STACKS_TEMPLATE:
+        find = '</OutputValue>'
+        replace = """</OutputValue>
+        {% if output.export_name %}
+        <ExportName>{{ output.export_name }}</ExportName>
+        {% endif %}
+        """
+        responses.DESCRIBE_STACKS_TEMPLATE = responses.DESCRIBE_STACKS_TEMPLATE.replace(find, replace)
+
     # Patch DynamoDB get_cfn_attribute(..) method in moto
 
     def DynamoDB_Table_get_cfn_attribute(self, attribute_name):


Test patch:
diff --git a/tests/integration/templates/template2.yaml b/tests/integration/templates/template2.yaml
--- a/tests/integration/templates/template2.yaml
+++ b/tests/integration/templates/template2.yaml
@@ -22,3 +22,5 @@ Outputs:
   SQSQueue1URL:
     Value:
       Ref: SQSQueue1
+    Export:
+      Name: SQSQueue1-URL
diff --git a/tests/integration/test_cloudformation.py b/tests/integration/test_cloudformation.py
--- a/tests/integration/test_cloudformation.py
+++ b/tests/integration/test_cloudformation.py
@@ -232,8 +232,9 @@ def test_list_stack_resources_returns_queue_urls(self):
         def check_stack():
             stack = get_stack_details(stack_name)
             self.assertEqual(stack['StackStatus'], 'CREATE_COMPLETE')
+            return stack
 
-        retry(check_stack, retries=3, sleep=2)
+        details = retry(check_stack, retries=3, sleep=2)
 
         stack_summaries = list_stack_resources(stack_name)
         queue_urls = get_queue_urls()
@@ -247,6 +248,11 @@ def check_stack():
         for resource in stack_topics:
             self.assertIn(resource['PhysicalResourceId'], topic_arns)
 
+        # assert that stack outputs are returned properly
+        outputs = details.get('Outputs', [])
+        self.assertEqual(len(outputs), 1)
+        self.assertEqual(outputs[0]['ExportName'], 'SQSQueue1-URL')
+
     def test_create_change_set(self):
         cloudformation = aws_stack.connect_to_service('cloudformation')