diff --git a/k8s/models/deployment.py b/k8s/models/deployment.py index 9f04595..90bcd5f 100644 --- a/k8s/models/deployment.py +++ b/k8s/models/deployment.py @@ -15,8 +15,8 @@ class LabelSelector(Model): class RollingUpdateDeployment(Model): - maxUnavailable = Field(six.text_type) - maxSurge = Field(six.text_type) + maxUnavailable = Field(int, alt_type=six.text_type) + maxSurge = Field(int, alt_type=six.text_type) class DeploymentStrategy(Model): diff --git a/tests/k8s/test_deployer.py b/tests/k8s/test_deployer.py index 1ffcb35..c2a7d5d 100644 --- a/tests/k8s/test_deployer.py +++ b/tests/k8s/test_deployer.py @@ -1,116 +1,187 @@ #!/usr/bin/env python # -*- coding: utf-8 -from pprint import pformat - import pytest +import mock + +from k8s.client import NotFound from k8s.models.common import ObjectMeta -from k8s.models.deployment import Deployment, DeploymentSpec, LabelSelector +from k8s.models.deployment import Deployment, DeploymentSpec, LabelSelector, RollingUpdateDeployment, \ + DeploymentStrategy from k8s.models.pod import ContainerPort, Container, LocalObjectReference, Probe, HTTPGetAction, TCPSocketAction, \ PodTemplateSpec, PodSpec -from util import get_vcr - -vcr = get_vcr(__file__) NAME = "my-name" NAMESPACE = "my-namespace" - -@pytest.mark.usefixtures("logger", "k8s_config") +@pytest.mark.usefixtures("k8s_config") class TestDeployer(object): + @pytest.fixture + def post(self): + with mock.patch('k8s.client.Client.post') as m: + yield m + + @pytest.fixture + def put(self): + with mock.patch('k8s.client.Client.put') as m: + yield m + + @pytest.fixture + def get(self): + with mock.patch('k8s.client.Client.get') as m: + yield m + + @pytest.fixture + def delete(self): + with mock.patch('k8s.client.Client.delete') as m: + yield m + + @pytest.fixture + def api_get(self): + with mock.patch('k8s.base.ApiMixIn.get') as m: + yield m + def test_create_blank_deployment(self): object_meta = ObjectMeta(name=NAME, namespace=NAMESPACE) deployment = Deployment(metadata=object_meta) assert deployment.as_dict()[u"metadata"][u"name"] == NAME - def test_lifecycle(self, logger): - labels = {"test": "true"} - object_meta = ObjectMeta(name=NAME, namespace=NAMESPACE, labels=labels) - container_port = ContainerPort(name="http5000", containerPort=5000) - http = HTTPGetAction(path="/", port="http5000") - liveness = Probe(httpGet=http) - tcp = TCPSocketAction(port=5000) - readiness = Probe(tcpSocket=tcp) - container = Container( - name="container", - image="dummy_image", - ports=[container_port], - livenessProbe=liveness, - readinessProbe=readiness - ) - image_pull_secret = LocalObjectReference(name="image_pull_secret") - pod_spec = PodSpec(containers=[container], imagePullSecrets=[image_pull_secret], serviceAccountName="default") - pod_template_spec = PodTemplateSpec(metadata=object_meta, spec=pod_spec) - deployer_spec = DeploymentSpec(replicas=2, selector=LabelSelector(matchLabels=labels), - template=pod_template_spec, revisionHistoryLimit=5) - first = Deployment(metadata=object_meta, spec=deployer_spec) - logger.debug(pformat(first.as_dict())) - - pytest.helpers.assert_dicts(first.as_dict(), { - u"metadata": { - u"labels": { - u"test": u"true" - }, - u"namespace": u"my-namespace", - u"name": u"my-name" + def test_created_if_not_exists(self, post, api_get): + api_get.side_effect = NotFound() + deployment = _create_default_deployment() + call_params = deployment.as_dict() + + assert deployment._new + deployment.save() + assert not deployment._new + + pytest.helpers.assert_any_call(post, _uri(NAMESPACE), call_params) + + def test_created_if_not_exists_with_percentage_rollout_strategy(self, post, api_get): + api_get.side_effect = NotFound() + deployment = _create_default_deployment() + rolling_update = RollingUpdateDeployment(maxUnavailable="50%", maxSurge="50%") + deployment.spec.strategy = DeploymentStrategy(type="RollingUpdate", rollingUpdate=rolling_update) + call_params = deployment.as_dict() + + assert deployment._new + deployment.save() + assert not deployment._new + + pytest.helpers.assert_any_call(post, _uri(NAMESPACE), call_params) + + def test_updated_if_exists(self, get, put): + mock_response = _create_mock_response() + get.return_value = mock_response + deployment = _create_default_deployment() + + from_api = Deployment.get_or_create(metadata=deployment.metadata, spec=deployment.spec) + assert not from_api._new + assert from_api.spec.replicas == 2 + call_params = from_api.as_dict() + + from_api.save() + pytest.helpers.assert_any_call(put, _uri(NAMESPACE, NAME), call_params) + + def test_delete(self, delete): + Deployment.delete(NAME, namespace=NAMESPACE) + pytest.helpers.assert_any_call(delete, _uri(NAMESPACE, NAME)) + +def _create_default_deployment(): + labels = {"test": "true"} + object_meta = ObjectMeta(name=NAME, namespace=NAMESPACE, labels=labels) + container_port = ContainerPort(name="http5000", containerPort=5000) + http = HTTPGetAction(path="/", port="http5000") + liveness = Probe(httpGet=http) + tcp = TCPSocketAction(port=5000) + readiness = Probe(tcpSocket=tcp) + container = Container( + name="container", + image="dummy_image", + ports=[container_port], + livenessProbe=liveness, + readinessProbe=readiness + ) + image_pull_secret = LocalObjectReference(name="image_pull_secret") + pod_spec = PodSpec(containers=[container], imagePullSecrets=[image_pull_secret], serviceAccountName="default") + pod_template_spec = PodTemplateSpec(metadata=object_meta, spec=pod_spec) + deployer_spec = DeploymentSpec(replicas=2, selector=LabelSelector(matchLabels=labels), + template=pod_template_spec, revisionHistoryLimit=5) + deployment = Deployment(metadata=object_meta, spec=deployer_spec) + + return deployment + +def _create_mock_response(): + mock_response = mock.Mock() + mock_response.json.return_value = { + u"metadata": { + u"labels": { + u"test": u"true" }, - u"spec": { - u"replicas": 2, - u"revisionHistoryLimit": 5, - u"template": { - u"spec": { - u"dnsPolicy": u"ClusterFirst", - u"serviceAccountName": u"default", - u"restartPolicy": u"Always", - u"volumes": [], - u"imagePullSecrets": [ - { - u"name": u"image_pull_secret" - } - ], - u"containers": [ - { - u"livenessProbe": { - u"initialDelaySeconds": 5, - u"httpGet": { - u"path": u"/", - u"scheme": u"HTTP", - u"port": u"http5000" - } - }, - u"name": u"container", - u"image": u"dummy_image", - u"volumeMounts": [], - u"env": [], - u"imagePullPolicy": u"IfNotPresent", - u"readinessProbe": { - u"initialDelaySeconds": 5, - u"tcpSocket": { - u"port": 5000 - } - }, - u"ports": [ - { - u"protocol": u"TCP", - u"containerPort": 5000, - u"name": u"http5000" - } - ] - } - ] - }, - u"metadata": { - u"labels": { - u"test": u"true" - }, - u"namespace": u"my-namespace", - u"name": u"my-name" - } + u"namespace": u"my-namespace", + u"name": u"my-name" + }, + u"spec": { + u"replicas": 2, + u"revisionHistoryLimit": 5, + u"template": { + u"spec": { + u"dnsPolicy": u"ClusterFirst", + u"serviceAccountName": u"default", + u"restartPolicy": u"Always", + u"volumes": [], + u"imagePullSecrets": [ + { + u"name": u"image_pull_secret" + } + ], + u"containers": [ + { + u"livenessProbe": { + u"initialDelaySeconds": 5, + u"httpGet": { + u"path": u"/", + u"scheme": u"HTTP", + u"port": u"http5000" + } + }, + u"name": u"container", + u"image": u"dummy_image", + u"volumeMounts": [], + u"env": [], + u"imagePullPolicy": u"IfNotPresent", + u"readinessProbe": { + u"initialDelaySeconds": 5, + u"tcpSocket": { + u"port": 5000 + } + }, + u"ports": [ + { + u"protocol": u"TCP", + u"containerPort": 5000, + u"name": u"http5000" + } + ] + } + ] }, - u"selector": { - u"matchLabels": { + u"metadata": { + u"labels": { u"test": u"true" - } + }, + u"namespace": u"my-namespace", + u"name": u"my-name" + } + }, + u"selector": { + u"matchLabels": { + u"test": u"true" } } - }) + } + } + return mock_response + +def _uri(namespace, name=""): + return "/apis/extensions/v1beta1/namespaces/{namespace}/deployments/{name}".format(name=name, namespace=namespace)