From 57eb380247fa9e69f8c5964490b88be58485ffbc Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 25 May 2026 03:26:04 +0000 Subject: [PATCH] docs record pod hotfix command hygiene --- docs/reference/dev-runtime-hotfix-runbook.md | 3 ++ docs/reference/gateway-outbound-demo.md | 1 + .../gateway-outbound-nonblocking-probe.mjs | 48 +++++++++++++++---- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/reference/dev-runtime-hotfix-runbook.md b/docs/reference/dev-runtime-hotfix-runbook.md index 202aa6e7..f0b772eb 100644 --- a/docs/reference/dev-runtime-hotfix-runbook.md +++ b/docs/reference/dev-runtime-hotfix-runbook.md @@ -54,6 +54,9 @@ NODE - 不要用 `kubectl patch --patch-file -` 假设 stdin 可用;目标 `kubectl` 版本可能把 `-` 当普通文件名。用 `/tmp/*.patch.json` 再 `--patch-file`。 - 不要把大 JSON patch 放进 `kubectl patch -p "$patch"` 或 shell argv;ConfigMap 覆盖源码文件时很容易触发 `Argument list too long`。用 stdin 写远端临时文件。 +- 不要把多层脚本内联穿过 `ssh -> bash -lc -> kubectl exec -> sh -lc -> node/python/powershell`。这种路径会让 heredoc、`$!`、反引号、JSON 字符串、PowerShell 管道和中文内容被不同 shell 多次解释,常见结果是实验命令还没进入 pod 就被截断或改写。 +- 可靠做法是把实验逻辑沉淀为 repo-owned 脚本,或先写入远端 `/tmp/*.mjs|*.sh|*.ps1`,再用 `kubectl cp`/ConfigMap 挂载到 pod,最后用 `kubectl exec -- node /tmp/script.mjs --arg value` 或等价 argv 形态执行。只允许在外层 shell 保留短命令和固定参数,不在命令行里嵌大段 JS/PowerShell/heredoc。 +- 需要从 pod 内验证同一份实验时,优先复用同一个脚本在 D601、pod 和 CI 中运行;不要把探测逻辑在每个通道手写一遍。若必须临时写脚本,文件名包含目标和 commit,issue 中记录脚本路径、commit、命令和输出摘要。 - 不要对已经很大的 hotfix ConfigMap 使用 `kubectl apply -f -`;`apply` 会尝试写入 `kubectl.kubernetes.io/last-applied-configuration`,可能超过 Kubernetes annotation 256 KiB 限制。局部更新用 `patch --type merge --patch-file`。 - 多文件运行面覆盖第一次创建 ConfigMap 时也不要默认用 `kubectl apply -f -`;如果 data 可能超过 annotation 限制,先 `kubectl delete configmap --ignore-not-found`,再 `kubectl create configmap --from-file=...`,随后用 Deployment annotation 记录 hotfix 版本并 rollout。正式 CD 收口时必须删除这些 unmanaged ConfigMap/volumeMount,不让热修覆盖继续漂在运行面。 - 不要通过会截断 stdout 的控制 CLI 把 `kubectl get configmap -o json` 回读到本地再 `replace`;大 data 可能被日志/截断污染成非法 JSON。只有在确认完整无截断的原生通道中,才允许对完整对象做 `kubectl replace -f -`。 diff --git a/docs/reference/gateway-outbound-demo.md b/docs/reference/gateway-outbound-demo.md index 6106f994..c0bedf8c 100644 --- a/docs/reference/gateway-outbound-demo.md +++ b/docs/reference/gateway-outbound-demo.md @@ -60,6 +60,7 @@ npm run gateway:demo:edge-smoke - `gateway:demo:smoke` 启动本地 `hwlab-cloud-api` 和 `hwlab-gateway`,验证 `hardware.invoke.shell` 返回 `stdout=hwlab-demo`。 - `gateway:demo:edge-smoke` 额外启动本地 `hwlab-edge-proxy`,验证普通 HTTP proxy 能转发 `/v1/gateway/poll`、`/v1/gateway/result` 和 `/json-rpc`。 - 两个 smoke 都会设置 `NO_PROXY/no_proxy`,用于规避本地代理误触发。 +- 对 D601 pod 内部 `127.0.0.1:6667` 或 service `:6667` 做 Node 探测时,不要用 Web/undici `fetch`;`6667` 属于 WHATWG bad port,Node 会直接报 `bad port`。repo-owned 探测脚本应使用 `http/https` 原生 request,公网 `:16667` 或浏览器入口不受这个限制。 ## DEV 使用 diff --git a/scripts/gateway-outbound-nonblocking-probe.mjs b/scripts/gateway-outbound-nonblocking-probe.mjs index b2e6326e..2340d665 100644 --- a/scripts/gateway-outbound-nonblocking-probe.mjs +++ b/scripts/gateway-outbound-nonblocking-probe.mjs @@ -1,5 +1,7 @@ #!/usr/bin/env node import assert from "node:assert/strict"; +import http from "node:http"; +import https from "node:https"; import { setTimeout as delay } from "node:timers/promises"; const args = parseArgs(process.argv.slice(2)); @@ -63,16 +65,14 @@ async function waitForGatewaySession() { } async function getJson(path) { - const response = await fetch(`${apiBaseUrl}${path}`, { + return requestJson(`${apiBaseUrl}${path}`, { + method: "GET", headers: { accept: "application/json" } }); - const text = await response.text(); - if (!response.ok) throw new Error(`GET ${path} HTTP ${response.status}: ${text}`); - return text ? JSON.parse(text) : null; } async function postRpc({ id, traceId, command }) { - const response = await fetch(`${apiBaseUrl}/json-rpc`, { + return requestJson(`${apiBaseUrl}/json-rpc`, { method: "POST", headers: { accept: "application/json", @@ -101,9 +101,41 @@ async function postRpc({ id, traceId, command }) { } }) }); - const text = await response.text(); - if (!response.ok) throw new Error(`RPC ${id} HTTP ${response.status}: ${text}`); - return text ? JSON.parse(text) : null; +} + +function requestJson(url, { method, headers = {}, body }) { + return new Promise((resolve, reject) => { + const target = new URL(url); + const client = target.protocol === "https:" ? https : http; + const payload = body ? JSON.stringify(body) : ""; + const request = client.request(target, { + method, + headers: { + ...headers, + ...(payload ? { "content-length": Buffer.byteLength(payload) } : {}) + } + }, (response) => { + let text = ""; + response.setEncoding("utf8"); + response.on("data", (chunk) => { + text += chunk; + }); + response.on("end", () => { + if ((response.statusCode ?? 0) < 200 || (response.statusCode ?? 0) >= 300) { + reject(new Error(`${method} ${target.pathname} HTTP ${response.statusCode}: ${text}`)); + return; + } + try { + resolve(text ? JSON.parse(text) : null); + } catch (error) { + reject(new Error(`${method} ${target.pathname} returned non-JSON: ${error.message}`)); + } + }); + }); + request.on("error", reject); + if (payload) request.write(payload); + request.end(); + }); } function parseArgs(argv) {