diff --git a/.ratignore b/.ratignore index ba54ecd9cd..9668cf036d 100644 --- a/.ratignore +++ b/.ratignore @@ -10,12 +10,14 @@ test/storage/fixtures/ test/compute/fixtures/ test/loadbalancer/fixtures/ test/dns/fixtures/ +test/container/fixtures/ coverage_html_report/ .coverage .coveragerc libcloud/data/pricing.json libcloud/common/__init__.py libcloud/compute/__init__.py +libcloud/container/__init__.py libcloud/storage/__init__.py libcloud/loadbalancer/__init__.py libcloud/dns/__init__.py diff --git a/.travis.yml b/.travis.yml index a48465f5e4..5a635be651 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ python: - 3.4 - 3.5 - "pypy" -sudo: false os: - linux # Note: OS X has been broken on Travis for a long time @@ -17,15 +16,18 @@ env: matrix: fast_finish: true - allow_failures: - - os: osx include: - - python: 2.7 + - sudo: required + python: 2.7 env: ENV=lint before_script: TOX_ENV=lint - - python: 2.7 + - sudo: required + python: 2.7 env: ENV=docs before_script: TOX_ENV=docs-travis + before_install: + - sudo apt-get update -qq + - sudo apt-get install -y graphviz # Trigger ReadTheDocs build after_success: ./contrib/trigger_rtd_build.py 8284 diff --git a/CHANGES.rst b/CHANGES.rst index a6fffb311d..4cbf09fe1e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,11 @@ Changes with current version of Apache Libcloud General ~~~~~~~ +- Introduction of container based drivers for Docker, Rkt and Container-as-a-service + providers + (LIBCLOUD-781, GITHUB-666) + [Anthony Shaw] + - Introduce a new ``libcloud.backup`` API for Backup as a Service projects and products. (GITHUB-621) @@ -63,7 +68,7 @@ DNS [Wido den Hollander] Changes with Apache Libcloud 0.20.0 ------------------------------------ +------------------------------------------- General ~~~~~~~ diff --git a/README.rst b/README.rst index dbdd704fcf..0d000d6f58 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,8 @@ Resources you can manage with Libcloud are divided into the following categories CloudFiles (``libcloud.storage.*``) * **Load Balancers** - Load Balancers as a Service, LBaaS (``libcloud.loadbalancer.*``) * **DNS** - DNS as a Service, DNSaaS (``libcloud.dns.*``) +* **Container** - Container virtualization services (``libcloud.container.*``) + Apache Libcloud is an Apache project, see for more information. diff --git a/contrib/generate_provider_feature_matrix_table.py b/contrib/generate_provider_feature_matrix_table.py index b806434898..5bdcf3299f 100755 --- a/contrib/generate_provider_feature_matrix_table.py +++ b/contrib/generate_provider_feature_matrix_table.py @@ -45,6 +45,11 @@ from libcloud.dns.providers import DRIVERS as DNS_DRIVERS from libcloud.dns.types import Provider as DNSProvider +from libcloud.container.base import ContainerDriver +from libcloud.container.providers import get_driver as get_container_driver +from libcloud.container.providers import DRIVERS as CONTAINER_DRIVERS +from libcloud.container.types import Provider as ContainerProvider + from libcloud.backup.base import BackupDriver from libcloud.backup.providers import get_driver as get_backup_driver from libcloud.backup.providers import DRIVERS as BACKUP_DRIVERS @@ -94,6 +99,11 @@ 'dns': ['list_zones', 'list_records', 'iterate_zones', 'iterate_records', 'create_zone', 'update_zone', 'create_record', 'update_record', 'delete_zone', 'delete_record'], + 'container': ['install_image', 'list_images', 'deploy_container', + 'get_container', 'start_container', 'stop_container', + 'restart_container', 'destroy_container', 'list_containers', + 'list_locations', 'create_cluster', 'destroy_cluster', + 'list_clusters'], 'backup': ['get_supported_target_types', 'list_targets', 'create_target', 'create_target_from_node', 'create_target_from_storage_container', 'update_target', 'delete_target', 'list_recovery_points', 'recover_target', 'recover_target_out_of_place', 'list_target_jobs', 'create_target_job', @@ -169,6 +179,21 @@ 'delete_zone': 'delete zone', 'delete_record': 'delete record' }, + 'container': { + 'install_image': 'install image', + 'list_images': 'list images', + 'deploy_container': 'deploy container', + 'get_container': 'get container', + 'list_containers': 'list containers', + 'start_container': 'start container', + 'stop_container': 'stop container', + 'restart_container': 'restart container', + 'destroy_container': 'destroy container', + 'list_locations': 'list locations', + 'create_cluster': 'create cluster', + 'destroy_cluster': 'destroy cluster', + 'list_clusters': 'list clusters' + }, 'backup': { 'get_supported_target_types': 'get supported target types', 'list_targets': 'list targets', @@ -185,7 +210,7 @@ 'resume_target_job': 'resume target job', 'suspend_target_job': 'suspend target job', 'cancel_target_job': 'cancel target job' - }, + } } IGNORED_PROVIDERS = [ @@ -229,6 +254,11 @@ def generate_providers_table(api): drivers = DNS_DRIVERS provider = DNSProvider get_driver_method = get_dns_driver + elif api == 'container': + driver = ContainerDriver + drivers = CONTAINER_DRIVERS + provider = ContainerProvider + get_driver_method = get_container_driver elif api == 'backup': driver = BackupDriver drivers = BACKUP_DRIVERS diff --git a/docs/_static/images/provider_logos/docker.png b/docs/_static/images/provider_logos/docker.png new file mode 100644 index 0000000000..8ff27ae939 Binary files /dev/null and b/docs/_static/images/provider_logos/docker.png differ diff --git a/docs/_static/images/provider_logos/kubernetes.png b/docs/_static/images/provider_logos/kubernetes.png new file mode 100644 index 0000000000..08da3fd146 Binary files /dev/null and b/docs/_static/images/provider_logos/kubernetes.png differ diff --git a/docs/_static/images/provider_logos/triton.png b/docs/_static/images/provider_logos/triton.png new file mode 100644 index 0000000000..6a20129d3a Binary files /dev/null and b/docs/_static/images/provider_logos/triton.png differ diff --git a/docs/conf.py b/docs/conf.py index c79010a034..0d9acf0d59 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode'] + 'sphinx.ext.viewcode', 'sphinx.ext.graphviz'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/container/_supported_methods.rst b/docs/container/_supported_methods.rst new file mode 100644 index 0000000000..4361d25bf9 --- /dev/null +++ b/docs/container/_supported_methods.rst @@ -0,0 +1,15 @@ +.. NOTE: This file has been generated automatically using generate_provider_feature_matrix_table.py script, don't manually edit it + +=================================== ============= =========== ================ ============= =============== ============== ================= ================= =============== ============== ============== =============== ============= +Provider install image list images deploy container get container start container stop container restart container destroy container list containers list locations create cluster destroy cluster list clusters +=================================== ============= =========== ================ ============= =============== ============== ================= ================= =============== ============== ============== =============== ============= +`Docker`_ yes yes yes yes yes yes yes yes yes no no no no +`Amazon Elastic Container Service`_ no yes yes yes yes yes yes yes yes no yes yes yes +`Joyent Triton`_ yes yes yes yes yes yes yes yes yes no no no no +`Kubernetes`_ no no yes yes no no no yes yes no yes yes yes +=================================== ============= =========== ================ ============= =============== ============== ================= ================= =============== ============== ============== =============== ============= + +.. _`Docker`: http://docker.io +.. _`Amazon Elastic Container Service`: https://aws.amazon.com/ecs/details/ +.. _`Joyent Triton`: http://joyent.com +.. _`Kubernetes`: http://kubernetes.io diff --git a/docs/container/_supported_providers.rst b/docs/container/_supported_providers.rst new file mode 100644 index 0000000000..3c400c6a25 --- /dev/null +++ b/docs/container/_supported_providers.rst @@ -0,0 +1,15 @@ +.. NOTE: This file has been generated automatically using generate_provider_feature_matrix_table.py script, don't manually edit it + +=================================== ============================================ ================= ============================================ ================================== +Provider Documentation Provider constant Module Class Name +=================================== ============================================ ================= ============================================ ================================== +`Docker`_ :doc:`Click ` DOCKER :mod:`libcloud.container.drivers.docker` :class:`DockerContainerDriver` +`Amazon Elastic Container Service`_ :doc:`Click ` ECS :mod:`libcloud.container.drivers.ecs` :class:`ElasticContainerDriver` +`Joyent Triton`_ :doc:`Click ` JOYENT :mod:`libcloud.container.drivers.joyent` :class:`JoyentContainerDriver` +`Kubernetes`_ :doc:`Click ` KUBERNETES :mod:`libcloud.container.drivers.kubernetes` :class:`KubernetesContainerDriver` +=================================== ============================================ ================= ============================================ ================================== + +.. _`Docker`: http://docker.io +.. _`Amazon Elastic Container Service`: https://aws.amazon.com/ecs/details/ +.. _`Joyent Triton`: http://joyent.com +.. _`Kubernetes`: http://kubernetes.io diff --git a/docs/container/api.rst b/docs/container/api.rst new file mode 100644 index 0000000000..257b1a7fd0 --- /dev/null +++ b/docs/container/api.rst @@ -0,0 +1,19 @@ +:orphan: + +Container Base API +================== + +.. autoclass:: libcloud.container.base.ContainerDriver + :members: + +.. autoclass:: libcloud.container.base.Container + :members: + +.. autoclass:: libcloud.container.base.ContainerImage + :members: + +.. autoclass:: libcloud.container.base.ContainerCluster + :members: + +.. autoclass:: libcloud.container.base.ClusterLocation + :members: diff --git a/docs/container/drivers/docker.rst b/docs/container/drivers/docker.rst new file mode 100644 index 0000000000..32cbd2c209 --- /dev/null +++ b/docs/container/drivers/docker.rst @@ -0,0 +1,26 @@ +Docker Container Driver Documentation +===================================== + +`Docker`_ containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: +code, runtime, system tools, system libraries – anything you can install on a server. This guarantees that it will always run the same, +regardless of the environment it is running in. + +.. figure:: /_static/images/provider_logos/docker.png + :align: center + :width: 300 + :target: http://docker.io/ + +Instantiating the driver +------------------------ + +.. literalinclude:: /examples/container/docker/instantiate_driver.py + :language: python + +API Docs +-------- + +.. autoclass:: libcloud.container.drivers.docker.DockerContainerDriver + :members: + :inherited-members: + +.. _`Docker`: https://docker.io/ \ No newline at end of file diff --git a/docs/container/drivers/ecs.rst b/docs/container/drivers/ecs.rst new file mode 100644 index 0000000000..40274d1f80 --- /dev/null +++ b/docs/container/drivers/ecs.rst @@ -0,0 +1,58 @@ +Amazon Elastic Container Service Documentation +============================================== + +Elastic Container Service is a container-as-a-service feature of `AWS`_. + +.. figure:: /_static/images/provider_logos/aws.png + :align: center + :width: 300 + :target: http://aws.amazon.com/ + +To provide API key access, you should apply one of the roles: +* AmazonEC2ContainerServiceFullAccess +* AmazonEC2ContainerServiceReadOnlyAccess + +Instantiating the driver +------------------------ + +.. literalinclude:: /examples/container/ecs/instantiate_driver.py + :language: python + +Deploying a container +--------------------- + +.. literalinclude:: /examples/container/ecs/deploy_container.py + :language: python + +Deploying a container from Docker Hub +------------------------------------- + +Docker Hub Client :class:`~libcloud.container.utils.docker.HubClient` is a shared utility class for interfacing to the public Docker Hub Service. + +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/ecs/container_registry.py + :language: python + +API Docs +-------- + +.. autoclass:: libcloud.container.drivers.ecs.ElasticContainerDriver + :members: + :inherited-members: + + +.. _`AWS`: https://aws.amazon.com/ \ No newline at end of file diff --git a/docs/container/drivers/index.rst b/docs/container/drivers/index.rst new file mode 100644 index 0000000000..fd1145edef --- /dev/null +++ b/docs/container/drivers/index.rst @@ -0,0 +1,12 @@ +:orphan: + +Container Drivers Documentation +=============================== + +This chapter includes links to driver (provider) specific documentation pages. + +.. toctree:: + :glob: + :maxdepth: 1 + + * \ No newline at end of file diff --git a/docs/container/drivers/joyent.rst b/docs/container/drivers/joyent.rst new file mode 100644 index 0000000000..a8b29d3e82 --- /dev/null +++ b/docs/container/drivers/joyent.rst @@ -0,0 +1,64 @@ +Joyent Triton Container Driver Documentation +============================================ + +`Joyent Triton`_ is a Docker hosting service, provided by service provider Joyent. +Docker-native tools and elastic hosts make deploying on Triton as easy as running Docker on your laptop. +There is no special software to install or configure. +Mix Docker containers with container-native Linux to extend the benefits of containerization to legacy applications and stateful services. + +.. figure:: /_static/images/provider_logos/triton.png + :align: center + :width: 300 + :target: http://joyent.com/ + +Instantiating the driver +------------------------ + +Download the script:: + + curl -O https://raw.githubusercontent.com/joyent/sdc-docker/master/tools/sdc-docker-setup.sh + +Now execute the script, substituting the correct values:: + + bash sdc-docker-setup.sh ~/.ssh/ + +This should output something similar to the following:: + + Setting up Docker client for SDC using: + CloudAPI: https://us-east-1.api.joyent.com + Account: jill + Key: /Users/localuser/.ssh/sdc-docker.id_rsa + + If you have a pass phrase on your key, the openssl command will + prompt you for your pass phrase now and again later. + + Verifying CloudAPI access. + CloudAPI access verified. + + Generating client certificate from SSH private key. + writing RSA key + Wrote certificate files to /Users/localuser/.sdc/docker/jill + + Get Docker host endpoint from cloudapi. + Docker service endpoint is: tcp://us-east-1.docker.joyent.com:2376 + + * * * + Success. Set your environment as follows: + + export DOCKER_CERT_PATH=/Users/localuser/.sdc/docker/jill + export DOCKER_HOST=tcp://us-east-1.docker.joyent.com:2376 + export DOCKER_CLIENT_TIMEOUT=300 + export DOCKER_TLS_VERIFY=1 + +.. literalinclude:: /examples/container/joyent/instantiate_driver.py + :language: python + +API Docs +-------- + +.. autoclass:: libcloud.container.drivers.joyent.JoyentContainerDriver + :members: + :inherited-members: + + +.. _`Joyent Triton`: https://www.joyent.com/blog/understanding-triton-containers diff --git a/docs/container/drivers/kubernetes.rst b/docs/container/drivers/kubernetes.rst new file mode 100644 index 0000000000..e1c77e954e --- /dev/null +++ b/docs/container/drivers/kubernetes.rst @@ -0,0 +1,47 @@ +Kubernetes Documentation +======================== + +.. note:: + + This Kubernetes driver will be subject to change from community feedback. How to map the core assets (pods, clusters) to API + entities will be subject to testing and further community feedback. + +Kubernetes is an open source orchestration system for Docker containers. It handles scheduling onto nodes in a compute cluster and actively manages workloads to ensure that their state matches the users declared intentions. Using the concepts of "labels" and "pods", +it groups the containers which make up an application into logical units for easy management and discovery. + +.. figure:: /_static/images/provider_logos/kubernetes.png + :align: center + :width: 300 + :target: http://kubernetes.io/ + + +Authentication +-------------- + +Authentication currently supported with the following methods: + +* Basic HTTP Authentication - http://kubernetes.io/v1.1/docs/admin/authentication.html +* No authentication (testing only) + +Instantiating the driver +------------------------ + +.. literalinclude:: /examples/container/kubernetes/instantiate_driver.py + :language: python + +Deploying a container from Docker Hub +------------------------------------- + +Docker Hub Client :class:`~libcloud.container.utils.docker.HubClient` is a shared utility class for interfacing to the public Docker Hub Service. + +You can use this class for fetching images to deploy to services like ECS + +.. literalinclude:: /examples/container/kubernetes/docker_hub.py + :language: python + +API Docs +-------- + +.. autoclass:: libcloud.container.drivers.kubernetes.KubernetesContainerDriver + :members: + :inherited-members: diff --git a/docs/container/examples.rst b/docs/container/examples.rst new file mode 100644 index 0000000000..f35662cdb0 --- /dev/null +++ b/docs/container/examples.rst @@ -0,0 +1,33 @@ +:orphan: + +Container Examples +================== + +Installing a container image and deploying it +--------------------------------------------- + +This example shows how to get install a container image, deploy and start that container. + +.. note:: + This example works with Libcloud version 0.21.0 and above. + +.. literalinclude:: /examples/container/install_and_deploy.py + :language: python + +Working with cluster supported providers +---------------------------------------- + +This example shows listing the clusters, find a specific named cluster and deploying a container to it. + +.. literalinclude:: /examples/container/working_with_clusters.py + :language: python + +Working with docker hub +----------------------- + +Docker Hub Client :class:`~libcloud.container.utils.docker.HubClient` is a shared utility class for interfacing to the public Docker Hub Service. + +You can use this class for fetching images to deploy to services like ECS + +.. literalinclude:: /examples/container/docker_hub.py + :language: python \ No newline at end of file diff --git a/docs/container/index.rst b/docs/container/index.rst new file mode 100644 index 0000000000..f763e9d91e --- /dev/null +++ b/docs/container/index.rst @@ -0,0 +1,141 @@ +Container +========= + +.. note:: + + Container API is available in Libcloud 1.0.0RC and higher. + +.. note:: + + Container API is currently in an EXPERIMENTAL state. + +Container API allows users to install and deploy containers onto container based virtualization platforms. This is designed to target both +on-premise installations of software like Docker as well as interfacing with Cloud Service Providers that offer Container-as-a-Service APIs. + + +.. graphviz:: + + digraph G { + graph [ fontname = "Roboto Slab", + fontsize = 18, + label = "Using the driver to deploy containers with or without clusters" ]; + + subgraph noncluster { + style=filled; + color=lightgrey; + node [style=filled,color=white]; + list_images -> install_image -> deploy_container; + label = "Non-Cluster Container Driver"; + } + + subgraph cluster { + node [style=filled]; + list_locations -> list_clusters -> create_cluster; + label = "Cluster supported Container Driver"; + color=blue + } + __init__ -> list_images; + __init__ -> list_locations; + create_cluster -> list_images; + deploy_container -> end; + + __init__ [shape=square]; + end [shape=squae]; + } + +For a working example of the container driver with cluster support, see the example for Amazon's Elastic Container Service: + +.. literalinclude:: /examples/container/ecs/deploy_container.py + :language: python + +For an example of the simple container support, see the Docker example: + +.. literalinclude:: /examples/container/docker/deploy_container.py + :language: python + +Drivers +------- +Container-as-a-Service providers will implement the `ContainerDriver` class to provide functionality for : + +* Listing deployed containers +* Starting, stopping and restarting containers (where supported) +* Destroying containers +* Creating/deploying containers +* Listing container images +* Installing container images (pulling an image from a local copy or remote repository) + +Driver base API documentation is found here: + +* :class:`~libcloud.container.base.ContainerDriver` - A driver for interfacing to a container provider + + +Simple Container Support +------------------------ + +* :class:`~libcloud.container.base.ContainerImage` - Represents an image that can be deployed, like an application or an operating system +* :class:`~libcloud.container.base.Container` - Represents a deployed container image running on a container host + +Cluster Suppport +---------------- + +Cluster support extends on the basic driver functions, but where drivers implement the class-level attribute `supports_clusters` as True +clusters may be listed, created and destroyed. When containers are deployed, the target cluster can be specified. + +* :class:`~libcloud.container.base.ContainerCluster` - Represents a deployed container image running on a container host +* :class:`~libcloud.container.base.ClusterLocation` - Represents a location for clusters to be deployed + +Bootstrapping Docker with Compute Drivers +----------------------------------------- + +The compute and container drivers can be combined using the :doc:`deployment ` feature of the compute driver to bootstrap an installation of a container virtualization provider like Docker. +Then using the Container driver, you can connect to that API and install images and deploy containers. + +.. graphviz:: + + digraph G2 { + + subgraph compute { + style=filled; + color=lightgrey; + node [style=filled,color=white]; + create_node -> deploy_node; + label = "Compute API"; + } + + subgraph container { + node [style=filled]; + __init__ -> install_image -> deploy_container; + label = "Container API"; + color=blue + } + start -> create_node; + deploy_node -> __init__; + deploy_container -> end; + + start [shape=Mdiamond]; + end [shape=Msquare]; + } + + +Supported Providers +------------------- + +For a list of supported providers see :doc:`supported providers page +`. + +Examples +-------- + +We have :doc:`examples of several common patterns `. + +API Reference +------------- + +For a full reference of all the classes and methods exposed by the Container +API, see :doc:`this page `. + +Utility Classes +--------------- + +There are some utility classes for example, a Docker Hub API client for fetching images +and iterating through repositories see :doc:`this page `. \ No newline at end of file diff --git a/docs/container/supported_providers.rst b/docs/container/supported_providers.rst new file mode 100644 index 0000000000..c7b8fdc055 --- /dev/null +++ b/docs/container/supported_providers.rst @@ -0,0 +1,14 @@ +:orphan: + +Supported Providers +=================== + +Provider Matrix +--------------- + +.. include:: _supported_providers.rst + +Supported Methods +----------------- + +.. include:: _supported_methods.rst \ No newline at end of file diff --git a/docs/container/utilities.rst b/docs/container/utilities.rst new file mode 100644 index 0000000000..c2b17e9e08 --- /dev/null +++ b/docs/container/utilities.rst @@ -0,0 +1,7 @@ +:orphan: + +Container Utility API +===================== + +.. autoclass:: libcloud.container.utils.docker.HubClient + :members: diff --git a/docs/examples/container/docker/deploy_container.py b/docs/examples/container/docker/deploy_container.py new file mode 100644 index 0000000000..7c59dc2b21 --- /dev/null +++ b/docs/examples/container/docker/deploy_container.py @@ -0,0 +1,10 @@ +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver + +cls = get_driver(Provider.DOCKER) + +driver = cls(host='https://198.61.239.128', port=4243, + key_file='key.pem', cert_file='cert.pem') + +image = driver.install_image('tomcat:8.0') +container = driver.deploy_container('tomcat', image) diff --git a/docs/examples/container/docker/instantiate_driver.py b/docs/examples/container/docker/instantiate_driver.py new file mode 100644 index 0000000000..6c6c54c28d --- /dev/null +++ b/docs/examples/container/docker/instantiate_driver.py @@ -0,0 +1,9 @@ +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver + +cls = get_driver(Provider.DOCKER) + +conn = cls(host='https://198.61.239.128', port=4243, + key_file='key.pem', cert_file='cert.pem') + +conn.list_images() diff --git a/docs/examples/container/docker_hub.py b/docs/examples/container/docker_hub.py new file mode 100644 index 0000000000..dd9d1a69e7 --- /dev/null +++ b/docs/examples/container/docker_hub.py @@ -0,0 +1,20 @@ +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver +from libcloud.container.utils.docker import HubClient + +cls = get_driver(Provider.ECS) + +conn = cls(access_id='SDHFISJDIFJSIDFJ', + secret='THIS_IS)+_MY_SECRET_KEY+I6TVkv68o4H', + region='ap-southeast-2') +hub = HubClient() + +image = hub.get_image('ubuntu', 'latest') + +for cluster in conn.list_clusters(): + print(cluster.name) + if cluster.name == 'default': + container = conn.deploy_container( + cluster=cluster, + name='my-simple-app', + image=image) diff --git a/docs/examples/container/ecs/container_registry.py b/docs/examples/container/ecs/container_registry.py new file mode 100644 index 0000000000..9df770f628 --- /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-image:14.04 + +# Deploy that image +cluster = conn.list_clusters()[0] +container = conn.deploy_container( + cluster=cluster, + name='my-simple-app', + image=image +) diff --git a/docs/examples/container/ecs/deploy_container.py b/docs/examples/container/ecs/deploy_container.py new file mode 100644 index 0000000000..6d005a4d81 --- /dev/null +++ b/docs/examples/container/ecs/deploy_container.py @@ -0,0 +1,24 @@ +from libcloud.container.base import ContainerImage +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver + +cls = get_driver(Provider.ECS) + +conn = cls(access_id='SDHFISJDIFJSIDFJ', + secret='THIS_IS)+_MY_SECRET_KEY+I6TVkv68o4H', + region='ap-southeast-2') + +for cluster in conn.list_clusters(): + print(cluster.name) + if cluster.name == 'default': + container = conn.deploy_container( + cluster=cluster, + name='my-simple-app', + image=ContainerImage( + id=None, + name='simple-app', + path='simple-app', + version=None, + driver=conn + ) + ) diff --git a/docs/examples/container/ecs/instantiate_driver.py b/docs/examples/container/ecs/instantiate_driver.py new file mode 100644 index 0000000000..4085fef93f --- /dev/null +++ b/docs/examples/container/ecs/instantiate_driver.py @@ -0,0 +1,14 @@ +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver + +cls = get_driver(Provider.ECS) + +conn = cls(access_id='SDHFISJDIFJSIDFJ', + secret='THIS_IS)+_MY_SECRET_KEY+I6TVkv68o4H', + region='ap-southeast-2') + +for container in conn.list_containers(): + print(container.name) + +for cluster in conn.list_clusters(): + print(cluster.name) diff --git a/docs/examples/container/install_and_deploy.py b/docs/examples/container/install_and_deploy.py new file mode 100644 index 0000000000..4e25c5485a --- /dev/null +++ b/docs/examples/container/install_and_deploy.py @@ -0,0 +1,12 @@ +from libcloud.container.providers import get_driver +from libcloud.container.types import Provider + +CREDS = ('user', 'api key') + +Cls = get_driver(Provider.DOCKER) +driver = Cls(*CREDS) + +image = driver.install_image('tomcat:8.0') +container = driver.deploy_container('tomcat', image) + +container.restart() diff --git a/docs/examples/container/joyent/instantiate_driver.py b/docs/examples/container/joyent/instantiate_driver.py new file mode 100644 index 0000000000..97f011eae9 --- /dev/null +++ b/docs/examples/container/joyent/instantiate_driver.py @@ -0,0 +1,9 @@ +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver + +cls = get_driver(Provider.JOYENT) + +conn = cls(host='us-east-1.docker.joyent.com', port=2376, + key_file='key.pem', cert_file='~/.sdc/docker/admin/ca.pem') + +conn.list_images() diff --git a/docs/examples/container/kubernetes/docker_hub.py b/docs/examples/container/kubernetes/docker_hub.py new file mode 100644 index 0000000000..dce1befd93 --- /dev/null +++ b/docs/examples/container/kubernetes/docker_hub.py @@ -0,0 +1,20 @@ +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver +from libcloud.container.utils.docker import HubClient + +cls = get_driver(Provider.KUBERNETES) + +conn = cls(key='my_username', + secret='THIS_IS)+_MY_SECRET_KEY+I6TVkv68o4H', + host='126.32.21.4') +hub = HubClient() + +image = hub.get_image('ubuntu', 'latest') + +for cluster in conn.list_clusters(): + print(cluster.name) + if cluster.name == 'default': + container = conn.deploy_container( + cluster=cluster, + name='my-simple-app', + image=image) diff --git a/docs/examples/container/kubernetes/instantiate_driver.py b/docs/examples/container/kubernetes/instantiate_driver.py new file mode 100644 index 0000000000..95fe997fd9 --- /dev/null +++ b/docs/examples/container/kubernetes/instantiate_driver.py @@ -0,0 +1,14 @@ +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver + +cls = get_driver(Provider.KUBERNETES) + +conn = cls(key='my_username', + secret='THIS_IS)+_MY_SECRET_KEY+I6TVkv68o4H', + host='126.32.21.4') + +for container in conn.list_containers(): + print(container.name) + +for cluster in conn.list_clusters(): + print(cluster.name) diff --git a/docs/examples/container/working_with_clusters.py b/docs/examples/container/working_with_clusters.py new file mode 100644 index 0000000000..e4d519e63b --- /dev/null +++ b/docs/examples/container/working_with_clusters.py @@ -0,0 +1,24 @@ +from libcloud.container.base import ContainerImage +from libcloud.container.types import Provider +from libcloud.container.providers import get_driver + +cls = get_driver(Provider.ECS) + +conn = cls(access_id='SDHFISJDIFJSIDFJ', + secret='THIS_IS)+_MY_SECRET_KEY+I6TVkv68o4H', + region='ap-southeast-2') + +for cluster in conn.list_clusters(): + print(cluster.name) + if cluster.name == 'my-cluster': + conn.list_containers(cluster=cluster) + container = conn.deploy_container( + name='my-simple-app', + image=ContainerImage( + id=None, + name='simple-app', + path='simple-app', + version=None, + driver=conn + ), + cluster=cluster) diff --git a/docs/index.rst b/docs/index.rst index 929151c797..d44bc36f2e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,9 +13,9 @@ Resource you can manage with Libcloud are divided in the following categories: Rackspace CloudFiles * :doc:`Load Balancers as a Service ` - services such as Amazon Elastic Load Balancer and GoGrid LoadBalancers * :doc:`DNS as a Service ` - services such as Amazon Route 53 and Zerigo +* :doc:`Container Services ` - container virtualization like Docker and Rkt as well as container based services * :doc:`Backup as a Service ` - services such as Amazon EBS and OpenStack Freezer - .. figure:: /_static/images/supported_providers.png :align: center @@ -38,6 +38,7 @@ Main storage/index loadbalancer/index dns/index + container/index backup/index troubleshooting api_docs diff --git a/docs/supported_providers.rst b/docs/supported_providers.rst index f4735e627b..f7f24645ac 100644 --- a/docs/supported_providers.rst +++ b/docs/supported_providers.rst @@ -57,21 +57,21 @@ Supported Methods (CDN) .. include:: storage/_supported_methods_cdn.rst -DNS ---- +Container +--------- Provider Matrix ~~~~~~~~~~~~~~~ -.. include:: dns/_supported_providers.rst +.. include:: container/_supported_providers.rst Supported Methods ~~~~~~~~~~~~~~~~~ -.. include:: dns/_supported_methods.rst +.. include:: container/_supported_methods.rst Backup ---- +------ Provider Matrix ~~~~~~~~~~~~~~~ @@ -81,4 +81,17 @@ Provider Matrix Supported Methods ~~~~~~~~~~~~~~~~~ -.. include:: backup/_supported_methods.rst \ No newline at end of file +.. include:: backup/_supported_methods.rst + +DNS +--- + +Provider Matrix +~~~~~~~~~~~~~~~ + +.. include:: dns/_supported_providers.rst + +Supported Methods +~~~~~~~~~~~~~~~~~ + +.. include:: dns/_supported_methods.rst diff --git a/libcloud/common/aws.py b/libcloud/common/aws.py index c0ac9b884d..2cc1d4b045 100644 --- a/libcloud/common/aws.py +++ b/libcloud/common/aws.py @@ -20,12 +20,18 @@ import time from hashlib import sha256 +try: + import simplejson as json +except ImportError: + import json + try: from lxml import etree as ET except ImportError: from xml.etree import ElementTree as ET from libcloud.common.base import ConnectionUserAndKey, XmlResponse, BaseDriver +from libcloud.common.base import JsonResponse from libcloud.common.types import InvalidCredsError, MalformedResponseError from libcloud.utils.py3 import b, httplib, urlquote from libcloud.utils.xml import findtext, findall @@ -179,7 +185,8 @@ def __init__(self, access_key, access_secret, version, connection): def get_request_params(self, params, method='GET', path='/'): return params - def get_request_headers(self, params, headers, method='GET', path='/'): + def get_request_headers(self, params, headers, method='GET', path='/', + data=None): return params, headers @@ -236,27 +243,32 @@ def _get_aws_auth_param(self, params, secret_key, path='/'): class AWSRequestSignerAlgorithmV4(AWSRequestSigner): def get_request_params(self, params, method='GET', path='/'): - params['Version'] = self.version + if method == 'GET': + params['Version'] = self.version return params - def get_request_headers(self, params, headers, method='GET', path='/'): + def get_request_headers(self, params, headers, method='GET', path='/', + data=None): now = datetime.utcnow() headers['X-AMZ-Date'] = now.strftime('%Y%m%dT%H%M%SZ') headers['Authorization'] = \ self._get_authorization_v4_header(params=params, headers=headers, - dt=now, method=method, path=path) + dt=now, method=method, path=path, + data=data) return params, headers def _get_authorization_v4_header(self, params, headers, dt, method='GET', - path='/'): - assert method == 'GET', 'AWS Signature V4 not implemented for ' \ - 'other methods than GET' + path='/', data=None): + assert method in ['GET', 'POST'], 'AWS Signature V4 ' \ + 'not implemented for ' \ + 'other methods than GET and POST' credentials_scope = self._get_credential_scope(dt=dt) signed_headers = self._get_signed_headers(headers=headers) signature = self._get_signature(params=params, headers=headers, - dt=dt, method=method, path=path) + dt=dt, method=method, path=path, + data=data) return 'AWS4-HMAC-SHA256 Credential=%(u)s/%(c)s, ' \ 'SignedHeaders=%(sh)s, Signature=%(s)s' % { @@ -266,11 +278,12 @@ def _get_authorization_v4_header(self, params, headers, dt, method='GET', 's': signature } - def _get_signature(self, params, headers, dt, method, path): + def _get_signature(self, params, headers, dt, method, path, data): key = self._get_key_to_sign_with(dt) string_to_sign = self._get_string_to_sign(params=params, headers=headers, dt=dt, - method=method, path=path) + method=method, path=path, + data=data) return _sign(key=key, msg=string_to_sign, hex=True) def _get_key_to_sign_with(self, dt): @@ -283,11 +296,12 @@ def _get_key_to_sign_with(self, dt): self.connection.service_name), 'aws4_request') - def _get_string_to_sign(self, params, headers, dt, method, path): + def _get_string_to_sign(self, params, headers, dt, method, path, data): canonical_request = self._get_canonical_request(params=params, headers=headers, method=method, - path=path) + path=path, + data=data) return '\n'.join(['AWS4-HMAC-SHA256', dt.strftime('%Y%m%dT%H%M%SZ'), @@ -307,8 +321,11 @@ def _get_canonical_headers(self, headers): return '\n'.join([':'.join([k.lower(), v.strip()]) for k, v in sorted(headers.items())]) + '\n' - def _get_payload_hash(self): - return _hash('') + def _get_payload_hash(self, method, data=None): + if method == 'GET': + return _hash('') + elif method == 'POST': + return _hash(data) def _get_request_params(self, params): # For self.method == GET @@ -316,14 +333,14 @@ def _get_request_params(self, params): (urlquote(k, safe=''), urlquote(str(v), safe='~')) for k, v in sorted(params.items())]) - def _get_canonical_request(self, params, headers, method, path): + def _get_canonical_request(self, params, headers, method, path, data): return '\n'.join([ method, path, self._get_request_params(params), self._get_canonical_headers(headers), self._get_signed_headers(headers), - self._get_payload_hash() + self._get_payload_hash(method, data) ]) @@ -364,10 +381,23 @@ def pre_connect_hook(self, params, headers): params, headers = self.signer.get_request_headers(params=params, headers=headers, method=self.method, - path=self.action) + path=self.action, + data=self.data) return params, headers +class AWSJsonResponse(JsonResponse): + """ + Amazon ECS response class. + ECS API uses JSON unlike the s3, elb drivers + """ + def parse_error(self): + response = json.loads(self.body) + code = response['__type'] + message = response.get('Message', response['message']) + return ('%s: %s' % (code, message)) + + def _sign(key, msg, hex=False): if hex: return hmac.new(b(key), b(msg), hashlib.sha256).hexdigest() diff --git a/libcloud/common/base.py b/libcloud/common/base.py index 959215a316..177798d030 100644 --- a/libcloud/common/base.py +++ b/libcloud/common/base.py @@ -744,6 +744,7 @@ def request(self, action, params=None, data=None, headers=None, action = self.morph_action_hook(action) self.action = action self.method = method + self.data = data # Extend default parameters params = self.add_default_params(params) diff --git a/libcloud/container/__init__.py b/libcloud/container/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libcloud/container/base.py b/libcloud/container/base.py new file mode 100644 index 0000000000..ea41e500e9 --- /dev/null +++ b/libcloud/container/base.py @@ -0,0 +1,416 @@ +# 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. + +from __future__ import with_statement + +from libcloud.common.base import ConnectionUserAndKey, BaseDriver + + +__all__ = [ + 'Container', + 'ContainerImage', + 'ContainerCluster', + 'ClusterLocation', + 'ContainerDriver' +] + + +class Container(object): + """ + Container. + """ + + def __init__(self, id, name, image, state, + ip_addresses, driver, extra=None): + """ + :param id: Container id. + :type id: ``str`` + + :param name: The name of the container. + :type name: ``str`` + + :param image: The image this container was deployed using. + :type image: :class:`.ContainerImage` + + :param state: The state of the container, e.g. running + :type state: :class:`libcloud.container.types.ContainerState` + + :param ip_addresses: A list of IP addresses for this container + :type ip_addresses: ``list`` of ``str`` + + :param driver: ContainerDriver instance. + :type driver: :class:`.ContainerDriver` + + :param extra: (optional) Extra attributes (driver specific). + :type extra: ``dict`` + """ + self.id = str(id) if id else None + self.name = name + self.image = image + self.state = state + self.ip_addresses = ip_addresses + self.driver = driver + self.extra = extra or {} + + def start(self): + return self.driver.start_container(container=self) + + def stop(self): + return self.driver.stop_container(container=self) + + def restart(self): + return self.driver.restart_container(container=self) + + def destroy(self): + return self.driver.destroy_container(container=self) + + def __repr__(self): + return ('' % + (self.id, self.name, self.state, + self.driver.name)) + + +class ContainerImage(object): + """ + Container Image. + """ + + def __init__(self, id, name, path, version, driver, extra=None): + """ + :param id: Container Image id. + :type id: ``str`` + + :param name: The name of the image. + :type name: ``str`` + + :param path: The path to the image + :type path: ``str`` + + :param version: The version of the image + :type version: ``str`` + + :param driver: ContainerDriver instance. + :type driver: :class:`.ContainerDriver` + + :param extra: (optional) Extra attributes (driver specific). + :type extra: ``dict`` + """ + self.id = str(id) if id else None + self.name = name + self.path = path + self.version = version + self.driver = driver + self.extra = extra or {} + + def deploy(self, name, parameters, *args, **kwargs): + return self.driver.deploy_container(name=name, + image=self, + parameters=parameters, + *args, + **kwargs) + + def __repr__(self): + return ('' % + (self.id, self.name, self.path)) + + +class ContainerCluster(object): + """ + A cluster group for containers + """ + + def __init__(self, id, name, driver, extra=None): + """ + :param id: Container Image id. + :type id: ``str`` + + :param name: The name of the image. + :type name: ``str`` + + :param driver: ContainerDriver instance. + :type driver: :class:`.ContainerDriver` + + :param extra: (optional) Extra attributes (driver specific). + :type extra: ``dict`` + """ + self.id = str(id) if id else None + self.name = name + self.driver = driver + self.extra = extra or {} + + def list_containers(self): + return self.driver.list_containers(cluster=self) + + def destroy(self): + return self.driver.destroy_cluster(cluster=self) + + def __repr__(self): + return ('' % + (self.id, self.name, self.driver.name)) + + +class ClusterLocation(object): + """ + A physical location where clusters can be. + + >>> from libcloud.container.drivers.dummy import DummyContainerDriver + >>> driver = DummyContainerDriver(0) + >>> location = driver.list_locations()[0] + >>> location.country + 'US' + """ + + def __init__(self, id, name, country, driver): + """ + :param id: Location ID. + :type id: ``str`` + + :param name: Location name. + :type name: ``str`` + + :param country: Location country. + :type country: ``str`` + + :param driver: Driver this location belongs to. + :type driver: :class:`.ContainerDriver` + """ + self.id = str(id) + self.name = name + self.country = country + self.driver = driver + + def __repr__(self): + return (('') + % (self.id, self.name, self.country, self.driver.name)) + + +class ContainerDriver(BaseDriver): + """ + A base ContainerDriver class to derive from + + This class is always subclassed by a specific driver. + """ + connectionCls = ConnectionUserAndKey + name = None + website = None + supports_clusters = False + """ + Whether the driver supports containers being deployed into clusters + """ + + def __init__(self, key, secret=None, secure=True, host=None, port=None, + **kwargs): + """ + :param key: API key or username to used (required) + :type key: ``str`` + + :param secret: Secret password to be used (required) + :type secret: ``str`` + + :param secure: Whether to use HTTPS or HTTP. Note: Some providers + only support HTTPS, and it is on by default. + :type secure: ``bool`` + + :param host: Override hostname used for connections. + :type host: ``str`` + + :param port: Override port used for connections. + :type port: ``int`` + + :return: ``None`` + """ + super(ContainerDriver, self).__init__( + key=key, secret=secret, secure=secure, + host=host, port=port, **kwargs) + + def install_image(self, path): + """ + Install a container image from a remote path. + + :param path: Path to the container image + :type path: ``str`` + + :rtype: :class:`.ContainerImage` + """ + raise NotImplementedError( + 'install_image not implemented for this driver') + + def list_images(self): + """ + List the installed container images + + :rtype: ``list`` of :class:`.ContainerImage` + """ + raise NotImplementedError( + 'list_images not implemented for this driver') + + 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:`.ContainerImage` + + :param cluster: Filter to containers in a cluster + :type cluster: :class:`.ContainerCluster` + + :rtype: ``list`` of :class:`.Container` + """ + raise NotImplementedError( + 'list_containers not implemented for this driver') + + def deploy_container(self, name, image, cluster=None, + parameters=None, start=True): + """ + Deploy an installed container image + + :param name: The name of the new container + :type name: ``str`` + + :param image: The container image to deploy + :type image: :class:`.ContainerImage` + + :param cluster: The cluster to deploy to, None is default + :type cluster: :class:`.ContainerCluster` + + :param parameters: Container Image parameters + :type parameters: ``str`` + + :param start: Start the container on deployment + :type start: ``bool`` + + :rtype: :class:`.Container` + """ + raise NotImplementedError( + 'deploy_container not implemented for this 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:`.Container` + """ + raise NotImplementedError( + 'get_container not implemented for this driver') + + def start_container(self, container): + """ + Start a deployed container + + :param container: The container to start + :type container: :class:`.Container` + + :rtype: :class:`.Container` + """ + raise NotImplementedError( + 'start_container not implemented for this driver') + + def stop_container(self, container): + """ + Stop a deployed container + + :param container: The container to stop + :type container: :class:`.Container` + + :rtype: :class:`.Container` + """ + raise NotImplementedError( + 'stop_container not implemented for this driver') + + def restart_container(self, container): + """ + Restart a deployed container + + :param container: The container to restart + :type container: :class:`.Container` + + :rtype: :class:`.Container` + """ + raise NotImplementedError( + 'restart_container not implemented for this driver') + + def destroy_container(self, container): + """ + Destroy a deployed container + + :param container: The container to destroy + :type container: :class:`.Container` + + :rtype: :class:`.Container` + """ + raise NotImplementedError( + 'destroy_container not implemented for this driver') + + def list_locations(self): + """ + Get a list of potential locations to deploy clusters into + + :rtype: ``list`` of :class:`.ClusterLocation` + """ + raise NotImplementedError( + 'list_locations not implemented for this driver') + + 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:`.ClusterLocation` + + :rtype: :class:`.ContainerCluster` + """ + raise NotImplementedError( + 'create_cluster not implemented for this driver') + + def destroy_cluster(self, cluster): + """ + Delete a cluster + + :return: ``True`` if the destroy was successful, otherwise ``False``. + :rtype: ``bool`` + """ + raise NotImplementedError( + 'destroy_cluster not implemented for this driver') + + def list_clusters(self, location=None): + """ + Get a list of potential locations to deploy clusters into + + :param location: The location to search in + :type location: :class:`.ClusterLocation` + + :rtype: ``list`` of :class:`.ContainerCluster` + """ + raise NotImplementedError( + 'list_clusters not implemented for this driver') + + def get_cluster(self, id): + """ + Get a cluster by ID + + :param id: The ID of the cluster to get + :type id: ``str`` + + :rtype: :class:`.ContainerCluster` + """ + raise NotImplementedError( + 'list_clusters not implemented for this driver') diff --git a/libcloud/container/drivers/__init__.py b/libcloud/container/drivers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libcloud/container/drivers/docker.py b/libcloud/container/drivers/docker.py new file mode 100644 index 0000000000..d7c8419187 --- /dev/null +++ b/libcloud/container/drivers/docker.py @@ -0,0 +1,656 @@ +# 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. + +import base64 +import datetime +import shlex +import re + +try: + import simplejson as json +except: + import json + +from libcloud.utils.py3 import httplib +from libcloud.utils.py3 import b + +from libcloud.common.base import JsonResponse, ConnectionUserAndKey +from libcloud.common.types import InvalidCredsError + +from libcloud.container.base import (Container, ContainerDriver, + ContainerImage) + +from libcloud.container.providers import Provider +from libcloud.container.types import ContainerState + + +VALID_RESPONSE_CODES = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, + httplib.NO_CONTENT] + + +class DockerResponse(JsonResponse): + + valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, + httplib.NO_CONTENT] + + def parse_body(self): + if len(self.body) == 0 and not self.parse_zero_length_body: + return self.body + + try: + # error responses are tricky in Docker. Eg response could be + # an error, but response status could still be 200 + content_type = self.headers.get('content-type', 'application/json') + if content_type == 'application/json' or content_type == '': + body = json.loads(self.body) + else: + body = self.body + except ValueError: + m = re.search('Error: (.+?)"', self.body) + if m: + error_msg = m.group(1) + raise Exception(error_msg) + else: + raise Exception( + 'ConnectionError: Failed to parse JSON response') + return body + + def parse_error(self): + if self.status == 401: + raise InvalidCredsError('Invalid credentials') + return self.body + + def success(self): + return self.status in self.valid_response_codes + + +class DockerException(Exception): + + def __init__(self, code, message): + self.code = code + self.message = message + self.args = (code, message) + + def __str__(self): + return "%s %s" % (self.code, self.message) + + def __repr__(self): + return "DockerException %s %s" % (self.code, self.message) + + +class DockerConnection(ConnectionUserAndKey): + + responseCls = DockerResponse + timeout = 60 + + def add_default_headers(self, headers): + """ + Add parameters that are necessary for every request + If user and password are specified, include a base http auth + header + """ + headers['Content-Type'] = 'application/json' + if self.user_id and self.key: + user_b64 = base64.b64encode(b('%s:%s' % (self.user_id, self.key))) + headers['Authorization'] = 'Basic %s' % (user_b64.decode('utf-8')) + return headers + + +class DockerContainerDriver(ContainerDriver): + """ + Docker container driver class. + + >>> from libcloud.container.providers import get_driver + >>> driver = get_driver('docker') + >>> conn = driver(host='198.61.239.128', port=4243) + >>> conn.list_containers() + or connecting to http basic auth protected https host: + >>> conn = driver('user', 'pass', host='https://198.61.239.128', port=443) + + connect with tls authentication, by providing a hostname, port, a private + key file (.pem) and certificate (.pem) file + >>> conn = driver(host='https://198.61.239.128', + >>> port=4243, key_file='key.pem', cert_file='cert.pem') + """ + + type = Provider.DOCKER + name = 'Docker' + website = 'http://docker.io' + connectionCls = DockerConnection + supports_clusters = False + + def __init__(self, key=None, secret=None, secure=False, host='localhost', + port=4243, key_file=None, cert_file=None): + """ + :param key: API key or username to used (required) + :type key: ``str`` + + :param secret: Secret password to be used (required) + :type secret: ``str`` + + :param secure: Whether to use HTTPS or HTTP. Note: Some providers + only support HTTPS, and it is on by default. + :type secure: ``bool`` + + :param host: Override hostname used for connections. + :type host: ``str`` + + :param port: Override port used for connections. + :type port: ``int`` + + :param key_file: Path to private key for TLS connection (optional) + :type key_file: ``str`` + + :param cert_file: Path to public key for TLS connection (optional) + :type cert_file: ``str`` + + :return: ``None`` + """ + super(DockerContainerDriver, self).__init__(key=key, secret=secret, + secure=secure, host=host, + port=port, + key_file=key_file, + cert_file=cert_file) + if host.startswith('https://'): + secure = True + + # strip the prefix + prefixes = ['http://', 'https://'] + for prefix in prefixes: + if host.startswith(prefix): + host = host.strip(prefix) + + if key_file or cert_file: + # docker tls authentication- + # https://docs.docker.com/articles/https/ + # We pass two files, a key_file with the + # private key and cert_file with the certificate + # libcloud will handle them through LibcloudHTTPSConnection + if not (key_file and cert_file): + raise Exception( + 'Needs both private key file and ' + 'certificate file for tls authentication') + self.connection.key_file = key_file + self.connection.cert_file = cert_file + self.connection.secure = True + else: + self.connection.secure = secure + + self.connection.host = host + self.connection.port = port + + def install_image(self, path): + """ + Install a container image from a remote path. + + :param path: Path to the container image + :type path: ``str`` + + :rtype: :class:`libcloud.container.base.ContainerImage` + """ + payload = { + } + data = json.dumps(payload) + + result = self.connection.request('/images/create?fromImage=%s' % + (path), data=data, method='POST') + if "errorDetail" in result.body: + raise DockerException(None, result.body) + try: + # get image id + image_id = re.findall( + r'{"status":"Download complete"' + r',"progressDetail":{},"id":"\w+"}', + result.body)[-1] + image_id = json.loads(image_id).get('id') + except: + raise DockerException(None, 'failed to install image') + + image = ContainerImage( + id=image_id, + name=path, + path=path, + version=None, + driver=self.connection.driver, + extra={}) + return image + + def list_images(self): + """ + List the installed container images + + :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage` + """ + result = self.connection.request('/images/json').object + images = [] + for image in result: + try: + name = image.get('RepoTags')[0] + except: + name = image.get('Id') + images.append(ContainerImage( + id=image.get('Id'), + name=name, + path=name, + version=None, + driver=self.connection.driver, + extra={ + "created": image.get('Created'), + "size": image.get('Size'), + "virtual_size": image.get('VirtualSize'), + }, + )) + + return images + + def list_containers(self, image=None, all=True): + """ + List the deployed container images + + :param image: Filter to containers with a certain image + :type image: :class:`libcloud.container.base.ContainerImage` + + :param all: Show all container (including stopped ones) + :type all: ``bool`` + + :rtype: ``list`` of :class:`libcloud.container.base.Container` + """ + if all: + ex = '?all=1' + else: + ex = '' + try: + result = self.connection.request( + "/containers/json%s" % (ex)).object + except Exception as exc: + if hasattr(exc, 'errno') and exc.errno == 111: + raise DockerException( + exc.errno, + 'Make sure docker host is accessible' + 'and the API port is correct') + raise + + containers = [self._to_container(value) for value in result] + return containers + + def deploy_container(self, name, image, parameters=None, start=True, + command=None, hostname=None, user='', + stdin_open=True, tty=True, + mem_limit=0, ports=None, environment=None, dns=None, + volumes=None, volumes_from=None, + network_disabled=False, entrypoint=None, + cpu_shares=None, working_dir='', domainname=None, + memswap_limit=0, port_bindings=None): + """ + Deploy an installed container image + + For details on the additional parameters see : http://bit.ly/1PjMVKV + + :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 parameters: Container Image parameters + :type parameters: ``str`` + + :param start: Start the container on deployment + :type start: ``bool`` + + :rtype: :class:`Container` + """ + command = shlex.split(str(command)) + if port_bindings is None: + port_bindings = {} + params = { + 'name': name + } + + payload = { + 'Hostname': hostname, + 'Domainname': domainname, + 'ExposedPorts': ports, + 'User': user, + 'Tty': tty, + 'OpenStdin': stdin_open, + 'StdinOnce': False, + 'Memory': mem_limit, + 'AttachStdin': True, + 'AttachStdout': True, + 'AttachStderr': True, + 'Env': environment, + 'Cmd': command, + 'Dns': dns, + 'Image': image.name, + 'Volumes': volumes, + 'VolumesFrom': volumes_from, + 'NetworkDisabled': network_disabled, + 'Entrypoint': entrypoint, + 'CpuShares': cpu_shares, + 'WorkingDir': working_dir, + 'MemorySwap': memswap_limit, + 'PublishAllPorts': True, + 'PortBindings': port_bindings, + } + + data = json.dumps(payload) + try: + result = self.connection.request('/containers/create', data=data, + params=params, method='POST') + except Exception as e: + if e.message.startswith('No such image:'): + raise DockerException(None, 'No such image: %s' % image.name) + else: + raise DockerException(None, e) + + id_ = result.object['Id'] + + payload = { + 'Binds': [], + 'PublishAllPorts': True, + 'PortBindings': port_bindings, + } + + data = json.dumps(payload) + if start: + result = self.connection.request( + '/containers/%s/start' % id_, data=data, + method='POST') + + return self.get_container(id_) + + 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` + """ + result = self.connection.request("/containers/%s/json" % + id).object + + return self._to_container(result) + + def start_container(self, container): + """ + Start a container + + :param container: The container to be started + :type container: :class:`libcloud.container.base.Container` + + :return: The container refreshed with current data + :rtype: :class:`libcloud.container.base.Container` + """ + payload = { + 'Binds': [], + 'PublishAllPorts': True, + } + data = json.dumps(payload) + result = self.connection.request( + '/containers/%s/start' % + (container.id), + method='POST', data=data) + if result.status in VALID_RESPONSE_CODES: + return self.get_container(container.id) + else: + raise DockerException(result.status, + 'failed to start container') + + def stop_container(self, container): + """ + Stop a container + + :param container: The container to be stopped + :type container: :class:`libcloud.container.base.Container` + + :return: The container refreshed with current data + :rtype: :class:`libcloud.container.base.Container` + """ + result = self.connection.request('/containers/%s/stop' % + (container.id), + method='POST') + if result.status in VALID_RESPONSE_CODES: + return self.get_container(container.id) + else: + raise DockerException(result.status, + 'failed to stop container') + + def restart_container(self, container): + """ + Restart a container + + :param container: The container to be stopped + :type container: :class:`libcloud.container.base.Container` + + :return: The container refreshed with current data + :rtype: :class:`libcloud.container.base.Container` + """ + data = json.dumps({'t': 10}) + # number of seconds to wait before killing the container + result = self.connection.request('/containers/%s/restart' % + (container.id), + data=data, method='POST') + if result.status in VALID_RESPONSE_CODES: + return self.get_container(container.id) + else: + raise DockerException(result.status, + 'failed to restart container') + + def destroy_container(self, container): + """ + Remove a container + + :param container: The container to be destroyed + :type container: :class:`libcloud.container.base.Container` + + :return: True if the destroy was successful, False otherwise. + :rtype: ``bool`` + """ + result = self.connection.request('/containers/%s' % (container.id), + method='DELETE') + return result.status in VALID_RESPONSE_CODES + + def ex_list_processes(self, container): + """ + List processes running inside a container + + :param container: The container to list processes for. + :type container: :class:`libcloud.container.base.Container` + + :rtype: ``str`` + """ + result = self.connection.request("/containers/%s/top" % + container.id).object + + return result + + def ex_rename_container(self, container, name): + """ + Rename a container + + :param container: The container to be renamed + :type container: :class:`libcloud.container.base.Container` + + :param name: The new name + :type name: ``str`` + + :rtype: :class:`libcloud.container.base.Container` + """ + result = self.connection.request('/containers/%s/rename?name=%s' + % (container.id, name), + method='POST') + if result.status in VALID_RESPONSE_CODES: + return self.get_container(container.id) + + def ex_get_logs(self, container, stream=False): + """ + Get container logs + + If stream == True, logs will be yielded as a stream + From Api Version 1.11 and above we need a GET request to get the logs + Logs are in different format of those of Version 1.10 and below + + :param container: The container to list logs for + :type container: :class:`libcloud.container.base.Container` + + :param stream: Stream the output + :type stream: ``bool`` + + :rtype: ``bool`` + """ + payload = {} + data = json.dumps(payload) + + if float(self._get_api_version()) > 1.10: + result = self.connection.request( + "/containers/%s/logs?follow=%s&stdout=1&stderr=1" % + (container.id, str(stream))).object + logs = result + else: + result = self.connection.request( + "/containers/%s/attach?logs=1&stream=%s&stdout=1&stderr=1" % + (container.id, str(stream)), method='POST', data=data) + logs = result.body + + return logs + + def ex_search_images(self, term): + """Search for an image on Docker.io. + Returns a list of ContainerImage objects + + >>> images = conn.ex_search_images(term='mistio') + >>> images + [, + ] + + :param term: The search term + :type term: ``str`` + + :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage` + """ + + term = term.replace(' ', '+') + result = self.connection.request('/images/search?term=%s' % + term).object + images = [] + for image in result: + name = image.get('name') + images.append( + ContainerImage( + id=name, + path=name, + version=None, + name=name, + driver=self.connection.driver, + extra={ + "description": image.get('description'), + "is_official": image.get('is_official'), + "is_trusted": image.get('is_trusted'), + "star_count": image.get('star_count'), + }, + )) + + return images + + def ex_delete_image(self, image): + """ + Remove image from the filesystem + + :param image: The image to remove + :type image: :class:`libcloud.container.base.ContainerImage` + + :rtype: ``bool`` + """ + result = self.connection.request('/images/%s' % (image.name), + method='DELETE') + return result.status in VALID_RESPONSE_CODES + + def _to_container(self, data): + """ + Convert container in Container instances + """ + try: + name = data.get('Name').strip('/') + except: + try: + name = data.get('Names')[0].strip('/') + except: + name = data.get('Id') + state = data.get('State') + status = data.get('Status', + state.get('Status') + if state is not None else None) + if 'Exited' in status: + state = ContainerState.STOPPED + elif status.startswith('Up '): + state = ContainerState.RUNNING + else: + state = ContainerState.STOPPED + image = data.get('Image') + ports = data.get('Ports', []) + created = data.get('Created') + if isinstance(created, float): + created = ts_to_str(created) + extra = { + 'id': data.get('Id'), + 'status': data.get('Status'), + 'created': created, + 'image': image, + 'ports': ports, + 'command': data.get('Command'), + 'sizerw': data.get('SizeRw'), + 'sizerootfs': data.get('SizeRootFs'), + } + ips = [] + if ports is not None: + for port in ports: + if port.get('IP') is not None: + ips.append(port.get('IP')) + return Container( + id=data['Id'], + name=name, + image=ContainerImage( + id=data.get('ImageID', None), + path=image, + name=image, + version=None, + driver=self.connection.driver + ), + ip_addresses=ips, + state=state, + driver=self.connection.driver, + extra=extra) + + def _get_api_version(self): + """ + Get the docker API version information + """ + result = self.connection.request('/version').object + api_version = result.get('ApiVersion') + + return api_version + + +def ts_to_str(timestamp): + """ + Return a timestamp as a nicely formated datetime string. + """ + date = datetime.datetime.fromtimestamp(timestamp) + date_string = date.strftime("%d/%m/%Y %H:%M %Z") + return date_string diff --git a/libcloud/container/drivers/dummy.py b/libcloud/container/drivers/dummy.py new file mode 100644 index 0000000000..2c99259490 --- /dev/null +++ b/libcloud/container/drivers/dummy.py @@ -0,0 +1,46 @@ +# 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. + +from libcloud.container.base import ContainerDriver + + +class DummyContainerDriver(ContainerDriver): + """ + Dummy Container driver. + + >>> from libcloud.container.drivers.dummy import DummyContainerDriver + >>> driver = DummyContainerDriver('key', 'secret') + >>> driver.name + 'Dummy Container Provider' + """ + + name = 'Dummy Container Provider' + website = 'http://example.com' + supports_clusters = False + + def __init__(self, api_key, api_secret): + """ + :param api_key: API key or username to used (required) + :type api_key: ``str`` + + :param api_secret: Secret password to be used (required) + :type api_secret: ``str`` + + :rtype: ``None`` + """ + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/libcloud/container/drivers/ecs.py b/libcloud/container/drivers/ecs.py new file mode 100644 index 0000000000..36c5be3e04 --- /dev/null +++ b/libcloud/container/drivers/ecs.py @@ -0,0 +1,627 @@ +# 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 + ) diff --git a/libcloud/container/drivers/joyent.py b/libcloud/container/drivers/joyent.py new file mode 100644 index 0000000000..39df0194d2 --- /dev/null +++ b/libcloud/container/drivers/joyent.py @@ -0,0 +1,73 @@ +# 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. + + +from libcloud.container.providers import Provider + +from libcloud.container.drivers.docker import (DockerContainerDriver, + DockerConnection) + + +class JoyentContainerDriver(DockerContainerDriver): + """ + Joyent Triton container driver class. + + >>> from libcloud.container.providers import get_driver + >>> driver = get_driver('joyent') + >>> conn = driver(host='https://us-east-1.docker.joyent.com', + port=2376, key_file='key.pem', cert_file='cert.pem') + """ + + type = Provider.JOYENT + name = 'Joyent Triton' + website = 'http://joyent.com' + connectionCls = DockerConnection + supports_clusters = False + + def __init__(self, key=None, secret=None, secure=False, host='localhost', + port=2376, key_file=None, cert_file=None): + + super(JoyentContainerDriver, self).__init__(key=key, secret=secret, + secure=secure, host=host, + port=port, + key_file=key_file, + cert_file=cert_file) + if host.startswith('https://'): + secure = True + + # strip the prefix + prefixes = ['http://', 'https://'] + for prefix in prefixes: + if host.startswith(prefix): + host = host.strip(prefix) + + if key_file or cert_file: + # docker tls authentication- + # https://docs.docker.com/articles/https/ + # We pass two files, a key_file with the + # private key and cert_file with the certificate + # libcloud will handle them through LibcloudHTTPSConnection + if not (key_file and cert_file): + raise Exception( + 'Needs both private key file and ' + 'certificate file for tls authentication') + self.connection.key_file = key_file + self.connection.cert_file = cert_file + self.connection.secure = True + else: + self.connection.secure = secure + + self.connection.host = host + self.connection.port = port diff --git a/libcloud/container/drivers/kubernetes.py b/libcloud/container/drivers/kubernetes.py new file mode 100644 index 0000000000..ed02112be7 --- /dev/null +++ b/libcloud/container/drivers/kubernetes.py @@ -0,0 +1,406 @@ +# 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. + +import base64 +import datetime + +try: + import simplejson as json +except: + import json + +from libcloud.utils.py3 import httplib +from libcloud.utils.py3 import b + +from libcloud.common.base import JsonResponse, ConnectionUserAndKey +from libcloud.common.types import InvalidCredsError + +from libcloud.container.base import (Container, ContainerDriver, + ContainerImage, ContainerCluster) + +from libcloud.container.providers import Provider +from libcloud.container.types import ContainerState + + +VALID_RESPONSE_CODES = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, + httplib.NO_CONTENT] + +ROOT_URL = '/api/' + + +class KubernetesResponse(JsonResponse): + + valid_response_codes = [httplib.OK, httplib.ACCEPTED, httplib.CREATED, + httplib.NO_CONTENT] + + def parse_error(self): + if self.status == 401: + raise InvalidCredsError('Invalid credentials') + return self.body + + def success(self): + return self.status in self.valid_response_codes + + +class KubernetesException(Exception): + + def __init__(self, code, message): + self.code = code + self.message = message + self.args = (code, message) + + def __str__(self): + return "%s %s" % (self.code, self.message) + + def __repr__(self): + return "KubernetesException %s %s" % (self.code, self.message) + + +class KubernetesConnection(ConnectionUserAndKey): + responseCls = KubernetesResponse + timeout = 60 + + def add_default_headers(self, headers): + """ + Add parameters that are necessary for every request + If user and password are specified, include a base http auth + header + """ + headers['Content-Type'] = 'application/json' + if self.key and self.secret: + user_b64 = base64.b64encode(b('%s:%s' % (self.key, self.secret))) + headers['Authorization'] = 'Basic %s' % (user_b64.decode('utf-8')) + return headers + + +class KubernetesPod(object): + def __init__(self, name, containers, namespace): + """ + A Kubernetes pod + """ + self.name = name + self.containers = containers + self.namespace = namespace + + +class KubernetesContainerDriver(ContainerDriver): + type = Provider.KUBERNETES + name = 'Kubernetes' + website = 'http://kubernetes.io' + connectionCls = KubernetesConnection + supports_clusters = True + + def __init__(self, key=None, secret=None, secure=False, host='localhost', + port=4243): + """ + :param key: API key or username to used (required) + :type key: ``str`` + + :param secret: Secret password to be used (required) + :type secret: ``str`` + + :param secure: Whether to use HTTPS or HTTP. Note: Some providers + only support HTTPS, and it is on by default. + :type secure: ``bool`` + + :param host: Override hostname used for connections. + :type host: ``str`` + + :param port: Override port used for connections. + :type port: ``int`` + + :return: ``None`` + """ + super(KubernetesContainerDriver, self).__init__(key=key, secret=secret, + secure=secure, + host=host, + port=port) + if host.startswith('https://'): + secure = True + + # strip the prefix + prefixes = ['http://', 'https://'] + for prefix in prefixes: + if host.startswith(prefix): + host = host.strip(prefix) + + self.connection.secure = secure + self.connection.key = key + self.connection.secret = secret + + self.connection.host = host + self.connection.port = port + + def list_containers(self, image=None, all=True): + """ + List the deployed container images + + :param image: Filter to containers with a certain image + :type image: :class:`libcloud.container.base.ContainerImage` + + :param all: Show all container (including stopped ones) + :type all: ``bool`` + + :rtype: ``list`` of :class:`libcloud.container.base.Container` + """ + try: + result = self.connection.request( + ROOT_URL + "v1/pods").object + except Exception as exc: + if hasattr(exc, 'errno') and exc.errno == 111: + raise KubernetesException( + exc.errno, + 'Make sure kube host is accessible' + 'and the API port is correct') + raise + + pods = [self._to_pod(value) for value in result['items']] + containers = [] + for pod in pods: + containers.extend(pod.containers) + return containers + + 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` + """ + result = self.connection.request(ROOT_URL + "v1/nodes/%s" % + id).object + + return self._to_container(result) + + def list_clusters(self): + """ + Get a list of namespaces that pods can be deployed into + + :param location: The location to search in + :type location: :class:`libcloud.container.base.ClusterLocation` + + :rtype: ``list`` of :class:`libcloud.container.base.ContainerCluster` + """ + try: + result = self.connection.request( + ROOT_URL + "v1/namespaces/").object + except Exception as exc: + if hasattr(exc, 'errno') and exc.errno == 111: + raise KubernetesException( + exc.errno, + 'Make sure kube host is accessible' + 'and the API port is correct') + raise + + clusters = [self._to_cluster(value) for value in result['items']] + return clusters + + def get_cluster(self, id): + """ + Get a cluster by ID + + :param id: The ID of the cluster to get + :type id: ``str`` + + :rtype: :class:`libcloud.container.base.ContainerCluster` + """ + result = self.connection.request(ROOT_URL + "v1/namespaces/%s" % + id).object + + return self._to_cluster(result) + + def destroy_cluster(self, cluster): + """ + Delete a cluster (namespace) + + :return: ``True`` if the destroy was successful, otherwise ``False``. + :rtype: ``bool`` + """ + self.connection.request(ROOT_URL + "v1/namespaces/%s" % + cluster.id, method='DELETE').object + return True + + def create_cluster(self, name, location=None): + """ + Create a container cluster (a namespace) + + :param name: The name of the cluster + :type name: ``str`` + + :param location: The location to create the cluster in + :type location: :class:`.ClusterLocation` + + :rtype: :class:`.ContainerCluster` + """ + request = { + 'metadata': { + 'name': name + } + } + result = self.connection.request(ROOT_URL + "v1/namespaces", + method='POST', + data=json.dumps(request)).object + return self._to_cluster(result) + + def deploy_container(self, name, image, cluster=None, + parameters=None, start=True): + """ + Deploy an installed container image. + In kubernetes this deploys a single container Pod. + https://cloud.google.com/container-engine/docs/pods/single-container + + :param name: The name of the new container + :type name: ``str`` + + :param image: The container image to deploy + :type image: :class:`.ContainerImage` + + :param cluster: The cluster to deploy to, None is default + :type cluster: :class:`.ContainerCluster` + + :param parameters: Container Image parameters + :type parameters: ``str`` + + :param start: Start the container on deployment + :type start: ``bool`` + + :rtype: :class:`.Container` + """ + if cluster is None: + namespace = 'default' + else: + namespace = cluster.id + request = { + "metadata": { + "name": name + }, + "spec": { + "containers": [ + { + "name": name, + "image": image.name + } + ] + } + } + result = self.connection.request(ROOT_URL + "v1/namespaces/%s/pods" + % namespace, + method='POST', + data=json.dumps(request)).object + return self._to_cluster(result) + + def destroy_container(self, container): + """ + Destroy a deployed container. Because the containers are single + container pods, this will delete the pod. + + :param container: The container to destroy + :type container: :class:`.Container` + + :rtype: ``bool`` + """ + return self.ex_delete_pod(container.extra['namespace'], + container.extra['pod']) + + def ex_list_pods(self): + """ + List available Pods + + :rtype: ``list`` of :class:`.KubernetesPod` + """ + result = self.connection.request(ROOT_URL + "v1/pods").object + return [self._to_pod(value) for value in result['items']] + + def ex_destroy_pod(self, namespace, pod_name): + """ + Delete a pod and the containers within it. + """ + self.connection.request( + ROOT_URL + "v1/namespaces/%s/pods/%s" % ( + namespace, pod_name), + method='DELETE').object + return True + + def _to_pod(self, data): + """ + Convert an API response to a Pod object + """ + container_statuses = data['status']['containerStatuses'] + containers = [] + # response contains the status of the containers in a separate field + for container in data['spec']['containers']: + spec = list(filter(lambda i: i['name'] == container['name'], + container_statuses))[0] + containers.append( + self._to_container(container, spec, data) + ) + return KubernetesPod( + name=data['metadata']['name'], + namespace=data['metadata']['namespace'], + containers=containers) + + def _to_container(self, data, container_status, pod_data): + """ + Convert container in Container instances + """ + return Container( + id=container_status['containerID'], + name=data['name'], + image=ContainerImage( + id=container_status['imageID'], + name=data['image'], + path=None, + version=None, + driver=self.connection.driver + ), + ip_addresses=None, + state=ContainerState.RUNNING, + driver=self.connection.driver, + extra={ + 'pod': pod_data['metadata']['name'], + 'namespace': pod_data['metadata']['namespace'] + }) + + def _to_cluster(self, data): + """ + Convert namespace to a cluster + """ + metadata = data['metadata'] + status = data['status'] + return ContainerCluster( + id=metadata['name'], + name=metadata['name'], + driver=self.connection.driver, + extra={'phase': status['phase']}) + + def _get_api_version(self): + """ + Get the docker API version information + """ + result = self.connection.request('/version').object + api_version = result.get('ApiVersion') + + return api_version + + +def ts_to_str(timestamp): + """ + Return a timestamp as a nicely formated datetime string. + """ + date = datetime.datetime.fromtimestamp(timestamp) + date_string = date.strftime("%d/%m/%Y %H:%M %Z") + return date_string diff --git a/libcloud/container/providers.py b/libcloud/container/providers.py new file mode 100644 index 0000000000..47897ed467 --- /dev/null +++ b/libcloud/container/providers.py @@ -0,0 +1,39 @@ +# 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. + +from libcloud.utils.misc import get_driver as get_provider_driver +from libcloud.utils.misc import set_driver as set_provider_driver +from libcloud.container.types import Provider + +DRIVERS = { + Provider.DUMMY: + ('libcloud.container.drivers.dummy', 'DummyContainerDriver'), + Provider.DOCKER: + ('libcloud.container.drivers.docker', 'DockerContainerDriver'), + Provider.JOYENT: + ('libcloud.container.drivers.joyent', 'JoyentContainerDriver'), + Provider.ECS: + ('libcloud.container.drivers.ecs', 'ElasticContainerDriver'), + Provider.KUBERNETES: + ('libcloud.container.drivers.kubernetes', 'KubernetesContainerDriver'), +} + + +def get_driver(provider): + return get_provider_driver(DRIVERS, provider) + + +def set_driver(provider, module, klass): + return set_provider_driver(DRIVERS, provider, module, klass) diff --git a/libcloud/container/types.py b/libcloud/container/types.py new file mode 100644 index 0000000000..263d9d44b6 --- /dev/null +++ b/libcloud/container/types.py @@ -0,0 +1,76 @@ +# 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. + +__all__ = [ + 'Provider', + 'ContainerState' +] + + +class Type(object): + @classmethod + def tostring(cls, value): + """Return the string representation of the state object attribute + :param str value: the state object to turn into string + :return: the uppercase string that represents the state object + :rtype: str + """ + return value.upper() + + @classmethod + def fromstring(cls, value): + """Return the state object attribute that matches the string + :param str value: the string to look up + :return: the state object attribute that matches the string + :rtype: str + """ + return getattr(cls, value.upper(), None) + + +class Provider(object): + DUMMY = 'dummy' + DOCKER = 'docker' + JOYENT = 'joyent' + ECS = 'ecs' + KUBERNETES = 'kubernetes' + + +class ContainerState(Type): + """ + Standard states for a container + + :cvar RUNNING: Container is running. + :cvar REBOOTING: Container is rebooting. + :cvar TERMINATED: Container is terminated. + This container can't be started later on. + :cvar STOPPED: Container is stopped. + This container can be started later on. + :cvar PENDING: Container is pending. + :cvar SUSPENDED: Container is suspended. + :cvar ERROR: Container is an error state. + Usually no operations can be performed + on the container once it ends up in the error state. + :cvar PAUSED: Container is paused. + :cvar UNKNOWN: Container state is unknown. + """ + RUNNING = 'running' + REBOOTING = 'rebooting' + TERMINATED = 'terminated' + PENDING = 'pending' + UNKNOWN = 'unknown' + STOPPED = 'stopped' + SUSPENDED = 'suspended' + ERROR = 'error' + PAUSED = 'paused' diff --git a/libcloud/container/utils/__init__.py b/libcloud/container/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/libcloud/container/utils/docker.py b/libcloud/container/utils/docker.py new file mode 100644 index 0000000000..ba83097488 --- /dev/null +++ b/libcloud/container/utils/docker.py @@ -0,0 +1,179 @@ +# 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. + +from __future__ import with_statement + +from base64 import b64encode + +from libcloud.common.base import Connection, JsonResponse +from libcloud.container.base import ContainerImage + +__all__ = [ + 'RegistryClient', + 'HubClient' +] + + +class DockerHubConnection(Connection): + responseCls = JsonResponse + + def __init__(self, host, username=None, password=None, + secure=True, + port=None, url=None, timeout=None, + proxy_url=None, backoff=None, retry_delay=None): + super(DockerHubConnection, self).__init__( + secure=secure, host=host, + port=port, url=url, + timeout=timeout, + proxy_url=proxy_url, + backoff=backoff, + retry_delay=retry_delay + ) + self.username = username + self.password = password + + def add_default_headers(self, headers): + headers['Content-Type'] = 'application/json' + if self.username is not None: + authstr = 'Basic ' + str( + b64encode( + ('%s:%s' % (self.username, + self.password)) + .encode('latin1')) + .strip() + ) + headers['Authorization'] = authstr + return headers + + +class RegistryClient(object): + """ + A client for the Docker v2 registry API + """ + connectionCls = DockerHubConnection + + def __init__(self, host, username=None, password=None, **kwargs): + """ + Construct a Docker hub client + + :param username: (optional) Your Hub account username + :type username: ``str`` + + :param password: (optional) Your hub account password + :type password: ``str`` + """ + self.connection = self.connectionCls(host, + username, + password, + **kwargs) + + def list_images(self, repository_name, namespace='library', max_count=100): + """ + List the tags (versions) in a repository + + :param repository_name: The name of the repository e.g. 'ubuntu' + :type repository_name: ``str`` + + :param namespace: (optional) The docker namespace + :type namespace: ``str`` + + :param max_count: The maximum number of records to return + :type max_count: ``int`` + + :return: A list of images + :rtype: ``list`` of :class:`libcloud.container.base.ContainerImage` + """ + path = '/v2/repositories/%s/%s/tags/?page=1&page_size=%s' \ + % (namespace, repository_name, max_count) + response = self.connection.request(path) + images = [] + for image in response.object['results']: + images.append(self._to_image(repository_name, image)) + return images + + def get_repository(self, repository_name, namespace='library'): + """ + Get the information about a specific repository + + :param repository_name: The name of the repository e.g. 'ubuntu' + :type repository_name: ``str`` + + :param namespace: (optional) The docker namespace + :type namespace: ``str`` + + :return: The details of the repository + :rtype: ``object`` + """ + path = '/v2/repositories/%s/%s' % (namespace, repository_name) + response = self.connection.request(path) + return response.object + + def get_image(self, repository_name, tag='latest', namespace='library'): + """ + Get an image from a repository with a specific tag + + :param repository_name: The name of the repository, e.g. ubuntu + :type repository_name: ``str`` + + :param tag: (optional) The image tag (defaults to latest) + :type tag: ``str`` + + :param namespace: (optional) The docker namespace + :type namespace: ``str`` + + :return: A container image + :rtype: :class:`libcloud.container.base.ContainerImage` + """ + path = '/v2/repositories/%s/%s/tags/%s' \ + % (namespace, repository_name, tag) + response = self.connection.request(path) + return self._to_image(repository_name, response.object) + + def _to_image(self, repository_name, obj): + path = '%s/%s:%s' % (self.connection.host, + repository_name, + obj['name']) + return ContainerImage( + id=obj['id'], + path=path, + name=path, + version=obj['name'], + extra={ + 'full_size': obj['full_size'] + }, + driver=None + ) + + +class HubClient(RegistryClient): + """ + A client for the Docker Hub API + + The hub is based on the v2 registry API + """ + host = 'registry.hub.docker.com' + + def __init__(self, username=None, password=None, **kwargs): + """ + Construct a Docker hub client + + :param username: (optional) Your Hub account username + :type username: ``str`` + + :param password: (optional) Your hub account password + :type password: ``str`` + """ + super(HubClient, self).__init__(self.host, username, + password, **kwargs) diff --git a/libcloud/test/__init__.py b/libcloud/test/__init__.py index a168630c2e..efc89c2f3a 100644 --- a/libcloud/test/__init__.py +++ b/libcloud/test/__init__.py @@ -123,6 +123,9 @@ def _get_method_name(self, type, use_param, qs, path): param = qs[use_param][0].replace('.', '_').replace('-', '_') meth_name = '%s_%s' % (meth_name, param) + if meth_name == '': + meth_name = 'root' + return meth_name diff --git a/libcloud/test/common/test_aws.py b/libcloud/test/common/test_aws.py index 6661f7d71c..c855534b6d 100644 --- a/libcloud/test/common/test_aws.py +++ b/libcloud/test/common/test_aws.py @@ -53,10 +53,10 @@ def test_v4_signature(self): 'SignedHeaders=accept-encoding;host;user-agent;x-amz-date, ' 'Signature=f9868f8414b3c3f856c7955019cc1691265541f5162b9b772d26044280d39bd3') - def test_v4_signature_raises_error_if_request_method_not_GET(self): + def test_v4_signature_raises_error_if_request_method_not_GET_OR_POST(self): with self.assertRaises(Exception): self.signer._get_authorization_v4_header(params={}, headers={}, - dt=self.now, method='POST') + dt=self.now, method='PUT') def test_v4_signature_contains_user_id(self): sig = self.signer._get_authorization_v4_header(params={}, headers={}, @@ -97,7 +97,7 @@ def _sign(key, msg, hex=False): mock_get_key.return_value = 'my_signing_key' mock_get_string.return_value = 'my_string_to_sign' sig = self.signer._get_signature({}, {}, self.now, - method='GET', path='/') + method='GET', path='/', data=None) self.assertEqual(sig, 'H|my_signing_key|my_string_to_sign') @@ -105,7 +105,7 @@ def test_get_string_to_sign(self): with mock.patch('hashlib.sha256') as mock_sha256: mock_sha256.return_value.hexdigest.return_value = 'chksum_of_canonical_request' to_sign = self.signer._get_string_to_sign({}, {}, self.now, - method='GET', path='/') + method='GET', path='/', data=None) self.assertEqual(to_sign, 'AWS4-HMAC-SHA256\n' @@ -216,7 +216,7 @@ def test_get_request_params_urlquotes_params_values_allows_safe_chars_in_value(s def test_get_payload_hash_returns_digest_of_empty_string_for_GET_requests(self): SignedAWSConnection.method = 'GET' - self.assertEqual(self.signer._get_payload_hash(), + self.assertEqual(self.signer._get_payload_hash(method='GET'), 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') def test_get_canonical_request(self): @@ -224,7 +224,8 @@ def test_get_canonical_request(self): {'Action': 'DescribeInstances', 'Version': '2013-10-15'}, {'Accept-Encoding': 'gzip,deflate', 'User-Agent': 'My-UA'}, method='GET', - path='/my_action/' + path='/my_action/', + data=None ) self.assertEqual(req, 'GET\n' '/my_action/\n' @@ -235,5 +236,22 @@ def test_get_canonical_request(self): 'accept-encoding;user-agent\n' 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + def test_post_canonical_request(self): + req = self.signer._get_canonical_request( + {'Action': 'DescribeInstances', 'Version': '2013-10-15'}, + {'Accept-Encoding': 'gzip,deflate', 'User-Agent': 'My-UA'}, + method='POST', + path='/my_action/', + data='{}' + ) + self.assertEqual(req, 'POST\n' + '/my_action/\n' + 'Action=DescribeInstances&Version=2013-10-15\n' + 'accept-encoding:gzip,deflate\n' + 'user-agent:My-UA\n' + '\n' + 'accept-encoding;user-agent\n' + '44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a') + if __name__ == '__main__': sys.exit(unittest.main()) diff --git a/libcloud/test/container/__init__.py b/libcloud/test/container/__init__.py new file mode 100644 index 0000000000..df27b768b6 --- /dev/null +++ b/libcloud/test/container/__init__.py @@ -0,0 +1,30 @@ +# 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. + +from libcloud.container.base import ContainerImage + + +class TestCaseMixin(object): + + def test_list_images_response(self): + images = self.driver.list_images() + self.assertTrue(isinstance(images, list)) + for image in images: + self.assertTrue(isinstance(image, ContainerImage)) + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/libcloud/test/container/fixtures/docker/container_a68.json b/libcloud/test/container/fixtures/docker/container_a68.json new file mode 100644 index 0000000000..88282ec1ce --- /dev/null +++ b/libcloud/test/container/fixtures/docker/container_a68.json @@ -0,0 +1,163 @@ +{ + "Id": "a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303", + "Created": "2015-12-23T01:05:40.56937184Z", + "Path": "/entrypoint.sh", + "Args": [ + "None" + ], + "State": { + "Status": "exited", + "Running": false, + "Paused": false, + "Restarting": false, + "OOMKilled": false, + "Dead": false, + "Pid": 0, + "ExitCode": 127, + "Error": "", + "StartedAt": "2015-12-23T01:06:29.018395755Z", + "FinishedAt": "2015-12-23T01:06:30.144487212Z" + }, + "Image": "cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0", + "ResolvConfPath": "/var/lib/docker/containers/a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303/resolv.conf", + "HostnamePath": "/var/lib/docker/containers/a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303/hostname", + "HostsPath": "/var/lib/docker/containers/a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303/hosts", + "LogPath": "/var/lib/docker/containers/a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303/a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303-json.log", + "Name": "/gigantic_goldberg", + "RestartCount": 0, + "Driver": "aufs", + "ExecDriver": "native-0.2", + "MountLabel": "", + "ProcessLabel": "", + "AppArmorProfile": "", + "ExecIDs": null, + "HostConfig": { + "Binds": null, + "ContainerIDFile": "", + "LxcConf": null, + "Memory": 0, + "MemoryReservation": 0, + "MemorySwap": 0, + "KernelMemory": 0, + "CpuShares": 0, + "CpuPeriod": 0, + "CpusetCpus": "", + "CpusetMems": "", + "CpuQuota": 0, + "BlkioWeight": 0, + "OomKillDisable": false, + "MemorySwappiness": null, + "Privileged": false, + "PortBindings": {}, + "Links": null, + "PublishAllPorts": true, + "Dns": null, + "DnsOptions": null, + "DnsSearch": null, + "ExtraHosts": null, + "VolumesFrom": null, + "Devices": null, + "NetworkMode": "default", + "IpcMode": "", + "PidMode": "", + "UTSMode": "", + "CapAdd": null, + "CapDrop": null, + "GroupAdd": null, + "RestartPolicy": { + "Name": "", + "MaximumRetryCount": 0 + }, + "SecurityOpt": null, + "ReadonlyRootfs": false, + "Ulimits": null, + "LogConfig": { + "Type": "json-file", + "Config": {} + }, + "CgroupParent": "", + "ConsoleSize": [ + 0, + 0 + ], + "VolumeDriver": "" + }, + "GraphDriver": { + "Name": "aufs", + "Data": null + }, + "Mounts": [ + { + "Name": "b1a70d8e1ebd7d5865e59ff91cf06357e3ef4d829af44c31675c2d0a24894444", + "Source": "/var/lib/docker/volumes/b1a70d8e1ebd7d5865e59ff91cf06357e3ef4d829af44c31675c2d0a24894444/_data", + "Destination": "/data/db", + "Driver": "local", + "Mode": "", + "RW": true + } + ], + "Config": { + "Hostname": "a68c1872c746", + "Domainname": "", + "User": "", + "AttachStdin": true, + "AttachStdout": true, + "AttachStderr": true, + "ExposedPorts": { + "27017/tcp": {} + }, + "Tty": true, + "OpenStdin": true, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "GPG_KEYS=DFFA3DCF326E302C4787673A01C4E7FAAAB2461C \t42F3E95A2C4F08279C4960ADD68FA50FEA312927", + "MONGO_MAJOR=3.2", + "MONGO_VERSION=3.2.0" + ], + "Cmd": [ + "None" + ], + "Image": "mongo:latest", + "Volumes": { + "/data/db": {} + }, + "WorkingDir": "", + "Entrypoint": [ + "/entrypoint.sh" + ], + "OnBuild": null, + "Labels": {} + }, + "NetworkSettings": { + "Bridge": "", + "SandboxID": "", + "HairpinMode": false, + "LinkLocalIPv6Address": "", + "LinkLocalIPv6PrefixLen": 0, + "Ports": null, + "SandboxKey": "", + "SecondaryIPAddresses": null, + "SecondaryIPv6Addresses": null, + "EndpointID": "", + "Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "MacAddress": "", + "Networks": { + "bridge": { + "EndpointID": "", + "Gateway": "", + "IPAddress": "", + "IPPrefixLen": 0, + "IPv6Gateway": "", + "GlobalIPv6Address": "", + "GlobalIPv6PrefixLen": 0, + "MacAddress": "" + } + } + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker/containers.json b/libcloud/test/container/fixtures/docker/containers.json new file mode 100644 index 0000000000..3228e3f10c --- /dev/null +++ b/libcloud/test/container/fixtures/docker/containers.json @@ -0,0 +1,143 @@ +[ + { + "Id": "160936dc54fe8c332095676d9379003534b8cddd7565fa63018996e06dae1b6b", + "Names": [ + "/hubot" + ], + "Image": "stackstorm/hubot", + "ImageID": "05c5761707b3970a9bf17c00886176add79ac087b4d6a500ac87985bf8ec07b1", + "Command": "/app/bin/hubot", + "Created": 1450130345, + "Ports": [], + "Labels": {}, + "Status": "Exited (137) 11 minutes ago", + "HostConfig": { + "NetworkMode": "bridge" + } + }, + { + "Id": "f159072147ee7d253e21ec8fd2778a27ac29d7fc5f865641900d16665b46215a", + "Names": [ + "/mongo" + ], + "Image": "mongo", + "ImageID": "cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0", + "Command": "/entrypoint.sh mongod", + "Created": 1450130332, + "Ports": [], + "Labels": {}, + "Status": "Exited (14) 2 hours ago", + "HostConfig": { + "NetworkMode": "bridge" + } + }, + { + "Id": "e687b33f9ced0153104308e6ff7a2138b8cc026fa4085d31da831a02ed0dc03d", + "Names": [ + "/rabbitmq" + ], + "Image": "rabbitmq", + "ImageID": "448afeda0388b18c6f3be18c7aaece29e0f8dbdfab30364e678c382bab1037c5", + "Command": "/docker-entrypoint.sh rabbitmq-server", + "Created": 1450130331, + "Ports": [ + { + "IP": "0.0.0.0", + "PrivatePort": 5672, + "PublicPort": 5672, + "Type": "tcp" + }, + { + "PrivatePort": 25672, + "Type": "tcp" + }, + { + "PrivatePort": 4369, + "Type": "tcp" + }, + { + "PrivatePort": 5671, + "Type": "tcp" + } + ], + "Labels": {}, + "Status": "Exited (137) 11 minutes ago", + "HostConfig": { + "NetworkMode": "bridge" + } + }, + { + "Id": "b82c16423c6dbb7cd1564f8fc413c822df45cc0c7aa35c24683a1329af6ec102", + "Names": [ + "/fervent_bhabha" + ], + "Image": "rabbitmq", + "ImageID": "448afeda0388b18c6f3be18c7aaece29e0f8dbdfab30364e678c382bab1037c5", + "Command": "/docker-entrypoint.sh rabbitmq-server", + "Created": 1450059506, + "Ports": [ + { + "PrivatePort": 4369, + "Type": "tcp" + }, + { + "PrivatePort": 5671, + "Type": "tcp" + }, + { + "IP": "0.0.0.0", + "PrivatePort": 5672, + "PublicPort": 5672, + "Type": "tcp" + }, + { + "PrivatePort": 25672, + "Type": "tcp" + } + ], + "Labels": {}, + "Status": "Dead", + "HostConfig": { + "NetworkMode": "bridge" + } + }, + { + "Id": "8cc5481aa4621578f8dd2c942d74e27e75170c6899ea012db7a44ea5f1ba2069", + "Names": [ + "/suspicious_swirles" + ], + "Image": "mongo", + "ImageID": "cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0", + "Command": "/entrypoint.sh mongod", + "Created": 1450059505, + "Ports": [ + { + "IP": "0.0.0.0", + "PrivatePort": 27017, + "PublicPort": 27017, + "Type": "tcp" + } + ], + "Labels": {}, + "Status": "Dead", + "HostConfig": { + "NetworkMode": "bridge" + } + }, + { + "Id": "598b3e4d15a406390baaa2947f910e7b52b810a4120028692ed309247f2e8346", + "Names": [ + "/mongodata" + ], + "Image": "mongo", + "ImageID": "cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0", + "Command": "/entrypoint.sh /bin/true", + "Created": 1449637213, + "Ports": [], + "Labels": {}, + "Status": "Created", + "HostConfig": { + "NetworkMode": "default" + } + } +] \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker/create_container.json b/libcloud/test/container/fixtures/docker/create_container.json new file mode 100644 index 0000000000..e05a941720 --- /dev/null +++ b/libcloud/test/container/fixtures/docker/create_container.json @@ -0,0 +1,4 @@ +{ + "Id": "a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303", + "Warnings": null +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker/create_image.json b/libcloud/test/container/fixtures/docker/create_image.json new file mode 100644 index 0000000000..4509f559ad --- /dev/null +++ b/libcloud/test/container/fixtures/docker/create_image.json @@ -0,0 +1 @@ +{"status":"Download complete","progressDetail":{},"id":"cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0"} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker/images.json b/libcloud/test/container/fixtures/docker/images.json new file mode 100644 index 0000000000..ac04cf38cb --- /dev/null +++ b/libcloud/test/container/fixtures/docker/images.json @@ -0,0 +1,50 @@ +[ + { + "Id": "cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0", + "ParentId": "3e408cde1b7f6276b9ead7b8111d80a367f9223dfbbd4102ea89a5fc42947960", + "RepoTags": [ + "mongo:latest" + ], + "RepoDigests": [], + "Created": 1449618009, + "Size": 0, + "VirtualSize": 316957672, + "Labels": null + }, + { + "Id": "05c5761707b3970a9bf17c00886176add79ac087b4d6a500ac87985bf8ec07b1", + "ParentId": "be7965ce1bef5d2e3b27efb3f4fe2253683bc7144d2ebae614e9e7155066c833", + "RepoTags": [ + "stackstorm/hubot:latest" + ], + "RepoDigests": [], + "Created": 1449466772, + "Size": 0, + "VirtualSize": 550102318, + "Labels": {} + }, + { + "Id": "448afeda0388b18c6f3be18c7aaece29e0f8dbdfab30364e678c382bab1037c5", + "ParentId": "67edbf589f9af9b2c6f87e8481ec0299c50bfce5f9b98b95316c7235494c7bea", + "RepoTags": [ + "rabbitmq:latest" + ], + "RepoDigests": [], + "Created": 1449312753, + "Size": 0, + "VirtualSize": 304310861, + "Labels": null + }, + { + "Id": "9da5438fedb2e9a1e11a3361c4a53e0801ed1f8f4c014d83a5a514f0c60892bf", + "ParentId": "64ccc5e9d20c638849eadddab4f23204c3fcdd62d497cdbd0ecf44d863b086c8", + "RepoTags": [ + "mongo:2.4.14" + ], + "RepoDigests": [], + "Created": 1449299455, + "Size": 0, + "VirtualSize": 344445131, + "Labels": null + } +] \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker/logs.txt b/libcloud/test/container/fixtures/docker/logs.txt new file mode 100644 index 0000000000..12c3a44e48 --- /dev/null +++ b/libcloud/test/container/fixtures/docker/logs.txt @@ -0,0 +1 @@ +/entrypoint.sh: line 19: exec: None: not found diff --git a/libcloud/test/container/fixtures/docker/search.json b/libcloud/test/container/fixtures/docker/search.json new file mode 100644 index 0000000000..dd9454c9b7 --- /dev/null +++ b/libcloud/test/container/fixtures/docker/search.json @@ -0,0 +1,202 @@ +[ + { + "star_count": 1502, + "is_official": true, + "name": "mysql", + "is_trusted": false, + "is_automated": false, + "description": "MySQL is a widely used, open-source relational database management system (RDBMS)." + }, + { + "star_count": 80, + "is_official": false, + "name": "mysql/mysql-server", + "is_trusted": true, + "is_automated": true, + "description": "Optimized MySQL Server Docker images. Created, maintained and supported by the MySQL team at Oracle" + }, + { + "star_count": 31, + "is_official": false, + "name": "centurylink/mysql", + "is_trusted": true, + "is_automated": true, + "description": "Image containing mysql. Optimized to be linked to another image/container." + }, + { + "star_count": 6, + "is_official": false, + "name": "appcontainers/mysql", + "is_trusted": true, + "is_automated": true, + "description": "CentOS/Ubuntu/Debian based customizible MySQL 5.5 Container - 284MB/283MB/245MB - Updated 12/14/2015" + }, + { + "star_count": 2, + "is_official": false, + "name": "alterway/mysql", + "is_trusted": true, + "is_automated": true, + "description": "Docker Mysql" + }, + { + "star_count": 0, + "is_official": false, + "name": "tozd/mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL (MariaDB fork) Docker image." + }, + { + "star_count": 0, + "is_official": false, + "name": "wenzizone/mysql", + "is_trusted": true, + "is_automated": true, + "description": "mysql" + }, + { + "star_count": 0, + "is_official": false, + "name": "dockerizedrupal/mysql", + "is_trusted": true, + "is_automated": true, + "description": "docker-mysql" + }, + { + "star_count": 2, + "is_official": false, + "name": "azukiapp/mysql", + "is_trusted": true, + "is_automated": true, + "description": "Docker image to run MySQL by Azuki - http://azk.io" + }, + { + "star_count": 1, + "is_official": false, + "name": "phpmentors/mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL server image" + }, + { + "star_count": 0, + "is_official": false, + "name": "lancehudson/docker-mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL is a widely used, open-source relational database management system (RDBMS)." + }, + { + "star_count": 1, + "is_official": false, + "name": "bahmni/mysql", + "is_trusted": true, + "is_automated": true, + "description": "Mysql container for bahmni. Contains the openmrs database" + }, + { + "star_count": 2, + "is_official": false, + "name": "yfix/mysql", + "is_trusted": true, + "is_automated": true, + "description": "Yfix docker built mysql" + }, + { + "star_count": 23, + "is_official": false, + "name": "sameersbn/mysql", + "is_trusted": true, + "is_automated": true, + "description": "" + }, + { + "star_count": 0, + "is_official": false, + "name": "nanobox/mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL service for nanobox.io" + }, + { + "star_count": 0, + "is_official": false, + "name": "withinboredom/mysql", + "is_trusted": true, + "is_automated": true, + "description": "A MySQL container using s6 and Consul -- built on tatum/mysql" + }, + { + "star_count": 4, + "is_official": false, + "name": "marvambass/mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL Server based on Ubuntu 14.04" + }, + { + "star_count": 14, + "is_official": false, + "name": "google/mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL server for Google Compute Engine" + }, + { + "star_count": 1, + "is_official": false, + "name": "frodenas/mysql", + "is_trusted": true, + "is_automated": true, + "description": "A Docker Image for MySQL" + }, + { + "star_count": 0, + "is_official": false, + "name": "ahmet2mir/mysql", + "is_trusted": true, + "is_automated": true, + "description": "This is a Debian based image with MySQL server installed listening on port 3306. " + }, + { + "star_count": 25, + "is_official": false, + "name": "wnameless/mysql-phpmyadmin", + "is_trusted": true, + "is_automated": true, + "description": "MySQL + phpMyAdmin\nhttps://index.docker.io/u/wnameless/mysql-phpmyadmin/" + }, + { + "star_count": 0, + "is_official": false, + "name": "drupaldocker/mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL for Drupal" + }, + { + "star_count": 0, + "is_official": false, + "name": "tetraweb/mysql", + "is_trusted": true, + "is_automated": true, + "description": "" + }, + { + "star_count": 1, + "is_official": false, + "name": "boomtownroi/mysql-dev", + "is_trusted": true, + "is_automated": true, + "description": "A mysql box with consul integration for development. Based on tatum box" + }, + { + "star_count": 5, + "is_official": false, + "name": "ioggstream/mysql", + "is_trusted": true, + "is_automated": true, + "description": "MySQL Image with Master-Slave replication" + } +] \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker/version.json b/libcloud/test/container/fixtures/docker/version.json new file mode 100644 index 0000000000..3b51d72d7b --- /dev/null +++ b/libcloud/test/container/fixtures/docker/version.json @@ -0,0 +1,10 @@ +{ + "Version": "1.9.1", + "ApiVersion": "1.21", + "GitCommit": "a34a1d5", + "GoVersion": "go1.4.3", + "Os": "linux", + "Arch": "amd64", + "KernelVersion": "3.13.0-46-generic", + "BuildTime": "Fri Nov 20 17:56:04 UTC 2015" +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu.json b/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu.json new file mode 100644 index 0000000000..d60b25d4d1 --- /dev/null +++ b/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu.json @@ -0,0 +1,15 @@ +{ + "user": "library", + "name": "ubuntu", + "namespace": "library", + "status": 1, + "description": "Ubuntu is a Debian-based Linux operating system based on free software.", + "is_private": false, + "is_automated": false, + "can_edit": false, + "star_count": 2954, + "pull_count": 37627788, + "last_updated": "2016-01-04T19:00:53.547174Z", + "has_starred": false, + "full_description": "# Supported tags and respective `Dockerfile` links\n\n-\t[`12.04.5`, `12.04`, `precise-20151208`, `precise` (*precise/Dockerfile*)](https://github.com/tianon/docker-brew-ubuntu-core/blob/d7f2045ad9b08962d9728f6d9910fa252282b85f/precise/Dockerfile)\n-\t[`14.04.3`, `14.04`, `trusty-20151218`, `trusty`, `latest` (*trusty/Dockerfile*)](https://github.com/tianon/docker-brew-ubuntu-core/blob/d7f2045ad9b08962d9728f6d9910fa252282b85f/trusty/Dockerfile)\n-\t[`15.04`, `vivid-20151208`, `vivid` (*vivid/Dockerfile*)](https://github.com/tianon/docker-brew-ubuntu-core/blob/d7f2045ad9b08962d9728f6d9910fa252282b85f/vivid/Dockerfile)\n-\t[`15.10`, `wily-20151208`, `wily` (*wily/Dockerfile*)](https://github.com/tianon/docker-brew-ubuntu-core/blob/d7f2045ad9b08962d9728f6d9910fa252282b85f/wily/Dockerfile)\n-\t[`16.04`, `xenial-20151218.1`, `xenial` (*xenial/Dockerfile*)](https://github.com/tianon/docker-brew-ubuntu-core/blob/d7f2045ad9b08962d9728f6d9910fa252282b85f/xenial/Dockerfile)\n\nFor more information about this image and its history, please see [the relevant manifest file (`library/ubuntu`)](https://github.com/docker-library/official-images/blob/master/library/ubuntu). This image is updated via pull requests to [the `docker-library/official-images` GitHub repo](https://github.com/docker-library/official-images).\n\nFor detailed information about the virtual/transfer sizes and individual layers of each of the above supported tags, please see [the `ubuntu/tag-details.md` file](https://github.com/docker-library/docs/blob/master/ubuntu/tag-details.md) in [the `docker-library/docs` GitHub repo](https://github.com/docker-library/docs).\n\n# What is Ubuntu?\n\nUbuntu is a Debian-based Linux operating system, with Unity as its default desktop environment. It is based on free software and named after the Southern African philosophy of ubuntu (literally, \"human-ness\"), which often is translated as \"humanity towards others\" or \"the belief in a universal bond of sharing that connects all humanity\".\n\nDevelopment of Ubuntu is led by UK-based Canonical Ltd., a company owned by South African entrepreneur Mark Shuttleworth. Canonical generates revenue through the sale of technical support and other services related to Ubuntu. The Ubuntu project is publicly committed to the principles of open-source software development; people are encouraged to use free software, study how it works, improve upon it, and distribute it.\n\n> [wikipedia.org/wiki/Ubuntu_(operating_system)](https://en.wikipedia.org/wiki/Ubuntu_%28operating_system%29)\n\n![logo](https://raw.githubusercontent.com/docker-library/docs/01c12653951b2fe592c1f93a13b4e289ada0e3a1/ubuntu/logo.png)\n\n# What's in this image?\n\n## `/etc/apt/sources.list`\n\n### `ubuntu:14.04`\n\n```console\n$ docker run ubuntu:14.04 grep -v '^#' /etc/apt/sources.list\n\ndeb http://archive.ubuntu.com/ubuntu/ trusty main restricted\ndeb-src http://archive.ubuntu.com/ubuntu/ trusty main restricted\n\ndeb http://archive.ubuntu.com/ubuntu/ trusty-updates main restricted\ndeb-src http://archive.ubuntu.com/ubuntu/ trusty-updates main restricted\n\ndeb http://archive.ubuntu.com/ubuntu/ trusty universe\ndeb-src http://archive.ubuntu.com/ubuntu/ trusty universe\ndeb http://archive.ubuntu.com/ubuntu/ trusty-updates universe\ndeb-src http://archive.ubuntu.com/ubuntu/ trusty-updates universe\n\n\ndeb http://archive.ubuntu.com/ubuntu/ trusty-security main restricted\ndeb-src http://archive.ubuntu.com/ubuntu/ trusty-security main restricted\ndeb http://archive.ubuntu.com/ubuntu/ trusty-security universe\ndeb-src http://archive.ubuntu.com/ubuntu/ trusty-security universe\n```\n\n### `ubuntu:12.04`\n\n```console\n$ docker run ubuntu:12.04 cat /etc/apt/sources.list\n\ndeb http://archive.ubuntu.com/ubuntu/ precise main restricted\ndeb-src http://archive.ubuntu.com/ubuntu/ precise main restricted\n\ndeb http://archive.ubuntu.com/ubuntu/ precise-updates main restricted\ndeb-src http://archive.ubuntu.com/ubuntu/ precise-updates main restricted\n\ndeb http://archive.ubuntu.com/ubuntu/ precise universe\ndeb-src http://archive.ubuntu.com/ubuntu/ precise universe\ndeb http://archive.ubuntu.com/ubuntu/ precise-updates universe\ndeb-src http://archive.ubuntu.com/ubuntu/ precise-updates universe\n\n\ndeb http://archive.ubuntu.com/ubuntu/ precise-security main restricted\ndeb-src http://archive.ubuntu.com/ubuntu/ precise-security main restricted\ndeb http://archive.ubuntu.com/ubuntu/ precise-security universe\ndeb-src http://archive.ubuntu.com/ubuntu/ precise-security universe\n```\n\n# Supported Docker versions\n\nThis image is officially supported on Docker version 1.9.1.\n\nSupport for older versions (down to 1.6) is provided on a best-effort basis.\n\nPlease see [the Docker installation documentation](https://docs.docker.com/installation/) for details on how to upgrade your Docker daemon.\n\n# User Feedback\n\n## Documentation\n\nDocumentation for this image is stored in the [`ubuntu/` directory](https://github.com/docker-library/docs/tree/master/ubuntu) of the [`docker-library/docs` GitHub repo](https://github.com/docker-library/docs). Be sure to familiarize yourself with the [repository's `README.md` file](https://github.com/docker-library/docs/blob/master/README.md) before attempting a pull request.\n\n## Issues\n\nIf you have any problems with or questions about this image, please contact us through a [GitHub issue](https://github.com/tianon/docker-brew-ubuntu-core/issues).\n\nYou can also reach many of the official image maintainers via the `#docker-library` IRC channel on [Freenode](https://freenode.net).\n\n## Contributing\n\nYou are invited to contribute new features, fixes, or updates, large or small; we are always thrilled to receive pull requests, and do our best to process them as fast as we can.\n\nBefore you start to code, we recommend discussing your plans through a [GitHub issue](https://github.com/tianon/docker-brew-ubuntu-core/issues), especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give you feedback on your design, and help you find out if someone else is working on the same thing." +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu_tags.json b/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu_tags.json new file mode 100644 index 0000000000..f3914c939a --- /dev/null +++ b/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu_tags.json @@ -0,0 +1,975 @@ +{ + "count": 88, + "next": null, + "previous": null, + "results": [ + { + "name": "xenial", + "full_size": 47439662, + "id": 1589976, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:51.344198Z", + "image_id": null, + "v2": true + }, + { + "name": "xenial-20151218.1", + "full_size": 47439662, + "id": 1589974, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:45.326998Z", + "image_id": null, + "v2": true + }, + { + "name": "16.04", + "full_size": 47439662, + "id": 1589970, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:39.215205Z", + "image_id": null, + "v2": true + }, + { + "name": "wily", + "full_size": 50294202, + "id": 2332, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:30.141301Z", + "image_id": null, + "v2": true + }, + { + "name": "wily-20151208", + "full_size": 50294202, + "id": 1509180, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:24.324323Z", + "image_id": null, + "v2": true + }, + { + "name": "15.10", + "full_size": 50294202, + "id": 2327, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:18.625895Z", + "image_id": null, + "v2": true + }, + { + "name": "vivid", + "full_size": 49334628, + "id": 2329, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:12.764756Z", + "image_id": null, + "v2": true + }, + { + "name": "vivid-20151208", + "full_size": 49334628, + "id": 1509158, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:07.282476Z", + "image_id": null, + "v2": true + }, + { + "name": "15.04", + "full_size": 49334628, + "id": 2298, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T19:00:01.659187Z", + "image_id": null, + "v2": true + }, + { + "name": "latest", + "full_size": 65747044, + "id": 2343, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T18:59:54.779484Z", + "image_id": null, + "v2": true + }, + { + "name": "trusty", + "full_size": 65747044, + "id": 2305, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T18:59:49.102906Z", + "image_id": null, + "v2": true + }, + { + "name": "trusty-20151218", + "full_size": 65747044, + "id": 1657173, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T18:59:43.103725Z", + "image_id": null, + "v2": true + }, + { + "name": "14.04", + "full_size": 65747044, + "id": 2324, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T18:59:37.496154Z", + "image_id": null, + "v2": true + }, + { + "name": "14.04.3", + "full_size": 65747044, + "id": 693829, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T18:58:46.261151Z", + "image_id": null, + "v2": true + }, + { + "name": "precise", + "full_size": 44194573, + "id": 2292, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T18:57:33.494399Z", + "image_id": null, + "v2": true + }, + { + "name": "precise-20151208", + "full_size": 44194573, + "id": 1509115, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2016-01-04T18:57:22.912930Z", + "image_id": null, + "v2": true + }, + { + "name": "12.04", + "full_size": 44194573, + "id": 2310, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T18:57:14.495660Z", + "image_id": null, + "v2": true + }, + { + "name": "12.04.5", + "full_size": 44194573, + "id": 2295, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T18:57:08.237857Z", + "image_id": null, + "v2": true + }, + { + "name": "trusty-20151208", + "full_size": 65742980, + "id": 1509143, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-12-18T18:26:56.770757Z", + "image_id": null, + "v2": true + }, + { + "name": "wily-20151019", + "full_size": 49817335, + "id": 1168566, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-12-08T09:04:03.336941Z", + "image_id": null, + "v2": true + }, + { + "name": "vivid-20151111", + "full_size": 49333876, + "id": 1389462, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-12-08T09:02:56.243906Z", + "image_id": null, + "v2": true + }, + { + "name": "trusty-20151028", + "full_size": 65742789, + "id": 1313714, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-12-08T09:02:32.340602Z", + "image_id": null, + "v2": true + }, + { + "name": "precise-20151028", + "full_size": 44096878, + "id": 1313721, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-12-08T09:02:07.834932Z", + "image_id": null, + "v2": true + }, + { + "name": "vivid-20151106", + "full_size": 49329280, + "id": 1313735, + "repository": 130, + "creator": 2215, + "last_updater": 213249, + "last_updated": "2015-11-21T01:14:52.126272Z", + "image_id": null, + "v2": true + }, + { + "name": "12.10", + "full_size": 58078433, + "id": 2339, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:42:40.878495Z", + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150218.1", + "full_size": 65832655, + "id": 2325, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:42:29.777917Z", + "image_id": null, + "v2": true + }, + { + "name": "utopic-20150211", + "full_size": 68374766, + "id": 2307, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:41:34.805100Z", + "image_id": null, + "v2": true + }, + { + "name": "quantal", + "full_size": 58078433, + "id": 2341, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:40:29.962553Z", + "image_id": null, + "v2": true + }, + { + "name": "14.04.1", + "full_size": 65827193, + "id": 2322, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:39:42.479726Z", + "image_id": null, + "v2": true + }, + { + "name": "raring", + "full_size": 57667348, + "id": 2316, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:38:47.745980Z", + "image_id": null, + "v2": true + }, + { + "name": "vivid-20150218", + "full_size": 44122689, + "id": 2320, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:38:37.964774Z", + "image_id": null, + "v2": true + }, + { + "name": "vivid-20150309", + "full_size": 49448856, + "id": 2317, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:37:59.209284Z", + "image_id": null, + "v2": true + }, + { + "name": "10.04", + "full_size": 63533781, + "id": 2299, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:37:16.010344Z", + "image_id": null, + "v2": true + }, + { + "name": "saucy", + "full_size": 60522580, + "id": 2301, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:37:04.113916Z", + "image_id": null, + "v2": true + }, + { + "name": "precise-20150228.11", + "full_size": 43657047, + "id": 2331, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:36:51.552688Z", + "image_id": null, + "v2": true + }, + { + "name": "utopic-20150228.11", + "full_size": 68379536, + "id": 2318, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:36:02.986916Z", + "image_id": null, + "v2": true + }, + { + "name": "13.10", + "full_size": 60522580, + "id": 2304, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:34:52.333445Z", + "image_id": null, + "v2": true + }, + { + "name": "13.04", + "full_size": 57667348, + "id": 2294, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:33:49.391093Z", + "image_id": null, + "v2": true + }, + { + "name": "precise-20150212", + "full_size": 43616335, + "id": 2340, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:32:59.135708Z", + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150228.11", + "full_size": 65828716, + "id": 2323, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:32:14.798440Z", + "image_id": null, + "v2": true + }, + { + "name": "lucid", + "full_size": 63533781, + "id": 2321, + "repository": 130, + "creator": 7, + "last_updater": 134455, + "last_updated": "2015-11-14T14:31:11.758283Z", + "image_id": null, + "v2": true + }, + { + "name": "vivid-20151021", + "full_size": 49328003, + "id": 1168551, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-10-28T12:21:31.210637Z", + "image_id": null, + "v2": true + }, + { + "name": "trusty-20151021", + "full_size": 65741561, + "id": 1168539, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-10-28T12:21:07.780326Z", + "image_id": null, + "v2": true + }, + { + "name": "precise-20151020", + "full_size": 44096883, + "id": 1168524, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": "2015-10-28T12:20:39.816211Z", + "image_id": null, + "v2": true + }, + { + "name": "wily-20151009", + "full_size": 49844614, + "id": 1096696, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20151009", + "full_size": 65861875, + "id": 1096682, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "wily-20151006", + "full_size": 49861095, + "id": 1081815, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "vivid-20150930", + "full_size": 49345386, + "id": 1081804, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20151001", + "full_size": 65757468, + "id": 1081789, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "precise-20150924", + "full_size": 44037965, + "id": 1081772, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "wily-20150829", + "full_size": 49614664, + "id": 828778, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "wily-20150818", + "full_size": 50298307, + "id": 778618, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "precise-20150813", + "full_size": 43977816, + "id": 776393, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "vivid-20150813", + "full_size": 49343696, + "id": 776144, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150814", + "full_size": 65859249, + "id": 775535, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "wily-20150807", + "full_size": 50528668, + "id": 693839, + "repository": 130, + "creator": 2215, + "last_updater": 213249, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150806", + "full_size": 65857914, + "id": 693834, + "repository": 130, + "creator": 2215, + "last_updater": 213249, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "wily-20150731", + "full_size": 50488452, + "id": 674054, + "repository": 130, + "creator": 2215, + "last_updater": 213249, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "vivid-20150802", + "full_size": 49340063, + "id": 674043, + "repository": 130, + "creator": 2215, + "last_updater": 213249, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150730", + "full_size": 65860360, + "id": 674034, + "repository": 130, + "creator": 2215, + "last_updater": 213249, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "precise-20150729", + "full_size": 43967445, + "id": 674016, + "repository": 130, + "creator": 2215, + "last_updater": 213249, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "wily-20150708", + "full_size": 50494409, + "id": 541269, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "utopic-20150625", + "full_size": 68399747, + "id": 541258, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150630", + "full_size": 65858138, + "id": 541253, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "precise-20150626", + "full_size": 43878461, + "id": 541246, + "repository": 130, + "creator": 2215, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "vivid-20150528", + "full_size": 131333439, + "id": 2338, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "utopic-20150528", + "full_size": 194454267, + "id": 2337, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "utopic", + "full_size": 68399747, + "id": 2336, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "precise-20150528", + "full_size": 133416464, + "id": 2335, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "utopic-20150319", + "full_size": 194424279, + "id": 2334, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "utopic-20150418", + "full_size": 194463410, + "id": 2333, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "precise-20150427", + "full_size": 132465012, + "id": 2330, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "trusty-20150612", + "full_size": 188284994, + "id": 2328, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "wily-20150528.1", + "full_size": 132392276, + "id": 2326, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "vivid-20150319.1", + "full_size": 131685773, + "id": 2315, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "14.04.2", + "full_size": 65860360, + "id": 2314, + "repository": 130, + "creator": 7, + "last_updater": 213249, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150528", + "full_size": 188281989, + "id": 2313, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "precise-20150612", + "full_size": 133706040, + "id": 2312, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "trusty-20150320", + "full_size": 188300556, + "id": 2311, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "vivid-20150611", + "full_size": 49338475, + "id": 2309, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "trusty-20150427", + "full_size": 188278440, + "id": 2308, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "wily-20150611", + "full_size": 133648792, + "id": 2306, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "utopic-20150612", + "full_size": 194462706, + "id": 2303, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "utopic-20150427", + "full_size": 194461653, + "id": 2302, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "precise-20150320", + "full_size": 131886863, + "id": 2300, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "14.10", + "full_size": 68399747, + "id": 2297, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": null, + "image_id": null, + "v2": true + }, + { + "name": "vivid-20150427", + "full_size": 131302888, + "id": 2296, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + }, + { + "name": "vivid-20150421", + "full_size": 131279915, + "id": 2293, + "repository": 130, + "creator": 7, + "last_updater": 7, + "last_updated": null, + "image_id": null, + "v2": false + } + ] +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu_tags_latest.json b/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu_tags_latest.json new file mode 100644 index 0000000000..c5ce942bcc --- /dev/null +++ b/libcloud/test/container/fixtures/docker_utils/v2_repositories_library_ubuntu_tags_latest.json @@ -0,0 +1,11 @@ +{ + "name": "latest", + "full_size": 65747044, + "id": 2343, + "repository": 130, + "creator": 7, + "last_updater": 2215, + "last_updated": "2016-01-04T18:59:54.779484Z", + "image_id": null, + "v2": true +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/createcluster.json b/libcloud/test/container/fixtures/ecs/createcluster.json new file mode 100644 index 0000000000..eb6ced3baa --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/createcluster.json @@ -0,0 +1,11 @@ +{ + "cluster": { + "activeServicesCount": 0, + "clusterArn": "arn:aws:ecs:ap-southeast-2:647433528374:cluster/my-cluster", + "clusterName": "my-cluster", + "pendingTasksCount": 0, + "registeredContainerInstancesCount": 0, + "runningTasksCount": 0, + "status": "ACTIVE" + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/createservice.json b/libcloud/test/container/fixtures/ecs/createservice.json new file mode 100644 index 0000000000..2f9d421e84 --- /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 diff --git a/libcloud/test/container/fixtures/ecs/deletecluster.json b/libcloud/test/container/fixtures/ecs/deletecluster.json new file mode 100644 index 0000000000..9c8c0cdb32 --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/deletecluster.json @@ -0,0 +1,11 @@ +{ + "cluster": { + "activeServicesCount": 0, + "clusterArn": "arn:aws:ecs:ap-southeast-2:647433528374:cluster/my-cluster", + "clusterName": "my-cluster", + "pendingTasksCount": 0, + "registeredContainerInstancesCount": 0, + "runningTasksCount": 0, + "status": "INACTIVE" + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/deleteservice.json b/libcloud/test/container/fixtures/ecs/deleteservice.json new file mode 100644 index 0000000000..d89d3233b0 --- /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 diff --git a/libcloud/test/container/fixtures/ecs/describeclusters.json b/libcloud/test/container/fixtures/ecs/describeclusters.json new file mode 100644 index 0000000000..1e894261a7 --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/describeclusters.json @@ -0,0 +1,14 @@ +{ + "clusters": [ + { + "activeServicesCount": 1, + "clusterArn": "arn:aws:ecs:us-east-1:012345678910:cluster/default", + "clusterName": "default", + "pendingTasksCount": 0, + "registeredContainerInstancesCount": 0, + "runningTasksCount": 0, + "status": "ACTIVE" + } + ], + "failures": [] +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/describerepositories.json b/libcloud/test/container/fixtures/ecs/describerepositories.json new file mode 100644 index 0000000000..c79400552a --- /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 diff --git a/libcloud/test/container/fixtures/ecs/describeservices.json b/libcloud/test/container/fixtures/ecs/describeservices.json new file mode 100644 index 0000000000..76acc75af6 --- /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 diff --git a/libcloud/test/container/fixtures/ecs/describetasks.json b/libcloud/test/container/fixtures/ecs/describetasks.json new file mode 100644 index 0000000000..1dd5f1bba0 --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/describetasks.json @@ -0,0 +1,35 @@ +{ + "failures": [], + "tasks": [{ + "clusterArn": "arn:aws:ecs:ap-southeast-2:647433528374:cluster/default", + "containerInstanceArn": "arn:aws:ecs:ap-southeast-2:647433528374:container-instance/13b83f4b-d557-48a6-a4d7-2b0e8068e62b", + "containers": [{ + "containerArn": "arn:aws:ecs:ap-southeast-2:647433528374:container/d56d4e2c-9804-42a7-9f2a-6029cb50d4a2", + "lastStatus": "RUNNING", + "name": "simple-app", + "networkBindings": [{ + "bindIP": "0.0.0.0", + "containerPort": 80, + "hostPort": 80, + "protocol": "tcp" + } + ], + "taskArn": "arn:aws:ecs:ap-southeast-2:647433528374:task/c15bcab8-39e6-4c28-a47d-27b433269e5c" + } + ], + "createdAt": 1.451468104403E9, + "desiredStatus": "RUNNING", + "lastStatus": "RUNNING", + "overrides": { + "containerOverrides": [{ + "name": "simple-app" + } + ] + }, + "startedAt": 1.45146812139E9, + "startedBy": "ecs-svc/9223370585386692588", + "taskArn": "arn:aws:ecs:ap-southeast-2:647433528374:task/c15bcab8-39e6-4c28-a47d-27b433269e5c", + "taskDefinitionArn": "arn:aws:ecs:ap-southeast-2:647433528374:task-definition/console-sample-app-static:1" + } + ] +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/getauthorizationtoken.json b/libcloud/test/container/fixtures/ecs/getauthorizationtoken.json new file mode 100644 index 0000000000..469c9efd8d --- /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 diff --git a/libcloud/test/container/fixtures/ecs/listclusters.json b/libcloud/test/container/fixtures/ecs/listclusters.json new file mode 100644 index 0000000000..11c0d0e20b --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/listclusters.json @@ -0,0 +1 @@ +{"clusterArns":["arn:aws:ecs:ap-southeast-2:647433528374:cluster/my-cluster","arn:aws:ecs:ap-southeast-2:647433528374:cluster/default"]} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/listimages.json b/libcloud/test/container/fixtures/ecs/listimages.json new file mode 100644 index 0000000000..ae598f0efc --- /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 diff --git a/libcloud/test/container/fixtures/ecs/listservices.json b/libcloud/test/container/fixtures/ecs/listservices.json new file mode 100644 index 0000000000..62a3cf5dee --- /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 diff --git a/libcloud/test/container/fixtures/ecs/listtasks.json b/libcloud/test/container/fixtures/ecs/listtasks.json new file mode 100644 index 0000000000..644ff731da --- /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 diff --git a/libcloud/test/container/fixtures/ecs/registertaskdefinition.json b/libcloud/test/container/fixtures/ecs/registertaskdefinition.json new file mode 100644 index 0000000000..fd53f9d4d5 --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/registertaskdefinition.json @@ -0,0 +1,21 @@ +{ + "taskDefinition": { + "containerDefinitions": [{ + "cpu": 10, + "environment": [], + "essential": true, + "image": "simple-app", + "memory": 500, + "mountPoints": [], + "name": "my-simple-app", + "portMappings": [], + "volumesFrom": [] + } + ], + "family": "my-simple-app", + "revision": 1, + "status": "ACTIVE", + "taskDefinitionArn": "arn:aws:ecs:ap-southeast-2:647433528374:task-definition/my-simple-app:1", + "volumes": [] + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/runtask.json b/libcloud/test/container/fixtures/ecs/runtask.json new file mode 100644 index 0000000000..d53c97a638 --- /dev/null +++ b/libcloud/test/container/fixtures/ecs/runtask.json @@ -0,0 +1,26 @@ +{ + "failures": [], + "tasks": [{ + "clusterArn": "arn:aws:ecs:ap-southeast-2:647433528374:cluster/default", + "containerInstanceArn": "arn:aws:ecs:ap-southeast-2:647433528374:container-instance/13b83f4b-d557-48a6-a4d7-2b0e8068e62b", + "containers": [{ + "containerArn": "arn:aws:ecs:ap-southeast-2:647433528374:container/e443d10f-dea3-481e-8a1e-966b9ad4e498", + "lastStatus": "PENDING", + "name": "my-simple-app", + "taskArn": "arn:aws:ecs:ap-southeast-2:647433528374:task/b7c76236-b96f-4de1-93c8-9da3c30ccc23" + } + ], + "createdAt": 1.45181726008E9, + "desiredStatus": "RUNNING", + "lastStatus": "PENDING", + "overrides": { + "containerOverrides": [{ + "name": "my-simple-app" + } + ] + }, + "taskArn": "arn:aws:ecs:ap-southeast-2:647433528374:task/b7c76236-b96f-4de1-93c8-9da3c30ccc23", + "taskDefinitionArn": "arn:aws:ecs:ap-southeast-2:647433528374:task-definition/my-simple-app:1" + } + ] +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/ecs/stoptask.json b/libcloud/test/container/fixtures/ecs/stoptask.json new file mode 100644 index 0000000000..72b5ebb71a --- /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 diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces.json new file mode 100644 index 0000000000..38e8bec1ac --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces.json @@ -0,0 +1,44 @@ +{ + "kind": "NamespaceList", + "apiVersion": "v1", + "metadata": { + "selfLink": "/api/v1/namespaces", + "resourceVersion": "443" + }, + "items": [ + { + "metadata": { + "name": "default", + "selfLink": "/api/v1/namespaces/default", + "uid": "43e99cf9-b99d-11e5-8d53-0050568157ec", + "resourceVersion": "6", + "creationTimestamp": "2016-01-13T02:28:00Z" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } + }, + { + "metadata": { + "name": "test", + "selfLink": "/api/v1/namespaces/test", + "uid": "7cb89199-b9a6-11e5-8d53-0050568157ec", + "resourceVersion": "419", + "creationTimestamp": "2016-01-13T03:34:01Z" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } + } + ] +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default.json new file mode 100644 index 0000000000..d59d2c1c15 --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default.json @@ -0,0 +1,19 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "name": "default", + "selfLink": "/api/v1/namespaces/default", + "uid": "43e99cf9-b99d-11e5-8d53-0050568157ec", + "resourceVersion": "6", + "creationTimestamp": "2016-01-13T02:28:00Z" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default_DELETE.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default_DELETE.json new file mode 100644 index 0000000000..8b47e269e7 --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default_DELETE.json @@ -0,0 +1,20 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "name": "test", + "selfLink": "/api/v1/namespaces/test", + "uid": "7cb89199-b9a6-11e5-8d53-0050568157ec", + "resourceVersion": "447", + "creationTimestamp": "2016-01-13T03:34:01Z", + "deletionTimestamp": "2016-01-13T03:38:05Z" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Terminating" + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default_pods_POST.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default_pods_POST.json new file mode 100644 index 0000000000..49387d1467 --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default_pods_POST.json @@ -0,0 +1,47 @@ +{ + "kind": "Pod", + "apiVersion": "v1", + "metadata": { + "name": "hello-world", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/pods/hello-world", + "uid": "1fad5411-b9af-11e5-8701-0050568157ec", + "resourceVersion": "32", + "creationTimestamp": "2016-01-13T04:35:50Z" + }, + "spec": { + "volumes": [ + { + "name": "default-token-dpyh0", + "secret": { + "secretName": "default-token-dpyh0" + } + } + ], + "containers": [ + { + "name": "hello-world", + "image": "ubuntu:14.04", + "resources": {}, + "volumeMounts": [ + { + "name": "default-token-dpyh0", + "readOnly": true, + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" + } + ], + "terminationMessagePath": "/dev/termination-log", + "imagePullPolicy": "IfNotPresent" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "securityContext": {} + }, + "status": { + "phase": "Pending" + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_test.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_test.json new file mode 100644 index 0000000000..fddefa0a0c --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_test.json @@ -0,0 +1,19 @@ +{ + "kind": "Namespace", + "apiVersion": "v1", + "metadata": { + "name": "test", + "selfLink": "/api/v1/namespaces/test", + "uid": "7cb89199-b9a6-11e5-8d53-0050568157ec", + "resourceVersion": "419", + "creationTimestamp": "2016-01-13T03:34:01Z" + }, + "spec": { + "finalizers": [ + "kubernetes" + ] + }, + "status": { + "phase": "Active" + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_nodes.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_nodes.json new file mode 100644 index 0000000000..2664914b1f --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_nodes.json @@ -0,0 +1,81 @@ +{ + "kind": "NodeList", + "apiVersion": "v1", + "metadata": { + "selfLink": "/api/v1/nodes", + "resourceVersion": "24" + }, + "items": [ + { + "metadata": { + "name": "127.0.0.1", + "selfLink": "/api/v1/nodes/127.0.0.1", + "uid": "45949cbb-b99d-11e5-8d53-0050568157ec", + "resourceVersion": "24", + "creationTimestamp": "2016-01-13T02:28:03Z", + "labels": { + "kubernetes.io/hostname": "127.0.0.1" + } + }, + "spec": { + "externalID": "127.0.0.1" + }, + "status": { + "capacity": { + "cpu": "2", + "memory": "4048236Ki", + "pods": "40" + }, + "allocatable": { + "cpu": "2", + "memory": "4048236Ki", + "pods": "40" + }, + "conditions": [ + { + "type": "OutOfDisk", + "status": "False", + "lastHeartbeatTime": "2016-01-13T02:28:53Z", + "lastTransitionTime": "2016-01-13T02:28:03Z", + "reason": "KubeletHasSufficientDisk", + "message": "kubelet has sufficient disk space available" + }, + { + "type": "Ready", + "status": "True", + "lastHeartbeatTime": "2016-01-13T02:28:53Z", + "lastTransitionTime": "2016-01-13T02:28:03Z", + "reason": "KubeletReady", + "message": "kubelet is posting ready status" + } + ], + "addresses": [ + { + "type": "LegacyHostIP", + "address": "127.0.0.1" + }, + { + "type": "InternalIP", + "address": "127.0.0.1" + } + ], + "daemonEndpoints": { + "kubeletEndpoint": { + "Port": 10250 + } + }, + "nodeInfo": { + "machineID": "1d9faaba9168d4b4a3416e99000002a2", + "systemUUID": "42015AA3-9AF2-D089-9105-63CEF89EFE35", + "bootID": "c31fdd67-f995-4be5-942d-10df26b40501", + "kernelVersion": "3.13.0-46-generic", + "osImage": "Ubuntu 14.04.2 LTS", + "containerRuntimeVersion": "docker://1.9.1", + "kubeletVersion": "v1.2.0-alpha.5.848+3f2e99b7e7d6d8", + "kubeProxyVersion": "v1.2.0-alpha.5.848+3f2e99b7e7d6d8" + }, + "images": null + } + } + ] +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_nodes_127_0_0_1.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_nodes_127_0_0_1.json new file mode 100644 index 0000000000..1077b73fb9 --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_nodes_127_0_0_1.json @@ -0,0 +1,73 @@ +{ + "kind": "Node", + "apiVersion": "v1", + "metadata": { + "name": "127.0.0.1", + "selfLink": "/api/v1/nodes/127.0.0.1", + "uid": "45949cbb-b99d-11e5-8d53-0050568157ec", + "resourceVersion": "184", + "creationTimestamp": "2016-01-13T02:28:03Z", + "labels": { + "kubernetes.io/hostname": "127.0.0.1" + } + }, + "spec": { + "externalID": "127.0.0.1" + }, + "status": { + "capacity": { + "cpu": "2", + "memory": "4048236Ki", + "pods": "40" + }, + "allocatable": { + "cpu": "2", + "memory": "4048236Ki", + "pods": "40" + }, + "conditions": [ + { + "type": "OutOfDisk", + "status": "False", + "lastHeartbeatTime": "2016-01-13T02:55:34Z", + "lastTransitionTime": "2016-01-13T02:28:03Z", + "reason": "KubeletHasSufficientDisk", + "message": "kubelet has sufficient disk space available" + }, + { + "type": "Ready", + "status": "True", + "lastHeartbeatTime": "2016-01-13T02:55:34Z", + "lastTransitionTime": "2016-01-13T02:28:03Z", + "reason": "KubeletReady", + "message": "kubelet is posting ready status" + } + ], + "addresses": [ + { + "type": "LegacyHostIP", + "address": "127.0.0.1" + }, + { + "type": "InternalIP", + "address": "127.0.0.1" + } + ], + "daemonEndpoints": { + "kubeletEndpoint": { + "Port": 10250 + } + }, + "nodeInfo": { + "machineID": "1d9faaba9168d4b4a3416e99000002a2", + "systemUUID": "42015AA3-9AF2-D089-9105-63CEF89EFE35", + "bootID": "c31fdd67-f995-4be5-942d-10df26b40501", + "kernelVersion": "3.13.0-46-generic", + "osImage": "Ubuntu 14.04.2 LTS", + "containerRuntimeVersion": "docker://1.9.1", + "kubeletVersion": "v1.2.0-alpha.5.848+3f2e99b7e7d6d8", + "kubeProxyVersion": "v1.2.0-alpha.5.848+3f2e99b7e7d6d8" + }, + "images": null + } +} \ No newline at end of file diff --git a/libcloud/test/container/fixtures/kubernetes/_api_v1_pods.json b/libcloud/test/container/fixtures/kubernetes/_api_v1_pods.json new file mode 100644 index 0000000000..f29ffdc5b7 --- /dev/null +++ b/libcloud/test/container/fixtures/kubernetes/_api_v1_pods.json @@ -0,0 +1,94 @@ +{ + "kind": "PodList", + "apiVersion": "v1", + "metadata": { + "selfLink": "/api/v1/pods", + "resourceVersion": "63" + }, + "items": [ + { + "metadata": { + "name": "hello-world", + "namespace": "default", + "selfLink": "/api/v1/namespaces/default/pods/hello-world", + "uid": "1fad5411-b9af-11e5-8701-0050568157ec", + "resourceVersion": "62", + "creationTimestamp": "2016-01-13T04:35:50Z" + }, + "spec": { + "volumes": [ + { + "name": "default-token-dpyh0", + "secret": { + "secretName": "default-token-dpyh0" + } + } + ], + "containers": [ + { + "name": "hello-world", + "image": "ubuntu:14.04", + "resources": {}, + "volumeMounts": [ + { + "name": "default-token-dpyh0", + "readOnly": true, + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount" + } + ], + "terminationMessagePath": "/dev/termination-log", + "imagePullPolicy": "IfNotPresent" + } + ], + "restartPolicy": "Always", + "terminationGracePeriodSeconds": 30, + "dnsPolicy": "ClusterFirst", + "serviceAccountName": "default", + "serviceAccount": "default", + "nodeName": "127.0.0.1", + "securityContext": {} + }, + "status": { + "phase": "Running", + "conditions": [ + { + "type": "Ready", + "status": "False", + "lastProbeTime": null, + "lastTransitionTime": "2016-01-13T04:37:09Z", + "reason": "ContainersNotReady", + "message": "containers with unready status: [hello-world]" + } + ], + "hostIP": "127.0.0.1", + "podIP": "172.17.0.2", + "startTime": "2016-01-13T04:35:50Z", + "containerStatuses": [ + { + "name": "hello-world", + "state": { + "waiting": { + "reason": "CrashLoopBackOff", + "message": "Back-off 20s restarting failed container=hello-world pod=hello-world_default(1fad5411-b9af-11e5-8701-0050568157ec)" + } + }, + "lastState": { + "terminated": { + "exitCode": 0, + "reason": "Completed", + "startedAt": "2016-01-13T04:37:07Z", + "finishedAt": "2016-01-13T04:37:07Z", + "containerID": "docker://3c48b5cda79bce4c8866f02a3b96a024edb8f660d10e7d1755e9ced49ef47b36" + } + }, + "ready": false, + "restartCount": 2, + "image": "ubuntu:14.04", + "imageID": "docker://c4bea91afef3764163fd506f5c1090be1d34a9b63ece81867cb863455937048e", + "containerID": "docker://3c48b5cda79bce4c8866f02a3b96a024edb8f660d10e7d1755e9ced49ef47b36" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/libcloud/test/container/test_base.py b/libcloud/test/container/test_base.py new file mode 100644 index 0000000000..e34cb3c78b --- /dev/null +++ b/libcloud/test/container/test_base.py @@ -0,0 +1,29 @@ +# 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 + +from __future__ import with_statement + +import sys + +from libcloud.test import unittest +from libcloud.container.base import ContainerDriver + + +class BaseTestCase(unittest.TestCase): + def setUp(self): + self.driver = ContainerDriver('none', 'none') + + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/libcloud/test/container/test_docker.py b/libcloud/test/container/test_docker.py new file mode 100644 index 0000000000..a1978f7145 --- /dev/null +++ b/libcloud/test/container/test_docker.py @@ -0,0 +1,212 @@ +# 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. + +import sys + +from libcloud.test import unittest + +from libcloud.container.base import ContainerImage + +from libcloud.container.drivers.docker import DockerContainerDriver + +from libcloud.utils.py3 import httplib +from libcloud.test.secrets import CONTAINER_PARAMS_DOCKER +from libcloud.test.file_fixtures import ContainerFileFixtures +from libcloud.test import MockHttp + + +class DockerContainerDriverTestCase(unittest.TestCase): + + def setUp(self): + DockerContainerDriver.connectionCls.conn_classes = ( + DockerMockHttp, DockerMockHttp) + DockerMockHttp.type = None + DockerMockHttp.use_param = 'a' + self.driver = DockerContainerDriver(*CONTAINER_PARAMS_DOCKER) + + def test_list_images(self): + images = self.driver.list_images() + self.assertEqual(len(images), 4) + self.assertIsInstance(images[0], ContainerImage) + self.assertEqual(images[0].id, + 'cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0') + self.assertEqual(images[0].name, 'mongo:latest') + + def test_install_image(self): + image = self.driver.install_image('ubuntu:12.04') + self.assertTrue(image is not None) + self.assertEqual(image.id, 'cf55d61f5307b7a18a45980971d6cfd40b737dd661879c4a6b3f2aecc3bc37b0') + + def test_list_containers(self): + containers = self.driver.list_containers(all=True) + self.assertEqual(len(containers), 6) + self.assertEqual(containers[0].id, + '160936dc54fe8c332095676d9379003534b8cddd7565fa63018996e06dae1b6b') + self.assertEqual(containers[0].name, 'hubot') + self.assertEqual(containers[0].image.name, 'stackstorm/hubot') + + def test_deploy_container(self): + image = self.driver.list_images()[0] + container = self.driver.deploy_container(image=image, name='test') + self.assertEqual(container.id, 'a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + self.assertEqual(container.name, 'gigantic_goldberg') + + def test_get_container(self): + container = self.driver.get_container('a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + self.assertEqual(container.id, 'a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + self.assertEqual(container.name, 'gigantic_goldberg') + + def test_start_container(self): + container = self.driver.get_container('a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + container.start() + + def test_stop_container(self): + container = self.driver.get_container('a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + container.stop() + + def test_restart_container(self): + container = self.driver.get_container('a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + container.restart() + + def test_delete_container(self): + container = self.driver.get_container('a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + container.destroy() + + def test_ex_rename_container(self): + container = self.driver.get_container('a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + self.driver.ex_rename_container(container, 'bob') + + def test_ex_get_logs(self): + container = self.driver.get_container('a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303') + logs = self.driver.ex_get_logs(container) + self.assertTrue(logs is not None) + + def test_ex_search_images(self): + images = self.driver.ex_search_images('mysql') + self.assertEqual(len(images), 25) + self.assertEqual(images[0].name, 'mysql') + + +class DockerMockHttp(MockHttp): + fixtures = ContainerFileFixtures('docker') + + def _version( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('version.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _images_search( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('search.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _images_json( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('images.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _images_create( + self, method, url, body, headers): + if method == 'POST': + body = self.fixtures.load('create_image.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {'Content-Type': 'application/json'}, + httplib.responses[httplib.OK]) + + def _containers_json( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('containers.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _containers_create( + self, method, url, body, headers): + if method == 'POST': + body = self.fixtures.load('create_container.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _containers_a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303( + self, method, url, body, headers): + if method == 'DELETE': + body = '' + else: + raise AssertionError('Unsupported method') + return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.OK]) + + def _containers_a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303_start( + self, method, url, body, headers): + if method == 'POST': + body = '' + else: + raise AssertionError('Unsupported method') + return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.OK]) + + def _containers_a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303_restart( + self, method, url, body, headers): + if method == 'POST': + body = '' + else: + raise AssertionError('Unsupported method') + return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.OK]) + + def _containers_a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303_rename( + self, method, url, body, headers): + if method == 'POST': + body = '' + else: + raise AssertionError('Unsupported method') + return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.OK]) + + def _containers_a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303_stop( + self, method, url, body, headers): + if method == 'POST': + body = '' + else: + raise AssertionError('Unsupported method') + return (httplib.NO_CONTENT, body, {}, httplib.responses[httplib.OK]) + + def _containers_a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303_json( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('container_a68.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _containers_a68c1872c74630522c7aa74b85558b06824c5e672cee334296c50fb209825303_logs( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('logs.txt') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {'content-type': 'text/plain'}, httplib.responses[httplib.OK]) + + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/libcloud/test/container/test_docker_utils.py b/libcloud/test/container/test_docker_utils.py new file mode 100644 index 0000000000..8c8bd9020f --- /dev/null +++ b/libcloud/test/container/test_docker_utils.py @@ -0,0 +1,78 @@ +# 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. + +import sys + +from libcloud.test import unittest +from libcloud.container.utils.docker import HubClient +from libcloud.utils.py3 import httplib +from libcloud.test.file_fixtures import ContainerFileFixtures +from libcloud.test import MockHttp + + +class DockerUtilitiesTestCase(unittest.TestCase): + + def setUp(self): + HubClient.connectionCls.conn_classes = ( + DockerMockHttp, DockerMockHttp) + DockerMockHttp.type = None + DockerMockHttp.use_param = 'a' + self.driver = HubClient() + + def test_list_tags(self): + tags = self.driver.list_images('ubuntu', max_count=100) + self.assertEqual(len(tags), 88) + self.assertEqual(tags[0].name, 'registry.hub.docker.com/ubuntu:xenial') + + def test_get_repository(self): + repo = self.driver.get_repository('ubuntu') + self.assertEqual(repo['name'], 'ubuntu') + + def test_get_image(self): + image = self.driver.get_image('ubuntu', 'latest') + self.assertEqual(image.id, '2343') + self.assertEqual(image.name, 'registry.hub.docker.com/ubuntu:latest') + self.assertEqual(image.path, 'registry.hub.docker.com/ubuntu:latest') + + +class DockerMockHttp(MockHttp): + fixtures = ContainerFileFixtures('docker_utils') + + def _v2_repositories_library_ubuntu_tags_latest( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('v2_repositories_library_ubuntu_tags_latest.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_repositories_library_ubuntu_tags( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('v2_repositories_library_ubuntu_tags.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _v2_repositories_library_ubuntu( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('v2_repositories_library_ubuntu.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/libcloud/test/container/test_ecs.py b/libcloud/test/container/test_ecs.py new file mode 100644 index 0000000000..73a35b32a6 --- /dev/null +++ b/libcloud/test/container/test_ecs.py @@ -0,0 +1,206 @@ +# 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. + +import sys + +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 +from libcloud.test.file_fixtures import ContainerFileFixtures +from libcloud.test import MockHttp + + +class ElasticContainerDriverTestCase(unittest.TestCase): + + def setUp(self): + ElasticContainerDriver.connectionCls.conn_classes = ( + ECSMockHttp, ECSMockHttp) + ECSMockHttp.type = None + ECSMockHttp.use_param = 'a' + ElasticContainerDriver.ecrConnectionClass.conn_classes = ( + ECSMockHttp, ECSMockHttp) + + self.driver = ElasticContainerDriver(*CONTAINER_PARAMS_ECS) + + def test_list_clusters(self): + clusters = self.driver.list_clusters() + self.assertEqual(len(clusters), 1) + self.assertEqual(clusters[0].id, 'arn:aws:ecs:us-east-1:012345678910:cluster/default') + self.assertEqual(clusters[0].name, 'default') + + def test_create_cluster(self): + cluster = self.driver.create_cluster('my-cluster') + self.assertEqual(cluster.name, 'my-cluster') + + def test_destroy_cluster(self): + self.assertTrue( + self.driver.destroy_cluster( + ContainerCluster( + id='arn:aws:ecs:us-east-1:012345678910:cluster/jim', + name='jim', + driver=self.driver))) + + def test_list_containers(self): + containers = self.driver.list_containers() + self.assertEqual(len(containers), 1) + + def test_list_containers_for_cluster(self): + cluster = self.driver.list_clusters()[0] + containers = self.driver.list_containers(cluster=cluster) + self.assertEqual(len(containers), 1) + + 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:ap-southeast-2:647433528374:container/e443d10f-dea3-481e-8a1e-966b9ad4e498') + + 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:ap-southeast-2:647433528374:container/d56d4e2c-9804-42a7-9f2a-6029cb50d4a2') + self.assertEqual(container.name, 'simple-app') + self.assertEqual(container.image.name, 'simple-app') + + 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) + + 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') + fixture_map = { + 'DescribeClusters': 'describeclusters.json', + 'CreateCluster': 'createcluster.json', + 'DeleteCluster': 'deletecluster.json', + 'DescribeTasks': 'describetasks.json', + 'ListTasks': 'listtasks.json', + 'ListClusters': 'listclusters.json', + 'RegisterTaskDefinition': 'registertaskdefinition.json', + 'RunTask': 'runtask.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( + self, method, url, body, headers): + target = headers['x-amz-target'] + if target is not None: + type = target.split('.')[-1] + if type is None or self.fixture_map.get(type) is None: + raise AssertionError('Unsupported request type %s' % (target)) + body = self.fixtures.load(self.fixture_map.get(type)) + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/libcloud/test/container/test_kubernetes.py b/libcloud/test/container/test_kubernetes.py new file mode 100644 index 0000000000..b60a648d3f --- /dev/null +++ b/libcloud/test/container/test_kubernetes.py @@ -0,0 +1,150 @@ +# 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. + +import sys + +from libcloud.test import unittest + +from libcloud.container.base import ContainerImage + +from libcloud.container.drivers.kubernetes import KubernetesContainerDriver + +from libcloud.utils.py3 import httplib +from libcloud.test.secrets import CONTAINER_PARAMS_KUBERNETES +from libcloud.test.file_fixtures import ContainerFileFixtures +from libcloud.test import MockHttp + + +class KubernetesContainerDriverTestCase(unittest.TestCase): + + def setUp(self): + KubernetesContainerDriver.connectionCls.conn_classes = ( + KubernetesMockHttp, KubernetesMockHttp) + KubernetesMockHttp.type = None + KubernetesMockHttp.use_param = 'a' + self.driver = KubernetesContainerDriver(*CONTAINER_PARAMS_KUBERNETES) + + def test_list_containers(self): + containers = self.driver.list_containers() + self.assertEqual(len(containers), 1) + self.assertEqual(containers[0].id, + 'docker://3c48b5cda79bce4c8866f02a3b96a024edb8f660d10e7d1755e9ced49ef47b36') + self.assertEqual(containers[0].name, 'hello-world') + + def test_list_clusters(self): + clusters = self.driver.list_clusters() + self.assertEqual(len(clusters), 2) + self.assertEqual(clusters[0].id, + 'default') + self.assertEqual(clusters[0].name, 'default') + + def test_get_cluster(self): + cluster = self.driver.get_cluster('default') + self.assertEqual(cluster.id, + 'default') + self.assertEqual(cluster.name, 'default') + + def test_create_cluster(self): + cluster = self.driver.create_cluster('test') + self.assertEqual(cluster.id, + 'test') + self.assertEqual(cluster.name, 'test') + + def test_destroy_cluster(self): + cluster = self.driver.get_cluster('default') + result = self.driver.destroy_cluster(cluster) + self.assertTrue(result) + + def test_deploy_container(self): + image = ContainerImage( + id=None, + name='hello-world', + path=None, + driver=self.driver, + version=None + ) + container = self.driver.deploy_container('hello-world', image=image) + self.assertEqual(container.name, 'hello-world') + + +class KubernetesMockHttp(MockHttp): + fixtures = ContainerFileFixtures('kubernetes') + + def _version( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('version.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_v1_pods( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('_api_v1_pods.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_v1_nodes( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('_api_v1_nodes.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_v1_nodes_127_0_0_1( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('_api_v1_nodes_127_0_0_1.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_v1_namespaces( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('_api_v1_namespaces.json') + elif method == 'POST': + body = self.fixtures.load('_api_v1_namespaces_test.json') + elif method == 'DELETE': + body = self.fixtures.load('_api_v1_namespaces_DELETE.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_v1_namespaces_default( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('_api_v1_namespaces_default.json') + elif method == 'DELETE': + body = self.fixtures.load('_api_v1_namespaces_default_DELETE.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + + def _api_v1_namespaces_default_pods( + self, method, url, body, headers): + if method == 'GET': + body = self.fixtures.load('_api_v1_namespaces_default_pods.json') + elif method == 'POST': + body = self.fixtures.load('_api_v1_namespaces_default_pods_POST.json') + else: + raise AssertionError('Unsupported method') + return (httplib.OK, body, {}, httplib.responses[httplib.OK]) + +if __name__ == '__main__': + sys.exit(unittest.main()) diff --git a/libcloud/test/file_fixtures.py b/libcloud/test/file_fixtures.py index 60d4358720..59f8e2eb61 100644 --- a/libcloud/test/file_fixtures.py +++ b/libcloud/test/file_fixtures.py @@ -29,6 +29,7 @@ 'dns': 'dns/fixtures', 'backup': 'backup/fixtures', 'openstack': 'compute/fixtures/openstack', + 'container': 'container/fixtures' } @@ -84,6 +85,12 @@ def __init__(self, sub_dir=''): sub_dir=sub_dir) +class ContainerFileFixtures(FileFixtures): + def __init__(self, sub_dir=''): + super(ContainerFileFixtures, self).__init__(fixtures_type='container', + sub_dir=sub_dir) + + class BackupFileFixtures(FileFixtures): def __init__(self, sub_dir=''): super(BackupFileFixtures, self).__init__(fixtures_type='backup', diff --git a/libcloud/test/secrets.py-dist b/libcloud/test/secrets.py-dist index dfc7a5b65e..216a7d58df 100644 --- a/libcloud/test/secrets.py-dist +++ b/libcloud/test/secrets.py-dist @@ -81,3 +81,8 @@ DNS_PARAMS_DURABLEDNS = ('api_user', 'api_key') DNS_PARAMS_GODADDY = ('customer-id', 'api_user', 'api_key') DNS_PARAMS_CLOUDFLARE = ('user@example.com', 'key') DNS_PARAMS_AURORADNS = ('apikey', 'secretkey') + +# Container +CONTAINER_PARAMS_DOCKER = ('user', 'password') +CONTAINER_PARAMS_ECS = ('user', 'password', 'region') +CONTAINER_PARAMS_KUBERNETES = ('user', 'password') diff --git a/setup.py b/setup.py index 7daac5284e..e8cc1bfff9 100644 --- a/setup.py +++ b/setup.py @@ -48,10 +48,12 @@ PROJECT_BASE_DIR = 'http://libcloud.apache.org' TEST_PATHS = ['libcloud/test', 'libcloud/test/common', 'libcloud/test/compute', 'libcloud/test/storage', 'libcloud/test/loadbalancer', - 'libcloud/test/dns', 'libcloud/test/backup'] + 'libcloud/test/dns', 'libcloud/test/container', + 'libcloud/test/backup'] DOC_TEST_MODULES = ['libcloud.compute.drivers.dummy', 'libcloud.storage.drivers.dummy', 'libcloud.dns.drivers.dummy', + 'libcloud.container.drivers.dummy', 'libcloud.backup.drivers.dummy'] SUPPORTED_VERSIONS = ['2.5', '2.6', '2.7', 'PyPy', '3.x']