Merge remote-tracking branch 'origin/master'

This commit is contained in:
Codex
2026-07-09 14:34:33 +02:00
6 changed files with 256 additions and 6 deletions
+1
View File
@@ -14,6 +14,7 @@ bun scripts/cli.ts platform-infra sub2api report
bun scripts/cli.ts platform-infra sub2api status --target PK01
bun scripts/cli.ts platform-infra sub2api validate --target PK01
bun scripts/cli.ts platform-infra sub2api plan --target PK01
bun scripts/cli.ts platform-infra sub2api image-prepull --target PK01 --confirm
```
先看报表和状态,再做计划或变更。详细规则按职责拆在 `references/` 下;不要新增 `full.md``all.md``guide.md` 这类变相超级文件。
@@ -39,6 +39,8 @@ bun scripts/cli.ts platform-infra sub2api codex-pool trace --request-id <request
```bash
bun scripts/cli.ts platform-infra sub2api plan
bun scripts/cli.ts platform-infra sub2api plan --target G14
bun scripts/cli.ts platform-infra sub2api image-prepull --target PK01
bun scripts/cli.ts platform-infra sub2api image-prepull --target PK01 --confirm
bun scripts/cli.ts platform-infra sub2api apply --dry-run
bun scripts/cli.ts platform-infra sub2api apply --target G14 --dry-run
bun scripts/cli.ts platform-infra sub2api apply --confirm
@@ -67,6 +69,8 @@ PK01 没有 k3s control plane。`codex-pool sync --target PK01 --confirm` 和 `c
PK01 host-Docker apply 仍必须由 `platform-infra sub2api apply --target PK01 --confirm` 受控执行。若 dry-run 或 apply 输出显示 `docker compose is absent; apply will use raw docker run fallback`,这表示 CLI 选择了 host-Docker fallback,不是裸手工 Docker 操作;只要 YAML image、env、ports、Caddy managed block 和 `status/validate` 最终对齐,可作为受控滚动升级证据。不要改用手工 `docker run`、手工 compose 文件或直接编辑 PK01 Caddyfile。
正式镜像升级前必须用 `platform-infra sub2api image-prepull --target <id> --confirm` 预拉 YAML 声明的 Sub2API runtime images;不带 `--confirm` 只做 inspect/dry-run。`--confirm` 默认返回 async job,显式 `--wait` 才同步等待。不得用临时 `trans <node> docker pull ...` 作为长期入口。
## D601 Egress Proxy
D601 的目标级 `egressProxy` 完全由 `config/platform-infra/sub2api.yaml` 控制。当前成熟形态是 master Docker `shadowsocks-rust` 作为加密出站源,D601 k3s 内 `sing-box` 暴露 HTTP/mixed ClusterIP proxy 给 Sub2API 和按 YAML 启用的 sentinel 使用。不要把 endpoint、端口、密码、健康探针或镜像 tag 写进 skill;只以 YAML 和 `config/platform-infra/sub2api-master-egress-proxy.compose.yaml` 为准。
@@ -89,8 +93,9 @@ proxy secret/config 文件只允许放在受控 Secret/state 路径,输出只
2. 只修改目标 target 的 `image.repository``image.tag``pullPolicy`;不要顺手改全局默认、PK01 host-Docker target、standby target 或其它共用 `api2.pikapython.com` 历史 target。
3. 提交前用 `git diff -- config/platform-infra/sub2api.yaml``rg -n "id: <target>|tag: <version>|publicBaseUrl"` 确认只有目标 target 变化,且要保护的公开入口没有被误改。
4. 执行 `sub2api plan --target <target>``sub2api apply --target <target> --dry-run`,确认策略检查通过。
5. 提交并推送 YAML source truth 后,执行 `sub2api apply --target <target> --confirm`,按返回`job status` 命令轮询完成
6. 执行 `sub2api status --target <target>`,确认运行镜像等于 YAML 声明;滚动过程中短暂 `0/1` 先等待并复查,不要立即改账号池、Caddy 或 Secret
7. 执行 `sub2api validate --target <target>`、目标 public `/health` 和最小 public `/v1/responses` smoke;若需要确认未影响其它公开入口,再跑对应 target 的 `validate`
5. 提交并推送 YAML source truth 后,执行 `sub2api image-prepull --target <target> --confirm`,按返回 job 或 `--wait` 输出确认 YAML 声明的镜像已存在于目标 runtime
6. 镜像准备好后执行 `sub2api apply --target <target> --confirm`,按返回的 `job status` 命令轮询完成
7. 执行 `sub2api status --target <target>`,确认运行镜像等于 YAML 声明;滚动过程中短暂 `0/1` 先等待并复查,不要立即改账号池、Caddy 或 Secret
8. 执行 `sub2api validate --target <target>`、目标 public `/health` 和最小 public `/v1/responses` smoke;若需要确认未影响其它公开入口,再跑对应 target 的 `validate`
不要把镜像版本写进脚本常量、JSON 或 manifest 模板。
+2 -3
View File
@@ -317,7 +317,7 @@ targets:
namespace: platform-infra
role: retired
enabled: true
databaseMode: external-active
databaseMode: external-pending
redisMode: local-ephemeral
appReplicas: 0
redisReplicas: 0
@@ -448,8 +448,7 @@ runtime:
mode: empty-dir
sentinel:
mode: singleton
enabledOnTargets:
- JD01
enabledOnTargets: []
security:
urlAllowlist:
enabled: false
+1
View File
@@ -393,6 +393,7 @@ export function platformInfraHelp(): unknown {
output: "json",
usage: [
"bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]",
"bun scripts/cli.ts platform-infra sub2api image-prepull [--target G14|D601|PK01] [--dry-run|--confirm] [--wait]",
"bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --dry-run",
"bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --confirm",
"bun scripts/cli.ts platform-infra sub2api status [--target G14|D601] [--full|--raw]",
+180
View File
@@ -0,0 +1,180 @@
import type { UniDeskConfig } from "../config";
import { capture, compactCapture, parseJsonOutput, shQuote } from "../platform-infra-public-service";
import { startJob } from "../jobs";
import { readSub2ApiConfig } from "./config";
import { imageRef, resolveTarget, targetDependencyImages } from "./manifest";
import type { ApplyOptions } from "./options";
import { boolField } from "./utils";
function imagePrepullScript(images: string[], confirm: boolean): string {
const imageList = images.join("\n");
const mode = confirm ? "confirm" : "dry-run";
return `
set -u
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
mode=${shQuote(mode)}
images_file="$tmp/images.txt"
cat > "$images_file" <<'EOF_IMAGES'
${imageList}
EOF_IMAGES
docker_bin=""
if docker ps >/dev/null 2>&1; then
docker_bin="docker"
elif sudo docker ps >/dev/null 2>&1; then
docker_bin="sudo docker"
fi
crictl_bin=""
if command -v crictl >/dev/null 2>&1; then
crictl_bin="crictl"
elif sudo crictl images >/dev/null 2>&1; then
crictl_bin="sudo crictl"
fi
runtime="none"
if [ -n "$docker_bin" ]; then
runtime="docker"
elif [ -n "$crictl_bin" ]; then
runtime="crictl"
fi
jsonl="$tmp/images.jsonl"
: > "$jsonl"
ok=true
while IFS= read -r image; do
[ -n "$image" ] || continue
present_before=false
present_after=false
pull_rc=0
inspect_id=""
inspect_digest=""
safe_name="$(printf '%s' "$image" | tr '/:@' '___')"
pull_stdout="$tmp/$safe_name.pull.out"
pull_stderr="$tmp/$safe_name.pull.err"
: > "$pull_stdout"
: > "$pull_stderr"
if [ "$runtime" = "docker" ]; then
if $docker_bin image inspect "$image" >/dev/null 2>&1; then present_before=true; fi
if [ "$mode" = "confirm" ]; then
$docker_bin pull "$image" >"$pull_stdout" 2>"$pull_stderr"
pull_rc=$?
fi
if $docker_bin image inspect "$image" >/dev/null 2>&1; then
present_after=true
inspect_id="$($docker_bin image inspect "$image" --format '{{.Id}}' 2>/dev/null || true)"
inspect_digest="$($docker_bin image inspect "$image" --format '{{join .RepoDigests ","}}' 2>/dev/null || true)"
fi
elif [ "$runtime" = "crictl" ]; then
if $crictl_bin inspecti "$image" >/dev/null 2>&1; then present_before=true; fi
if [ "$mode" = "confirm" ]; then
$crictl_bin pull "$image" >"$pull_stdout" 2>"$pull_stderr"
pull_rc=$?
fi
if $crictl_bin inspecti "$image" >/dev/null 2>&1; then
present_after=true
inspect_id="$($crictl_bin inspecti "$image" 2>/dev/null | python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get("status",{}).get("id") or data.get("id") or "")' 2>/dev/null || true)"
fi
else
pull_rc=1
printf '%s\\n' 'neither docker nor crictl is available for image prepull' >"$pull_stderr"
fi
if [ "$mode" = "confirm" ]; then
if [ "$pull_rc" -ne 0 ] || [ "$present_after" != true ]; then ok=false; fi
else
if [ "$runtime" = "none" ]; then ok=false; fi
fi
python3 - "$jsonl" "$image" "$runtime" "$present_before" "$present_after" "$pull_rc" "$inspect_id" "$inspect_digest" "$pull_stdout" "$pull_stderr" <<'PY'
import json
import sys
path, image, runtime, before, after, pull_rc, inspect_id, inspect_digest, pull_stdout, pull_stderr = sys.argv[1:]
def text(file_path: str, limit: int = 4000) -> str:
try:
return open(file_path, encoding="utf-8", errors="replace").read()[-limit:]
except FileNotFoundError:
return ""
record = {
"image": image,
"runtime": runtime,
"presentBefore": before == "true",
"presentAfter": after == "true",
"pull": {
"exitCode": int(pull_rc),
"stdout": text(pull_stdout),
"stderr": text(pull_stderr),
},
"imageId": inspect_id or None,
"repoDigests": [item for item in inspect_digest.split(",") if item],
}
with open(path, "a", encoding="utf-8") as handle:
handle.write(json.dumps(record, ensure_ascii=False) + "\\n")
PY
done < "$images_file"
python3 - "$jsonl" "$ok" "$mode" "$runtime" <<'PY'
import json
import sys
jsonl, ok, mode, runtime = sys.argv[1:]
images = []
try:
with open(jsonl, encoding="utf-8") as handle:
for line in handle:
if line.strip():
images.append(json.loads(line))
except FileNotFoundError:
pass
payload = {
"ok": ok == "true",
"mode": mode,
"runtime": runtime,
"images": images,
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
`;
}
export async function imagePrepull(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
const sub2api = readSub2ApiConfig();
const target = resolveTarget(sub2api, options.targetId);
const dependencyImages = targetDependencyImages(sub2api, target);
const images = Array.from(new Set([imageRef(sub2api, target), dependencyImages.redis]));
if (options.confirm && !options.wait) {
const job = startJob(
`platform_infra_sub2api_image_prepull_${target.id.toLowerCase()}`,
["bun", "scripts/cli.ts", "platform-infra", "sub2api", "image-prepull", "--target", target.id, "--confirm", "--wait"],
`Pre-pull ${target.id} Sub2API runtime images from YAML-controlled image refs`,
);
return {
ok: true,
action: "platform-infra-sub2api-image-prepull",
mode: "async-job",
target: { id: target.id, route: target.route, namespace: target.namespace },
images,
job,
statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
next: {
status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
verify: `bun scripts/cli.ts platform-infra sub2api image-prepull --target ${target.id}`,
apply: `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm`,
},
};
}
const result = await capture(config, target.route, ["sh"], imagePrepullScript(images, options.confirm));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
action: "platform-infra-sub2api-image-prepull",
mode: options.confirm ? "confirmed" : "dry-run",
target: { id: target.id, route: target.route, namespace: target.namespace },
images,
remote: parsed ?? compactCapture(result, { full: true }),
next: {
apply: `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm`,
status: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`,
},
};
}
+64
View File
@@ -17,6 +17,7 @@ import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile,
import { apply, plan, status } from "./actions";
import { defaultSub2ApiTargetId, readSub2ApiConfig, validateOptions } from "./config";
import { configPath, platformInfraHelp, serviceName } from "./entry";
import { imagePrepull } from "./image-prepull";
import { isHostDockerTarget, resolveTarget } from "./manifest";
import { validate } from "./policy";
@@ -88,6 +89,11 @@ export async function runPlatformInfraCommand(config: UniDeskConfig, args: strin
const result = await apply(config, options);
return options.full || options.raw ? result : renderSub2ApiApply(result);
}
if (action === "image-prepull") {
const options = parseApplyOptions(args.slice(2));
const result = await imagePrepull(config, options);
return options.full || options.raw ? result : renderSub2ApiImagePrepull(result);
}
if (action === "status") {
const options = parseDisclosureOptions(args.slice(2));
const result = await status(config, options);
@@ -307,6 +313,64 @@ function renderSub2ApiApply(result: Record<string, unknown>): RenderedCliResult
return rendered(result, "platform-infra sub2api apply", lines);
}
function renderSub2ApiImagePrepull(result: Record<string, unknown>): RenderedCliResult {
const target = record(result.target);
const mode = stringValue(result.mode);
const targetId = stringValue(target.id, stringValue(result.target));
const status = result.ok === false ? "failed" : "ok";
const lines = [
"PLATFORM-INFRA SUB2API IMAGE PREPULL",
...table(["TARGET", "ROUTE", "NAMESPACE", "MODE", "STATUS"], [[targetId, stringValue(target.route), stringValue(target.namespace), mode, status]]),
];
if (mode === "async-job") {
const job = record(result.job);
const next = record(result.next);
lines.push(
"",
"JOB",
...table(["ID", "STATUS", "COMMAND"], [[stringValue(job.id), stringValue(job.status, "started"), stringValue(result.statusCommand)]]),
"",
"IMAGES",
...table(["IMAGE"], arrayValues(result.images).map((image) => [stringValue(image)])),
"",
"NEXT",
` status: ${stringValue(next.status, stringValue(result.statusCommand))}`,
` verify: ${stringValue(next.verify, `bun scripts/cli.ts platform-infra sub2api image-prepull --target ${targetId}`)}`,
` apply: ${stringValue(next.apply, `bun scripts/cli.ts platform-infra sub2api apply --target ${targetId} --confirm`)}`,
);
return rendered(result, "platform-infra sub2api image-prepull", lines);
}
const remote = record(result.remote);
const imageRows = arrayRecords(remote.images).map((item) => [
stringValue(item.image),
stringValue(item.runtime),
boolText(item.presentBefore),
boolText(item.presentAfter),
stringValue(record(item.pull).exitCode, "-"),
stringValue(item.imageId, "-").slice(0, 24),
]);
const next = record(result.next);
lines.push(
"",
"IMAGES",
...(imageRows.length === 0 ? ["-"] : table(["IMAGE", "RUNTIME", "BEFORE", "AFTER", "PULL", "IMAGE_ID"], imageRows)),
"",
"CHECKS",
...table(["CHECK", "VALUE"], [
["ok", boolText(result.ok !== false)],
["runtime", stringValue(remote.runtime)],
["valuesPrinted", "false"],
]),
"",
"NEXT",
` apply: ${stringValue(next.apply, `bun scripts/cli.ts platform-infra sub2api apply --target ${targetId} --confirm`)}`,
` status: ${stringValue(next.status, `bun scripts/cli.ts platform-infra sub2api status --target ${targetId}`)}`,
` full: bun scripts/cli.ts platform-infra sub2api image-prepull --target ${targetId} ${mode === "confirmed" ? "--confirm --wait" : "--dry-run"} --full`,
"Disclosure: default output is bounded; Secret values are not printed.",
);
return rendered(result, "platform-infra sub2api image-prepull", lines);
}
function renderApplyRemoteSummary(remote: Record<string, unknown>): string[] {
const steps = record(remote.steps);
const stepRows = Object.entries(steps).map(([name, value]) => {