from kubernetes import client, config import random, string from minio_client import generate_password, MINIO_URL, MINIO_CONSOLE_URL import hashlib def generate_service_password(username: str, service_type: str) -> str: """Génère un password unique par user+service, persistant""" seed = f"datalab-{username}-{service_type}-2026" return hashlib.sha256(seed.encode()).hexdigest()[:12] S3_INIT_SCRIPT = ( "pip install s3contents -q --target=/shared/packages && " "mkdir -p /shared/jupyter && " "printf '%s\\n' " "'import sys' " "'sys.path.insert(0, \"/shared/packages\")' " "'import os' " "'c.ServerApp.contents_manager_class = \"s3contents.S3ContentsManager\"' " "'c.S3ContentsManager.endpoint_url = \"https://\" + os.environ.get(\"MINIO_ENDPOINT\", \"\")' " "'c.S3ContentsManager.access_key_id = os.environ.get(\"MINIO_ACCESS_KEY\", \"\")' " "'c.S3ContentsManager.secret_access_key = os.environ.get(\"MINIO_SECRET_KEY\", \"\")' " "'c.S3ContentsManager.bucket = os.environ.get(\"MINIO_BUCKET\", \"\")' " "> /shared/jupyter/jupyter_server_config.py && echo OK" ) S3_START_CMD = ( "mkdir -p /home/jovyan/.jupyter && " "cp /shared/jupyter/jupyter_server_config.py /home/jovyan/.jupyter/ && " "export PYTHONPATH=/shared/packages:$PYTHONPATH && " "start-notebook.sh" ) try: config.load_incluster_config() except: config.load_kube_config() DOMAIN = 'lab.leadwire.dev' def get_namespace(username): return f"user-{username}" def random_id(length=5): return ''.join(random.choices(string.digits, k=length)) def ensure_namespace(username): v1 = client.CoreV1Api() ns = get_namespace(username) try: v1.read_namespace(ns) except: v1.create_namespace(client.V1Namespace( metadata=client.V1ObjectMeta( name=ns, labels={"managed-by": "datalab-lite", "user": username} ) )) return ns def create_service(username, service_type, svc_config): # Générer password unique par user+service service_password = generate_service_password(username, service_type) # Remplacer {token} dans les env vars resolved_env = {} for k, v in svc_config["env"].items(): resolved_env[k] = v.replace("{token}", service_password) svc_config = {**svc_config, "env": resolved_env} ns = ensure_namespace(username) uid = random_id() name = f"{username}-{service_type}-{uid}" port = svc_config["port"] apps = client.AppsV1Api() v1 = client.CoreV1Api() net = client.NetworkingV1Api() minio_password = generate_password(username) minio_envs = { "MINIO_ENDPOINT": MINIO_URL, "MINIO_ACCESS_KEY": username, "MINIO_SECRET_KEY": minio_password, "MINIO_BUCKET": f"user-{username}", "AWS_ACCESS_KEY_ID": username, "AWS_SECRET_ACCESS_KEY": minio_password, "AWS_S3_ENDPOINT": f"https://{MINIO_URL}", "NB_USER": username, "PYTHONPATH": "/home/jovyan/jupyter-packages", "PYTHONHTTPSVERIFY": "0", } env_vars = [client.V1EnvVar(name=k, value=v) for k, v in {**svc_config["env"], **minio_envs}.items()] deployment = client.V1Deployment( metadata=client.V1ObjectMeta( name=name, namespace=ns, labels={"app": name, "user": username, "service-type": service_type, "token": svc_config["env"].get("JUPYTER_TOKEN", svc_config["env"].get("PASSWORD", ""))} ), spec=client.V1DeploymentSpec( replicas=1, selector=client.V1LabelSelector(match_labels={"app": name}), template=client.V1PodTemplateSpec( metadata=client.V1ObjectMeta(labels={"app": name}), spec=client.V1PodSpec( init_containers=([ client.V1Container( name="install-s3", image=svc_config["image"], command=["sh", "-c", S3_INIT_SCRIPT], env=env_vars, volume_mounts=[client.V1VolumeMount(name="shared", mount_path="/shared")] ) ] if service_type == "jupyter" else []), containers=[ client.V1Container( name=name, image=svc_config["image"], ports=[client.V1ContainerPort(container_port=port)], env=env_vars, command=(["sh", "-c", S3_START_CMD] if service_type == "jupyter" else None), volume_mounts=([client.V1VolumeMount(name="shared", mount_path="/shared")] if service_type == "jupyter" else []), resources=client.V1ResourceRequirements( requests={"memory": "512Mi", "cpu": "250m"}, limits={"memory": "2Gi", "cpu": "1"} ) ) ], volumes=([client.V1Volume(name="shared", empty_dir=client.V1EmptyDirVolumeSource())] if service_type == "jupyter" else []) ) ) ) ) apps.create_namespaced_deployment(ns, deployment) svc = client.V1Service( metadata=client.V1ObjectMeta(name=name, namespace=ns), spec=client.V1ServiceSpec( selector={"app": name}, ports=[client.V1ServicePort(port=port, target_port=port)] ) ) v1.create_namespaced_service(ns, svc) host = f"{name}.{DOMAIN}" ingress = client.V1Ingress( metadata=client.V1ObjectMeta( name=name, namespace=ns, annotations={"cert-manager.io/cluster-issuer": "letsencrypt-prod", "nginx.ingress.kubernetes.io/proxy-read-timeout": "3600", "nginx.ingress.kubernetes.io/proxy-send-timeout": "3600"} ), spec=client.V1IngressSpec( ingress_class_name="nginx", rules=[client.V1IngressRule( host=host, http=client.V1HTTPIngressRuleValue(paths=[ client.V1HTTPIngressPath( path="/", path_type="Prefix", backend=client.V1IngressBackend( service=client.V1IngressServiceBackend( name=name, port=client.V1ServiceBackendPort(number=port) ) ) ) ]) )], tls=[client.V1IngressTLS(hosts=[host], secret_name=f"{name}-tls")] ) ) net.create_namespaced_ingress(ns, ingress) return f"https://{host}" def delete_service(username, service_name): """service_name = nom complet ex: datauser-jupyter-12345""" ns = get_namespace(username) apps = client.AppsV1Api() v1 = client.CoreV1Api() net = client.NetworkingV1Api() for fn in [ lambda: apps.delete_namespaced_deployment(service_name, ns), lambda: v1.delete_namespaced_service(service_name, ns), lambda: net.delete_namespaced_ingress(service_name, ns), ]: try: fn() except: pass def list_user_services(username): ns = get_namespace(username) apps = client.AppsV1Api() result = [] try: deps = apps.list_namespaced_deployment( ns, label_selector=f"user={username}" ) for d in deps.items: stype = d.metadata.labels.get("service-type", "") name = d.metadata.name ready = d.status.ready_replicas or 0 result.append({ "name": name, "type": stype, "ready": ready > 0, "url": f"https://{name}.{DOMAIN}", "token": d.metadata.labels.get("token", "") }) except: pass return result