Skip to content

Commit 6dfa61a

Browse files
committed
feat: support optional root mode for default notebook pods
1 parent 54ddae3 commit 6dfa61a

2 files changed

Lines changed: 100 additions & 4 deletions

File tree

config/jupyterhub/08-pi-home-and-spawn-fixes.py

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -592,13 +592,34 @@
592592
PI_SPECS_BY_SIZE[_size].update(_override)
593593

594594
PI_RUN_AS_ROOT = bool(z2jh.get_config("custom.pi-run-as-root", False))
595+
DEFAULT_RUN_AS_ROOT = bool(z2jh.get_config("custom.default-run-as-root", False))
595596

596597
code_server_bootstrap_script = textwrap.dedent(
597598
"""
598599
set -u
599600
if [ "${JUPYTERHUB_SERVER_NAME:-}" = "pi" ]; then
600601
exit 0
601602
fi
603+
604+
# Local/dev convenience: provide a minimal sudo shim for root-run pods
605+
# when the base image does not ship sudo.
606+
if [ "$(id -u)" = "0" ] && ! command -v sudo >/dev/null 2>&1; then
607+
cat >/usr/local/bin/sudo <<'EOF'
608+
#!/bin/sh
609+
if [ "$#" -eq 0 ]; then
610+
exec /bin/sh
611+
fi
612+
while [ "$#" -gt 0 ]; do
613+
case "$1" in
614+
-*) shift ;;
615+
*) break ;;
616+
esac
617+
done
618+
exec "$@"
619+
EOF
620+
chmod 0755 /usr/local/bin/sudo || true
621+
fi
622+
602623
CS_BIN="$HOME/.local/bin/code-server"
603624
if [ -x "$CS_BIN" ]; then
604625
exit 0
@@ -736,6 +757,74 @@ def _apply_pi_root_access(override):
736757
return override
737758

738759

760+
def _is_server_root_enabled(server_name: str) -> bool:
761+
name = (server_name or "").strip()
762+
if name == "pi":
763+
return PI_RUN_AS_ROOT
764+
return DEFAULT_RUN_AS_ROOT
765+
766+
767+
def _ensure_root_container_config(container_cfg):
768+
cfg = copy.deepcopy(container_cfg or {})
769+
if not isinstance(cfg, dict):
770+
cfg = {}
771+
sec_ctx = copy.deepcopy(cfg.get("securityContext") or {})
772+
if not isinstance(sec_ctx, dict):
773+
sec_ctx = {}
774+
sec_ctx["runAsUser"] = 0
775+
sec_ctx["runAsGroup"] = 0
776+
sec_ctx["runAsNonRoot"] = False
777+
sec_ctx["allowPrivilegeEscalation"] = True
778+
cfg["securityContext"] = sec_ctx
779+
return cfg
780+
781+
782+
def _ensure_root_pod_config(pod_cfg):
783+
cfg = copy.deepcopy(pod_cfg or {})
784+
if not isinstance(cfg, dict):
785+
cfg = {}
786+
sec_ctx = copy.deepcopy(cfg.get("securityContext") or {})
787+
if not isinstance(sec_ctx, dict):
788+
sec_ctx = {}
789+
sec_ctx["runAsNonRoot"] = False
790+
cfg["securityContext"] = sec_ctx
791+
return cfg
792+
793+
794+
def _spawner_cmd_supports_allow_root(spawner) -> bool:
795+
cmd = getattr(spawner, "cmd", None)
796+
if cmd in (None, "", []):
797+
return True
798+
if isinstance(cmd, str):
799+
joined = cmd.lower()
800+
elif isinstance(cmd, (list, tuple)):
801+
joined = " ".join(str(part).lower() for part in cmd)
802+
else:
803+
joined = str(cmd).lower()
804+
return "jupyter" in joined
805+
806+
807+
def _apply_root_access_to_spawner(spawner):
808+
spawner.uid = 0
809+
spawner.gid = 0
810+
spawner.extra_container_config = _ensure_root_container_config(
811+
getattr(spawner, "extra_container_config", None)
812+
)
813+
spawner.extra_pod_config = _ensure_root_pod_config(
814+
getattr(spawner, "extra_pod_config", None)
815+
)
816+
817+
# Jupyter singleuser disallows root unless explicitly permitted.
818+
# Add --allow-root for notebook-ish spawns.
819+
user_opts = getattr(spawner, "user_options", {}) or {}
820+
jhub_app = user_opts.get("jhub_app") if isinstance(user_opts, dict) else None
821+
if (not jhub_app) or _spawner_cmd_supports_allow_root(spawner):
822+
current_args = list(getattr(spawner, "args", []) or [])
823+
if "--allow-root" not in current_args:
824+
current_args.append("--allow-root")
825+
spawner.args = current_args
826+
827+
739828
def _build_pi_profile_from_base(base_profile, size_key):
740829
spec = PI_SPECS_BY_SIZE.get(size_key) or {}
741830
p = copy.deepcopy(base_profile)
@@ -856,7 +945,7 @@ async def _profile_list_without_nebula_pi_cli(spawner):
856945
c.KubeSpawner.pod_security_context = existing_pod_security_context
857946

858947
# Keep fsGroup enabled for default (non-pi) servers so user home PVCs are writable
859-
# as uid 1000, while still disabling fsGroup for Pi servers (which run as root here).
948+
# as uid 1000, unless local/dev root mode is explicitly enabled.
860949
DEFAULT_FS_GID = int(z2jh.get_config("custom.default-fs-gid", 100) or 100)
861950
c.KubeSpawner.fs_gid = DEFAULT_FS_GID
862951

@@ -865,10 +954,14 @@ async def _profile_list_without_nebula_pi_cli(spawner):
865954

866955
async def _pre_spawn_adjust_fs_gid(spawner):
867956
server_name = (getattr(spawner, "name", "") or "").strip()
868-
if server_name == "pi":
957+
if _is_server_root_enabled(server_name):
869958
spawner.fs_gid = None
959+
_apply_root_access_to_spawner(spawner)
870960
else:
871-
spawner.fs_gid = DEFAULT_FS_GID
961+
if server_name == "pi":
962+
spawner.fs_gid = None
963+
else:
964+
spawner.fs_gid = DEFAULT_FS_GID
872965

873966
if callable(_previous_pre_spawn_hook):
874967
result = _previous_pre_spawn_hook(spawner)
@@ -893,7 +986,7 @@ async def _modify_pod_adjust_fs_group(spawner, pod):
893986
sec = pod.spec.security_context
894987
server_name = (getattr(spawner, "name", "") or "").strip()
895988
if sec is not None:
896-
if server_name == "pi":
989+
if _is_server_root_enabled(server_name) or server_name == "pi":
897990
sec.fs_group = None
898991
sec.fs_group_change_policy = None
899992
else:

values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ jupyterhub:
193193
# This grants full in-container filesystem permissions for that user's pod.
194194
# User isolation is still provided by per-user pods/PVCs.
195195
pi-run-as-root: false
196+
# If true, default (non-pi) notebook servers also run as root.
197+
# Use only for local/dev scenarios where full root access is required.
198+
default-run-as-root: false
196199
pi-profiles:
197200
small:
198201
display_name: Pi Small

0 commit comments

Comments
 (0)