diff --git a/lib/ansible/modules/cloud/amazon/ecs_task.py b/lib/ansible/modules/cloud/amazon/ecs_task.py index de9a3df3fd4..e1e41252bab 100644 --- a/lib/ansible/modules/cloud/amazon/ecs_task.py +++ b/lib/ansible/modules/cloud/amazon/ecs_task.py @@ -84,6 +84,12 @@ options: version_added: 2.8 choices: ["EC2", "FARGATE"] type: str + tags: + type: dict + description: + - Tags that will be added to ecs tasks on start and run + required: false + version_added: "2.10" extends_documentation_fragment: - aws - ec2 @@ -108,6 +114,11 @@ EXAMPLES = ''' cluster: console-sample-app-static-cluster task_definition: console-sample-app-static-taskdef task: "arn:aws:ecs:us-west-2:172139249013:task/3f8353d1-29a8-4689-bbf6-ad79937ffe8a" + tags: + resourceName: a_task_for_ansible_to_run + type: long_running_task + network: internal + version: 1.4 container_instances: - arn:aws:ecs:us-west-2:172139249013:container-instance/79c23f22-876c-438a-bddf-55c98a3538a8 started_by: ansible_user @@ -209,7 +220,8 @@ task: ''' from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ec2_argument_spec, get_ec2_security_group_ids_from_names +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.ec2 import ec2_argument_spec, get_ec2_security_group_ids_from_names, ansible_dict_to_boto3_tag_list try: import botocore @@ -254,7 +266,7 @@ class EcsExecManager: return c return None - def run_task(self, cluster, task_definition, overrides, count, startedBy, launch_type): + def run_task(self, cluster, task_definition, overrides, count, startedBy, launch_type, tags): if overrides is None: overrides = dict() params = dict(cluster=cluster, taskDefinition=task_definition, @@ -263,6 +275,10 @@ class EcsExecManager: params['networkConfiguration'] = self.format_network_configuration(self.module.params['network_configuration']) if launch_type: params['launchType'] = launch_type + if tags: + params['tags'] = ansible_dict_to_boto3_tag_list(tags, 'key', 'value') + + # TODO: need to check if long arn format enabled. try: response = self.ecs.run_task(**params) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: @@ -270,7 +286,7 @@ class EcsExecManager: # include tasks and failures return response['tasks'] - def start_task(self, cluster, task_definition, overrides, container_instances, startedBy): + def start_task(self, cluster, task_definition, overrides, container_instances, startedBy, tags): args = dict() if cluster: args['cluster'] = cluster @@ -284,6 +300,8 @@ class EcsExecManager: args['startedBy'] = startedBy if self.module.params['network_configuration']: args['networkConfiguration'] = self.format_network_configuration(self.module.params['network_configuration']) + if tags: + args['tags'] = ansible_dict_to_boto3_tag_list(tags, 'key', 'value') try: response = self.ecs.start_task(**args) except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: @@ -302,6 +320,17 @@ class EcsExecManager: # to e.g. ecs.run_task, it's just passed as a keyword argument) return LooseVersion(botocore.__version__) >= LooseVersion('1.8.4') + def ecs_task_long_format_enabled(self): + account_support = self.ecs.list_account_settings(name='taskLongArnFormat', effectiveSettings=True) + return account_support['settings'][0]['value'] == 'enabled' + + def ecs_api_handles_tags(self): + from distutils.version import LooseVersion + # There doesn't seem to be a nice way to inspect botocore to look + # for attributes (and networkConfiguration is not an explicit argument + # to e.g. ecs.run_task, it's just passed as a keyword argument) + return LooseVersion(botocore.__version__) >= LooseVersion('1.12.46') + def ecs_api_handles_network_configuration(self): from distutils.version import LooseVersion # There doesn't seem to be a nice way to inspect botocore to look @@ -322,7 +351,8 @@ def main(): container_instances=dict(required=False, type='list'), # S* started_by=dict(required=False, type='str'), # R S network_configuration=dict(required=False, type='dict'), - launch_type=dict(required=False, choices=['EC2', 'FARGATE']) + launch_type=dict(required=False, choices=['EC2', 'FARGATE']), + tags=dict(required=False, type='dict') )) module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, @@ -359,6 +389,12 @@ def main(): if module.params['launch_type'] and not service_mgr.ecs_api_handles_launch_type(): module.fail_json(msg='botocore needs to be version 1.8.4 or higher to use launch type') + if module.params['tags']: + if not service_mgr.ecs_api_handles_tags(): + module.fail_json(msg=missing_required_lib("botocore >= 1.12.46", reason="to use tags")) + if not service_mgr.ecs_task_long_format_enabled(): + module.fail_json(msg="Cannot set task tags: long format task arns are required to set tags") + existing = service_mgr.list_tasks(module.params['cluster'], task_to_list, status_type) results = dict(changed=False) @@ -374,7 +410,9 @@ def main(): module.params['overrides'], module.params['count'], module.params['started_by'], - module.params['launch_type']) + module.params['launch_type'], + module.params['tags'], + ) results['changed'] = True elif module.params['operation'] == 'start': @@ -388,7 +426,8 @@ def main(): module.params['task_definition'], module.params['overrides'], module.params['container_instances'], - module.params['started_by'] + module.params['started_by'], + module.params['tags'], ) results['changed'] = True diff --git a/test/integration/targets/ecs_cluster/tasks/full_test.yml b/test/integration/targets/ecs_cluster/tasks/full_test.yml index ab9039c9dc1..40813b8720a 100644 --- a/test/integration/targets/ecs_cluster/tasks/full_test.yml +++ b/test/integration/targets/ecs_cluster/tasks/full_test.yml @@ -623,7 +623,7 @@ - name: check that facts contain network configuration assert: that: - - "'networkConfiguration' in ecs_service_info.ansible_facts.services[0]" + - "'networkConfiguration' in ecs_service_info.services[0]" - name: attempt to get facts from missing task definition ecs_taskdefinition_info: @@ -738,6 +738,11 @@ <<: *aws_connection_info register: ecs_fargate_service_network_with_awsvpc + - name: assert that public IP assignment is enabled + assert: + that: + - 'ecs_fargate_service_network_with_awsvpc.service.networkConfiguration.awsvpcConfiguration.assignPublicIp == "ENABLED"' + - name: create fargate ECS task with run task ecs_task: operation: run @@ -754,10 +759,67 @@ <<: *aws_connection_info register: fargate_run_task_output - - name: assert that public IP assignment is enabled - assert: - that: - - 'ecs_fargate_service_network_with_awsvpc.service.networkConfiguration.awsvpcConfiguration.assignPublicIp == "ENABLED"' + # aws cli not installed in docker container; make sure it's installed. + - name: install awscli + pip: + state: present + name: awscli + + - name: disable taskLongArnFormat + command: aws ecs put-account-setting --name taskLongArnFormat --value disabled + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + + - name: create fargate ECS task with run task and tags (LF disabled) (should fail) + ecs_task: + operation: run + cluster: "{{ ecs_cluster_name }}" + task_definition: "{{ ecs_task_name }}-vpc" + launch_type: FARGATE + count: 1 + tags: + tag_key: tag_value + tag_key2: tag_value2 + network_configuration: + subnets: "{{ setup_subnet.results | json_query('[].subnet.id') }}" + security_groups: + - '{{ setup_sg.group_id }}' + assign_public_ip: true + started_by: ansible_user + <<: *aws_connection_info + register: fargate_run_task_output_with_tags_fail + ignore_errors: yes + + - name: enable taskLongArnFormat + command: aws ecs put-account-setting --name taskLongArnFormat --value enabled + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + + - name: create fargate ECS task with run task and tags + ecs_task: + operation: run + cluster: "{{ ecs_cluster_name }}" + task_definition: "{{ ecs_task_name }}-vpc" + launch_type: FARGATE + count: 1 + tags: + tag_key: tag_value + tag_key2: tag_value2 + network_configuration: + subnets: "{{ setup_subnet.results | json_query('[].subnet.id') }}" + security_groups: + - '{{ setup_sg.group_id }}' + assign_public_ip: true + started_by: ansible_user + <<: *aws_connection_info + register: fargate_run_task_output_with_tags + # ============================================================ # End tests for Fargate @@ -795,12 +857,12 @@ state: present name: "{{ ecs_service_name }}" cluster: "{{ ecs_cluster_name }}" - task_definition: "{{ ecs_service_info.ansible_facts.services[0].taskDefinition }}" + task_definition: "{{ ecs_service_info.services[0].taskDefinition }}" desired_count: 0 deployment_configuration: "{{ ecs_service_deployment_configuration }}" placement_strategy: "{{ ecs_service_placement_strategy }}" load_balancers: - - targetGroupArn: "{{ ecs_service_info.ansible_facts.services[0].loadBalancers[0].targetGroupArn }}" + - targetGroupArn: "{{ ecs_service_info.services[0].loadBalancers[0].targetGroupArn }}" containerName: "{{ ecs_task_name }}" containerPort: "{{ ecs_task_container_port }}" <<: *aws_connection_info @@ -821,12 +883,12 @@ state: present name: "{{ ecs_service_name }}2" cluster: "{{ ecs_cluster_name }}" - task_definition: "{{ ecs_service_info.ansible_facts.services[0].taskDefinition }}" + task_definition: "{{ ecs_service_info.services[0].taskDefinition }}" desired_count: 0 deployment_configuration: "{{ ecs_service_deployment_configuration }}" placement_strategy: "{{ ecs_service_placement_strategy }}" load_balancers: - - targetGroupArn: "{{ ecs_service_info.ansible_facts.services[0].loadBalancers[0].targetGroupArn }}" + - targetGroupArn: "{{ ecs_service_info.services[0].loadBalancers[0].targetGroupArn }}" containerName: "{{ ecs_task_name }}" containerPort: "{{ ecs_task_container_port }}" <<: *aws_connection_info @@ -904,6 +966,14 @@ <<: *aws_connection_info ignore_errors: yes + - name: stop Fargate ECS task + ecs_task: + task: "{{ fargate_run_task_output_with_tags.task[0].taskArn }}" + task_definition: "{{ ecs_task_name }}-vpc" + operation: stop + cluster: "{{ ecs_cluster_name }}" + <<: *aws_connection_info + ignore_errors: yes - name: pause to allow services to scale down pause: seconds: 60 diff --git a/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment.yml b/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment.yml index f956f86c7ff..c86e7222b20 100644 --- a/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment.yml +++ b/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment.yml @@ -25,6 +25,28 @@ <<: *aws_connection_info register: ecs_taskdefinition_creation + # even after deleting the cluster and recreating with a different name + # the previous service can prevent the current service from starting + # while it's in a draining state. Check the service info and sleep + # if the service does not report as inactive. + + - name: check if service is still running from a previous task + ecs_service_info: + service: "{{ resource_prefix }}" + cluster: "{{ resource_prefix }}" + details: yes + <<: *aws_connection_info + register: ecs_service_info_results + - name: delay if the service was not inactive + debug: var=ecs_service_info_results + + - name: delay if the service was not inactive + pause: + seconds: 30 + when: + - ecs_service_info_results.services|length >0 + - ecs_service_info_results.services[0]['status'] != 'INACTIVE' + - name: create ecs_service ecs_service: name: "{{ resource_prefix }}" diff --git a/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment_fail.yml b/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment_fail.yml index 1335ecadced..95e8c576dec 100644 --- a/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment_fail.yml +++ b/test/integration/targets/ecs_cluster/tasks/network_force_new_deployment_fail.yml @@ -25,6 +25,28 @@ <<: *aws_connection_info register: ecs_taskdefinition_creation + # even after deleting the cluster and recreating with a different name + # the previous service can prevent the current service from starting + # while it's in a draining state. Check the service info and sleep + # if the service does not report as inactive. + + - name: check if service is still running from a previous task + ecs_service_info: + service: "{{ resource_prefix }}" + cluster: "{{ resource_prefix }}" + details: yes + <<: *aws_connection_info + register: ecs_service_info_results + - name: delay if the service was not inactive + debug: var=ecs_service_info_results + + - name: delay if the service was not inactive + pause: + seconds: 30 + when: + - ecs_service_info_results.services|length >0 + - ecs_service_info_results.services[0]['status'] != 'INACTIVE' + - name: create ecs_service ecs_service: name: "{{ resource_prefix }}"