| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| try: |
| import simplejson as json |
| except ImportError: |
| import json |
| |
| 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__ = [ |
| 'ElasticContainerDriver' |
| ] |
| |
| |
| ECS_VERSION = '2014-11-13' |
| ECR_VERSION = '2015-09-21' |
| ECS_HOST = 'ecs.%s.amazonaws.com' |
| ECR_HOST = 'ecr.%s.amazonaws.com' |
| ROOT = '/' |
| ECS_TARGET_BASE = 'AmazonEC2ContainerServiceV%s' % \ |
| (ECS_VERSION.replace('-', '')) |
| ECR_TARGET_BASE = 'AmazonEC2ContainerRegistry_V%s' % \ |
| (ECR_VERSION.replace('-', '')) |
| |
| |
| class ECSJsonConnection(SignedAWSConnection): |
| version = ECS_VERSION |
| host = ECS_HOST |
| responseCls = AWSJsonResponse |
| service_name = 'ecs' |
| |
| |
| class ECRJsonConnection(SignedAWSConnection): |
| version = ECR_VERSION |
| host = ECR_HOST |
| responseCls = AWSJsonResponse |
| service_name = 'ecr' |
| |
| |
| 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 |
| status_map = { |
| 'RUNNING': ContainerState.RUNNING |
| } |
| |
| def __init__(self, access_id, secret, region): |
| super(ElasticContainerDriver, self).__init__(access_id, secret) |
| self.region = region |
| self.region_name = region |
| self.connection.host = ECS_HOST % (region) |
| |
| # Setup another connection class for ECR |
| conn_kwargs = self._ex_connection_class_kwargs() |
| self.ecr_connection = self.ecrConnectionClass( |
| access_id, secret, **conn_kwargs) |
| self.ecr_connection.host = ECR_HOST % (region) |
| self.ecr_connection.driver = self |
| self.ecr_connection.connect() |
| |
| def _ex_connection_class_kwargs(self): |
| return {'signature_version': '4'} |
| |
| def list_images(self, ex_repository_name): |
| """ |
| List the images in an ECR repository |
| |
| :param ex_repository_name: The name of the repository to check |
| defaults to the default repository. |
| :type ex_repository_name: ``str`` |
| |
| :return: a list of images |
| :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage` |
| """ |
| 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 |
| 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): |
| """ |
| Get a list of potential locations to deploy clusters into |
| |
| :param location: The location to search in |
| :type location: :class:`libcloud.container.base.ClusterLocation` |
| |
| :rtype: ``list`` of :class:`libcloud.container.base.ContainerCluster` |
| """ |
| listdata = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps({}), |
| headers=self._get_headers('ListClusters') |
| ).object |
| request = {'clusters': listdata['clusterArns']} |
| data = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('DescribeClusters') |
| ).object |
| return self._to_clusters(data) |
| |
| def create_cluster(self, name, location=None): |
| """ |
| Create a container cluster |
| |
| :param name: The name of the cluster |
| :type name: ``str`` |
| |
| :param location: The location to create the cluster in |
| :type location: :class:`libcloud.container.base.ClusterLocation` |
| |
| :rtype: :class:`libcloud.container.base.ContainerCluster` |
| """ |
| request = {'clusterName': name} |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('CreateCluster') |
| ).object |
| return self._to_cluster(response['cluster']) |
| |
| def destroy_cluster(self, cluster): |
| """ |
| Delete a cluster |
| |
| :return: ``True`` if the destroy was successful, otherwise ``False``. |
| :rtype: ``bool`` |
| """ |
| request = {'cluster': cluster.id} |
| data = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('DeleteCluster') |
| ).object |
| return data['cluster']['status'] == 'INACTIVE' |
| |
| def list_containers(self, image=None, cluster=None): |
| """ |
| List the deployed container images |
| |
| :param image: Filter to containers with a certain image |
| :type image: :class:`libcloud.container.base.ContainerImage` |
| |
| :param cluster: Filter to containers in a cluster |
| :type cluster: :class:`libcloud.container.base.ContainerCluster` |
| |
| :rtype: ``list`` of :class:`libcloud.container.base.Container` |
| """ |
| request = {'cluster': 'default'} |
| if cluster is not None: |
| request['cluster'] = cluster.id |
| if image is not None: |
| request['family'] = image.name |
| list_response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('ListTasks') |
| ).object |
| if len(list_response['taskArns']) == 0: |
| return [] |
| containers = self.ex_list_containers_for_task( |
| list_response['taskArns']) |
| return containers |
| |
| def deploy_container(self, name, image, cluster=None, |
| parameters=None, start=True, ex_cpu=10, ex_memory=500, |
| ex_container_port=None, ex_host_port=None): |
| """ |
| 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`` |
| |
| :param image: The container image to deploy |
| :type image: :class:`libcloud.container.base.ContainerImage` |
| |
| :param cluster: The cluster to deploy to, None is default |
| :type cluster: :class:`libcloud.container.base.ContainerCluster` |
| |
| :param parameters: Container Image parameters |
| :type parameters: ``str`` |
| |
| :param start: Start the container on deployment |
| :type start: ``bool`` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| data = {} |
| if ex_container_port is None and ex_host_port is None: |
| port_maps = [] |
| else: |
| port_maps = [ |
| { |
| "containerPort": ex_container_port, |
| "hostPort": ex_host_port |
| } |
| ] |
| data['containerDefinitions'] = [ |
| { |
| "mountPoints": [], |
| "name": name, |
| "image": image.name, |
| "cpu": ex_cpu, |
| "environment": [], |
| "memory": ex_memory, |
| "portMappings": port_maps, |
| "essential": True, |
| "volumesFrom": [] |
| } |
| ] |
| data['family'] = name |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(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): |
| """ |
| Get a container by ID |
| |
| :param id: The ID of the container to get |
| :type id: ``str`` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| containers = self.ex_list_containers_for_task([id]) |
| return containers[0] |
| |
| def start_container(self, container, count=1): |
| """ |
| Start a deployed task |
| |
| :param container: The container to start |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :param count: Number of containers to start |
| :type count: ``int`` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| return self.ex_start_task(container.extra['taskDefinitionArn'], count) |
| |
| def stop_container(self, container): |
| """ |
| Stop a deployed container |
| |
| :param container: The container to stop |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| request = {'task': container.extra['taskArn']} |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(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): |
| """ |
| Restart a deployed container |
| |
| :param container: The container to restart |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| self.stop_container(container) |
| return self.start_container(container) |
| |
| def destroy_container(self, container): |
| """ |
| Destroy a deployed container |
| |
| :param container: The container to destroy |
| :type container: :class:`libcloud.container.base.Container` |
| |
| :rtype: :class:`libcloud.container.base.Container` |
| """ |
| 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:`libcloud.container.base.Container` |
| """ |
| request = None |
| request = {'count': count, |
| 'taskDefinition': task_arn} |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(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:`libcloud.container.base.Container` |
| """ |
| describe_request = {'tasks': task_arns} |
| descripe_response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(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 ex_create_service(self, name, cluster, |
| task_definition, desired_count=1): |
| """ |
| Runs and maintains a desired number of tasks from a specified |
| task definition. If the number of tasks running in a service |
| drops below desired_count, Amazon ECS spawns another |
| instantiation of the task in the specified cluster. |
| |
| :param name: the name of the service |
| :type name: ``str`` |
| |
| :param cluster: The cluster to run the service on |
| :type cluster: :class:`libcloud.container.base.ContainerCluster` |
| |
| :param task_definition: The task definition name or ARN for the |
| service |
| :type task_definition: ``str`` |
| |
| :param desired_count: The desired number of tasks to be running |
| at any one time |
| :type desired_count: ``int`` |
| |
| :rtype: ``object`` The service object |
| """ |
| request = { |
| 'serviceName': name, |
| 'taskDefinition': task_definition, |
| 'desiredCount': desired_count, |
| 'cluster': cluster.id} |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('CreateService') |
| ).object |
| return response['service'] |
| |
| def ex_list_service_arns(self, cluster=None): |
| """ |
| List the services |
| |
| :param cluster: The cluster hosting the services |
| :type cluster: :class:`libcloud.container.base.ContainerCluster` |
| |
| :rtype: ``list`` of ``str`` |
| """ |
| request = {} |
| if cluster is not None: |
| request['cluster'] = cluster.id |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('ListServices') |
| ).object |
| return response['serviceArns'] |
| |
| def ex_describe_service(self, service_arn): |
| """ |
| Get the details of a service |
| |
| :param cluster: The hosting cluster |
| :type cluster: :class:`libcloud.container.base.ContainerCluster` |
| |
| :param service_arn: The service ARN to describe |
| :type service_arn: ``str`` |
| |
| :return: The service object |
| :rtype: ``object`` |
| """ |
| request = {'services': [service_arn]} |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('DescribeServices') |
| ).object |
| return response['services'][0] |
| |
| def ex_destroy_service(self, service_arn): |
| """ |
| Deletes a service |
| |
| :param cluster: The target cluster |
| :type cluster: :class:`libcloud.container.base.ContainerCluster` |
| |
| :param service_arn: The service ARN to destroy |
| :type service_arn: ``str`` |
| """ |
| request = { |
| 'service': service_arn} |
| response = self.connection.request( |
| ROOT, |
| method='POST', |
| data=json.dumps(request), |
| headers=self._get_headers('DeleteService') |
| ).object |
| 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 _to_clusters(self, data): |
| clusters = [] |
| for cluster in data['clusters']: |
| clusters.append(self._to_cluster(cluster)) |
| return clusters |
| |
| def _to_cluster(self, data): |
| return ContainerCluster( |
| id=data['clusterArn'], |
| name=data['clusterName'], |
| driver=self.connection.driver |
| ) |
| |
| def _to_containers(self, data, task_definition_arn): |
| clusters = [] |
| for cluster in data['containers']: |
| clusters.append(self._to_container(cluster, task_definition_arn)) |
| return clusters |
| |
| def _to_container(self, data, task_definition_arn): |
| return Container( |
| id=data['containerArn'], |
| name=data['name'], |
| image=ContainerImage( |
| id=None, |
| name=data['name'], |
| path=None, |
| version=None, |
| driver=self.connection.driver |
| ), |
| ip_addresses=None, |
| state=self.status_map.get(data['lastStatus'], None), |
| extra={ |
| 'taskArn': data['taskArn'], |
| 'taskDefinitionArn': task_definition_arn |
| }, |
| driver=self.connection.driver |
| ) |
| |
| def _to_images(self, data, host, repository_name): |
| images = [] |
| for image in data: |
| images.append(self._to_image(image, host, repository_name)) |
| return images |
| |
| def _to_image(self, data, host, repository_name): |
| path = '%s/%s:%s' % ( |
| host, |
| repository_name, |
| data['imageTag'] |
| ) |
| return ContainerImage( |
| id=None, |
| name=path, |
| path=path, |
| version=data['imageTag'], |
| driver=self.connection.driver |
| ) |