fix: gate baidu netdisk artifact deploy auth

This commit is contained in:
Codex
2026-05-20 11:53:59 +00:00
parent 4c94b27920
commit 34e0dca884
6 changed files with 260 additions and 11 deletions
+184 -8
View File
@@ -1,8 +1,8 @@
import { createHash } from "node:crypto";
import { readFileSync, writeFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { runCommand, type CommandResult } from "./command";
import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "./config";
import { resolveComposeCommand, writeComposeEnv } from "./docker";
import { startJob } from "./jobs";
type ArtifactRegistryAction = "plan" | "render" | "status" | "health" | "install" | "deploy-backend-core" | "deploy-service";
@@ -118,6 +118,8 @@ interface ArtifactConsumerTarget {
workDir?: string;
composeFile?: string;
projectHint?: string;
requiredRuntimeSecrets?: RuntimeSecretRequirement[];
authHealthGate?: AuthHealthGate;
};
k3s?: {
namespace: string;
@@ -133,10 +135,46 @@ interface ArtifactConsumerTarget {
};
}
export interface RuntimeSecretRequirement {
sourceEnvName: string;
containerEnvName: string;
}
interface AuthHealthGate {
requiredFields: string[];
recoveryHint: string;
}
interface ComposeArtifactRuntime {
workDir: string;
composeFile: string;
envFile: string;
project: string;
command: string[];
}
export interface RuntimeSecretPresence {
sourceEnvName: string;
containerEnvName: string;
present: boolean;
length: number;
}
function todoNoteHealthProbeCommand(): string {
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; const deploy=body.deploy&&typeof body.deploy==='object'&&!Array.isArray(body.deploy)?body.deploy:{}; body.deploy={...deploy,serviceId:deploy.serviceId||process.env.UNIDESK_DEPLOY_SERVICE_ID||'todo-note',ref:deploy.ref||process.env.UNIDESK_DEPLOY_REF||'',repo:deploy.repo||process.env.UNIDESK_DEPLOY_REPO||'',commit:deploy.commit||process.env.UNIDESK_DEPLOY_COMMIT||'',requestedCommit:deploy.requestedCommit||process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT||''}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
}
export const baiduNetdiskRuntimeSecretRequirements: RuntimeSecretRequirement[] = [
{ sourceEnvName: "UNIDESK_BAIDU_NETDISK_CLIENT_ID", containerEnvName: "BAIDU_NETDISK_CLIENT_ID" },
{ sourceEnvName: "UNIDESK_BAIDU_NETDISK_CLIENT_SECRET", containerEnvName: "BAIDU_NETDISK_CLIENT_SECRET" },
{ sourceEnvName: "UNIDESK_BAIDU_NETDISK_TOKEN_KEY", containerEnvName: "BAIDU_NETDISK_TOKEN_KEY" },
];
const baiduNetdiskAuthHealthGate: AuthHealthGate = {
requiredFields: ["configured", "clientIdConfigured", "clientSecretConfigured", "tokenKeyConfigured", "loggedIn"],
recoveryHint: "Restore UNIDESK_BAIDU_NETDISK_CLIENT_ID, UNIDESK_BAIDU_NETDISK_CLIENT_SECRET, and UNIDESK_BAIDU_NETDISK_TOKEN_KEY in the canonical .state/docker-compose.env, recreate only baidu-netdisk with the canonical env file, then verify microservice health baidu-netdisk.",
};
const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
"backend-core": {
serviceId: "backend-core",
@@ -178,6 +216,8 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4244/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
requiredRuntimeSecrets: baiduNetdiskRuntimeSecretRequirements,
authHealthGate: baiduNetdiskAuthHealthGate,
},
},
prod: {
@@ -190,6 +230,8 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4244/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
requireHealthCommit: true,
requiredRuntimeSecrets: baiduNetdiskRuntimeSecretRequirements,
authHealthGate: baiduNetdiskAuthHealthGate,
},
},
},
@@ -1347,6 +1389,95 @@ function composeLockScript(innerScript: string): string {
].join("; ");
}
function parseEnvFile(raw: string): Record<string, string> {
const values: Record<string, string> = {};
for (const line of raw.split(/\r?\n/u)) {
if (!line.trim() || line.trimStart().startsWith("#")) continue;
const index = line.indexOf("=");
if (index <= 0) continue;
const key = line.slice(0, index);
let value = line.slice(index + 1);
if (value.startsWith("\"") && value.endsWith("\"")) {
try {
value = JSON.parse(value) as string;
} catch {
value = value.slice(1, -1);
}
}
values[key] = value;
}
return values;
}
export function runtimeSecretPresenceFromEnvText(envText: string, requirements: RuntimeSecretRequirement[]): RuntimeSecretPresence[] {
const values = parseEnvFile(envText);
return requirements.map((requirement) => {
const value = values[requirement.sourceEnvName] ?? "";
return {
sourceEnvName: requirement.sourceEnvName,
containerEnvName: requirement.containerEnvName,
present: value.length > 0,
length: value.length,
};
});
}
export function baiduNetdiskAuthHealthGateStatus(health: unknown): { ok: boolean; requiredFields: string[]; failedFields: string[] } {
const record = typeof health === "object" && health !== null && !Array.isArray(health) ? health as Record<string, unknown> : {};
const auth = typeof record.auth === "object" && record.auth !== null && !Array.isArray(record.auth) ? record.auth as Record<string, unknown> : {};
const failedFields = baiduNetdiskAuthHealthGate.requiredFields.filter((field) => auth[field] !== true);
return {
ok: failedFields.length === 0,
requiredFields: baiduNetdiskAuthHealthGate.requiredFields,
failedFields,
};
}
function composeArtifactRuntime(config: UniDeskConfig, target: ArtifactConsumerTarget): ComposeArtifactRuntime {
if (target.compose === undefined) throw new Error("compose artifact target missing compose config");
const compose = target.compose;
const workDir = compose.workDir ?? config.providerGateway.upgrade.hostProjectRoot;
const composeFile = compose.composeFile ?? config.providerGateway.upgrade.composeFile;
const envFile = join(workDir, config.providerGateway.upgrade.composeEnvFile);
const project = compose.projectHint ?? (config.providerGateway.upgrade.composeProject || config.docker.projectName);
const command = ["docker", "compose", "--env-file", envFile, "-f", join(workDir, composeFile), "-p", project];
return { workDir, composeFile, envFile, project, command };
}
function composeArtifactSecretPreflight(envFile: string, requirements: RuntimeSecretRequirement[] | undefined): { ok: boolean; envFile: string; requirements: RuntimeSecretPresence[]; missing: RuntimeSecretPresence[]; valuesLogged: false } {
const requirementList = requirements ?? [];
const envText = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
const presence = runtimeSecretPresenceFromEnvText(envText, requirementList);
const missing = presence.filter((item) => !item.present);
return {
ok: requirementList.length === 0 || (existsSync(envFile) && missing.length === 0),
envFile,
requirements: presence,
missing,
valuesLogged: false,
};
}
function authHealthGateScript(gate: AuthHealthGate | undefined, healthJsonShellArg: string): string[] {
if (gate === undefined) return [];
return [
`auth_health_gate_fields=${shellQuote(gate.requiredFields.join(","))}`,
`python3 - ${healthJsonShellArg} "$auth_health_gate_fields" <<'PY'`,
"import json, sys",
"path, fields_text = sys.argv[1:]",
"fields = [item for item in fields_text.split(',') if item]",
"body = json.load(open(path, encoding='utf-8'))",
"auth = body.get('auth') if isinstance(body, dict) else None",
"auth = auth if isinstance(auth, dict) else {}",
"failed = [field for field in fields if auth.get(field) is not True]",
"if failed:",
" print('baidu_netdisk_auth_health_gate_failed=' + ','.join(failed))",
" raise SystemExit(1)",
"print('baidu_netdisk_auth_health_gate=ok')",
"PY",
];
}
function upsertEnvFileValues(path: string, values: Record<string, string>): void {
const existing = readFileSync(path, "utf8");
const seen = new Set<string>();
@@ -1469,6 +1600,24 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
const commit = options.commit;
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const config = readConfig();
const runtime = composeArtifactRuntime(config, target);
const secretPreflight = composeArtifactSecretPreflight(runtime.envFile, target.compose.requiredRuntimeSecrets);
if (!secretPreflight.ok) {
return {
ok: false,
supported: true,
serviceId: spec.serviceId,
step: "runtime-secret-preflight",
error: "runtime-secret-presence-missing",
environment: options.environment ?? "prod",
envFile: runtime.envFile,
requirements: secretPreflight.requirements,
missing: secretPreflight.missing,
valuesLogged: false,
recoveryHint: target.compose.authHealthGate?.recoveryHint ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
};
}
const health = runReadonlyStatus(options, true);
if (health.ok !== true) {
return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health };
@@ -1509,14 +1658,11 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
if (tagCommit.exitCode !== 0 || tagCommit.timedOut) {
return { ok: false, step: "docker-tag-commit", targetImage: commitImage, tag: commandTail(tagCommit), registryProbe: commandTail(registryProbe) };
}
const config = readConfig();
const runtimeEnv = writeComposeEnv(config, false);
upsertEnvFileValues(runtimeEnv.envFile, {
upsertEnvFileValues(runtime.envFile, {
...composeArtifactEnvValues(spec, target, options, commit),
});
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
const projectIndex = compose.indexOf("-p");
const composeProject = projectIndex >= 0 && compose[projectIndex + 1] !== undefined ? compose[projectIndex + 1] : "unidesk";
const compose = runtime.command;
const composeProject = runtime.project;
const serviceName = target.compose.serviceName;
const containerName = target.compose.containerName;
const serviceLogPrefix = spec.serviceId.replace(/-/gu, "_");
@@ -1544,6 +1690,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
`docker exec "$cid" sh -lc ${shellQuote(target.compose.healthProbeCommand)} > "$health_json"`,
"cat \"$health_json\"",
...authHealthGateScript(target.compose.authHealthGate, "\"$health_json\""),
...(target.compose.requireHealthCommit ? [
"health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_json\")",
"health_requested_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"requestedCommit\") or \"\"))' \"$health_json\")",
@@ -1560,7 +1707,9 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
sourceImage,
targetImage: composeImage,
registryProbe: commandTail(registryProbe),
runtimeSecretPreflight: secretPreflight,
deploy: commandTail(deploy),
recoveryHint: target.compose.authHealthGate?.recoveryHint ?? undefined,
};
}
const running = runCommand(["docker", "inspect", containerName, "--format", "image={{.Config.Image}} imageId={{.Image}} status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}"], repoRoot);
@@ -1584,6 +1733,11 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
imageLabelCommit: commit,
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
requiredRuntimeSecrets: secretPreflight.requirements,
authHealthGate: target.compose.authHealthGate === undefined ? "not-required" : {
requiredFields: target.compose.authHealthGate.requiredFields,
passed: true,
},
healthyOldVersionAccepted: false,
},
rollback: {
@@ -1696,6 +1850,7 @@ function d601ComposeArtifactDeployScript(options: ArtifactRegistryOptions, spec:
"health_probe=$(printf '%s' \"$health_probe_b64\" | base64 -d)",
"docker exec \"$cid\" sh -lc \"$health_probe\" >/tmp/unidesk-artifact-health.out",
"cat /tmp/unidesk-artifact-health.out",
...authHealthGateScript(compose.authHealthGate, shellQuote("/tmp/unidesk-artifact-health.out")),
"printf 'artifact_cd_service=%s\\nartifact_cd_source_repo=%s\\nartifact_cd_deploy_ref=%s\\nartifact_cd_source_image=%s\\nartifact_cd_stable_image=%s\\nartifact_cd_runtime_image=%s\\nartifact_cd_commit=%s\\nartifact_cd_container=%s\\nartifact_cd_container_id=%s\\nartifact_cd_image_label_commit=%s\\nartifact_cd_container_label_commit=%s\\n' \"$service_id\" \"$source_repo\" \"$deploy_ref\" \"$registry_image\" \"$stable_image\" \"$commit_image\" \"$commit\" \"$container\" \"$cid\" \"$actual_commit\" \"$container_commit\"",
].join("\n");
}
@@ -1866,7 +2021,28 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
: target.compose.requireHealthCommit
? `${spec.serviceId} runtime health probe succeeds and reports deploy.commit/deploy.requestedCommit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`,
...(target.compose.requiredRuntimeSecrets === undefined ? [] : [
`${spec.serviceId} live apply checks required runtime secret presence in the canonical Compose env file without reading or printing values`,
]),
...(target.compose.authHealthGate === undefined ? [] : [
`${spec.serviceId} live apply gates success on /health.auth fields: ${target.compose.authHealthGate.requiredFields.join(", ")}`,
]),
],
runtimeSecrets: target.compose.requiredRuntimeSecrets === undefined ? undefined : {
check: "live-apply-preflight",
valuesPrinted: false,
requirements: target.compose.requiredRuntimeSecrets.map((item) => ({
sourceEnvName: item.sourceEnvName,
containerEnvName: item.containerEnvName,
presence: "not-read-during-dry-run",
})),
dryRunDisposition: "pending-live-check",
},
authHealthGate: target.compose.authHealthGate === undefined ? undefined : {
path: "/health",
requiredAuthFields: target.compose.authHealthGate.requiredFields,
dryRunDisposition: "pending-live-check",
},
rollback: rollbackInfo(spec, target, environment, commit),
};
}
+2
View File
@@ -300,12 +300,14 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(commandItem("code-queue:stale-active-owner-expired", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:stale-active-owner-expired"], 30_000));
items.push(commandItem("code-queue:control-plane-split-brain-diagnostics", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:control-plane-split-brain-diagnostics"], 30_000));
items.push(commandItem("code-queue:oa-publisher-degraded-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:oa-publisher-degraded-visible"], 30_000));
items.push(commandItem("baidu-netdisk:artifact-guard-contract", ["bun", "scripts/baidu-netdisk-artifact-guard-contract-test.ts"], 30_000));
} else {
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:prompt-observation-contract", "prompt observation contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:issue3-diagnostics-and-image-preflight", "Code Queue issue #3 regression fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:trace-summary-contract", "Code Queue trace summary contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("baidu-netdisk:artifact-guard-contract", "Baidu Netdisk artifact guard contract is opt-in with script checks", "--scripts-typecheck or --full"));
}
if (options.logs) {
items.push(unifiedLogRotationItem());