重构模型平台前后端并移除Redis依赖
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# Runtime Adapter 契约
|
||||
|
||||
`runtime_adapter.py` 是 Runtime Manager 与具体运行环境之间的可执行契约。
|
||||
|
||||
当前 Compose 环境使用 `SharedJupyterAdapter`。它把共享 Jupyter Server 作为
|
||||
首期 Provider,并在 MySQL 中为每个 `Workspace` 建立并复用一个逻辑
|
||||
Runtime。每个打开的 Notebook 在该 Runtime 中建立独立 Jupyter Session,
|
||||
每个 Session 绑定自己的 Kernel。
|
||||
后续 Docker/Kubernetes Provider 必须实现同一 Protocol,公共锁接口和
|
||||
`runtime_instances` 状态模型保持不变。
|
||||
|
||||
核心约束:
|
||||
|
||||
- `ensure_running` 对同一作用域必须幂等;
|
||||
- Runtime 的唯一复用作用域是 Workspace,不能按用户重复创建;
|
||||
- Runtime 的期望状态、实际状态和租约写入 MySQL;
|
||||
- Notebook 对应独立 Jupyter Session/Kernel,其创建和终止只能由 Runtime
|
||||
Manager 调用;
|
||||
- Provider 不能依赖 Platform API 的进程内状态;
|
||||
- `proxy_base_path` 与 Jupyter 内部 `base_url` 必须一致。
|
||||
- Provider 调用必须携带内部服务凭据,不能通过关闭 XSRF 检查规避认证;
|
||||
- Workspace 的目录、文件权限必须同时满足 Platform 原子写入和 Jupyter
|
||||
读写,当前 Compose 环境使用共享 Unix 组完成。
|
||||
|
||||
Jupyter 浏览器代理、访问票据和 WebSocket 约束见
|
||||
`jupyter-proxy-v1.md`。
|
||||
@@ -0,0 +1 @@
|
||||
"""Runtime adapter contract package."""
|
||||
@@ -0,0 +1,65 @@
|
||||
# Jupyter 代理与访问票据契约 V1
|
||||
|
||||
## 1. 浏览器访问流程
|
||||
|
||||
1. 前端携带用户、Workspace、`edit_session_id` 和 `lock_token` 调用
|
||||
`POST /api/v1/jupyter/access-tickets`。
|
||||
2. Platform API 校验编辑锁、Runtime 与 Jupyter Session 的归属及状态。
|
||||
3. 成功后返回 `/jupyter/...` 地址,并设置短期
|
||||
`jupyter_access` Cookie。
|
||||
4. 前端在当前平台页面的编辑区内嵌同源 `/jupyter/...` 页面,不新开浏览器
|
||||
标签页。
|
||||
5. 内嵌页面经 Nginx 访问 Jupyter HTTP/WebSocket;Nginx 先执行内部鉴权
|
||||
子请求,再把内部 Token 注入上游请求。
|
||||
|
||||
一个 Workspace 只建立或复用一个 Jupyter Runtime/Server;同一 Server
|
||||
中的每个 Notebook 使用独立 Jupyter Session 和独立 Kernel。
|
||||
|
||||
浏览器响应、URL、JavaScript 和日志中都不得出现 Jupyter 内部 Token。
|
||||
|
||||
## 2. 访问票据
|
||||
|
||||
- 票据绑定:`user_id`、`workspace_id`、`edit_session_id`、
|
||||
`runtime_id`、`jupyter_session_id`。
|
||||
- 默认有效期:60 秒;最长不超过 5 分钟。
|
||||
- Cookie:`HttpOnly; SameSite=Lax; Path=/jupyter/`,生产环境必须增加
|
||||
`Secure`。
|
||||
- 票据只授权当前 Workspace 的 Jupyter 路径,不能跨 Workspace 使用。
|
||||
- Jupyter 页面必须与平台使用同一站点入口,并允许同源 iframe 嵌入。
|
||||
|
||||
## 3. Nginx 内部鉴权接口
|
||||
|
||||
```http
|
||||
GET /internal/v1/jupyter/authorize
|
||||
Cookie: jupyter_access=<opaque-ticket>
|
||||
X-Original-URI: /jupyter/...
|
||||
X-Request-ID: <request-id>
|
||||
```
|
||||
|
||||
该接口只允许 Nginx 在内部网络调用:
|
||||
|
||||
- `204`:票据有效,响应头提供上游地址和内部认证信息。
|
||||
- `401`:票据缺失、伪造或过期。
|
||||
- `403`:用户、Workspace、Session 不匹配,或编辑锁/Runtime 已失效。
|
||||
|
||||
成功响应头:
|
||||
|
||||
```http
|
||||
X-Jupyter-Upstream: http://jupyter:8888
|
||||
X-Jupyter-Authorization: token <internal-token>
|
||||
X-Workspace-ID: <workspace-id>
|
||||
```
|
||||
|
||||
Nginx 必须删除客户端传入的 `Authorization`,使用内部鉴权结果重建上游
|
||||
认证头。
|
||||
|
||||
## 4. WebSocket 约束
|
||||
|
||||
- `/jupyter/api/kernels/*/channels` 与其他 WebSocket 路径使用 HTTP/1.1。
|
||||
- 转发 `Upgrade`、`Connection`、`Host`、`Origin` 和请求 ID。
|
||||
- 关闭代理缓冲,读超时不小于 3600 秒。
|
||||
- HTTP 和 WebSocket 使用同一票据校验规则。
|
||||
- 连接关闭只断开交互连接,不自动停止 Workspace Runtime。
|
||||
- Compose Demo 使用最长 5 分钟票据;前端必须继续发送编辑锁心跳,并在
|
||||
票据到期前重新签发。
|
||||
- Jupyter Lab HTML 中出现的内部 Token 必须由 Nginx 在返回浏览器前清除。
|
||||
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnsureRuntimeRequest:
|
||||
runtime_id: str
|
||||
workspace_id: str
|
||||
workspace_code: str
|
||||
owner_user_id: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeEndpoint:
|
||||
runtime_id: str
|
||||
runtime_type: str
|
||||
provider: str
|
||||
runtime_ref: str
|
||||
internal_url: str
|
||||
proxy_base_path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeHealth:
|
||||
runtime_id: str
|
||||
healthy: bool
|
||||
checked_at: datetime
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CreateSessionRequest:
|
||||
runtime_id: str
|
||||
workspace_code: str
|
||||
relative_path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RuntimeSession:
|
||||
runtime_id: str
|
||||
session_id: str
|
||||
jupyter_url: str
|
||||
reused: bool
|
||||
|
||||
|
||||
class RuntimeAdapter(Protocol):
|
||||
async def ensure_running(
|
||||
self,
|
||||
request: EnsureRuntimeRequest,
|
||||
) -> RuntimeEndpoint: ...
|
||||
|
||||
async def stop(self, runtime_id: str, reason: str) -> None: ...
|
||||
|
||||
async def restart(self, runtime_id: str) -> RuntimeEndpoint: ...
|
||||
|
||||
async def health(self, runtime_id: str) -> RuntimeHealth: ...
|
||||
|
||||
async def create_session(
|
||||
self,
|
||||
request: CreateSessionRequest,
|
||||
) -> RuntimeSession: ...
|
||||
|
||||
async def terminate_session(
|
||||
self,
|
||||
runtime_id: str,
|
||||
session_id: str,
|
||||
) -> None: ...
|
||||
Reference in New Issue
Block a user