97 lines
3.4 KiB
Python
97 lines
3.4 KiB
Python
from typing import Annotated
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Header, HTTPException, Request, Response, Security
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
|
|
from kf_oj.contracts.judging import JobManifest, LeaseBinding
|
|
from kf_oj.modules.submissions.public import WorkerAccessDenied, WorkerReadBusy
|
|
from kf_oj.modules.submissions.worker_access import WorkerAccess
|
|
|
|
router = APIRouter(prefix="/internal/evaluations", tags=["worker artifacts"])
|
|
bearer = HTTPBearer(auto_error=False, scheme_name="WorkerLease")
|
|
|
|
|
|
def grant(
|
|
evaluation_id: UUID,
|
|
attempt_id: UUID,
|
|
worker: UUID,
|
|
fence: int,
|
|
credentials: HTTPAuthorizationCredentials | None,
|
|
) -> tuple[LeaseBinding, str]:
|
|
if (
|
|
credentials is None
|
|
or credentials.scheme.lower() != "bearer"
|
|
or len(credentials.credentials) != 64
|
|
):
|
|
raise HTTPException(401, "worker lease required")
|
|
try:
|
|
return LeaseBinding(
|
|
evaluation_id=evaluation_id,
|
|
attempt_id=attempt_id,
|
|
worker_id=worker,
|
|
fencing_version=fence,
|
|
), credentials.credentials
|
|
except ValueError:
|
|
raise HTTPException(404, "worker grant rejected") from None
|
|
|
|
|
|
def component(request: Request) -> WorkerAccess:
|
|
access = getattr(request.app.state, "worker_access", None)
|
|
if access is None:
|
|
raise HTTPException(503, "worker artifact service unavailable")
|
|
return access
|
|
|
|
|
|
@router.get("/{evaluation_id}/attempts/{attempt_id}/manifest", response_model=JobManifest)
|
|
async def manifest(
|
|
evaluation_id: UUID,
|
|
attempt_id: UUID,
|
|
request: Request,
|
|
worker: Annotated[UUID, Header(alias="X-Worker-ID")],
|
|
fence: Annotated[int, Header(alias="X-Fencing-Version", ge=1)],
|
|
credentials: Annotated[HTTPAuthorizationCredentials | None, Security(bearer)],
|
|
):
|
|
binding, token = grant(evaluation_id, attempt_id, worker, fence, credentials)
|
|
try:
|
|
payload = await component(request).manifest(binding, token)
|
|
except WorkerAccessDenied:
|
|
raise HTTPException(404, "worker grant rejected") from None
|
|
except WorkerReadBusy:
|
|
raise HTTPException(429, "worker reads temporarily limited") from None
|
|
return Response(payload, media_type="application/json", headers={"Cache-Control": "no-store"})
|
|
|
|
|
|
@router.get(
|
|
"/{evaluation_id}/attempts/{attempt_id}/artifacts/{digest}",
|
|
response_class=Response,
|
|
responses={
|
|
200: {
|
|
"content": {
|
|
"application/octet-stream": {"schema": {"type": "string", "format": "binary"}}
|
|
}
|
|
}
|
|
},
|
|
)
|
|
async def artifact(
|
|
evaluation_id: UUID,
|
|
attempt_id: UUID,
|
|
digest: str,
|
|
request: Request,
|
|
worker: Annotated[UUID, Header(alias="X-Worker-ID")],
|
|
fence: Annotated[int, Header(alias="X-Fencing-Version", ge=1)],
|
|
credentials: Annotated[HTTPAuthorizationCredentials | None, Security(bearer)],
|
|
):
|
|
binding, token = grant(evaluation_id, attempt_id, worker, fence, credentials)
|
|
try:
|
|
payload = await component(request).artifact(binding, token, digest)
|
|
except WorkerAccessDenied:
|
|
raise HTTPException(404, "worker grant rejected") from None
|
|
except WorkerReadBusy:
|
|
raise HTTPException(429, "worker reads temporarily limited") from None
|
|
return Response(
|
|
payload,
|
|
media_type="application/octet-stream",
|
|
headers={"Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"},
|
|
)
|