Added ecs attributes methods
This commit is contained in:
parent
eccb536d8a
commit
6e28c58e26
@ -1483,10 +1483,10 @@
|
|||||||
- [ ] post_content
|
- [ ] post_content
|
||||||
- [ ] post_text
|
- [ ] post_text
|
||||||
|
|
||||||
## ecs - 74% implemented
|
## ecs - 90% implemented
|
||||||
- [X] create_cluster
|
- [X] create_cluster
|
||||||
- [X] create_service
|
- [X] create_service
|
||||||
- [ ] delete_attributes
|
- [X] delete_attributes
|
||||||
- [X] delete_cluster
|
- [X] delete_cluster
|
||||||
- [X] delete_service
|
- [X] delete_service
|
||||||
- [X] deregister_container_instance
|
- [X] deregister_container_instance
|
||||||
@ -1496,15 +1496,15 @@
|
|||||||
- [X] describe_services
|
- [X] describe_services
|
||||||
- [X] describe_task_definition
|
- [X] describe_task_definition
|
||||||
- [X] describe_tasks
|
- [X] describe_tasks
|
||||||
- [ ] discover_poll_endpoint
|
- [X] discover_poll_endpoint
|
||||||
- [ ] list_attributes
|
- [X] list_attributes
|
||||||
- [X] list_clusters
|
- [X] list_clusters
|
||||||
- [X] list_container_instances
|
- [X] list_container_instances
|
||||||
- [X] list_services
|
- [X] list_services
|
||||||
- [ ] list_task_definition_families
|
- [X] list_task_definition_families
|
||||||
- [X] list_task_definitions
|
- [X] list_task_definitions
|
||||||
- [X] list_tasks
|
- [X] list_tasks
|
||||||
- [ ] put_attributes
|
- [X] put_attributes
|
||||||
- [X] register_container_instance
|
- [X] register_container_instance
|
||||||
- [X] register_task_definition
|
- [X] register_task_definition
|
||||||
- [X] run_task
|
- [X] run_task
|
||||||
|
@ -4,6 +4,7 @@ from datetime import datetime
|
|||||||
from random import random, randint
|
from random import random, randint
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from moto.core.exceptions import JsonRESTError
|
||||||
from moto.core import BaseBackend, BaseModel
|
from moto.core import BaseBackend, BaseModel
|
||||||
from moto.ec2 import ec2_backends
|
from moto.ec2 import ec2_backends
|
||||||
from copy import copy
|
from copy import copy
|
||||||
@ -148,7 +149,7 @@ class Task(BaseObject):
|
|||||||
resource_requirements, overrides={}, started_by=''):
|
resource_requirements, overrides={}, started_by=''):
|
||||||
self.cluster_arn = cluster.arn
|
self.cluster_arn = cluster.arn
|
||||||
self.task_arn = 'arn:aws:ecs:us-east-1:012345678910:task/{0}'.format(
|
self.task_arn = 'arn:aws:ecs:us-east-1:012345678910:task/{0}'.format(
|
||||||
str(uuid.uuid1()))
|
str(uuid.uuid4()))
|
||||||
self.container_instance_arn = container_instance_arn
|
self.container_instance_arn = container_instance_arn
|
||||||
self.last_status = 'RUNNING'
|
self.last_status = 'RUNNING'
|
||||||
self.desired_status = 'RUNNING'
|
self.desired_status = 'RUNNING'
|
||||||
@ -288,7 +289,7 @@ class ContainerInstance(BaseObject):
|
|||||||
'stringSetValue': [],
|
'stringSetValue': [],
|
||||||
'type': 'STRINGSET'}]
|
'type': 'STRINGSET'}]
|
||||||
self.container_instance_arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format(
|
self.container_instance_arn = "arn:aws:ecs:us-east-1:012345678910:container-instance/{0}".format(
|
||||||
str(uuid.uuid1()))
|
str(uuid.uuid4()))
|
||||||
self.pending_tasks_count = 0
|
self.pending_tasks_count = 0
|
||||||
self.remaining_resources = [
|
self.remaining_resources = [
|
||||||
{'doubleValue': 0.0,
|
{'doubleValue': 0.0,
|
||||||
@ -321,6 +322,8 @@ class ContainerInstance(BaseObject):
|
|||||||
'dockerVersion': 'DockerVersion: 1.5.0'
|
'dockerVersion': 'DockerVersion: 1.5.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.attributes = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def response_object(self):
|
def response_object(self):
|
||||||
response_object = self.gen_response_object()
|
response_object = self.gen_response_object()
|
||||||
@ -766,6 +769,102 @@ class EC2ContainerServiceBackend(BaseBackend):
|
|||||||
raise Exception("{0} is not a cluster".format(cluster_name))
|
raise Exception("{0} is not a cluster".format(cluster_name))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def put_attributes(self, cluster_name, attributes=None):
|
||||||
|
if cluster_name is None or cluster_name not in self.clusters:
|
||||||
|
raise JsonRESTError('ClusterNotFoundException', 'Cluster not found', status=400)
|
||||||
|
|
||||||
|
if attributes is None:
|
||||||
|
raise JsonRESTError('InvalidParameterException', 'attributes value is required')
|
||||||
|
|
||||||
|
for attr in attributes:
|
||||||
|
self._put_attribute(cluster_name, attr['name'], attr.get('value'), attr.get('targetId'), attr.get('targetType'))
|
||||||
|
|
||||||
|
def _put_attribute(self, cluster_name, name, value=None, target_id=None, target_type=None):
|
||||||
|
if target_id is None and target_type is None:
|
||||||
|
for instance in self.container_instances[cluster_name].values():
|
||||||
|
instance.attributes[name] = value
|
||||||
|
elif target_type is None:
|
||||||
|
# targetId is full container instance arn
|
||||||
|
try:
|
||||||
|
arn = target_id.rsplit('/', 1)[-1]
|
||||||
|
self.container_instances[cluster_name][arn].attributes[name] = value
|
||||||
|
except KeyError:
|
||||||
|
raise JsonRESTError('TargetNotFoundException', 'Could not find {0}'.format(target_id))
|
||||||
|
else:
|
||||||
|
# targetId is container uuid, targetType must be container-instance
|
||||||
|
try:
|
||||||
|
if target_type != 'container-instance':
|
||||||
|
raise JsonRESTError('TargetNotFoundException', 'Could not find {0}'.format(target_id))
|
||||||
|
|
||||||
|
self.container_instances[cluster_name][target_id].attributes[name] = value
|
||||||
|
except KeyError:
|
||||||
|
raise JsonRESTError('TargetNotFoundException', 'Could not find {0}'.format(target_id))
|
||||||
|
|
||||||
|
def list_attributes(self, target_type, cluster_name=None, attr_name=None, attr_value=None, max_results=None, next_token=None):
|
||||||
|
if target_type != 'container-instance':
|
||||||
|
raise JsonRESTError('InvalidParameterException', 'targetType must be container-instance')
|
||||||
|
|
||||||
|
filters = [lambda x: True]
|
||||||
|
|
||||||
|
# item will be {0 cluster_name, 1 arn, 2 name, 3 value}
|
||||||
|
if cluster_name is not None:
|
||||||
|
filters.append(lambda item: item[0] == cluster_name)
|
||||||
|
if attr_name:
|
||||||
|
filters.append(lambda item: item[2] == attr_name)
|
||||||
|
if attr_name:
|
||||||
|
filters.append(lambda item: item[3] == attr_value)
|
||||||
|
|
||||||
|
all_attrs = []
|
||||||
|
for cluster_name, cobj in self.container_instances.items():
|
||||||
|
for container_instance in cobj.values():
|
||||||
|
for key, value in container_instance.attributes.items():
|
||||||
|
all_attrs.append((cluster_name, container_instance.container_instance_arn, key, value))
|
||||||
|
|
||||||
|
return filter(lambda x: all(f(x) for f in filters), all_attrs)
|
||||||
|
|
||||||
|
def delete_attributes(self, cluster_name, attributes=None):
|
||||||
|
if cluster_name is None or cluster_name not in self.clusters:
|
||||||
|
raise JsonRESTError('ClusterNotFoundException', 'Cluster not found', status=400)
|
||||||
|
|
||||||
|
if attributes is None:
|
||||||
|
raise JsonRESTError('InvalidParameterException', 'attributes value is required')
|
||||||
|
|
||||||
|
for attr in attributes:
|
||||||
|
self._delete_attribute(cluster_name, attr['name'], attr.get('value'), attr.get('targetId'), attr.get('targetType'))
|
||||||
|
|
||||||
|
def _delete_attribute(self, cluster_name, name, value=None, target_id=None, target_type=None):
|
||||||
|
if target_id is None and target_type is None:
|
||||||
|
for instance in self.container_instances[cluster_name].values():
|
||||||
|
if name in instance.attributes and instance.attributes[name] == value:
|
||||||
|
del instance.attributes[name]
|
||||||
|
elif target_type is None:
|
||||||
|
# targetId is full container instance arn
|
||||||
|
try:
|
||||||
|
arn = target_id.rsplit('/', 1)[-1]
|
||||||
|
instance = self.container_instances[cluster_name][arn]
|
||||||
|
if name in instance.attributes and instance.attributes[name] == value:
|
||||||
|
del instance.attributes[name]
|
||||||
|
except KeyError:
|
||||||
|
raise JsonRESTError('TargetNotFoundException', 'Could not find {0}'.format(target_id))
|
||||||
|
else:
|
||||||
|
# targetId is container uuid, targetType must be container-instance
|
||||||
|
try:
|
||||||
|
if target_type != 'container-instance':
|
||||||
|
raise JsonRESTError('TargetNotFoundException', 'Could not find {0}'.format(target_id))
|
||||||
|
|
||||||
|
instance = self.container_instances[cluster_name][target_id]
|
||||||
|
if name in instance.attributes and instance.attributes[name] == value:
|
||||||
|
del instance.attributes[name]
|
||||||
|
except KeyError:
|
||||||
|
raise JsonRESTError('TargetNotFoundException', 'Could not find {0}'.format(target_id))
|
||||||
|
|
||||||
|
def list_task_definition_families(self, family_prefix=None, status=None, max_results=None, next_token=None):
|
||||||
|
for task_fam in self.task_definitions:
|
||||||
|
if family_prefix is not None and not task_fam.startswith(family_prefix):
|
||||||
|
continue
|
||||||
|
|
||||||
|
yield task_fam
|
||||||
|
|
||||||
|
|
||||||
ecs_backends = {}
|
ecs_backends = {}
|
||||||
for region, ec2_backend in ec2_backends.items():
|
for region, ec2_backend in ec2_backends.items():
|
||||||
|
@ -9,6 +9,12 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ecs_backend(self):
|
def ecs_backend(self):
|
||||||
|
"""
|
||||||
|
ECS Backend
|
||||||
|
|
||||||
|
:return: ECS Backend object
|
||||||
|
:rtype: moto.ecs.models.EC2ContainerServiceBackend
|
||||||
|
"""
|
||||||
return ecs_backends[self.region]
|
return ecs_backends[self.region]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -34,7 +40,7 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||||||
cluster_arns = self.ecs_backend.list_clusters()
|
cluster_arns = self.ecs_backend.list_clusters()
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
'clusterArns': cluster_arns
|
'clusterArns': cluster_arns
|
||||||
# 'nextToken': str(uuid.uuid1())
|
# 'nextToken': str(uuid.uuid4())
|
||||||
})
|
})
|
||||||
|
|
||||||
def describe_clusters(self):
|
def describe_clusters(self):
|
||||||
@ -66,7 +72,7 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||||||
task_definition_arns = self.ecs_backend.list_task_definitions()
|
task_definition_arns = self.ecs_backend.list_task_definitions()
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
'taskDefinitionArns': task_definition_arns
|
'taskDefinitionArns': task_definition_arns
|
||||||
# 'nextToken': str(uuid.uuid1())
|
# 'nextToken': str(uuid.uuid4())
|
||||||
})
|
})
|
||||||
|
|
||||||
def describe_task_definition(self):
|
def describe_task_definition(self):
|
||||||
@ -159,7 +165,7 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||||||
return json.dumps({
|
return json.dumps({
|
||||||
'serviceArns': service_arns
|
'serviceArns': service_arns
|
||||||
# ,
|
# ,
|
||||||
# 'nextToken': str(uuid.uuid1())
|
# 'nextToken': str(uuid.uuid4())
|
||||||
})
|
})
|
||||||
|
|
||||||
def describe_services(self):
|
def describe_services(self):
|
||||||
@ -245,3 +251,62 @@ class EC2ContainerServiceResponse(BaseResponse):
|
|||||||
'failures': [ci.response_object for ci in failures],
|
'failures': [ci.response_object for ci in failures],
|
||||||
'containerInstances': [ci.response_object for ci in container_instances]
|
'containerInstances': [ci.response_object for ci in container_instances]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def put_attributes(self):
|
||||||
|
cluster_name = self._get_param('cluster')
|
||||||
|
attributes = self._get_param('attributes')
|
||||||
|
|
||||||
|
self.ecs_backend.put_attributes(cluster_name, attributes)
|
||||||
|
|
||||||
|
return json.dumps({'attributes': attributes})
|
||||||
|
|
||||||
|
def list_attributes(self):
|
||||||
|
cluster_name = self._get_param('cluster')
|
||||||
|
attr_name = self._get_param('attributeName')
|
||||||
|
attr_value = self._get_param('attributeValue')
|
||||||
|
target_type = self._get_param('targetType')
|
||||||
|
max_results = self._get_param('maxResults')
|
||||||
|
next_token = self._get_param('nextToken')
|
||||||
|
|
||||||
|
results = self.ecs_backend.list_attributes(target_type, cluster_name, attr_name, attr_value, max_results, next_token)
|
||||||
|
# Result will be [item will be {0 cluster_name, 1 arn, 2 name, 3 value}]
|
||||||
|
|
||||||
|
formatted_results = []
|
||||||
|
for _, arn, name, value in results:
|
||||||
|
tmp_result = {
|
||||||
|
'name': name,
|
||||||
|
'targetId': arn
|
||||||
|
}
|
||||||
|
if value is not None:
|
||||||
|
tmp_result['value'] = value
|
||||||
|
formatted_results.append(tmp_result)
|
||||||
|
|
||||||
|
return json.dumps({'attributes': formatted_results})
|
||||||
|
|
||||||
|
def delete_attributes(self):
|
||||||
|
cluster_name = self._get_param('cluster')
|
||||||
|
attributes = self._get_param('attributes')
|
||||||
|
|
||||||
|
self.ecs_backend.delete_attributes(cluster_name, attributes)
|
||||||
|
|
||||||
|
return json.dumps({'attributes': attributes})
|
||||||
|
|
||||||
|
def discover_poll_endpoint(self):
|
||||||
|
# Here are the arguments, this api is used by the ecs client so obviously no decent
|
||||||
|
# documentation. Hence I've responded with valid but useless data
|
||||||
|
# cluster_name = self._get_param('cluster')
|
||||||
|
# instance = self._get_param('containerInstance')
|
||||||
|
return json.dumps({
|
||||||
|
'endpoint': 'http://localhost',
|
||||||
|
'telemetryEndpoint': 'http://localhost'
|
||||||
|
})
|
||||||
|
|
||||||
|
def list_task_definition_families(self):
|
||||||
|
family_prefix = self._get_param('familyPrefix')
|
||||||
|
status = self._get_param('status')
|
||||||
|
max_results = self._get_param('maxResults')
|
||||||
|
next_token = self._get_param('nextToken')
|
||||||
|
|
||||||
|
results = self.ecs_backend.list_task_definition_families(family_prefix, status, max_results, next_token)
|
||||||
|
|
||||||
|
return json.dumps({'families': list(results)})
|
||||||
|
@ -1611,6 +1611,152 @@ def test_update_service_through_cloudformation_should_trigger_replacement():
|
|||||||
len(resp['serviceArns']).should.equal(1)
|
len(resp['serviceArns']).should.equal(1)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ec2
|
||||||
|
@mock_ecs
|
||||||
|
def test_attributes():
|
||||||
|
# Combined put, list delete attributes into the same test due to the amount of setup
|
||||||
|
ecs_client = boto3.client('ecs', region_name='us-east-1')
|
||||||
|
ec2 = boto3.resource('ec2', region_name='us-east-1')
|
||||||
|
|
||||||
|
test_cluster_name = 'test_ecs_cluster'
|
||||||
|
|
||||||
|
_ = ecs_client.create_cluster(
|
||||||
|
clusterName=test_cluster_name
|
||||||
|
)
|
||||||
|
|
||||||
|
test_instance = ec2.create_instances(
|
||||||
|
ImageId="ami-1234abcd",
|
||||||
|
MinCount=1,
|
||||||
|
MaxCount=1,
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
instance_id_document = json.dumps(
|
||||||
|
ec2_utils.generate_instance_identity_document(test_instance)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = ecs_client.register_container_instance(
|
||||||
|
cluster=test_cluster_name,
|
||||||
|
instanceIdentityDocument=instance_id_document
|
||||||
|
)
|
||||||
|
|
||||||
|
response['containerInstance'][
|
||||||
|
'ec2InstanceId'].should.equal(test_instance.id)
|
||||||
|
full_arn1 = response['containerInstance']['containerInstanceArn']
|
||||||
|
|
||||||
|
test_instance = ec2.create_instances(
|
||||||
|
ImageId="ami-1234abcd",
|
||||||
|
MinCount=1,
|
||||||
|
MaxCount=1,
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
instance_id_document = json.dumps(
|
||||||
|
ec2_utils.generate_instance_identity_document(test_instance)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = ecs_client.register_container_instance(
|
||||||
|
cluster=test_cluster_name,
|
||||||
|
instanceIdentityDocument=instance_id_document
|
||||||
|
)
|
||||||
|
|
||||||
|
response['containerInstance'][
|
||||||
|
'ec2InstanceId'].should.equal(test_instance.id)
|
||||||
|
full_arn2 = response['containerInstance']['containerInstanceArn']
|
||||||
|
partial_arn2 = full_arn2.rsplit('/', 1)[-1]
|
||||||
|
|
||||||
|
full_arn2.should_not.equal(full_arn1) # uuid1 isnt unique enough when the pc is fast ;-)
|
||||||
|
|
||||||
|
# Ok set instance 1 with 1 attribute, instance 2 with another, and all of them with a 3rd.
|
||||||
|
ecs_client.put_attributes(
|
||||||
|
cluster=test_cluster_name,
|
||||||
|
attributes=[
|
||||||
|
{'name': 'env', 'value': 'prod'},
|
||||||
|
{'name': 'attr1', 'value': 'instance1', 'targetId': full_arn1},
|
||||||
|
{'name': 'attr1', 'value': 'instance2', 'targetId': partial_arn2, 'targetType': 'container-instance'}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = ecs_client.list_attributes(
|
||||||
|
cluster=test_cluster_name,
|
||||||
|
targetType='container-instance'
|
||||||
|
)
|
||||||
|
attrs = resp['attributes']
|
||||||
|
len(attrs).should.equal(4)
|
||||||
|
|
||||||
|
# Tests that the attrs have been set properly
|
||||||
|
len(list(filter(lambda item: item['name'] == 'env', attrs))).should.equal(2)
|
||||||
|
len(list(filter(lambda item: item['name'] == 'attr1' and item['value'] == 'instance1', attrs))).should.equal(1)
|
||||||
|
|
||||||
|
ecs_client.delete_attributes(
|
||||||
|
cluster=test_cluster_name,
|
||||||
|
attributes=[
|
||||||
|
{'name': 'attr1', 'value': 'instance2', 'targetId': partial_arn2, 'targetType': 'container-instance'}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = ecs_client.list_attributes(
|
||||||
|
cluster=test_cluster_name,
|
||||||
|
targetType='container-instance'
|
||||||
|
)
|
||||||
|
attrs = resp['attributes']
|
||||||
|
len(attrs).should.equal(3)
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecs
|
||||||
|
def test_poll_endpoint():
|
||||||
|
# Combined put, list delete attributes into the same test due to the amount of setup
|
||||||
|
ecs_client = boto3.client('ecs', region_name='us-east-1')
|
||||||
|
|
||||||
|
# Just a placeholder until someone actually wants useless data, just testing it doesnt raise an exception
|
||||||
|
resp = ecs_client.discover_poll_endpoint(cluster='blah', containerInstance='blah')
|
||||||
|
resp.should.contain('endpoint')
|
||||||
|
resp.should.contain('telemetryEndpoint')
|
||||||
|
|
||||||
|
|
||||||
|
@mock_ecs
|
||||||
|
def test_list_task_definition_families():
|
||||||
|
client = boto3.client('ecs', region_name='us-east-1')
|
||||||
|
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'}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
client.register_task_definition(
|
||||||
|
family='alt_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'}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
resp1 = client.list_task_definition_families()
|
||||||
|
resp2 = client.list_task_definition_families(familyPrefix='alt')
|
||||||
|
|
||||||
|
len(resp1['families']).should.equal(2)
|
||||||
|
len(resp2['families']).should.equal(1)
|
||||||
|
|
||||||
|
|
||||||
def _fetch_container_instance_resources(container_instance_description):
|
def _fetch_container_instance_resources(container_instance_description):
|
||||||
remaining_resources = {}
|
remaining_resources = {}
|
||||||
registered_resources = {}
|
registered_resources = {}
|
||||||
|
Loading…
Reference in New Issue
Block a user