Extended the ECS driver to support the new container registry API, with tests 
and doc examples


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/f00a9b75
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/f00a9b75
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/f00a9b75

Branch: refs/heads/trunk
Commit: f00a9b7599cb01e5885f160f2e7e31e24c0598be
Parents: d29629d
Author: anthony-shaw <anthony.p.s...@gmail.com>
Authored: Mon Jan 11 15:19:24 2016 +1100
Committer: anthony-shaw <anthony.p.s...@gmail.com>
Committed: Mon Jan 11 15:19:24 2016 +1100

----------------------------------------------------------------------
 docs/container/drivers/ecs.rst                  |  13 ++
 .../container/ecs/container_registry.py         |  30 +++++
 libcloud/common/aws.py                          |   3 +-
 libcloud/container/drivers/ecs.py               | 123 +++++++++++++++----
 libcloud/container/utils/docker.py              |   8 +-
 .../container/fixtures/ecs/createservice.json   |  30 +++++
 .../container/fixtures/ecs/deleteservice.json   |  30 +++++
 .../fixtures/ecs/describerepositories.json      |   8 ++
 .../fixtures/ecs/describeservices.json          |  33 +++++
 .../fixtures/ecs/getauthorizationtoken.json     |   9 ++
 .../test/container/fixtures/ecs/listimages.json |   7 ++
 .../container/fixtures/ecs/listservices.json    |   6 +
 libcloud/test/container/test_ecs.py             |  41 ++++++-
 13 files changed, 309 insertions(+), 32 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/docs/container/drivers/ecs.rst
----------------------------------------------------------------------
diff --git a/docs/container/drivers/ecs.rst b/docs/container/drivers/ecs.rst
index fbde93f..4dc3912 100644
--- a/docs/container/drivers/ecs.rst
+++ b/docs/container/drivers/ecs.rst
@@ -34,6 +34,19 @@ You can use this class for fetching images to deploy to 
services like ECS
 .. literalinclude:: /examples/container/docker_hub.py
    :language: python
 
+Deploying a container from Amazon Elastic Container Registry (ECR)
+------------------------------------------------------------------
+
+Amazon ECR is a combination of the Docker Registry V2 API and a proprietary 
API. The ECS driver includes methods for talking to both APIs.
+
+Docker Registry API Client 
:class:`~libcloud.container.utils.docker.RegistryClient` is a shared utility 
class for interfacing to the public Docker Hub Service.
+
+You can use a factory method to generate an instance of RegsitryClient from 
the ECS driver. This will request a 12 hour token from the Amazon API and 
instantiate a :class:`~libcloud.container.utils.docker.RegistryClient`
+object with those credentials.
+
+.. literalinclude:: /examples/container/container_registry.py
+   :language: python
+
 API Docs
 --------
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/docs/examples/container/ecs/container_registry.py
----------------------------------------------------------------------
diff --git a/docs/examples/container/ecs/container_registry.py 
b/docs/examples/container/ecs/container_registry.py
new file mode 100644
index 0000000..2003d5b
--- /dev/null
+++ b/docs/examples/container/ecs/container_registry.py
@@ -0,0 +1,30 @@
+from libcloud.container.types import Provider
+from libcloud.container.providers import get_driver
+
+cls = get_driver(Provider.ECS)
+
+# Connect to AWS
+conn = cls(access_id='SDHFISJDIFJSIDFJ',
+           secret='THIS_IS)+_MY_SECRET_KEY+I6TVkv68o4H',
+           region='ap-southeast-2')
+
+# Get a Registry API client for an existing repository
+client = conn.ex_get_registry_client('my-image')
+
+# List all the images
+for image in client.list_images('my-image'):
+    print(image.name)
+
+# Get a specific image
+image = client.get_image('my-image', '14.04')
+
+print(image.path)
+# >> 647433528374.dkr.ecr.region.amazonaws.com/my-images:14.04
+
+# Deploy that image
+cluster = conn.list_clusters()[0]
+container = conn.deploy_container(
+    cluster=cluster,
+    name='my-simple-app',
+    image=image
+)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/common/aws.py
----------------------------------------------------------------------
diff --git a/libcloud/common/aws.py b/libcloud/common/aws.py
index b7153e3..2cc1d4b 100644
--- a/libcloud/common/aws.py
+++ b/libcloud/common/aws.py
@@ -394,8 +394,7 @@ class AWSJsonResponse(JsonResponse):
     def parse_error(self):
         response = json.loads(self.body)
         code = response['__type']
-        message = response['Message']
-
+        message = response.get('Message', response['message'])
         return ('%s: %s' % (code, message))
 
 

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/container/drivers/ecs.py
----------------------------------------------------------------------
diff --git a/libcloud/container/drivers/ecs.py 
b/libcloud/container/drivers/ecs.py
index 7f6faf3..36c5be3 100644
--- a/libcloud/container/drivers/ecs.py
+++ b/libcloud/container/drivers/ecs.py
@@ -21,6 +21,7 @@ except ImportError:
 from libcloud.container.base import (ContainerDriver, Container,
                                      ContainerCluster, ContainerImage)
 from libcloud.container.types import ContainerState
+from libcloud.container.utils.docker import RegistryClient
 from libcloud.common.aws import SignedAWSConnection, AWSJsonResponse
 
 __all__ = [
@@ -56,6 +57,7 @@ class ECRJsonConnection(SignedAWSConnection):
 class ElasticContainerDriver(ContainerDriver):
     name = 'Amazon Elastic Container Service'
     website = 'https://aws.amazon.com/ecs/details/'
+    ecr_repository_host = '%s.dkr.ecr.%s.amazonaws.com'
     connectionCls = ECSJsonConnection
     ecrConnectionClass = ECRJsonConnection
     supports_clusters = False
@@ -80,7 +82,7 @@ class ElasticContainerDriver(ContainerDriver):
     def _ex_connection_class_kwargs(self):
         return {'signature_version': '4'}
 
-    def list_images(self, ex_repository_name=None):
+    def list_images(self, ex_repository_name):
         """
         List the images in an ECR repository
 
@@ -91,14 +93,19 @@ class ElasticContainerDriver(ContainerDriver):
         :return: a list of images
         :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage`
         """
-        request = {'repositoryName': 'default'}
+        request = {}
+        request['repositoryName'] = ex_repository_name
         list_response = self.ecr_connection.request(
             ROOT,
             method='POST',
             data=json.dumps(request),
             headers=self._get_ecr_headers('ListImages')
         ).object
-        return self._to_images(list_response)
+        repository_id = self.ex_get_repository_id(ex_repository_name)
+        host = self._get_ecr_host(repository_id)
+        return self._to_images(list_response['imageIds'],
+                               host,
+                               ex_repository_name)
 
     def list_clusters(self):
         """
@@ -415,7 +422,7 @@ class ElasticContainerDriver(ContainerDriver):
             data=json.dumps(request),
             headers=self._get_headers('CreateService')
         ).object
-        return response
+        return response['service']
 
     def ex_list_service_arns(self, cluster=None):
         """
@@ -437,7 +444,7 @@ class ElasticContainerDriver(ContainerDriver):
         ).object
         return response['serviceArns']
 
-    def ex_describe_service(self, cluster, service_arn):
+    def ex_describe_service(self, service_arn):
         """
         Get the details of a service
 
@@ -451,8 +458,6 @@ class ElasticContainerDriver(ContainerDriver):
         :rtype: ``object``
         """
         request = {'services': [service_arn]}
-        if cluster is not None:
-            request['cluster'] = cluster.id
         response = self.connection.request(
             ROOT,
             method='POST',
@@ -461,7 +466,7 @@ class ElasticContainerDriver(ContainerDriver):
         ).object
         return response['services'][0]
 
-    def ex_destroy_service(self, cluster, service_arn):
+    def ex_destroy_service(self, service_arn):
         """
         Deletes a service
 
@@ -472,37 +477,96 @@ class ElasticContainerDriver(ContainerDriver):
         :type   service_arn: ``str``
         """
         request = {
-            'service': service_arn,
-            'cluster': cluster.id}
+            'service': service_arn}
         response = self.connection.request(
             ROOT,
             method='POST',
             data=json.dumps(request),
             headers=self._get_headers('DeleteService')
         ).object
-        return response
+        return response['service']
+
+    def ex_get_registry_client(self, repository_name):
+        """
+        Get a client for an ECR repository
+
+        :param  repository_name: The unique name of the repository
+        :type   repository_name: ``str``
+
+        :return: a docker registry API client
+        :rtype: :class:`libcloud.container.utils.docker.RegistryClient`
+        """
+        repository_id = self.ex_get_repository_id(repository_name)
+        token = self.ex_get_repository_token(repository_id)
+        host = self._get_ecr_host(repository_id)
+        return RegistryClient(
+            host=host,
+            username='AWS',
+            password=token
+        )
+
+    def ex_get_repository_token(self, repository_id):
+        """
+        Get the authorization token (12 hour expiry) for a repository
+
+        :param  repository_id: The ID of the repository
+        :type   repository_id: ``str``
+
+        :return: A token for login
+        :rtype: ``str``
+        """
+        request = {'RegistryIds': [repository_id]}
+        response = self.ecr_connection.request(
+            ROOT,
+            method='POST',
+            data=json.dumps(request),
+            headers=self._get_ecr_headers('GetAuthorizationToken')
+        ).object
+        return response['authorizationData'][0]['authorizationToken']
+
+    def ex_get_repository_id(self, repository_name):
+        """
+        Get the ID of a repository
+
+        :param  repository_name: The unique name of the repository
+        :type   repository_name: ``str``
+
+        :return: The repository ID
+        :rtype: ``str``
+        """
+        request = {'repositoryNames': [repository_name]}
+        list_response = self.ecr_connection.request(
+            ROOT,
+            method='POST',
+            data=json.dumps(request),
+            headers=self._get_ecr_headers('DescribeRepositories')
+        ).object
+        repository_id = list_response['repositories'][0]['registryId']
+        return repository_id
+
+    def _get_ecr_host(self, repository_id):
+        return self.ecr_repository_host % (
+            repository_id,
+            self.region)
 
     def _get_headers(self, action):
+        """
+        Get the default headers for a request to the ECS API
+        """
         return {'x-amz-target': '%s.%s' %
                 (ECS_TARGET_BASE, action),
                 'Content-Type': 'application/x-amz-json-1.1'
                 }
 
     def _get_ecr_headers(self, action):
+        """
+        Get the default headers for a request to the ECR API
+        """
         return {'x-amz-target': '%s.%s' %
                 (ECR_TARGET_BASE, action),
                 'Content-Type': 'application/x-amz-json-1.1'
                 }
 
-    def ex_docker_hub_image(self, name):
-        return ContainerImage(
-            id=None,
-            name=name,
-            path=None,
-            version=None,
-            driver=self.connection.driver
-        )
-
     def _to_clusters(self, data):
         clusters = []
         for cluster in data['clusters']:
@@ -542,17 +606,22 @@ class ElasticContainerDriver(ContainerDriver):
             driver=self.connection.driver
         )
 
-    def _to_images(self, data):
+    def _to_images(self, data, host, repository_name):
         images = []
-        for image in data['images']:
-            images.append(self._to_image(image))
+        for image in data:
+            images.append(self._to_image(image, host, repository_name))
         return images
 
-    def _to_image(self, data):
+    def _to_image(self, data, host, repository_name):
+        path = '%s/%s:%s' % (
+            host,
+            repository_name,
+            data['imageTag']
+        )
         return ContainerImage(
             id=None,
-            name=data['name'],
-            path=None,
-            version=None,
+            name=path,
+            path=path,
+            version=data['imageTag'],
             driver=self.connection.driver
         )

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/container/utils/docker.py
----------------------------------------------------------------------
diff --git a/libcloud/container/utils/docker.py 
b/libcloud/container/utils/docker.py
index 9e9703c..a8a544a 100644
--- a/libcloud/container/utils/docker.py
+++ b/libcloud/container/utils/docker.py
@@ -63,7 +63,10 @@ class RegistryClient(object):
         :param password: (optional) Your hub account password
         :type  password: ``str``
         """
-        self.connection = DockerHubConnection(host, username, password, 
**kwargs)
+        self.connection = DockerHubConnection(host,
+                                              username,
+                                              password,
+                                              **kwargs)
 
     def list_images(self, repository_name, namespace='library', max_count=100):
         """
@@ -161,4 +164,5 @@ class HubClient(RegistryClient):
         :param password: (optional) Your hub account password
         :type  password: ``str``
         """
-        self.connection = DockerHubConnection(self.host, username, password, 
**kwargs)
+        self.connection = DockerHubConnection(self.host, username,
+                                              password, **kwargs)

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/fixtures/ecs/createservice.json
----------------------------------------------------------------------
diff --git a/libcloud/test/container/fixtures/ecs/createservice.json 
b/libcloud/test/container/fixtures/ecs/createservice.json
new file mode 100644
index 0000000..2f9d421
--- /dev/null
+++ b/libcloud/test/container/fixtures/ecs/createservice.json
@@ -0,0 +1,30 @@
+{
+  "service": {
+    "clusterArn": "arn:aws:ecs:us-east-1:012345678910:cluster/default",
+    "deploymentConfiguration": {
+        "maximumPercent": 200,
+        "minimumHealthyPercent": 100
+    },
+    "deployments": [
+      {
+        "createdAt": 1430326887.362,
+        "desiredCount": 10,
+        "id": "ecs-svc/9223370606527888445",
+        "pendingCount": 0,
+        "runningCount": 0,
+        "status": "PRIMARY",
+        "taskDefinition": 
"arn:aws:ecs:us-east-1:012345678910:task-definition/ecs-demo:1",
+        "updatedAt": 1430326887.362
+      }
+    ],
+    "desiredCount": 10,
+    "events": [],
+    "loadBalancers": [],
+    "pendingCount": 0,
+    "runningCount": 0,
+    "serviceArn": "arn:aws:ecs:us-east-1:012345678910:service/test",
+    "serviceName": "test",
+    "status": "ACTIVE",
+    "taskDefinition": 
"arn:aws:ecs:us-east-1:012345678910:task-definition/ecs-demo:1"
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/fixtures/ecs/deleteservice.json
----------------------------------------------------------------------
diff --git a/libcloud/test/container/fixtures/ecs/deleteservice.json 
b/libcloud/test/container/fixtures/ecs/deleteservice.json
new file mode 100644
index 0000000..d89d323
--- /dev/null
+++ b/libcloud/test/container/fixtures/ecs/deleteservice.json
@@ -0,0 +1,30 @@
+{
+  "service": {
+    "clusterArn": "arn:aws:ecs:us-east-1:012345678910:cluster/default",
+    "deploymentConfiguration": {
+        "maximumPercent": 200,
+        "minimumHealthyPercent": 100
+    },
+    "deployments": [
+      {
+        "createdAt": 1430320735.285,
+        "desiredCount": 0,
+        "id": "ecs-svc/9223370606534040511",
+        "pendingCount": 0,
+        "runningCount": 0,
+        "status": "PRIMARY",
+        "taskDefinition": 
"arn:aws:ecs:us-east-1:012345678910:task-definition/sleep360:27",
+        "updatedAt": 1430320735.285
+      }
+    ],
+    "desiredCount": 0,
+    "events": [],
+    "loadBalancers": [],
+    "pendingCount": 0,
+    "runningCount": 0,
+    "serviceArn": "arn:aws:ecs:us-east-1:012345678910:service/test",
+    "serviceName": "test",
+    "status": "DRAINING",
+    "taskDefinition": 
"arn:aws:ecs:us-east-1:012345678910:task-definition/sleep360:27"
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/fixtures/ecs/describerepositories.json
----------------------------------------------------------------------
diff --git a/libcloud/test/container/fixtures/ecs/describerepositories.json 
b/libcloud/test/container/fixtures/ecs/describerepositories.json
new file mode 100644
index 0000000..c794005
--- /dev/null
+++ b/libcloud/test/container/fixtures/ecs/describerepositories.json
@@ -0,0 +1,8 @@
+{
+    "repositories": [{
+            "registryId": "647433528374",
+            "repositoryArn": 
"arn:aws:ecr:us-east-1:647433528374:repository/my-images",
+            "repositoryName": "my-images"
+        }
+    ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/fixtures/ecs/describeservices.json
----------------------------------------------------------------------
diff --git a/libcloud/test/container/fixtures/ecs/describeservices.json 
b/libcloud/test/container/fixtures/ecs/describeservices.json
new file mode 100644
index 0000000..76acc75
--- /dev/null
+++ b/libcloud/test/container/fixtures/ecs/describeservices.json
@@ -0,0 +1,33 @@
+{
+  "failures": [],
+  "services": [
+    {
+      "clusterArn": "arn:aws:ecs:us-west-2:012345678910:cluster/telemetry",
+      "deploymentConfiguration": {
+          "maximumPercent": 200,
+          "minimumHealthyPercent": 100
+      },
+      "deployments": [
+        {
+          "createdAt": 1432829320.611,
+          "desiredCount": 4,
+          "id": "ecs-svc/9223370604025455196",
+          "pendingCount": 0,
+          "runningCount": 4,
+          "status": "PRIMARY",
+          "taskDefinition": 
"arn:aws:ecs:us-west-2:012345678910:task-definition/hpcc-t2-medium:1",
+          "updatedAt": 1432829320.611
+        }
+      ],
+      "desiredCount": 4,
+      "events": [],
+      "loadBalancers": [],
+      "pendingCount": 0,
+      "runningCount": 4,
+      "serviceArn": "arn:aws:ecs:us-west-2:012345678910:service/bunker-buster",
+      "serviceName": "test",
+      "status": "ACTIVE",
+      "taskDefinition": 
"arn:aws:ecs:us-west-2:012345678910:task-definition/hpcc-t2-medium:1"
+    }
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/fixtures/ecs/getauthorizationtoken.json
----------------------------------------------------------------------
diff --git a/libcloud/test/container/fixtures/ecs/getauthorizationtoken.json 
b/libcloud/test/container/fixtures/ecs/getauthorizationtoken.json
new file mode 100644
index 0000000..469c9ef
--- /dev/null
+++ b/libcloud/test/container/fixtures/ecs/getauthorizationtoken.json
@@ -0,0 +1,9 @@
+{
+  "authorizationData": [
+    {
+      "authorizationToken": "QVdTOkNpQzErSHF1ZXZPcUR...",
+      "expiresAt": 1448878779.809,
+      "proxyEndpoint": "https://012345678910.dkr.ecr.us-east-1.amazonaws.com";
+    }
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/fixtures/ecs/listimages.json
----------------------------------------------------------------------
diff --git a/libcloud/test/container/fixtures/ecs/listimages.json 
b/libcloud/test/container/fixtures/ecs/listimages.json
new file mode 100644
index 0000000..ae598f0
--- /dev/null
+++ b/libcloud/test/container/fixtures/ecs/listimages.json
@@ -0,0 +1,7 @@
+{
+    "imageIds": [{
+            "imageDigest": 
"sha256:9bacaf947ed397fcc9afb7359a1a8eaa1f6944ba8cd4ddca1c69bdcf4acf12a2",
+            "imageTag": "latest"
+        }
+    ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/fixtures/ecs/listservices.json
----------------------------------------------------------------------
diff --git a/libcloud/test/container/fixtures/ecs/listservices.json 
b/libcloud/test/container/fixtures/ecs/listservices.json
new file mode 100644
index 0000000..62a3cf5
--- /dev/null
+++ b/libcloud/test/container/fixtures/ecs/listservices.json
@@ -0,0 +1,6 @@
+{
+  "serviceArns": [
+    "arn:aws:ecs:us-east-1:012345678910:service/hello_world",
+    "arn:aws:ecs:us-east-1:012345678910:service/ecs-simple-service"
+  ]
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/libcloud/blob/f00a9b75/libcloud/test/container/test_ecs.py
----------------------------------------------------------------------
diff --git a/libcloud/test/container/test_ecs.py 
b/libcloud/test/container/test_ecs.py
index 518daae..73a35b3 100644
--- a/libcloud/test/container/test_ecs.py
+++ b/libcloud/test/container/test_ecs.py
@@ -19,6 +19,7 @@ from libcloud.test import unittest
 
 from libcloud.container.base import ContainerCluster, ContainerImage, Container
 from libcloud.container.drivers.ecs import ElasticContainerDriver
+from libcloud.container.utils.docker import RegistryClient
 
 from libcloud.utils.py3 import httplib
 from libcloud.test.secrets import CONTAINER_PARAMS_ECS
@@ -136,6 +137,37 @@ class ElasticContainerDriverTestCase(unittest.TestCase):
         )
         self.assertFalse(container is None)
 
+    def test_list_images(self):
+        images = self.driver.list_images('my-images')
+        self.assertEqual(len(images), 1)
+        self.assertEqual(images[0].name, 
'647433528374.dkr.ecr.region.amazonaws.com/my-images:latest')
+
+    def test_ex_create_service(self):
+        cluster = self.driver.list_clusters()[0]
+        task_definition = 
self.driver.list_containers()[0].extra['taskDefinitionArn']
+        service = self.driver.ex_create_service(cluster=cluster,
+                                                name='jim',
+                                                
task_definition=task_definition)
+        self.assertEqual(service['serviceName'], 'test')
+
+    def test_ex_list_service_arns(self):
+        arns = self.driver.ex_list_service_arns()
+        self.assertEqual(len(arns), 2)
+
+    def test_ex_describe_service(self):
+        arn = self.driver.ex_list_service_arns()[0]
+        service = self.driver.ex_describe_service(arn)
+        self.assertEqual(service['serviceName'], 'test')
+
+    def test_ex_destroy_service(self):
+        arn = self.driver.ex_list_service_arns()[0]
+        service = self.driver.ex_destroy_service(arn)
+        self.assertEqual(service['status'], 'DRAINING')
+
+    def test_ex_get_registry_client(self):
+        client = self.driver.ex_get_registry_client('my-images')
+        self.assertIsInstance(client, RegistryClient)
+
 
 class ECSMockHttp(MockHttp):
     fixtures = ContainerFileFixtures('ecs')
@@ -148,7 +180,14 @@ class ECSMockHttp(MockHttp):
         'ListClusters': 'listclusters.json',
         'RegisterTaskDefinition': 'registertaskdefinition.json',
         'RunTask': 'runtask.json',
-        'StopTask': 'stoptask.json'
+        'StopTask': 'stoptask.json',
+        'ListImages': 'listimages.json',
+        'DescribeRepositories': 'describerepositories.json',
+        'CreateService': 'createservice.json',
+        'ListServices': 'listservices.json',
+        'DescribeServices': 'describeservices.json',
+        'DeleteService': 'deleteservice.json',
+        'GetAuthorizationToken': 'getauthorizationtoken.json'
     }
 
     def root(

Reply via email to