Skip to content

Commit cebd736

Browse files
authored
feat(scheduler): add scheduler tests that do not go through normal api tests (#965)
This expands the scheduler test suite and can run outside of the API setup. Brings is simple testing per resource type. Also fixes a few mock and scheduler discrepencies that were discovered in the testing
1 parent 87a92f6 commit cebd736

15 files changed

Lines changed: 1030 additions & 72 deletions

rootfs/api/tests/test_utils.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,42 @@
44

55
class TestUtils(unittest.TestCase):
66
"""Test utils functions"""
7+
def test_dict_merge_not_dict(self):
8+
"""
9+
second item is not a dict, which dict_merge will just return
10+
"""
11+
a = {'key': 'value'}
12+
b = 'somethig'
13+
c = utils.dict_merge(a, b)
14+
self.assertEqual(c, b)
715

816
def test_dict_merge_simple(self):
917
a = {'key': 'value'}
1018
b = {'key': 'value'}
1119

1220
c = utils.dict_merge(a, b)
13-
assert c == {'key': 'value'}
21+
self.assertEqual(c, {'key': 'value'})
1422

1523
a = {'key': 'value'}
1624
b = {'key2': 'value'}
1725

1826
c = utils.dict_merge(a, b)
19-
assert c == {'key': 'value', 'key2': 'value'}
27+
self.assertEqual(c, {'key': 'value', 'key2': 'value'})
2028

2129
def test_dict_merge_deeper(self):
2230
a = {'key': 'value', 'here': {'without': 'you'}}
2331
b = {'this': 'that', 'here': {'with': 'me'}, 'other': {'magic', 'unicorn'}}
2432

2533
c = utils.dict_merge(a, b)
26-
assert c == {
34+
self.assertEqual(c, {
2735
'key': 'value',
2836
'this': 'that',
2937
'here': {
3038
'with': 'me',
3139
'without': 'you'
3240
},
3341
'other': {'magic', 'unicorn'}
34-
}
42+
})
3543

3644
def test_dict_merge_even_deeper(self):
3745
a = {
@@ -48,7 +56,7 @@ def test_dict_merge_even_deeper(self):
4856
}
4957

5058
c = utils.dict_merge(a, b)
51-
assert c == {
59+
self.assertEqual(c, {
5260
'key': 'value',
5361
'this': 'that',
5462
'here': {'with': 'me', 'without': 'you'},
@@ -60,19 +68,28 @@ def test_dict_merge_even_deeper(self):
6068
'char3': 'Cox'
6169
}
6270
}
63-
}
71+
})
6472

6573
def test_dict_merge_with_list(self):
6674
a = {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 'jimbo']}
6775
b = {'key': 'value', 'names': ['kenny', 'cartman', 'stan']}
6876

6977
c = utils.dict_merge(a, b)
70-
assert c == {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 'jimbo', 'cartman', 'stan']}
78+
self.assertEqual(c, {'key': 'value', 'names': ['bob', 'kyle', 'kenny',
79+
'jimbo', 'cartman', 'stan']})
80+
81+
a = {'key': 'value', 'names': ['bob', 'kyle', 'kenny', 'jimbo']}
82+
b = {'key': 'value', 'last_names': ['kenny', 'cartman', 'stan']}
83+
84+
c = utils.dict_merge(a, b)
85+
self.assertEqual(c, {'key': 'value',
86+
'names': ['bob', 'kyle', 'kenny', 'jimbo'],
87+
'last_names': ['kenny', 'cartman', 'stan']})
7188

7289
def test_dict_merge_bad_merge(self):
7390
"""Returns b because it isn't a dict"""
7491
a = {'key': 'value'}
7592
b = 'duh'
7693

7794
c = utils.dict_merge(a, b)
78-
assert c == b
95+
self.assertEqual(c, b)

rootfs/scheduler/__init__.py

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,10 @@ def __init__(self, response, errmsg, *args, **kwargs):
3131
self.response = response
3232

3333
msg = errmsg.format(*args)
34-
msg = "failed to {}: {} {}\n{}".format(
34+
msg = "failed to {}: {} {}".format(
3535
msg,
3636
response.status_code,
37-
response.reason,
38-
response.json()
37+
response.reason
3938
)
4039
KubeException.__init__(self, msg, *args, **kwargs)
4140

@@ -110,30 +109,21 @@ def deploy(self, namespace, name, image, entrypoint, command, **kwargs): # noqa
110109
return
111110
except KubeException:
112111
# create the initial deployment object (and the first revision)
113-
self.create_deployment(namespace,
114-
name,
115-
image,
116-
entrypoint,
117-
command,
118-
**kwargs)
112+
self.create_deployment(
113+
namespace, name, image, entrypoint, command, **kwargs
114+
)
119115
else:
120116
try:
121117
# kick off a new revision of the deployment
122-
self.update_deployment(namespace,
123-
name,
124-
image,
125-
entrypoint,
126-
command,
127-
**kwargs)
118+
self.update_deployment(
119+
namespace, name, image, entrypoint, command, **kwargs
120+
)
128121
except KubeException as e:
129122
# rollback to the previous Deployment
130123
kwargs['rollback'] = True
131-
self.update_deployment(namespace,
132-
name,
133-
image,
134-
entrypoint,
135-
command,
136-
**kwargs)
124+
self.update_deployment(
125+
namespace, name, image, entrypoint, command, **kwargs
126+
)
137127

138128
raise KubeException(
139129
'There was a problem while deploying {} of {}-{}. '
@@ -307,7 +297,13 @@ def scale(self, namespace, name, image, entrypoint, command, **kwargs):
307297
try:
308298
self.create_deployment(namespace, name, image, entrypoint, command, **kwargs)
309299
except KubeException:
310-
self.delete_deployment(namespace, name)
300+
# see if the deployment got created
301+
try:
302+
self.get_deployment(namespace, name)
303+
except KubeHTTPException as e:
304+
if e.response.status_code != 404:
305+
self.delete_deployment(namespace, name)
306+
311307
raise
312308

313309
# let the scale failure bubble up
@@ -819,9 +815,7 @@ def create_namespace(self, namespace):
819815
def delete_namespace(self, namespace):
820816
url = self._api("/namespaces/{}", namespace)
821817
response = self.session.delete(url)
822-
if response.status_code == 404:
823-
logger.warn('delete Namespace "{}": not found'.format(namespace))
824-
elif response.status_code != 200:
818+
if unhealthy(response.status_code):
825819
raise KubeHTTPException(response, 'delete Namespace "{}"', namespace)
826820

827821
return response
@@ -1176,7 +1170,7 @@ def get_secret(self, namespace, name):
11761170
secrets['data'][key] = ""
11771171
continue
11781172
value = base64.b64decode(value)
1179-
value = value if isinstance(value, bytes) else bytes(value, 'UTF-8')
1173+
value = value if isinstance(value, bytes) else bytes(str(value), 'UTF-8')
11801174
secrets['data'][key] = value.decode(encoding='UTF-8')
11811175

11821176
# tell python-requests it actually hasn't consumed the data
@@ -1216,7 +1210,11 @@ def _build_secret_manifest(self, namespace, name, data, secret_type='Opaque', la
12161210
manifest['metadata']['labels'].update(labels)
12171211

12181212
for key, value in data.items():
1219-
value = value if isinstance(value, bytes) else bytes(value, 'UTF-8')
1213+
if value is None:
1214+
manifest['data'].update({key: ''})
1215+
continue
1216+
1217+
value = value if isinstance(value, bytes) else bytes(str(value), 'UTF-8')
12201218
item = base64.b64encode(value).decode(encoding='UTF-8')
12211219
manifest['data'].update({key: item})
12221220

@@ -1279,13 +1277,13 @@ def get_services(self, namespace, **kwargs):
12791277

12801278
return response
12811279

1282-
def create_service(self, namespace, name, data={}, **kwargs):
1280+
def create_service(self, namespace, name, **kwargs):
12831281
# Ports and app type will be overwritten as required
12841282
manifest = {
12851283
'kind': 'Service',
12861284
'apiVersion': 'v1',
12871285
'metadata': {
1288-
'name': namespace,
1286+
'name': name,
12891287
'labels': {
12901288
'app': namespace,
12911289
'heritage': 'deis'
@@ -1306,7 +1304,7 @@ def create_service(self, namespace, name, data={}, **kwargs):
13061304
}
13071305
}
13081306

1309-
data = dict_merge(manifest, data)
1307+
data = dict_merge(manifest, kwargs.get('data', {}))
13101308
url = self._api("/namespaces/{}/services", namespace)
13111309
response = self.session.post(url, json=data)
13121310
if unhealthy(response.status_code):
@@ -1841,6 +1839,10 @@ def _build_deployment_manifest(self, namespace, name, image, entrypoint, command
18411839
return manifest
18421840

18431841
def _scale_deployment(self, namespace, name, image, entrypoint, command, **kwargs):
1842+
"""
1843+
A convenience wrapper around Deployment update that does a little bit of introspection
1844+
to determine if scale level is already where it needs to be
1845+
"""
18441846
deployment = self.get_deployment(namespace, name).json()
18451847
desired = int(kwargs.get('replicas'))
18461848
current = int(deployment['spec']['replicas'])

rootfs/scheduler/mock.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
from datetime import datetime, timedelta
23
import json
34
import random
@@ -360,7 +361,7 @@ def manage_replicasets(deployment, url):
360361
rs_url = url.replace('_deployments_', '_replicasets_')
361362

362363
# create new RS
363-
rs = deployment.copy()
364+
rs = copy.deepcopy(deployment)
364365
rs['kind'] = 'ReplicaSet'
365366
# fix up the name
366367
rs['metadata']['name'] = rs['metadata']['name'] + '-' + pod_hash
@@ -553,13 +554,21 @@ def post(request, context):
553554
"""Process a POST request to the kubernetes API"""
554555
data = request.json()
555556
url = cache_key(request.url + '/' + data['metadata']['name'] + '/')
557+
resource_type = get_type(request.url)
558+
# check if the namespace being posted to exists
559+
if resource_type != 'namespaces':
560+
namespace, _ = url.split('_{}_'.format(resource_type))
561+
namespace = namespace.replace('apis_extensions_v1beta1', 'api_v1')
562+
if cache.get(namespace) is None:
563+
context.status_code = 404
564+
context.reason = 'Not Found'
565+
return {}
566+
556567
if cache.get(url) is not None:
557568
context.status_code = 409
558569
context.reason = 'Conflict'
559570
return {}
560571

561-
resource_type = get_type(request.url)
562-
563572
# fill in generic data
564573
timestamp = str(datetime.utcnow().strftime(settings.DEIS_DATETIME_FORMAT))
565574
data['metadata']['creationTimestamp'] = timestamp
@@ -598,6 +607,17 @@ def post(request, context):
598607
def put(request, context):
599608
"""Process a PUT request to the kubernetes API"""
600609
url = cache_key(request.url)
610+
# type is the second last element
611+
resource_type = get_type(request.url, -2)
612+
# check if the namespace being posted to exists
613+
if resource_type != 'namespaces':
614+
namespace, _ = url.split('_{}_'.format(resource_type))
615+
namespace = namespace.replace('apis_extensions_v1beta1', 'api_v1')
616+
if cache.get(namespace) is None:
617+
context.status_code = 404
618+
context.reason = 'Not Found'
619+
return {}
620+
601621
item = cache.get(url)
602622
if item is None:
603623
context.status_code = 404
@@ -606,9 +626,6 @@ def put(request, context):
606626

607627
data = request.json()
608628

609-
# type is the second last element
610-
resource_type = get_type(request.url, -2)
611-
612629
# merge new data into old but keep labels separate in case they changed
613630
labels = data['metadata'].pop('labels')
614631
item['metadata'].update(data['metadata'])

rootfs/scheduler/tests/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from django.core.cache import cache
2+
from django.test import TestCase as DjangoTestCase
3+
4+
from scheduler import mock
5+
from scheduler.utils import generate_random_name
6+
7+
8+
class TestCase(DjangoTestCase):
9+
def setUp(self):
10+
self.scheduler = mock.MockSchedulerClient()
11+
# have a namespace available at all times
12+
self.namespace = self.create_namespace()
13+
14+
def tearDown(self):
15+
# make sure every test has a clean slate for k8s mocking
16+
cache.clear()
17+
18+
def create_namespace(self):
19+
namespace = generate_random_name()
20+
response = self.scheduler.create_namespace(namespace)
21+
self.assertEqual(response.status_code, 201, response.json())
22+
# assert minimal amount data
23+
data = response.json()
24+
self.assertEqual(data['apiVersion'], 'v1')
25+
self.assertEqual(data['kind'], 'Namespace')
26+
self.assertDictContainsSubset(
27+
{
28+
'name': namespace,
29+
'labels': {
30+
'heritage': 'deis'
31+
}
32+
},
33+
data['metadata']
34+
)
35+
36+
return namespace

0 commit comments

Comments
 (0)