diff --git a/changelogs/fragments/543-ecs_propagate_tags_support.yml b/changelogs/fragments/543-ecs_propagate_tags_support.yml new file mode 100644 index 00000000000..181346c680a --- /dev/null +++ b/changelogs/fragments/543-ecs_propagate_tags_support.yml @@ -0,0 +1,3 @@ +--- +minor_changes: + - ecs_service - added ``tags`` and ``tag_propagation`` support to the module (https://github.com/ansible-collections/community.aws/pull/543). diff --git a/plugins/modules/ecs_service.py b/plugins/modules/ecs_service.py index 78e352447ee..800d670be21 100644 --- a/plugins/modules/ecs_service.py +++ b/plugins/modules/ecs_service.py @@ -5,7 +5,6 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type - DOCUMENTATION = r''' --- module: ecs_service @@ -252,6 +251,19 @@ type: bool default: false version_added: 4.1.0 + propagate_tags: + description: + - Propagate tags from ECS task defintition or ECS service to ECS task. + required: false + choices: ["TASK_DEFINITION", "SERVICE"] + type: str + version_added: 4.1.0 + tags: + description: + - A dictionary of tags to add or remove from the resource. + type: dict + required: false + version_added: 4.1.0 extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 @@ -259,7 +271,6 @@ EXAMPLES = r''' # Note: These examples do not set authentication details, see the AWS Guide for details. - # Basic provisioning example - community.aws.ecs_service: state: present @@ -328,6 +339,18 @@ - capacity_provider: test-capacity-provider-1 weight: 1 base: 0 + +# With tags and tag propagation +- community.aws.ecs_service: + state: present + name: tags-test-service + cluster: new_cluster + task_definition: 'new_cluster-task:1' + desired_count: 1 + tags: + Firstname: jane + lastName: doe + propagate_tags: SERVICE ''' RETURN = r''' @@ -401,6 +424,10 @@ description: The valid values are ACTIVE, DRAINING, or INACTIVE. returned: always type: str + tags: + description: The tags applied to this resource. + returned: success + type: dict taskDefinition: description: The ARN of a task definition to use for tasks in the service. returned: always @@ -472,7 +499,10 @@ such as attribute:ecs.availability-zone. For the binpack placement strategy, valid values are CPU and MEMORY. returned: always type: str - + propagateTags: + description: The type of tag propagation applied to the resource. + returned: always + type: str ansible_facts: description: Facts about deleted service. returned: when deleting a service @@ -530,6 +560,11 @@ description: The valid values are ACTIVE, DRAINING, or INACTIVE. returned: always type: str + tags: + description: The tags applied to this resource. + returned: when tags found + type: list + elements: dict taskDefinition: description: The ARN of a task definition to use for tasks in the service. returned: always @@ -601,6 +636,11 @@ such as attribute:ecs.availability-zone. For the binpack placement strategy, valid values are CPU and MEMORY. returned: always type: str + propagateTags: + description: The type of tag propagation applied to the resource + returned: always + type: str + ''' import time @@ -614,8 +654,13 @@ 'deployment_circuit_breaker': 'dict', } +from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict + from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule -from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict, map_complex_type, get_ec2_security_group_ids_from_names +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import map_complex_type +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_ec2_security_group_ids_from_names +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 try: import botocore @@ -662,8 +707,11 @@ def find_in_array(self, array_of_services, service_name, field_name='serviceArn' def describe_service(self, cluster_name, service_name): response = self.ecs.describe_services( cluster=cluster_name, - services=[service_name]) + services=[service_name], + include=['TAGS'], + ) msg = '' + if len(response['failures']) > 0: c = self.find_in_array(response['failures'], service_name, 'arn') msg += ", failure reason is " + c['reason'] @@ -692,6 +740,12 @@ def is_matching_service(self, expected, existing): if (expected['load_balancers'] or []) != existing['loadBalancers']: return False + if expected['propagate_tags'] != existing['propagateTags']: + return False + + if boto3_tag_list_to_ansible_dict(existing['tags']) != expected['tags']: + return False + # expected is params. DAEMON scheduling strategy returns desired count equal to # number of instances running; don't check desired count if scheduling strat is daemon if (expected['scheduling_strategy'] != 'DAEMON'): @@ -704,7 +758,7 @@ def create_service(self, service_name, cluster_name, task_definition, load_balan desired_count, client_token, role, deployment_controller, deployment_configuration, placement_constraints, placement_strategy, health_check_grace_period_seconds, network_configuration, service_registries, launch_type, platform_version, - scheduling_strategy, capacity_provider_strategy): + scheduling_strategy, capacity_provider_strategy, tags, propagate_tags): params = dict( cluster=cluster_name, @@ -740,6 +794,14 @@ def create_service(self, service_name, cluster_name, task_definition, load_balan params['desiredCount'] = desired_count if capacity_provider_strategy: params['capacityProviderStrategy'] = capacity_provider_strategy + if propagate_tags: + params['propagateTags'] = propagate_tags + # desired count is not required if scheduling strategy is daemon + if desired_count is not None: + params['desiredCount'] = desired_count + if tags: + params['tags'] = ansible_dict_to_boto3_tag_list(tags, 'key', 'value') + if scheduling_strategy: params['schedulingStrategy'] = scheduling_strategy response = self.ecs.create_service(**params) @@ -850,7 +912,9 @@ def main(): weight=dict(type='int'), base=dict(type='int') ) - ) + ), + propagate_tags=dict(required=False, choices=['TASK_DEFINITION', 'SERVICE']), + tags=dict(required=False, type='dict'), ) module = AnsibleAWSModule(argument_spec=argument_spec, @@ -888,7 +952,9 @@ def main(): try: existing = service_mgr.describe_service(module.params['cluster'], module.params['name']) except Exception as e: - module.fail_json(msg="Exception describing service '" + module.params['name'] + "' in cluster '" + module.params['cluster'] + "': " + str(e)) + module.fail_json_aws(e, + msg="Exception describing service '{0}' in cluster '{1}'" + .format(module.params['name'], module.params['cluster'])) results = dict(changed=False) @@ -948,6 +1014,12 @@ def main(): else: task_definition = module.params['task_definition'] + if module.params['propagate_tags'] and module.params['propagate_tags'] != existing['propagateTags']: + module.fail_json(msg="It is not currently supported to enable propagation tags of an existing service") + + if module.params['tags'] and boto3_tag_list_to_ansible_dict(existing['tags']) != module.params['tags']: + module.fail_json(msg="It is not currently supported to change tags of an existing service") + # update required response = service_mgr.update_service(module.params['name'], module.params['cluster'], @@ -957,7 +1029,7 @@ def main(): network_configuration, module.params['health_check_grace_period_seconds'], module.params['force_new_deployment'], - capacityProviders + capacityProviders, ) else: @@ -977,13 +1049,18 @@ def main(): network_configuration, serviceRegistries, module.params['launch_type'], + module.params['scheduling_strategy'], module.params['platform_version'], module.params['scheduling_strategy'], - capacityProviders + capacityProviders, + module.params['tags'], + module.params['propagate_tags'], ) except botocore.exceptions.ClientError as e: module.fail_json_aws(e, msg="Couldn't create service") + if response.get('tags', None): + response['tags'] = boto3_tag_list_to_ansible_dict(response['tags']) results['service'] = response results['changed'] = True @@ -1044,7 +1121,10 @@ def main(): break time.sleep(delay) if i is repeat - 1: - module.fail_json(msg="Service still not deleted after " + str(repeat) + " tries of " + str(delay) + " seconds each.") + module.fail_json( + msg="Service still not deleted after {0} tries of {1} seconds each." + .format(repeat, delay) + ) return module.exit_json(**results) diff --git a/tests/integration/targets/ecs_tag/tasks/main.yml b/tests/integration/targets/ecs_tag/tasks/main.yml index 78a118375f0..fff9ee27d5c 100644 --- a/tests/integration/targets/ecs_tag/tasks/main.yml +++ b/tests/integration/targets/ecs_tag/tasks/main.yml @@ -92,21 +92,6 @@ that: - taglist.changed == false - - name: cluster tags - List tags - ecs_tag: - cluster_name: "{{ resource_prefix}}" - resource: "{{ resource_prefix}}" - resource_type: cluster - state: list - register: taglist - - - name: cluster tags - should have 2 tags - assert: - that: - - taglist.tags|list|length == 2 - - taglist.failed == false - - taglist.changed == false - - name: cluster tags - remove tag another ecs_tag: cluster_name: "{{resource_prefix}}" @@ -249,21 +234,6 @@ - taglist.changed == false - taglist.tags.Name == "task_definition-{{ resource_prefix }}" - - name: task_definition tags - retrieve all tags on a task_definition - ecs_tag: - cluster_name: "{{resource_prefix}}" - resource: "{{ecs_taskdefinition_creation.taskdefinition.family}}" - resource_type: task_definition - state: list - register: taglist - - - name: task_definition tags - should have 1 tag - assert: - that: - - taglist.tags|list|length == 1 - - taglist.failed == false - - taglist.changed == false - - name: task_definition tags - remove task_definition tags ecs_tag: cluster_name: "{{resource_prefix}}" @@ -281,6 +251,51 @@ - taglist.changed == true - '"Name" not in taglist.tags' + # Test tags and tags_propagate with service creation + + - name: create ecs_service with tags + ecs_service: + name: "{{ resource_prefix }}-tags" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 1 + state: present + tags: + Name: foo + "Last Name": bar + register: ecs_service_creation_tags + + - name: ecs_service up + assert: + that: + - ecs_service_creation_tags.changed + + - name: service tags - tags should be there + assert: + that: + - '"Name" in ecs_service_creation_tags.service.tags' + - '"Last Name" in ecs_service_creation_tags.service.tags' + - ecs_service_creation_tags.service.tags.Name == "foo" + - ecs_service_creation_tags.service.tags["Last Name"] == "bar" + + - name: create the same ecs_service with tags + ecs_service: + name: "{{ resource_prefix }}-tags" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 1 + state: present + tags: + test: test + ignore_errors: yes + register: ecs_service_creation_again + + - name: check that creation again with tags failed + assert: + that: + - ecs_service_creation_again is failed + - '"msg" in ecs_service_creation_again' + always: - name: scale down ecs service ecs_service: @@ -291,6 +306,15 @@ state: present ignore_errors: yes + - name: scale down ecs service + ecs_service: + name: "{{ resource_prefix }}-tags" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 0 + state: present + ignore_errors: yes + - name: pause to wait for scale down pause: seconds: 30 @@ -304,6 +328,15 @@ state: absent ignore_errors: yes + - name: remove ecs service + ecs_service: + name: "{{ resource_prefix }}-tags" + cluster: "{{ resource_prefix }}" + task_definition: "{{ resource_prefix }}" + desired_count: 1 + state: absent + ignore_errors: yes + - name: remove ecs task definition ecs_taskdefinition: containers: