Repository: getmoto/moto

Issue:
Creating S3 via cloudformation: Bucket does not support attribute type Arn in Fn::GetAtt
Trying to create a mocked S3 bucket by just using its cloudformation, but it does not like `!GetAtt` within the template. You may be able to see the error just by looking at the exception below and the S3 template at the bottom, but I put my code so you can see what I am doing.

Error:
```
        if http.status_code >= 300:
            error_code = parsed_response.get("Error", {}).get("Code")
            error_class = self.exceptions.from_code(error_code)
>           raise error_class(parsed_response, operation_name)
E           botocore.exceptions.ClientError: An error occurred (ValidationError) when calling the CreateStack operation: Template error: resource DistilleryBucket does not support attribute type Arn in Fn::GetAtt 
```

conftest.py
```python
import pytest
import boto3
from moto import (
    mock_kms,
    mock_cloudformation,
    mock_dynamodb2,
    mock_s3
)

from core import context_modified_environ

@pytest.fixture(scope='session')
def drone_parameters():
    return {
        "Team": "test_team",
        "Unit": "team",
        "Owner": "somer_user",
        "Environment": "qa",
        "Product": "test_product",
        "Version": "1.0.0"
    }

@pytest.fixture(scope='function')
def aws_environment(context_modified_environ, drone_parameters):
    parameters = {
        'AWS_ACCESS_KEY_ID': 'testing',
        'AWS_SECRET_ACCESS_KEY': 'testing',
        'AWS_SECURITY_TOKEN': 'testing',
        'AWS_SESSION_TOKEN': 'testing',
        'AWS_DEFAULT_REGION': 'us-east-1',
        'Environment': 'qa', # Drone should add this
    }
    with context_modified_environ(**parameters):
        with mock_kms(), mock_cloudformation(), mock_dynamodb2(), mock_s3():
            cfn_client = boto3.client('cloudformation')
            cfn_resource = boto3.resource('cloudformation')
            outputs = {}

            # ===================
            # KMS
            # ===================
            with open ('templates/kms.yml', 'rb') as template_file:
                template_body = template_file.read().decode()
            stack_name = f"{drone_parameters['Environment']}-distillery-kms"
            cfn_client.create_stack(
                StackName=stack_name,
                TemplateBody=template_body,
                Parameters=[
                    *[{
                        'ParameterKey': key,
                        'ParameterValue': val
                    } for key, val in drone_parameters.items()]
                ],
            )
            outputs_list = cfn_resource.Stack(stack_name).outputs
            outputs['kms'] = {dic['OutputKey']: dic['OutputValue'] for dic in outputs_list}

            # ===========
            # S3
            # ===========
            with open ('templates/s3.yml', 'rb') as template_file:
                template_body = template_file.read().decode()
            stack_name = f"{drone_parameters['Environment']}-distillery-s3"
            cfn_client.create_stack(
                StackName=stack_name,
                TemplateBody=template_body,
                Parameters=[
                    {
                        'ParameterKey': 'KMSS3Key',
                        'ParameterValue': outputs['kms']['KMSS3Key'],
                    },
                    *[{
                        'ParameterKey': key,
                        'ParameterValue': val
                    } for key, val in drone_parameters.items()]
                ],
            )
            outputs_list = cfn_resource.Stack(stack_name).outputs
            outputs['s3'] = {dic['OutputKey']: dic['OutputValue'] for dic in outputs_list}

```
core.py, just to run my conftest. You I just do this to mock out environment variables.
```python
# Built In
import os
import contextlib

# 3rd Party
import pytest

# Owned

@contextlib.contextmanager
def modified_environ(*remove, **update):
    """
    https://stackoverflow.com/a/34333710/8150685
    Temporarily updates the ``os.environ`` dictionary in-place.

    The ``os.environ`` dictionary is updated in-place so that the modification
    is sure to work in all situations.

    :param remove: Environment variables to remove.
    :param update: Dictionary of environment variables and values to add/update.
    """
    env = os.environ
    update = update or {}
    remove = remove or []

    # List of environment variables being updated or removed.
    stomped = (set(update.keys()) | set(remove)) & set(env.keys())
    # Environment variables and values to restore on exit.
    update_after = {k: env[k] for k in stomped}
    # Environment variables and values to remove on exit.
    remove_after = frozenset(k for k in update if k not in env)

    try:
        env.update(update)
        [env.pop(k, None) for k in remove]
        yield
    finally:
        env.update(update_after)
        [env.pop(k) for k in remove_after]

@pytest.fixture(scope='session')
def context_modified_environ():
    """
    Used to patch environment variables for a test.
    with context_modified_environ(MY_ENV="Something"):
       pass
    """
    return modified_environ

```

kms.yml:
```yaml

# ==================================================================================================
# Metadata
# ==================================================================================================

Metadata:
  cfn-lint:
    config:
      ignore_checks:
        - W2001 # Ignore unused parameters

# ==================================================================================================
# Parameters
# ==================================================================================================

Parameters:
  Team:
    Type: String
  Unit:
    Type: String
  Owner:
    Type: String
  Environment:
    Type: String
  Product:
    Type: String
  Version:
    Type: String

# ==================================================================================================
# Resources
# ==================================================================================================

Resources:

  KMSBucketKey:
    Type: AWS::KMS::Key
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Properties:
      Description: Key used to encrypt distillery resources.
      EnableKeyRotation: true
      Enabled: true
      KeyPolicy:
        Statement:
          - Action: kms:*
            Effect: Allow
            Principal:
              AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root'
            Resource: '*'
          - Action:
              - kms:GenerateDataKey
              - kms:GenerateDataKeyWithoutPlaintext
              - kms:Encrypt
              - kms:ReEncryptFrom
              - kms:ReEncryptTo
              - kms:Decrypt
            Condition:
              StringEquals:
                kms:CallerAccount: !Ref 'AWS::AccountId'
                kms:ViaService: s3.amazonaws.com
            Effect: Allow
            Principal:
              AWS: '*'
            Resource: '*'
          - Action:
              - kms:CreateGrant
              - kms:ListGrants
              - kms:RevokeGrant
            Condition:
              Bool:
                kms:GrantIsForAWSResource: False
            Effect: Deny
            Principal: '*'
            Resource: '*'
      PendingWindowInDays: 7
  

  KMSDynamoDB: # https://docs.aws.amazon.com/kms/latest/developerguide/services-dynamodb.html
    Type: AWS::KMS::Key
    DeletionPolicy: Retain
    UpdateReplacePolicy: Retain
    Properties:
      Description: Encrypted Dynamo DB for Distillery
      Enabled: True
      EnableKeyRotation: Yes
      PendingWindowInDays: 7
      KeyPolicy:
        Version: 2012-10-17
        Statement:
          - Sid: Allow access through Amazon DynamoDB for all principals in the account that are authorized to use Amazon DynamoDB
            Effect: Allow
            Principal:
              AWS: "*"
            Action:
              - kms:GenerateDataKey
              - kms:GenerateDataKeyWithoutPlaintext
              - kms:Encrypt
              - kms:ReEncryptFrom
              - kms:ReEncryptTo
              - kms:Decrypt
            Resource: "*"
            Condition:
              StringLike:
                "kms:ViaService": dynamodb.*.amazonaws.com
          - Sid: Allow direct access to key metadata to the account
            Effect: Allow
            Principal:
              AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
            Action:
              - kms:RevokeGrant
              - kms:Create*
              - kms:Describe*
              - kms:Enable*
              - kms:List*
              - kms:Put*
              - kms:Update*
              - kms:Revoke*
              - kms:Disable*
              - kms:Get*
              - kms:Delete*
              - kms:ScheduleKeyDeletion
              - kms:CancelKeyDeletion
              - kms:TagResource
            Resource: "*"
          - Sid: Allow DynamoDB Service with service principal name dynamodb.amazonaws.com to describe the key directly
            Effect: Allow
            Principal:
              Service: dynamodb.amazonaws.com
            Action:
              - kms:Descrive
              - kms:Get*
              - kms:List
            Resource: "*"

# ==================================================================================================
# Outputs
# ==================================================================================================

Outputs:
  KMSS3ARN:
    Description: ARN for the KMS key used to encrypt the S3.
    Export:
      Name: !Sub ${AWS::StackName}:KMSS3ARN
    Value: !GetAtt KMSBucketKey.Arn
  KMSS3Key:
    Description: Key ID for the KMS key used to encrypt the S3.
    Export:
      Name: !Sub ${AWS::StackName}:KMSS3Key
    Value: !Ref KMSBucketKey
  KMSDynamoDBARN:
    Description: ARN for the KMS key used to encrypt the DynamoDB.
    Export:
      Name: !Sub ${AWS::StackName}:KMSDynamoDBARN
    Value: !GetAtt KMSDynamoDB.Arn
  KMSDynamoDBKey:
    Description: Key ID for the KMS key used to encrypt the DynamoDB.
    Export:
      Name: !Sub ${AWS::StackName}:KMSDynamoDBKey
    Value: !Ref KMSDynamoDB
```
s3.yml:
```yaml
# ==================
# Metadata
# ==================

Metadata:
  cfn-lint:
    config:
      ignore_checks:
        - W2001 # Ignore unused parameters

# =========================
# Parameters
# =========================

Parameters:
  Team:
    Type: String
  Unit:
    Type: String
  Owner:
    Type: String
  Environment:
    Type: String
  Product:
    Type: String
  Version:
    Type: String
  KMSS3Key:
    Type: String
    Description: ID of the KMS Key to be used by the bucket.

# ==================
# Resources
# ==================

Resources:

  DistilleryBucket:
    Type: AWS::S3::Bucket
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              KMSMasterKeyID: !Ref KMSS3Key
              SSEAlgorithm: aws:kms
      BucketName: !Sub ${Environment}-distillery-bucket
      LifecycleConfiguration:
        Rules:
          - Id: DefaultLifecycleRule
            Status: Enabled
            Transitions:
              - StorageClass: INTELLIGENT_TIERING
                TransitionInDays: 60
      VersioningConfiguration:
        Status: Enabled


# ================
# Outputs
# ================

Outputs:
  BucketARN:
    Description: ARN of the Distillery S3 Bucket.
    Export:
      Name: !Sub ${AWS::StackName}:BucketARN
    Value: !GetAtt DistilleryBucket.Arn
  BucketName:
    Description: Logical ID of Distillery S3 Bucket.
    Export:
      Name: !Sub ${AWS::StackName}:BucketName
    Value: !Ref DistilleryBucket

```


Discussion:

Resolving patch:
diff --git a/moto/s3/models.py b/moto/s3/models.py
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1065,6 +1065,8 @@ def get_cfn_attribute(self, attribute_name):
             raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "DomainName" ]"')
         elif attribute_name == "WebsiteURL":
             raise NotImplementedError('"Fn::GetAtt" : [ "{0}" , "WebsiteURL" ]"')
+        elif attribute_name == "Arn":
+            return self.arn
         raise UnformattedGetAttTemplateException()
 
     def set_acl(self, acl):


Test patch:
diff --git a/tests/test_s3/test_s3_cloudformation.py b/tests/test_s3/test_s3_cloudformation.py
--- a/tests/test_s3/test_s3_cloudformation.py
+++ b/tests/test_s3/test_s3_cloudformation.py
@@ -143,3 +143,37 @@ def test_s3_bucket_cloudformation_update_replacement():
     cf.update_stack(StackName="test_stack", TemplateBody=template_json)
     stack_description = cf.describe_stacks(StackName="test_stack")["Stacks"][0]
     s3.head_bucket(Bucket=stack_description["Outputs"][0]["OutputValue"])
+
+
+@mock_s3
+@mock_cloudformation
+def test_s3_bucket_cloudformation_outputs():
+    s3 = boto3.client("s3", region_name="us-east-1")
+    cf = boto3.resource("cloudformation", region_name="us-east-1")
+    stack_name = "test-stack"
+    bucket_name = "test-bucket"
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Resources": {
+            "TestBucket": {
+                "Type": "AWS::S3::Bucket",
+                "Properties": {"BucketName": bucket_name},
+            }
+        },
+        "Outputs": {
+            "BucketARN": {
+                "Value": {"Fn::GetAtt": ["TestBucket", "Arn"]},
+                "Export": {"Name": {"Fn::Sub": "${AWS::StackName}:BucketARN"}},
+            },
+            "BucketName": {
+                "Value": {"Ref": "TestBucket"},
+                "Export": {"Name": {"Fn::Sub": "${AWS::StackName}:BucketName"}},
+            },
+        },
+    }
+    cf.create_stack(StackName=stack_name, TemplateBody=json.dumps(template))
+    outputs_list = cf.Stack(stack_name).outputs
+    output = {item["OutputKey"]: item["OutputValue"] for item in outputs_list}
+    s3.head_bucket(Bucket=output["BucketName"])
+    output["BucketARN"].should.match("arn:aws:s3.+{bucket}".format(bucket=bucket_name))
+    output["BucketName"].should.equal(bucket_name)