From d3e0d157a5dcd98bafdae58a8506f4d76398ea08 Mon Sep 17 00:00:00 2001 From: Mark Chappell Date: Fri, 3 Jun 2022 13:15:00 +0200 Subject: [PATCH] wafv2_ip_set - add support for updating tags --- .../fragments/1205-tagging-wafv2_ip_set.yml | 5 + plugins/module_utils/wafv2.py | 61 ++++- plugins/modules/wafv2_ip_set.py | 49 ++-- plugins/modules/wafv2_ip_set_info.py | 8 +- .../targets/wafv2_ip_set/tasks/main.yml | 3 +- .../targets/wafv2_ip_set/tasks/tagging.yml | 256 ++++++++++++++++++ 6 files changed, 357 insertions(+), 25 deletions(-) create mode 100644 changelogs/fragments/1205-tagging-wafv2_ip_set.yml create mode 100644 tests/integration/targets/wafv2_ip_set/tasks/tagging.yml diff --git a/changelogs/fragments/1205-tagging-wafv2_ip_set.yml b/changelogs/fragments/1205-tagging-wafv2_ip_set.yml new file mode 100644 index 00000000000..8a09659e7ed --- /dev/null +++ b/changelogs/fragments/1205-tagging-wafv2_ip_set.yml @@ -0,0 +1,5 @@ +minor_changes: +- wafv2_ip_set - Added support for ``purge_tags`` parameter (https://github.com/ansible-collections/community.aws/pull/1205). +- wafv2_ip_set - Added support for updating tags (https://github.com/ansible-collections/community.aws/pull/1205). +- wafv2_ip_set - Added support for returning tags (https://github.com/ansible-collections/community.aws/pull/1205). +- wafv2_ip_set_info - Added support for returning tags (https://github.com/ansible-collections/community.aws/pull/1205). diff --git a/plugins/module_utils/wafv2.py b/plugins/module_utils/wafv2.py index 6777265fb93..18f19974daf 100644 --- a/plugins/module_utils/wafv2.py +++ b/plugins/module_utils/wafv2.py @@ -6,6 +6,63 @@ except ImportError: pass # caught by AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict +from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags + + +@AWSRetry.jittered_backoff() +def _list_tags(wafv2, arn, fail_json_aws, next_marker=None): + params = dict(ResourceARN=arn) + if next_marker: + params['NextMarker'] = next_marker + try: + return wafv2.list_tags_for_resource(**params) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to list wafv2 tags") + + +def describe_wafv2_tags(wafv2, arn, fail_json_aws): + next_marker = None + tag_list = [] + # there is currently no paginator for wafv2 + while True: + responce = _list_tags(wafv2, arn, fail_json_aws) + next_marker = responce.get('NextMarker', None) + tag_info = responce.get('TagInfoForResource', {}) + tag_list.extend(tag_info.get('TagList', [])) + if not next_marker: + break + return boto3_tag_list_to_ansible_dict(tag_list) + + +def ensure_wafv2_tags(wafv2, arn, tags, purge_tags, fail_json_aws, check_mode): + if tags is None: + return False + + current_tags = describe_wafv2_tags(wafv2, arn, fail_json_aws) + tags_to_add, tags_to_remove = compare_aws_tags(current_tags, tags, purge_tags) + if not tags_to_add and not tags_to_remove: + return False + + if check_mode: + return True + + if tags_to_add: + try: + boto3_tags = ansible_dict_to_boto3_tag_list(tags_to_add) + wafv2.tag_resource(ResourceARN=arn, Tags=boto3_tags) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to add wafv2 tags") + if tags_to_remove: + try: + wafv2.untag_resource(ResourceARN=arn, TagKeys=tags_to_remove) + except (BotoCoreError, ClientError) as e: + fail_json_aws(e, msg="Failed to remove wafv2 tags") + + return True + def wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=None): # there is currently no paginator for wafv2 @@ -19,7 +76,7 @@ def wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=None): try: response = wafv2.list_web_acls(**req_obj) except (BotoCoreError, ClientError) as e: - fail_json_aws(e, msg="Failed to list wafv2 web acl.") + fail_json_aws(e, msg="Failed to list wafv2 web acl") if response.get('NextMarker'): response['WebACLs'] += wafv2_list_web_acls(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('WebACLs') @@ -38,7 +95,7 @@ def wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=None): try: response = wafv2.list_rule_groups(**req_obj) except (BotoCoreError, ClientError) as e: - fail_json_aws(e, msg="Failed to list wafv2 rule group.") + fail_json_aws(e, msg="Failed to list wafv2 rule group") if response.get('NextMarker'): response['RuleGroups'] += wafv2_list_rule_groups(wafv2, scope, fail_json_aws, nextmarker=response.get('NextMarker')).get('RuleGroups') diff --git a/plugins/modules/wafv2_ip_set.py b/plugins/modules/wafv2_ip_set.py index 1efaf31f77a..add677eba1f 100644 --- a/plugins/modules/wafv2_ip_set.py +++ b/plugins/modules/wafv2_ip_set.py @@ -53,21 +53,19 @@ from the IP set. The entire IP set itself will stay present. type: list elements: str - tags: - description: - - Key value pairs to associate with the resource. - - Currently tags are not visible. Nor in the web ui, nor via cli and nor in boto3. - required: false - type: dict purge_addresses: description: - When set to C(no), keep the existing addresses in place. Will modify and add, but will not delete. default: yes type: bool +notes: + - Support for I(purge_tags) was added in release 4.0.0. + extends_documentation_fragment: -- amazon.aws.aws -- amazon.aws.ec2 + - amazon.aws.aws + - amazon.aws.ec2 + - amazon.aws.tags ''' @@ -125,6 +123,8 @@ from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule 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 camel_dict_to_snake_dict +from ansible_collections.community.aws.plugins.module_utils.wafv2 import describe_wafv2_tags +from ansible_collections.community.aws.plugins.module_utils.wafv2 import ensure_wafv2_tags class IpSet: @@ -133,15 +133,18 @@ def __init__(self, wafv2, name, scope, fail_json_aws): self.name = name self.scope = scope self.fail_json_aws = fail_json_aws - self.existing_set, self.id, self.locktoken = self.get_set() + self.existing_set, self.id, self.locktoken, self.arn = self.get_set() def description(self): return self.existing_set.get('Description') + def _format_set(self, ip_set): + if ip_set is None: + return None + return camel_dict_to_snake_dict(self.existing_set, ignore_list=['tags']) + def get(self): - if self.existing_set: - return camel_dict_to_snake_dict(self.existing_set) - return None + return self._format_set(self.existing_set) def remove(self): try: @@ -174,8 +177,8 @@ def create(self, description, ip_address_version, addresses, tags): except (BotoCoreError, ClientError) as e: self.fail_json_aws(e, msg="Failed to create wafv2 ip set.") - self.existing_set, self.id, self.locktoken = self.get_set() - return camel_dict_to_snake_dict(self.existing_set) + self.existing_set, self.id, self.locktoken, self.arn = self.get_set() + return self._format_set(self.existing_set) def update(self, description, addresses): req_obj = { @@ -194,13 +197,14 @@ def update(self, description, addresses): except (BotoCoreError, ClientError) as e: self.fail_json_aws(e, msg="Failed to update wafv2 ip set.") - self.existing_set, self.id, self.locktoken = self.get_set() - return camel_dict_to_snake_dict(self.existing_set) + self.existing_set, self.id, self.locktoken, self.arn = self.get_set() + return self._format_set(self.existing_set) def get_set(self): response = self.list() existing_set = None id = None + arn = None locktoken = None for item in response.get('IPSets'): if item.get('Name') == self.name: @@ -216,8 +220,10 @@ def get_set(self): ).get('IPSet') except (BotoCoreError, ClientError) as e: self.fail_json_aws(e, msg="Failed to get wafv2 ip set.") + tags = describe_wafv2_tags(self.wafv2, arn, self.fail_json_aws) + existing_set['tags'] = tags - return existing_set, id, locktoken + return existing_set, id, locktoken, arn def list(self, Nextmarker=None): # there is currently no paginator for wafv2 @@ -275,8 +281,9 @@ def main(): description=dict(type='str'), ip_address_version=dict(type='str', choices=['IPV4', 'IPV6']), addresses=dict(type='list', elements='str'), - tags=dict(type='dict'), - purge_addresses=dict(type='bool', default=True) + tags=dict(type='dict', aliases=['resource_tags']), + purge_tags=dict(type='bool', default=True), + purge_addresses=dict(type='bool', default=True), ) module = AnsibleAWSModule( @@ -292,6 +299,7 @@ def main(): ip_address_version = module.params.get("ip_address_version") addresses = module.params.get("addresses") tags = module.params.get("tags") + purge_tags = module.params.get("purge_tags") purge_addresses = module.params.get("purge_addresses") check_mode = module.check_mode @@ -303,7 +311,9 @@ def main(): ip_set = IpSet(wafv2, name, scope, module.fail_json_aws) if state == 'present': + if ip_set.get(): + tags_updated = ensure_wafv2_tags(wafv2, ip_set.arn, tags, purge_tags, module.fail_json_aws, module.check_mode) change, addresses = compare(ip_set.get(), addresses, purge_addresses, state) if (change or ip_set.description() != description) and not check_mode: retval = ip_set.update( @@ -312,6 +322,7 @@ def main(): ) else: retval = ip_set.get() + change |= tags_updated else: if not check_mode: retval = ip_set.create( diff --git a/plugins/modules/wafv2_ip_set_info.py b/plugins/modules/wafv2_ip_set_info.py index 0c2bf3f0e4e..6e3c1075257 100644 --- a/plugins/modules/wafv2_ip_set_info.py +++ b/plugins/modules/wafv2_ip_set_info.py @@ -77,6 +77,7 @@ from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict +from ansible_collections.community.aws.plugins.module_utils.wafv2 import describe_wafv2_tags def list_ip_sets(wafv2, scope, fail_json_aws, Nextmarker=None): @@ -93,7 +94,7 @@ def list_ip_sets(wafv2, scope, fail_json_aws, Nextmarker=None): if response.get('NextMarker'): response['IPSets'] += list_ip_sets(wafv2, scope, fail_json_aws, Nextmarker=response.get('NextMarker')).get('IPSets') except (BotoCoreError, ClientError) as e: - fail_json_aws(e, msg="Failed to list wafv2 ip set.") + fail_json_aws(e, msg="Failed to list wafv2 ip set") return response @@ -105,7 +106,7 @@ def get_ip_set(wafv2, name, scope, id, fail_json_aws): Id=id ) except (BotoCoreError, ClientError) as e: - fail_json_aws(e, msg="Failed to get wafv2 ip set.") + fail_json_aws(e, msg="Failed to get wafv2 ip set") return response @@ -134,13 +135,14 @@ def main(): for item in response.get('IPSets'): if item.get('Name') == name: id = item.get('Id') + arn = item.get('ARN') retval = {} existing_set = None if id: existing_set = get_ip_set(wafv2, name, scope, id, module.fail_json_aws) retval = camel_dict_to_snake_dict(existing_set.get('IPSet')) - + retval['tags'] = describe_wafv2_tags(wafv2, arn, module.fail_json_aws) or {} module.exit_json(**retval) diff --git a/tests/integration/targets/wafv2_ip_set/tasks/main.yml b/tests/integration/targets/wafv2_ip_set/tasks/main.yml index 6436fc81cc4..51d15cc08dc 100644 --- a/tests/integration/targets/wafv2_ip_set/tasks/main.yml +++ b/tests/integration/targets/wafv2_ip_set/tasks/main.yml @@ -5,7 +5,6 @@ aws_secret_key: "{{ aws_secret_key }}" security_token: "{{ security_token | default(omit) }}" region: "{{ aws_region }}" - block: - name: check_mode create ip set wafv2_ip_set: @@ -177,6 +176,8 @@ that: - out.addresses | count == 1 + - include_tasks: 'tagging.yml' + - name: delete ip set wafv2_ip_set: name: "{{ ip_set_name }}" diff --git a/tests/integration/targets/wafv2_ip_set/tasks/tagging.yml b/tests/integration/targets/wafv2_ip_set/tasks/tagging.yml new file mode 100644 index 00000000000..d6125b32e2f --- /dev/null +++ b/tests/integration/targets/wafv2_ip_set/tasks/tagging.yml @@ -0,0 +1,256 @@ +- name: Tests relating to tagging wavf2_ip_sets + vars: + first_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + second_tags: + 'New Key with Spaces': Value with spaces + NewCamelCaseKey: CamelCaseValue + newPascalCaseKey: pascalCaseValue + new_snake_case_key: snake_case_value + third_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + 'New Key with Spaces': Updated Value with spaces + final_tags: + 'Key with Spaces': Value with spaces + CamelCaseKey: CamelCaseValue + pascalCaseKey: pascalCaseValue + snake_case_key: snake_case_value + 'New Key with Spaces': Updated Value with spaces + NewCamelCaseKey: CamelCaseValue + newPascalCaseKey: pascalCaseValue + new_snake_case_key: snake_case_value + # Mandatory settings + module_defaults: + community.aws.wafv2_ip_set: + name: '{{ ip_set_name }}' + state: present + scope: REGIONAL + ip_address_version: IPV4 + purge_addresses: no + addresses: [] + community.aws.wafv2_ip_set_info: + name: '{{ ip_set_name }}' + scope: REGIONAL + block: + + ### + + - name: test adding tags to wafv2_ip_set (check mode) + wafv2_ip_set: + tags: '{{ first_tags }}' + purge_tags: True + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is changed + + - name: test adding tags to wafv2_ip_set + wafv2_ip_set: + tags: '{{ first_tags }}' + purge_tags: True + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is changed + - update_result.tags == first_tags + + - name: test adding tags to wafv2_ip_set - idempotency (check mode) + wafv2_ip_set: + tags: '{{ first_tags }}' + purge_tags: True + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is not changed + + - name: test adding tags to wafv2_ip_set - idempotency + wafv2_ip_set: + tags: '{{ first_tags }}' + purge_tags: True + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is not changed + - update_result.tags == first_tags + + ### + + - name: test updating tags with purge on wafv2_ip_set (check mode) + wafv2_ip_set: + tags: '{{ second_tags }}' + purge_tags: True + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is changed + + - name: test updating tags with purge on wafv2_ip_set + wafv2_ip_set: + tags: '{{ second_tags }}' + purge_tags: True + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is changed + - update_result.tags == second_tags + + - name: test updating tags with purge on wafv2_ip_set - idempotency (check mode) + wafv2_ip_set: + tags: '{{ second_tags }}' + purge_tags: True + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is not changed + + - name: test updating tags with purge on wafv2_ip_set - idempotency + wafv2_ip_set: + tags: '{{ second_tags }}' + purge_tags: True + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is not changed + - update_result.tags == second_tags + + ### + + - name: test updating tags without purge on wafv2_ip_set (check mode) + wafv2_ip_set: + tags: '{{ third_tags }}' + purge_tags: False + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is changed + + - name: test updating tags without purge on wafv2_ip_set + wafv2_ip_set: + tags: '{{ third_tags }}' + purge_tags: False + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is changed + - update_result.tags == final_tags + + - name: test updating tags without purge on wafv2_ip_set - idempotency (check mode) + wafv2_ip_set: + tags: '{{ third_tags }}' + purge_tags: False + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is not changed + + - name: test updating tags without purge on wafv2_ip_set - idempotency + wafv2_ip_set: + tags: '{{ third_tags }}' + purge_tags: False + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is not changed + - update_result.tags == final_tags + + ### + + - name: test that wafv2_ip_set_info returns the tags + wafv2_ip_set_info: + register: tag_info + - name: assert tags present + assert: + that: + - tag_info.tags == final_tags + + ### + + - name: test no tags param wafv2_ip_set (check mode) + wafv2_ip_set: {} + register: update_result + check_mode: yes + - name: assert no change + assert: + that: + - update_result is not changed + - update_result.tags == final_tags + + + - name: test no tags param wafv2_ip_set + wafv2_ip_set: {} + register: update_result + - name: assert no change + assert: + that: + - update_result is not changed + - update_result.tags == final_tags + + ### + + - name: test removing tags from wafv2_ip_set (check mode) + wafv2_ip_set: + tags: {} + purge_tags: True + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is changed + + - name: test removing tags from wafv2_ip_set + wafv2_ip_set: + tags: {} + purge_tags: True + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is changed + - update_result.tags == {} + + - name: test removing tags from wafv2_ip_set - idempotency (check mode) + wafv2_ip_set: + tags: {} + purge_tags: True + register: update_result + check_mode: yes + - name: assert that update succeeded + assert: + that: + - update_result is not changed + + - name: test removing tags from wafv2_ip_set - idempotency + wafv2_ip_set: + tags: {} + purge_tags: True + register: update_result + - name: assert that update succeeded + assert: + that: + - update_result is not changed + - update_result.tags == {}