Skip to content

Commit

Permalink
Merge pull request #667 from dolfinus/ingress_host
Browse files Browse the repository at this point in the history
[KubeIngressProxy] Add KubeIngressProxy.ingress_specifications
  • Loading branch information
consideRatio committed Nov 11, 2022
2 parents c6bb2a1 + 6cc6632 commit 0f22abf
Show file tree
Hide file tree
Showing 4 changed files with 660 additions and 30 deletions.
94 changes: 66 additions & 28 deletions kubespawner/objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import json
import operator
import re
from typing import List, Optional
from typing import Dict, List, Optional
from urllib.parse import urlparse

from kubernetes_asyncio.client.models import (
Expand All @@ -24,6 +24,7 @@
V1IngressRule,
V1IngressServiceBackend,
V1IngressSpec,
V1IngressTLS,
V1Lifecycle,
V1LocalObjectReference,
V1Namespace,
Expand Down Expand Up @@ -59,7 +60,7 @@
except ImportError:
from kubernetes_asyncio.client.models import V1EndpointPort as CoreV1EndpointPort

from .utils import get_k8s_model, update_k8s_model
from .utils import get_k8s_model, host_matching, update_k8s_model


def make_pod(
Expand Down Expand Up @@ -738,6 +739,7 @@ def make_ingress(
ingress_extra_labels: Optional[dict] = None,
ingress_extra_annotations: Optional[dict] = None,
ingress_class_name: Optional[str] = None,
ingress_specifications: Optional[List[Dict]] = None,
):
"""
Returns an ingress, service, endpoint object that'll work for this service
Expand All @@ -762,11 +764,12 @@ def make_ingress(
labels=common_labels,
)

if routespec.startswith('/'):
host = None
path = routespec
else:
host, path = routespec.split('/', 1)
# /myuser/myserver or https://myuser.example.com/myserver for spawned server
# /services/myservice or https://services.example.com/myservice for service
# / or https://example.com/ for hub
route_parts = urlparse(routespec)
routespec_host = route_parts.netloc or None
routespec_path = route_parts.path

target_parts = urlparse(target)
target_port = target_parts.port
Expand Down Expand Up @@ -830,32 +833,67 @@ def make_ingress(
labels=ingress_labels,
)

ingress_specifications = ingress_specifications or []

hosts = []
add_routespec_host = True
for ingress_spec in ingress_specifications:
if routespec_host and host_matching(routespec_host, ingress_spec["host"]):
# if routespec is URL like "http[s]://user.example.com"
# and ingress_specifications contains item like
# {"host": "user.example.com"} or {"host": "*.example.com"},
# prefer routespec_host over than wildcard
if add_routespec_host:
hosts.append(routespec_host)

add_routespec_host = False
elif ingress_spec["host"] not in hosts:
hosts.append(ingress_spec["host"])

if add_routespec_host and (routespec_host or not hosts):
# if routespec is URL like "http[s]://user.example.com"
# and does not match any host from ingress_specifications, create rule with routespec_host.

# if routespec is path like /base/url, and ingress_specifications is empty,
# create one ingress rule without host name.
hosts.insert(0, routespec_host)

rules = [
V1IngressRule(
host=host,
http=V1HTTPIngressRuleValue(
paths=[
V1HTTPIngressPath(
path=routespec_path,
path_type="Prefix",
backend=V1IngressBackend(
service=V1IngressServiceBackend(
name=name,
port=V1ServiceBackendPort(
number=target_port,
),
),
),
),
],
),
)
for host in hosts
]

tls = [
V1IngressTLS(hosts=[spec["host"]], secret_name=spec["tlsSecret"])
for spec in ingress_specifications
if "tlsSecret" in spec
]

ingress = V1Ingress(
kind='Ingress',
metadata=ingress_meta,
spec=V1IngressSpec(
rules=rules,
tls=tls or None,
ingress_class_name=ingress_class_name,
rules=[
V1IngressRule(
host=host,
http=V1HTTPIngressRuleValue(
paths=[
V1HTTPIngressPath(
path=path,
path_type="Prefix",
backend=V1IngressBackend(
service=V1IngressServiceBackend(
name=name,
port=V1ServiceBackendPort(
number=target_port,
),
),
),
)
]
),
)
],
),
)

Expand Down
29 changes: 28 additions & 1 deletion kubespawner/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from jupyterhub.proxy import Proxy
from jupyterhub.utils import exponential_backoff
from kubernetes_asyncio import client
from traitlets import Dict, Unicode
from traitlets import Dict, List, Unicode

from .clients import load_config, shared_client
from .objects import make_ingress
Expand Down Expand Up @@ -232,6 +232,26 @@ def _namespace_default(self):
""",
)

ingress_specifications = List(
trait=Dict,
config=True,
help="""
Specifications for ingress routes. List of dicts of the following format:
[{'host': 'host.example.domain'}]
[{'host': 'host.example.domain', 'tlsSecret': 'tlsSecretName'}]
[{'host': 'jh.{hubnamespace}.domain', 'tlsSecret': 'tlsSecretName'}]
Wildcard might not work, refer to your ingress controller documentation.
`{routespec}`, `{hubnamespace}`, and `{unescaped_routespec}` will be expanded if
found within strings of this configuration.
Names have to be are escaped to follow the [DNS label
standard](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names).
""",
)

k8s_api_ssl_ca_cert = Unicode(
"",
config=True,
Expand Down Expand Up @@ -374,6 +394,12 @@ async def add_route(self, routespec, target, data):
self.ingress_extra_annotations, routespec, data
)

ingress_specifications = self._expand_all(
self.ingress_specifications,
routespec,
data,
)

endpoint, service, ingress = make_ingress(
name=safe_name,
routespec=routespec,
Expand All @@ -383,6 +409,7 @@ async def add_route(self, routespec, target, data):
ingress_extra_labels=ingress_extra_labels,
ingress_extra_annotations=ingress_extra_annotations,
ingress_class_name=self.ingress_class_name,
ingress_specifications=ingress_specifications,
)

async def ensure_object(create_func, patch_func, body, kind):
Expand Down
16 changes: 16 additions & 0 deletions kubespawner/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,19 @@ def _get_k8s_model_attribute(model_type, field_name):
model_type.__name__, field_name
)
)


def host_matching(host: str, wildcard: str) -> bool:
# user.example.com == user.example.com
# user.example.com != wrong.example.com
# user.example.com != example.com
if not wildcard.startswith("*."):
return host == wildcard

host_parts = host.split(".")
wildcard_parts = wildcard.split(".")

# user.example.com =~ *.example.com
# user.example.com !~ *.user.example.com
# user.example.com !~ *.example
return host_parts[1:] == wildcard_parts[1:]

0 comments on commit 0f22abf

Please sign in to comment.