diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 13f16d50b..764df13d1 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -2343,7 +2343,7 @@ - [ ] upload_layer_part ## ecs -63% implemented +49% implemented - [X] create_cluster - [X] create_service - [ ] create_task_set @@ -2381,8 +2381,8 @@ - [ ] submit_attachment_state_changes - [ ] submit_container_state_change - [ ] submit_task_state_change -- [ ] tag_resource -- [ ] untag_resource +- [x] tag_resource +- [x] untag_resource - [ ] update_container_agent - [X] update_container_instances_state - [X] update_service diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 863cfc49e..692f7c31b 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -192,7 +192,7 @@ class Task(BaseObject): class Service(BaseObject): - def __init__(self, cluster, service_name, task_definition, desired_count, load_balancers=None, scheduling_strategy=None): + def __init__(self, cluster, service_name, task_definition, desired_count, load_balancers=None, scheduling_strategy=None, tags=None): self.cluster_arn = cluster.arn self.arn = 'arn:aws:ecs:us-east-1:012345678910:service/{0}'.format( service_name) @@ -216,6 +216,7 @@ class Service(BaseObject): ] self.load_balancers = load_balancers if load_balancers is not None else [] self.scheduling_strategy = scheduling_strategy if scheduling_strategy is not None else 'REPLICA' + self.tags = tags if tags is not None else [] self.pending_count = 0 @property @@ -225,7 +226,7 @@ class Service(BaseObject): @property def response_object(self): response_object = self.gen_response_object() - del response_object['name'], response_object['arn'] + del response_object['name'], response_object['arn'], response_object['tags'] response_object['serviceName'] = self.name response_object['serviceArn'] = self.arn response_object['schedulingStrategy'] = self.scheduling_strategy @@ -691,7 +692,7 @@ class EC2ContainerServiceBackend(BaseBackend): raise Exception("Could not find task {} on cluster {}".format( task_str, cluster_name)) - def create_service(self, cluster_str, service_name, task_definition_str, desired_count, load_balancers=None, scheduling_strategy=None): + def create_service(self, cluster_str, service_name, task_definition_str, desired_count, load_balancers=None, scheduling_strategy=None, tags=None): cluster_name = cluster_str.split('/')[-1] if cluster_name in self.clusters: cluster = self.clusters[cluster_name] @@ -701,7 +702,7 @@ class EC2ContainerServiceBackend(BaseBackend): desired_count = desired_count if desired_count is not None else 0 service = Service(cluster, service_name, - task_definition, desired_count, load_balancers, scheduling_strategy) + task_definition, desired_count, load_balancers, scheduling_strategy, tags) cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name) self.services[cluster_service_pair] = service @@ -958,22 +959,31 @@ class EC2ContainerServiceBackend(BaseBackend): yield task_fam - def list_tags_for_resource(self, resource_arn): - """Currently only implemented for task definitions""" + @staticmethod + def _parse_resource_arn(resource_arn): match = re.match( "^arn:aws:ecs:(?P[^:]+):(?P[^:]+):(?P[^:]+)/(?P.*)$", resource_arn) if not match: raise JsonRESTError('InvalidParameterException', 'The ARN provided is invalid.') + return match.groupdict() - service = match.group("service") - if service == "task-definition": + def list_tags_for_resource(self, resource_arn): + """Currently implemented only for task definitions and services""" + parsed_arn = self._parse_resource_arn(resource_arn) + if parsed_arn["service"] == "task-definition": for task_definition in self.task_definitions.values(): for revision in task_definition.values(): if revision.arn == resource_arn: return revision.tags else: raise TaskDefinitionNotFoundException() + elif parsed_arn["service"] == "service": + for service in self.services.values(): + if service.arn == resource_arn: + return service.tags + else: + raise ServiceNotFoundException(service_name=parsed_arn["id"]) raise NotImplementedError() def _get_last_task_definition_revision_id(self, family): @@ -981,6 +991,42 @@ class EC2ContainerServiceBackend(BaseBackend): if definitions: return max(definitions.keys()) + def tag_resource(self, resource_arn, tags): + """Currently implemented only for services""" + parsed_arn = self._parse_resource_arn(resource_arn) + if parsed_arn["service"] == "service": + for service in self.services.values(): + if service.arn == resource_arn: + service.tags = self._merge_tags(service.tags, tags) + return {} + else: + raise ServiceNotFoundException(service_name=parsed_arn["id"]) + raise NotImplementedError() + + def _merge_tags(self, existing_tags, new_tags): + merged_tags = new_tags + new_keys = self._get_keys(new_tags) + for existing_tag in existing_tags: + if existing_tag["key"] not in new_keys: + merged_tags.append(existing_tag) + return merged_tags + + @staticmethod + def _get_keys(tags): + return [tag['key'] for tag in tags] + + def untag_resource(self, resource_arn, tag_keys): + """Currently implemented only for services""" + parsed_arn = self._parse_resource_arn(resource_arn) + if parsed_arn["service"] == "service": + for service in self.services.values(): + if service.arn == resource_arn: + service.tags = [tag for tag in service.tags if tag["key"] not in tag_keys] + return {} + else: + raise ServiceNotFoundException(service_name=parsed_arn["id"]) + raise NotImplementedError() + available_regions = boto3.session.Session().get_available_regions("ecs") ecs_backends = {region: EC2ContainerServiceBackend(region) for region in available_regions} diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index abb79ea78..053b079b9 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -156,8 +156,9 @@ class EC2ContainerServiceResponse(BaseResponse): desired_count = self._get_int_param('desiredCount') load_balancers = self._get_param('loadBalancers') scheduling_strategy = self._get_param('schedulingStrategy') + tags = self._get_param('tags') service = self.ecs_backend.create_service( - cluster_str, service_name, task_definition_str, desired_count, load_balancers, scheduling_strategy) + cluster_str, service_name, task_definition_str, desired_count, load_balancers, scheduling_strategy, tags) return json.dumps({ 'service': service.response_object }) @@ -319,3 +320,15 @@ class EC2ContainerServiceResponse(BaseResponse): resource_arn = self._get_param('resourceArn') tags = self.ecs_backend.list_tags_for_resource(resource_arn) return json.dumps({'tags': tags}) + + def tag_resource(self): + resource_arn = self._get_param('resourceArn') + tags = self._get_param('tags') + results = self.ecs_backend.tag_resource(resource_arn, tags) + return json.dumps(results) + + def untag_resource(self): + resource_arn = self._get_param('resourceArn') + tag_keys = self._get_param('tagKeys') + results = self.ecs_backend.untag_resource(resource_arn, tag_keys) + return json.dumps(results) diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 9937af26b..3e2e08068 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -2360,3 +2360,229 @@ def test_list_tags_for_resource_unknown(): client.list_tags_for_resource(resourceArn=task_definition_arn) except ClientError as err: err.response['Error']['Code'].should.equal('ClientException') + + +@mock_ecs +def test_list_tags_for_resource_ecs_service(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2, + tags=[ + {'key': 'createdBy', 'value': 'moto-unittest'}, + {'key': 'foo', 'value': 'bar'}, + ] + ) + response = client.list_tags_for_resource(resourceArn=response['service']['serviceArn']) + type(response['tags']).should.be(list) + response['tags'].should.equal([ + {'key': 'createdBy', 'value': 'moto-unittest'}, + {'key': 'foo', 'value': 'bar'}, + ]) + + +@mock_ecs +def test_list_tags_for_resource_unknown_service(): + client = boto3.client('ecs', region_name='us-east-1') + service_arn = 'arn:aws:ecs:us-east-1:012345678910:service/unknown:1' + try: + client.list_tags_for_resource(resourceArn=service_arn) + except ClientError as err: + err.response['Error']['Code'].should.equal('ServiceNotFoundException') + + +@mock_ecs +def test_ecs_service_tag_resource(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2 + ) + client.tag_resource( + resourceArn=response['service']['serviceArn'], + tags=[ + {'key': 'createdBy', 'value': 'moto-unittest'}, + {'key': 'foo', 'value': 'bar'}, + ] + ) + response = client.list_tags_for_resource(resourceArn=response['service']['serviceArn']) + type(response['tags']).should.be(list) + response['tags'].should.equal([ + {'key': 'createdBy', 'value': 'moto-unittest'}, + {'key': 'foo', 'value': 'bar'}, + ]) + + +@mock_ecs +def test_ecs_service_tag_resource_overwrites_tag(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2, + tags=[ + {'key': 'foo', 'value': 'bar'}, + ] + ) + client.tag_resource( + resourceArn=response['service']['serviceArn'], + tags=[ + {'key': 'createdBy', 'value': 'moto-unittest'}, + {'key': 'foo', 'value': 'hello world'}, + ] + ) + response = client.list_tags_for_resource(resourceArn=response['service']['serviceArn']) + type(response['tags']).should.be(list) + response['tags'].should.equal([ + {'key': 'createdBy', 'value': 'moto-unittest'}, + {'key': 'foo', 'value': 'hello world'}, + ]) + + +@mock_ecs +def test_ecs_service_untag_resource(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2, + tags=[ + {'key': 'foo', 'value': 'bar'}, + ] + ) + client.untag_resource( + resourceArn=response['service']['serviceArn'], + tagKeys=['foo'] + ) + response = client.list_tags_for_resource(resourceArn=response['service']['serviceArn']) + response['tags'].should.equal([]) + + +@mock_ecs +def test_ecs_service_untag_resource_multiple_tags(): + client = boto3.client('ecs', region_name='us-east-1') + _ = client.create_cluster( + clusterName='test_ecs_cluster' + ) + _ = client.register_task_definition( + family='test_ecs_task', + containerDefinitions=[ + { + 'name': 'hello_world', + 'image': 'docker/hello-world:latest', + 'cpu': 1024, + 'memory': 400, + 'essential': True, + 'environment': [{ + 'name': 'AWS_ACCESS_KEY_ID', + 'value': 'SOME_ACCESS_KEY' + }], + 'logConfiguration': {'logDriver': 'json-file'} + } + ] + ) + response = client.create_service( + cluster='test_ecs_cluster', + serviceName='test_ecs_service', + taskDefinition='test_ecs_task', + desiredCount=2, + tags=[ + {'key': 'foo', 'value': 'bar'}, + {'key': 'createdBy', 'value': 'moto-unittest'}, + {'key': 'hello', 'value': 'world'}, + ] + ) + client.untag_resource( + resourceArn=response['service']['serviceArn'], + tagKeys=['foo', 'createdBy'] + ) + response = client.list_tags_for_resource(resourceArn=response['service']['serviceArn']) + response['tags'].should.equal([ + {'key': 'hello', 'value': 'world'}, + ])