Repository: localstack/localstack

Issue:
Fn::GetAtt with S3 w/serverless-localstack
I have a simple S3 bucket 'MyBucket' declared alongside an IAM Role 'MyBucketRole', if I attempt to access the bucket's ARN via:
{ "Fn::GetAtt": ["MyBucket", "Arn"] }
it will throw:
Template error: resource MyBucket does not support attribute type Arn in Fn::GetAtt

However, if I attempt to access the ARN via:
{ "Fn::GetAtt": [{"Fn::GetAtt": ["MyBucket", "BucketName"]}, "Arn"] }
it works fine.

Under normal Serverless circumstance the first option works fine, am I being silly or is this a legitimate/known issue?

It also appears that the working version only works if the resource is already deployed. If attempting to launch both bucket and role attached to bucket at the same time, results in the following error:

ERROR:localstack.services.cloudformation.cloudformation_starter: Unable to parse and create resource "MyBucketRole": unhashable type: 'dict'

Serverless also throws a 500 error.

**The following log may also help**, 
This occurs when attempting to get the resource's ARN upon launching it in the same stack as the role attempting to reference it:

```
2019-09-09T13:22:54:ERROR:localstack.services.cloudformation.cloudformation_starter: Unable to parse and create resource "MyBucketRole": 400 Bad Request: <ErrorResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
  <Error>
    <Type>Sender</Type>
    <Code>ValidationError</Code>
    <Message>Template error: resource MyBucket does not support attribute type Arn in Fn::GetAtt</Message>
  </Error>
  <RequestId>cf4c737e-5ae2-11e4-a7c9-ad44eEXAMPLE</RequestId>
</ErrorResponse>
```


Discussion:
UPDATE: Seems as though it also happens with the exact same syntax and referencing a Kinesis Stream's ARN. I'm gonna make the assumption that this would happen with multiple resource types.
I am facing the same issue with localstack and serverless-localstack. Any update on this?

An update on this would be great as it's currently a blocker for me

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
@@ -399,6 +399,16 @@ def SQS_Queue_get_cfn_attribute(self, attribute_name):
     SQS_Queue_get_cfn_attribute_orig = sqs_models.Queue.get_cfn_attribute
     sqs_models.Queue.get_cfn_attribute = SQS_Queue_get_cfn_attribute
 
+    # Patch S3 Bucket get_cfn_attribute(..) method in moto
+
+    def S3_Bucket_get_cfn_attribute(self, attribute_name):
+        if attribute_name in ['Arn']:
+            return aws_stack.s3_bucket_arn(self.name)
+        return S3_Bucket_get_cfn_attribute_orig(self, attribute_name)
+
+    S3_Bucket_get_cfn_attribute_orig = s3_models.FakeBucket.get_cfn_attribute
+    s3_models.FakeBucket.get_cfn_attribute = S3_Bucket_get_cfn_attribute
+
     # Patch SQS physical_resource_id(..) method in moto
 
     @property
diff --git a/localstack/services/sns/sns_listener.py b/localstack/services/sns/sns_listener.py
--- a/localstack/services/sns/sns_listener.py
+++ b/localstack/services/sns/sns_listener.py
@@ -44,7 +44,7 @@ def forward_request(self, method, path, data, headers):
             # parse payload and extract fields
             req_data = urlparse.parse_qs(to_str(data))
             req_action = req_data['Action'][0]
-            topic_arn = req_data.get('TargetArn') or req_data.get('TopicArn')
+            topic_arn = req_data.get('TargetArn') or req_data.get('TopicArn') or req_data.get('ResourceArn')
 
             if topic_arn:
                 topic_arn = topic_arn[0]
@@ -97,14 +97,11 @@ def forward_request(self, method, path, data, headers):
                         content += '</member>'
                     content += '</Tags>'
                 return make_response(req_action, content=content)
+            elif req_action == 'CreateTopic':
+                topic_arn = aws_stack.sns_topic_arn(req_data['Name'][0])
+                self._extract_tags(topic_arn, req_data)
             elif req_action == 'TagResource':
-                tags = []
-                req_tags = {k: v for k, v in req_data.items() if k.startswith('Tags.member.')}
-                for i in range(int(len(req_tags.keys()) / 2)):
-                    key = req_tags['Tags.member.' + str(i + 1) + '.Key'][0]
-                    value = req_tags['Tags.member.' + str(i + 1) + '.Value'][0]
-                    tags.append({'Key': key, 'Value': value})
-                do_tag_resource(topic_arn, tags)
+                self._extract_tags(topic_arn, req_data)
                 return make_response(req_action)
             elif req_action == 'UntagResource':
                 tags_to_remove = []
@@ -120,6 +117,15 @@ def forward_request(self, method, path, data, headers):
 
         return True
 
+    def _extract_tags(self, topic_arn, req_data):
+        tags = []
+        req_tags = {k: v for k, v in req_data.items() if k.startswith('Tags.member.')}
+        for i in range(int(len(req_tags.keys()) / 2)):
+            key = req_tags['Tags.member.' + str(i + 1) + '.Key'][0]
+            value = req_tags['Tags.member.' + str(i + 1) + '.Value'][0]
+            tags.append({'Key': key, 'Value': value})
+        do_tag_resource(topic_arn, tags)
+
     def _reset_account_id(self, data):
         """ Fix account ID in request payload. All external-facing responses contain our
             predefined account ID (defaults to 000000000000), whereas the backend endpoint


Test patch:
diff --git a/tests/integration/templates/template1.yaml b/tests/integration/templates/template1.yaml
--- a/tests/integration/templates/template1.yaml
+++ b/tests/integration/templates/template1.yaml
@@ -19,9 +19,11 @@ Resources:
     Properties:
       TopicName: { "Fn::Join": [ "", [ { "Ref": "AWS::StackName" }, "-test-topic-1-1" ] ] }
       Tags:
-        - Key: foobar
+        - Key: foo
           Value:
             Ref: S3Bucket
+        - Key: bar
+          Value: { "Fn::GetAtt": ["S3Bucket", "Arn"] }
   TopicSubscription:
     Type: AWS::SNS::Subscription
     Properties:
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
@@ -137,16 +137,22 @@ def check_stack():
         # assert that queue has been created
         assert queue_exists('cf-test-queue-1')
         # assert that topic has been created
-        assert topic_exists('%s-test-topic-1-1' % stack_name)
+        topic_arn = topic_exists('%s-test-topic-1-1' % stack_name)
+        assert topic_arn
         # assert that stream has been created
         assert stream_exists('cf-test-stream-1')
         # assert that queue has been created
         resource = describe_stack_resource(stack_name, 'SQSQueueNoNameProperty')
         assert queue_exists(resource['PhysicalResourceId'])
 
-        # assert that topic tags have been created
+        # assert that tags have been created
         tags = s3.get_bucket_tagging(Bucket='cf-test-bucket-1')['TagSet']
         self.assertEqual(tags, [{'Key': 'foobar', 'Value': aws_stack.get_sqs_queue_url('cf-test-queue-1')}])
+        tags = sns.list_tags_for_resource(ResourceArn=topic_arn)['Tags']
+        self.assertEqual(tags, [
+            {'Key': 'foo', 'Value': 'cf-test-bucket-1'},
+            {'Key': 'bar', 'Value': aws_stack.s3_bucket_arn('cf-test-bucket-1')}
+        ])
         # assert that subscriptions have been created
         subs = sns.list_subscriptions()['Subscriptions']
         subs = [s for s in subs if (':%s:cf-test-queue-1' % TEST_AWS_ACCOUNT_ID) in s['Endpoint']]