Initial commit - Datalab Lite source code
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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
|
||||
Reference in New Issue
Block a user