Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Make maxUnavailable and maxSurge default int with alt_type text
These values have completely different semantics based on whether it is a string or an int. See https://kubernetes.io/docs/api-reference/v1.7/#rollingupdatedeployment-v1beta1-apps for details. Currently these are always handled as text, which makes it impossible to set absolute values, and also breaks updating of resources which have absolute values, as they are onverted to string and thus are invalid because of a missing trailing %.
- Loading branch information
Showing
2 changed files
with
167 additions
and
96 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |