Skip to content

Commit db75777

Browse files
committed
feat(controller): add LimitRanges support
1 parent 54b869b commit db75777

6 files changed

Lines changed: 152 additions & 37 deletions

File tree

charts/controller/templates/controller-clusterrole.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ rules:
3838
- apiGroups: [""]
3939
resources: ["resourcequotas"]
4040
verbs: ["get", "create"]
41+
- apiGroups: [""]
42+
resources: ["limitranges"]
43+
verbs: ["get", "create"]
4144
- apiGroups: ["extensions"]
4245
resources: ["replicasets"]
4346
verbs: ["get", "list", "delete", "update"]

rootfs/api/models/app.py

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import time
1515
from itertools import groupby
1616
from urllib.parse import urljoin
17-
from collections import defaultdict
1817

1918
from django.conf import settings
2019
from django.db import models
@@ -270,7 +269,7 @@ def create(self, *args, **kwargs): # noqa
270269
)
271270

272271
# create required minimum resources in k8s for the application
273-
namespace = quota_name = service = self.id
272+
namespace = limits_name = quota_name = service = self.id
274273
try:
275274
self.log('creating Namespace {} and services'.format(namespace), level=logging.DEBUG)
276275
# Create essential resources
@@ -290,7 +289,14 @@ def create(self, *args, **kwargs): # noqa
290289
self._scheduler.quota.get(namespace, quota_name)
291290
except KubeException:
292291
self._scheduler.quota.create(namespace, quota_name, spec=quota_spec)
293-
292+
if settings.KUBERNETES_NAMESPACE_DEFAULT_LIMIT_RANGES_SPEC != '':
293+
limits_spec = json.loads(settings.KUBERNETES_NAMESPACE_DEFAULT_LIMIT_RANGES_SPEC)
294+
self.log('creating LimitRanges {} for namespace {}'.format(limits_name, namespace),
295+
level=logging.DEBUG)
296+
try:
297+
self._scheduler.limits.get(namespace, limits_name)
298+
except KubeException:
299+
self._scheduler.limits.create(namespace, limits_name, spec=limits_spec)
294300
try:
295301
self._scheduler.svc.get(namespace, service)
296302
except KubeException:
@@ -1096,7 +1102,7 @@ def _get_cpu_allocation(size):
10961102
)
10971103
)
10981104
return "{num}{unit}".format(
1099-
num=math.ceil(int(num) / cpu_allocation_ratio),
1105+
num=math.floor(int(num) / cpu_allocation_ratio),
11001106
unit=unit
11011107
)
11021108

@@ -1109,24 +1115,10 @@ def _get_ram_allocation(size):
11091115
)
11101116
)
11111117
return "{num}{unit}".format(
1112-
num=math.ceil(int(num) / ram_allocation_ratio),
1118+
num=math.floor(int(num) / ram_allocation_ratio),
11131119
unit=unit
11141120
)
11151121

1116-
def _get_default_resources(self):
1117-
resources = defaultdict(dict)
1118-
resources.update(
1119-
json.loads(settings.KUBERNETES_POD_DEFAULT_RESOURCES))
1120-
if "cpu" in resources["limits"]:
1121-
if "cpu" not in resources["requests"]:
1122-
resources["requests"]["cpu"] = self._get_cpu_allocation(
1123-
resources["limits"]["cpu"])
1124-
if "memory" in resources["limits"]:
1125-
if "memory" not in resources["requests"]:
1126-
resources["requests"]["memory"] = self._get_ram_allocation(
1127-
resources["limits"]["memory"])
1128-
return resources
1129-
11301122
def _gather_app_settings(self, release, app_settings, process_type, replicas, volumes=None):
11311123
"""
11321124
Gathers all required information needed in one easy place for passing into
@@ -1187,7 +1179,7 @@ def _gather_app_settings(self, release, app_settings, process_type, replicas, vo
11871179
'replicas': replicas,
11881180
'version': 'v{}'.format(release.version),
11891181
'app_type': process_type,
1190-
'resources': self._get_default_resources(),
1182+
'resources': json.loads(settings.KUBERNETES_POD_DEFAULT_RESOURCES),
11911183
'build_type': release.build.type,
11921184
'healthcheck': healthcheck,
11931185
'lifecycle_post_start': config.lifecycle_post_start,

rootfs/api/settings/production.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -337,35 +337,61 @@
337337

338338
KUBERNETES_CPU_ALLOCATION_RATIO = int(os.environ.get('KUBERNETES_CPU_ALLOCATION_RATIO', '10'))
339339
KUBERNETES_RAM_ALLOCATION_RATIO = int(os.environ.get('KUBERNETES_RAM_ALLOCATION_RATIO', '2'))
340+
340341
# Default pod spec for application.
341-
# Please do not set requests.cpu and requests.memory.
342-
# If set, they will not be dynamically computed when the first resource is allocated;
343-
# unless in the future through `drycc limits:set` manual setting
344342
KUBERNETES_POD_DEFAULT_RESOURCES = os.environ.get(
345343
'KUBERNETES_POD_DEFAULT_RESOURCES',
346344
json.dumps({
347345
"requests": {
348-
"ephemeral-storage": "256Mi"
346+
"cpu": "100m",
347+
"memory": "128M",
348+
"ephemeral-storage": "256Mi",
349349
},
350350
"limits": {
351-
"cpu": "500m",
352-
"memory": "512Mi",
353-
"ephemeral-storage": "1Gi"
351+
"cpu": "1",
352+
"memory": "256M",
353+
"ephemeral-storage": "512Mi",
354354
}
355355
})
356356
)
357357
# Default quota spec for application namespace
358358
KUBERNETES_NAMESPACE_DEFAULT_QUOTA_SPEC = os.environ.get(
359-
'KUBERNETES_NAMESPACE_DEFAULT_QUOTA_SPEC',
359+
'KUBERNETES_NAMESPACE_DEFAULT_QUOTA_SPEC', ''
360+
)
361+
# Default limit range spec for application namespace
362+
KUBERNETES_NAMESPACE_DEFAULT_LIMIT_RANGES_SPEC = os.environ.get(
363+
'KUBERNETES_NAMESPACE_DEFAULT_LIMIT_RANGES_SPEC',
360364
json.dumps({
361-
"hard": {
362-
"cpu": "64",
363-
"pods": "64",
364-
"memory": "128Gi",
365-
"ephemeral-storage": "64Gi",
366-
"requests.storage": "256Gi",
367-
"persistentvolumeclaims": 8,
368-
}
365+
"limits": [
366+
{
367+
"default": {
368+
"cpu": "100m",
369+
"memory": "128Mi"
370+
},
371+
"defaultRequest": {
372+
"cpu": "100m",
373+
"memory": "128Mi"
374+
},
375+
"max": {
376+
"cpu": "32",
377+
"memory": "128Gi"
378+
},
379+
"min": {
380+
"cpu": "100m",
381+
"memory": "128Mi"
382+
},
383+
"type": "Container"
384+
},
385+
{
386+
"max": {
387+
"storage": "100Gi"
388+
},
389+
"min": {
390+
"storage": "100Mi"
391+
},
392+
"type": "PersistentVolumeClaim"
393+
}
394+
]
369395
})
370396
)
371397

rootfs/scheduler/mock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ def _acquire(self):
8181
'namespaces', 'nodes', 'pods', 'replicationcontrollers',
8282
'secrets', 'services', 'events', 'deployments', 'replicasets',
8383
'horizontalpodautoscalers', 'scale', 'resourcequotas', 'ingresses',
84-
'persistentvolumeclaims', "serviceinstances", "servicebindings"
84+
'persistentvolumeclaims', 'serviceinstances', 'servicebindings',
85+
'limitranges'
8586
]
8687

8788

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from scheduler.exceptions import KubeHTTPException
2+
from scheduler.resources import Resource
3+
4+
5+
class LimitRanges(Resource):
6+
short_name = 'limits'
7+
8+
def get(self, namespace_name, name):
9+
"""
10+
Fetch a single LimitRanges
11+
"""
12+
url = '/namespaces/{}/limitranges/{}'.format(namespace_name, name)
13+
message = 'get LimitRanges {} for namespace {}'.format(name, namespace_name)
14+
url = self.api(url)
15+
response = self.http_get(url)
16+
if self.unhealthy(response.status_code):
17+
raise KubeHTTPException(response, message)
18+
19+
return response
20+
21+
def create(self, namespace_name, name, **kwargs):
22+
"""
23+
Create resource LimitRanges for namespace
24+
"""
25+
url = self.api("/namespaces/{}/limitranges".format(namespace_name))
26+
manifest = {
27+
"apiVersion": "v1",
28+
"kind": "LimitRange",
29+
"metadata": {
30+
"namespace": namespace_name,
31+
"name": name,
32+
'labels': {
33+
'app': namespace_name,
34+
'heritage': 'drycc'
35+
},
36+
},
37+
'spec': kwargs.get('spec', {})
38+
}
39+
response = self.http_post(url, json=manifest)
40+
if not response.status_code == 201:
41+
raise KubeHTTPException(response,
42+
"create LimitRanges {} for namespace {}".format(
43+
name, namespace_name))
44+
return response
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Unit tests for the Drycc scheduler module.
3+
4+
Run the tests with './manage.py test scheduler'
5+
"""
6+
from scheduler.tests import TestCase
7+
from scheduler import KubeHTTPException
8+
9+
10+
class LimitRangesTest(TestCase):
11+
12+
def test_create_quota(self):
13+
namespace_name = self.create_namespace()
14+
spec = {
15+
"limits": [
16+
{
17+
"type": "Container",
18+
"max": {
19+
"cpu": "32",
20+
"memory": "128Gi"
21+
},
22+
"min": {
23+
"cpu": "100m",
24+
"memory": "128Mi"
25+
}
26+
},
27+
{
28+
"type": "PersistentVolumeClaim",
29+
"max": {
30+
"storage": "100Gi"
31+
},
32+
"min": {
33+
"storage": "100Mi"
34+
}
35+
}
36+
]
37+
}
38+
self.scheduler.limits.create(namespace_name, 'test-1', spec=spec)
39+
response = self.scheduler.limits.get(namespace_name, 'test-1')
40+
data = response.json()
41+
self.assertEqual(data.get('spec', {}), spec)
42+
self.assertEqual(data['metadata']['namespace'], namespace_name)
43+
44+
def test_create_with_nonexistent_namespace(self):
45+
with self.assertRaises(
46+
KubeHTTPException,
47+
msg='failed to create LimitRanges test-1 for namespace ghost-namespace: 404 Not Found'
48+
):
49+
self.scheduler.quota.create('ghost-namespace', 'test-1', data={})

0 commit comments

Comments
 (0)