Skip to content

Commit fd375f1

Browse files
committed
feat(controller): traefik v2 support
1 parent 017724c commit fd375f1

4 files changed

Lines changed: 201 additions & 92 deletions

File tree

rootfs/api/models/app.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,15 @@ def _refresh_ingress(self, hosts, tls_map, ssl_redirect):
196196
if ingress == "":
197197
raise ServiceUnavailable('Empty hostname')
198198
try:
199-
data = self._scheduler.ingress.get(namespace, ingress).json()
199+
data = self._scheduler.ingress(
200+
settings.INGRESS_CLASS).get(namespace, ingress).json()
200201
version = data["metadata"]["resourceVersion"]
201-
self._scheduler.ingress.put(
202-
ingress, settings.INGRESS_CLASS, namespace, version, **kwargs)
202+
self._scheduler.ingress(settings.INGRESS_CLASS).put(
203+
namespace, ingress, version, **kwargs)
203204
except KubeException:
204205
self.log("creating Ingress {}".format(namespace), level=logging.INFO)
205-
self._scheduler.ingress.create(
206-
ingress, settings.INGRESS_CLASS, namespace, **kwargs)
206+
self._scheduler.ingress(settings.INGRESS_CLASS).create(
207+
namespace, ingress, **kwargs)
207208
except KubeException as e:
208209
raise ServiceUnavailable('Could not create Ingress in Kubernetes') from e
209210

@@ -944,7 +945,7 @@ def routable(self, routable):
944945
else:
945946
try:
946947
namespace = ingress = self.id
947-
self._scheduler.ingress.delete(namespace, ingress)
948+
self._scheduler.ingress(settings.INGRESS_CLASS).delete(namespace, ingress)
948949
except KubeException as e:
949950
raise ServiceUnavailable(str(e)) from e
950951

rootfs/scheduler/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,33 +38,41 @@ def get_session(k8s_api_verify_tls):
3838

3939

4040
class KubeHTTPClient(object):
41+
abstract = False
42+
api_version = 'v1'
43+
api_prefix = 'api'
4144
# ISO-8601 which is used by kubernetes
4245
DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
4346

4447
def __init__(self, url, k8s_api_verify_tls=True):
4548
global resource_mapping
4649
self.url = url
47-
self.session = get_session(k8s_api_verify_tls)
50+
self.k8s_api_verify_tls = k8s_api_verify_tls
51+
self.session = get_session(self.k8s_api_verify_tls)
4852

4953
# map the various k8s Resources to an internal property
5054
from scheduler.resources import Resource # lazy load
5155
for res in Resource:
5256
name = str(res.__name__).lower() # singular
5357
component = name + 's' # make plural
5458
# check if component has already been processed
55-
if component in resource_mapping:
59+
if component in resource_mapping or res.abstract:
5660
continue
5761

5862
# get past recursion problems in case of self reference
5963
resource_mapping[component] = ''
60-
resource_mapping[component] = res(self.url)
64+
resource_mapping[component] = res(self.url, self.k8s_api_verify_tls)
6165
# map singular Resource name to the plural one
6266
resource_mapping[name] = component
6367
if res.short_name is not None:
6468
# map short name to long name so a resource can be named rs
6569
# but have the main object live at replicasets
6670
resource_mapping[str(res.short_name).lower()] = component
6771

72+
def api(self, tmpl, *args):
73+
"""Return a fully-qualified Kubernetes API URL from a string template with args."""
74+
return "/{}/{}".format(self.api_prefix, self.api_version) + tmpl.format(*args)
75+
6876
def __getattr__(self, name):
6977
global resource_mapping
7078
if name in resource_mapping:
Lines changed: 171 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
from scheduler.exceptions import KubeHTTPException
22
from scheduler.resources import Resource
33

4-
MANIFEAT_CLASSES = {}
54

5+
class BaseIngress(Resource):
6+
abstract = True
7+
api_version = 'networking.k8s.io/v1'
8+
api_prefix = 'apis'
9+
short_name = 'ingress'
10+
ingress_path = "/"
11+
ingress_class = None
612

7-
class BaseManifest(object):
8-
9-
def manifest(self, api_version, ingress, ingress_class, namespace, **kwargs):
10-
path = "/*" if ingress_class in ("gce", "alb") else "/"
13+
def manifest(self, ingress, **kwargs):
1114
hosts, tls = kwargs.pop("hosts", None), kwargs.pop("tls", None)
1215
version = kwargs.pop("version", None)
1316
data = {
1417
"kind": "Ingress",
15-
"apiVersion": api_version,
18+
"apiVersion": self.api_version,
1619
"metadata": {
1720
"name": ingress,
1821
"annotations": {
1922
"kubernetes.io/tls-acme": "true",
23+
"kubernetes.io/ingress.class": self.ingress_class
2024
}
2125
},
2226
"spec": {}
@@ -27,7 +31,7 @@ def manifest(self, api_version, ingress, ingress_class, namespace, **kwargs):
2731
"http": {
2832
"paths": [
2933
{
30-
"path": path,
34+
"path": self.ingress_path,
3135
"pathType": "Prefix",
3236
"backend": {
3337
"service": {
@@ -41,72 +45,12 @@ def manifest(self, api_version, ingress, ingress_class, namespace, **kwargs):
4145
]
4246
}
4347
} for host in hosts]
44-
if ingress_class:
45-
data["metadata"]["annotations"].update({
46-
"kubernetes.io/ingress.class": ingress_class
47-
})
4848
if tls:
4949
data["spec"]["tls"] = tls
5050
if version:
5151
data["metadata"]["resourceVersion"] = version
5252
return data
5353

54-
55-
class NginxManifest(BaseManifest):
56-
57-
def manifest(self, api_version, ingress, ingress_class, namespace, **kwargs):
58-
data = BaseManifest.manifest(
59-
self, api_version, ingress, ingress_class, namespace, **kwargs)
60-
if "allowlist" in kwargs:
61-
allowlist = ", ".join(kwargs.pop("allowlist"))
62-
data["metadata"]["annotations"].update({
63-
"nginx.ingress.kubernetes.io/whitelist-source-range": allowlist
64-
})
65-
if "ssl_redirect" in kwargs:
66-
ssl_redirect = kwargs.pop("ssl_redirect")
67-
data["metadata"]["annotations"].update({
68-
"nginx.ingress.kubernetes.io/ssl-redirect": ssl_redirect
69-
})
70-
return data
71-
72-
73-
MANIFEAT_CLASSES["nginx"] = NginxManifest
74-
75-
76-
class TraefikManifest(BaseManifest):
77-
78-
def manifest(self, api_version, ingress, ingress_class, namespace, **kwargs):
79-
data = BaseManifest.manifest(
80-
self, api_version, ingress, ingress_class, namespace, **kwargs)
81-
if "allowlist" in kwargs:
82-
allowlist = ", ".join(kwargs.pop("allowlist"))
83-
data["metadata"]["annotations"].update({
84-
"ingress.kubernetes.io/whitelist-x-forwarded-for": "true",
85-
"traefik.ingress.kubernetes.io/whitelist-source-range": allowlist
86-
})
87-
if "ssl_redirect" in kwargs:
88-
ssl_redirect = kwargs.pop("ssl_redirect")
89-
data["metadata"]["annotations"].update({
90-
"ingress.kubernetes.io/ssl-redirect": ssl_redirect
91-
})
92-
return data
93-
94-
95-
MANIFEAT_CLASSES["traefik"] = TraefikManifest
96-
97-
98-
class Ingress(Resource):
99-
100-
api_version = 'networking.k8s.io/v1'
101-
api_prefix = 'apis'
102-
short_name = 'ingress'
103-
104-
@staticmethod
105-
def manifest(api_version, ingress, ingress_class, namespace, **kwargs):
106-
return MANIFEAT_CLASSES.get(ingress_class, BaseManifest)().manifest(
107-
api_version, ingress, ingress_class, namespace, **kwargs
108-
)
109-
11054
def get(self, namespace, ingress=None, **kwargs):
11155
"""
11256
Fetch a single Ingress or a list of Ingresses
@@ -124,28 +68,28 @@ def get(self, namespace, ingress=None, **kwargs):
12468

12569
return response
12670

127-
def create(self, ingress, ingress_class, namespace, **kwargs):
71+
def create(self, namespace, ingress, **kwargs):
12872
url = self.api("/namespaces/{}/ingresses", namespace)
129-
data = self.manifest(self.api_version, ingress, ingress_class, namespace, **kwargs)
73+
data = self.manifest(ingress, **kwargs)
13074
response = self.http_post(url, json=data)
13175

132-
if not response.status_code == 201:
76+
if self.unhealthy(response.status_code):
13377
raise KubeHTTPException(response, "create Ingress {}".format(namespace))
13478

13579
return response
13680

137-
def put(self, ingress, ingress_class, namespace, version, **kwargs):
81+
def put(self, namespace, ingress, version, **kwargs):
13882
url = self.api("/namespaces/{}/ingresses/{}", namespace, ingress)
13983
kwargs["version"] = version
140-
data = self.manifest(self.api_version, ingress, ingress_class, namespace, **kwargs)
84+
data = self.manifest(ingress, **kwargs)
14185
response = self.http_put(url, json=data)
14286

14387
if self.unhealthy(response.status_code):
14488
raise KubeHTTPException(response, "put Ingress {}".format(namespace))
14589

14690
return response
14791

148-
def patch(self, ingress, namespace, data):
92+
def patch(self, namespace, ingress, data, **kwargs):
14993
url = self.api("/namespaces/{}/ingresses/{}", namespace, ingress)
15094
response = self.http_put(url, json=data)
15195

@@ -161,3 +105,157 @@ def delete(self, namespace, ingress):
161105
raise KubeHTTPException(response, 'delete Ingress "{}"', namespace)
162106

163107
return response
108+
109+
110+
class IngressFactory(Resource):
111+
112+
short_name = 'ingress'
113+
ingress_class_map = {
114+
"default": BaseIngress
115+
}
116+
117+
def __call__(self, ingress_name):
118+
ingress_cls = self.ingress_class_map.get(ingress_name, self.ingress_class_map["default"])
119+
ingress_cls.ingress_class = ingress_name
120+
return ingress_cls(self.url, self.k8s_api_verify_tls)
121+
122+
@classmethod
123+
def register(cls, ingress_name, ingress_cls):
124+
cls.ingress_class_map[ingress_name] = ingress_cls
125+
126+
127+
class NginxIngress(BaseIngress):
128+
129+
def manifest(self, ingress, **kwargs):
130+
data = BaseIngress.manifest(self, ingress, **kwargs)
131+
if "allowlist" in kwargs:
132+
allowlist = ", ".join(kwargs.pop("allowlist"))
133+
data["metadata"]["annotations"].update({
134+
"nginx.ingress.kubernetes.io/whitelist-source-range": allowlist
135+
})
136+
if "ssl_redirect" in kwargs:
137+
ssl_redirect = kwargs.pop("ssl_redirect")
138+
data["metadata"]["annotations"].update({
139+
"nginx.ingress.kubernetes.io/ssl-redirect": ssl_redirect
140+
})
141+
return data
142+
143+
144+
class TraefikIngress(BaseIngress):
145+
146+
class Middleware(Resource):
147+
abstract = True
148+
api_version = 'traefik.containo.us/v1alpha1'
149+
api_prefix = 'apis'
150+
151+
def manifest(self, name, allowlist, ssl_redirect, resource_version=None):
152+
manifest = {
153+
"apiVersion": self.api_version,
154+
"kind": "Middleware",
155+
"metadata": {
156+
"name": name
157+
},
158+
"spec": {}
159+
}
160+
if allowlist:
161+
manifest["spec"]["ipWhiteList"] = {
162+
"sourceRange": allowlist
163+
}
164+
if ssl_redirect:
165+
manifest["spec"]["redirectScheme"] = {
166+
"scheme": "https",
167+
"permanent": True
168+
}
169+
if resource_version:
170+
manifest["metadata"]["resourceVersion"] = resource_version
171+
return manifest
172+
173+
def get(self, namespace, name=None, **kwargs):
174+
"""
175+
Fetch a single Middleware or a list of Middlewares
176+
"""
177+
if name is not None:
178+
url = self.api("/namespaces/{}/middlewares/{}", namespace, name)
179+
else:
180+
url = self.api("/namespaces/{}/middlewares", namespace)
181+
return self.http_get(url, params=self.query_params(**kwargs))
182+
183+
def create(self, namespace, name, allowlist, ssl_redirect):
184+
url = self.api("/namespaces/{}/middlewares", namespace)
185+
data = self.manifest(name, allowlist, ssl_redirect)
186+
response = self.http_put(url, json=data)
187+
if self.unhealthy(response.status_code):
188+
raise KubeHTTPException(response, "create middleware {}".format(namespace))
189+
return response
190+
191+
def put(self, namespace, name, allowlist, ssl_redirect, resource_version):
192+
url = self.api("/namespaces/{}/middlewares/{}", namespace, name)
193+
data = self.manifest(allowlist, ssl_redirect, resource_version)
194+
response = self.http_put(url, json=data)
195+
if self.unhealthy(response.status_code):
196+
raise KubeHTTPException(response, "put middleware {}".format(namespace))
197+
return response
198+
199+
def create_or_put(self, namespace, name, allowlist, ssl_redirect):
200+
response = self.get(namespace, name)
201+
if response.status_code == 404:
202+
return self.create(namespace, name, allowlist, ssl_redirect)
203+
elif self.unhealthy(response.status_code):
204+
raise KubeHTTPException(response, "get middleware {}".format(namespace))
205+
resource_version = response.json()["metadata"]["resourceVersion"]
206+
return self.put(namespace, name, allowlist, ssl_redirect, resource_version)
207+
208+
def delete(self, namespace, name):
209+
url = self.api("/namespaces/{}/middlewares/{}", namespace, name)
210+
response = self.http_delete(url)
211+
if self.unhealthy(response.status_code):
212+
raise KubeHTTPException(response, 'delete middlewares "{}"', namespace)
213+
214+
return response
215+
216+
def __init__(self, url, k8s_api_verify_tls=True):
217+
super().__init__(url, k8s_api_verify_tls)
218+
self.middleware = self.Middleware(url, k8s_api_verify_tls)
219+
220+
def create(self, namespace, ingress, **kwargs):
221+
response = super().create(ingress, namespace, **kwargs)
222+
if "allowlist" in kwargs or "ssl_redirect" in kwargs:
223+
self.middleware.create_or_put(
224+
namespace, ingress,
225+
kwargs.get("allowlist"), kwargs.get("ssl_redirect")
226+
)
227+
return response
228+
229+
def put(self, namespace, ingress, version, **kwargs):
230+
response = super().put(ingress, namespace, version, **kwargs)
231+
if "allowlist" in kwargs or "ssl_redirect" in kwargs:
232+
self.middleware.create_or_put(
233+
namespace, ingress,
234+
kwargs.get("allowlist"), kwargs.get("ssl_redirect")
235+
)
236+
return response
237+
238+
def patch(self, namespace, ingress, data, **kwargs):
239+
response = super().patch(ingress, namespace, data, **kwargs)
240+
if "allowlist" in kwargs or "ssl_redirect" in kwargs:
241+
self.middleware.create_or_put(
242+
namespace, ingress,
243+
kwargs.get("allowlist"), kwargs.get("ssl_redirect")
244+
)
245+
return response
246+
247+
def delete(self, namespace, ingress):
248+
response = super().delete(namespace, ingress)
249+
if not self.unhealthy(self.middleware.get(namespace, ingress)):
250+
self.middleware.delete(namespace, ingress)
251+
return response
252+
253+
254+
class WildcardPathIngress(BaseIngress):
255+
ingress_path = "/*"
256+
257+
258+
IngressFactory.register("nginx", NginxIngress)
259+
IngressFactory.register("traefik", TraefikIngress)
260+
IngressFactory.register("gce", WildcardPathIngress)
261+
IngressFactory.register("alb", WildcardPathIngress)

0 commit comments

Comments
 (0)