chore(k8s): 下线 K8s 后端形态,移除依赖 @kubernetes/client-node

生产形态是单机 local(DEFAULT_DEPLOY_MODE=local,env 未覆盖)⇒ K8s 分支
在 local 下本来不可达;且该形态与官方 dsh 基座、插件体系均无关
=> 整体下线,并移除该形态唯一的第三方依赖。

移除(备份在 D:/github/_dsh_shenxian_K8s后端备份_20260915/,含还原命令与
「集群化方案要复用的模板清单」):
  src/supervisor/{k8s-spawner,leader,reconcile}.ts
  src/fs/k8s-user-fs.ts · src/tcp-bridge.ts · src/web/file-service.ts
  test/{k8s-spawner,leader}.test.mjs · scripts/smoke-file-service.mjs

改写调用方:cli.ts(5 条 import / 选主块 / file-service 与 tcp-bridge 两个
子命令 / dispatch / HELP)、web/server.ts(改为 fail-loud 守卫 + 恒用
LocalSpawner)、fs/provider.ts(只留 LocalUserFs)、package.json(测试与
smoke 入口),外加 3 处指向已删类型的悬空 JSDoc。

保留(集群化方案列为未来可选):deploy/ · poc/01-04 · Dockerfile.dsh ·
docs/k8s*.md(已加「代码已下线」状态横幅)· config.ts 的 K8s 配置字段与
DeployMode 联合类型。

验证:tsc --noEmit exit 0;npm test 36 测试 / 35 通过 / 0 失败 / 1 跳过;
npm run verify exit 0;依赖与被删符号全仓 0 命中。
This commit is contained in:
admin committed 2026-09-15 06:36:20 +08:00
1 parent ed0c3c0ad5
commit cf8b7b1f5c
19 files changed
+29 -2690

No files matched your search

-163
View File
@@ -1,163 +0,0 @@
// K8sSpawner unit tests against fake API clients. Covers the six defects fixed
// in Stage 4 without needing a cluster: 404-vs-500 error handling, full
// teardown of every generated object, idempotent Secret/ConfigMap, and the
// file-sidecar Pod + init container shape.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { K8sSpawner } from '../lib/supervisor/k8s-spawner.js'
import { resolveConfig } from '../lib/config.js'
import { AlreadyRunningError } from '../lib/supervisor/spawner.js'
import { FILE_SERVICE_PORT } from '../lib/web/file-service.js'
function notFound() {
const err = new Error('not found')
err.code = 404
return err
}
function conflict() {
const err = new Error('already exists')
err.code = 409
return err
}
/** A tiny in-memory fake for the K8s API surface K8sSpawner touches. */
function makeClients() {
const pods = new Map()
const services = new Map()
const secrets = new Map()
const configMaps = new Map()
const policies = new Map()
const jobs = new Map()
const deletes = []
return {
pods, services, secrets, configMaps, policies, jobs, deletes,
core: {
async readNamespacedPod({ name }) { return pods.get(name) ?? (() => { throw notFound() })() },
async createNamespacedPod({ body }) {
// Stored pods report Ready so `waitForPodReady` returns immediately.
const pod = { ...body, status: { phase: 'Running', conditions: [{ type: 'Ready', status: 'True' }] } }
pods.set(body.metadata.name, pod)
return pod
},
async deleteNamespacedPod({ name }) { deletes.push(`pod:${name}`); pods.delete(name) },
async readNamespacedSecret({ name }) { return secrets.get(name) ?? (() => { throw notFound() })() },
async createNamespacedSecret({ body }) { secrets.set(body.metadata.name, body); return body },
async deleteNamespacedSecret({ name }) { deletes.push(`secret:${name}`); secrets.delete(name) },
async readNamespacedConfigMap({ name }) { return configMaps.get(name) ?? (() => { throw notFound() })() },
async createNamespacedConfigMap({ body }) { configMaps.set(body.metadata.name, body); return body },
async deleteNamespacedConfigMap({ name }) { deletes.push(`configmap:${name}`); configMaps.delete(name) },
async readNamespacedService({ name }) { return services.get(name) ?? (() => { throw notFound() })() },
async createNamespacedService({ body }) { services.set(body.metadata.name, body); return body },
async deleteNamespacedService({ name }) { deletes.push(`service:${name}`); services.delete(name) },
},
networking: {
async readNamespacedNetworkPolicy({ name }) { return policies.get(name) ?? (() => { throw notFound() })() },
async createNamespacedNetworkPolicy({ body }) { policies.set(body.metadata.name, body); return body },
async deleteNamespacedNetworkPolicy({ name }) { deletes.push(`np:${name}`); policies.delete(name) },
},
batch: {
async createNamespacedJob({ body }) { jobs.set(body.metadata.name, body); return body },
async deleteNamespacedJob({ name }) { deletes.push(`job:${name}`); jobs.delete(name) },
},
}
}
function spawner(clients) {
const config = resolveConfig({ deployMode: 'k8s', controlPlaneImage: 'acr/cp:tag', dshImage: 'acr/dsh:tag' })
const db = { findUserInstance: async () => undefined }
return new K8sSpawner(config, db, async () => 'sk-test', async () => 100042, clients)
}
test('k8s: launch creates DSH Pod/Service/NP and calls ensureFileService first', async () => {
const clients = makeClients()
const s = spawner(clients)
const inst = await s.launch('u1', '/ws/proj')
assert.equal(inst.id, 'dsh-u1')
assert.ok(clients.pods.has('dsh-files-u1'), 'files Pod created first')
assert.ok(clients.pods.has('dsh-u1'), 'dsh Pod created')
assert.ok(clients.services.has('dsh-u1'), 'dsh Service created')
assert.ok(clients.policies.has('dsh-u1'), 'dsh NetworkPolicy created')
const filesPod = clients.pods.get('dsh-files-u1')
assert.equal(filesPod.spec.containers[0].args[0], 'file-service', 'files container runs the file-service subcommand')
assert.equal(filesPod.spec.initContainers.length, 1, 'files Pod carries the user-dir init container')
const filesMounts = filesPod.spec.containers[0].volumeMounts
assert.equal(filesMounts.length, 2, 'files container mounts its subPath + /tmp')
assert.equal(filesMounts[0].name, 'data')
assert.equal(filesMounts[0].subPath, 'u1', 'files container mounts <pvc>/u1 via subPath')
assert.equal(filesMounts[1].name, 'tmp', 'files container mounts emptyDir /tmp (read-only rootfs)')
assert.equal(filesPod.spec.containers[0].securityContext.readOnlyRootFilesystem, true, 'files container rootfs is read-only')
const initMount = filesPod.spec.initContainers[0].volumeMounts[0]
assert.equal(initMount.name, 'data-root', 'init container mounts the PVC root (no subPath)')
assert.equal(initMount.subPath, undefined, 'init container has no subPath')
await assert.rejects(() => s.launch('u1', '/ws/proj'), AlreadyRunningError)
})
test('k8s: endpointFor returns the Pod IP (not Service DNS) for a Running Pod', async () => {
const clients = makeClients()
const s = spawner(clients)
assert.equal(await s.endpointFor('u1'), undefined, 'absent Pod → undefined')
clients.pods.set('dsh-u1', { status: { phase: 'Running', podIP: '10.42.0.7' } })
assert.deepEqual(await s.endpointFor('u1'), { host: '10.42.0.7', port: 8081 })
clients.pods.set('dsh-u1', { status: { phase: 'Pending' } })
assert.equal(await s.endpointFor('u1'), undefined, 'non-Running Pod → undefined')
})
test('k8s: stop deletes every generated object (Pod/Service/NP/Job/ConfigMap)', async () => {
const clients = makeClients()
const s = spawner(clients)
await s.launch('u1', '/ws/proj', '- insert:\n')
// Pre-seed the patch ConfigMap + watchdog Job as a prior launch would have.
clients.configMaps.set('dsh-u1-patch', { metadata: { name: 'dsh-u1-patch' } })
clients.jobs.set('dsh-u1-watchdog', { metadata: { name: 'dsh-u1-watchdog' } })
await s.stop('u1')
const names = clients.deletes
for (const expected of ['pod:dsh-u1', 'service:dsh-u1', 'np:dsh-u1', 'job:dsh-u1-watchdog', 'configmap:dsh-u1-patch']) {
assert.ok(names.includes(expected), `stop deletes ${expected}`)
}
// stop() must NOT touch the files Pod (it lives independently of the DSH).
assert.ok(clients.pods.has('dsh-files-u1'), 'files Pod survives a DSH stop')
})
test('k8s: a 404 is swallowed, but 403/500 propagate', async () => {
const clients = makeClients()
const s = spawner(clients)
// endpointFor must not mask a real failure as "not running".
clients.core.readNamespacedPod = async () => { const e = new Error('forbidden'); e.code = 403; throw e }
await assert.rejects(() => s.endpointFor('u1'), (err) => err.code === 403, '403 surfaces')
clients.core.readNamespacedPod = async () => { throw notFound() }
assert.equal(await s.endpointFor('u1'), undefined, '404 is "no Pod"')
})
test('k8s: ensureSecret is idempotent (no 409 on relaunch)', async () => {
const clients = makeClients()
const s = spawner(clients)
// First call: read → 404 → create.
await s.ensureFileService('u1') // exercises the image gate, not the secret
await s.launch('u1', '/ws')
// Simulate a stale Secret still present after a stop that skipped it.
clients.secrets.set('dsh-key-u1', { metadata: { name: 'dsh-key-u1' } })
clients.pods.delete('dsh-u1')
await s.launch('u1', '/ws') // read → present → no create, no throw
assert.ok(clients.secrets.has('dsh-key-u1'), 'secret still present, no 409')
})
test('k8s: ensureFileService fails loudly without a control-plane image', async () => {
const config = resolveConfig({ deployMode: 'k8s', controlPlaneImage: '', dshImage: 'acr/dsh:tag' })
const s = new K8sSpawner(config, { findUserInstance: async () => undefined }, async () => null, async () => 100042, makeClients())
await assert.rejects(() => s.ensureFileService('u1'), /CONTROL_PLANE_IMAGE/, 'missing image is a hard failure')
})
test('k8s: files NetworkPolicy only allows the control plane on the file port', async () => {
const clients = makeClients()
const s = spawner(clients)
await s.ensureFileService('u1')
const np = clients.policies.get('dsh-files-u1')
assert.equal(np.spec.ingress[0]._from[0].podSelector.matchLabels.app, 'dsh-orchestrator')
assert.equal(np.spec.ingress[0].ports[0].port, FILE_SERVICE_PORT)
// No 443 egress for the files sidecar — it never reaches the LLM API.
assert.equal(np.spec.egress.some((rule) => (rule.ports ?? []).some((p) => p.port === 443)), false)
})
-117
View File
@@ -1,117 +0,0 @@
// Leader election + reconcile planning, without a cluster. `planReconcile` is
// pure; `LeaderElector` is driven through a fake CoordinationV1Api and a fake
// clock so the acquire / back-off / take-over state machine is deterministic.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { LeaderElector } from '../lib/supervisor/leader.js'
import { planReconcile } from '../lib/supervisor/reconcile.js'
// The real client deserializes V1MicroTime back to an ISO *string*, so fake the
// same shape here to exercise the normalize-on-read path.
const lease = (holder, renewMs, transitions = 0, rv = '1') => ({
metadata: { resourceVersion: rv },
spec: {
holderIdentity: holder,
renewTime: new Date(renewMs).toISOString(),
leaseTransitions: transitions,
},
})
test('reconcile: relaunch desired mains with a missing/stopped Pod, delete orphans', () => {
const desired = [
{ id: 'dsh-a', userId: 'a', role: 'main', folder: '/ws', patch: null },
{ id: 'dsh-b', userId: 'b', role: 'main', folder: '/ws', patch: null },
]
const live = [
{ name: 'dsh-a', userId: 'a', running: true, crashed: false },
{ name: 'dsh-orphan', userId: 'x', running: true, crashed: false },
]
const plan = planReconcile(desired, live)
assert.deepEqual(plan.launch.map((i) => i.userId), ['b'], 'missing main is relaunched')
assert.deepEqual(plan.delete.map((p) => p.userId), ['x'], 'orphan Pod is deleted')
})
test('reconcile: a crashed (non-running) Pod still counts as absent', () => {
const plan = planReconcile(
[{ id: 'dsh-a', userId: 'a', role: 'main', folder: '/ws', patch: null }],
[{ name: 'dsh-a', userId: 'a', running: false, crashed: true }],
)
assert.equal(plan.launch.length, 1, 'crashed Pod → relaunch')
assert.equal(plan.delete.length, 0)
})
test('leader: first candidate creates the lease and leads', async () => {
let created = false
let patched = []
const coordination = {
async createNamespacedLease({ body }) {
created = true
return body
},
async readNamespacedLease() { throw Object.assign(new Error('nf'), { code: 404 }) },
async replaceNamespacedLease({ body }) { patched.push(body) },
}
const started = []
const elector = new LeaderElector({ namespace: 'dsh', identity: 'pod-1', coordination, now: () => 1_000_000 })
elector.setLeadershipCallbacks((f) => started.push(f))
await elector.start()
assert.equal(created, true, 'first candidate creates the lease')
assert.equal(elector.isLeader, true)
assert.equal(started[0].holder, 'pod-1')
assert.equal(started[0].operationId, 0)
elector.stop()
})
test('leader: a second candidate backs off while a live holder leads', async () => {
const coordination = {
async createNamespacedLease() { throw Object.assign(new Error('exists'), { code: 409 }) },
async readNamespacedLease() { return lease('pod-1', 10_000_000) }, // renew 1s ago, live
async replaceNamespacedLease() { throw new Error('should not replace') },
}
const elector = new LeaderElector({
namespace: 'dsh',
identity: 'pod-2',
coordination,
now: () => 11_000_000, // 1s after the holder's renew, still within 15s
})
await elector.start()
assert.equal(elector.isLeader, false, 'live holder → back off')
elector.stop()
})
test('leader: a stale lease is taken over with a bumped transition', async () => {
let patched
const coordination = {
async createNamespacedLease() { throw Object.assign(new Error('exists'), { code: 409 }) },
async readNamespacedLease() { return lease('pod-1', 10_000_000, 3, 'rv-9') }, // renew 20s ago, expired
async replaceNamespacedLease({ body }) { patched = body },
}
const started = []
const elector = new LeaderElector({
namespace: 'dsh',
identity: 'pod-2',
coordination,
now: () => 30_000_000,
})
elector.setLeadershipCallbacks((f) => started.push(f))
await elector.start()
assert.equal(elector.isLeader, true, 'expired lease → take over')
assert.equal(patched.metadata.resourceVersion, 'rv-9', 'CAS on the read resourceVersion')
assert.equal(patched.spec.leaseTransitions, 4, 'transition bumps')
assert.equal(started[0].operationId, 4, 'fencing token carries the new transition')
elector.stop()
})
test('leader: re-acquiring our own lease renews rather than bumps', async () => {
let patched
const coordination = {
async createNamespacedLease() { throw Object.assign(new Error('exists'), { code: 409 }) },
async readNamespacedLease() { return lease('pod-1', 30_000_000, 0, 'rv-2') },
async replaceNamespacedLease({ body }) { patched = body },
}
const elector = new LeaderElector({ namespace: 'dsh', identity: 'pod-1', coordination, now: () => 30_000_000 })
await elector.start()
assert.equal(elector.isLeader, true)
assert.equal(patched.spec.leaseTransitions, 0, 'renewing preserves transitions (does not bump)')
elector.stop()
})