fix: expose baidu netdisk secret source contract

This commit is contained in:
Codex
2026-05-21 10:09:22 +00:00
parent 1749897d1a
commit f701eab29d
8 changed files with 245 additions and 30 deletions
+141 -21
View File
@@ -166,6 +166,7 @@ interface AuthHealthGate {
interface ComposeArtifactRuntime {
workDir: string;
composeFile: string;
composeEnvFile: string;
envFile: string;
project: string;
command: string[];
@@ -178,6 +179,36 @@ export interface RuntimeSecretPresence {
length: number;
}
export interface RuntimeSecretSource {
kind: "compose-env-file";
path: string;
exists: boolean;
configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile";
workDir: string;
composeEnvFile: string;
composeService: string | null;
containerName: string | null;
valuesPrinted: false;
}
export interface RuntimeSecretRequirementStatus {
sourceEnvName: string;
containerEnvName: string;
present: boolean;
valuePrinted: false;
}
export interface RuntimeSecretContract {
check: "runtime-secret-presence";
secretSource: RuntimeSecretSource;
requiredSecretsPresent: boolean;
missingSecretKeys: string[];
recommendedAction: string;
valuesPrinted: false;
requirements: RuntimeSecretRequirementStatus[];
dryRunDisposition: "not-required" | "ready-for-live-apply" | "secret-source-blocked";
}
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)})\"";
}
@@ -1553,13 +1584,12 @@ function composeLockScript(innerScript: string): string {
].join("; ");
}
function parseEnvFile(raw: string): Record<string, string> {
const values: Record<string, string> = {};
function findEnvValueLength(raw: string, key: string): number {
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);
if (line.slice(0, index) !== key) continue;
let value = line.slice(index + 1);
if (value.startsWith("\"") && value.endsWith("\"")) {
try {
@@ -1568,20 +1598,19 @@ function parseEnvFile(raw: string): Record<string, string> {
value = value.slice(1, -1);
}
}
values[key] = value;
return value.length;
}
return values;
return 0;
}
export function runtimeSecretPresenceFromEnvText(envText: string, requirements: RuntimeSecretRequirement[]): RuntimeSecretPresence[] {
const values = parseEnvFile(envText);
return requirements.map((requirement) => {
const value = values[requirement.sourceEnvName] ?? "";
const length = findEnvValueLength(envText, requirement.sourceEnvName);
return {
sourceEnvName: requirement.sourceEnvName,
containerEnvName: requirement.containerEnvName,
present: value.length > 0,
length: value.length,
present: length > 0,
length,
};
});
}
@@ -1602,10 +1631,97 @@ function composeArtifactRuntime(config: UniDeskConfig, target: ArtifactConsumerT
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 composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
const envFile = join(workDir, 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 };
return { workDir, composeFile, composeEnvFile, envFile, project, command };
}
function runtimeSecretSourceForTarget(config: UniDeskConfig, target: ArtifactConsumerTarget): RuntimeSecretSource {
const runtime = composeArtifactRuntime(config, target);
return {
kind: "compose-env-file",
path: runtime.envFile,
exists: existsSync(runtime.envFile),
configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
workDir: runtime.workDir,
composeEnvFile: runtime.composeEnvFile,
composeService: target.compose?.serviceName ?? null,
containerName: target.compose?.containerName ?? null,
valuesPrinted: false,
};
}
function runtimeSecretRecommendedAction(missing: string[]): string {
if (missing.length === 0) return "none";
return "Restore the missing source env keys in the canonical Compose env file without printing values, then rerun deploy apply --env <env> --service <service> --dry-run before any live apply.";
}
function runtimeSecretRecommendedActionForService(contract: RuntimeSecretContract, environment: ArtifactDeployEnvironment, serviceId: string): RuntimeSecretContract {
if (contract.requiredSecretsPresent) return { ...contract, recommendedAction: "none" };
return {
...contract,
recommendedAction: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${serviceId} --dry-run before any live apply.`,
};
}
function runtimeSecretContractFromPresence(
source: RuntimeSecretSource,
requirements: RuntimeSecretRequirement[],
presence: RuntimeSecretPresence[],
): RuntimeSecretContract {
const missingSecretKeys = presence.filter((item) => !item.present).map((item) => item.sourceEnvName);
const requiredSecretsPresent = requirements.length === 0 || (source.exists && missingSecretKeys.length === 0);
return {
check: "runtime-secret-presence",
secretSource: source,
requiredSecretsPresent,
missingSecretKeys,
recommendedAction: runtimeSecretRecommendedAction(missingSecretKeys),
valuesPrinted: false,
requirements: presence.map((item) => ({
sourceEnvName: item.sourceEnvName,
containerEnvName: item.containerEnvName,
present: item.present,
valuePrinted: false,
})),
dryRunDisposition: requirements.length === 0
? "not-required"
: requiredSecretsPresent
? "ready-for-live-apply"
: "secret-source-blocked",
};
}
export function runtimeSecretContractFromEnvText(
envText: string,
requirements: RuntimeSecretRequirement[],
sourceOverrides: Partial<Omit<RuntimeSecretSource, "kind" | "valuesPrinted">> = {},
): RuntimeSecretContract {
const source: RuntimeSecretSource = {
kind: "compose-env-file",
path: sourceOverrides.path ?? "<env-text>",
exists: sourceOverrides.exists ?? true,
configSource: sourceOverrides.configSource ?? "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile",
workDir: sourceOverrides.workDir ?? "",
composeEnvFile: sourceOverrides.composeEnvFile ?? "",
composeService: sourceOverrides.composeService ?? null,
containerName: sourceOverrides.containerName ?? null,
valuesPrinted: false,
};
return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
}
export function runtimeSecretContractForComposeTarget(
config: UniDeskConfig,
target: ArtifactConsumerTarget,
requirements: RuntimeSecretRequirement[] | undefined,
): RuntimeSecretContract | undefined {
if (requirements === undefined) return undefined;
const source = runtimeSecretSourceForTarget(config, target);
const envText = source.exists ? readFileSync(source.path, "utf8") : "";
return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements));
}
function composeArtifactSecretPreflight(envFile: string, requirements: RuntimeSecretRequirement[] | undefined): { ok: boolean; envFile: string; requirements: RuntimeSecretPresence[]; missing: RuntimeSecretPresence[]; valuesLogged: false } {
@@ -1767,6 +1883,10 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
const config = readConfig();
const runtime = composeArtifactRuntime(config, target);
const secretPreflight = composeArtifactSecretPreflight(runtime.envFile, target.compose.requiredRuntimeSecrets);
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
const runtimeSecrets = runtimeSecretsRaw === undefined
? undefined
: runtimeSecretRecommendedActionForService(runtimeSecretsRaw, options.environment ?? "prod", spec.serviceId);
if (!secretPreflight.ok) {
return {
ok: false,
@@ -1778,6 +1898,11 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
envFile: runtime.envFile,
requirements: secretPreflight.requirements,
missing: secretPreflight.missing,
secretSource: runtimeSecrets?.secretSource,
requiredSecretsPresent: runtimeSecrets?.requiredSecretsPresent ?? false,
missingSecretKeys: runtimeSecrets?.missingSecretKeys ?? secretPreflight.missing.map((item) => item.sourceEnvName),
recommendedAction: runtimeSecrets?.recommendedAction ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
runtimeSecrets,
valuesLogged: false,
recoveryHint: target.compose.authHealthGate?.recoveryHint ?? "Restore the required runtime secrets in the canonical Compose env file, then rerun artifact deploy.",
};
@@ -1898,6 +2023,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
requiredRuntimeSecrets: secretPreflight.requirements,
runtimeSecrets,
authHealthGate: target.compose.authHealthGate === undefined ? "not-required" : {
requiredFields: target.compose.authHealthGate.requiredFields,
passed: true,
@@ -2119,6 +2245,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
const livePolicy = environment === "prod" ? spec.prodLiveApply : "enabled";
const sourceImage = artifactImageRef(options, spec, commit);
const registryEndpoint = `http://127.0.0.1:${options.port}`;
const config = readConfig();
const k3sDeployments = target.k3s === undefined
? []
: [
@@ -2180,6 +2307,8 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
};
if (spec.kind === "compose" || spec.kind === "d601-compose") {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
const runtimeSecretsRaw = runtimeSecretContractForComposeTarget(config, target, target.compose.requiredRuntimeSecrets);
const runtimeSecrets = runtimeSecretsRaw === undefined ? undefined : runtimeSecretRecommendedActionForService(runtimeSecretsRaw, environment, spec.serviceId);
return {
...common,
target: {
@@ -2227,16 +2356,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
`${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",
},
runtimeSecrets,
authHealthGate: target.compose.authHealthGate === undefined ? undefined : {
path: "/health",
requiredAuthFields: target.compose.authHealthGate.requiredFields,
+24 -1
View File
@@ -5,7 +5,7 @@ import { pathToFileURL } from "node:url";
import { runCommand } from "./command";
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "./config";
import { ensureGithubSshIdentityForProvider } from "./deploy-ssh-identity";
import { runArtifactRegistryCommand } from "./artifact-registry";
import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "./artifact-registry";
import { startJob } from "./jobs";
import { coreInternalFetch } from "./microservices";
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "./code-queue-source-guard";
@@ -1216,6 +1216,27 @@ function directComposeEnvFile(service: UniDeskMicroserviceConfig): string {
return targetIsMain(service) ? writeComposeEnvFallbackPath() : "";
}
function redactedSecretContractForService(config: UniDeskConfig | null, service: UniDeskMicroserviceConfig, environment: DeployEnvironment): RuntimeSecretContract | undefined {
if (config === null || service.id !== "baidu-netdisk" || !targetIsMain(service)) return undefined;
const composeEnvFile = config.providerGateway.upgrade.composeEnvFile;
const envFile = join(config.providerGateway.upgrade.hostProjectRoot, composeEnvFile);
const envText = existsSync(envFile) ? readFileSync(envFile, "utf8") : "";
const contract = runtimeSecretContractFromEnvText(envText, baiduNetdiskRuntimeSecretRequirements, {
path: envFile,
exists: existsSync(envFile),
workDir: config.providerGateway.upgrade.hostProjectRoot,
composeEnvFile,
composeService: service.repository.composeService,
containerName: service.repository.containerName,
});
return {
...contract,
recommendedAction: contract.requiredSecretsPresent
? "none"
: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${service.id} --dry-run before any live apply.`,
};
}
function directBuildContextOverride(service: UniDeskMicroserviceConfig): string {
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) return targetWorkDir(service);
return "";
@@ -2946,6 +2967,7 @@ function environmentDryRunPlan(
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
const planKind = serviceConfig === null ? "unsupported" : artifactConsumerPlanKind(serviceConfig, environment);
const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment);
const runtimeSecrets = serviceConfig === null ? undefined : redactedSecretContractForService(config, serviceConfig, environment);
const unsupportedReason = unsupported ? unsupportedEnvironmentPlanReason(service.id, environment) : null;
const effectiveTarget = unsupported
? unsupportedPlanTarget(service.id, environment, unsupportedReason ?? "unsupported")
@@ -2992,6 +3014,7 @@ function environmentDryRunPlan(
noRuntimeSourceBuild: unsupported || planKind !== "d601-dev-target-side-build",
dryRunOnly: unsupported || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
blockedReason: unsupportedReason ?? dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
runtimeSecrets,
},
target: effectiveTarget,
validation: unsupported