diff --git a/lib/ansible/modules/extras/cloud/amazon/ecs_taskdefinition.py b/lib/ansible/modules/extras/cloud/amazon/ecs_taskdefinition.py index d3f9d434b8..55ed9dab9c 100644 --- a/lib/ansible/modules/extras/cloud/amazon/ecs_taskdefinition.py +++ b/lib/ansible/modules/extras/cloud/amazon/ecs_taskdefinition.py @@ -137,6 +137,33 @@ class EcsTaskManager: containerDefinitions=container_definitions, volumes=volumes) return response['taskDefinition'] + def describe_task_definitions(self, family): + data = { + "taskDefinitionArns": [], + "nextToken": None + } + + def fetch(): + # Boto3 is weird about params passed, so only pass nextToken if we have a value + params = { + 'familyPrefix': family + } + + if data['nextToken']: + params['nextToken'] = data['nextToken'] + + result = self.ecs.list_task_definitions(**params) + data['taskDefinitionArns'] += result['taskDefinitionArns'] + data['nextToken'] = result.get('nextToken', None) + return data['nextToken'] is not None + + # Fetch all the arns, possibly across multiple pages + while fetch(): + pass + + # Return the full descriptions of the task definitions, sorted ascending by revision + return list(sorted([self.ecs.describe_task_definition(taskDefinition=arn)['taskDefinition'] for arn in data['taskDefinitionArns']], key=lambda td: td['revision'])) + def deregister_task(self, taskArn): response = self.ecs.deregister_task_definition(taskDefinition=taskArn) return response['taskDefinition'] @@ -145,12 +172,12 @@ def main(): argument_spec = ec2_argument_spec() argument_spec.update(dict( - state=dict(required=True, choices=['present', 'absent'] ), - arn=dict(required=False, type='str' ), - family=dict(required=False, type='str' ), - revision=dict(required=False, type='int' ), - containers=dict(required=False, type='list' ), - volumes=dict(required=False, type='list' ) + state=dict(required=True, choices=['present', 'absent']), + arn=dict(required=False, type='str'), + family=dict(required=False, type='str'), + revision=dict(required=False, type='int'), + containers=dict(required=False, type='list'), + volumes=dict(required=False, type='list') )) module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) @@ -162,52 +189,143 @@ def main(): module.fail_json(msg='boto3 is required.') task_to_describe = None - # When deregistering a task, we can specify the ARN OR - # the family and revision. - if module.params['state'] == 'absent': - if 'arn' in module.params and module.params['arn'] is not None: - task_to_describe = module.params['arn'] - elif 'family' in module.params and module.params['family'] is not None and 'revision' in module.params and module.params['revision'] is not None: - task_to_describe = module.params['family']+":"+str(module.params['revision']) - else: - module.fail_json(msg="To use task definitions, an arn or family and revision must be specified") - # When registering a task, we can specify the ARN OR - # the family and revision. - if module.params['state'] == 'present': - if not 'family' in module.params: - module.fail_json(msg="To use task definitions, a family must be specified") - if not 'containers' in module.params: - module.fail_json(msg="To use task definitions, a list of containers must be specified") - task_to_describe = module.params['family'] - task_mgr = EcsTaskManager(module) - existing = task_mgr.describe_task(task_to_describe) - results = dict(changed=False) + if module.params['state'] == 'present': - if existing and 'status' in existing and existing['status']=="ACTIVE": - results['taskdefinition']=existing + if 'containers' not in module.params or not module.params['containers']: + module.fail_json(msg="To use task definitions, a list of containers must be specified") + + if 'family' not in module.params or not module.params['family']: + module.fail_json(msg="To use task definitions, a family must be specified") + + family = module.params['family'] + existing_definitions_in_family = task_mgr.describe_task_definitions(module.params['family']) + + if 'revision' in module.params and module.params['revision']: + # The definition specifies revision. We must gurantee that an active revision of that number will result from this. + revision = int(module.params['revision']) + + # A revision has been explicitly specified. Attempt to locate a matching revision + tasks_defs_for_revision = [td for td in existing_definitions_in_family if td['revision'] == revision] + existing = tasks_defs_for_revision[0] if len(tasks_defs_for_revision) > 0 else None + + if existing and existing['status'] != "ACTIVE": + # We cannot reactivate an inactive revision + module.fail_json(msg="A task in family '%s' already exists for revsion %d, but it is inactive" % (family, revision)) + elif not existing: + if len(existing_definitions_in_family) == 0 and revision != 1: + module.fail_json(msg="You have specified a revision of %d but a created revision would be 1" % revision) + elif existing_definitions_in_family[-1]['revision'] + 1 != revision: + module.fail_json(msg="You have specified a revision of %d but a created revision would be %d" % (revision, existing_definitions_in_family[-1]['revision'] + 1)) + else: + existing = None + + def _right_has_values_of_left(left, right): + # Make sure the values are equivalent for everything left has + for k, v in left.iteritems(): + if not ((not v and (k not in right or not right[k])) or (k in right and v == right[k])): + # We don't care about list ordering because ECS can change things + if isinstance(v, list) and k in right: + left_list = v + right_list = right[k] or [] + + if len(left_list) != len(right_list): + return False + + for list_val in left_list: + if list_val not in right_list: + return False + else: + return False + + # Make sure right doesn't have anything that left doesn't + for k, v in right.iteritems(): + if v and k not in left: + return False + + return True + + def _task_definition_matches(requested_volumes, requested_containers, existing_task_definition): + if td['status'] != "ACTIVE": + return None + + existing_volumes = td.get('volumes', []) or [] + + if len(requested_volumes) != len(existing_volumes): + # Nope. + return None + + if len(requested_volumes) > 0: + for requested_vol in requested_volumes: + found = False + + for actual_vol in existing_volumes: + if _right_has_values_of_left(requested_vol, actual_vol): + found = True + break + + if not found: + return None + + existing_containers = td.get('containerDefinitions', []) or [] + + if len(requested_containers) != len(existing_containers): + # Nope. + return None + + for requested_container in requested_containers: + found = False + + for actual_container in existing_containers: + if _right_has_values_of_left(requested_container, actual_container): + found = True + break + + if not found: + return None + + return existing_task_definition + + # No revision explicitly specified. Attempt to find an active, matching revision that has all the properties requested + for td in existing_definitions_in_family: + requested_volumes = module.params.get('volumes', []) or [] + requested_containers = module.params.get('containers', []) or [] + existing = _task_definition_matches(requested_volumes, requested_containers, td) + + if existing: + break + + if existing: + # Awesome. Have an existing one. Nothing to do. + results['taskdefinition'] = existing else: if not module.check_mode: - # doesn't exist. create it. - volumes = [] - if 'volumes' in module.params: - volumes = module.params['volumes'] - if volumes is None: - volumes = [] + # Doesn't exist. create it. + volumes = module.params.get('volumes', []) or [] results['taskdefinition'] = task_mgr.register_task(module.params['family'], - module.params['containers'], volumes) + module.params['containers'], volumes) results['changed'] = True - # delete the cloudtrai elif module.params['state'] == 'absent': + # When de-registering a task definition, we can specify the ARN OR the family and revision. + if module.params['state'] == 'absent': + if 'arn' in module.params and module.params['arn'] is not None: + task_to_describe = module.params['arn'] + elif 'family' in module.params and module.params['family'] is not None and 'revision' in module.params and \ + module.params['revision'] is not None: + task_to_describe = module.params['family'] + ":" + str(module.params['revision']) + else: + module.fail_json(msg="To use task definitions, an arn or family and revision must be specified") + + existing = task_mgr.describe_task(task_to_describe) + if not existing: pass else: - # it exists, so we should delete it and mark changed. - # return info about the cluster deleted + # It exists, so we should delete it and mark changed. Return info about the task definition deleted results['taskdefinition'] = existing - if 'status' in existing and existing['status']=="INACTIVE": + if 'status' in existing and existing['status'] == "INACTIVE": results['changed'] = False else: if not module.check_mode: