Skip to content

Commit

Permalink
Make maxUnavailable and maxSurge default int with alt_type text
Browse files Browse the repository at this point in the history
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
oyvindio committed Aug 30, 2017
1 parent 0ea5b47 commit ed25823
Show file tree
Hide file tree
Showing 2 changed files with 167 additions and 96 deletions.
4 changes: 2 additions & 2 deletions k8s/models/deployment.py
Expand Up @@ -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):
Expand Down
259 changes: 165 additions & 94 deletions 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)

0 comments on commit ed25823

Please sign in to comment.