Merge pull request #2665 from pikasTech/fix/public-edge-compact-status
收敛 public-edge status 默认输出并提供显式下钻
This commit is contained in:
@@ -182,6 +182,11 @@ bun scripts/cli.ts hwlab nodes control-plane legacy-cicd --help
|
||||
health path 或 expected A 与稳定错误码;
|
||||
- public-edge 失败定位只执行一次 `status`,
|
||||
禁止再串联 plan、临时脚本或额外 `trans` probe;
|
||||
- 默认 target status 必须返回有界摘要并直接披露 `firstFailure`,
|
||||
禁止触发 `outputTruncated` 或依赖临时 dump;
|
||||
- 默认摘要必须包含 authority、installed/desired commit、source relation、
|
||||
managed/Compose fingerprint、endpoint 健康和 warning 计数;
|
||||
- 站点详情使用 `--site <site-id>`,完整对象只通过显式 `--full` 获取;
|
||||
- 不披露 Secret、token 或 archive payload。
|
||||
- public-edge 专站状态:
|
||||
- 领域服务统一调用
|
||||
|
||||
@@ -129,6 +129,10 @@
|
||||
health path 或 expected A 与稳定错误码;
|
||||
- `status` 是失败站点定位的唯一首选入口,
|
||||
禁止再组合 plan、临时脚本和额外 `trans` probe;
|
||||
- 默认 target 状态只返回有界摘要,必须直接包含 authority、运行 commit、
|
||||
managed/Compose fingerprint、endpoint 健康和 `firstFailure`,
|
||||
不得触发 `outputTruncated`;
|
||||
- 单站点详情使用 `--site <site-id>`,一次性完整对象使用显式 `--full`;
|
||||
- 输出保持 `valuesPrinted=false`,不披露 Secret 或 archive payload。
|
||||
- 受控入口:
|
||||
|
||||
@@ -136,6 +140,8 @@
|
||||
bun scripts/cli.ts platform-infra public-edge plan --target NC01
|
||||
bun scripts/cli.ts platform-infra public-edge apply --target NC01 --dry-run
|
||||
bun scripts/cli.ts platform-infra public-edge status --target NC01
|
||||
bun scripts/cli.ts platform-infra public-edge status --target NC01 --site <site-id>
|
||||
bun scripts/cli.ts platform-infra public-edge status --target NC01 --full
|
||||
```
|
||||
|
||||
### 异地节点只读 Artifact Registry
|
||||
|
||||
@@ -30,6 +30,7 @@ interface PublicEdgeOptions {
|
||||
authorityId: string | null;
|
||||
sourceCommit: string | null;
|
||||
ancestryFile: string | null;
|
||||
full: boolean;
|
||||
}
|
||||
|
||||
interface PublicEdgeDelivery {
|
||||
@@ -127,6 +128,7 @@ export function publicEdgeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts platform-infra public-edge apply --target NC01 --dry-run",
|
||||
"bun scripts/cli.ts platform-infra public-edge status --target NC01",
|
||||
"bun scripts/cli.ts platform-infra public-edge status --target NC01 --site superapi",
|
||||
"bun scripts/cli.ts platform-infra public-edge status --target NC01 --full",
|
||||
],
|
||||
source: "config/platform-infra/public-edge.yaml",
|
||||
guarantees: [
|
||||
@@ -161,7 +163,11 @@ export async function runPlatformInfraPublicEdgeCommand(config: UniDeskConfig, a
|
||||
}
|
||||
if (options.action === "status") {
|
||||
const result = await status(config, options.configPath, delivery, target, resolution);
|
||||
const visible = options.siteId === null ? result : narrowStatusToSite(result, options.siteId);
|
||||
const visible = options.siteId !== null
|
||||
? narrowStatusToSite(result, options.siteId)
|
||||
: options.full
|
||||
? result
|
||||
: compactStatus(result);
|
||||
return renderMachine("platform-infra public-edge status", visible, "json", visible.ok === true);
|
||||
}
|
||||
if (resolution.sites.length === 0) {
|
||||
@@ -191,6 +197,7 @@ function parseOptions(args: string[]): PublicEdgeOptions {
|
||||
let authorityId: string | null = null;
|
||||
let sourceCommit: string | null = null;
|
||||
let ancestryFile: string | null = null;
|
||||
let full = false;
|
||||
for (let index = 1; index < args.length; index += 1) {
|
||||
const arg = args[index]!;
|
||||
if (arg === "--config" || arg === "--target" || arg === "--site" || arg === "--authority" || arg === "--source-commit" || arg === "--ancestry-file") {
|
||||
@@ -207,6 +214,8 @@ function parseOptions(args: string[]): PublicEdgeOptions {
|
||||
dryRun = true;
|
||||
} else if (arg === "--confirm") {
|
||||
confirm = true;
|
||||
} else if (arg === "--full") {
|
||||
full = true;
|
||||
} else {
|
||||
throw inputError(`不支持的 public-edge 参数:${arg}`, "unsupported-option", arg);
|
||||
}
|
||||
@@ -226,7 +235,10 @@ function parseOptions(args: string[]): PublicEdgeOptions {
|
||||
if (siteId !== null && action !== "status") {
|
||||
throw inputError("--site 只适用于 status", "site-filter-not-allowed", "--site");
|
||||
}
|
||||
return { action, configPath, targetId, siteId, dryRun, confirm, authorityId, sourceCommit, ancestryFile };
|
||||
if (full && action !== "status") {
|
||||
throw inputError("--full 只适用于 status", "full-disclosure-not-allowed", "--full");
|
||||
}
|
||||
return { action, configPath, targetId, siteId, dryRun, confirm, authorityId, sourceCommit, ancestryFile, full };
|
||||
}
|
||||
|
||||
function readConfig(configPath: string): Record<string, unknown> {
|
||||
@@ -698,6 +710,131 @@ function narrowStatusToSite(result: Record<string, unknown>, siteId: string): Re
|
||||
};
|
||||
}
|
||||
|
||||
function compactStatus(result: Record<string, unknown>): Record<string, unknown> {
|
||||
const runtime = optionalRecord(result.runtime) ?? {};
|
||||
const target = optionalRecord(result.target) ?? {};
|
||||
const authority = optionalRecord(result.authority) ?? {};
|
||||
const provenance = optionalRecord(runtime.provenance) ?? {};
|
||||
const installed = optionalRecord(provenance.installed) ?? {};
|
||||
const config = optionalRecord(runtime.config) ?? {};
|
||||
const dns = optionalRecord(runtime.dns) ?? {};
|
||||
const probes = optionalRecord(runtime.siteProbes) ?? {};
|
||||
const sites = Array.isArray(result.sites) ? result.sites : [];
|
||||
const unresolved = Array.isArray(result.unresolved) ? result.unresolved : [];
|
||||
const dnsFailures = Array.isArray(dns.failures) ? dns.failures : [];
|
||||
const probeFailures = Array.isArray(probes.failures) ? probes.failures : [];
|
||||
const declaredWarnings = Array.isArray(result.warnings) ? result.warnings : [];
|
||||
const warnings = [...declaredWarnings, ...dnsFailures];
|
||||
const listenerPorts = Array.isArray(target.listeners) ? target.listeners : [];
|
||||
const check = (key: string): { ok: boolean; exitCode: number | null } => {
|
||||
const exitCode = optionalRecord(runtime[key])?.exitCode;
|
||||
return { ok: exitCode === 0, exitCode: typeof exitCode === "number" ? exitCode : null };
|
||||
};
|
||||
return {
|
||||
ok: result.ok === true,
|
||||
action: result.action,
|
||||
mutation: false,
|
||||
target: {
|
||||
id: target.id ?? null,
|
||||
publicAddress: target.publicAddress ?? null,
|
||||
containerName: target.containerName ?? null,
|
||||
endpoints: listenerPorts.map((port) => ({
|
||||
scheme: port === 443 ? "https" : port === 80 ? "http" : "tcp",
|
||||
port,
|
||||
listenerReady: check("tcpListeners").ok,
|
||||
})),
|
||||
},
|
||||
authority: {
|
||||
id: authority.id ?? null,
|
||||
sourceBranch: authority.sourceBranch ?? null,
|
||||
pipelineId: authority.pipelineId ?? null,
|
||||
},
|
||||
configRef: result.configRef,
|
||||
complete: result.complete === true,
|
||||
siteSummary: {
|
||||
resolved: sites.length,
|
||||
unresolved: unresolved.length,
|
||||
probeFailures: probeFailures.length,
|
||||
dnsWarnings: dnsFailures.length,
|
||||
},
|
||||
runtime: {
|
||||
ok: runtime.ok === true,
|
||||
checks: {
|
||||
container: check("container"),
|
||||
tcpListeners: check("tcpListeners"),
|
||||
preservedUdpListeners: check("preservedUdpListeners"),
|
||||
caddyValidate: check("validate"),
|
||||
legacyRetired: optionalRecord(runtime.legacyContainer)?.retired === true,
|
||||
siteProbes: { ok: probes.exitCode === 0, exitCode: typeof probes.exitCode === "number" ? probes.exitCode : null },
|
||||
},
|
||||
source: {
|
||||
installedCommit: installed.sourceCommit ?? null,
|
||||
desiredCommit: provenance.desiredSourceCommit ?? null,
|
||||
relation: provenance.sourceRelation ?? null,
|
||||
acceptable: provenance.sourceAcceptable === true,
|
||||
matchesRuntime: provenance.matchesRuntime === true,
|
||||
},
|
||||
fingerprints: {
|
||||
managedConfig: {
|
||||
current: config.currentManagedFingerprint ?? null,
|
||||
desired: config.desiredManagedFingerprint ?? null,
|
||||
},
|
||||
compose: {
|
||||
current: config.currentComposeFingerprint ?? null,
|
||||
desired: config.desiredComposeFingerprint ?? null,
|
||||
},
|
||||
matchesDesired: config.matchesDesired === true,
|
||||
},
|
||||
},
|
||||
firstFailure: firstStatusFailure(runtime),
|
||||
warningCount: warnings.length,
|
||||
warning: warnings.length === 0 ? null : compactStatusWarning(warnings[0]),
|
||||
next: {
|
||||
site: `bun scripts/cli.ts platform-infra public-edge status --target ${String(target.id ?? "NC01")} --site <site-id>`,
|
||||
full: `bun scripts/cli.ts platform-infra public-edge status --target ${String(target.id ?? "NC01")} --full`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function firstStatusFailure(runtime: Record<string, unknown>): Record<string, unknown> | null {
|
||||
if (optionalRecord(runtime.capture) !== null) {
|
||||
return { stage: "remoteStatus", code: "public-edge-status-capture-failed" };
|
||||
}
|
||||
const failures: Array<[string, boolean, string]> = [
|
||||
["container", optionalRecord(runtime.container)?.exitCode !== 0, "public-edge-container-not-running"],
|
||||
["legacyContainer", optionalRecord(runtime.legacyContainer)?.retired !== true, "public-edge-legacy-container-active"],
|
||||
["tcpListeners", optionalRecord(runtime.tcpListeners)?.exitCode !== 0, "public-edge-tcp-listener-not-ready"],
|
||||
["preservedUdpListeners", optionalRecord(runtime.preservedUdpListeners)?.exitCode !== 0, "public-edge-preserved-udp-listener-not-ready"],
|
||||
["caddyValidate", optionalRecord(runtime.validate)?.exitCode !== 0, "public-edge-caddy-invalid"],
|
||||
["provenance", optionalRecord(runtime.provenance)?.matchesRuntime !== true, "public-edge-runtime-provenance-mismatch"],
|
||||
["sourceCommit", optionalRecord(runtime.provenance)?.sourceAcceptable !== true, "public-edge-source-commit-unacceptable"],
|
||||
["config", optionalRecord(runtime.config)?.matchesDesired !== true, "public-edge-config-drift"],
|
||||
["siteProbes", optionalRecord(runtime.siteProbes)?.exitCode !== 0, "public-edge-site-probe-failed"],
|
||||
];
|
||||
const failure = failures.find(([, failed]) => failed);
|
||||
if (failure === undefined) return null;
|
||||
const result: Record<string, unknown> = { stage: failure[0], code: failure[2] };
|
||||
if (failure[0] === "siteProbes") {
|
||||
const failures = optionalRecord(runtime.siteProbes)?.failures;
|
||||
if (Array.isArray(failures) && failures.length > 0) result.site = compactStatusWarning(failures[0]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function compactStatusWarning(value: unknown): Record<string, unknown> {
|
||||
const warning = optionalRecord(value) ?? {};
|
||||
return {
|
||||
code: warning.code ?? "public-edge-warning",
|
||||
siteId: warning.siteId ?? warning.id ?? null,
|
||||
hostname: warning.hostname ?? null,
|
||||
path: warning.path ?? null,
|
||||
expectedA: warning.expectedA ?? null,
|
||||
detail: typeof warning.detail === "string" ? warning.detail.slice(0, 240) : null,
|
||||
blocking: warning.blocking === true,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderPublicEdgeArtifacts(target: PublicEdgeTarget, sites: ResolvedSite[]): { caddyfile: string; compose: string } {
|
||||
const protocols = target.listener.protocols.join(" ");
|
||||
const global = renderManagedBlock("public-edge-global", `\{\n\tservers \{\n\t\tprotocols ${protocols}\n\t\}\n\}`);
|
||||
|
||||
Reference in New Issue
Block a user