Repository: getmoto/moto

Issue:
CloudFormation output for DynamoDB table
## **how to reproduce the issue**

```python
import json
import os

import boto3
import pytest
from moto import mock_cloudformation

CFN_TEMPLATE = {
    "Outputs": {
        "MyTableName": {
            "Value": {
                "Ref": "MyTable"
            }
        },
    },
    "Resources": {
        "MyTable": {
            "Type": "AWS::DynamoDB::Table",
            "Properties": {
                "KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}],
                "AttributeDefinitions": [
                    {"AttributeName": "id", "AttributeType": "S"}
                ],
            },
        },
    },
}


@pytest.fixture(scope="function")
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"


@mock_cloudformation
def test_get_stack(aws_credentials):
    stack_name = "foobar"
    cfn = boto3.client("cloudformation")
    cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(CFN_TEMPLATE))
    # Wait until moto creates the stack
    waiter = cfn.get_waiter("stack_create_complete")
    waiter.wait(StackName=stack_name)
    assert True

```

## **what you expected to happen**

I have a template like that and it creates a valid stack, with the name of the table in the MyTablename CfnOutput "MyTableName".

## **what actually happens**

```
======================================== FAILURES ========================================
_____________________________________ test_get_stack _____________________________________

self = <botocore.parsers.QueryParser object at 0x7f5ce7c3e828>
xml_string = b'<DescribeStacksResponse><DescribeStacksResult><Stacks><member><StackName>foobar</StackName><StackId>arn:aws:cloudfor...ber></Outputs><Parameters></Parameters><Tags></T
ags></member></Stacks></DescribeStacksResult></DescribeStacksResponse>'

    def _parse_xml_string_to_dom(self, xml_string):
        try:
            parser = xml.etree.cElementTree.XMLParser(
                target=xml.etree.cElementTree.TreeBuilder(),
                encoding=self.DEFAULT_ENCODING)
>           parser.feed(xml_string)
E             File "<string>", line None
E           xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 1, column 452

../../../../.local/lib/python3.6/site-packages/botocore/parsers.py:452: ParseError

During handling of the above exception, another exception occurred:

aws_credentials = None

    @mock_cloudformation
    def test_get_stack(aws_credentials):
        stack_name = "foobar"
        cfn = boto3.client("cloudformation")
        cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(CFN_TEMPLATE))
        # Wait until moto creates the stack
        waiter = cfn.get_waiter("stack_create_complete")
>       waiter.wait(StackName=stack_name)

../../../../min-test-case.py:46:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
../../../../.local/lib/python3.6/site-packages/botocore/waiter.py:53: in wait
    Waiter.wait(self, **kwargs)
../../../../.local/lib/python3.6/site-packages/botocore/waiter.py:297: in wait
    response = self._operation_method(**kwargs)
../../../../.local/lib/python3.6/site-packages/botocore/waiter.py:84: in __call__
    return self._client_method(**kwargs)     
../../../../.local/lib/python3.6/site-packages/botocore/client.py:316: in _api_call
    return self._make_api_call(operation_name, kwargs)              
../../../../.local/lib/python3.6/site-packages/botocore/client.py:622: in _make_api_call
    operation_model, request_dict, request_context)
../../../../.local/lib/python3.6/site-packages/botocore/client.py:641: in _make_request
    return self._endpoint.make_request(operation_model, request_dict)
../../../../.local/lib/python3.6/site-packages/botocore/endpoint.py:102: in make_request
    return self._send_request(request_dict, operation_model)
../../../../.local/lib/python3.6/site-packages/botocore/endpoint.py:135: in _send_request
    request, operation_model, context)
../../../../.local/lib/python3.6/site-packages/botocore/endpoint.py:167: in _get_response
    request, operation_model)
../../../../.local/lib/python3.6/site-packages/botocore/endpoint.py:218: in _do_get_response
    response_dict, operation_model.output_shape)
../../../../.local/lib/python3.6/site-packages/botocore/parsers.py:246: in parse
    parsed = self._do_parse(response, shape)
../../../../.local/lib/python3.6/site-packages/botocore/parsers.py:524: in _do_parse
    return self._parse_body_as_xml(response, shape, inject_metadata=True)
../../../../.local/lib/python3.6/site-packages/botocore/parsers.py:528: in _parse_body_as_xml
    root = self._parse_xml_string_to_dom(xml_contents)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <botocore.parsers.QueryParser object at 0x7f5ce7c3e828>
xml_string = b'<DescribeStacksResponse><DescribeStacksResult><Stacks><member><StackName>foobar</StackName><StackId>arn:aws:cloudfor...ber></Outputs><Parameters></Parameters><Tags></T
ags></member></Stacks></DescribeStacksResult></DescribeStacksResponse>'

    def _parse_xml_string_to_dom(self, xml_string):
        try:
            parser = xml.etree.cElementTree.XMLParser(
                target=xml.etree.cElementTree.TreeBuilder(),
                encoding=self.DEFAULT_ENCODING)
            parser.feed(xml_string)
            root = parser.close()
        except XMLParseError as e:
            raise ResponseParserError(
                "Unable to parse response (%s), "
                "invalid XML received. Further retries may succeed:\n%s" %
>               (e, xml_string))
E           botocore.parsers.ResponseParserError: Unable to parse response (not well-formed (invalid token): line 1, column 452), invalid XML received. Further retries may succeed:
E           b'<DescribeStacksResponse><DescribeStacksResult><Stacks><member><StackName>foobar</StackName><StackId>arn:aws:cloudformation:us-east-1:123456789:stack/foobar/3781cd29-805
c-4564-b7e0-9ac3647f350e</StackId><CreationTime>2010-07-27T22:28:28Z</CreationTime><StackStatus>CREATE_COMPLETE</StackStatus><NotificationARNs/><DisableRollback>false</DisableRollbac
k><Outputs><member><OutputKey>MyTableName</OutputKey><OutputValue><moto.dynamodb2.models.Table object at 0x7f5ce7c3b240></OutputValue></member></Outputs><Parameters></Parameters><Tag
s></Tags></member></Stacks></DescribeStacksResult></DescribeStacksResponse>'

../../../../.local/lib/python3.6/site-packages/botocore/parsers.py:458: ResponseParserError
=================================== 1 failed in 2.23s ====================================
```

It seems `Ref` for ddb tables outputs this: `<moto.dynamodb2.models.Table object at 0x7f5ce7c3b240>`, but I think it should output the name of the table: `MyTableName` as that's the behavior of AWS.

## **what version of Moto you're using**, and **how you installed it**

moto 1.3.14 installed with pip



Discussion:
Thanks for raising this and providing the exact test case @tigermonkey - I can reproduce this on master as well.

Marking it as a bug

Resolving patch:
diff --git a/moto/dynamodb2/models/__init__.py b/moto/dynamodb2/models/__init__.py
--- a/moto/dynamodb2/models/__init__.py
+++ b/moto/dynamodb2/models/__init__.py
@@ -413,6 +413,10 @@ def get_cfn_attribute(self, attribute_name):
 
         raise UnformattedGetAttTemplateException()
 
+    @property
+    def physical_resource_id(self):
+        return self.name
+
     @classmethod
     def create_from_cloudformation_json(
         cls, resource_name, cloudformation_json, region_name


Test patch:
diff --git a/tests/test_cloudformation/test_cloudformation_stack_integration.py b/tests/test_cloudformation/test_cloudformation_stack_integration.py
--- a/tests/test_cloudformation/test_cloudformation_stack_integration.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_integration.py
@@ -2767,3 +2767,39 @@ def test_stack_events_get_attribute_integration():
 
     output_arn["OutputValue"].should.equal(event_bus["Arn"])
     output_name["OutputValue"].should.equal(event_bus["Name"])
+
+
+@mock_cloudformation
+@mock_dynamodb2
+def test_dynamodb_table_creation():
+    CFN_TEMPLATE = {
+        "Outputs": {"MyTableName": {"Value": {"Ref": "MyTable"}},},
+        "Resources": {
+            "MyTable": {
+                "Type": "AWS::DynamoDB::Table",
+                "Properties": {
+                    "KeySchema": [{"AttributeName": "id", "KeyType": "HASH"}],
+                    "AttributeDefinitions": [
+                        {"AttributeName": "id", "AttributeType": "S"}
+                    ],
+                    "BillingMode": "PAY_PER_REQUEST",
+                },
+            },
+        },
+    }
+    stack_name = "foobar"
+    cfn = boto3.client("cloudformation", "us-west-2")
+    cfn.create_stack(StackName=stack_name, TemplateBody=json.dumps(CFN_TEMPLATE))
+    # Wait until moto creates the stack
+    waiter = cfn.get_waiter("stack_create_complete")
+    waiter.wait(StackName=stack_name)
+    # Verify the TableName is part of the outputs
+    stack = cfn.describe_stacks(StackName=stack_name)["Stacks"][0]
+    outputs = stack["Outputs"]
+    outputs.should.have.length_of(1)
+    outputs[0]["OutputKey"].should.equal("MyTableName")
+    outputs[0]["OutputValue"].should.contain("foobar")
+    # Assert the table is created
+    ddb = boto3.client("dynamodb", "us-west-2")
+    table_names = ddb.list_tables()["TableNames"]
+    table_names.should.equal([outputs[0]["OutputValue"]])