Merge pull request #2450 from jimjshields/add-ecs-service-tag-resource
Add support for tagging and untagging ECS services
This commit is contained in:
commit
59543c404a
@ -2343,7 +2343,7 @@
|
|||||||
- [ ] upload_layer_part
|
- [ ] upload_layer_part
|
||||||
|
|
||||||
## ecs
|
## ecs
|
||||||
63% implemented
|
49% implemented
|
||||||
- [X] create_cluster
|
- [X] create_cluster
|
||||||
- [X] create_service
|
- [X] create_service
|
||||||
- [ ] create_task_set
|
- [ ] create_task_set
|
||||||
@ -2381,8 +2381,8 @@
|
|||||||
- [ ] submit_attachment_state_changes
|
- [ ] submit_attachment_state_changes
|
||||||
- [ ] submit_container_state_change
|
- [ ] submit_container_state_change
|
||||||
- [ ] submit_task_state_change
|
- [ ] submit_task_state_change
|
||||||
- [ ] tag_resource
|
- [x] tag_resource
|
||||||
- [ ] untag_resource
|
- [x] untag_resource
|
||||||
- [ ] update_container_agent
|
- [ ] update_container_agent
|
||||||
- [X] update_container_instances_state
|
- [X] update_container_instances_state
|
||||||
- [X] update_service
|
- [X] update_service
|
||||||
|
@ -192,7 +192,7 @@ class Task(BaseObject):
|
|||||||
|
|
||||||
class Service(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.cluster_arn = cluster.arn
|
||||||
self.arn = 'arn:aws:ecs:us-east-1:012345678910:service/{0}'.format(
|
self.arn = 'arn:aws:ecs:us-east-1:012345678910:service/{0}'.format(
|
||||||
service_name)
|
service_name)
|
||||||
@ -216,6 +216,7 @@ class Service(BaseObject):
|
|||||||
]
|
]
|
||||||
self.load_balancers = load_balancers if load_balancers is not None else []
|
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.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
|
self.pending_count = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -225,7 +226,7 @@ class Service(BaseObject):
|
|||||||
@property
|
@property
|
||||||
def response_object(self):
|
def response_object(self):
|
||||||
response_object = self.gen_response_object()
|
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['serviceName'] = self.name
|
||||||
response_object['serviceArn'] = self.arn
|
response_object['serviceArn'] = self.arn
|
||||||
response_object['schedulingStrategy'] = self.scheduling_strategy
|
response_object['schedulingStrategy'] = self.scheduling_strategy
|
||||||
@ -691,7 +692,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||||||
raise Exception("Could not find task {} on cluster {}".format(
|
raise Exception("Could not find task {} on cluster {}".format(
|
||||||
task_str, cluster_name))
|
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]
|
cluster_name = cluster_str.split('/')[-1]
|
||||||
if cluster_name in self.clusters:
|
if cluster_name in self.clusters:
|
||||||
cluster = self.clusters[cluster_name]
|
cluster = self.clusters[cluster_name]
|
||||||
@ -701,7 +702,7 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||||||
desired_count = desired_count if desired_count is not None else 0
|
desired_count = desired_count if desired_count is not None else 0
|
||||||
|
|
||||||
service = Service(cluster, service_name,
|
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)
|
cluster_service_pair = '{0}:{1}'.format(cluster_name, service_name)
|
||||||
self.services[cluster_service_pair] = service
|
self.services[cluster_service_pair] = service
|
||||||
|
|
||||||
@ -958,22 +959,31 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||||||
|
|
||||||
yield task_fam
|
yield task_fam
|
||||||
|
|
||||||
def list_tags_for_resource(self, resource_arn):
|
@staticmethod
|
||||||
"""Currently only implemented for task definitions"""
|
def _parse_resource_arn(resource_arn):
|
||||||
match = re.match(
|
match = re.match(
|
||||||
"^arn:aws:ecs:(?P<region>[^:]+):(?P<account_id>[^:]+):(?P<service>[^:]+)/(?P<id>.*)$",
|
"^arn:aws:ecs:(?P<region>[^:]+):(?P<account_id>[^:]+):(?P<service>[^:]+)/(?P<id>.*)$",
|
||||||
resource_arn)
|
resource_arn)
|
||||||
if not match:
|
if not match:
|
||||||
raise JsonRESTError('InvalidParameterException', 'The ARN provided is invalid.')
|
raise JsonRESTError('InvalidParameterException', 'The ARN provided is invalid.')
|
||||||
|
return match.groupdict()
|
||||||
|
|
||||||
service = match.group("service")
|
def list_tags_for_resource(self, resource_arn):
|
||||||
if service == "task-definition":
|
"""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 task_definition in self.task_definitions.values():
|
||||||
for revision in task_definition.values():
|
for revision in task_definition.values():
|
||||||
if revision.arn == resource_arn:
|
if revision.arn == resource_arn:
|
||||||
return revision.tags
|
return revision.tags
|
||||||
else:
|
else:
|
||||||
raise TaskDefinitionNotFoundException()
|
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()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def _get_last_task_definition_revision_id(self, family):
|
def _get_last_task_definition_revision_id(self, family):
|
||||||
@ -981,6 +991,42 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||||||
if definitions:
|
if definitions:
|
||||||
return max(definitions.keys())
|
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")
|
available_regions = boto3.session.Session().get_available_regions("ecs")
|
||||||
ecs_backends = {region: EC2ContainerServiceBackend(region) for region in available_regions}
|
ecs_backends = {region: EC2ContainerServiceBackend(region) for region in available_regions}
|
||||||
|
@ -156,8 +156,9 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||||||
desired_count = self._get_int_param('desiredCount')
|
desired_count = self._get_int_param('desiredCount')
|
||||||
load_balancers = self._get_param('loadBalancers')
|
load_balancers = self._get_param('loadBalancers')
|
||||||
scheduling_strategy = self._get_param('schedulingStrategy')
|
scheduling_strategy = self._get_param('schedulingStrategy')
|
||||||
|
tags = self._get_param('tags')
|
||||||
service = self.ecs_backend.create_service(
|
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({
|
return json.dumps({
|
||||||
'service': service.response_object
|
'service': service.response_object
|
||||||
})
|
})
|
||||||
@ -319,3 +320,15 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||||||
resource_arn = self._get_param('resourceArn')
|
resource_arn = self._get_param('resourceArn')
|
||||||
tags = self.ecs_backend.list_tags_for_resource(resource_arn)
|
tags = self.ecs_backend.list_tags_for_resource(resource_arn)
|
||||||
return json.dumps({'tags': tags})
|
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)
|
||||||
|
@ -2360,3 +2360,229 @@ def test_list_tags_for_resource_unknown():
|
|||||||
client.list_tags_for_resource(resourceArn=task_definition_arn)
|
client.list_tags_for_resource(resourceArn=task_definition_arn)
|
||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
err.response['Error']['Code'].should.equal('ClientException')
|
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'},
|
||||||
|
])
|
||||||
|
Loading…
Reference in New Issue
Block a user