feat: add provider websocket http data plane

This commit is contained in:
Codex
2026-05-16 16:03:53 +00:00
parent 659d1c6148
commit 28cc2af121
19 changed files with 545 additions and 22 deletions
@@ -46,7 +46,11 @@ RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 &&
WORKDIR /app/src/components/microservices/code-queue
COPY src/components/microservices/code-queue/package.json ./package.json
RUN test -d node_modules/typescript || bun install
WORKDIR /app
COPY package.json /app/package.json
RUN bun install
COPY src/components/shared /app/src/components/shared
WORKDIR /app/src/components/microservices/code-queue
COPY src/components/microservices/code-queue/tsconfig.json ./tsconfig.json
COPY src/components/microservices/code-queue/src ./src
@@ -31,6 +31,11 @@ services:
K3SCTL_KUBE_API_LOCAL_PORT: "${K3SCTL_KUBE_API_LOCAL_PORT:-6443}"
K3SCTL_KUBE_API_REMOTE_HOST: "${K3SCTL_KUBE_API_REMOTE_HOST:-127.0.0.1}"
K3SCTL_KUBE_API_REMOTE_PORT: "${K3SCTL_KUBE_API_REMOTE_PORT:-6443}"
K3SCTL_NATIVE_SERVICE_PROXY_ENABLED: "${K3SCTL_NATIVE_SERVICE_PROXY_ENABLED:-true}"
K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS: "${K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS:-1200}"
K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS: "${K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS:-10000}"
K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE: "${K3SCTL_NATIVE_SERVICE_URL_CODE_QUEUE:-}"
K3SCTL_NATIVE_SERVICE_URL_MDTODO: "${K3SCTL_NATIVE_SERVICE_URL_MDTODO:-}"
K3SCTL_MANIFEST_PATHS: "${K3SCTL_MANIFEST_PATHS:-k3s/code-queue.k3s.json,k3s/mdtodo.k3s.json}"
K3SCTL_SERVICES_JSON: "${K3SCTL_SERVICES_JSON:-[]}"
UNIDESK_LOG_RETENTION_BYTES: "${UNIDESK_LOG_RETENTION_BYTES:-512MiB}"
@@ -44,6 +44,9 @@ interface RuntimeConfig {
kubeApiProxyEnabled: boolean;
kubeconfigPath: string;
kubeApiConnectHost: string;
nativeServiceProxyEnabled: boolean;
nativeServiceProbeTimeoutMs: number;
nativeServiceFailureCooldownMs: number;
requestTimeoutMs: number;
healthTimeoutMs: number;
services: ManagedService[];
@@ -69,6 +72,7 @@ const logWriter = config.logFile
})
: null;
const kubeClient = loadKubeApiClient();
const nativeServiceFailures = new Map<string, number>();
logWriter?.prune();
function envString(name: string, fallback: string): string {
@@ -92,6 +96,11 @@ function envBool(name: string, fallback: boolean): boolean {
return fallback;
}
function envOptionalString(name: string): string | null {
const value = process.env[name];
return value === undefined || value.trim().length === 0 ? null : value.trim();
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
@@ -259,6 +268,9 @@ function readConfig(): RuntimeConfig {
kubeApiProxyEnabled: envBool("K3SCTL_KUBE_API_PROXY_ENABLED", true),
kubeconfigPath: envString("K3SCTL_KUBECONFIG_PATH", "/var/lib/unidesk/k3s/kubeconfig"),
kubeApiConnectHost: envString("K3SCTL_KUBE_API_CONNECT_HOST", "host.docker.internal"),
nativeServiceProxyEnabled: envBool("K3SCTL_NATIVE_SERVICE_PROXY_ENABLED", true),
nativeServiceProbeTimeoutMs: Math.max(250, Math.min(5000, envNumber("K3SCTL_NATIVE_SERVICE_PROBE_TIMEOUT_MS", 1200))),
nativeServiceFailureCooldownMs: Math.max(1000, Math.min(60_000, envNumber("K3SCTL_NATIVE_SERVICE_FAILURE_COOLDOWN_MS", 10_000))),
requestTimeoutMs: Math.max(1000, Math.min(120_000, envNumber("K3SCTL_REQUEST_TIMEOUT_MS", 30_000))),
healthTimeoutMs: Math.max(500, Math.min(30_000, envNumber("K3SCTL_HEALTH_TIMEOUT_MS", 2500))),
services: mergeServices([...manifestServices, ...inlineServices]),
@@ -399,6 +411,37 @@ function endpointProxyApiPath(service: ManagedService, endpoint: ManagedEndpoint
return `/api/v1/namespaces/${encodeURIComponent(namespace)}/services/${encodeURIComponent(serviceRef)}/proxy${safeTargetPath}`;
}
function nativeServiceFailureKey(service: ManagedService): string {
return `${service.namespace}/${service.id}`;
}
function nativeServiceUrl(service: ManagedService, targetPath: string, query = ""): URL {
const serviceName = routeString(service, "serviceName", service.id);
const servicePort = routeNumber(service, "servicePort", 80);
const safeTargetPath = targetPath.startsWith("/") ? targetPath : `/${targetPath}`;
const override = envOptionalString(`K3SCTL_NATIVE_SERVICE_URL_${service.id.toUpperCase().replace(/[^A-Z0-9]/gu, "_")}`);
const base = override ?? `http://${serviceName}.${service.namespace}.svc.cluster.local:${servicePort}`;
const url = new URL(safeTargetPath, base.replace(/\/+$/u, "/"));
url.search = query;
return url;
}
function nativeServiceProxyUsable(service: ManagedService): boolean {
if (!config.nativeServiceProxyEnabled) return false;
const failedAt = nativeServiceFailures.get(nativeServiceFailureKey(service));
return failedAt === undefined || Date.now() - failedAt >= config.nativeServiceFailureCooldownMs;
}
function rememberNativeServiceFailure(service: ManagedService, error: unknown): void {
nativeServiceFailures.set(nativeServiceFailureKey(service), Date.now());
log("warn", "native_service_proxy_failed", {
serviceId: service.id,
namespace: service.namespace,
cooldownMs: config.nativeServiceFailureCooldownMs,
error: errorToJson(error),
});
}
function kubernetesEndpointServiceRef(service: ManagedService, endpoint: ManagedEndpoint): { namespace: string; serviceRef: string } {
const base = new URL(endpoint.baseUrl);
if (base.protocol !== "kubernetes:") throw new Error(`endpoint ${endpoint.id} must use kubernetes:// baseUrl`);
@@ -460,6 +503,48 @@ async function kubeApiServiceProxyResponse(
return kubeApiProxyResponse(service, req, serviceProxyApiPath(service, targetPath), query, timeoutMs);
}
async function nativeServiceProxyResponse(
service: ManagedService,
req: Request,
targetPath: string,
query: string,
timeoutMs: number,
): Promise<Response | null> {
if (!nativeServiceProxyUsable(service)) return null;
const upstreamUrl = nativeServiceUrl(service, targetPath, query);
const headers = forwardHeaders(req);
const bodyText = req.method === "GET" || req.method === "HEAD" ? "" : await req.text();
const controller = new AbortController();
const nativeTimeoutMs = Math.min(timeoutMs, config.nativeServiceProbeTimeoutMs);
const timer = setTimeout(() => controller.abort(), nativeTimeoutMs);
const startedAt = Date.now();
try {
const upstream = await fetch(upstreamUrl, {
method: req.method,
headers,
body: bodyText.length > 0 ? bodyText : undefined,
signal: controller.signal,
});
const body = await boundedText(upstream, 8 * 1024 * 1024);
return new Response(body.text, {
status: upstream.status,
headers: {
"content-type": upstream.headers.get("content-type") ?? "application/octet-stream",
"x-unidesk-proxy-mode": "kubernetes-native-service",
"x-unidesk-k3s-service": service.id,
"x-unidesk-k3s-service-url": upstreamUrl.origin,
"x-unidesk-upstream-duration-ms": String(Date.now() - startedAt),
"x-unidesk-response-truncated": body.truncated ? "true" : "false",
},
});
} catch (error) {
rememberNativeServiceFailure(service, error);
return null;
} finally {
clearTimeout(timer);
}
}
async function kubeApiEndpointProxyResponse(
service: ManagedService,
endpoint: ManagedEndpoint,
@@ -574,15 +659,13 @@ async function probeKubernetesServiceActive(service: ManagedService): Promise<Js
async function probeKubernetesEndpoint(service: ManagedService, endpoint: ManagedEndpoint, active = false): Promise<JsonRecord> {
if (!active && endpoint.healthMode === "pod-ready") return await probeKubernetesPodReady(service, endpoint);
const checkedAt = new Date().toISOString();
const response = active
? await kubeApiServiceProxyResponse(
service,
new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
endpoint.healthPath,
"",
config.healthTimeoutMs,
)
: await kubeApiEndpointProxyResponse(
let response: Response;
if (active) {
const request = new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } });
response = await nativeServiceProxyResponse(service, request.clone(), endpoint.healthPath, "", config.healthTimeoutMs)
?? await kubeApiServiceProxyResponse(service, request, endpoint.healthPath, "", config.healthTimeoutMs);
} else {
response = await kubeApiEndpointProxyResponse(
service,
endpoint,
new Request("http://k3sctl-adapter.local/health", { method: "GET", headers: { accept: "application/json" } }),
@@ -590,6 +673,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage
"",
config.healthTimeoutMs,
);
}
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
const bodyText = await response.text();
let body: JsonValue = bodyText.slice(0, 2000);
@@ -605,7 +689,7 @@ async function probeKubernetesEndpoint(service: ManagedService, endpoint: Manage
baseUrl: endpoint.baseUrl,
healthPath: endpoint.healthPath,
healthMode: endpoint.healthMode,
proxyMode: "kubernetes-api-service-proxy",
proxyMode: response.headers.get("x-unidesk-proxy-mode") ?? "kubernetes-api-service-proxy",
route: service.route,
healthy: response.ok,
status: response.ok ? "healthy" : "unhealthy",
@@ -761,7 +845,19 @@ async function controlPlaneSnapshot(): Promise<JsonRecord> {
manifestPaths: config.manifestPaths,
managedServicesHealthy,
noFallback: true,
runtimePath: "frontend -> backend-core -> k3sctl-adapter -> kubernetes api service proxy -> k3s service",
runtimePath: config.nativeServiceProxyEnabled
? "frontend -> backend-core -> provider websocket HTTP tunnel -> k3sctl-adapter -> kubernetes native service/DNS -> k3s service"
: "frontend -> backend-core -> k3sctl-adapter -> kubernetes api service proxy -> k3s service",
nativeServiceProxy: {
enabled: config.nativeServiceProxyEnabled,
mode: "kubernetes-native-service",
failureCooldownMs: config.nativeServiceFailureCooldownMs,
failedServices: Array.from(nativeServiceFailures.entries()).map(([key, failedAt]) => ({
key,
failedAt: new Date(failedAt).toISOString(),
retryAfterMs: Math.max(0, config.nativeServiceFailureCooldownMs - (Date.now() - failedAt)),
})),
},
kubeApiProxy: {
enabled: config.kubeApiProxyEnabled,
configured: kubeClient !== null,
@@ -786,6 +882,8 @@ function forwardHeaders(request: Request): Headers {
async function proxyToService(service: ManagedService, req: Request, targetPath: string, query: string): Promise<Response> {
if (isKubernetesServiceRoute(service)) {
const native = await nativeServiceProxyResponse(service, req.clone(), targetPath, query, config.requestTimeoutMs);
if (native !== null) return native;
return kubeApiServiceProxyResponse(service, req, targetPath, query, config.requestTimeoutMs);
}
log("error", "k3sctl_route_not_kubernetes_service", { serviceId: service.id, route: service.route, noFallback: true });