refactor: split control-plane cli modules
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user