Repository: ansible-collections/community.aws

Issue:
sns_topic: tagging support
### Summary

sns_topic doesn't not currently support resources tagging. It should have a `tags` parameter for AWS tags

### Issue Type

Feature Idea

### Component Name

sns_topic

### Additional Information

This issue was previously raised in the main ansible repo https://github.com/ansible/ansible/issues/65613 before the community migration but doesn't seems like it was raised here. There was also a PR for the issue https://github.com/ansible/ansible/pull/68287.

Similar to the previous proposal, I would like the ability to add tags dict as a parameter similar to `s3_bucket` and `ec2`

```yaml
-  sns_topic:
    name: "alarms"
    state: present
    tags:
     Environment: Production
```

### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:
Files identified in the description:
* [`plugins/modules/sns_topic.py`](https://github.com/['ansible-collections/amazon.aws', 'ansible-collections/community.aws', 'ansible-collections/community.vmware']/blob/main/plugins/modules/sns_topic.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @jillr @joelthompson @markuman @nand0p @s-hertel @tremble
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@dtnyn Thank you for bringing this. Would you be willing to open a PR with that feature here? For the required policies you should open a PR in this repo https://github.com/mattclay/aws-terminator.
Hi @alinabuzachis unfortunately I'm not looking to becoming a contributor, currently I have a work around by just using the awscli tagging as part of the playbook. I have only brought it up since it seems like it could be a good feature to have.

Resolving patch:
diff --git a/plugins/module_utils/sns.py b/plugins/module_utils/sns.py
--- a/plugins/module_utils/sns.py
+++ b/plugins/module_utils/sns.py
@@ -12,6 +12,10 @@
 from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict
+from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_aws_tags
 
 
 @AWSRetry.jittered_backoff()
@@ -87,6 +91,16 @@ def canonicalize_endpoint(protocol, endpoint):
     return endpoint
 
 
+def get_tags(client, module, topic_arn):
+    try:
+        return boto3_tag_list_to_ansible_dict(client.list_tags_for_resource(ResourceArn=topic_arn)['Tags'])
+    except is_boto3_error_code('AuthorizationError'):
+        module.warn("Permission denied accessing tags")
+        return {}
+    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Couldn't obtain topic tags")
+
+
 def get_info(connection, module, topic_arn):
     name = module.params.get('name')
     topic_type = module.params.get('topic_type')
@@ -121,5 +135,34 @@ def get_info(connection, module, topic_arn):
             info.update(camel_dict_to_snake_dict(connection.get_topic_attributes(TopicArn=topic_arn)['Attributes']))
             info['delivery_policy'] = info.pop('effective_delivery_policy')
         info['subscriptions'] = [camel_dict_to_snake_dict(sub) for sub in list_topic_subscriptions(connection, module, topic_arn)]
-
+        info["tags"] = get_tags(connection, module, topic_arn)
     return info
+
+
+def update_tags(client, module, topic_arn):
+
+    if module.params.get('tags') is None:
+        return False
+
+    existing_tags = get_tags(client, module, topic_arn)
+    to_update, to_delete = compare_aws_tags(existing_tags, module.params['tags'], module.params['purge_tags'])
+
+    if not bool(to_delete or to_update):
+        return False
+
+    if module.check_mode:
+        return True
+
+    if to_update:
+        try:
+            client.tag_resource(ResourceArn=topic_arn,
+                                Tags=ansible_dict_to_boto3_tag_list(to_update))
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg="Couldn't add tags to topic")
+    if to_delete:
+        try:
+            client.untag_resource(ResourceArn=topic_arn, TagKeys=to_delete)
+        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
+            module.fail_json_aws(e, msg="Couldn't remove tags from topic")
+
+    return True
diff --git a/plugins/modules/sns_topic.py b/plugins/modules/sns_topic.py
--- a/plugins/modules/sns_topic.py
+++ b/plugins/modules/sns_topic.py
@@ -12,8 +12,7 @@
 short_description: Manages AWS SNS topics and subscriptions
 version_added: 1.0.0
 description:
-    - The M(community.aws.sns_topic) module allows you to create, delete, and manage subscriptions for AWS SNS topics.
-    - As of 2.6, this module can be use to subscribe and unsubscribe to topics outside of your AWS account.
+  - The M(community.aws.sns_topic) module allows you to create, delete, and manage subscriptions for AWS SNS topics.
 author:
   - "Joel Thompson (@joelthompson)"
   - "Fernando Jose Pando (@nand0p)"
@@ -149,10 +148,13 @@
         Blame Amazon."
     default: true
     type: bool
+notes:
+  - Support for I(tags) and I(purge_tags) was added in release 5.3.0.
 extends_documentation_fragment:
-- amazon.aws.aws
-- amazon.aws.ec2
-- amazon.aws.boto3
+  - amazon.aws.aws
+  - amazon.aws.ec2
+  - amazon.aws.tags
+  - amazon.aws.boto3
 '''
 
 EXAMPLES = r"""
@@ -328,12 +330,14 @@
 from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
 from ansible_collections.amazon.aws.plugins.module_utils.core import scrub_none_parameters
 from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_policies
+from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list
 from ansible_collections.community.aws.plugins.module_utils.sns import list_topics
 from ansible_collections.community.aws.plugins.module_utils.sns import topic_arn_lookup
 from ansible_collections.community.aws.plugins.module_utils.sns import compare_delivery_policies
 from ansible_collections.community.aws.plugins.module_utils.sns import list_topic_subscriptions
 from ansible_collections.community.aws.plugins.module_utils.sns import canonicalize_endpoint
 from ansible_collections.community.aws.plugins.module_utils.sns import get_info
+from ansible_collections.community.aws.plugins.module_utils.sns import update_tags
 
 
 class SnsTopicManager(object):
@@ -349,6 +353,8 @@ def __init__(self,
                  delivery_policy,
                  subscriptions,
                  purge_subscriptions,
+                 tags,
+                 purge_tags,
                  check_mode):
 
         self.connection = module.client('sns')
@@ -371,6 +377,8 @@ def __init__(self,
         self.topic_deleted = False
         self.topic_arn = None
         self.attributes_set = []
+        self.tags = tags
+        self.purge_tags = purge_tags
 
     def _create_topic(self):
         attributes = {}
@@ -383,6 +391,9 @@ def _create_topic(self):
             if not self.name.endswith('.fifo'):
                 self.name = self.name + '.fifo'
 
+        if self.tags:
+            tags = ansible_dict_to_boto3_tag_list(self.tags)
+
         if not self.check_mode:
             try:
                 response = self.connection.create_topic(Name=self.name,
@@ -542,12 +553,13 @@ def ensure_ok(self):
         elif self.display_name or self.policy or self.delivery_policy:
             self.module.fail_json(msg="Cannot set display name, policy or delivery policy for SNS topics not owned by this account")
         changed |= self._set_topic_subs()
-
         self._init_desired_subscription_attributes()
         if self.topic_arn in list_topics(self.connection, self.module):
             changed |= self._set_topic_subs_attributes()
         elif any(self.desired_subscription_attributes.values()):
             self.module.fail_json(msg="Cannot set subscription attributes for SNS topics not owned by this account")
+        # Check tagging
+        changed |= update_tags(self.connection, self.module, self.topic_arn)
 
         return changed
 
@@ -600,6 +612,8 @@ def main():
         delivery_policy=dict(type='dict', options=delivery_args),
         subscriptions=dict(default=[], type='list', elements='dict'),
         purge_subscriptions=dict(type='bool', default=True),
+        tags=dict(type='dict', aliases=['resource_tags']),
+        purge_tags=dict(type='bool', default=True),
     )
 
     module = AnsibleAWSModule(argument_spec=argument_spec,
@@ -614,6 +628,8 @@ def main():
     subscriptions = module.params.get('subscriptions')
     purge_subscriptions = module.params.get('purge_subscriptions')
     check_mode = module.check_mode
+    tags = module.params.get('tags')
+    purge_tags = module.params.get('purge_tags')
 
     sns_topic = SnsTopicManager(module,
                                 name,
@@ -624,6 +640,8 @@ def main():
                                 delivery_policy,
                                 subscriptions,
                                 purge_subscriptions,
+                                tags,
+                                purge_tags,
                                 check_mode)
 
     if state == 'present':


Test patch:
diff --git a/tests/integration/targets/sns_topic/tasks/main.yml b/tests/integration/targets/sns_topic/tasks/main.yml
--- a/tests/integration/targets/sns_topic/tasks/main.yml
+++ b/tests/integration/targets/sns_topic/tasks/main.yml
@@ -14,13 +14,9 @@
       create_instance_profile: false
       managed_policies:
       - 'arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess'
+      wait: True
     register: iam_role
 
-  - name: pause if role was created
-    pause:
-      seconds: 10
-    when: iam_role is changed
-
   - name: list all the topics (check_mode)
     sns_topic_info:
     check_mode: true
@@ -428,6 +424,170 @@
       - third_party_deletion is failed
       - third_party_topic.sns_topic.subscriptions|length == third_party_deletion_facts.sns_topic.subscriptions|length
 
+  # Test tags
+  - name: create standard SNS topic
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      display_name: My topic name
+    register: sns_topic_create
+
+  - name: assert that creation worked
+    assert:
+      that:
+      - sns_topic_create.changed
+
+  - name: set sns_arn fact
+    set_fact:
+      sns_arn: '{{ sns_topic_create.sns_arn }}'
+
+  - name: Add tags to topic - CHECK_MODE
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      tags:
+        tag_one: '{{ tiny_prefix }} One'
+        "Tag Two": 'two {{ tiny_prefix }}'
+    check_mode: true
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Add tags to topic
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      tags:
+        tag_one: '{{ tiny_prefix }} One'
+        "Tag Two": 'two {{ tiny_prefix }}'
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Add tags to topic to verify idempotency - CHECK_MODE
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      tags:
+        tag_one: '{{ tiny_prefix }} One'
+        "Tag Two": 'two {{ tiny_prefix }}'
+    check_mode: true
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is not changed
+
+  - name: Add tags to topic to verify idempotency
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      tags:
+        tag_one: '{{ tiny_prefix }} One'
+        "Tag Two": 'two {{ tiny_prefix }}'
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is not changed
+
+  - name: Update (add/remove) tags - CHECK_MODE
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      tags:
+        tag_three: '{{ tiny_prefix }} Three'
+        "Tag Two": 'two {{ tiny_prefix }}'
+    check_mode: true
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Update tags to verify idempotency
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      tags:
+        tag_three: '{{ tiny_prefix }} Three'
+        "Tag Two": 'two {{ tiny_prefix }}'
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Update tags without purge - CHECK_MODE
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      purge_tags: no
+      tags:
+        tag_one: '{{ tiny_prefix }} One'
+    check_mode: true
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Update tags without purge
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      purge_tags: no
+      tags:
+        tag_one: '{{ tiny_prefix }} One'
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Remove all the tags - CHECK_MODE
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      purge_tags: yes
+      tags: {}
+    check_mode: true
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Remove all the tags
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      purge_tags: yes
+      tags: {}
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Update with CamelCase tags
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      purge_tags: no
+      tags:
+        "lowercase spaced": 'hello cruel world'
+        "Title Case": 'Hello Cruel World'
+        CamelCase: 'SimpleCamelCase'
+        snake_case: 'simple_snake_case'
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is changed
+
+  - name: Do not specify any tag to ensure previous tags are not removed
+    sns_topic:
+      name: '{{ sns_topic_topic_name }}'
+      purge_tags: no
+    register: sns_topic_tags
+
+  - assert:
+      that:
+      - sns_topic_tags is not changed
+
   always:
 
   - name: announce teardown start