diff --git a/docs/reference/provider-gateway.md b/docs/reference/provider-gateway.md index b04b89f1..ead0e4f7 100644 --- a/docs/reference/provider-gateway.md +++ b/docs/reference/provider-gateway.md @@ -124,6 +124,8 @@ provider-gateway 连接成功后必须周期性上报节点 CPU、内存、硬 backend-core 可以通过真实 WebSocket 调度向在线 provider 下发 `provider.upgrade`。`mode: "plan"` 只返回升级计划,用于 E2E 和人工预检;`mode: "schedule"` 会要求 provider-gateway 通过本地 Docker socket 启动一个 detached updater 容器。updater 的固定策略是先执行 `docker compose build provider-gateway`,构建成功后只按 `com.docker.compose.project` 与 `com.docker.compose.service=provider-gateway` label 删除旧 provider-gateway 容器,最后执行 `docker compose up -d --no-deps --force-recreate provider-gateway`。`--no-deps` 是强制要求,`--force-recreate` 用于保证 provider 重新注册能力标签并避免 compose no-op;升级 provider-gateway 时不得重建或停止 database、backend-core、frontend。对 D518、D601 这类计算节点,`mode: "schedule"` 是正式重建/升级 `provider-gateway` 容器的唯一标准路径;升级执行路径使用 Docker socket 和只读仓库挂载,不使用 Host SSH 维护桥作为自动调度通道。 +使用 compact build context 的节点必须把 context 视为派生缓存,不得让它成为版本真相。D601 这类节点可能让 Compose 从 `.state/provider--build-context` 构建兜底镜像,以便复用本地基础镜像和缩小构建输入;`provider.upgrade mode=schedule` 在 `docker compose build` 前必须从只读 `/workspace/src/components/provider-gateway` 与 `/workspace/src/components/shared` 刷新该 context,并把宿主 `.state` 以可写方式只挂给 detached updater。验收不能只看 updater 日志中的 `Built` 或 `candidate provider-gateway validated and promoted`,必须以主 server 可观测的 `providerGatewayVersion`、`unideskCapabilities` 和目标能力字段为准;如果线上仍上报旧版本,说明运行镜像没有真正更新,必须先修复构建 context 再重跑标准升级。 + 远程升级策略固定为 always-enabled:只要 provider-gateway 在线并声明 `provider.upgrade`,`mode: "schedule"` 就必须真正调度升级容器,不允许被 `PROVIDER_UPGRADE_ENABLED=false`、前端隐藏按钮或服务端特殊名单禁用。升级能力的安全边界不是开关,而是显式 `PROVIDER_UPGRADE_*` 配置、Docker socket 权限、只读仓库挂载、固定 Compose service 和 `--no-deps` 约束。升级计划中必须展示 `policy: "always-enabled"`、updater 容器名、runner image、workspace、Compose project/service、env file、compose file 和实际 `docker run` 命令,方便前端任务历史与 CLI debug 直接诊断。 `mode: "schedule"` 的成功返回只代表 updater 已被调度,最终升级成败由候选 gateway 自验证决定。updater 必须先按 Compose 构建新镜像,再用旧容器的 `Config.Env` 生成候选 env-file,并复用旧容器的 Docker socket、日志目录、SSH 私钥只读挂载、Compose 网络和 `extra_hosts`;候选容器启动时 restart policy 必须先是 `no`,并显式使用 `--pid host` 保持节点级进程资源采集,验证通过后才能改成 `always` 并删除旧容器。升级计划的 `replacementStrategy` 必须包含 `oldGatewaySleepMs`、`validationTimeoutMs`、`promoteOnlyAfterCandidateValidation`、`candidateRestartPolicyAfterPromotion: "always"`、`candidateUsesOldContainerEnvironment`、`candidateUsesOldContainerMounts`、`candidateUsesOldContainerNetworks`、`candidateUsesOldContainerExtraHosts` 和 `candidateUsesHostPidNamespace`,并且必须在 plan 中显示指定 Provider 的当前/目标 gateway 版本号,便于前端和 CLI 判断这不是旧的先删旧容器再 up 的危险流程。 diff --git a/src/components/provider-gateway/package.json b/src/components/provider-gateway/package.json index d132e823..a21be47c 100644 --- a/src/components/provider-gateway/package.json +++ b/src/components/provider-gateway/package.json @@ -1,6 +1,6 @@ { "name": "@unidesk/provider-gateway", - "version": "0.2.20", + "version": "0.2.21", "private": true, "type": "module", "scripts": { diff --git a/src/components/provider-gateway/src/index.ts b/src/components/provider-gateway/src/index.ts index 262e8d24..e27f8735 100644 --- a/src/components/provider-gateway/src/index.ts +++ b/src/components/provider-gateway/src/index.ts @@ -1552,8 +1552,13 @@ function candidateGatewayName(taskId: string): string { function upgradePlan(taskId: string): Record { const workspace = config.upgradeWorkspacePath; + const workspaceRoot = workspace.replace(/\/+$/, ""); const sleepMs = 300_000; const validationTimeoutMs = 180_000; + const compactBuildContextCandidates = Array.from(new Set([ + `${workspaceRoot}/.state/provider-${config.providerId}-build-context`, + `${workspaceRoot}/.state/provider-${safeProviderSlug(config.providerId)}-build-context`, + ])); const composeBaseCommand = [ "docker", "compose", @@ -1595,9 +1600,31 @@ function upgradePlan(taskId: string): Record { const validationNeedleOk = `"ok":true`; const validationAttempts = Math.max(1, Math.ceil(validationTimeoutMs / 2000)); const targetGatewayMetadata = readTargetGatewayMetadata(workspace); + const compactBuildContextRefreshScript = [ + `provider_src=${shellQuote(`${workspaceRoot}/src/components/provider-gateway`)}`, + `shared_src=${shellQuote(`${workspaceRoot}/src/components/shared`)}`, + `for compact_context in ${compactBuildContextCandidates.map(shellQuote).join(" ")}; do`, + ` if [ -d "$compact_context" ]; then`, + ` echo "refreshing provider-gateway build context: $compact_context"`, + ` mkdir -p "$compact_context/src/components/provider-gateway" "$compact_context/src/components/shared"`, + ` rm -rf "$compact_context/src/components/provider-gateway/src" "$compact_context/src/components/provider-gateway/scripts" "$compact_context/src/components/shared/src"`, + ` cp -a "$provider_src/package.json" "$compact_context/src/components/provider-gateway/package.json"`, + ` [ ! -f "$provider_src/tsconfig.json" ] || cp -a "$provider_src/tsconfig.json" "$compact_context/src/components/provider-gateway/tsconfig.json"`, + ` [ ! -f "$provider_src/Dockerfile" ] || cp -a "$provider_src/Dockerfile" "$compact_context/src/components/provider-gateway/Dockerfile"`, + ` [ -f "$compact_context/Dockerfile" ] || cp -a "$provider_src/Dockerfile" "$compact_context/Dockerfile"`, + ` cp -a "$provider_src/src" "$compact_context/src/components/provider-gateway/src"`, + ` cp -a "$provider_src/scripts" "$compact_context/src/components/provider-gateway/scripts"`, + ` cp -a "$shared_src/package.json" "$compact_context/src/components/shared/package.json"`, + ` [ ! -f "$shared_src/tsconfig.json" ] || cp -a "$shared_src/tsconfig.json" "$compact_context/src/components/shared/tsconfig.json"`, + ` cp -a "$shared_src/src" "$compact_context/src/components/shared/src"`, + ` date -u +%Y-%m-%dT%H:%M:%SZ > "$compact_context/.unidesk-build-context-refreshed-at"`, + ` fi`, + `done`, + ].join("\n"); const script = [ "set -eu", `cd ${shellQuote(workspace)}`, + compactBuildContextRefreshScript, `old_ids=$(${listServiceContainersCommand.map(shellQuote).join(" ")})`, `first_old=""`, `for old_id in $old_ids; do first_old="$old_id"; break; done`, @@ -1669,6 +1696,8 @@ function upgradePlan(taskId: string): Record { "/var/run/docker.sock:/var/run/docker.sock", "-v", `${config.upgradeHostProjectRoot}:${workspace}:ro`, + "-v", + `${config.upgradeHostProjectRoot.replace(/\/+$/, "")}/.state:${workspaceRoot}/.state`, "-w", workspace, config.upgradeRunnerImage, @@ -1714,7 +1743,8 @@ function upgradePlan(taskId: string): Record { candidateUsesOldContainerEnvironment: true, candidateUsesHostPidNamespace: true, startupSelfHealsRestartPolicy: true, - dockerStatusReportsRestartPolicyAndPidMode: true, + dockerStatusReportsRestartPolicyAndPidMode: true, + refreshesCompactBuildContextBeforeBuild: true, removeScope: { projectLabel: config.upgradeComposeProject, serviceLabel: config.upgradeService, @@ -1723,6 +1753,8 @@ function upgradePlan(taskId: string): Record { namedVolumesPreserved: true, }, dockerRunCommand, + compactBuildContextCandidates, + workspaceStateMount: `${config.upgradeHostProjectRoot.replace(/\/+$/, "")}/.state:${workspaceRoot}/.state`, }; }