Skip to content

Commit

Permalink
Merge branch 'master' into feature/shahn3_pvcDeletion
Browse files Browse the repository at this point in the history
  • Loading branch information
nsshah1288 authored May 5, 2021
2 parents 84688f9 + c82b354 commit 7cc6933
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 19 deletions.
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
include LICENSE
include README.md
3 changes: 3 additions & 0 deletions kubespawner/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def shared_client(ClientType, *args, **kwargs):
client = _client_cache[cache_key]()

if client is None:
# Kubernetes client configuration is handled globally
# in kubernetes.py and is already called in spawner.py
# or proxy.py prior to a shared_client being instantiated
Client = getattr(kubernetes.client, ClientType)
client = Client(*args, **kwargs)
# cache weakref so that clients can be garbage collected
Expand Down
49 changes: 49 additions & 0 deletions kubespawner/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from concurrent.futures import ThreadPoolExecutor

import escapism
import kubernetes.config
from jupyterhub.proxy import Proxy
from jupyterhub.utils import exponential_backoff
from kubernetes import client
Expand Down Expand Up @@ -79,6 +80,32 @@ def _namespace_default(self):
""",
)

k8s_api_ssl_ca_cert = Unicode(
"",
config=True,
help="""
Location (absolute filepath) for CA certs of the k8s API server.
Typically this is unnecessary, CA certs are picked up by
config.load_incluster_config() or config.load_kube_config.
In rare non-standard cases, such as using custom intermediate CA
for your cluster, you may need to mount root CA's elsewhere in
your Pod/Container and point this variable to that filepath
""",
)

k8s_api_host = Unicode(
"",
config=True,
help="""
Full host name of the k8s API server ("https://hostname:port").
Typically this is unnecessary, the hostname is picked up by
config.load_incluster_config() or config.load_kube_config.
""",
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand All @@ -101,9 +128,31 @@ def __init__(self, *args, **kwargs):
parent=self, namespace=self.namespace, labels=labels
)

# Global configuration before reflector.py code runs
self._set_k8s_client_configuration()
self.core_api = shared_client('CoreV1Api')
self.extension_api = shared_client('ExtensionsV1beta1Api')

def _set_k8s_client_configuration(self):
# The actual (singleton) Kubernetes client will be created
# in clients.py shared_client but the configuration
# for token / ca_cert / k8s api host is set globally
# in kubernetes.py syntax. It is being set here
# and this method called prior to shared_client
# for readability / coupling with traitlets values
try:
kubernetes.config.load_incluster_config()
except kubernetes.config.ConfigException:
kubernetes.config.load_kube_config()
if self.k8s_api_ssl_ca_cert:
global_conf = client.Configuration.get_default_copy()
global_conf.ssl_ca_cert = self.k8s_api_ssl_ca_cert
client.Configuration.set_default(global_conf)
if self.k8s_api_host:
global_conf = client.Configuration.get_default_copy()
global_conf.host = self.k8s_api_host
client.Configuration.set_default(global_conf)

@run_on_executor
def asynchronize(self, method, *args, **kwargs):
return method(*args, **kwargs)
Expand Down
7 changes: 1 addition & 6 deletions kubespawner/reflector.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,7 @@ class ResourceReflector(LoggingConfigurable):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Load kubernetes config here, since this is a Singleton and
# so this __init__ will be run way before anything else gets run.
try:
config.load_incluster_config()
except config.ConfigException:
config.load_kube_config()
# client configuration for kubernetes has already taken place
self.api = shared_client(self.api_group_name)

# FIXME: Protect against malicious labels?
Expand Down
76 changes: 63 additions & 13 deletions kubespawner/spawner.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from urllib.parse import urlparse

import escapism
import kubernetes.config
from jinja2 import BaseLoader
from jinja2 import Environment
from jupyterhub.spawner import Spawner
Expand Down Expand Up @@ -198,14 +199,17 @@ def __init__(self, *args, **kwargs):
max_workers=self.k8s_api_threadpool_workers
)

# Set global kubernetes client configurations
# before reflector.py code runs
self._set_k8s_client_configuration()
self.api = shared_client('CoreV1Api')

# This will start watching in __init__, so it'll start the first
# time any spawner object is created. Not ideal but works!
self._start_watching_pods()
if self.events_enabled:
self._start_watching_events()

self.api = shared_client('CoreV1Api')

# runs during both test and normal execution
self.pod_name = self._expand_user_properties(self.pod_name_template)
self.dns_name = self.dns_name_template.format(
Expand All @@ -220,6 +224,52 @@ def __init__(self, *args, **kwargs):
# Our default port is 8888
self.port = 8888

def _set_k8s_client_configuration(self):
# The actual (singleton) Kubernetes client will be created
# in clients.py shared_client but the configuration
# for token / ca_cert / k8s api host is set globally
# in kubernetes.py syntax. It is being set here
# and this method called prior to shared_client
# for readability / coupling with traitlets values
try:
kubernetes.config.load_incluster_config()
except kubernetes.config.ConfigException:
kubernetes.config.load_kube_config()
if self.k8s_api_ssl_ca_cert:
global_conf = client.Configuration.get_default_copy()
global_conf.ssl_ca_cert = self.k8s_api_ssl_ca_cert
client.Configuration.set_default(global_conf)
if self.k8s_api_host:
global_conf = client.Configuration.get_default_copy()
global_conf.host = self.k8s_api_host
client.Configuration.set_default(global_conf)

k8s_api_ssl_ca_cert = Unicode(
"",
config=True,
help="""
Location (absolute filepath) for CA certs of the k8s API server.
Typically this is unnecessary, CA certs are picked up by
config.load_incluster_config() or config.load_kube_config.
In rare non-standard cases, such as using custom intermediate CA
for your cluster, you may need to mount root CA's elsewhere in
your Pod/Container and point this variable to that filepath
""",
)

k8s_api_host = Unicode(
"",
config=True,
help="""
Full host name of the k8s API server ("https://hostname:port").
Typically this is unnecessary, the hostname is picked up by
config.load_incluster_config() or config.load_kube_config.
""",
)

k8s_api_threadpool_workers = Integer(
# Set this explicitly, since this is the default in Python 3.5+
# but not in 3.4
Expand Down Expand Up @@ -353,18 +403,14 @@ def _namespace_default(self):
minlen=0,
config=True,
help="""
The command used for starting the single-user server.
Provide either a string or a list containing the path to the startup script command. Extra arguments,
other than this path, should be provided via `args`.
The command used to start the single-user server.
This is usually set if you want to start the single-user server in a different python
environment (with virtualenv/conda) than JupyterHub itself.
Either
- a string containing a single command or path to a startup script
- a list of the command and arguments
- `None` (default) to use the Docker image's `CMD`
Some spawners allow shell-style expansion here, allowing you to use environment variables.
Most, including the default, do not. Consult the documentation for your spawner to verify!
If set to `None`, Kubernetes will start the `CMD` that is specified in the Docker image being started.
If `cmd` is set, it will be augmented with `spawner.get_args(). This will override the `CMD` specified in the Docker image.
""",
)

Expand Down Expand Up @@ -2602,7 +2648,11 @@ def _options_form_default(self):
else:
return self._render_options_form(self.profile_list)

def options_from_form(self, formdata):
@default('options_from_form')
def _options_from_form_default(self):
return self._options_from_form

def _options_from_form(self, formdata):
"""get the option selected by the user on the form
This only constructs the user_options dict,
Expand Down

0 comments on commit 7cc6933

Please sign in to comment.