diff --git a/changelogs/fragments/933-ec2_asg-detach-instances-feature.yml b/changelogs/fragments/933-ec2_asg-detach-instances-feature.yml new file mode 100644 index 00000000000..3ebb13fff78 --- /dev/null +++ b/changelogs/fragments/933-ec2_asg-detach-instances-feature.yml @@ -0,0 +1,3 @@ +minor_changes: + - ec2_asg - Added functionality to detach specific instances and/or decrement desired capacity + from ASG without terminating instances (https://github.com/ansible-collections/community.aws/pull/933). 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 46cdcbf15b8..fa91232cbe6 100644 --- a/plugins/modules/ec2_asg.py +++ b/plugins/modules/ec2_asg.py @@ -182,6 +182,21 @@ matching the current launch configuration. type: list elements: str + detach_instances: + description: + - Removes one or more instances from the specified AutoScalingGroup. + - If I(decrement_desired_capacity) flag is not set, new instance(s) are launched to replace the detached instance(s). + - If a Classic Load Balancer is attached to the AutoScalingGroup, the instances are also deregistered from the load balancer. + - If there are target groups attached to the AutoScalingGroup, the instances are also deregistered from the target groups. + type: list + elements: str + version_added: 3.2.0 + decrement_desired_capacity: + description: + - Indicates whether the AutoScalingGroup decrements the desired capacity value by the number of instances detached. + default: false + type: bool + version_added: 3.2.0 lc_check: description: - Check to make sure instances that are being replaced with I(replace_instances) do not already have the current I(launch_config). @@ -205,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. @@ -630,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', @@ -756,6 +779,12 @@ def terminate_asg_instance(connection, instance_id, decrement_capacity): ShouldDecrementDesiredCapacity=decrement_capacity) +@AWSRetry.jittered_backoff(**backoff_params) +def detach_asg_instances(connection, instance_ids, as_group_name, decrement_capacity): + connection.detach_instances(InstanceIds=instance_ids, AutoScalingGroupName=as_group_name, + ShouldDecrementDesiredCapacity=decrement_capacity) + + def enforce_required_arguments_for_create(): ''' As many arguments are not required for autoscale group deletion they cannot be mandatory arguments for the module, so we enforce @@ -1076,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') @@ -1184,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: @@ -1197,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) @@ -1523,6 +1558,40 @@ def replace(connection): return changed, asg_properties +def detach(connection): + group_name = module.params.get('name') + detach_instances = module.params.get('detach_instances') + as_group = describe_autoscaling_groups(connection, group_name)[0] + decrement_desired_capacity = module.params.get('decrement_desired_capacity') + min_size = module.params.get('min_size') + props = get_properties(as_group) + instances = props['instances'] + + # check if provided instance exists in asg, create list of instances to detach which exist in asg + instances_to_detach = [] + for instance_id in detach_instances: + if instance_id in instances: + instances_to_detach.append(instance_id) + + # check if setting decrement_desired_capacity will make desired_capacity smaller + # than the currently set minimum size in ASG configuration + if decrement_desired_capacity: + decremented_desired_capacity = len(instances) - len(instances_to_detach) + if min_size and min_size > decremented_desired_capacity: + module.fail_json( + msg="Detaching instance(s) with 'decrement_desired_capacity' flag set reduces number of instances to {0}\ + which is below current min_size {1}, please update AutoScalingGroup Sizes properly.".format(decremented_desired_capacity, min_size)) + + if instances_to_detach: + try: + detach_asg_instances(connection, instances_to_detach, group_name, decrement_desired_capacity) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to detach instances from AutoScaling Group") + + asg_properties = get_properties(as_group) + return True, asg_properties + + def get_instances_by_launch_config(props, lc_check, initial_instances): new_instances = [] old_instances = [] @@ -1776,11 +1845,14 @@ def main(): replace_batch_size=dict(type='int', default=1), replace_all_instances=dict(type='bool', default=False), replace_instances=dict(type='list', default=[], elements='str'), + detach_instances=dict(type='list', default=[], elements='str'), + decrement_desired_capacity=dict(type='bool', default=False), lc_check=dict(type='bool', default=True), lt_check=dict(type='bool', default=True), 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), @@ -1821,16 +1893,18 @@ def main(): argument_spec=argument_spec, mutually_exclusive=[ ['replace_all_instances', 'replace_instances'], - ['launch_config_name', 'launch_template'] + ['replace_all_instances', 'detach_instances'], + ['launch_config_name', 'launch_template'], ] ) state = module.params.get('state') replace_instances = module.params.get('replace_instances') replace_all_instances = module.params.get('replace_all_instances') + detach_instances = module.params.get('detach_instances') connection = module.client('autoscaling') - changed = create_changed = replace_changed = False + changed = create_changed = replace_changed = detach_changed = False exists = asg_exists(connection) if state == 'present': @@ -1847,7 +1921,15 @@ def main(): ): replace_changed, asg_properties = replace(connection) - if create_changed or replace_changed: + # Only detach instances if asg existed at start of call + if ( + exists + and (detach_instances) + and (module.params.get('launch_config_name') or module.params.get('launch_template')) + ): + detach_changed, asg_properties = detach(connection) + + if create_changed or replace_changed or detach_changed: changed = True module.exit_json(changed=changed, **asg_properties) diff --git a/tests/integration/targets/ec2_asg/tasks/instance_detach.yml b/tests/integration/targets/ec2_asg/tasks/instance_detach.yml new file mode 100644 index 00000000000..fbd29d39640 --- /dev/null +++ b/tests/integration/targets/ec2_asg/tasks/instance_detach.yml @@ -0,0 +1,208 @@ +- name: Running instance detach tests + block: + #---------------------------------------------------------------------- + - name: create a launch configuration + ec2_lc: + name: "{{ resource_prefix }}-lc-detach-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 instance_detach test + ec2_asg: + name: "{{ resource_prefix }}-asg-detach-test" + launch_config_name: "{{ resource_prefix }}-lc-detach-test" + health_check_period: 60 + health_check_type: ELB + replace_all_instances: yes + min_size: 3 + max_size: 6 + desired_capacity: 3 + region: "{{ aws_region }}" + register: create_asg + + - name: ensure that AutoScalingGroup is created + assert: + that: + - create_asg is changed + - create_asg.failed is false + - create_asg.instances | length == 3 + - create_asg.desired_capacity == 3 + - create_asg.in_service_instances == 3 + - '"autoscaling:CreateAutoScalingGroup" in create_asg.resource_actions' + + - name: gather info about asg, get instance ids + ec2_asg_info: + name: "{{ resource_prefix }}-asg-detach-test" + register: asg_info + - set_fact: + init_instance_1: "{{ asg_info.results[0].instances[0].instance_id }}" + init_instance_2: "{{ asg_info.results[0].instances[1].instance_id }}" + init_instance_3: "{{ asg_info.results[0].instances[2].instance_id }}" + + - name: Gather information about recently detached instances + amazon.aws.ec2_instance_info: + instance_ids: + - "{{ init_instance_1 }}" + - "{{ init_instance_2 }}" + - "{{ init_instance_3 }}" + register: instances_info + + # assert that there are 3 instances running in the AutoScalingGroup + - assert: + that: + - asg_info.results[0].instances | length == 3 + - "'{{ instances_info.instances[0].state.name }}' == 'running'" + - "'{{ instances_info.instances[1].state.name }}' == 'running'" + - "'{{ instances_info.instances[2].state.name }}' == 'running'" + + #---------------------------------------------------------------------- + + - name: detach 2 instance from the asg and replace with other instances + ec2_asg: + name: "{{ resource_prefix }}-asg-detach-test" + launch_config_name: "{{ resource_prefix }}-lc-detach-test" + health_check_period: 60 + health_check_type: ELB + min_size: 3 + max_size: 3 + desired_capacity: 3 + region: "{{ aws_region }}" + detach_instances: + - '{{ init_instance_1 }}' + - '{{ init_instance_2 }}' + + # pause to allow completion of instance replacement + - name: Pause for 30 seconds + pause: + seconds: 30 + + # gather info about asg and get instance ids + - ec2_asg_info: + name: "{{ resource_prefix }}-asg-detach-test" + register: asg_info_replaced + - set_fact: + instance_replace_1: "{{ asg_info_replaced.results[0].instances[0].instance_id }}" + instance_replace_2: "{{ asg_info_replaced.results[0].instances[1].instance_id }}" + instance_replace_3: "{{ asg_info_replaced.results[0].instances[2].instance_id }}" + + # create a list of instance currently attached to asg + - set_fact: + asg_instance_detach_replace: "{{ asg_info_replaced.results[0].instances | map(attribute='instance_id') | list }}" + + - name: Gather information about recently detached instances + amazon.aws.ec2_instance_info: + instance_ids: + - "{{ init_instance_1 }}" + - "{{ init_instance_2 }}" + register: detached_instances_info + + # assert that + # there are 3 still instances in the AutoScalingGroup + # two specified instances are detached and still running independently(not terminated) + - assert: + that: + - asg_info_replaced.results[0].desired_capacity == 3 + - asg_info_replaced.results[0].instances | length == 3 + - "'{{ init_instance_1 }}' not in {{ asg_instance_detach_replace }}" + - "'{{ init_instance_2 }}' not in {{ asg_instance_detach_replace }}" + - "'{{ detached_instances_info.instances[0].state.name }}' == 'running'" + - "'{{ detached_instances_info.instances[1].state.name }}' == 'running'" + + #---------------------------------------------------------------------- + + # detach 2 instances from the asg and reduce the desired capacity from 3 to 1 + - name: detach 2 instance from the asg and reduce the desired capacity from 3 to 1 + ec2_asg: + name: "{{ resource_prefix }}-asg-detach-test" + launch_config_name: "{{ resource_prefix }}-lc-detach-test" + health_check_period: 60 + health_check_type: ELB + min_size: 1 + max_size: 5 + desired_capacity: 3 + region: "{{ aws_region }}" + decrement_desired_capacity: true + detach_instances: + - '{{ instance_replace_1 }}' + - '{{ instance_replace_2 }}' + + - name: Pause for 30 seconds to allow completion of above task + pause: + seconds: 30 + + # gather information about asg and get instance id + - ec2_asg_info: + name: "{{ resource_prefix }}-asg-detach-test" + register: asg_info_decrement + - set_fact: + instance_detach_decrement: "{{ asg_info_decrement.results[0].instances[0].instance_id }}" + # create a list of instance ids from info result and set variable value to instance ID + - set_fact: + asg_instance_detach_decrement: "{{ asg_info_decrement.results[0].instances | map(attribute='instance_id') | list }}" + + - name: Gather information about recently detached instances + amazon.aws.ec2_instance_info: + instance_ids: + - "{{ instance_replace_1 }}" + - "{{ instance_replace_2 }}" + register: detached_instances_info + + # assert that + # detached instances are not replaced and there is only 1 instance in the AutoScalingGroup + # desired capacity is reduced to 1 + # detached instances are not terminated + - assert: + that: + - asg_info_decrement.results[0].instances | length == 1 + - asg_info_decrement.results[0].desired_capacity == 1 + - "'{{ instance_replace_1 }}' not in {{ asg_instance_detach_decrement }}" + - "'{{ instance_replace_2 }}' not in {{ asg_instance_detach_decrement }}" + - "'{{ detached_instances_info.instances[0].state.name }}' == 'running'" + - "'{{ detached_instances_info.instances[1].state.name }}' == 'running'" + - "'{{ instance_replace_3 }}' == '{{ instance_detach_decrement }}'" + + #---------------------------------------------------------------------- + + always: + + - name: terminate any instances created during this test + amazon.aws.ec2_instance: + instance_ids: + - "{{ item }}" + state: absent + loop: + - "{{ init_instance_1 }}" + - "{{ init_instance_2 }}" + - "{{ init_instance_3 }}" + - "{{ instance_replace_1 }}" + - "{{ instance_replace_2 }}" + - "{{ instance_replace_3 }}" + + - name: kill asg created in this test + ec2_asg: + name: "{{ resource_prefix }}-asg-detach-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-detach-test" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 diff --git a/tests/integration/targets/ec2_asg/tasks/main.yml b/tests/integration/targets/ec2_asg/tasks/main.yml index 7f196442904..d26e15b068c 100644 --- a/tests/integration/targets/ec2_asg/tasks/main.yml +++ b/tests/integration/targets/ec2_asg/tasks/main.yml @@ -30,7 +30,6 @@ aws_secret_key: "{{ aws_secret_key }}" security_token: "{{ security_token | default(omit) }}" region: "{{ aws_region }}" - collections: - amazon.aws @@ -106,6 +105,10 @@ cidr_ip: 0.0.0.0/0 register: sg + - include_tasks: tag_operations.yml + + - include_tasks: instance_detach.yml + - name: ensure launch configs exist ec2_lc: name: "{{ item }}" @@ -143,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