Skip to content
This repository was archived by the owner on Jun 16, 2021. It is now read-only.

Commit ca8c176

Browse files
committed
Add launcher for k8s to this repo (from remote-kernel-provider)
Since the kubernetes launcher contains code specific for kubernetes only and already has synergy with the lifecycle manager, it seems more appropriate to have this "pod launcher" code in this repo. Pure kernel launchers (with listeners) will remain in base provider.
1 parent ef37c94 commit ca8c176

File tree

6 files changed

+196
-7
lines changed

6 files changed

+196
-7
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,6 @@ ENV/
103103

104104
# mypy
105105
.mypy_cache/
106+
107+
# mac - desktop services meta-files
108+
.DS_Store

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ include LICENSE
44
include README.rst
55

66
recursive-include kubernetes_kernel_provider/kernelspecs *
7+
recursive-include kubernetes_kernel_provider/pod-launcher *
78
recursive-include kubernetes_kernel_provider/tests *
89

910
recursive-exclude * __pycache__

kubernetes_kernel_provider/kernelspecapp.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,6 @@ class K8SKP_SpecInstaller(JupyterApp):
6666
def _kernel_spec_manager_default(self):
6767
return KernelSpecManager(kernel_file=KubernetesKernelProvider.kernel_file)
6868

69-
source_dir = Unicode()
70-
staging_dir = Unicode()
7169
template_dir = Unicode()
7270

7371
kernel_name = Unicode(DEFAULT_KERNEL_NAMES[DEFAULT_LANGUAGE], config=True,
@@ -152,16 +150,21 @@ def start(self):
152150
self._validate_parameters()
153151

154152
# create staging dir
155-
self.staging_dir = spec_utils.create_staging_directory()
153+
staging_dir = spec_utils.create_staging_directory()
156154

157155
# copy files from installed area to staging dir
158-
self.source_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'kernelspecs', self.template_dir))
159-
dir_util.copy_tree(src=self.source_dir, dst=self.staging_dir)
160-
spec_utils.copy_kernelspec_files(self.staging_dir, launcher_type='kubernetes',
156+
source_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'kernelspecs', self.template_dir))
157+
dir_util.copy_tree(src=source_dir, dst=staging_dir)
158+
159+
source_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'pod-launcher'))
160+
dir_util.copy_tree(src=source_dir, dst=staging_dir)
161+
162+
# copy appropriate resource files
163+
spec_utils.copy_kernelspec_files(staging_dir, launcher_type=None,
161164
resource_type=TENSORFLOW if self.tensorflow else self.language)
162165
# install to destination
163166
self.log.info("Installing Kubernetes Kernel Provider kernel specification for '{}'".format(self.display_name))
164-
install_dir = self.kernel_spec_manager.install_kernel_spec(self.staging_dir,
167+
install_dir = self.kernel_spec_manager.install_kernel_spec(staging_dir,
165168
kernel_name=self.kernel_name,
166169
user=self.user,
167170
prefix=self.prefix)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# This file defines the Kubernetes objects necessary for Enterprise Gateway kernels to run witihin Kubernetes.
2+
# Substitution parameters are processed by the launch_kubernetes.py code located in the
3+
# same directory. Some values are factory values, while others (typically prefixed with 'kernel_') can be
4+
# provided by the client.
5+
#
6+
# This file can be customized as needed. No changes are required to launch_kubernetes.py provided kernel_
7+
# values are used - which be automatically set from corresponding KERNEL_ env values. Updates will be required
8+
# to launch_kubernetes.py if new document sections (i.e., new k8s 'kind' objects) are introduced.
9+
#
10+
apiVersion: v1
11+
kind: Pod
12+
metadata:
13+
name: "{{ kernel_pod_name }}"
14+
namespace: "{{ kernel_namespace }}"
15+
labels:
16+
kernel_id: "{{ kernel_id }}"
17+
app: enterprise-gateway
18+
component: kernel
19+
spec:
20+
restartPolicy: Never
21+
serviceAccountName: "{{ kernel_service_account_name }}"
22+
# NOTE: that using runAsGroup requires that feature-gate RunAsGroup be enabled.
23+
# WARNING: Only using runAsUser w/o runAsGroup or NOT enabling the RunAsGroup feature-gate
24+
# will result in the new kernel pod's effective group of 0 (root)! although the user will
25+
# correspond to the runAsUser value. As a result, BOTH should be uncommented AND the feature-gate
26+
# should be enabled to ensure expected behavior. In addition, 'fsGroup: 100' is recommended so
27+
# that /home/jovyan can be written to via the 'users' group (gid: 100) irrespective of the
28+
# "kernel_uid" and "kernel_gid" values.
29+
{% if kernel_uid is defined or kernel_gid is defined %}
30+
securityContext:
31+
{% if kernel_uid is defined %}
32+
runAsUser: {{ kernel_uid | int }}
33+
{% endif %}
34+
{% if kernel_gid is defined %}
35+
runAsGroup: {{ kernel_gid | int }}
36+
{% endif %}
37+
fsGroup: 100
38+
{% endif %}
39+
containers:
40+
- env:
41+
- name: EG_RESPONSE_ADDRESS
42+
value: "{{ eg_response_address }}"
43+
- name: KERNEL_LANGUAGE
44+
value: "{{ kernel_language }}"
45+
- name: KERNEL_SPARK_CONTEXT_INIT_MODE
46+
value: "{{ kernel_spark_context_init_mode }}"
47+
- name: KERNEL_NAME
48+
value: "{{ kernel_name }}"
49+
- name: KERNEL_USERNAME
50+
value: "{{ kernel_username }}"
51+
- name: KERNEL_ID
52+
value: "{{ kernel_id }}"
53+
- name: KERNEL_NAMESPACE
54+
value: "{{ kernel_namespace }}"
55+
image: "{{ kernel_image }}"
56+
name: "{{ kernel_pod_name }}"
57+
{% if kernel_working_dir is defined %}
58+
workingDir: "{{ kernel_working_dir }}"
59+
{% endif %}
60+
{% if kernel_volume_mounts is defined %}
61+
volumeMounts:
62+
{% for volume_mount in kernel_volume_mounts %}
63+
- {{ volume_mount }}
64+
{% endfor %}
65+
{% endif %}
66+
{% if kernel_volumes is defined %}
67+
volumes:
68+
{% for volume in kernel_volumes %}
69+
- {{ volume }}
70+
{% endfor %}
71+
{% endif %}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import os
2+
import sys
3+
import yaml
4+
import argparse
5+
from kubernetes import client, config
6+
import urllib3
7+
8+
from jinja2 import FileSystemLoader, Environment
9+
10+
urllib3.disable_warnings()
11+
12+
KERNEL_POD_TEMPLATE_PATH = '/kernel-pod.yaml.j2'
13+
14+
15+
def generate_kernel_pod_yaml(keywords):
16+
"""Return the kubernetes pod spec as a yaml string.
17+
18+
- load jinja2 template from this file directory.
19+
- substitute template variables with keywords items.
20+
"""
21+
j_env = Environment(loader=FileSystemLoader(os.path.dirname(__file__)), trim_blocks=True, lstrip_blocks=True)
22+
# jinja2 template substitutes template variables with None though keywords doesn't contain corresponding item.
23+
# Therefore, no need to check if any are left unsubstituted; Kubernetes API server will validate the pod spec.
24+
k8s_yaml = j_env.get_template(KERNEL_POD_TEMPLATE_PATH).render(**keywords)
25+
26+
return k8s_yaml
27+
28+
29+
def launch_kubernetes_kernel(kernel_id, response_addr, spark_context_init_mode):
30+
# Launches a containerized kernel as a kubernetes pod.
31+
32+
config.load_incluster_config()
33+
34+
# Capture keywords and their values.
35+
keywords = dict()
36+
37+
# Factory values...
38+
# Since jupyter lower cases the kernel directory as the kernel-name, we need to capture its case-sensitive
39+
# value since this is used to locate the kernel launch script within the image.
40+
keywords['kernel_name'] = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
41+
keywords['kernel_id'] = kernel_id
42+
keywords['eg_response_address'] = response_addr
43+
keywords['kernel_spark_context_init_mode'] = spark_context_init_mode
44+
45+
# Walk env variables looking for names prefixed with KERNEL_. When found, set corresponding keyword value
46+
# with name in lower case.
47+
for name, value in os.environ.items():
48+
if name.startswith('KERNEL_'):
49+
keywords[name.lower()] = yaml.safe_load(value)
50+
51+
# Substitute all template variable (wrapped with {{ }}) and generate `yaml` string.
52+
k8s_yaml = generate_kernel_pod_yaml(keywords)
53+
54+
# For each k8s object (kind), call the appropriate API method. Too bad there isn't a method
55+
# that can take a set of objects.
56+
#
57+
# Creation for additional kinds of k8s objects can be added below. Refer to
58+
# https://github.com/kubernetes-client/python for API signatures. Other examples can be found in
59+
# https://github.com/jupyter-incubator/enterprise_gateway/blob/master/enterprise_gateway/services/processproxies/k8s.py
60+
#
61+
kernel_namespace = keywords['kernel_namespace']
62+
k8s_objs = yaml.safe_load_all(k8s_yaml)
63+
for k8s_obj in k8s_objs:
64+
if k8s_obj.get('kind'):
65+
if k8s_obj['kind'] == 'Pod':
66+
# print("{}".format(k8s_obj)) # useful for debug
67+
client.CoreV1Api(client.ApiClient()).create_namespaced_pod(body=k8s_obj, namespace=kernel_namespace)
68+
elif k8s_obj['kind'] == 'Secret':
69+
client.CoreV1Api(client.ApiClient()).create_namespaced_secret(body=k8s_obj, namespace=kernel_namespace)
70+
elif k8s_obj['kind'] == 'PersistentVolumeClaim':
71+
client.CoreV1Api(client.ApiClient()).create_namespaced_persistent_volume_claim(
72+
body=k8s_obj, namespace=kernel_namespace)
73+
elif k8s_obj['kind'] == 'PersistentVolume':
74+
client.CoreV1Api(client.ApiClient()).create_persistent_volume(body=k8s_obj)
75+
else:
76+
sys.exit("ERROR - Unhandled Kubernetes object kind '{}' found in yaml file - "
77+
"kernel launch terminating!".format(k8s_obj['kind']))
78+
else:
79+
sys.exit("ERROR - Unknown Kubernetes object '{}' found in yaml file - kernel launch terminating!".
80+
format(k8s_obj))
81+
82+
83+
if __name__ == '__main__':
84+
"""
85+
Usage: launch_kubernetes_kernel
86+
[--RemoteProcessProxy.kernel-id <kernel_id>]
87+
[--RemoteProcessProxy.response-address <response_addr>]
88+
[--RemoteProcessProxy.spark-context-initialization-mode <mode>]
89+
"""
90+
91+
parser = argparse.ArgumentParser()
92+
parser.add_argument('--RemoteProcessProxy.kernel-id', dest='kernel_id', nargs='?',
93+
help='Indicates the id associated with the launched kernel.')
94+
parser.add_argument('--RemoteProcessProxy.response-address', dest='response_address', nargs='?',
95+
metavar='<ip>:<port>', help='Connection address (<ip>:<port>) for returning connection file')
96+
parser.add_argument('--RemoteProcessProxy.spark-context-initialization-mode', dest='spark_context_init_mode',
97+
nargs='?', help='Indicates whether or how a spark context should be created',
98+
default='none')
99+
100+
arguments = vars(parser.parse_args())
101+
kernel_id = arguments['kernel_id']
102+
response_addr = arguments['response_address']
103+
spark_context_init_mode = arguments['spark_context_init_mode']
104+
105+
launch_kubernetes_kernel(kernel_id, response_addr, spark_context_init_mode)

kubernetes_kernel_provider/tests/test_kernelspec_app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ def test_create_python_kernelspec(script_runner, mock_kernels_dir):
107107
assert os.path.isdir(os.path.join(mock_kernels_dir, 'kernels', 'my_python_kernel'))
108108
assert os.path.isfile(os.path.join(mock_kernels_dir, 'kernels', 'my_python_kernel', 'k8skp_kernel.json'))
109109

110+
assert os.path.isdir(os.path.join(mock_kernels_dir, 'kernels', 'my_python_kernel', 'scripts'))
111+
assert os.path.isfile(os.path.join(mock_kernels_dir, 'kernels', 'my_python_kernel', 'scripts',
112+
'launch_kubernetes.py'))
113+
assert os.path.isfile(os.path.join(mock_kernels_dir, 'kernels', 'my_python_kernel', 'scripts',
114+
'kernel-pod.yaml.j2'))
115+
110116
with open(os.path.join(mock_kernels_dir, 'kernels', 'my_python_kernel', 'k8skp_kernel.json'), "r") as fd:
111117
kernel_json = json.load(fd)
112118
assert kernel_json["display_name"] == 'My Python Kernel'

0 commit comments

Comments
 (0)