592592 PI_SPECS_BY_SIZE [_size ].update (_override )
593593
594594PI_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
596597code_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+
739828def _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 .
860949DEFAULT_FS_GID = int (z2jh .get_config ("custom.default-fs-gid" , 100 ) or 100 )
861950c .KubeSpawner .fs_gid = DEFAULT_FS_GID
862951
@@ -865,10 +954,14 @@ async def _profile_list_without_nebula_pi_cli(spawner):
865954
866955async 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 :
0 commit comments