From b3b09f800d2b864f239dfbb6be336235ddd559e1 Mon Sep 17 00:00:00 2001 From: Mandar Kulkarni Date: Thu, 10 Mar 2022 18:52:45 -0800 Subject: [PATCH] ec2_asg: Add purge_tags to AutoScalingGroups. (#960) ec2_asg: Add purge_tags to AutoScalingGroups. SUMMARY Add purge_tags to ec2_asg module. Fixes #481. ISSUE TYPE Feature Pull Request COMPONENT NAME ec2_asg ADDITIONAL INFORMATION There was another PR (currently closed) #482 - with similar functionality but I'm not sure if having modules to handle tags for individual services/modules is a good way to have this functionality. It will certainly cause increase in number of modules. Hence tried modifying existing ec2_asg module to be able to do this. This utilizes underlying API calls to: https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_DescribeTags.html https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_CreateOrUpdateTags.html https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_DeleteTags.html Reviewed-by: Alina Buzachis Reviewed-by: Jill R Reviewed-by: Mandar Kulkarni --- .../fragments/960-ec2_asg-purge-tags.yml | 2 + plugins/modules/ec2_asg.py | 23 +- .../targets/ec2_asg/tasks/instance_detach.yml | 4 +- .../targets/ec2_asg/tasks/main.yml | 58 +-- .../targets/ec2_asg/tasks/tag_operations.yml | 352 ++++++++++++++++++ 5 files changed, 377 insertions(+), 62 deletions(-) create mode 100644 changelogs/fragments/960-ec2_asg-purge-tags.yml create mode 100644 tests/integration/targets/ec2_asg/tasks/tag_operations.yml diff --git a/changelogs/fragments/960-ec2_asg-purge-tags.yml b/changelogs/fragments/960-ec2_asg-purge-tags.yml new file mode 100644 index 00000000000..064264dfb6a --- /dev/null +++ b/changelogs/fragments/960-ec2_asg-purge-tags.yml @@ -0,0 +1,2 @@ +minor_changes: +- ec2_asg - add support for ``purge_tags`` to ec2_asg (https://github.com/ansible-collections/community.aws/pull/960). diff --git a/plugins/modules/ec2_asg.py b/plugins/modules/ec2_asg.py index 8dc7cd783f2..fa91232cbe6 100644 --- a/plugins/modules/ec2_asg.py +++ b/plugins/modules/ec2_asg.py @@ -220,6 +220,13 @@ - When I(propagate_at_launch) is true the tags will be propagated to the Instances created. type: list elements: dict + purge_tags: + description: + - If C(true), existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. + - If the I(tags) parameter is not set then tags will not be modified. + default: true + type: bool + version_added: 3.2.0 health_check_period: description: - Length of time in seconds after a new EC2 instance comes into service that Auto Scaling starts checking its health. @@ -645,6 +652,7 @@ from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict 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_filter_list ASG_ATTRIBUTES = ('AvailabilityZones', 'DefaultCooldown', 'DesiredCapacity', 'HealthCheckGracePeriod', 'HealthCheckType', 'LaunchConfigurationName', @@ -1097,6 +1105,7 @@ def create_autoscaling_group(connection): desired_capacity = module.params.get('desired_capacity') vpc_zone_identifier = module.params.get('vpc_zone_identifier') set_tags = module.params.get('tags') + purge_tags = module.params.get('purge_tags') health_check_period = module.params.get('health_check_period') health_check_type = module.params.get('health_check_type') default_cooldown = module.params.get('default_cooldown') @@ -1205,9 +1214,12 @@ def create_autoscaling_group(connection): changed = True # process tag changes + have_tags = as_group.get('Tags') + want_tags = asg_tags + if purge_tags and not want_tags and have_tags: + connection.delete_tags(Tags=list(have_tags)) + if len(set_tags) > 0: - have_tags = as_group.get('Tags') - want_tags = asg_tags if have_tags: have_tags.sort(key=lambda x: x["Key"]) if want_tags: @@ -1218,9 +1230,11 @@ def create_autoscaling_group(connection): for dead_tag in set(have_tag_keyvals).difference(want_tag_keyvals): changed = True - dead_tags.append(dict(ResourceId=as_group['AutoScalingGroupName'], - ResourceType='auto-scaling-group', Key=dead_tag)) + if purge_tags: + dead_tags.append(dict( + ResourceId=as_group['AutoScalingGroupName'], ResourceType='auto-scaling-group', Key=dead_tag)) have_tags = [have_tag for have_tag in have_tags if have_tag['Key'] != dead_tag] + if dead_tags: connection.delete_tags(Tags=dead_tags) @@ -1838,6 +1852,7 @@ def main(): wait_timeout=dict(type='int', default=300), state=dict(default='present', choices=['present', 'absent']), tags=dict(type='list', default=[], elements='dict'), + purge_tags=dict(type='bool', default=True), health_check_period=dict(type='int', default=300), health_check_type=dict(default='EC2', choices=['EC2', 'ELB']), default_cooldown=dict(type='int', default=300), diff --git a/tests/integration/targets/ec2_asg/tasks/instance_detach.yml b/tests/integration/targets/ec2_asg/tasks/instance_detach.yml index da574c2ebf2..fbd29d39640 100644 --- a/tests/integration/targets/ec2_asg/tasks/instance_detach.yml +++ b/tests/integration/targets/ec2_asg/tasks/instance_detach.yml @@ -83,7 +83,7 @@ - '{{ init_instance_2 }}' # pause to allow completion of instance replacement - - name: Pause for 1 minute + - name: Pause for 30 seconds pause: seconds: 30 @@ -137,7 +137,7 @@ - '{{ instance_replace_1 }}' - '{{ instance_replace_2 }}' - - name: Pause for 1 minute to allow completion of above task + - name: Pause for 30 seconds to allow completion of above task pause: seconds: 30 diff --git a/tests/integration/targets/ec2_asg/tasks/main.yml b/tests/integration/targets/ec2_asg/tasks/main.yml index 0fc14cceb08..d26e15b068c 100644 --- a/tests/integration/targets/ec2_asg/tasks/main.yml +++ b/tests/integration/targets/ec2_asg/tasks/main.yml @@ -105,6 +105,8 @@ cidr_ip: 0.0.0.0/0 register: sg + - include_tasks: tag_operations.yml + - include_tasks: instance_detach.yml - name: ensure launch configs exist @@ -144,62 +146,6 @@ that: - "output.viable_instances == 1" - - name: Tag asg - ec2_asg: - name: "{{ resource_prefix }}-asg" - tags: - - tag_a: 'value 1' - propagate_at_launch: no - - tag_b: 'value 2' - propagate_at_launch: yes - register: output - - - assert: - that: - - "output.tags | length == 2" - - output is changed - - - name: Re-Tag asg (different order) - ec2_asg: - name: "{{ resource_prefix }}-asg" - tags: - - tag_b: 'value 2' - propagate_at_launch: yes - - tag_a: 'value 1' - propagate_at_launch: no - register: output - - - assert: - that: - - "output.tags | length == 2" - - output is not changed - - - name: Re-Tag asg new tags - ec2_asg: - name: "{{ resource_prefix }}-asg" - tags: - - tag_c: 'value 3' - propagate_at_launch: no - register: output - - - assert: - that: - - "output.tags | length == 1" - - output is changed - - - name: Re-Tag asg update propagate_at_launch - ec2_asg: - name: "{{ resource_prefix }}-asg" - tags: - - tag_c: 'value 3' - propagate_at_launch: yes - register: output - - - assert: - that: - - "output.tags | length == 1" - - output is changed - - name: Enable metrics collection ec2_asg: name: "{{ resource_prefix }}-asg" diff --git a/tests/integration/targets/ec2_asg/tasks/tag_operations.yml b/tests/integration/targets/ec2_asg/tasks/tag_operations.yml new file mode 100644 index 00000000000..2f9cc118c4f --- /dev/null +++ b/tests/integration/targets/ec2_asg/tasks/tag_operations.yml @@ -0,0 +1,352 @@ +- name: Running AutoScalingGroup Tag operations test + block: + #---------------------------------------------------------------------- + - name: create a launch configuration + ec2_lc: + name: "{{ resource_prefix }}-lc-tag-test" + image_id: "{{ ec2_ami_image }}" + region: "{{ aws_region }}" + instance_type: t2.micro + assign_public_ip: yes + register: create_lc + + - name: ensure that lc is created + assert: + that: + - create_lc is changed + - create_lc.failed is false + - '"autoscaling:CreateLaunchConfiguration" in create_lc.resource_actions' + + #---------------------------------------------------------------------- + - name: create a AutoScalingGroup to be used for tag_operations test + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + launch_config_name: "{{ resource_prefix }}-lc-tag-test" + health_check_period: 60 + health_check_type: ELB + replace_all_instances: yes + min_size: 1 + max_size: 1 + desired_capacity: 1 + region: "{{ aws_region }}" + register: create_asg + + - name: ensure that AutoScalingGroup is created + assert: + that: + - create_asg is changed + - create_asg.failed is false + - '"autoscaling:CreateAutoScalingGroup" in create_asg.resource_actions' + + #---------------------------------------------------------------------- + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + + - assert: + that: + - info_result.results[0].tags | length == 0 + + - name: Tag asg + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - tag_a: 'value 1' + propagate_at_launch: no + - tag_b: 'value 2' + propagate_at_launch: yes + register: output + + - assert: + that: + - "output.tags | length == 2" + - output is changed + + - name: Re-Tag asg (different order) + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - tag_b: 'value 2' + propagate_at_launch: yes + - tag_a: 'value 1' + propagate_at_launch: no + register: output + + - assert: + that: + - "output.tags | length == 2" + - output is not changed + + - name: Re-Tag asg new tags + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - tag_c: 'value 3' + propagate_at_launch: no + register: output + + - assert: + that: + - "output.tags | length == 1" + - output is changed + + - name: Re-Tag asg update propagate_at_launch + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - tag_c: 'value 3' + propagate_at_launch: yes + register: output + + - assert: + that: + - "output.tags | length == 1" + - output is changed + + - name: Remove all tags + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: [] + register: add_empty + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + # create a list of tag_keys from info result + - set_fact: + tag_keys: "{{ info_result.results[0].tags | map(attribute='key') | list }}" + + - assert: + that: + - add_empty is changed + - info_result.results[0].tags | length == 0 + - '"autoscaling:CreateOrUpdateTags" not in add_empty.resource_actions' + - '"autoscaling:DeleteTags" in add_empty.resource_actions' + + - name: Add 4 new tags - do not purge existing tags + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - lowercase spaced: "hello cruel world" + propagate_at_launch: no + - Title Case: "Hello Cruel World" + propagate_at_launch: yes + - CamelCase: "SimpleCamelCase" + propagate_at_launch: yes + - snake_case: "simple_snake_case" + propagate_at_launch: no + purge_tags: false + register: add_result + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + # create a list of tag_keys from info result + - set_fact: + tag_keys: "{{ info_result.results[0].tags | map(attribute='key') | list }}" + + - assert: + that: + - add_result is changed + - info_result.results[0].tags | length == 4 + - '"lowercase spaced" in tag_keys' + - '"Title Case" in tag_keys' + - '"CamelCase" in tag_keys' + - '"snake_case" in tag_keys' + - '"autoscaling:CreateOrUpdateTags" in add_result.resource_actions' + + - name: Add 4 new tags - do not purge existing tags - idempotency + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - lowercase spaced: "hello cruel world" + propagate_at_launch: no + - Title Case: "Hello Cruel World" + propagate_at_launch: yes + - CamelCase: "SimpleCamelCase" + propagate_at_launch: yes + - snake_case: "simple_snake_case" + propagate_at_launch: no + purge_tags: false + register: add_result + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + + - assert: + that: + - add_result is not changed + - info_result.results[0].tags | length == 4 + - '"autoscaling:CreateOrUpdateTags" not in add_result.resource_actions' + + - name: Add 2 new tags - purge existing tags + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - tag_a: 'val_a' + propagate_at_launch: no + - tag_b: 'val_b' + propagate_at_launch: yes + register: add_purge_result + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + # create a list of tag_keys from info result + - set_fact: + tag_keys: "{{ info_result.results[0].tags | map(attribute='key') | list }}" + + - assert: + that: + - add_purge_result is changed + - info_result.results[0].tags | length == 2 + - '"tag_a" in tag_keys' + - '"tag_b" in tag_keys' + - '"lowercase spaced" not in tag_keys' + - '"Title Case" not in tag_keys' + - '"CamelCase" not in tag_keys' + - '"snake_case" not in tag_keys' + - '"autoscaling:CreateOrUpdateTags" in add_purge_result.resource_actions' + + - name: Re-tag ASG - modify values + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - tag_a: 'new_val_a' + propagate_at_launch: no + - tag_b: 'new_val_b' + propagate_at_launch: yes + register: add_purge_result + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + # create a list of tag_keys and tag_values from info result + - set_fact: + tag_keys: "{{ info_result.results[0].tags | map(attribute='key') | list }}" + - set_fact: + tag_values: "{{ info_result.results[0].tags | map(attribute='value') | list }}" + + + - assert: + that: + - add_purge_result is changed + - info_result.results[0].tags | length == 2 + - '"tag_a" in tag_keys' + - '"tag_b" in tag_keys' + - '"new_val_a" in tag_values' + - '"new_val_b" in tag_values' + - '"lowercase spaced" not in tag_keys' + - '"Title Case" not in tag_keys' + - '"CamelCase" not in tag_keys' + - '"snake_case" not in tag_keys' + - '"autoscaling:CreateOrUpdateTags" in add_purge_result.resource_actions' + + - name: Add 2 more tags - do not purge existing tags + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: + - lowercase spaced: "hello cruel world" + propagate_at_launch: no + - Title Case: "Hello Cruel World" + propagate_at_launch: yes + purge_tags: false + register: add_result + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + # create a list of tag_keys from info result + - set_fact: + tag_keys: "{{ info_result.results[0].tags | map(attribute='key') | list }}" + + - assert: + that: + - add_result is changed + - info_result.results[0].tags | length == 4 + - '"tag_a" in tag_keys' + - '"tag_b" in tag_keys' + - '"lowercase spaced" in tag_keys' + - '"Title Case" in tag_keys' + - '"autoscaling:CreateOrUpdateTags" in add_result.resource_actions' + + - name: Add empty tags with purge set to false to assert that existing tags are retained + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: [] + purge_tags: false + register: add_empty + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + # create a list of tag_keys from info result + - set_fact: + tag_keys: "{{ info_result.results[0].tags | map(attribute='key') | list }}" + + - assert: + that: + - add_empty is not changed + - info_result.results[0].tags | length == 4 + - '"tag_a" in tag_keys' + - '"tag_b" in tag_keys' + - '"lowercase spaced" in tag_keys' + - '"Title Case" in tag_keys' + - '"autoscaling:CreateOrUpdateTags" not in add_empty.resource_actions' + + - name: Add empty tags with purge set to true to assert that existing tags are removed + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + tags: [] + register: add_empty + + - name: Get asg info + ec2_asg_info: + name: "{{ resource_prefix }}-asg-tag-test" + register: info_result + # create a list of tag_keys from info result + - set_fact: + tag_keys: "{{ info_result.results[0].tags | map(attribute='key') | list }}" + + - assert: + that: + - add_empty is changed + - info_result.results[0].tags | length == 0 + - '"tag_a" not in tag_keys' + - '"tag_b" not in tag_keys' + - '"lowercase spaced" not in tag_keys' + - '"Title Case" not in tag_keys' + - '"autoscaling:CreateOrUpdateTags" not in add_empty.resource_actions' + - '"autoscaling:DeleteTags" in add_empty.resource_actions' + + #---------------------------------------------------------------------- + + always: + + - name: kill asg created in this test + ec2_asg: + name: "{{ resource_prefix }}-asg-tag-test" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + + - name: remove launch config created in this test + ec2_lc: + name: "{{ resource_prefix }}-lc-tag-test" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10