Skip to content

Commit 23949e5

Browse files
Merge pull request #3690 from sunu/volume-volumemounts-as-dict
Support dict values for volumes, volume_mounts and other list based configs that support dict values
2 parents 06db6c3 + 9fca3ce commit 23949e5

File tree

9 files changed

+113
-38
lines changed

9 files changed

+113
-38
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ only rebuild images if their dependent files in their respective directories or
214214
1. Use `helm` to upgrade (or install) your local JupyterHub Helm chart.
215215

216216
```shell
217-
helm upgrade --install jupyterhub ./jupyterhub --cleanup-on-fail --values dev-config.yaml
217+
helm upgrade --install jupyterhub ./jupyterhub --cleanup-on-fail --values dev-config.yaml --values dev-config-local-chart-extra-config.yaml
218218
```
219219

220220
Note that `--cleanup-on-fail` is a very good practice to avoid `<resource name> already exist` errors in future upgrades following a failed upgrade.

dev-config-local-chart-extra-config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,14 @@
88
# include this config and then pass --set hub.some-option=null to null it out
99
# when it must not be passed, but that still triggers schema validation errors.
1010
#
11+
12+
singleuser:
13+
storage:
14+
extraVolumes:
15+
test-volume:
16+
name: test-volume
17+
emptyDir: {}
18+
extraVolumeMounts:
19+
test-volume-mount:
20+
name: test-volume
21+
mountPath: /test-volume

docs/source/jupyterhub/customizing/user-resources.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,15 @@ The following configuration will increase the SHM allocation by mounting a
118118
singleuser:
119119
storage:
120120
extraVolumes:
121-
- name: shm-volume
121+
# Arbitrary key for identity & ordering
122+
1-shm-volume:
123+
name: shm-volume
122124
emptyDir:
123125
medium: Memory
124126
extraVolumeMounts:
125-
- name: shm-volume
127+
# Arbitrary key for identity & ordering
128+
1-shm-volume:
129+
name: shm-volume
126130
mountPath: /dev/shm
127131
```
128132

docs/source/jupyterhub/customizing/user-storage.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,11 +235,15 @@ pods:
235235
singleuser:
236236
storage:
237237
extraVolumes:
238-
- name: jupyterhub-shared
238+
# Arbitrary key for identity & ordering
239+
1-jupyterhub-shared:
240+
name: jupyterhub-shared
239241
persistentVolumeClaim:
240242
claimName: jupyterhub-shared-volume
241243
extraVolumeMounts:
242-
- name: jupyterhub-shared
244+
# Arbitrary key for identity & ordering
245+
1-jupyterhub-shared:
246+
name: jupyterhub-shared
243247
mountPath: /home/shared
244248
```
245249

jupyterhub/files/hub/jupyterhub_config.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,9 @@ def camelCaseify(s):
252252
if tolerations:
253253
c.KubeSpawner.tolerations = tolerations
254254

255+
volumes = {}
256+
volume_mounts = {}
257+
255258
# Configure dynamically provisioning pvc
256259
storage_type = get_config("singleuser.storage.type")
257260
if storage_type == "dynamic":
@@ -273,32 +276,35 @@ def camelCaseify(s):
273276
)
274277

275278
# Add volumes to singleuser pods
276-
c.KubeSpawner.volumes = [
277-
{
279+
volumes = {
280+
volume_name_template: {
278281
"name": volume_name_template,
279282
"persistentVolumeClaim": {"claimName": "{pvc_name}"},
280283
}
281-
]
282-
c.KubeSpawner.volume_mounts = [
283-
{
284+
}
285+
volume_mounts = {
286+
volume_name_template: {
284287
"mountPath": get_config("singleuser.storage.homeMountPath"),
285288
"name": volume_name_template,
286289
"subPath": get_config("singleuser.storage.dynamic.subPath"),
287290
}
288-
]
291+
}
289292
elif storage_type == "static":
290293
pvc_claim_name = get_config("singleuser.storage.static.pvcName")
291-
c.KubeSpawner.volumes = [
292-
{"name": "home", "persistentVolumeClaim": {"claimName": pvc_claim_name}}
293-
]
294+
volumes = {
295+
"home": {
296+
"name": "home",
297+
"persistentVolumeClaim": {"claimName": pvc_claim_name},
298+
}
299+
}
294300

295-
c.KubeSpawner.volume_mounts = [
296-
{
301+
volume_mounts = {
302+
"home": {
297303
"mountPath": get_config("singleuser.storage.homeMountPath"),
298304
"name": "home",
299305
"subPath": get_config("singleuser.storage.static.subPath"),
300306
}
301-
]
307+
}
302308

303309
# Inject singleuser.extraFiles as volumes and volumeMounts with data loaded from
304310
# the dedicated k8s Secret prepared to hold the extraFiles actual content.
@@ -323,24 +329,37 @@ def camelCaseify(s):
323329
"secretName": get_name("singleuser"),
324330
"items": items,
325331
}
326-
c.KubeSpawner.volumes.append(volume)
332+
volumes[volume["name"]] = volume
327333

328-
volume_mounts = []
329-
for file_key, file_details in extra_files.items():
330-
volume_mounts.append(
331-
{
332-
"mountPath": file_details["mountPath"],
333-
"subPath": file_key,
334-
"name": "files",
335-
}
336-
)
337-
c.KubeSpawner.volume_mounts.extend(volume_mounts)
334+
for idx, (file_key, file_details) in enumerate(extra_files.items()):
335+
volume_mount = {
336+
"mountPath": file_details["mountPath"],
337+
"subPath": file_key,
338+
"name": "files",
339+
}
340+
volume_mounts[f"{idx}-files"] = volume_mount
338341

339342
# Inject extraVolumes / extraVolumeMounts
340-
c.KubeSpawner.volumes.extend(get_config("singleuser.storage.extraVolumes", []))
341-
c.KubeSpawner.volume_mounts.extend(
342-
get_config("singleuser.storage.extraVolumeMounts", [])
343-
)
343+
extra_volumes = get_config("singleuser.storage.extraVolumes", default={})
344+
if isinstance(extra_volumes, dict):
345+
volumes.update(extra_volumes)
346+
elif isinstance(extra_volumes, list):
347+
for volume in extra_volumes:
348+
volumes[volume["name"]] = volume
349+
350+
extra_volume_mounts = get_config("singleuser.storage.extraVolumeMounts", default={})
351+
if isinstance(extra_volume_mounts, dict):
352+
volume_mounts.update(extra_volume_mounts)
353+
elif isinstance(extra_volume_mounts, list):
354+
# If extraVolumeMounts is a list, we need to add them to the volume_mounts
355+
# dictionary with a unique key.
356+
# Since volume mount's name is not guaranteed to be unique, we use the index
357+
# as part of the key.
358+
for idx, volume_mount in enumerate(extra_volume_mounts):
359+
volume_mounts[f"{idx}-{volume_mount['name']}"] = volume_mount
360+
361+
c.KubeSpawner.volumes = volumes
362+
c.KubeSpawner.volume_mounts = volume_mounts
344363

345364
c.JupyterHub.services = []
346365
c.JupyterHub.load_roles = []

jupyterhub/values.schema.yaml

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2448,8 +2448,29 @@ properties:
24482448
Configures `KubeSpawner.storage_extra_labels`. Note that these
24492449
labels are set on the PVC during creation only and won't be
24502450
updated after creation.
2451-
extraVolumeMounts: *extraVolumeMounts-spec
2452-
extraVolumes: *extraVolumes-spec
2451+
extraVolumeMounts:
2452+
type: [object, array]
2453+
description: |
2454+
Injects extra volume mounts into `KubeSpawner.volume_mounts` dictionary.
2455+
Can be a dictionary or an array.
2456+
If it's an array, each item must be a volume mount configuration in k8s
2457+
native syntax. A combination of the volume name and its index is used as the key
2458+
in `KubeSpawner.volume_mounts` dictionary and the value is the volume mount
2459+
configuration.
2460+
If `extraVolumeMounts` is defined as a dictionary, the keys of the dictionary
2461+
can be any descriptive name for the volume mount and the value is the volume mount
2462+
configuration in k8s native syntax.
2463+
extraVolumes:
2464+
type: [object, array]
2465+
description: |
2466+
Injects extra volumes into `KubeSpawner.volumes` dictionary. Can be a dictionary
2467+
or an array.
2468+
If it's an array, each item must be volume configuration in k8s native
2469+
syntax. The name of the volume is used as the key in `KubeSpawner.volumes`
2470+
dictionary while the value is the volume configuration.
2471+
If `extraVolumes` is defined as a dictionary, the keys of the dictionary
2472+
can be any descriptive name for the volume and the value must be the volume
2473+
configuration in k8s native syntax.
24532474
homeMountPath:
24542475
type: string
24552476
description: |

jupyterhub/values.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,8 +409,8 @@ singleuser:
409409
storage:
410410
type: dynamic
411411
extraLabels: {}
412-
extraVolumes: []
413-
extraVolumeMounts: []
412+
extraVolumes: {}
413+
extraVolumeMounts: {}
414414
static:
415415
pvcName:
416416
subPath: "{username}"

tests/test_spawn.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ def test_spawn_basic(
129129
assert (
130130
c.returncode == 0
131131
), "The singleuser.extraFiles configuration doesn't seem to have been honored!"
132+
133+
# check the extra volume and volume mount exists
134+
c = subprocess.run(
135+
[
136+
"kubectl",
137+
"exec",
138+
pod_name,
139+
"--",
140+
"sh",
141+
"-c",
142+
"if [ ! -d /test-volume ]; then exit 1; fi",
143+
]
144+
)
145+
assert (
146+
c.returncode == 0
147+
), "The singleuser.storage.extraVolumes and extraVolumeMounts configuration doesn't seem to have been honored!"
132148
finally:
133149
_delete_server(api_request, jupyter_user, request_data["test_timeout"])
134150

tools/templates/lint-and-validate-values.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,8 @@ singleuser:
465465
storage:
466466
type: dynamic
467467
extraLabels: *labels
468-
extraVolumes: []
469-
extraVolumeMounts: []
468+
extraVolumes: {}
469+
extraVolumeMounts: {}
470470
static:
471471
pvcName:
472472
subPath: "{username}"

0 commit comments

Comments
 (0)