Repository: localstack/localstack

Issue:
SNS missing ListTagsForResource implementation
We use terraform in combination with localstack for testing purposes and recently there has been an update the the AWS terraform provider (https://github.com/terraform-providers/terraform-provider-aws/blob/master/CHANGELOG.md#2110-may-17-2019) that adds tag support to the SNS resource. Unforntunately the change to the terraform aws provider breaks compatibly with localstack as it now uses the ListTagsForResource API on SNS which localstack replies with a HTTP 500.

Here is the terraform debug output that shows the issue:
```
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: 2019/05/19 22:44:48 [DEBUG] [aws-sdk-go] DEBUG: Request sns/ListTagsForResource Details:                                                                                                                        
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: ---[ REQUEST POST-SIGN ]-----------------------------                                                                                                                                                           
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: POST / HTTP/1.1                                                                                                                                                                                                 
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Host: localstack.local.halter.io:4575                                                                                                                                                                           
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: User-Agent: aws-sdk-go/1.19.30 (go1.12.2; linux; amd64) APN/1.0 HashiCorp/1.0 Terraform/0.12.0-rc1                                                                                                              
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Content-Length: 137                                                                                                                                                                                             
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Authorization: AWS4-HMAC-SHA256 Credential=fakeAccessKey/20190519/ap-southeast-2/sns/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date, Signature=6d096165364670ac519063d10e916b38f0428adc
ee4296b645bc24df1079e54b                                                                                                                                                                                                                                                                   
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Content-Type: application/x-www-form-urlencoded; charset=utf-8                                                                                                                                                  
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: X-Amz-Date: 20190519T224448Z                                                                                                                                                                                    
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Accept-Encoding: gzip                                                                                                                                                                                           
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4:                                                                                                                                                                                                                 
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Action=ListTagsForResource&ResourceArn=arn%3Aaws%3Asns%3Aap-southeast-2%3A123456789012%3Ahalter-core_push_notification&Version=2010-03-31                                                                       
2019-05-19T22:44:48.251Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: -----------------------------------------------------                                                                                                                                                           
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: 2019/05/19 22:44:48 [DEBUG] [aws-sdk-go] DEBUG: Response sns/ListTagsForResource Details:
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: ---[ RESPONSE ]--------------------------------------                                                                                                                                                           
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: HTTP/1.1 500 Internal Server Error      
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Content-Length: 290                                           
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Access-Control-Allow-Headers: authorization,content-type,content-md5,cache-control,x-amz-content-sha256,x-amz-date,x-amz-security-token,x-amz-user-agent         
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Access-Control-Allow-Methods: HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH                                                                                                               
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Access-Control-Allow-Origin: *                                                                                                                                                                                  
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Content-Type: text/html                                          
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Date: Sun, 19 May 2019 22:44:48 GMT                                                                                                                                                                             
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: Server: BaseHTTP/0.6 Python/3.6.8                                                                                                                                                                               
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4:                                                      
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4:                
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: -----------------------------------------------------
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: 2019/05/19 22:44:48 [DEBUG] [aws-sdk-go] <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">  
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: <title>500 Internal Server Error</title>                                                                                                                                                                        
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: <h1>Internal Server Error</h1>                                                                                                                                                                                  
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: <p>The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.</p>
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: 2019/05/19 22:44:48 [DEBUG] [aws-sdk-go] DEBUG: Validate Response sns/ListTagsForResource failed, not retrying, error SerializationError: failed to decode query XML error response                             
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4:      status code: 500, request id:                                                                                                                                                                              
2019-05-19T22:44:48.268Z [DEBUG] plugin.terraform-provider-aws_v2.11.0_x4: caused by: expected element type <ErrorResponse> but have <title>
```

Is there any plan to support this tags API for SNS in localstack? If so is there anything that I could do to help out?


Discussion:

Resolving patch:
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
@@ -17,6 +17,9 @@
 # mappings for SNS topic subscriptions
 SNS_SUBSCRIPTIONS = {}
 
+# mappings for SNS tags
+SNS_TAGS = {}
+
 # set up logger
 LOGGER = logging.getLogger(__name__)
 
@@ -77,6 +80,35 @@ def forward_request(self, method, path, data, headers):
                     publish_message(topic_arn, req_data)
                 # return response here because we do not want the request to be forwarded to SNS backend
                 return make_response(req_action)
+            elif req_action == 'ListTagsForResource':
+                tags = do_list_tags_for_resource(topic_arn)
+                content = '<Tags/>'
+                if len(tags) > 0:
+                    content = '<Tags>'
+                    for tag in tags:
+                        content += '<member>'
+                        content += '<Key>%s</Key>' % tag['Key']
+                        content += '<Value>%s</Value>' % tag['Value']
+                        content += '</member>'
+                    content += '</Tags>'
+                return make_response(req_action, content=content)
+            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)
+                return make_response(req_action)
+            elif req_action == 'UntagResource':
+                tags_to_remove = []
+                req_tags = {k: v for k, v in req_data.items() if k.startswith('TagKeys.member.')}
+                req_tags = req_tags.values()
+                for tag in req_tags:
+                    tags_to_remove.append(tag[0])
+                do_untag_resource(topic_arn, tags_to_remove)
+                return make_response(req_action)
 
             data = self._reset_account_id(data)
             return Request(data=data, headers=headers, method=method)
@@ -207,10 +239,28 @@ def do_unsubscribe(subscription_arn):
         ]
 
 
+def _get_tags(topic_arn):
+    if topic_arn not in SNS_TAGS:
+        SNS_TAGS[topic_arn] = []
+    return SNS_TAGS[topic_arn]
+
+
+def do_list_tags_for_resource(topic_arn):
+    return _get_tags(topic_arn)
+
+
+def do_tag_resource(topic_arn, tags):
+    _get_tags(topic_arn).extend(tags)
+
+
+def do_untag_resource(topic_arn, tag_keys):
+    SNS_TAGS[topic_arn] = [t for t in _get_tags(topic_arn) if t['Key'] not in tag_keys]
+
 # ---------------
 # HELPER METHODS
 # ---------------
 
+
 def get_topic_by_arn(topic_arn):
     return SNS_SUBSCRIPTIONS.get(topic_arn)
 


Test patch:
diff --git a/tests/integration/test_sns.py b/tests/integration/test_sns.py
--- a/tests/integration/test_sns.py
+++ b/tests/integration/test_sns.py
@@ -83,3 +83,37 @@ def test_publish_sms(self):
         response = self.sns_client.publish(PhoneNumber='+33000000000', Message='This is a SMS')
         self.assertTrue('MessageId' in response)
         self.assertEqual(response['ResponseMetadata']['HTTPStatusCode'], 200)
+
+    def test_tags(self):
+        self.sns_client.tag_resource(
+            ResourceArn=self.topic_arn,
+            Tags=[
+                {
+                    'Key': '123',
+                    'Value': 'abc'
+                },
+                {
+                    'Key': '456',
+                    'Value': 'def'
+                },
+            ]
+        )
+
+        tags = self.sns_client.list_tags_for_resource(ResourceArn=self.topic_arn)
+        self.assertEqual(len(tags['Tags']), 2)
+        self.assertEqual(tags['Tags'][0]['Key'], '123')
+        self.assertEqual(tags['Tags'][0]['Value'], 'abc')
+        self.assertEqual(tags['Tags'][1]['Key'], '456')
+        self.assertEqual(tags['Tags'][1]['Value'], 'def')
+
+        self.sns_client.untag_resource(
+            ResourceArn=self.topic_arn,
+            TagKeys=[
+                '123',
+            ]
+        )
+
+        tags = self.sns_client.list_tags_for_resource(ResourceArn=self.topic_arn)
+        self.assertEqual(len(tags['Tags']), 1)
+        self.assertEqual(tags['Tags'][0]['Key'], '456')
+        self.assertEqual(tags['Tags'][0]['Value'], 'def')