Further implementation details to the ECS driver
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/a1adaae2 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/a1adaae2 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/a1adaae2 Branch: refs/heads/trunk Commit: a1adaae2df83ec7731a64eccbbcdbebd163a875e Parents: f4c1e2e Author: anthony-shaw <[email protected]> Authored: Tue Dec 29 21:57:57 2015 +1100 Committer: anthony-shaw <[email protected]> Committed: Tue Dec 29 21:57:57 2015 +1100 ---------------------------------------------------------------------- libcloud/container/base.py | 1 + libcloud/container/drivers/ecs.py | 157 +++++++++++++++---- .../test/container/fixtures/ecs/listtasks.json | 8 + .../fixtures/ecs/registertaskdefinition.json | 26 +++ .../test/container/fixtures/ecs/runtask.json | 37 +++++ .../test/container/fixtures/ecs/stoptask.json | 42 +++++ libcloud/test/container/test_ecs.py | 79 +++++++++- 7 files changed, 317 insertions(+), 33 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/a1adaae2/libcloud/container/base.py ---------------------------------------------------------------------- diff --git a/libcloud/container/base.py b/libcloud/container/base.py index b9750d4..48ee573 100644 --- a/libcloud/container/base.py +++ b/libcloud/container/base.py @@ -22,6 +22,7 @@ __all__ = [ 'Container', 'ContainerImage', 'ContainerCluster', + 'ClusterLocation', 'ContainerDriver' ] http://git-wip-us.apache.org/repos/asf/libcloud/blob/a1adaae2/libcloud/container/drivers/ecs.py ---------------------------------------------------------------------- diff --git a/libcloud/container/drivers/ecs.py b/libcloud/container/drivers/ecs.py index e3a356f..9aa98d3 100644 --- a/libcloud/container/drivers/ecs.py +++ b/libcloud/container/drivers/ecs.py @@ -71,7 +71,6 @@ class ElasticContainerDriver(ContainerDriver): params = {'Action': 'DescribeClusters'} data = self.connection.request( ROOT, - params=params, headers=self._get_headers(params['Action']) ).object return self._to_clusters(data) @@ -92,7 +91,6 @@ class ElasticContainerDriver(ContainerDriver): request = {'clusterName': name} response = self.connection.request( ROOT, - params=params, data=request, headers=self._get_headers(params['Action']) ).object @@ -109,7 +107,6 @@ class ElasticContainerDriver(ContainerDriver): request = {'cluster': cluster.id} data = self.connection.request( ROOT, - params=params, data=request, headers=self._get_headers(params['Action']) ).object @@ -129,7 +126,8 @@ class ElasticContainerDriver(ContainerDriver): def list_images(self): """ - List the installed container images + List the installed container images, in ECS these are + equivalent to the containers within task definitions. :rtype: ``list`` of :class:`ContainerImage` """ @@ -148,25 +146,26 @@ class ElasticContainerDriver(ContainerDriver): :rtype: ``list`` of :class:`Container` """ - params = {'Action': 'DescribeTasks'} request = None if cluster is not None: request = {'cluster': cluster.id} - response = self.connection.request( + if image is not None: + request['family'] = image.name + list_response = self.connection.request( ROOT, - params=params, data=request, - headers=self._get_headers(params['Action']) + headers=self._get_headers('ListTasks') ).object - containers = [] - for task in response['tasks']: - containers.extend(self._to_containers(task)) + containers = self.ex_list_containers_for_task( + list_response['taskArns']) return containers def deploy_container(self, name, image, cluster=None, - parameters=None, start=True): + parameters=None, start=True, ex_cpu=10, ex_memory=500, + ex_container_port=None, ex_host_port=None): """ - Deploy an installed container image + Creates a task definition from a container image that can be run + in a cluster. :param name: The name of the new container :type name: ``str`` @@ -185,8 +184,47 @@ class ElasticContainerDriver(ContainerDriver): :rtype: :class:`Container` """ - raise NotImplementedError( - 'deploy_container not implemented for this driver') + data = {} + data['containerDefinitions'] = [ + { + "mountPoints": [], + "name": name, + "image": image.name, + "cpu": ex_cpu, + "environment": [], + "memory": ex_memory, + "portMappings": [ + { + "containerPort": ex_container_port, + "hostPort": ex_host_port + } + ], + "essential": True, + "volumesFrom": [] + } + ] + data['family'] = name + response = self.connection.request( + ROOT, + data=data, + headers=self._get_headers('RegisterTaskDefinition') + ).object + if start: + return self.ex_start_task( + response['taskDefinition']['taskDefinitionArn'])[0] + else: + return Container( + id=None, + name=name, + image=image, + state=ContainerState.RUNNING, + ip_addresses=[], + extra={ + 'taskDefinitionArn': + response['taskDefinition']['taskDefinitionArn'] + }, + driver=self.connection.driver + ) def get_container(self, id): """ @@ -197,20 +235,22 @@ class ElasticContainerDriver(ContainerDriver): :rtype: :class:`Container` """ - raise NotImplementedError( - 'get_container not implemented for this driver') + containers = self.ex_list_containers_for_task([id]) + return containers[0] - def start_container(self, container): + def start_container(self, container, count=1): """ - Start a deployed container + Start a deployed task :param container: The container to start :type container: :class:`Container` + :param count: Number of containers to start + :type count: ``int`` + :rtype: :class:`Container` """ - raise NotImplementedError( - 'start_container not implemented for this driver') + return self.ex_start_task(container.extra['taskDefinitionArn'], count) def stop_container(self, container): """ @@ -221,8 +261,17 @@ class ElasticContainerDriver(ContainerDriver): :rtype: :class:`Container` """ - raise NotImplementedError( - 'stop_container not implemented for this driver') + request = {'task': container.extra['taskArn']} + response = self.connection.request( + ROOT, + data=request, + headers=self._get_headers('StopTask') + ).object + containers = [] + containers.extend(self._to_containers( + response['task'], + container.extra['taskDefinitionArn'])) + return containers def restart_container(self, container): """ @@ -233,8 +282,8 @@ class ElasticContainerDriver(ContainerDriver): :rtype: :class:`Container` """ - raise NotImplementedError( - 'restart_container not implemented for this driver') + self.stop_container(container) + return self.start_container(container) def destroy_container(self, container): """ @@ -245,8 +294,53 @@ class ElasticContainerDriver(ContainerDriver): :rtype: :class:`Container` """ - raise NotImplementedError( - 'destroy_container not implemented for this driver') + return self.stop_container(container) + + def ex_start_task(self, task_arn, count=1): + """ + Run a task definition and get the containers + + :param task_arn: The task ARN to Run + :type task_arn: ``str`` + + :param count: The number of containers to start + :type count: ``int`` + + :rtype: ``list`` of :class:`Container` + """ + request = None + request = {'count': count, + 'taskDefinition': task_arn} + response = self.connection.request( + ROOT, + data=request, + headers=self._get_headers('RunTask') + ).object + containers = [] + for task in response['tasks']: + containers.extend(self._to_containers(task, task_arn)) + return containers + + def ex_list_containers_for_task(self, task_arns): + """ + Get a list of containers by ID collection (ARN) + + :param task_arns: The list of ARNs + :type task_arns: ``list`` of ``str`` + + :rtype: ``list`` of :class:`Container` + """ + describe_request = {'tasks': task_arns} + descripe_response = self.connection.request( + ROOT, + data=describe_request, + headers=self._get_headers('DescribeTasks') + ).object + containers = [] + for task in descripe_response['tasks']: + containers.extend(self._to_containers( + task, task['taskDefinitionArn'])) + return containers def _get_headers(self, action): return {'x-amz-target': '%s.%s' % @@ -265,13 +359,13 @@ class ElasticContainerDriver(ContainerDriver): driver=self.connection.driver ) - def _to_containers(self, data): + def _to_containers(self, data, task_definition_arn): clusters = [] for cluster in data['containers']: - clusters.append(self._to_container(cluster)) + clusters.append(self._to_container(cluster, task_definition_arn)) return clusters - def _to_container(self, data): + def _to_container(self, data, task_definition_arn): return Container( id=data['containerArn'], name=data['name'], @@ -285,7 +379,8 @@ class ElasticContainerDriver(ContainerDriver): ip_addresses=None, state=self.status_map.get(data['lastStatus'], None), extra={ - 'taskArn': data['taskArn'] + 'taskArn': data['taskArn'], + 'taskDefinitionArn': task_definition_arn }, driver=self.connection.driver ) http://git-wip-us.apache.org/repos/asf/libcloud/blob/a1adaae2/libcloud/test/container/fixtures/ecs/listtasks.json ---------------------------------------------------------------------- diff --git a/libcloud/test/container/fixtures/ecs/listtasks.json b/libcloud/test/container/fixtures/ecs/listtasks.json new file mode 100644 index 0000000..644ff73 --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/listtasks.json @@ -0,0 +1,8 @@ +{ + "taskArns": [ + "arn:aws:ecs:us-east-1:012345678910:task/0b69d5c0-d655-4695-98cd-5d2d526d9d5a", + "arn:aws:ecs:us-east-1:012345678910:task/51a01bdf-d00e-487e-ab14-7645330b6207", + "arn:aws:ecs:us-east-1:012345678910:task/b0b28bb8-2be3-4810-b52b-88df129d893c", + "arn:aws:ecs:us-east-1:012345678910:task/c09f0188-7f87-4b0f-bfc3-16296622b6fe" + ] +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/a1adaae2/libcloud/test/container/fixtures/ecs/registertaskdefinition.json ---------------------------------------------------------------------- diff --git a/libcloud/test/container/fixtures/ecs/registertaskdefinition.json b/libcloud/test/container/fixtures/ecs/registertaskdefinition.json new file mode 100644 index 0000000..163061d --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/registertaskdefinition.json @@ -0,0 +1,26 @@ +{ + "taskDefinition": { + "containerDefinitions": [ + { + "cpu": 10, + "environment": [ + { + "name": "MYSQL_ROOT_PASSWORD", + "value": "password" + } + ], + "essential": true, + "image": "mysql", + "memory": 500, + "mountPoints": [], + "name": "mysql", + "portMappings": [], + "volumesFrom": [] + } + ], + "family": "jim", + "revision": 11, + "taskDefinitionArn": "arn:aws:ecs:us-east-1:012345678910:task-definition/hello_world:11", + "volumes": [] + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/a1adaae2/libcloud/test/container/fixtures/ecs/runtask.json ---------------------------------------------------------------------- diff --git a/libcloud/test/container/fixtures/ecs/runtask.json b/libcloud/test/container/fixtures/ecs/runtask.json new file mode 100644 index 0000000..70896e5 --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/runtask.json @@ -0,0 +1,37 @@ +{ + "failures": [], + "tasks": [ + { + "clusterArn": "arn:aws:ecs:us-east-1:012345678910:cluster/default", + "containerInstanceArn": "arn:aws:ecs:us-east-1:012345678910:container-instance/cf447635-790d-477d-be24-58a9cb819d45", + "containers": [ + { + "containerArn": "arn:aws:ecs:us-east-1:012345678910:container/e1ed7aac-d9b2-4315-8726-d2432bf11868", + "lastStatus": "PENDING", + "name": "wordpress", + "taskArn": "arn:aws:ecs:us-east-1:012345678910:task/d8c67b3c-ac87-4ffe-a847-4785bc3a8b55" + }, + { + "containerArn": "arn:aws:ecs:us-east-1:012345678910:container/4b69fabd-991d-4781-bbf9-7efa03c754aa", + "lastStatus": "PENDING", + "name": "mysql", + "taskArn": "arn:aws:ecs:us-east-1:012345678910:task/d8c67b3c-ac87-4ffe-a847-4785bc3a8b55" + } + ], + "desiredStatus": "RUNNING", + "lastStatus": "PENDING", + "overrides": { + "containerOverrides": [ + { + "name": "wordpress" + }, + { + "name": "mysql" + } + ] + }, + "taskArn": "arn:aws:ecs:us-east-1:012345678910:task/d8c67b3c-ac87-4ffe-a847-4785bc3a8b55", + "taskDefinitionArn": "arn:aws:ecs:us-east-1:012345678910:task-definition/hello_world:11" + } + ] +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/a1adaae2/libcloud/test/container/fixtures/ecs/stoptask.json ---------------------------------------------------------------------- diff --git a/libcloud/test/container/fixtures/ecs/stoptask.json b/libcloud/test/container/fixtures/ecs/stoptask.json new file mode 100644 index 0000000..72b5ebb --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/stoptask.json @@ -0,0 +1,42 @@ +{ + "task": { + "clusterArn": "arn:aws:ecs:us-east-1:012345678910:cluster/default", + "containerInstanceArn": "arn:aws:ecs:us-east-1:012345678910:container-instance/8db248d6-16a7-42b5-b9f9-43d3b1ad9430", + "containers": [ + { + "containerArn": "arn:aws:ecs:us-east-1:012345678910:container/05a5528c-77f6-4e5b-8f9a-2b0a1928a926", + "lastStatus": "RUNNING", + "name": "mysql", + "networkBindings": [], + "taskArn": "arn:aws:ecs:us-east-1:012345678910:task/a126249b-b7e4-4b06-9d8f-1b56e75a99b5" + }, + { + "containerArn": "arn:aws:ecs:us-east-1:012345678910:container/37234a82-77f6-41d7-b54b-591f1e278093", + "lastStatus": "RUNNING", + "name": "wordpress", + "networkBindings": [ + { + "bindIP": "0.0.0.0", + "containerPort": 80, + "hostPort": 80 + } + ], + "taskArn": "arn:aws:ecs:us-east-1:012345678910:task/a126249b-b7e4-4b06-9d8f-1b56e75a99b5" + } + ], + "desiredStatus": "STOPPED", + "lastStatus": "RUNNING", + "overrides": { + "containerOverrides": [ + { + "name": "mysql" + }, + { + "name": "wordpress" + } + ] + }, + "taskArn": "arn:aws:ecs:us-east-1:012345678910:task/a126249b-b7e4-4b06-9d8f-1b56e75a99b5", + "taskDefinitionArn": "arn:aws:ecs:us-east-1:012345678910:task-definition/hello_world:11" + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/libcloud/blob/a1adaae2/libcloud/test/container/test_ecs.py ---------------------------------------------------------------------- diff --git a/libcloud/test/container/test_ecs.py b/libcloud/test/container/test_ecs.py index 81bdf09..1a4af3d 100644 --- a/libcloud/test/container/test_ecs.py +++ b/libcloud/test/container/test_ecs.py @@ -17,7 +17,7 @@ import sys from libcloud.test import unittest -from libcloud.container.base import ContainerCluster +from libcloud.container.base import ContainerCluster, ContainerImage, Container from libcloud.container.drivers.ecs import ElasticContainerDriver from libcloud.utils.py3 import httplib @@ -62,6 +62,77 @@ class ElasticContainerDriverTestCase(unittest.TestCase): containers = self.driver.list_containers(cluster=cluster) self.assertEqual(len(containers), 2) + def test_deploy_container(self): + container = self.driver.deploy_container( + name='jim', + image=ContainerImage( + id=None, + name='mysql', + path='mysql', + version=None, + driver=self.driver + ) + ) + self.assertEqual(container.id, 'arn:aws:ecs:us-east-1:012345678910:container/e1ed7aac-d9b2-4315-8726-d2432bf11868') + + def test_get_container(self): + container = self.driver.get_container( + 'arn:aws:ecs:us-east-1:012345678910:container/76c980a8-2454-4a9c-acc4-9eb103117273' + ) + self.assertEqual(container.id, 'arn:aws:ecs:us-east-1:012345678910:container/76c980a8-2454-4a9c-acc4-9eb103117273') + self.assertEqual(container.name, 'mysql') + self.assertEqual(container.image.name, 'mysql') + + def test_start_container(self): + container = self.driver.start_container( + Container( + id=None, + name=None, + image=None, + state=None, + ip_addresses=None, + driver=self.driver, + extra={ + 'taskDefinitionArn': '' + } + ) + ) + self.assertFalse(container is None) + + def test_stop_container(self): + container = self.driver.stop_container( + Container( + id=None, + name=None, + image=None, + state=None, + ip_addresses=None, + driver=self.driver, + extra={ + 'taskArn': '12345', + 'taskDefinitionArn': '123556' + } + ) + ) + self.assertFalse(container is None) + + def test_restart_container(self): + container = self.driver.restart_container( + Container( + id=None, + name=None, + image=None, + state=None, + ip_addresses=None, + driver=self.driver, + extra={ + 'taskArn': '12345', + 'taskDefinitionArn': '123556' + } + ) + ) + self.assertFalse(container is None) + class ECSMockHttp(MockHttp): fixtures = ContainerFileFixtures('ecs') @@ -69,7 +140,11 @@ class ECSMockHttp(MockHttp): 'DescribeClusters': 'describeclusters.json', 'CreateCluster': 'createcluster.json', 'DeleteCluster': 'deletecluster.json', - 'DescribeTasks': 'describetasks.json' + 'DescribeTasks': 'describetasks.json', + 'ListTasks': 'listtasks.json', + 'RegisterTaskDefinition': 'registertaskdefinition.json', + 'RunTask': 'runtask.json', + 'StopTask': 'stoptask.json' } def _2014_11_13(
