feat(overlay): 覆盖网络线序①–⑮ 代码与测试产物入库
覆盖网络线累积产物(此前只在工作区、未入版本库):
- 新增 relay 子系统 src/net/relay/**(wire/duplex/server/client/dialer/switcher/directory/identity/keys/placement/network/addr-override/main/index)
- 新增 src/worker/relay-tunnel.ts、src/web/routes/overlay.ts
- 新增观测/演练脚本 overlay-probe、overlay-failover-drill、overlay-keyring、overlay-holepunch、overlay-jitter、overlay-wan、overlay-relaykey-add、relay-mem-calibrate
- 新增测试 12 个(relay / relay-failover / remote-spawner / instance-port / overlay-{network,auth,bootstrap,identity} / remote-user-fs 等)
验收基线:npm test = 162 pass / 0 fail / 1 skip;--scene all = 12 PASS / 0 SKIP / 0 FAIL;overlay-probe = 12/12
This commit is contained in:
1 parent
640813e84e
commit
146c3d25ef
53 files changed
+16187
-63
No files matched your search
+15
-5
@@ -53,7 +53,7 @@ types.setTypeParser(20, (value: string) => Number(value))
|
||||
const USER_COLS = 'id, username, pass_hash, role, home_dir, api_key_ref, created_at, approved_by, uid'
|
||||
const DOMAIN_COLS = 'id, user_id, domain, verified, nginx_config, updated_at'
|
||||
const BUSINESS_PLUGIN_COLS = 'id, name, description, version, tgz_path, file_size, uploaded_by, created_at, updated_at'
|
||||
const HOST_COLS = 'id, endpoint, agent_token, capacity_mb, used_mb, status, last_heartbeat'
|
||||
const HOST_COLS = 'id, endpoint, via, network_id, agent_token, capacity_mb, used_mb, status, last_heartbeat'
|
||||
const INSTANCE_COLS =
|
||||
'id, user_id, workspace_id, role, pid, port, status, started_at, last_exit, exit_code, last_error, folder, patch, '
|
||||
+ 'host_id, epoch, heartbeat_at, lease_until' // v7 集群化归属/租约(T08 S2)—— 漏了它们会让 hostId 恒为 null
|
||||
@@ -602,15 +602,25 @@ export class PgAdapter implements DbAdapter {
|
||||
async upsertDshHost(input: UpsertDshHostInput): Promise<DshHost> {
|
||||
try {
|
||||
const { rows } = await this.pool.query(
|
||||
`INSERT INTO dsh_hosts (id, endpoint, agent_token, capacity_mb, used_mb, status, last_heartbeat)
|
||||
VALUES ($1, $2, $3, $4, 0, $5, NULL)
|
||||
`INSERT INTO dsh_hosts (id, endpoint, via, network_id, agent_token, capacity_mb, used_mb, status, last_heartbeat)
|
||||
VALUES ($1, $2, COALESCE($3::text, 'manager-ssh'), COALESCE($4::text, 'ops'), $5, $6, 0, $7, NULL)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
endpoint = excluded.endpoint,
|
||||
via = COALESCE($3::text, dsh_hosts.via),
|
||||
network_id = COALESCE($4::text, dsh_hosts.network_id),
|
||||
agent_token = excluded.agent_token,
|
||||
capacity_mb = excluded.capacity_mb,
|
||||
status = excluded.status
|
||||
RETURNING id, endpoint, agent_token, capacity_mb, used_mb, status, last_heartbeat`,
|
||||
[input.id, input.endpoint, input.agentToken, input.capacityMb, input.status ?? 'up'],
|
||||
RETURNING id, endpoint, via, network_id, agent_token, capacity_mb, used_mb, status, last_heartbeat`,
|
||||
[
|
||||
input.id,
|
||||
input.endpoint,
|
||||
input.via ?? null,
|
||||
input.networkId ?? null,
|
||||
input.agentToken,
|
||||
input.capacityMb,
|
||||
input.status ?? 'up',
|
||||
],
|
||||
)
|
||||
return toDshHost(rows[0] as Record<string, unknown>)
|
||||
} catch (e) {
|
||||
|
||||
+22
-5
@@ -595,19 +595,36 @@ export function deleteBusinessPlugin(db: Database, id: string): boolean {
|
||||
// ⚠️ local 模式**不调用**这些函数(`LocalSpawner` 靠进程内 Map + 单机互斥),
|
||||
// 所以它们的存在不会改变现有单机行为。
|
||||
|
||||
const HOST_COLS = 'id, endpoint, agent_token, capacity_mb, used_mb, status, last_heartbeat'
|
||||
const HOST_COLS = 'id, endpoint, via, network_id, agent_token, capacity_mb, used_mb, status, last_heartbeat'
|
||||
|
||||
/** 注册/更新一台 worker。join 幂等:同 id 重复执行 = 更新(并把它标回 `up`)。 */
|
||||
/** 注册/更新一台 worker。join 幂等:同 id 重复执行 = 更新(并把它标回 `up`)。
|
||||
*
|
||||
* ⚠️ `via` 与 `networkId` **省略时不覆盖已有值**(`COALESCE` 到旧值):否则一次不带它们的
|
||||
* join 会把回填好的 `local` / 已划定的网络归属冲回列默认值(那种漂移在换中继、建第二张网之前
|
||||
* **看不出症状**)。
|
||||
*/
|
||||
export function upsertDshHost(db: Database, input: UpsertDshHostInput): DshHost {
|
||||
prepare(db, `
|
||||
INSERT INTO dsh_hosts (id, endpoint, agent_token, capacity_mb, used_mb, status, last_heartbeat)
|
||||
VALUES (?, ?, ?, ?, 0, ?, NULL)
|
||||
INSERT INTO dsh_hosts (id, endpoint, via, network_id, agent_token, capacity_mb, used_mb, status, last_heartbeat)
|
||||
VALUES (?, ?, COALESCE(?, 'manager-ssh'), COALESCE(?, 'ops'), ?, ?, 0, ?, NULL)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
endpoint = excluded.endpoint,
|
||||
via = COALESCE(?, dsh_hosts.via),
|
||||
network_id = COALESCE(?, dsh_hosts.network_id),
|
||||
agent_token = excluded.agent_token,
|
||||
capacity_mb = excluded.capacity_mb,
|
||||
status = excluded.status
|
||||
`).run(input.id, input.endpoint, input.agentToken, input.capacityMb, input.status ?? 'up')
|
||||
`).run(
|
||||
input.id,
|
||||
input.endpoint,
|
||||
input.via ?? null,
|
||||
input.networkId ?? null,
|
||||
input.agentToken,
|
||||
input.capacityMb,
|
||||
input.status ?? 'up',
|
||||
input.via ?? null,
|
||||
input.networkId ?? null,
|
||||
)
|
||||
const row = prepare(db, `SELECT ${HOST_COLS} FROM dsh_hosts WHERE id = ?`).get(input.id)
|
||||
return toDshHost(row as Record<string, unknown>)
|
||||
}
|
||||
|
||||
@@ -345,6 +345,43 @@ CREATE INDEX IF NOT EXISTS idx_dsh_instances_host ON dsh_instances (host_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_dsh_instances_lease ON dsh_instances (lease_until);
|
||||
`
|
||||
|
||||
// v8(覆盖网络 S2):`dsh_hosts.via` = **"这台 worker 经谁可达"**(`Reachability.via` 词表)。
|
||||
//
|
||||
// 为什么必须加列:现网两条 `endpoint` 字符串**同形、语义不同**,表里无法表达"经谁中转"
|
||||
// (会合中继拆分方案 §2 C3)⇒ 换会合 / 中继组件时无处安放,越晚改代价越大。
|
||||
//
|
||||
// 加列**带默认值** `manager-ssh` ⇒ **先加列 → 再改代码 → 最后回填**,任一步中断都不崩;
|
||||
// 旧代码只读 `endpoint`,完全不受影响(列可留着不删 ⇒ 零风险回滚)。
|
||||
// ⚠️ 默认值对 `w-106`(隧道落点)正确、对 `w-47`(同机直连)**不正确** ⇒ 回填由部署步骤
|
||||
// 显式 `UPDATE ... WHERE id='w-47'` 完成,**不写进迁移**(迁移是静态 SQL,写死 hostId 在别的部署上会错)。
|
||||
const SQLITE_V8 = `
|
||||
ALTER TABLE dsh_hosts ADD COLUMN via TEXT NOT NULL DEFAULT 'manager-ssh';
|
||||
`
|
||||
|
||||
const PG_V8 = `
|
||||
ALTER TABLE dsh_hosts ADD COLUMN via TEXT NOT NULL DEFAULT 'manager-ssh';
|
||||
`
|
||||
|
||||
// v9(覆盖网络 ②·P0-1):`dsh_hosts.network_id` = **这台 worker 属于哪张网**。
|
||||
//
|
||||
// 为什么必须加列:relay 的会话表 / 端点表原先只按 `hostId` 索引 —— 即平台自己的 Worker 隧道与
|
||||
// 未来的用户设备挤进**同一个扁平命名空间**。今天只有 1 张网,问题不显形;一进第二类节点就会变成
|
||||
// 「一张巨网 + 靠 ACL 兜」,而写错一条 ACL 就泄露 ⇒ 与「**权限只准收窄**」直接冲突。
|
||||
// ⇒ 网维度必须做成**结构性**的:控制面这一侧落成显式列(本迁移),数据面落成 relay 的
|
||||
// 「按网络分桶白名单 + 同网校验」(`src/net/relay/server.ts`)。
|
||||
//
|
||||
// 加列**带默认值** `ops`(运维网)⇒ **存量行天然正确**(现网 47 / 106 / manager 全在运维网),
|
||||
// 且顺序是「**先加列 → 再改代码 → 最后才谈回填**」,任一步中断都不崩;旧代码只读旧列,零影响。
|
||||
// ⚠️ 与 v8 的 `via` 同一条纪律:**迁移里不写 `UPDATE ... WHERE id='...'`**(迁移是静态 SQL,
|
||||
// 写死 hostId 在别的部署上会错)—— 真要改某台机的网络归属,由部署步骤显式 `UPDATE` 完成。
|
||||
const SQLITE_V9 = `
|
||||
ALTER TABLE dsh_hosts ADD COLUMN network_id TEXT NOT NULL DEFAULT 'ops';
|
||||
`
|
||||
|
||||
const PG_V9 = `
|
||||
ALTER TABLE dsh_hosts ADD COLUMN network_id TEXT NOT NULL DEFAULT 'ops';
|
||||
`
|
||||
|
||||
interface Migration {
|
||||
version: number
|
||||
name: string
|
||||
@@ -360,6 +397,8 @@ const MIGRATIONS: readonly Migration[] = [
|
||||
{ version: 5, name: 'business plugin candidate pool', sqlite: SQLITE_V5, pg: PG_V5 },
|
||||
{ version: 6, name: 'user model providers', sqlite: SQLITE_V6, pg: PG_V6 },
|
||||
{ version: 7, name: 'cluster host registry + instance lease', sqlite: SQLITE_V7, pg: PG_V7 },
|
||||
{ version: 8, name: 'host reachability via (覆盖网络 S2)', sqlite: SQLITE_V8, pg: PG_V8 },
|
||||
{ version: 9, name: 'host network id (覆盖网络 P0-1)', sqlite: SQLITE_V9, pg: PG_V9 },
|
||||
]
|
||||
|
||||
/** Apply unapplied SQLite migrations inside a single transaction. */
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
* @module dshs/db/types
|
||||
*/
|
||||
|
||||
// 覆盖网络 S2:`via` 列的**词表来源**统一在 `net/reachability`(同属 ③ 能力层 ⇒ 不破分层)。
|
||||
// 这里只引一个常量,不引取址逻辑 —— `db` 不做网络判定。
|
||||
import { VIA_MANAGER_SSH } from '../net/reachability.js'
|
||||
|
||||
export type UserRole = 'admin' | 'pending' | 'active' | 'disabled'
|
||||
|
||||
/** A full user row, including secrets (never serialized to clients). */
|
||||
@@ -308,11 +312,38 @@ export function toBusinessPlugin(row: Record<string, unknown>): BusinessPlugin {
|
||||
/** Worker 健康状态(`dsh_hosts.status` 的 CHECK 镜像)。 */
|
||||
export type DshHostStatus = 'up' | 'draining' | 'down'
|
||||
|
||||
/**
|
||||
* 节点默认所属网 = **运维网**(覆盖网络 P0-1)。
|
||||
*
|
||||
* ⚠️ 与 `src/net/relay/network.ts` 的 `OPS_NETWORK` 是**同一个词表的同一个值**,但本层
|
||||
* (④基础层)**不 import 能力层**(分层纪律,`npm run check:layering` 会拦)⇒ 两边各写一次
|
||||
* 字面量,**改动必须两处同改**。取值口径见 `network.ts`:`ops` | `u:<userId>`。
|
||||
*/
|
||||
export const DEFAULT_HOST_NETWORK = 'ops'
|
||||
|
||||
/** 一台承载用户实例的 worker(= 设计里的 Worker 节点)。 */
|
||||
export interface DshHost {
|
||||
id: string
|
||||
/** agent 的内网地址,如 `10.0.1.11:9000`。 */
|
||||
endpoint: string
|
||||
/**
|
||||
* **经谁可达**(覆盖网络 S2):`Reachability.via` 的词表值,指向一个 `Rendezvous` 实现。
|
||||
*
|
||||
* ⚠️ 两条现网记录的 `endpoint` 字符串同形、语义不同 ⇒ **不能靠 endpoint 猜**:
|
||||
* · `w-47` = `127.0.0.1:19100` = **同机直连**(Manager 与 worker 同机)⇒ `local`
|
||||
* · `w-106` = `127.0.0.1:19000` = **Manager 主机上 sshd 的反向隧道落点** ⇒ `manager-ssh`
|
||||
*/
|
||||
via: string
|
||||
/**
|
||||
* **属于哪张网**(覆盖网络 P0-1,`dsh_hosts.network_id`)。
|
||||
*
|
||||
* `ops` = 运维网(平台自己的机器:`manager` / `w-47` / `w-106`,以及未来的中继与骨干);
|
||||
* `u:<userId>` = 该用户名下的全部设备。取值与 `src/net/relay/network.ts` 同一词表。
|
||||
*
|
||||
* 为什么它是**结构性**维度而不是又一个标签:relay 侧按 `<network>/<hostId>` 建会话、且
|
||||
* `DIAL` 必须**同网** ⇒ 不同网络的节点之间连"能不能拨"这一步都走不到(写错配置也不会泄露)。
|
||||
*/
|
||||
networkId: string
|
||||
/** 内部 HMAC 密钥(**只应存在于 DB 与 Manager 内存**,绝不经 API 返回)。 */
|
||||
agentToken: string
|
||||
/** 该机可用内存预算(MB);0 = 不承载实例(只做门户/控制)。 */
|
||||
@@ -328,6 +359,13 @@ export interface DshHost {
|
||||
export interface UpsertDshHostInput {
|
||||
id: string
|
||||
endpoint: string
|
||||
/** 不传 ⇒ 走列默认值 `manager-ssh`(S2 加列时的默认,保证旧 join 脚本不改也能用)。 */
|
||||
via?: string
|
||||
/**
|
||||
* 所属网(P0-1)。**不传 ⇒ 不覆盖已有值**(`COALESCE` 到旧值,与 `via` 同款),
|
||||
* 新行走列默认值 `ops`。⇒ 旧 join 脚本一行不改也不会把某台机的网络归属冲掉。
|
||||
*/
|
||||
networkId?: string
|
||||
agentToken: string
|
||||
capacityMb: number
|
||||
status?: DshHostStatus
|
||||
@@ -356,6 +394,13 @@ export function toDshHost(row: Record<string, unknown>): DshHost {
|
||||
return {
|
||||
id: row.id as string,
|
||||
endpoint: row.endpoint as string,
|
||||
// S2:列有默认值 ⇒ 行上必然有值;仍做一次兜底,避免"旧库 + 新代码"组合下出现 undefined。
|
||||
via: ((row.via as string | null) ?? '') === '' ? VIA_MANAGER_SSH : (row.via as string),
|
||||
// P0-1:列有默认值 ⇒ 行上必然有值;仍做一次兜底("旧库 + 新代码"组合下不出现 undefined)。
|
||||
networkId:
|
||||
((row.network_id as string | null) ?? '') === ''
|
||||
? DEFAULT_HOST_NETWORK
|
||||
: (row.network_id as string),
|
||||
agentToken: row.agent_token as string,
|
||||
capacityMb: (row.capacity_mb as number | null) ?? 0,
|
||||
usedMb: (row.used_mb as number | null) ?? 0,
|
||||
|
||||
Reference in new issue
Block a user