Skip to content

Commit 9b2d32e

Browse files
committed
feat: add optional full-access hub and k8s tokens for pi pods
1 parent 3d4cdf3 commit 9b2d32e

4 files changed

Lines changed: 151 additions & 22 deletions

File tree

config/jupyterhub/05-pi-launcher.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1690,30 +1690,34 @@ def _profile_spec_text(size_key, default_text):
16901690
# Token is injected via hub env (PI_M4_TOOLS_API_TOKEN) from chart-managed Secret.
16911691
pi_tools_api_token = (os.environ.get("PI_M4_TOOLS_API_TOKEN") or "").strip()
16921692
pi_tools_service_name = str(z2jh.get_config("custom.pi-m4-tools-service-name", "pi-m4-tools") or "pi-m4-tools")
1693+
pi_tools_full_access_enabled = bool(z2jh.get_config("custom.pi-m4-tools-full-access-enabled", False))
16931694

16941695
if pi_tools_api_token:
1695-
c.JupyterHub.services.append(
1696-
{
1697-
"name": pi_tools_service_name,
1698-
"api_token": pi_tools_api_token,
1699-
"display": False,
1700-
}
1701-
)
1702-
1703-
pi_tools_role = {
1704-
"name": "pi-m4-tools-role",
1705-
"services": [pi_tools_service_name],
1706-
"scopes": [
1707-
"admin:servers",
1708-
"admin:server_state",
1709-
"read:users",
1710-
"read:users:name",
1711-
"access:servers",
1712-
],
1696+
service_def = {
1697+
"name": pi_tools_service_name,
1698+
"api_token": pi_tools_api_token,
1699+
"display": False,
17131700
}
1714-
if isinstance(c.JupyterHub.load_roles, list):
1715-
c.JupyterHub.load_roles.append(pi_tools_role)
1716-
else:
1717-
c.JupyterHub.load_roles = [pi_tools_role]
1701+
if pi_tools_full_access_enabled:
1702+
# POC mode: make this token full Hub admin access.
1703+
service_def["admin"] = True
1704+
c.JupyterHub.services.append(service_def)
1705+
1706+
if not pi_tools_full_access_enabled:
1707+
pi_tools_role = {
1708+
"name": "pi-m4-tools-role",
1709+
"services": [pi_tools_service_name],
1710+
"scopes": [
1711+
"admin:servers",
1712+
"admin:server_state",
1713+
"read:users",
1714+
"read:users:name",
1715+
"access:servers",
1716+
],
1717+
}
1718+
if isinstance(c.JupyterHub.load_roles, list):
1719+
c.JupyterHub.load_roles.append(pi_tools_role)
1720+
else:
1721+
c.JupyterHub.load_roles = [pi_tools_role]
17181722
else:
17191723
print("WARN: PI_M4_TOOLS_API_TOKEN not set; skipping pi-m4-tools service registration")

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import copy
22
import inspect
33
import os
4+
import re
45
import textwrap
56
from pathlib import Path
67

78
import z2jh
9+
from kubernetes import client as k8s_client
10+
from kubernetes import config as k8s_config
811

912
# 1) Home page UX: remove the old "Pi -> /hub/user-redirect/lab/workspaces/pi"
1013
# entry and ensure "Pi Coding Agent -> /services/pi-launcher/".
@@ -527,6 +530,9 @@
527530
"NEBARI_HUB_API_TOKEN": os.environ.get("PI_M4_TOOLS_API_TOKEN", ""),
528531
"NEBARI_HUB_API_URL": str(z2jh.get_config("custom.pi-hub-api-url", "http://hub:8081/hub/api") or "http://hub:8081/hub/api"),
529532
"NEBARI_PROXY_URL": str(z2jh.get_config("custom.pi-proxy-url", "http://proxy-public") or "http://proxy-public"),
533+
# Explicit aliases for POC flows inside the Pi terminal.
534+
"JUPYTERHUB_FULL_API_TOKEN": os.environ.get("PI_M4_TOOLS_API_TOKEN", ""),
535+
"JUPYTERHUB_FULL_API_URL": str(z2jh.get_config("custom.pi-hub-api-url", "http://hub:8081/hub/api") or "http://hub:8081/hub/api"),
530536
# Keep Pi runtime state outside potentially read-only /home mounts.
531537
"PI_CODING_AGENT_DIR": str(z2jh.get_config("custom.pi-coding-agent-dir", "/tmp/pi-agent") or "/tmp/pi-agent"),
532538
# Shared skills are baked into the image.
@@ -593,6 +599,8 @@
593599

594600
PI_RUN_AS_ROOT = bool(z2jh.get_config("custom.pi-run-as-root", False))
595601
DEFAULT_RUN_AS_ROOT = bool(z2jh.get_config("custom.default-run-as-root", False))
602+
PI_K8S_USER_ACCESS_ENABLED = bool(z2jh.get_config("custom.pi-k8s-user-access-enabled", False))
603+
HUB_NAMESPACE = (os.environ.get("POD_NAMESPACE", "default") or "default").strip() or "default"
596604

597605
code_server_bootstrap_script = textwrap.dedent(
598606
"""
@@ -825,6 +833,48 @@ def _apply_root_access_to_spawner(spawner):
825833
spawner.args = current_args
826834

827835

836+
_core_v1_api = None
837+
838+
839+
def _k8s_core_v1_api():
840+
global _core_v1_api
841+
if _core_v1_api is None:
842+
k8s_config.load_incluster_config()
843+
_core_v1_api = k8s_client.CoreV1Api()
844+
return _core_v1_api
845+
846+
847+
def _slug_username(value: str, max_len: int = 28) -> str:
848+
raw = re.sub(r"[^a-z0-9-]+", "-", (value or "").strip().lower()).strip("-")
849+
if not raw:
850+
raw = "user"
851+
return raw[:max_len].rstrip("-") or "user"
852+
853+
854+
def _ensure_pi_user_k8s_access(username: str) -> str:
855+
user_slug = _slug_username(username)
856+
sa_name = f"pi-user-{user_slug}"
857+
858+
core_api = _k8s_core_v1_api()
859+
try:
860+
core_api.read_namespaced_service_account(name=sa_name, namespace=HUB_NAMESPACE)
861+
except Exception:
862+
core_api.create_namespaced_service_account(
863+
namespace=HUB_NAMESPACE,
864+
body=k8s_client.V1ServiceAccount(
865+
metadata=k8s_client.V1ObjectMeta(
866+
name=sa_name,
867+
labels={
868+
"pi.nebari.dev/access": "k8s-user-token",
869+
"pi.nebari.dev/owner": user_slug,
870+
},
871+
)
872+
),
873+
)
874+
875+
return sa_name
876+
877+
828878
def _build_pi_profile_from_base(base_profile, size_key):
829879
spec = PI_SPECS_BY_SIZE.get(size_key) or {}
830880
p = copy.deepcopy(base_profile)
@@ -963,6 +1013,18 @@ async def _pre_spawn_adjust_fs_gid(spawner):
9631013
else:
9641014
spawner.fs_gid = DEFAULT_FS_GID
9651015

1016+
if server_name == "pi" and PI_K8S_USER_ACCESS_ENABLED:
1017+
username = getattr(getattr(spawner, "user", None), "name", "") or ""
1018+
sa_name = _ensure_pi_user_k8s_access(username)
1019+
spawner.service_account = sa_name
1020+
1021+
env = dict(getattr(spawner, "environment", {}) or {})
1022+
env["PI_USER_K8S_SERVICE_ACCOUNT"] = sa_name
1023+
env["KUBECTL_API_SERVER"] = "https://kubernetes.default.svc"
1024+
env["KUBECTL_TOKEN_FILE"] = "/var/run/secrets/kubernetes.io/serviceaccount/token"
1025+
env["KUBECTL_CA_FILE"] = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
1026+
spawner.environment = env
1027+
9661028
if callable(_previous_pre_spawn_hook):
9671029
result = _previous_pre_spawn_hook(spawner)
9681030
if inspect.isawaitable(result):
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
{{- if .Values.pi.enabled }}
2+
apiVersion: rbac.authorization.k8s.io/v1
3+
kind: Role
4+
metadata:
5+
name: {{ include "nebari-pi-pack.fullname" . }}-pi-user-k8s-full
6+
namespace: {{ .Release.Namespace }}
7+
labels:
8+
{{- include "nebari-pi-pack.labels" . | nindent 4 }}
9+
rules:
10+
- apiGroups: ["*"]
11+
resources: ["*"]
12+
verbs: ["*"]
13+
---
14+
apiVersion: rbac.authorization.k8s.io/v1
15+
kind: RoleBinding
16+
metadata:
17+
name: {{ include "nebari-pi-pack.fullname" . }}-pi-user-k8s-full
18+
namespace: {{ .Release.Namespace }}
19+
labels:
20+
{{- include "nebari-pi-pack.labels" . | nindent 4 }}
21+
subjects:
22+
- kind: Group
23+
name: system:serviceaccounts:{{ .Release.Namespace }}
24+
apiGroup: rbac.authorization.k8s.io
25+
roleRef:
26+
apiGroup: rbac.authorization.k8s.io
27+
kind: Role
28+
name: {{ include "nebari-pi-pack.fullname" . }}-pi-user-k8s-full
29+
---
30+
apiVersion: rbac.authorization.k8s.io/v1
31+
kind: Role
32+
metadata:
33+
name: {{ include "nebari-pi-pack.fullname" . }}-pi-user-k8s-access-provisioner
34+
namespace: {{ .Release.Namespace }}
35+
labels:
36+
{{- include "nebari-pi-pack.labels" . | nindent 4 }}
37+
rules:
38+
- apiGroups: [""]
39+
resources: ["serviceaccounts"]
40+
verbs: ["get", "list", "watch", "create", "update", "patch"]
41+
---
42+
apiVersion: rbac.authorization.k8s.io/v1
43+
kind: RoleBinding
44+
metadata:
45+
name: {{ include "nebari-pi-pack.fullname" . }}-pi-user-k8s-access-provisioner
46+
namespace: {{ .Release.Namespace }}
47+
labels:
48+
{{- include "nebari-pi-pack.labels" . | nindent 4 }}
49+
subjects:
50+
- kind: ServiceAccount
51+
name: hub
52+
namespace: {{ .Release.Namespace }}
53+
roleRef:
54+
apiGroup: rbac.authorization.k8s.io
55+
kind: Role
56+
name: {{ include "nebari-pi-pack.fullname" . }}-pi-user-k8s-access-provisioner
57+
{{- end }}

values.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,12 @@ jupyterhub:
189189
pi-proxy-url: http://proxy-public
190190
pi-coding-agent-dir: /tmp/pi-agent
191191
pi-m4-tools-service-name: pi-m4-tools
192+
# If true, expose PI_M4_TOOLS_API_TOKEN in Pi pods as a full-admin Hub API token.
193+
# Intended for demo/POC workflows.
194+
pi-m4-tools-full-access-enabled: false
195+
# If true, each Pi user gets a dedicated Kubernetes ServiceAccount + RoleBinding
196+
# and the Pi pod runs with that ServiceAccount token mounted.
197+
pi-k8s-user-access-enabled: false
192198
# If true, Pi named-server pods run as root inside the container.
193199
# This grants full in-container filesystem permissions for that user's pod.
194200
# User isolation is still provided by per-user pods/PVCs.

0 commit comments

Comments
 (0)