diff --git a/k8s/models/pod.py b/k8s/models/pod.py index 930b1a7..f3f0016 100644 --- a/k8s/models/pod.py +++ b/k8s/models/pod.py @@ -44,6 +44,21 @@ class EnvVarSource(Model): secretKeyRef = Field(SecretKeySelector) +class SecretEnvSource(Model): + name = Field(six.text_type) + optional = Field(bool) + + +class ConfigMapEnvSource(Model): + name = Field(six.text_type) + optional = Field(bool) + + +class EnvFromSource(Model): + configMapRef = Field(ConfigMapEnvSource) + secretRef = Field(SecretEnvSource) + + class EnvVar(Model): name = Field(six.text_type) value = Field(six.text_type) @@ -97,6 +112,7 @@ class Container(Model): image = Field(six.text_type) ports = ListField(ContainerPort) env = ListField(EnvVar) + envFrom = ListField(EnvFromSource) resources = Field(ResourceRequirements) volumeMounts = ListField(VolumeMount) livenessProbe = Field(Probe) @@ -106,6 +122,8 @@ class Container(Model): class SecretVolumeSource(Model): secretName = Field(six.text_type) + optional = Field(bool) + defaultMode = Field(int) class KeyToPath(Model): @@ -115,6 +133,8 @@ class KeyToPath(Model): class ConfigMapVolumeSource(Model): name = Field(six.text_type) + optional = Field(bool) + defaultMode = Field(int) class EmptyDirVolumeSource(Model): diff --git a/tests/k8s/test_pod.py b/tests/k8s/test_pod.py index 76ff4b5..4e1c814 100644 --- a/tests/k8s/test_pod.py +++ b/tests/k8s/test_pod.py @@ -1,45 +1,129 @@ #!/usr/bin/env python # -*- coding: utf-8 +from __future__ import absolute_import, unicode_literals -from pprint import pformat - +import mock import pytest -from k8s.models.common import ObjectMeta -from k8s.models.pod import Pod, ContainerPort, Container, LocalObjectReference, PodSpec, Volume, VolumeMount, SecretVolumeSource -from util import get_vcr -vcr = get_vcr(__file__) +from k8s.client import NotFound +from k8s.models.common import ObjectMeta +from k8s.models.pod import Pod, ContainerPort, Container, LocalObjectReference, PodSpec, Volume, VolumeMount, \ + SecretVolumeSource NAME = "my-name" NAMESPACE = "my-namespace" +POD_URI = Pod._meta.url_template.format(name="", namespace=NAMESPACE) @pytest.mark.usefixtures("logger", "k8s_config") class TestPod(object): - @vcr.use_cassette() - def teardown(self): - for name, namespace in ((NAME, "default"), (NAME, NAMESPACE)): - try: - Pod.delete(name, namespace) - except: - pass - - @vcr.use_cassette() - def test_lifecycle(self, logger): - object_meta = ObjectMeta(name=NAME, namespace=NAMESPACE, labels={"test": "true", "app": NAME}) - container_port = ContainerPort(name="http5000", containerPort=5000) - secrets_volume_mounts = [VolumeMount(name=NAME, readOnly=True, mountPath="/var/run/secrets/kubernetes.io/kubernetes-secrets")] - secret_volumes = [Volume(name=NAME, secret=SecretVolumeSource(secretName=NAME))] - container = Container(name="container", image="dummy_image", ports=[container_port], volumeMounts=secrets_volume_mounts) - image_pull_secret = LocalObjectReference(name="image_pull_secret") - pod_spec = PodSpec(containers=[container], imagePullSecrets=[image_pull_secret], - volumes=secret_volumes, serviceAccountName="default") - first = Pod(metadata=object_meta, spec=pod_spec) - logger.debug(pformat(first.as_dict())) - first.save() - - pods = Pod.find(NAME, NAMESPACE) - assert len(pods) == 1 - second = pods[0] - assert first.metadata.name == second.metadata.name - assert first.metadata.namespace == second.metadata.namespace + def test_create_blank_pod(self): + pod = _create_pod() + assert pod.metadata.name == NAME + assert pod.as_dict()["metadata"]["name"] == NAME + + def test_pod_created_if_not_exists(self, post, api_get): + api_get.side_effect = NotFound() + pod = _create_pod() + call_params = pod.as_dict() + assert pod._new + pod.save() + assert not pod._new + pytest.helpers.assert_any_call(post, POD_URI, call_params) + + def test_get_or_create_pod_not_new(self, put, get): + mock_response = mock.Mock() + mock_response.json.return_value = { + 'apiVersion': 'v1', + 'kind': 'Pod', + 'metadata': { + 'creationTimestamp': '2017-10-03T10:36:20Z', + 'labels': { + 'app': 'my-name', 'test': 'true' + }, + 'name': 'my-name', + 'namespace': 'my-namespace', + 'resourceVersion': '852', + 'selfLink': '/api/v1/namespaces/my-namespace/pods/my-name', + 'uid': 'b1e35ab5-a826-11e7-ba76-0800273598c9' + }, + 'spec': { + 'containers': [{ + 'image': 'dummy_image', + 'imagePullPolicy': 'IfNotPresent', + 'name': 'container', + 'ports': [{ + 'containerPort': 5000, + 'name': 'http5000', + 'protocol': 'TCP' + }], + 'resources': {}, + 'volumeMounts': [{ + 'mountPath': '/var/run/secrets/kubernetes.io/kubernetes-secrets', + 'name': 'my-name', + 'readOnly': True + }, { + 'mountPath': '/var/run/secrets/kubernetes.io/serviceaccount', + 'name': 'default-token-0g73b', + 'readOnly': True + }] + }], + 'imagePullSecrets': [{ + 'name': 'image_pull_secret' + }], + 'restartPolicy': 'Always', + 'serviceAccount': 'default', + 'serviceAccountName': 'default', + 'terminationGracePeriodSeconds': 30, + 'volumes': [{ + 'name': 'my-name', + 'secret': { + 'defaultMode': 420, + 'secretName': 'my-name' + }}, { + 'name': 'default-token-0g73b', + 'secret': { + 'defaultMode': 420, + 'secretName': 'default-token-0g73b' + }}] + }, + 'status': { + 'conditions': [{ + 'lastProbeTime': None, + 'lastTransitionTime': '2017-10-03T10:36:20Z', + 'status': 'True', + 'type': 'PodScheduled' + }], + 'phase': 'Pending', + 'qosClass': 'BestEffort' + } + } + get.return_value = mock_response + pod = Pod.get(name=NAME, namespace=NAMESPACE) + assert not pod._new + assert pod.metadata.name == NAME + assert pod.spec.containers[0].ports[0].name == "http5000" + call_params = pod.as_dict() + pod.save() + pytest.helpers.assert_any_call(put, POD_URI + NAME, call_params) + + def test_pod_deleted(self, delete): + Pod.delete(NAME, NAMESPACE) + + # call delete with service_name + pytest.helpers.assert_any_call(delete, POD_URI + NAME) + + +def _create_pod(): + object_meta = ObjectMeta(name=NAME, namespace=NAMESPACE, labels={"test": "true", "app": NAME}) + container_port = ContainerPort(name="http5000", containerPort=5000) + secrets_volume_mounts = [ + VolumeMount(name=NAME, readOnly=True, mountPath="/var/run/secrets/kubernetes.io/kubernetes-secrets")] + secret_volumes = [Volume(name=NAME, secret=SecretVolumeSource(secretName=NAME))] + container = Container(name="container", image="dummy_image", ports=[container_port], + volumeMounts=secrets_volume_mounts) + image_pull_secret = LocalObjectReference(name="image_pull_secret") + pod_spec = PodSpec(containers=[container], imagePullSecrets=[image_pull_secret], + volumes=secret_volumes, serviceAccountName="default") + first = Pod(metadata=object_meta, spec=pod_spec) + return first diff --git a/tests/k8s/test_pod/teardown.yaml b/tests/k8s/test_pod/teardown.yaml deleted file mode 100644 index d3c86ff..0000000 --- a/tests/k8s/test_pod/teardown.yaml +++ /dev/null @@ -1,44 +0,0 @@ -interactions: -- request: - body: null - headers: - Accept: ['*/*'] - Accept-Encoding: ['gzip, deflate'] - Authorization: [Bearer password] - Connection: [keep-alive] - Content-Length: ['0'] - User-Agent: [python-requests/2.9.1] - method: DELETE - uri: https://10.0.0.1/api/v1/namespaces/default/pods/my-name - response: - body: {string: !!python/unicode "{\n \"kind\": \"Status\",\n \"apiVersion\": - \"v1\",\n \"metadata\": {},\n \"status\": \"Failure\",\n \"message\": \"pods - \\\"my-name\\\" not found\",\n \"reason\": \"NotFound\",\n \"details\": - {\n \"name\": \"my-name\",\n \"kind\": \"pods\"\n },\n \"code\": 404\n}\n"} - headers: - content-length: ['230'] - content-type: [application/json] - date: ['Fri, 01 Apr 2016 11:53:49 GMT'] - status: {code: 404, message: Not Found} -- request: - body: null - headers: - Accept: ['*/*'] - Accept-Encoding: ['gzip, deflate'] - Authorization: [Bearer password] - Connection: [keep-alive] - Content-Length: ['0'] - User-Agent: [python-requests/2.9.1] - method: DELETE - uri: https://10.0.0.1/api/v1/namespaces/my-namespace/pods/my-name - response: - body: {string: !!python/unicode '{"kind":"Pod","apiVersion":"v1","metadata":{"name":"my-name","namespace":"my-namespace","selfLink":"/api/v1/namespaces/my-namespace/pods/my-name","uid":"65e65cac-f800-11e5-bba2-247703d2e388","resourceVersion":"200","creationTimestamp":"2016-04-01T11:53:49Z","deletionTimestamp":"2016-04-01T11:54:19Z","deletionGracePeriodSeconds":30,"labels":{"app":"my-name","test":"true"}},"spec":{"volumes":[{"name":"my-name","secret":{"secretName":"my-name"}}],"containers":[{"name":"container","image":"dummy_image","ports":[{"name":"http5000","containerPort":5000,"protocol":"TCP"}],"resources":{},"volumeMounts":[{"name":"my-name","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/kubernetes-secrets"}],"terminationMessagePath":"/dev/termination-log","imagePullPolicy":"IfNotPresent"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"127.0.0.1","imagePullSecrets":[{"name":"image_pull_secret"}]},"status":{"phase":"Pending","conditions":[{"type":"Ready","status":"False","lastProbeTime":null,"lastTransitionTime":null}],"hostIP":"127.0.0.1","startTime":"2016-04-01T11:53:49Z","containerStatuses":[{"name":"container","state":{"waiting":{"reason":"ImageNotReady","message":"Image: - dummy_image is not ready on the node"}},"lastState":{},"ready":false,"restartCount":0,"image":"dummy_image","imageID":""}]}} - -'} - headers: - content-length: ['1414'] - content-type: [application/json] - date: ['Fri, 01 Apr 2016 11:53:49 GMT'] - status: {code: 200, message: OK} -version: 1 diff --git a/tests/k8s/test_pod/test_lifecycle.yaml b/tests/k8s/test_pod/test_lifecycle.yaml deleted file mode 100644 index a1f7c17..0000000 --- a/tests/k8s/test_pod/test_lifecycle.yaml +++ /dev/null @@ -1,48 +0,0 @@ -interactions: -- request: - body: !!python/unicode '{"spec": {"dnsPolicy": "ClusterFirst", "serviceAccountName": - "default", "restartPolicy": "Always", "volumes": [{"secret": {"secretName": - "my-name"}, "name": "my-name"}], "initContainers": [], "imagePullSecrets": [{"name": - "image_pull_secret"}], "containers": [{"name": "container", "image": "dummy_image", - "volumeMounts": [{"readOnly": true, "mountPath": "/var/run/secrets/kubernetes.io/kubernetes-secrets", - "name": "my-name"}], "env": [], "imagePullPolicy": "IfNotPresent", "ports": - [{"protocol": "TCP", "containerPort": 5000, "name": "http5000"}]}]}, "metadata": - {"labels": {"test": "true", "app": "my-name"}, "namespace": "my-namespace", - "name": "my-name"}}' - headers: - Accept: ['*/*'] - Accept-Encoding: ['gzip, deflate'] - Connection: [keep-alive] - Content-Length: ['660'] - Content-Type: [application/json] - User-Agent: [python-requests/2.13.0] - method: POST - uri: http://localhost:8080/api/v1/namespaces/my-namespace/pods/ - response: - body: {string: !!python/unicode '{"kind":"Pod","apiVersion":"v1","metadata":{"name":"my-name","namespace":"my-namespace","selfLink":"/api/v1/namespaces/my-namespace/pods/my-name","uid":"b1e35ab5-a826-11e7-ba76-0800273598c9","resourceVersion":"850","creationTimestamp":"2017-10-03T10:36:20Z","labels":{"app":"my-name","test":"true"}},"spec":{"volumes":[{"name":"my-name","secret":{"secretName":"my-name","defaultMode":420}},{"name":"default-token-0g73b","secret":{"secretName":"default-token-0g73b","defaultMode":420}}],"containers":[{"name":"container","image":"dummy_image","ports":[{"name":"http5000","containerPort":5000,"protocol":"TCP"}],"resources":{},"volumeMounts":[{"name":"my-name","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/kubernetes-secrets"},{"name":"default-token-0g73b","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"IfNotPresent"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","securityContext":{},"imagePullSecrets":[{"name":"image_pull_secret"}],"schedulerName":"default-scheduler"},"status":{"phase":"Pending","qosClass":"BestEffort"}} - -'} - headers: - content-length: ['1272'] - content-type: [application/json] - date: ['Tue, 03 Oct 2017 10:36:20 GMT'] - status: {code: 201, message: Created} -- request: - body: null - headers: - Accept: ['*/*'] - Accept-Encoding: ['gzip, deflate'] - Connection: [keep-alive] - User-Agent: [python-requests/2.13.0] - method: GET - uri: http://localhost:8080/api/v1/namespaces/my-namespace/pods/?labelSelector=app%3Dmy-name - response: - body: {string: !!python/unicode '{"kind":"PodList","apiVersion":"v1","metadata":{"selfLink":"/api/v1/namespaces/my-namespace/pods/","resourceVersion":"853"},"items":[{"metadata":{"name":"my-name","namespace":"my-namespace","selfLink":"/api/v1/namespaces/my-namespace/pods/my-name","uid":"b1e35ab5-a826-11e7-ba76-0800273598c9","resourceVersion":"852","creationTimestamp":"2017-10-03T10:36:20Z","labels":{"app":"my-name","test":"true"},"ownerReferences":[{"apiVersion":"v1","kind":"ReplicationController","name":"my-name","uid":"a47913ea-a826-11e7-ba76-0800273598c9","controller":true,"blockOwnerDeletion":true}]},"spec":{"volumes":[{"name":"my-name","secret":{"secretName":"my-name","defaultMode":420}},{"name":"default-token-0g73b","secret":{"secretName":"default-token-0g73b","defaultMode":420}}],"containers":[{"name":"container","image":"dummy_image","ports":[{"name":"http5000","containerPort":5000,"protocol":"TCP"}],"resources":{},"volumeMounts":[{"name":"my-name","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/kubernetes-secrets"},{"name":"default-token-0g73b","readOnly":true,"mountPath":"/var/run/secrets/kubernetes.io/serviceaccount"}],"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File","imagePullPolicy":"IfNotPresent"}],"restartPolicy":"Always","terminationGracePeriodSeconds":30,"dnsPolicy":"ClusterFirst","serviceAccountName":"default","serviceAccount":"default","nodeName":"minikube","securityContext":{},"imagePullSecrets":[{"name":"image_pull_secret"}],"schedulerName":"default-scheduler"},"status":{"phase":"Pending","conditions":[{"type":"PodScheduled","status":"True","lastProbeTime":null,"lastTransitionTime":"2017-10-03T10:36:20Z"}],"qosClass":"BestEffort"}}]} - -'} - headers: - content-length: ['1695'] - content-type: [application/json] - date: ['Tue, 03 Oct 2017 10:36:20 GMT'] - status: {code: 200, message: OK} -version: 1 diff --git a/tests/k8s/test_replication_controller/test_lifecycle.yaml b/tests/k8s/test_replication_controller/test_lifecycle.yaml index 6eb397a..ae9e8bb 100644 --- a/tests/k8s/test_replication_controller/test_lifecycle.yaml +++ b/tests/k8s/test_replication_controller/test_lifecycle.yaml @@ -5,7 +5,7 @@ interactions: "volumes": [], "initContainers": [], "imagePullSecrets": [{"name": "image_pull_secret"}], "containers": [{"livenessProbe": {"initialDelaySeconds": 5, "httpGet": {"path": "/", "scheme": "HTTP", "port": "http5000"}}, "name": "container", "image": "dummy_image", - "volumeMounts": [], "env": [], "imagePullPolicy": "IfNotPresent", "readinessProbe": + "volumeMounts": [], "env": [], "envFrom": [], "imagePullPolicy": "IfNotPresent", "readinessProbe": {"initialDelaySeconds": 5, "tcpSocket": {"port": 5000}}, "ports": [{"protocol": "TCP", "containerPort": 5000, "name": "http5000"}]}]}, "metadata": {"labels": {"test": "true"}, "namespace": "my-namespace", "name": "my-name"}}, "selector": diff --git a/tests/k8s/test_service.py b/tests/k8s/test_service.py index f99d169..ecbddc8 100644 --- a/tests/k8s/test_service.py +++ b/tests/k8s/test_service.py @@ -3,6 +3,7 @@ import mock import pytest + from k8s.client import NotFound from k8s.models.common import ObjectMeta from k8s.models.service import Service, ServicePort, ServiceSpec @@ -33,10 +34,8 @@ def test_create_blank_object_meta(self): } } - @mock.patch('k8s.base.ApiMixIn.get') - @mock.patch('k8s.client.Client.post') - def test_service_created_if_not_exists(self, post, get): - get.side_effect = NotFound() + def test_service_created_if_not_exists(self, post, api_get): + api_get.side_effect = NotFound() service = create_default_service() call_params = service.as_dict() assert service._new @@ -44,8 +43,6 @@ def test_service_created_if_not_exists(self, post, get): assert not service._new pytest.helpers.assert_any_call(post, SERVICES_URI, call_params) - @mock.patch('k8s.client.Client.get') - @mock.patch('k8s.client.Client.put') def test_get_or_create_service_not_new(self, put, get): service = create_default_service() @@ -88,14 +85,12 @@ def test_get_or_create_service_not_new(self, put, get): from_api.save() pytest.helpers.assert_any_call(put, SERVICES_URI + SERVICE_NAME, call_params) - @mock.patch('k8s.client.Client.delete') def test_service_deleted(self, delete): Service.delete(SERVICE_NAME, SERVICE_NAMESPACE) # call delete with service_name pytest.helpers.assert_any_call(delete, (SERVICES_URI + SERVICE_NAME)) - @mock.patch('k8s.client.Client.get') def test_list_services(self, get): service_list = { "apiVersion": "v1",