feat: fix YAML field conversion, admin namespace, streaming logs, and vllm-serve deploy
- Fix Axios keysToSnake converting user values map keys (gpuMem -> gpu_mem) - Add skipRecurseKeys to keysToSnake for values/valuesYaml fields - Add values_yaml alt json tag and Normalize() in DTOs - Check both camelCase/snake_case in enforceNamespaceValues - Read both tailLines/tail_lines query param for diagnostics - Admin users can freely choose namespace in LaunchModal (free-text input) - Block only kube-system/kube-public/kube-node-lease for admin - Regular users keep existing namespace restrictions - Add SSE streaming pod logs endpoint (backend + frontend) - New PodLogStreamer interface and K8s Follow:true implementation - SSE handler with text/event-stream output - Frontend DiagnosticsModal: Stream button, auto-scroll, live indicator - Remove per-card Refresh button from InstanceCard (redundant with page refresh) - Deploy bge-m3 on vllm-serve 0.6.0 (gpuMem=10000, status=deployed)
This commit is contained in:
@ -76,7 +76,7 @@ import type {
|
||||
PutRegistriesRegistryIdPathParameters,
|
||||
} from './generated-orval/api.schemas';
|
||||
|
||||
import { customAxiosInstance } from './axios-mutator';
|
||||
import { AXIOS_INSTANCE, customAxiosInstance } from './axios-mutator';
|
||||
|
||||
import {
|
||||
GithubComOcdpClusterServiceInternalAdapterInputHttpDtoInstanceResponseLastOperation as GeneratedInstanceLastOperationEnum,
|
||||
@ -247,6 +247,88 @@ export const getInstanceDiagnostics = (
|
||||
params: options?.tailLines ? { tailLines: options.tailLines } : undefined,
|
||||
});
|
||||
|
||||
/**
|
||||
* Stream pod logs via SSE from the backend.
|
||||
* Returns an AbortController to cancel the stream at any time.
|
||||
*/
|
||||
export function streamInstanceLogs(
|
||||
clusterId: string,
|
||||
instanceId: string,
|
||||
pod: string,
|
||||
container: string,
|
||||
tailLines: number = 200,
|
||||
onLine: (line: string) => void,
|
||||
onDone: () => void,
|
||||
onError: (err: Error) => void,
|
||||
): AbortController {
|
||||
const controller = new AbortController();
|
||||
const baseUrl = AXIOS_INSTANCE.defaults.baseURL ?? "/api/v1";
|
||||
const authHeader = AXIOS_INSTANCE.defaults.headers.common["Authorization"] as string | undefined;
|
||||
|
||||
const params = new URLSearchParams({ pod, container, tailLines: String(tailLines) });
|
||||
const url = `${baseUrl}/clusters/${encodeURIComponent(clusterId)}/instances/${encodeURIComponent(instanceId)}/logs/stream?${params}`;
|
||||
|
||||
const headers: Record<string, string> = { Accept: "text/event-stream" };
|
||||
if (authHeader) {
|
||||
headers["Authorization"] = authHeader;
|
||||
}
|
||||
|
||||
fetch(url, { headers, signal: controller.signal })
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => response.statusText);
|
||||
onError(new Error(`HTTP ${response.status}: ${text}`));
|
||||
return;
|
||||
}
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
onError(new Error("ReadableStream not supported"));
|
||||
return;
|
||||
}
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
// Keep the last potentially-incomplete line in the buffer
|
||||
buffer = lines.pop() ?? "";
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || !trimmed.startsWith("data:")) continue;
|
||||
const data = trimmed.slice(5).trim();
|
||||
if (data === "[DONE]") {
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
if (data.startsWith("[ERROR]")) {
|
||||
onError(new Error(data.slice(7).trim()));
|
||||
continue;
|
||||
}
|
||||
onLine(data);
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
// Stream was intentionally cancelled - not an error
|
||||
return;
|
||||
}
|
||||
onError(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
onDone();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (err instanceof DOMException && err.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
onError(err instanceof Error ? err : new Error(String(err)));
|
||||
});
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
export const listRegistries = getRegistries;
|
||||
export const createRegistry = postRegistries;
|
||||
export const getRegistry = getRegistriesRegistryId;
|
||||
|
||||
Reference in New Issue
Block a user