mirror of
https://github.com/sredevopsorg/deepseek-kubernetes-sandbox.git
synced 2026-11-03 01:13:24 +00:00
fix(gateway): grant sandboxclaims patch and map API errors to 503 (#45)
* fix(gateway): grant patch on sandboxclaims for token rotation The SSO landing path calls K8sClient.patch_token(), which patches the `dsh-<user>` SandboxClaim's token-hash annotation whenever the browser has no valid gateway session: first visit, an expired or foreign cookie, or a gateway restart that drops the in-memory session store. The gateway Role only granted get/list/create/delete on sandboxclaims, so the patch returned 403 and, because ApiException is not handled, every such GET / answered a bare 500 "Internal Server Error". Grant the missing `patch` verb on extensions.agents.x-k8s.io/sandboxclaims, and update the least-privilege comment plus the two docs that enumerate the Role verbs. Verified with `make check` (versions, example layout, manifests) and `yamllint`; the 403 and the resulting 500 were reproduced from a running install and cleared by applying the equivalent Role patch (RBAC is evaluated per request, so no gateway restart is needed). * fix(gateway): return 503 instead of a bare 500 on Kubernetes API errors Every gateway route talks to the Kubernetes API, and ApiException used to be unhandled: Starlette's ServerErrorMiddleware turned it into a bare 500 "Internal Server Error" plus an ERROR traceback. That masked a missing RBAC verb as a client-visible crash on GET / (the SSO landing path), and it would equally hide an API outage or throttling. Register an ApiException handler that logs one credential-free line (status/reason) and answers 503 + Retry-After with a fixed body, so the Kubernetes Status payload (which can name cluster objects) is never proxied. Guard the first get_claim() in the WebSocket route the same way: WebSocket scopes bypass Starlette's HTTP exception handlers, so it now closes with 1011 instead of escaping as a traceback. Tests assert the 503 + Retry-After response, cover the SSO landing path that produced the reported 500 (forbidden claim patch), and assert the WebSocket close code. The failure-semantics tables document the new behaviour. --------- Co-authored-by: SREDevOpsAgents <agents@sredevops.org>
This commit is contained in:
committed by
GitHub
parent
c5b8afde48
commit
fbe886f3b9
@@ -1510,7 +1510,13 @@ against the JSON API. The browser session is an HttpOnly cookie.</p>
|
||||
user = websocket.path_params["user"]
|
||||
if not valid_user(user):
|
||||
return await self._ws_reject(websocket, 4401)
|
||||
claim = await self.k8s.get_claim(user)
|
||||
try:
|
||||
claim = await self.k8s.get_claim(user)
|
||||
except ApiException:
|
||||
# Starlette has no exception handler for WebSocket scopes, so an
|
||||
# unhandled API error would escape as a traceback and an unclean
|
||||
# close. 1011 keeps the HTTP 503 meaning on the WS channel.
|
||||
return await self._ws_reject(websocket, 1011)
|
||||
if claim is None:
|
||||
return await self._ws_reject(websocket, 4401)
|
||||
token, cookie_user = self.credentials(websocket.headers, websocket.cookies)
|
||||
@@ -1746,6 +1752,28 @@ async def client_gone(request: Request, exc: ClientDisconnect) -> Response:
|
||||
return Response(status_code=CLIENT_CLOSED_REQUEST)
|
||||
|
||||
|
||||
async def k8s_api_error(request: Request, exc: ApiException) -> Response:
|
||||
"""Answer a Kubernetes API failure with 503 instead of a bare 500.
|
||||
|
||||
The gateway is the only path between the browser and the cluster, so an
|
||||
API outage, a throttle or a missing RBAC verb is a server-side
|
||||
availability problem, not a client error. A 503 keeps ingress and
|
||||
browser retry semantics honest, and the body is a fixed message: the
|
||||
Kubernetes ``Status`` payload can name cluster objects and is never
|
||||
proxied. The API ``status``/``reason`` is logged once, credential-free.
|
||||
"""
|
||||
print(
|
||||
"gateway: kubernetes API error: "
|
||||
f"status={getattr(exc, 'status', None)} reason={getattr(exc, 'reason', None)}",
|
||||
flush=True,
|
||||
)
|
||||
return JSONResponse(
|
||||
{"error": "cluster API unavailable, retry shortly"},
|
||||
status_code=503,
|
||||
headers={"Retry-After": "10"},
|
||||
)
|
||||
|
||||
|
||||
def create_app(
|
||||
settings: Settings | None = None,
|
||||
k8s=None,
|
||||
@@ -1778,7 +1806,7 @@ def create_app(
|
||||
]
|
||||
app = Starlette(
|
||||
routes=routes,
|
||||
exception_handlers={ClientDisconnect: client_gone},
|
||||
exception_handlers={ClientDisconnect: client_gone, ApiException: k8s_api_error},
|
||||
lifespan=_lifespan(gateway, http_client if owns_http_client else None),
|
||||
)
|
||||
app.state.gateway = gateway
|
||||
|
||||
Reference in New Issue
Block a user