Skip to content

Commit 273a5f8

Browse files
authored
Merge pull request #106 from Cryptophobia/ess-fix-autoscale-take-2
Fix for autoscale on k8s-1.9+ without breaking manual scaling
2 parents a881e4d + 2988579 commit 273a5f8

7 files changed

Lines changed: 173 additions & 6 deletions

File tree

charts/controller/templates/controller-clusterrole.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ rules:
4444
- apiGroups: ["extensions", "apps"]
4545
resources: ["deployments"]
4646
verbs: ["get", "list", "create", "update", "delete"]
47-
- apiGroups: ["extensions"]
47+
- apiGroups: ["extensions", "apps"]
4848
resources: ["deployments/scale", "replicasets/scale"]
4949
verbs: ["get", "update"]
5050
- apiGroups: ["extensions", "autoscaling"]

rootfs/api/models/app.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,7 +968,9 @@ def autoscale(self, proc_type, autoscale):
968968
"""
969969
name = '{}-{}'.format(self.id, proc_type)
970970
# basically fake out a Deployment object (only thing we use) to assign to the HPA
971-
target = {'kind': 'Deployment', 'metadata': {'name': name}}
971+
target = {'apiVersion': 'extensions/v1beta1',
972+
'kind': 'Deployment',
973+
'metadata': {'name': name}}
972974

973975
try:
974976
# get the target for autoscaler, in this case Deployment

rootfs/scheduler/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from collections import OrderedDict
22
from datetime import datetime
33
import logging
4-
from packaging.version import Version
4+
from packaging.version import Version, parse
55
import requests
66
import requests.exceptions
77
from requests_toolbelt import user_agent
88
import time
9+
import re
910
from urllib.parse import urljoin
1011

1112
from api import __version__ as deis_version
@@ -84,7 +85,9 @@ def version(self):
8485
raise KubeHTTPException(response, 'fetching Kubernetes version')
8586

8687
data = response.json()
87-
return Version('{}.{}'.format(data['major'], data['minor']))
88+
parsed_version = parse(
89+
re.sub("[^0-9\.]", '', str('{}.{}'.format(data['major'], data['minor']))))
90+
return Version('{}'.format(parsed_version))
8891

8992
@staticmethod
9093
def parse_date(date):

rootfs/scheduler/resources/deployment.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
from datetime import datetime, timedelta
22
import json
33
import time
4+
5+
from packaging.version import parse
6+
47
from scheduler.resources import Resource
58
from scheduler.exceptions import KubeException, KubeHTTPException
69

710

811
class Deployment(Resource):
912
api_prefix = 'apis'
10-
api_version = 'extensions/v1beta1'
13+
14+
@property
15+
def api_version(self):
16+
if self.version() >= parse("1.9.0"):
17+
return 'extensions/v1beta1'
18+
19+
return 'extensions/v1beta1'
1120

1221
def get(self, namespace, name=None, **kwargs):
1322
"""
@@ -43,7 +52,7 @@ def manifest(self, namespace, name, image, entrypoint, command, spec_annotations
4352

4453
manifest = {
4554
'kind': 'Deployment',
46-
'apiVersion': 'extensions/v1beta1',
55+
'apiVersion': self.api_version,
4756
'metadata': {
4857
'name': name,
4958
'labels': labels,

rootfs/scheduler/resources/horizontalpodautoscaler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def manifest(self, namespace, name, app_type, target, **kwargs):
7575
manifest['spec']['targetCPUUtilizationPercentage'] = cpu_percent
7676

7777
manifest['spec']['scaleTargetRef'] = {
78+
'apiVersion': target['apiVersion'],
7879
# only works with Deployments, RS and RC
7980
'kind': target['kind'],
8081
'name': target['metadata']['name'],

rootfs/scheduler/tests/test_deployments.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
44
Run the tests with './manage.py test scheduler'
55
"""
6+
from unittest import mock
7+
import copy
8+
from packaging.version import parse, Version, InvalidVersion
69
from scheduler import KubeHTTPException, KubeException
710
from scheduler.tests import TestCase
811
from scheduler.utils import generate_random_name
@@ -73,6 +76,57 @@ def scale(self, namespace=None, name=generate_random_name(), **kwargs):
7376
self.scheduler.scale(namespace, name, **kwargs)
7477
return name
7578

79+
def test_good_init_api_version(self):
80+
try:
81+
data = "1.13"
82+
Version('{}'.format(data))
83+
except InvalidVersion:
84+
self.fail("Version {} raised InvalidVersion exception!".format(data))
85+
86+
def test_bad_init_api_version(self):
87+
data = "1.13+"
88+
with self.assertRaises(
89+
InvalidVersion,
90+
msg='packaging.version.InvalidVersion: Invalid version: {}'.format(data) # noqa
91+
):
92+
Version('{}'.format(data))
93+
94+
def test_deployment_api_version_1_9_and_up(self):
95+
cases = ['1.12', '1.11', '1.10', '1.9']
96+
97+
deployment = copy.copy(self.scheduler.deployment)
98+
99+
expected = 'extensions/v1beta1'
100+
101+
for canonical in cases:
102+
deployment.version = mock.MagicMock(return_value=parse(canonical))
103+
actual = deployment.api_version
104+
self.assertEqual(
105+
expected,
106+
actual,
107+
"{} breaks - expected {}, got {}".format(
108+
canonical,
109+
expected,
110+
actual))
111+
112+
def test_deployment_api_version_1_8_and_lower(self):
113+
cases = ['1.8', '1.7', '1.6', '1.5', '1.4', '1.3', '1.2']
114+
115+
deployment = copy.copy(self.scheduler.deployment)
116+
117+
expected = 'extensions/v1beta1'
118+
119+
for canonical in cases:
120+
deployment.version = mock.MagicMock(return_value=parse(canonical))
121+
actual = deployment.api_version
122+
self.assertEqual(
123+
expected,
124+
actual,
125+
"{} breaks - expected {}, got {}".format(
126+
canonical,
127+
expected,
128+
actual))
129+
76130
def test_create_failure(self):
77131
with self.assertRaises(
78132
KubeHTTPException,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Unit tests for the Deis scheduler module.
3+
4+
Run the tests with "./manage.py test scheduler"
5+
"""
6+
import requests
7+
import requests_mock
8+
from unittest import mock
9+
from packaging.version import parse
10+
11+
from django.test import TestCase
12+
13+
import scheduler
14+
15+
16+
def mock_session_for_version(blah=None):
17+
return requests.Session()
18+
19+
20+
def connection_refused_matcher(request):
21+
raise requests.ConnectionError("connection refused")
22+
23+
24+
@mock.patch('scheduler.get_session', mock_session_for_version)
25+
class KubeHTTPClientTest(TestCase):
26+
"""Tests kubernetes HTTP client version calls"""
27+
28+
def setUp(self):
29+
self.adapter = requests_mock.Adapter()
30+
self.url = 'http://versiontest.example.com'
31+
self.path = '/version'
32+
33+
# use the real scheduler client.
34+
self.scheduler = scheduler.KubeHTTPClient(self.url)
35+
self.scheduler.session.mount(self.url, self.adapter)
36+
37+
def test_version_for_gke(self):
38+
"""
39+
Ensure that version() sanitizes info from GKE clusters
40+
"""
41+
42+
cases = {
43+
"1.12": {"major": "1", "minor": "12-gke"},
44+
"1.10": {"major": "1", "minor": "10-gke"},
45+
"1.9": {"major": "1", "minor": "9-gke"},
46+
"1.8": {"major": "1", "minor": "8-gke"},
47+
}
48+
49+
for canonical in cases:
50+
resp = cases[canonical]
51+
self.adapter.register_uri('GET', self.url + self.path, json=resp)
52+
53+
expected = parse(canonical)
54+
actual = self.scheduler.version()
55+
56+
self.assertEqual(expected, actual, "{} breaks".format(resp))
57+
58+
def test_version_for_eks(self):
59+
"""
60+
Ensure that version() sanitizes info from EKS clusters
61+
"""
62+
63+
cases = {
64+
"1.12": {"major": "1", "minor": "12+"},
65+
"1.10": {"major": "1", "minor": "10+"},
66+
"1.9": {"major": "1", "minor": "9+"},
67+
"1.8": {"major": "1", "minor": "8+"},
68+
}
69+
70+
for canonical in cases:
71+
resp = cases[canonical]
72+
self.adapter.register_uri('GET', self.url + self.path, json=resp)
73+
74+
expected = parse(canonical)
75+
actual = self.scheduler.version()
76+
77+
self.assertEqual(expected, actual, "{} breaks".format(resp))
78+
79+
def test_version_vanilla(self):
80+
"""
81+
Ensure that version() sanitizes info from vanilla k8s clusters
82+
"""
83+
84+
cases = {
85+
"1.12": {"major": "1", "minor": "12"},
86+
"1.10": {"major": "1", "minor": "10"},
87+
"1.9": {"major": "1", "minor": "9"},
88+
"1.8": {"major": "1", "minor": "8"},
89+
}
90+
91+
for canonical in cases:
92+
resp = cases[canonical]
93+
self.adapter.register_uri('GET', self.url + self.path, json=resp)
94+
95+
expected = parse(canonical)
96+
actual = self.scheduler.version()
97+
98+
self.assertEqual(expected, actual, "{} breaks".format(resp))

0 commit comments

Comments
 (0)