/** * K8s backend for per-user DSH lifecycle (docs/k8s.md §5.2/§4.3/§4.4/§4.8). * * Each user gets a `dsh-` Pod (dsh + socat sidecar), a Headless Service, * a NetworkPolicy, and a `dsh-key-` Secret for the API key. A crash * pulls up a one-shot `dsh--watchdog` Job. The control plane runs * in-cluster and talks to the K8s API via @kubernetes/client-node. * @module dshs/supervisor/k8s-spawner */ import * as k8s from '@kubernetes/client-node' import type { ServerConfig } from '../config.js' import type { DbAdapter } from '../db/adapter.js' import { HANDOFF_FILE, HOME_DIR, USERS_DIR, WORKSPACE_DIR } from '../fs/workspace.js' import { FILE_SERVICE_PORT, USER_ROOT_ENV } from '../web/file-service.js' import type { Fencing } from './leader.js' import { AlreadyRunningError, type Endpoint, type Instance, type LivePod, type Spawner, type UserStatus } from './spawner.js' /** Loopback port the dsh container binds; the socat sidecar bridges 8081 → 8080. */ const DSH_LOOPBACK_PORT = 8080 /** Sidecar bridge port the per-user Service targets (80 → 8081). */ const SOCAT_PORT = 8081 /** Task the one-shot headless watchdog runs (executes the handoff command). */ const WATCHDOG_TASK = 'Read DSHS_HANDOFF_PATH. If it contains a JSON {"command": ...}, run that command. Then exit.' /** Shared RWX PVC every user's Pod mounts via subPath. */ const USERS_PVC = 'dsh-users' /** Per-user resource names (deterministic — idempotent create). */ function names(userId: string) { return { pod: `dsh-${userId}`, service: `dsh-${userId}`, networkPolicy: `dsh-${userId}`, secret: `dsh-key-${userId}`, job: `dsh-${userId}-watchdog`, patch: `dsh-${userId}-patch`, filesPod: `dsh-files-${userId}`, filesService: `dsh-files-${userId}`, filesNetworkPolicy: `dsh-files-${userId}`, } } /** The data root inside every per-user Pod, independent of the control plane's * own `dataRoot` (which under k8s points at a volume the Pod never sees). */ const POD_DATA_ROOT = '/var/lib/dshs' /** Home/workspace paths inside the DSH Pod (docs/k8s.md §4.3). Built with * POSIX separators — the control plane may be developed/tested on Windows. */ function userPaths(userId: string): { home: string; ws: string; mount: string } { const mount = `${POD_DATA_ROOT}/${USERS_DIR}/${userId}` return { home: `${mount}/${HOME_DIR}`, ws: `${mount}/${WORKSPACE_DIR}`, mount } } /** Pod-safe labels shared by the DSH Pod/Service/NetworkPolicy/Job. */ function podLabels(userId: string): { app: string; user: string } { return { app: 'dsh', user: userId } } /** Labels for the per-user file sidecar (a distinct `app`, so its own * Service/NetworkPolicy select it without touching the DSH Pod). */ function filesLabels(userId: string): { app: string; user: string } { return { app: 'dsh-files', user: userId } } /** API-key env entry, omitted when the user has no key. */ function apiKeyEnv(userId: string, apiKey: string | null): k8s.V1EnvVar[] { if (apiKey === null) return [] return [{ name: 'DEEPSEEK_API_KEY', valueFrom: { secretKeyRef: { name: names(userId).secret, key: 'key' } } }] } /** Common per-user container security context (non-root + drop ALL + seccomp + * read-only rootfs — /tmp comes from an emptyDir, docs/k8s.md Phase 4). */ function containerSecurity(uid: number): k8s.V1SecurityContext { return { runAsNonRoot: true, runAsUser: uid, allowPrivilegeEscalation: false, capabilities: { drop: ['ALL'] }, seccompProfile: { type: 'RuntimeDefault' }, readOnlyRootFilesystem: true, } } /** Writable scratch volume + mount for containers whose rootfs is read-only. */ function tmpVolume(): k8s.V1Volume[] { return [{ name: 'tmp', emptyDir: {} }] } function tmpMount(): k8s.V1VolumeMount { return { name: 'tmp', mountPath: '/tmp' } } /** The shared RWX volume (subPath mounts per-user at use time). */ function dataVolume(): k8s.V1Volume[] { return [{ name: 'data', persistentVolumeClaim: { claimName: USERS_PVC } }] } /** The shared RWX volume mounted at its **root** (no subPath) so an init * container can create/chown `/` as the user's own uid. */ function dataRootVolume(): k8s.V1Volume[] { return [{ name: 'data-root', persistentVolumeClaim: { claimName: USERS_PVC } }] } /** Every per-user Pod/Job references ACR private images, so each must carry the * cluster's pull secret (the control-plane Deployment has it in its manifest, * but generated Pods don't inherit it). */ function pullSecrets(name: string): k8s.V1LocalObjectReference[] { return name === '' ? [] : [{ name }] } /** Fencing labels stamped onto resources created by the leader, so a successor * can identify work a dead leader left in flight (docs/k8s.md §5.3). */ function stampFencing(labels: Record, fencing: Fencing | undefined): void { if (fencing === undefined) return labels['dsh.io/holder'] = fencing.holder labels['dsh.io/operation-id'] = String(fencing.operationId) } /** * K8s backend implementing {@link Spawner}. State lives in the cluster; every * method is a K8s API call (or a read). */ export class K8sSpawner implements Spawner { private readonly core: k8s.CoreV1Api private readonly networking: k8s.NetworkingV1Api private readonly batch: k8s.BatchV1Api private readonly namespace: string private readonly kc: k8s.KubeConfig private fencing: Fencing | undefined constructor( private readonly config: ServerConfig, private readonly db: DbAdapter, private readonly resolveApiKey: (userId: string) => Promise, private readonly resolveUid: (userId: string) => Promise, clients?: { core: k8s.CoreV1Api; networking: k8s.NetworkingV1Api; batch: k8s.BatchV1Api }, ) { // Injecting clients short-circuits cluster auth (used by tests). Otherwise // build them from the in-cluster config, which needs a mounted SA token. const kc = new k8s.KubeConfig() if (clients === undefined) { kc.loadFromCluster() clients = { core: kc.makeApiClient(k8s.CoreV1Api), networking: kc.makeApiClient(k8s.NetworkingV1Api), batch: kc.makeApiClient(k8s.BatchV1Api), } } this.kc = kc this.core = clients.core this.networking = clients.networking this.batch = clients.batch this.namespace = config.k8sNamespace } async launch(userId: string, folder: string, patch?: string): Promise { const n = names(userId) if (await this.podExists(n.pod)) throw new AlreadyRunningError(userId) await this.ensureFileService(userId) // the DSH Pod's subPath must already exist const apiKey = await this.resolveApiKey(userId) const uid = await this.resolveUid(userId) const hasPatch = this.config.enablePatch && patch !== undefined if (hasPatch) await this.ensurePatchConfigMap(n.patch, patch) await this.ensureSecret(n.secret, apiKey) await this.ensurePod(n.pod, userId, uid, apiKey, hasPatch ? n.patch : undefined) await this.ensureService(n.service, userId) await this.ensureNetworkPolicy(n.networkPolicy, userId) try { await this.db.upsertInstance({ id: n.pod, userId, role: 'main', status: 'starting', folder, patch }) } catch (err) { this.logError(err) // reconcile needs this row; don't fail the launch over it } return { id: n.pod, userId, role: 'main', folder, status: 'starting', patch } } async restartMain(userId: string): Promise { const desired = await this.db.findUserInstance(userId, 'main') if (desired === undefined) return undefined await this.stop(userId) return await this.launch(userId, desired.folder ?? '', desired.patch ?? undefined) } async restartAllMains(): Promise { const rows = await this.db.listInstancesByRole('main') for (const row of rows) { try { await this.restartMain(row.userId) } catch (err) { this.logError(err) // keep broadcasting — one pod failure must not abort the rest } } } async spawnWatchdog(userId: string): Promise { const n = names(userId) const apiKey = await this.resolveApiKey(userId) const uid = await this.resolveUid(userId) const { home, ws, mount } = userPaths(userId) const jobLabels = podLabels(userId) const podTemplateLabels = podLabels(userId) stampFencing(jobLabels, this.fencing) stampFencing(podTemplateLabels, this.fencing) await this.batch.createNamespacedJob({ namespace: this.namespace, body: { apiVersion: 'batch/v1', kind: 'Job', metadata: { name: n.job, namespace: this.namespace, labels: jobLabels }, spec: { ttlSecondsAfterFinished: 300, template: { metadata: { labels: podTemplateLabels }, spec: { automountServiceAccountToken: false, imagePullSecrets: pullSecrets(this.config.imagePullSecret), restartPolicy: 'Never', securityContext: { runAsNonRoot: true, runAsUser: uid, fsGroup: uid, seccompProfile: { type: 'RuntimeDefault' }, }, containers: [ { name: 'dsh', image: this.config.dshImage, args: ['--profile', 'headless', WATCHDOG_TASK], env: [ { name: 'HOME', value: ws }, { name: 'DSH_HOME', value: home }, { name: 'DSHS_ROLE', value: 'watchdog' }, { name: 'DSHS_HANDOFF_PATH', value: `${mount}/${HANDOFF_FILE}` }, ...apiKeyEnv(userId, apiKey), ], volumeMounts: [{ name: 'data', mountPath: mount, subPath: userId }, tmpMount()], securityContext: containerSecurity(uid), }, ], volumes: [...dataVolume(), ...tmpVolume()], }, }, }, } }) return { id: n.job, userId, role: 'watchdog', folder: ws, status: 'starting' } } async status(userId: string): Promise { return { main: await this.readInstance(names(userId).pod, userId, 'main') } } async endpointFor(userId: string): Promise { let pod: k8s.V1Pod try { pod = await this.core.readNamespacedPod({ name: names(userId).pod, namespace: this.namespace }) } catch (err) { if (this.isNotFound(err)) return undefined throw err // 403/500 are real failures, not "not running" } if (pod.status?.phase !== 'Running') return undefined // Dial the Pod IP directly (not the Headless Service DNS): re-reading the Pod // on every request gives the *current* IP immediately after a restart, so a // rebuilt Pod never leaves the proxy pointing at a stale IP for the ~30s DNS // TTL. Port is the sidecar's 8081 (no kube-proxy DNAT on Headless Services). const ip = pod.status?.podIP if (ip === undefined || ip === '') return undefined return { host: ip, port: SOCAT_PORT } } // k8s 模式无 launch token 概念(Pod 就绪由 endpointFor 的 phase 判断),no-op。 async waitForLaunchTokenForUser(_userId: string, _timeoutMs?: number): Promise {} /** * k8s spawner 未实现探活(本部署走本地 spawner)。返回 ok:true 保持与旧行为一致: * 不做回滚,交由 k8s 自身 readiness/liveness 处理。 */ async restartAndProbe(_userId: string, _settleMs?: number): Promise<{ ok: boolean; reason: string }> { return { ok: true, reason: "k8s spawner: 探活未实现" } } async stop(userId: string): Promise { const n = names(userId) await this.ignoreNotFound(() => this.core.deleteNamespacedPod({ name: n.pod, namespace: this.namespace })) await this.ignoreNotFound(() => this.core.deleteNamespacedService({ name: n.service, namespace: this.namespace })) await this.ignoreNotFound(() => this.networking.deleteNamespacedNetworkPolicy({ name: n.networkPolicy, namespace: this.namespace })) await this.ignoreNotFound(() => this.batch.deleteNamespacedJob({ name: n.job, namespace: this.namespace })) await this.ignoreNotFound(() => this.core.deleteNamespacedConfigMap({ name: n.patch, namespace: this.namespace })) try { await this.db.deleteInstance(n.pod) // desired state is gone once stopped } catch (err) { this.logError(err) } } /** No-op: per-user Pods outlive any single control-plane replica (reconcile/leader manages them). */ async teardown(): Promise {} /** Activity signal is unused under k8s: the reconcile loop idles-reaps by * session presence (reconcile.ts Phase 4), not by proxied-traffic recency. */ touch(_userId: string): void {} /** Bring up the user's file sidecar (docs/k8s.md §4.10). The sidecar is a * distinct always-on Pod so the desktop is usable *before* the on-demand DSH * launches; its init container creates the user's subPath directory. */ async ensureFileService(userId: string): Promise { if (this.config.controlPlaneImage === '') { throw new Error('DSHS_CONTROL_PLANE_IMAGE is required in k8s mode (file sidecar image)') } const n = names(userId) const uid = await this.resolveUid(userId) await this.ensureFilesPod(n.filesPod, userId, uid) await this.ensureFilesService(n.filesService, userId) await this.ensureFilesNetworkPolicy(n.filesNetworkPolicy, userId) // The Headless Service only publishes an A record once the Pod is Ready; // the caller resolves it immediately, so block until it comes up. await this.waitForPodReady(n.filesPod) } // --- reconcile / watch support (leader-only callers) --- /** Stamp the current leader's fencing token onto resources created from now on. */ setFencing(fencing: Fencing | undefined): void { this.fencing = fencing } /** Main DSH Pods (`app=dsh`), mapped to the shape the controller needs. */ async listUserPods(): Promise { const res = await this.core.listNamespacedPod({ namespace: this.namespace, labelSelector: 'app=dsh' }) return (res.items ?? []).map((pod) => { const phase = pod.status?.phase ?? '' return { name: pod.metadata?.name ?? '', userId: pod.metadata?.labels?.user ?? '', running: phase === 'Running', crashed: phase === 'Failed' || phase === 'Succeeded', } }) } /** Recreate a lost Service/NetworkPolicy for a user whose main Pod exists. */ async ensureUserResources(userId: string): Promise { await this.ensureService(names(userId).service, userId) await this.ensureNetworkPolicy(names(userId).networkPolicy, userId) } /** Watch main DSH Pods; `callback` fires on add/update/delete with their state. */ watchMainPods(callback: (pod: LivePod) => void): k8s.Informer & k8s.ObjectCache { const informer = k8s.makeInformer( this.kc, `/api/v1/namespaces/${this.namespace}/pods`, () => this.core.listNamespacedPod({ namespace: this.namespace, labelSelector: 'app=dsh' }), 'app=dsh', ) const emit = (obj: k8s.V1Pod | undefined): void => { const phase = obj?.status?.phase ?? '' callback({ name: obj?.metadata?.name ?? '', userId: obj?.metadata?.labels?.user ?? '', running: phase === 'Running', crashed: phase === 'Failed' || phase === 'Succeeded', }) } informer.on('add', (obj) => emit(obj)) informer.on('update', (obj) => emit(obj)) informer.on('delete', (obj) => emit(obj)) return informer } /** Best-effort error surface for the controller's tick loop. */ logError(err: unknown): void { console.error('[dsh-reconcile]', err) } // --- helpers --- private async podExists(name: string): Promise { try { await this.core.readNamespacedPod({ name, namespace: this.namespace }) return true } catch (err) { if (this.isNotFound(err)) return false throw err // 403/500 are real failures, not "no Pod" } } private async readInstance(name: string, userId: string, role: 'main' | 'watchdog'): Promise { try { const pod = await this.core.readNamespacedPod({ name, namespace: this.namespace }) const status = pod.status?.phase === 'Running' ? 'running' : pod.status?.phase === 'Failed' ? 'crashed' : 'starting' return { id: name, userId, role, folder: '', status } } catch (err) { if (this.isNotFound(err)) return undefined throw err } } /** Create-or-replace, so a relaunch after `stop()` (which deletes these) can * never 409 on a stale Secret/ConfigMap from a prior launch. */ private async ensureSecret(name: string, apiKey: string | null): Promise { if (apiKey === null) return await this.replace({ read: () => this.core.readNamespacedSecret({ name, namespace: this.namespace }), create: () => this.core.createNamespacedSecret({ namespace: this.namespace, body: { apiVersion: 'v1', kind: 'Secret', metadata: { name, namespace: this.namespace }, type: 'Opaque', stringData: { key: apiKey }, } }), del: () => this.core.deleteNamespacedSecret({ name, namespace: this.namespace }), }) } /** Create-or-replace a generated resource. Reads first for the cheap path, * then treats a 409 race by deleting and recreating (we own every resource * this helper touches, so replacement is safe). */ private async replace(ops: { read: () => Promise create: () => Promise del: () => Promise }): Promise { try { await ops.read() } catch (err) { if (!this.isNotFound(err)) throw err try { await ops.create() } catch (createErr) { if (!this.isConflict(createErr)) throw createErr await this.ignoreNotFound(ops.del) await ops.create() } } } private async ensurePod(name: string, userId: string, uid: number, apiKey: string | null, patchConfigMapName?: string): Promise { const { home, ws, mount } = userPaths(userId) const args = ['--profile', 'web', '--host', '127.0.0.1', '--port', String(DSH_LOOPBACK_PORT)] // The runtime plugin (dshs/runtime) is baked into the dsh image // but loaded via --patch; the rendered patch is mounted at /etc/dsh/patch.yml. if (patchConfigMapName !== undefined) args.splice(1, 0, '--patch', '/etc/dsh/patch.yml') const labels = podLabels(userId) stampFencing(labels, this.fencing) await this.core.createNamespacedPod({ namespace: this.namespace, body: { apiVersion: 'v1', kind: 'Pod', metadata: { name, namespace: this.namespace, labels }, spec: { automountServiceAccountToken: false, imagePullSecrets: pullSecrets(this.config.imagePullSecret), hostNetwork: false, hostPID: false, securityContext: { runAsNonRoot: true, runAsUser: uid, fsGroup: uid, seccompProfile: { type: 'RuntimeDefault' }, }, containers: [ { name: 'dsh', image: this.config.dshImage, args, // DSH binds loopback only, so a tcpSocket probe (which hits the Pod IP) // would never succeed; probe 127.0.0.1:8080 from inside the container. readinessProbe: { exec: { command: ['node', '-e', 'require("http").get("http://127.0.0.1:8080", r => process.exit(r.statusCode < 500 ? 0 : 1)).on("error", () => process.exit(1))'] }, initialDelaySeconds: 5, periodSeconds: 3, }, env: [ { name: 'HOME', value: ws }, { name: 'DSH_HOME', value: home }, ...apiKeyEnv(userId, apiKey), ], volumeMounts: [ { name: 'data', mountPath: mount, subPath: userId }, ...(patchConfigMapName !== undefined ? [{ name: 'patch', mountPath: '/etc/dsh', readOnly: true }] : []), tmpMount(), ], securityContext: containerSecurity(uid), resources: { requests: { cpu: '500m', memory: '1Gi' }, limits: { cpu: '2', memory: '4Gi' } }, }, { name: 'sidecar', image: this.config.controlPlaneImage, args: ['tcp-bridge', `0.0.0.0:${SOCAT_PORT}`, `127.0.0.1:${DSH_LOOPBACK_PORT}`], ports: [{ containerPort: SOCAT_PORT }], securityContext: containerSecurity(uid), resources: { requests: { cpu: '10m', memory: '32Mi' }, limits: { cpu: '100m', memory: '128Mi' } }, }, ], volumes: [ ...dataVolume(), ...(patchConfigMapName !== undefined ? [{ name: 'patch', configMap: { name: patchConfigMapName } }] : []), ...tmpVolume(), ], }, } }) } private async ensurePatchConfigMap(name: string, patch: string): Promise { await this.replace({ read: () => this.core.readNamespacedConfigMap({ name, namespace: this.namespace }), create: () => this.core.createNamespacedConfigMap({ namespace: this.namespace, body: { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name, namespace: this.namespace }, data: { 'patch.yml': patch }, } }), del: () => this.core.deleteNamespacedConfigMap({ name, namespace: this.namespace }), }) } private async ensureService(name: string, userId: string): Promise { await this.replace({ read: () => this.core.readNamespacedService({ name, namespace: this.namespace }), create: () => this.core.createNamespacedService({ namespace: this.namespace, body: { apiVersion: 'v1', kind: 'Service', metadata: { name, namespace: this.namespace }, spec: { clusterIP: 'None', // Headless: no ClusterIP, DNS A record → Pod IP selector: podLabels(userId), ports: [{ port: 80, targetPort: SOCAT_PORT }], }, } }), del: () => this.core.deleteNamespacedService({ name, namespace: this.namespace }), }) } private async ensureNetworkPolicy(name: string, userId: string): Promise { await this.replace({ read: () => this.networking.readNamespacedNetworkPolicy({ name, namespace: this.namespace }), create: () => this.networking.createNamespacedNetworkPolicy({ namespace: this.namespace, body: { apiVersion: 'networking.k8s.io/v1', kind: 'NetworkPolicy', metadata: { name, namespace: this.namespace }, spec: { podSelector: { matchLabels: podLabels(userId) }, policyTypes: ['Ingress', 'Egress'], ingress: [{ _from: [{ podSelector: { matchLabels: { app: 'dsh-orchestrator' } } }] }], egress: [ { // DNS to anywhere: the DNS backend varies by distribution (kube-dns / // coredns / node-local-dns on ACK), so don't pin a pod label. to: [{ ipBlock: { cidr: '0.0.0.0/0' } }], ports: [ { port: 53, protocol: 'UDP' }, { port: 53, protocol: 'TCP' }, ], }, { to: this.config.egressCidrs.length > 0 ? this.config.egressCidrs.map((cidr) => ({ ipBlock: { cidr, except: ['169.254.169.254/32', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] } })) : [{ ipBlock: { cidr: '0.0.0.0/0', except: ['169.254.169.254/32', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] } }], ports: [{ port: 443 }], }, ], }, } }), del: () => this.networking.deleteNamespacedNetworkPolicy({ name, namespace: this.namespace }), }) } private async ensureFilesPod(name: string, userId: string, uid: number): Promise { const mount = userPaths(userId).mount await this.replace({ read: () => this.core.readNamespacedPod({ name, namespace: this.namespace }), create: () => this.core.createNamespacedPod({ namespace: this.namespace, body: { apiVersion: 'v1', kind: 'Pod', metadata: { name, namespace: this.namespace, labels: filesLabels(userId) }, spec: { automountServiceAccountToken: false, imagePullSecrets: pullSecrets(this.config.imagePullSecret), hostNetwork: false, hostPID: false, securityContext: { runAsNonRoot: true, runAsUser: uid, fsGroup: uid, seccompProfile: { type: 'RuntimeDefault' }, }, // Creates `//{ws,home}` as the user's uid (docs/k8s.md // §4.9). Runs on the PVC *root* (no subPath), because the subPath dir // may not exist yet or be root-owned; the user's 0700 dir keeps other // users' files out of reach. initContainers: [ { name: 'init-user', image: this.config.controlPlaneImage, command: ['sh', '-ec', `mkdir -p /mnt/${userId}/${WORKSPACE_DIR} /mnt/${userId}/${HOME_DIR} && chmod 0700 /mnt/${userId}`], volumeMounts: [{ name: 'data-root', mountPath: '/mnt' }], securityContext: containerSecurity(uid), }, ], containers: [ { name: 'files', image: this.config.controlPlaneImage, args: ['file-service'], env: [{ name: USER_ROOT_ENV, value: mount }], ports: [{ containerPort: FILE_SERVICE_PORT }], readinessProbe: { tcpSocket: { port: FILE_SERVICE_PORT }, initialDelaySeconds: 2, periodSeconds: 3 }, volumeMounts: [{ name: 'data', mountPath: mount, subPath: userId }, tmpMount()], securityContext: containerSecurity(uid), resources: { requests: { cpu: '50m', memory: '128Mi' }, limits: { cpu: '200m', memory: '256Mi' } }, }, ], volumes: [...dataVolume(), ...dataRootVolume(), ...tmpVolume()], }, } }), del: () => this.core.deleteNamespacedPod({ name, namespace: this.namespace }), }) } private async ensureFilesService(name: string, userId: string): Promise { await this.replace({ read: () => this.core.readNamespacedService({ name, namespace: this.namespace }), create: () => this.core.createNamespacedService({ namespace: this.namespace, body: { apiVersion: 'v1', kind: 'Service', metadata: { name, namespace: this.namespace }, spec: { clusterIP: 'None', selector: filesLabels(userId), ports: [{ port: FILE_SERVICE_PORT, targetPort: FILE_SERVICE_PORT }], }, } }), del: () => this.core.deleteNamespacedService({ name, namespace: this.namespace }), }) } private async ensureFilesNetworkPolicy(name: string, userId: string): Promise { await this.replace({ read: () => this.networking.readNamespacedNetworkPolicy({ name, namespace: this.namespace }), create: () => this.networking.createNamespacedNetworkPolicy({ namespace: this.namespace, body: { apiVersion: 'networking.k8s.io/v1', kind: 'NetworkPolicy', metadata: { name, namespace: this.namespace }, spec: { podSelector: { matchLabels: filesLabels(userId) }, policyTypes: ['Ingress', 'Egress'], ingress: [{ _from: [{ podSelector: { matchLabels: { app: 'dsh-orchestrator' } } }], ports: [{ port: FILE_SERVICE_PORT }] }], // Files only; the sidecar never talks to the LLM API. egress: [ { to: [{ ipBlock: { cidr: '0.0.0.0/0' } }], ports: [ { port: 53, protocol: 'UDP' }, { port: 53, protocol: 'TCP' }, ], }, ], }, } }), del: () => this.networking.deleteNamespacedNetworkPolicy({ name, namespace: this.namespace }), }) } /** Poll until a Pod's Ready condition is true, or fail after `timeoutMs`. */ private async waitForPodReady(name: string, timeoutMs = 60_000): Promise { const deadline = Date.now() + timeoutMs for (;;) { const pod = await this.core.readNamespacedPod({ name, namespace: this.namespace }) const ready = pod.status?.conditions?.some((c) => c.type === 'Ready' && c.status === 'True') if (ready) return if (Date.now() >= deadline) throw new Error(`Pod ${name} not ready within ${timeoutMs}ms`) await new Promise((resolve) => setTimeout(resolve, 1000)) } } private isNotFound(err: unknown): boolean { return (err as { code?: number }).code === 404 } private isConflict(err: unknown): boolean { return (err as { code?: number }).code === 409 } private async ignoreNotFound(fn: () => Promise): Promise { try { await fn() } catch (err) { // `@kubernetes/client-node` throws `ApiException` with a `code` field. if (this.isNotFound(err)) return throw err } } }