230 lines
13 KiB
TypeScript
230 lines
13 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. apply-service module for scripts/src/deploy.ts.
|
|
|
|
// Moved mechanically from scripts/src/deploy.ts:2840-3031 for #903.
|
|
|
|
import { createHash } from "node:crypto";
|
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
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 { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "../artifact-registry";
|
|
import { startJob } from "../jobs";
|
|
import { coreInternalFetch } from "../microservices";
|
|
import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "../code-queue-source-guard";
|
|
import { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard";
|
|
import { composeRuntimeEnvValue } from "../runtime-env";
|
|
import {
|
|
compareDeployJsonExecutorMirrors,
|
|
deployJsonCommitImage,
|
|
deployJsonDriftResult,
|
|
deployJsonSourceOfTruth,
|
|
encodeDeployJsonServiceContract,
|
|
hasDeployJsonExecutorContract,
|
|
k3sManifestExecutorMirror,
|
|
parseDeployJsonServiceContract,
|
|
type DeployJsonExecutorMirror,
|
|
type DeployJsonServiceContract,
|
|
} from "../deploy-json-contract";
|
|
|
|
import type { DeployManifestService, DeployOptions, ServiceRuntimeState, StepResult } from "./types";
|
|
import { applyK8sScript, cleanupLegacyDirectCodeQueueScript, rolloutK8sScript, stampK8sScript, verifyK8sImagesScript } from "./health";
|
|
import { asRecord, compactTail, elapsedMs, isUnideskRepo, nowIso, parseFullCommit, progressLine } from "./options";
|
|
import { dockerBuildTimeoutMs, targetExportDir, targetIsMain, targetWorkDir } from "./paths";
|
|
import { healthVerify, manifestEnvironmentForService, pushStep, readRuntimeState, step } from "./remote";
|
|
import { buildDirectImageScript, buildImageScript, codeQueueSourcePreflightScript, composeDeployScript, ensureNativeK3sScript, imageLabelVerifyScript, importK3sImageScript, patchDevK3sManifestScript, prepareSourceScript, syncDevFrontendAuthScript, syncK8sControlManifestsScript, syncSourceScript } from "./scripts";
|
|
import { isD601MaintenanceDeployBlocked, isDevArtifactConsumerService, isDevK3sDeployService, isDirectComposeDeployMode, unsupportedReason } from "./service-plan";
|
|
|
|
export async function ensureGithubSshIdentityStep(config: UniDeskConfig, service: UniDeskMicroserviceConfig): Promise<StepResult> {
|
|
const startedAt = nowIso();
|
|
const startedMs = Date.now();
|
|
progressLine("github-ssh-identity", "start", { serviceId: service.id, providerId: service.providerId });
|
|
try {
|
|
const result = await ensureGithubSshIdentityForProvider(config, service.providerId);
|
|
progressLine("github-ssh-identity", result.ok ? "succeeded" : "failed", {
|
|
serviceId: service.id,
|
|
providerId: service.providerId,
|
|
elapsedMs: elapsedMs(startedMs),
|
|
fingerprint: result.fingerprint,
|
|
seededFromLocal: result.seededFromLocal,
|
|
detail: result.ok ? result.detail : compactTail(result.detail, 1200),
|
|
});
|
|
return {
|
|
step: "github-ssh-identity",
|
|
ok: result.ok,
|
|
detail: result.detail,
|
|
startedAt,
|
|
finishedAt: nowIso(),
|
|
raw: result.raw,
|
|
};
|
|
} catch (error) {
|
|
const detail = error instanceof Error ? error.message : String(error);
|
|
progressLine("github-ssh-identity", "failed", {
|
|
serviceId: service.id,
|
|
providerId: service.providerId,
|
|
elapsedMs: elapsedMs(startedMs),
|
|
detail: compactTail(detail, 1200),
|
|
});
|
|
return { step: "github-ssh-identity", ok: false, detail, startedAt, finishedAt: nowIso(), raw: null };
|
|
}
|
|
}
|
|
|
|
export async function runDevArtifactConsumerService(
|
|
service: UniDeskMicroserviceConfig,
|
|
desired: DeployManifestService,
|
|
options: DeployOptions,
|
|
before: ServiceRuntimeState,
|
|
): Promise<Record<string, unknown>> {
|
|
const commit = options.commitOverride ?? desired.commitId;
|
|
const artifactArgs = [
|
|
"deploy-service",
|
|
"--env", "dev",
|
|
"--service", service.id,
|
|
"--commit", commit,
|
|
"--source-repo", desired.repo,
|
|
"--timeout-ms", String(options.timeoutMs),
|
|
"--provider-id", options.providerId,
|
|
"--run-now",
|
|
...(options.dryRun ? ["--dry-run"] : []),
|
|
];
|
|
progressLine("dev-artifact-cd", options.dryRun ? "dry-run" : "reconcile", { serviceId: service.id, commit });
|
|
const result = await runArtifactRegistryCommand(artifactArgs);
|
|
const ok = asRecord(result)?.ok === true;
|
|
return {
|
|
ok,
|
|
action: ok ? "deployed" : "failed",
|
|
environment: "dev",
|
|
executor: "d601-registry-artifact-consumer",
|
|
resolvedCommit: commit,
|
|
before,
|
|
after: result,
|
|
steps: [{
|
|
step: "artifact-registry-consumer",
|
|
ok,
|
|
detail: JSON.stringify(result),
|
|
startedAt: nowIso(),
|
|
finishedAt: nowIso(),
|
|
raw: result,
|
|
}],
|
|
};
|
|
}
|
|
|
|
export async function applyOneService(config: UniDeskConfig, service: UniDeskMicroserviceConfig, desired: DeployManifestService, options: DeployOptions): Promise<Record<string, unknown>> {
|
|
const steps: StepResult[] = [];
|
|
const startedAt = nowIso();
|
|
const reason = unsupportedReason(service);
|
|
if (reason !== null) return { ok: false, serviceId: service.id, skipped: true, reason, steps };
|
|
const before = await readRuntimeState(config, service, desired);
|
|
if (!options.force && before.upToDate) return { ok: true, serviceId: service.id, action: "noop", before, steps };
|
|
|
|
if (manifestEnvironmentForService(service) === "dev" && isDevArtifactConsumerService(service)) {
|
|
const artifact = await runDevArtifactConsumerService(service, desired, options, before);
|
|
return {
|
|
...artifact,
|
|
before,
|
|
serviceId: service.id,
|
|
startedAt,
|
|
finishedAt: nowIso(),
|
|
};
|
|
}
|
|
|
|
if (options.dryRun) return { ok: true, serviceId: service.id, action: "would-deploy", before, steps };
|
|
|
|
if (!options.dryRun && isD601MaintenanceDeployBlocked(service)) {
|
|
return {
|
|
ok: false,
|
|
serviceId: service.id,
|
|
skipped: true,
|
|
reason: `D601 target-side deployment is allowed only for k3sctl-adapter as a control-bridge maintenance path; artifact consumers must use registry CD. ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
|
|
steps,
|
|
};
|
|
}
|
|
|
|
const runId = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
const exportDir = targetExportDir(service, runId);
|
|
if (!targetIsMain(service) && isUnideskRepo(desired.repo)) {
|
|
const identity = await ensureGithubSshIdentityStep(config, service);
|
|
if (!pushStep(steps, identity)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), before, steps };
|
|
}
|
|
const prepare = await step(config, service, "prepare-source", prepareSourceScript(service, desired, exportDir), targetIsMain(service) ? repoRoot : "/home/ubuntu", Math.min(options.timeoutMs, 180_000), !targetIsMain(service));
|
|
if (!pushStep(steps, prepare)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), before, steps };
|
|
const resolvedCommit = parseFullCommit(prepare.detail) || parseFullCommit(JSON.stringify(prepare.raw));
|
|
if (resolvedCommit.length !== 40) {
|
|
const failure = { step: "resolve-commit", ok: false, detail: "prepare-source did not expose a full 40-char commit", startedAt: nowIso(), finishedAt: nowIso(), raw: prepare };
|
|
steps.push(failure);
|
|
return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), before, steps };
|
|
}
|
|
|
|
const sync = await step(config, service, "sync-source", syncSourceScript(service, exportDir), targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, !targetIsMain(service));
|
|
if (!pushStep(steps, sync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
|
|
const codeQueueSourcePreflight = codeQueueSourcePreflightScript(service);
|
|
if (codeQueueSourcePreflight.length > 0) {
|
|
const sourcePreflight = await step(config, service, "code-queue-hostpath-source-preflight", codeQueueSourcePreflight, targetWorkDir(service), 60_000, !targetIsMain(service));
|
|
if (!pushStep(steps, sourcePreflight)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
}
|
|
|
|
const controlManifestSyncScript = syncK8sControlManifestsScript(service);
|
|
if (controlManifestSyncScript.length > 0) {
|
|
const controlManifestStep = isDevK3sDeployService(service) ? "verify-target-k3s-manifest" : "sync-k3s-control-manifests";
|
|
const controlManifestSync = await step(config, service, controlManifestStep, controlManifestSyncScript, targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, !targetIsMain(service));
|
|
if (!pushStep(steps, controlManifestSync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
}
|
|
|
|
if (isDevK3sDeployService(service)) {
|
|
const patchManifest = await step(config, service, "patch-dev-k3s-manifest", patchDevK3sManifestScript(service, desired, resolvedCommit), targetWorkDir(service), 60_000, false);
|
|
if (!pushStep(steps, patchManifest)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
}
|
|
|
|
if (isDevK3sDeployService(service) && service.id === "frontend") {
|
|
const authSync = await step(config, service, "sync-dev-frontend-auth", syncDevFrontendAuthScript(config), "/home/ubuntu", 60_000, false);
|
|
if (!pushStep(steps, authSync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
}
|
|
|
|
const buildScript = isDirectComposeDeployMode(service)
|
|
? buildDirectImageScript(service, desired, resolvedCommit)
|
|
: buildImageScript(service, desired, resolvedCommit);
|
|
const build = await step(config, service, "docker-build", buildScript, targetWorkDir(service), dockerBuildTimeoutMs(service, options), !targetIsMain(service));
|
|
if (!pushStep(steps, build)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
|
|
if (service.deployment.mode === "k3sctl-managed") {
|
|
const nativeK3s = await step(config, service, "ensure-native-k3s", ensureNativeK3sScript(), "/home/ubuntu", 600_000, true);
|
|
if (!pushStep(steps, nativeK3s)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
const imageImport = await step(config, service, "import-k3s-image", importK3sImageScript(service), targetWorkDir(service), Math.min(options.timeoutMs, 900_000), true);
|
|
if (!pushStep(steps, imageImport)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
const apply = await step(config, service, "kubectl-apply", applyK8sScript(service), targetWorkDir(service), 60_000, false);
|
|
if (!pushStep(steps, apply)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
const k8sImages = await step(config, service, "k8s-image-preflight", verifyK8sImagesScript(service), targetWorkDir(service), 60_000, false);
|
|
if (!pushStep(steps, k8sImages)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
const stamp = await step(config, service, "stamp-deploy-commit", stampK8sScript(service, desired, resolvedCommit), targetWorkDir(service), 60_000, false);
|
|
if (!pushStep(steps, stamp)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
const rollout = await step(config, service, "rollout", rolloutK8sScript(service), targetWorkDir(service), 240_000, true);
|
|
if (!pushStep(steps, rollout)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
const legacyCleanupScript = cleanupLegacyDirectCodeQueueScript(service);
|
|
if (legacyCleanupScript.length > 0) {
|
|
const cleanup = await step(config, service, "legacy-direct-cleanup", legacyCleanupScript, targetWorkDir(service), 90_000, false);
|
|
if (!pushStep(steps, cleanup)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
}
|
|
} else {
|
|
const deploy = await step(config, service, "compose-up", composeDeployScript(service, desired, resolvedCommit), targetIsMain(service) ? repoRoot : targetWorkDir(service), 180_000, !targetIsMain(service));
|
|
if (!pushStep(steps, deploy)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
const imageVerify = await step(config, service, "image-label-verify", imageLabelVerifyScript(service, resolvedCommit), targetIsMain(service) ? repoRoot : targetWorkDir(service), 60_000, false);
|
|
if (!pushStep(steps, imageVerify)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
|
}
|
|
|
|
const health = await healthVerify(config, service, desired, resolvedCommit, 90_000);
|
|
steps.push(health);
|
|
return {
|
|
ok: health.ok,
|
|
serviceId: service.id,
|
|
action: "deployed",
|
|
startedAt,
|
|
finishedAt: nowIso(),
|
|
resolvedCommit,
|
|
before,
|
|
after: health.raw,
|
|
steps,
|
|
};
|
|
}
|