Skip to content

Commit c841983

Browse files
authored
Merge pull request #2006 from consideRatio/pr/file-injection
Allow extraFiles to be injected to hub / singleuser pods and automatically load config in /usr/local/etc/jupyterhub_config.d
2 parents 8b63e21 + f1ca325 commit c841983

File tree

12 files changed

+474
-15
lines changed

12 files changed

+474
-15
lines changed

dev-config.yaml

+51
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,59 @@ hub:
4848
requests:
4949
memory: 0
5050
cpu: 0
51+
extraFiles:
52+
my_config:
53+
mountPath: /usr/local/etc/jupyterhub/jupyterhub_config.d/my_config.py
54+
stringData: |
55+
with open("/tmp/created-by-extra-files-config.txt", "w") as f:
56+
f.write("hello world!")
57+
binaryData1: &binaryData1
58+
mountPath: /tmp/binaryData.txt
59+
mode: 0666
60+
binaryData: |
61+
aGVsbG8gd
62+
29ybGQhCg==
63+
binaryData2: &binaryData2
64+
mountPath: /tmp/dir1/binaryData.txt
65+
mode: 0666
66+
binaryData: aGVsbG8gd29ybGQhCg==
67+
stringData1: &stringData1
68+
mountPath: /tmp/stringData.txt
69+
mode: 0666
70+
stringData: hello world!
71+
stringData2: &stringData2
72+
mountPath: /tmp/dir1/stringData.txt
73+
mode: 0666
74+
stringData: hello world!
75+
data-yaml: &data-yaml
76+
mountPath: /etc/test/data.yaml
77+
mode: 0444
78+
data:
79+
config:
80+
map:
81+
number: 123
82+
string: "hi"
83+
list: [1, 2]
84+
data-yml: &data-yml
85+
<<: *data-yaml
86+
mountPath: /etc/test/data.yml
87+
data-json: &data-json
88+
<<: *data-yaml
89+
mountPath: /etc/test/data.json
90+
data-toml: &data-toml
91+
<<: *data-yaml
92+
mountPath: /etc/test/data.toml
5193

5294
singleuser:
95+
extraFiles:
96+
binaryData1: *binaryData1
97+
binaryData2: *binaryData2
98+
stringData1: *stringData1
99+
stringData2: *stringData2
100+
data-yaml: *data-yaml
101+
data-yml: *data-yml
102+
data-json: *data-json
103+
data-toml: *data-toml
53104
storage:
54105
type: none
55106
memory:

images/hub/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,4 @@ ARG PIP_TOOLS=
6464
RUN test -z "$PIP_TOOLS" || pip install --no-cache pip-tools==$PIP_TOOLS
6565

6666
USER ${NB_USER}
67-
CMD ["jupyterhub", "--config", "/etc/jupyterhub/jupyterhub_config.py"]
67+
CMD ["jupyterhub", "--config", "/usr/local/etc/jupyterhub/jupyterhub_config.py"]

jupyterhub/files/hub/jupyterhub_config.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ def camelCaseify(s):
235235

236236
# Configure dynamically provisioning pvc
237237
storage_type = get_config("singleuser.storage.type")
238-
239238
if storage_type == "dynamic":
240239
pvc_name_template = get_config("singleuser.storage.dynamic.pvcNameTemplate")
241240
c.KubeSpawner.pvc_name_template = pvc_name_template
@@ -280,6 +279,43 @@ def camelCaseify(s):
280279
}
281280
]
282281

282+
# Inject singleuser.extraFiles as volumes and volumeMounts with data loaded from
283+
# the dedicated k8s Secret prepared to hold the extraFiles actual content.
284+
extra_files = get_config("singleuser.extraFiles", {})
285+
if extra_files:
286+
volume = {
287+
"name": "files",
288+
}
289+
items = []
290+
for file_key, file_details in extra_files.items():
291+
# Each item is a mapping of a key in the k8s Secret to a path in this
292+
# abstract volume, the goal is to enable us to set the mode /
293+
# permissions only though so we don't change the mapping.
294+
item = {
295+
"key": file_key,
296+
"path": file_key,
297+
}
298+
if "mode" in file_details:
299+
item["mode"] = file_details["mode"]
300+
items.append(item)
301+
volume["secret"] = {
302+
"secretName": get_name("singleuser"),
303+
"items": items,
304+
}
305+
c.KubeSpawner.volumes.append(volume)
306+
307+
volume_mounts = []
308+
for file_key, file_details in extra_files.items():
309+
volume_mounts.append(
310+
{
311+
"mountPath": file_details["mountPath"],
312+
"subPath": file_key,
313+
"name": "files",
314+
}
315+
)
316+
c.KubeSpawner.volume_mounts.extend(volume_mounts)
317+
318+
# Inject extraVolumes / extraVolumeMounts
283319
c.KubeSpawner.volumes.extend(get_config("singleuser.storage.extraVolumes", []))
284320
c.KubeSpawner.volume_mounts.extend(
285321
get_config("singleuser.storage.extraVolumeMounts", [])
@@ -371,6 +407,15 @@ def camelCaseify(s):
371407
c.JupyterHub.log_level = "DEBUG"
372408
c.Spawner.debug = True
373409

410+
# load /usr/local/etc/jupyterhub/jupyterhub_config.d config files
411+
config_dir = "/usr/local/etc/jupyterhub/jupyterhub_config.d"
412+
if os.path.isdir(config_dir):
413+
for file_name in sorted(os.listdir(config_dir)):
414+
print(f"Loading {config_dir} config: {file_name}")
415+
with open(f"{config_dir}/{file_name}") as f:
416+
file_content = f.read()
417+
# compiling makes debugging easier: https://stackoverflow.com/a/437857
418+
exec(compile(source=file_content, filename=file_name, mode="exec"))
374419

375420
# load potentially seeded secrets
376421
c.JupyterHub.proxy_auth_token = get_secret_value("JupyterHub.proxy_auth_token")

jupyterhub/files/hub/z2jh.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def _load_config():
1515
"""Load the Helm chart configuration used to render the Helm templates of
1616
the chart from a mounted k8s Secret."""
1717

18-
path = f"/etc/jupyterhub/secret/values.yaml"
18+
path = f"/usr/local/etc/jupyterhub/secret/values.yaml"
1919
if os.path.exists(path):
2020
print(f"Loading {path}")
2121
with open(path) as f:
@@ -28,7 +28,7 @@ def _load_config():
2828
def _get_config_value(key):
2929
"""Load value from the k8s ConfigMap given a key."""
3030

31-
path = f"/etc/jupyterhub/config/{key}"
31+
path = f"/usr/local/etc/jupyterhub/config/{key}"
3232
if os.path.exists(path):
3333
with open(path) as f:
3434
return f.read()
@@ -40,7 +40,7 @@ def _get_config_value(key):
4040
def get_secret_value(key):
4141
"""Load value from the k8s Secret given a key."""
4242

43-
path = f"/etc/jupyterhub/secret/{key}"
43+
path = f"/usr/local/etc/jupyterhub/secret/{key}"
4444
if os.path.exists(path):
4545
with open(path) as f:
4646
return f.read()

jupyterhub/schema.yaml

+135-5
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,135 @@ properties:
212212
the `--values` or `-f` flag. During merging, lists are replaced while
213213
dictionaries are updated.
214214
```
215+
extraFiles: &extraFiles
216+
type: string
217+
description: |
218+
A dictionary with extra files to be injected into the pod's container
219+
on startup. This can for example be used to inject: configuration
220+
files, custom user interface templates, images, and more.
221+
222+
```yaml
223+
hub:
224+
extraFiles:
225+
# The file key is just a reference that doesn't influence the
226+
# actual file name.
227+
<file key>:
228+
# mountPath is required and must be the absolute file path.
229+
mountPath: <full file path>
230+
231+
# Choose one out of the three ways to represent the actual file
232+
# content: data, stringData, or binaryData.
233+
#
234+
# data should be set to a mapping (dictionary). It will in the
235+
# end be rendered to either YAML, JSON, or TOML based on the
236+
# filename extension that are required to be either .yaml, .yml,
237+
# .json, or .toml.
238+
#
239+
# If your content is YAML, JSON, or TOML, it can make sense to
240+
# use data to represent it over stringData as data can be merged
241+
# instead of replaced if set partially from separate Helm
242+
# configuration files.
243+
#
244+
# Both stringData and binaryData should be set to a string
245+
# representing the content, where binaryData should be the
246+
# base64 encoding of the actual file content.
247+
#
248+
data:
249+
config:
250+
map:
251+
number: 123
252+
string: "hi"
253+
list:
254+
- 1
255+
- 2
256+
stringData: |
257+
hello world!
258+
binaryData: aGVsbG8gd29ybGQhCg==
259+
260+
# mode is by default 0644 and you can optionally override it
261+
# either by octal notation (example: 0400) or decimal notation
262+
# (example: 256).
263+
mode: <file system permissions>
264+
```
265+
266+
**Using --set-file**
267+
268+
To avoid embedding entire files in the Helm chart configuration, you
269+
can use the `--set-file` flag during `helm upgrade` to set the
270+
stringData or binaryData field.
271+
272+
```yaml
273+
hub:
274+
extraFiles:
275+
my_image:
276+
mountPath: /usr/local/share/jupyterhub/static/my_image.png
277+
278+
# Files in /usr/local/etc/jupyterhub/jupyterhub_config.d are
279+
# automatically loaded in alphabetical order of the final file
280+
# name when JupyterHub starts.
281+
my_config:
282+
mountPath: /usr/local/etc/jupyterhub/jupyterhub_config.d/my_jupyterhub_config.py
283+
```
284+
285+
```bash
286+
# --set-file expects a text based file, so you need to base64 encode
287+
# it manually first.
288+
base64 my_image.png > my_image.png.b64
289+
290+
helm upgrade <...> \
291+
--set-file hub.extraFiles.my_image.binaryData=./my_image.png.b64 \
292+
--set-file hub.extraFiles.my_config.stringData=./my_jupyterhub_config.py
293+
```
294+
295+
**Common uses**
296+
297+
1. **JupyterHub template customization**
298+
299+
You can replace the default JupyterHub user interface templates in
300+
the hub pod by injecting new ones to
301+
`/usr/local/share/jupyterhub/templates`. These can in turn
302+
reference custom images injected to
303+
`/usr/local/share/jupyterhub/static`.
304+
305+
1. **JupyterHub standalone file config**
306+
307+
Instead of embedding JupyterHub python configuration as a string
308+
within a YAML file through
309+
[`hub.extraConfig`](schema_hub.extraConfig), you can inject a
310+
standalone .py file into
311+
`/usr/local/etc/jupyterhub/jupyterhub_config.d` that is
312+
automatically loaded.
313+
314+
1. **Flexible configuration**
315+
316+
By injecting files, you don't have to embed them in a docker image
317+
that you have to rebuild.
318+
319+
If your configuration file is a YAML/JSON/TOML file, you can also
320+
use `data` instead of `stringData` which allow you to set various
321+
configuration in separate Helm config files. This can be useful to
322+
help dependent charts override only some configuration part of the
323+
file, or to allow for the configuration be set through multiple
324+
Helm configuration files.
325+
326+
**Limitations**
327+
328+
1. File size
329+
330+
The files in `hub.extraFiles` and `singleuser.extraFiles` are
331+
respectively stored in their own k8s Secret resource. As k8s
332+
Secret's are limited, typically to 1MB, you will be limited to a
333+
total file size of less than 1MB as there is also base64 encoding
334+
that takes place reducing available capacity to 75%.
335+
336+
2. File updates
337+
338+
The files that are mounted are only set during container startup.
339+
This is [because we use
340+
`subPath`](https://kubernetes.io/docs/concepts/storage/volumes/#secret)
341+
as is required to avoid replacing the content of the entire
342+
directory we mount in.
343+
215344
baseUrl:
216345
type: string
217346
description: |
@@ -243,10 +372,10 @@ properties:
243372
244373
```{warning}
245374
By replacing the entire configuration file, which is mounted to
246-
`/etc/jupyterhub/jupyterhub_config.py` by the Helm chart, instead of
247-
appending to it with `hub.extraConfig`, you expose your deployment for
248-
issues stemming from getting out of sync with the Helm chart's config
249-
file.
375+
`/usr/local/etc/jupyterhub/jupyterhub_config.py` by the Helm chart,
376+
instead of appending to it with `hub.extraConfig`, you expose your
377+
deployment for issues stemming from getting out of sync with the Helm
378+
chart's config file.
250379
251380
These kind of issues will be significantly harder to debug and
252381
diagnose, and can due to this could cause a lot of time expenditure
@@ -268,7 +397,7 @@ properties:
268397
args:
269398
- "jupyterhub"
270399
- "--config"
271-
- "/etc/jupyterhub/jupyterhub_config.py"
400+
- "/usr/local/etc/jupyterhub/jupyterhub_config.py"
272401
- "--debug"
273402
- "--upgrade-db"
274403
```
@@ -1154,6 +1283,7 @@ properties:
11541283
description: |
11551284
Deprecated and no longer does anything. Use the user-scheduler instead
11561285
in order to accomplish a good packing of the user pods.
1286+
extraFiles: *extraFiles
11571287
extraEnv:
11581288
type: object
11591289
description: |

0 commit comments

Comments
 (0)