|
1 | 1 | import copy |
2 | 2 | import inspect |
3 | 3 | import os |
| 4 | +import re |
4 | 5 | import textwrap |
5 | 6 | from pathlib import Path |
6 | 7 |
|
7 | 8 | import z2jh |
| 9 | +from kubernetes import client as k8s_client |
| 10 | +from kubernetes import config as k8s_config |
8 | 11 |
|
9 | 12 | # 1) Home page UX: remove the old "Pi -> /hub/user-redirect/lab/workspaces/pi" |
10 | 13 | # entry and ensure "Pi Coding Agent -> /services/pi-launcher/". |
|
527 | 530 | "NEBARI_HUB_API_TOKEN": os.environ.get("PI_M4_TOOLS_API_TOKEN", ""), |
528 | 531 | "NEBARI_HUB_API_URL": str(z2jh.get_config("custom.pi-hub-api-url", "http://hub:8081/hub/api") or "http://hub:8081/hub/api"), |
529 | 532 | "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"), |
530 | 536 | # Keep Pi runtime state outside potentially read-only /home mounts. |
531 | 537 | "PI_CODING_AGENT_DIR": str(z2jh.get_config("custom.pi-coding-agent-dir", "/tmp/pi-agent") or "/tmp/pi-agent"), |
532 | 538 | # Shared skills are baked into the image. |
|
593 | 599 |
|
594 | 600 | PI_RUN_AS_ROOT = bool(z2jh.get_config("custom.pi-run-as-root", False)) |
595 | 601 | 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" |
596 | 604 |
|
597 | 605 | code_server_bootstrap_script = textwrap.dedent( |
598 | 606 | """ |
@@ -825,6 +833,48 @@ def _apply_root_access_to_spawner(spawner): |
825 | 833 | spawner.args = current_args |
826 | 834 |
|
827 | 835 |
|
| 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 | + |
828 | 878 | def _build_pi_profile_from_base(base_profile, size_key): |
829 | 879 | spec = PI_SPECS_BY_SIZE.get(size_key) or {} |
830 | 880 | p = copy.deepcopy(base_profile) |
@@ -963,6 +1013,18 @@ async def _pre_spawn_adjust_fs_gid(spawner): |
963 | 1013 | else: |
964 | 1014 | spawner.fs_gid = DEFAULT_FS_GID |
965 | 1015 |
|
| 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 | + |
966 | 1028 | if callable(_previous_pre_spawn_hook): |
967 | 1029 | result = _previous_pre_spawn_hook(spawner) |
968 | 1030 | if inspect.isawaitable(result): |
|
0 commit comments