Skip to content

Support dict values for volumes, volume_mounts and other list based configs that support dict values #3690

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 4 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,13 @@ The following configuration will increase the SHM allocation by mounting a
singleuser:
storage:
extraVolumes:
- name: shm-volume
shm-volume:
name: shm-volume
emptyDir:
medium: Memory
extraVolumeMounts:
- name: shm-volume
shm-volume:
name: shm-volume
mountPath: /dev/shm
```

Expand Down
6 changes: 4 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,13 @@ pods:
singleuser:
storage:
extraVolumes:
- name: jupyterhub-shared
jupyterhub-shared:
name: jupyterhub-shared
persistentVolumeClaim:
claimName: jupyterhub-shared-volume
extraVolumeMounts:
- name: jupyterhub-shared
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