| # 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, |
| ) |