Skip to content
Merged
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ only rebuild images if their dependent files in their respective directories or
1. Use `helm` to upgrade (or install) your local JupyterHub Helm chart.

```shell
helm upgrade --install jupyterhub ./jupyterhub --cleanup-on-fail --values dev-config.yaml
helm upgrade --install jupyterhub ./jupyterhub --cleanup-on-fail --values dev-config.yaml --values dev-config-local-chart-extra-config.yaml
```

Note that `--cleanup-on-fail` is a very good practice to avoid `<resource name> already exist` errors in future upgrades following a failed upgrade.
Expand Down
11 changes: 11 additions & 0 deletions dev-config-local-chart-extra-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@
# include this config and then pass --set hub.some-option=null to null it out
# when it must not be passed, but that still triggers schema validation errors.
#

singleuser:
storage:
extraVolumes:
test-volume:
name: test-volume
emptyDir: {}
extraVolumeMounts:
test-volume-mount:
name: test-volume
mountPath: /test-volume
8 changes: 6 additions & 2 deletions docs/source/jupyterhub/customizing/user-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,15 @@ The following configuration will increase the SHM allocation by mounting a
singleuser:
storage:
extraVolumes:
- name: shm-volume
# Arbitrary key for identity & ordering
1-shm-volume:
name: shm-volume
emptyDir:
medium: Memory
extraVolumeMounts:
- name: shm-volume
# Arbitrary key for identity & ordering
1-shm-volume:
name: shm-volume
mountPath: /dev/shm
```

Expand Down
8 changes: 6 additions & 2 deletions docs/source/jupyterhub/customizing/user-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,11 +235,15 @@ pods:
singleuser:
storage:
extraVolumes:
- name: jupyterhub-shared
# Arbitrary key for identity & ordering
1-jupyterhub-shared:
name: jupyterhub-shared
persistentVolumeClaim:
claimName: jupyterhub-shared-volume
extraVolumeMounts:
- name: jupyterhub-shared
# Arbitrary key for identity & ordering
1-jupyterhub-shared:
name: jupyterhub-shared
mountPath: /home/shared
```

Expand Down
75 changes: 48 additions & 27 deletions jupyterhub/files/hub/jupyterhub_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,9 @@ def camelCaseify(s):
if tolerations:
c.KubeSpawner.tolerations = tolerations

volumes = {}
volume_mounts = {}

# Configure dynamically provisioning pvc
storage_type = get_config("singleuser.storage.type")
if storage_type == "dynamic":
Expand All @@ -273,32 +276,35 @@ def camelCaseify(s):
)

# Add volumes to singleuser pods
c.KubeSpawner.volumes = [
{
volumes = {
volume_name_template: {
"name": volume_name_template,
"persistentVolumeClaim": {"claimName": "{pvc_name}"},
}
]
c.KubeSpawner.volume_mounts = [
{
}
volume_mounts = {
volume_name_template: {
"mountPath": get_config("singleuser.storage.homeMountPath"),
"name": volume_name_template,
"subPath": get_config("singleuser.storage.dynamic.subPath"),
}
]
}
elif storage_type == "static":
pvc_claim_name = get_config("singleuser.storage.static.pvcName")
c.KubeSpawner.volumes = [
{"name": "home", "persistentVolumeClaim": {"claimName": pvc_claim_name}}
]
volumes = {
"home": {
"name": "home",
"persistentVolumeClaim": {"claimName": pvc_claim_name},
}
}

c.KubeSpawner.volume_mounts = [
{
volume_mounts = {
"home": {
"mountPath": get_config("singleuser.storage.homeMountPath"),
"name": "home",
"subPath": get_config("singleuser.storage.static.subPath"),
}
]
}

# Inject singleuser.extraFiles as volumes and volumeMounts with data loaded from
# the dedicated k8s Secret prepared to hold the extraFiles actual content.
Expand All @@ -323,24 +329,39 @@ def camelCaseify(s):
"secretName": get_name("singleuser"),
"items": items,
}
c.KubeSpawner.volumes.append(volume)
volumes.update({volume["name"]: volume})

volume_mounts = []
for file_key, file_details in extra_files.items():
volume_mounts.append(
{
"mountPath": file_details["mountPath"],
"subPath": file_key,
"name": "files",
}
)
c.KubeSpawner.volume_mounts.extend(volume_mounts)
for idx, (file_key, file_details) in enumerate(extra_files.items()):
volume_mount = {
"mountPath": file_details["mountPath"],
"subPath": file_key,
"name": "files",
}
volume_mounts.update({f"{idx}-files": volume_mount})

# Inject extraVolumes / extraVolumeMounts
c.KubeSpawner.volumes.extend(get_config("singleuser.storage.extraVolumes", []))
c.KubeSpawner.volume_mounts.extend(
get_config("singleuser.storage.extraVolumeMounts", [])
)
extra_volumes = get_config("singleuser.storage.extraVolumes", default={})
if isinstance(extra_volumes, dict):
for key, volume in extra_volumes.items():
volumes.update({key: volume})
elif isinstance(extra_volumes, list):
for volume in extra_volumes:
volumes.update({volume["name"]: volume})

extra_volume_mounts = get_config("singleuser.storage.extraVolumeMounts", default={})
if isinstance(extra_volume_mounts, dict):
for key, volume_mount in extra_volume_mounts.items():
volume_mounts.update({key: volume_mount})
elif isinstance(extra_volume_mounts, list):
# If extraVolumeMounts is a list, we need to add them to the volume_mounts
# dictionary with a unique key.
# Since volume mount's name is not guaranteed to be unique, we use the index
# as part of the key.
for idx, volume_mount in enumerate(extra_volume_mounts):
volume_mounts.update({f"{idx}-{volume_mount['name']}": volume_mount})

c.KubeSpawner.volumes = volumes
c.KubeSpawner.volume_mounts = volume_mounts

c.JupyterHub.services = []
c.JupyterHub.load_roles = []
Expand Down
25 changes: 23 additions & 2 deletions jupyterhub/values.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2418,8 +2418,29 @@ properties:
Configures `KubeSpawner.storage_extra_labels`. Note that these
labels are set on the PVC during creation only and won't be
updated after creation.
extraVolumeMounts: *extraVolumeMounts-spec
extraVolumes: *extraVolumes-spec
extraVolumeMounts:
type: [object, array]
description: |
Injects extra volume mounts into `KubeSpawner.volume_mounts` dictionary.
Can be a dictionary or an array.
If it's an array, each item must be a volume mount configuration in k8s
native syntax. A combination of the volume name and its index is used as the key
in `KubeSpawner.volume_mounts` dictionary and the value is the volume mount
configuration.
If `extraVolumeMounts` is defined as a dictionary, the keys of the dictionary
can be any descriptive name for the volume mount and the value is the volume mount
configuration in k8s native syntax.
extraVolumes:
type: [object, array]
description: |
Injects extra volumes into `KubeSpawner.volumes` dictionary. Can be a dictionary
or an array.
If it's an array, each item must be volume configuration in k8s native
syntax. The name of the volume is used as the key in `KubeSpawner.volumes`
dictionary while the value is the volume configuration.
If `extraVolumes` is defined as a dictionary, the keys of the dictionary
can be any descriptive name for the volume and the value must be the volume
configuration in k8s native syntax.
homeMountPath:
type: string
description: |
Expand Down
4 changes: 2 additions & 2 deletions jupyterhub/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,8 @@ singleuser:
storage:
type: dynamic
extraLabels: {}
extraVolumes: []
extraVolumeMounts: []
extraVolumes: {}
extraVolumeMounts: {}
static:
pvcName:
subPath: "{username}"
Expand Down
16 changes: 16 additions & 0 deletions tests/test_spawn.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ def test_spawn_basic(
assert (
c.returncode == 0
), "The singleuser.extraFiles configuration doesn't seem to have been honored!"

# check the extra volume and volume mount exists
c = subprocess.run(
[
"kubectl",
"exec",
pod_name,
"--",
"sh",
"-c",
"if [ ! -d /test-volume ]; then exit 1; fi",
]
)
assert (
c.returncode == 0
), "The singleuser.storage.extraVolumes and extraVolumeMounts configuration doesn't seem to have been honored!"
finally:
_delete_server(api_request, jupyter_user, request_data["test_timeout"])

Expand Down
4 changes: 2 additions & 2 deletions tools/templates/lint-and-validate-values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,8 @@ singleuser:
storage:
type: dynamic
extraLabels: *labels
extraVolumes: []
extraVolumeMounts: []
extraVolumes: {}
extraVolumeMounts: {}
static:
pvcName:
subPath: "{username}"
Expand Down
Loading