Extended the Kubernetes driver to support deploying containers as single-container pods. also added destroying containers (pods) via the API.
Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/0b875888 Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/0b875888 Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/0b875888 Branch: refs/heads/trunk Commit: 0b8758881942fbac1963f30363fd49cef9adfcb4 Parents: 936fe42 Author: anthony-shaw <[email protected]> Authored: Wed Jan 13 16:15:56 2016 +1100 Committer: anthony-shaw <[email protected]> Committed: Wed Jan 13 16:15:56 2016 +1100 ---------------------------------------------------------------------- libcloud/container/drivers/kubernetes.py | 138 +++++++++++++++++-- .../_api_v1_namespaces_default_pods_POST.json | 47 +++++++ .../fixtures/kubernetes/_api_v1_pods.json | 94 +++++++++++++ libcloud/test/container/test_kubernetes.py | 41 ++++-- 4 files changed, 302 insertions(+), 18 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/libcloud/blob/0b875888/libcloud/container/drivers/kubernetes.py ---------------------------------------------------------------------- diff --git a/libcloud/container/drivers/kubernetes.py b/libcloud/container/drivers/kubernetes.py index a947fd2..13f2a51 100644 --- a/libcloud/container/drivers/kubernetes.py +++ b/libcloud/container/drivers/kubernetes.py @@ -28,7 +28,7 @@ from libcloud.common.base import JsonResponse, ConnectionUserAndKey from libcloud.common.types import InvalidCredsError from libcloud.container.base import (Container, ContainerDriver, - ContainerCluster) + ContainerImage, ContainerCluster) from libcloud.container.providers import Provider from libcloud.container.types import ContainerState @@ -85,6 +85,16 @@ class KubernetesConnection(ConnectionUserAndKey): 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' @@ -169,7 +179,7 @@ class KubernetesContainerDriver(ContainerDriver): """ try: result = self.connection.request( - ROOT_URL + "v1/nodes/").object + ROOT_URL + "v1/pods").object except Exception as exc: if hasattr(exc, 'errno') and exc.errno == 111: raise KubernetesException( @@ -178,7 +188,10 @@ class KubernetesContainerDriver(ContainerDriver): 'and the API port is correct') raise - containers = [self._to_container(value) for value in result['items']] + 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): @@ -265,19 +278,124 @@ class KubernetesContainerDriver(ContainerDriver): data=json.dumps(request)).object return self._to_cluster(result) - def _to_container(self, data): + 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 """ - metadata = data['metadata'] return Container( - id=data['spec']['externalID'], - name=metadata['name'], - image=None, - ip_addresses="ips", + 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=None) + extra={ + 'pod': pod_data['metadata']['name'], + 'namespace': pod_data['metadata']['namespace'] + }) def _to_cluster(self, data): """ http://git-wip-us.apache.org/repos/asf/libcloud/blob/0b875888/libcloud/test/container/fixtures/kubernetes/_api_v1_namespaces_default_pods_POST.json ---------------------------------------------------------------------- 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 0000000..49387d1 --- /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 http://git-wip-us.apache.org/repos/asf/libcloud/blob/0b875888/libcloud/test/container/fixtures/kubernetes/_api_v1_pods.json ---------------------------------------------------------------------- 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 0000000..f29ffdc --- /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 http://git-wip-us.apache.org/repos/asf/libcloud/blob/0b875888/libcloud/test/container/test_kubernetes.py ---------------------------------------------------------------------- diff --git a/libcloud/test/container/test_kubernetes.py b/libcloud/test/container/test_kubernetes.py index 2e8da4e..b60a648 100644 --- a/libcloud/test/container/test_kubernetes.py +++ b/libcloud/test/container/test_kubernetes.py @@ -17,6 +17,8 @@ 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 @@ -38,14 +40,8 @@ class KubernetesContainerDriverTestCase(unittest.TestCase): containers = self.driver.list_containers() self.assertEqual(len(containers), 1) self.assertEqual(containers[0].id, - '127.0.0.1') - self.assertEqual(containers[0].name, '127.0.0.1') - - def test_get_container(self): - container = self.driver.get_container('127.0.0.1') - self.assertEqual(container.id, - '127.0.0.1') - self.assertEqual(container.name, '127.0.0.1') + 'docker://3c48b5cda79bce4c8866f02a3b96a024edb8f660d10e7d1755e9ced49ef47b36') + self.assertEqual(containers[0].name, 'hello-world') def test_list_clusters(self): clusters = self.driver.list_clusters() @@ -71,6 +67,17 @@ class KubernetesContainerDriverTestCase(unittest.TestCase): 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') @@ -83,6 +90,14 @@ class KubernetesMockHttp(MockHttp): 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': @@ -121,5 +136,15 @@ class KubernetesMockHttp(MockHttp): 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())
