diff --git a/changelogs/fragments/972-sns_topic-add_tags.yaml b/changelogs/fragments/972-sns_topic-add_tags.yaml new file mode 100644 index 00000000000..be2690a9fca --- /dev/null +++ b/changelogs/fragments/972-sns_topic-add_tags.yaml @@ -0,0 +1,2 @@ +minor_changes: +- sns_topic - add support for ``tags`` and ``purge_tags`` (https://github.com/ansible-collections/community.aws/pull/972). diff --git a/plugins/module_utils/sns.py b/plugins/module_utils/sns.py index 27ab8773531..17fc136a1d6 100644 --- 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 index 8ef63690fea..f8e44358cb7 100644 --- 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': diff --git a/tests/integration/targets/sns_topic/tasks/main.yml b/tests/integration/targets/sns_topic/tasks/main.yml index b60acec62b8..5465aad4fdb 100644 --- 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