v2.5 功能测试通过,待加上docker镜像

This commit is contained in:
2025-12-29 14:33:05 +08:00
parent ce8c2128b4
commit f02059126e
30 changed files with 1794 additions and 66 deletions

View File

@@ -43,19 +43,32 @@ def create_app(config_path: str) -> FastAPI:
app = FastAPI(title="mvp-v2", version="2.0")
def _require_token(req: Request) -> None:
def _auth(req: Request) -> dict[str, Any]:
token_env = v2_cfg.auth.token_env
expected = os.environ.get(token_env, "")
if not expected:
# Misconfigured service; treat as server error.
admin_token = os.environ.get(token_env, "")
if not admin_token:
raise HTTPException(status_code=500, detail=f"missing token env: {token_env}")
auth = req.headers.get("authorization") or ""
if not auth.startswith("Bearer "):
raise HTTPException(status_code=401, detail="missing bearer token")
got = auth.removeprefix("Bearer ").strip()
if got != expected:
if got == admin_token:
return {"user_id": "admin", "is_admin": True}
info = db.resolve_token(got)
if not info:
raise HTTPException(status_code=401, detail="invalid token")
if info.get("state") != "ACTIVE":
raise HTTPException(status_code=403, detail="user disabled")
return {"user_id": info["user_id"], "is_admin": False}
def _require_admin(req: Request) -> dict[str, Any]:
subject = _auth(req)
if not subject.get("is_admin"):
raise HTTPException(status_code=403, detail="admin required")
return subject
@app.on_event("startup")
def _startup() -> None:
@@ -66,9 +79,43 @@ def create_app(config_path: str) -> FastAPI:
def _shutdown() -> None:
stop_flag.set()
@app.post("/api/v2/users")
async def create_user(req: Request) -> dict[str, Any]:
_require_admin(req)
obj = await req.json()
if not isinstance(obj, dict):
raise HTTPException(status_code=400, detail="body must be a JSON object")
user_id = str(obj.get("user_id") or "").strip()
if not user_id:
raise HTTPException(status_code=400, detail="missing user_id")
display_name = obj.get("display_name")
try:
row = db.create_user(user_id=user_id, display_name=str(display_name) if display_name is not None else None)
except Exception as e:
raise HTTPException(status_code=409, detail=f"user create failed: {e!r}")
return {"user_id": row.get("user_id", user_id), "state": row.get("state", "ACTIVE")}
@app.post("/api/v2/users/{user_id}/tokens")
async def issue_token(user_id: str, req: Request) -> dict[str, Any]:
_require_admin(req)
u = db.get_user(user_id)
if not u:
raise HTTPException(status_code=404, detail="user not found")
token = db.issue_token(user_id=user_id)
return {"user_id": user_id, "token": token}
@app.post("/api/v2/users/{user_id}:disable")
async def disable_user(user_id: str, req: Request) -> dict[str, Any]:
_require_admin(req)
u = db.get_user(user_id)
if not u:
raise HTTPException(status_code=404, detail="user not found")
db.disable_user(user_id=user_id)
return {"user_id": user_id, "state": "DISABLED"}
@app.post("/api/v2/tasks")
async def submit_task(req: Request) -> dict[str, Any]:
_require_token(req)
subject = _auth(req)
body = (await req.body()).decode("utf-8")
obj = yaml.safe_load(body) or {}
if not isinstance(obj, dict):
@@ -79,9 +126,18 @@ def create_app(config_path: str) -> FastAPI:
except Exception as e:
raise HTTPException(status_code=400, detail=f"invalid jobspec: {e!r}")
task_id = new_task_id(spec.workload)
db.create_task(
# v2.5 constraint: training inputs must come from /private/common (dev/prod统一)。
common_prefix = ray_cfg.shared_root.rstrip("/") + "/common/"
for k, v in (("code_path", spec.code_path), ("train_file", spec.train_file), ("val_file", spec.val_file)):
if v is None:
continue
if not str(v).startswith(common_prefix):
raise HTTPException(status_code=400, detail=f"{k} must start with {common_prefix}")
task_id = new_task_id(spec.workload, user_id=str(subject["user_id"]))
db.create_task_v25(
task_id=task_id,
user_id=str(subject["user_id"]),
workload=spec.workload,
jobspec_yaml=body,
nnodes=spec.nnodes,
@@ -91,10 +147,13 @@ def create_app(config_path: str) -> FastAPI:
@app.get("/api/v2/tasks/{task_id}")
async def get_task(task_id: str, req: Request) -> dict[str, Any]:
_require_token(req)
subject = _auth(req)
row = db.get_task(task_id)
if not row:
raise HTTPException(status_code=404, detail="task not found")
if not subject.get("is_admin"):
if str(row.get("user_id") or "") != str(subject["user_id"]):
raise HTTPException(status_code=404, detail="task not found")
attempts = db.list_attempts(task_id)
latest_attempt = attempts[-1] if attempts else None
desired = {
@@ -125,18 +184,24 @@ def create_app(config_path: str) -> FastAPI:
@app.get("/api/v2/tasks/{task_id}/attempts")
async def get_attempts(task_id: str, req: Request) -> dict[str, Any]:
_require_token(req)
subject = _auth(req)
row = db.get_task(task_id)
if not row:
raise HTTPException(status_code=404, detail="task not found")
if not subject.get("is_admin"):
if str(row.get("user_id") or "") != str(subject["user_id"]):
raise HTTPException(status_code=404, detail="task not found")
return {"task_id": task_id, "attempts": db.list_attempts(task_id)}
@app.post("/api/v2/tasks/{task_id}:cancel")
async def cancel(task_id: str, req: Request) -> dict[str, Any]:
_require_token(req)
subject = _auth(req)
row = db.get_task(task_id)
if not row:
raise HTTPException(status_code=404, detail="task not found")
if not subject.get("is_admin"):
if str(row.get("user_id") or "") != str(subject["user_id"]):
raise HTTPException(status_code=404, detail="task not found")
state = str(row["state"])
if state in ("SUCCEEDED", "FAILED", "CANCELED"):
@@ -165,10 +230,13 @@ def create_app(config_path: str) -> FastAPI:
@app.get("/api/v2/tasks/{task_id}/logs")
async def logs(task_id: str, req: Request, tail: int = 2000, attempt: str = "latest") -> Response:
_require_token(req)
subject = _auth(req)
row = db.get_task(task_id)
if not row:
raise HTTPException(status_code=404, detail="task not found")
if not subject.get("is_admin"):
if str(row.get("user_id") or "") != str(subject["user_id"]):
raise HTTPException(status_code=404, detail="task not found")
attempts = db.list_attempts(task_id)
if not attempts:
raise HTTPException(status_code=404, detail="no attempts yet")
@@ -184,7 +252,9 @@ def create_app(config_path: str) -> FastAPI:
@app.get("/api/v2/queue")
async def queue(req: Request) -> dict[str, Any]:
_require_token(req)
return db.list_queue()
subject = _auth(req)
if subject.get("is_admin"):
return db.list_queue()
return db.list_queue(user_id=str(subject["user_id"]))
return app