fix: expose baidu netdisk secret source contract
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user