Files
pikasTech-unidesk/scripts/src/hwlab-node/plan.ts
T
2026-06-28 11:51:34 +00:00

213 lines
10 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. plan module for scripts/src/hwlab-node-impl.ts.
// Moved mechanically from scripts/src/hwlab-node-impl.ts:440-567 for #903.
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// Responsibility: YAML-first node/lane operations, including Workbench observability control commands.
import { createHash, randomBytes } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { repoRoot, rootPath, type Config } from "../config";
import { runCommand, type CommandResult } from "../command";
import { startJob } from "../jobs";
import { classifySshTcpPoolFailure } from "../ssh";
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
import { runWebProbeSentinelCommand, type WebProbeSentinelOptions } from "../hwlab-node-web-sentinel-cicd";
import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "../hwlab-node-help";
import { compactWebProbeResult, compactWebProbeScriptResult } from "../hwlab-node-web-probe-summary";
import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "../hwlab-node-observability-promql";
import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "../hwlab-node-transport";
import type { RenderedCliResult } from "../output";
import { NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS } from "./entry";
import { publicExposureSummary } from "./public-exposure";
import { assertNodeId, positiveIntegerOption, requiredOption } from "./utils";
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
export function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, args: string[]): {
domain: DelegatedNodeDomain;
action: string;
runtimeImageAction: string | null;
node: string;
lane: HwlabRuntimeLane;
confirm: boolean;
dryRun: boolean;
wait: boolean;
rerun: boolean;
allowLiveDbRead: boolean;
timeoutSeconds: number;
originalArgs: string[];
spec: HwlabRuntimeLaneSpec;
} {
const [actionRaw] = args;
if (typeof actionRaw !== "string" || actionRaw.startsWith("--")) throw new Error(`${domain} usage: ${domain} ACTION --node NODE --lane vNN [--dry-run|--confirm]`);
const runtimeImageAction = actionRaw === "runtime-image" && typeof args[1] === "string" && !args[1].startsWith("--") ? args[1] : null;
const node = requiredOption(args, "--node");
assertNodeId(node);
const laneRaw = requiredOption(args, "--lane");
if (!isHwlabRuntimeLane(laneRaw)) throw new Error(`--lane must be one of v02, v03; got ${laneRaw}`);
const spec = hwlabRuntimeLaneSpecForNode(laneRaw, node);
const confirm = args.includes("--confirm");
const dryRun = args.includes("--dry-run");
if (confirm && dryRun) throw new Error(`${domain} accepts only one of --confirm or --dry-run`);
return {
domain,
action: actionRaw,
runtimeImageAction,
node,
lane: laneRaw,
confirm,
dryRun,
wait: args.includes("--wait"),
rerun: args.includes("--rerun"),
allowLiveDbRead: args.includes("--allow-live-db-read"),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS, 3600),
originalArgs: [...args],
spec,
};
}
export function nodeRuntimeLocalPostgresExpectedAbsent(spec: HwlabRuntimeLaneSpec): boolean {
return spec.runtimeStore?.postgres?.mode === "platform-service";
}
export function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
const activeExternalPostgres = hwlabRuntimeActiveExternalPostgres(spec);
return {
configPath: hwlabRuntimeLaneConfigPath(),
node: spec.nodeId,
nodeRoute: spec.nodeRoute,
nodeKubeRoute: spec.nodeKubeRoute,
lane: spec.lane,
sourceBranch: spec.sourceBranch,
workspace: spec.workspace,
cicdRepo: spec.cicdRepo,
git: {
sourceUrl: spec.gitUrl,
readUrl: spec.gitReadUrl,
writeUrl: spec.gitWriteUrl,
},
argo: {
repoURL: spec.argoRepoUrl,
},
gitopsBranch: spec.gitopsBranch,
catalogPath: spec.catalogPath,
runtimePath: spec.runtimePath,
runtimeNamespace: spec.runtimeNamespace,
renderDir: spec.runtimeRenderDir,
pipeline: spec.pipeline,
pipelineRunPrefix: spec.pipelineRunPrefix,
serviceAccount: spec.serviceAccountName,
argoApplication: spec.app,
registryPrefix: spec.registryPrefix,
baseImage: {
image: spec.baseImage,
sourceImage: spec.baseImageSource ?? null,
},
serviceIds: spec.serviceIds,
buildkit: spec.buildkit === undefined ? null : {
sidecarImage: spec.buildkit.sidecarImage,
sourceImage: spec.buildkit.sourceImage,
},
dockerBuildProxy: {
http: spec.networkProfile.dockerBuildProxy.http,
https: spec.networkProfile.dockerBuildProxy.https,
all: spec.networkProfile.dockerBuildProxy.all,
noProxy: spec.networkProfile.dockerBuildProxy.noProxy,
},
stepEnv: spec.stepEnv,
public: {
webUrl: spec.publicWebUrl,
apiUrl: spec.publicApiUrl,
},
webProbe: spec.webProbe === undefined ? null : {
browserProxyMode: spec.webProbe.browserProxyMode ?? null,
defaultOrigin: spec.webProbe.defaultOrigin ?? null,
},
bootstrapAdmin: spec.bootstrapAdmin === undefined ? null : {
username: spec.bootstrapAdmin.username,
displayName: spec.bootstrapAdmin.displayName,
passwordSourceRef: spec.bootstrapAdmin.passwordSourceRef,
passwordSourceKey: spec.bootstrapAdmin.passwordSourceKey,
passwordHashTransform: spec.bootstrapAdmin.passwordHashTransform,
secretName: spec.bootstrapAdmin.secretName,
secretKey: spec.bootstrapAdmin.secretKey,
rolloutDeployment: spec.bootstrapAdmin.rolloutDeployment,
valuesPrinted: false,
},
codeAgentRuntime: spec.codeAgentRuntime === undefined ? null : {
enabled: spec.codeAgentRuntime.enabled,
adapter: spec.codeAgentRuntime.adapter,
managerUrl: spec.codeAgentRuntime.managerUrl,
apiKeySecretName: spec.codeAgentRuntime.apiKeySecretName,
apiKeySecretKey: spec.codeAgentRuntime.apiKeySecretKey,
runnerNamespace: spec.codeAgentRuntime.runnerNamespace,
secretNamespace: spec.codeAgentRuntime.secretNamespace,
repoUrlFrom: spec.codeAgentRuntime.repoUrlFrom,
repoUrl: spec.gitReadUrl,
providerIdFrom: spec.codeAgentRuntime.providerIdFrom,
providerId: spec.nodeId,
defaultProviderProfile: spec.codeAgentRuntime.defaultProviderProfile,
codexStdioSupervisor: spec.codeAgentRuntime.codexStdioSupervisor,
kafkaShadowProducer: spec.codeAgentRuntime.kafkaShadowProducer ?? null,
valuesPrinted: false,
},
sourceWorkspace: spec.sourceWorkspace === undefined ? null : {
requiredCommands: spec.sourceWorkspace.requiredCommands,
requiredFiles: spec.sourceWorkspace.requiredFiles,
install: spec.sourceWorkspace.install,
},
publicExposure: spec.publicExposure === null ? null : publicExposureSummary(spec.publicExposure),
runtimeStore: spec.runtimeStore ?? null,
downloadProfile: {
id: spec.downloadProfileId,
git: spec.downloadProfile.git,
npm: spec.downloadProfile.npm,
},
observability: spec.observability,
runtimeImageRewrites: spec.runtimeImageRewrites,
runtimeImageBuilds: spec.runtimeImageBuilds,
externalPostgres: spec.externalPostgres === undefined ? null : {
active: activeExternalPostgres !== undefined,
provider: spec.externalPostgres.provider,
configRef: spec.externalPostgres.configRef,
serviceName: spec.externalPostgres.serviceName,
endpointAddress: spec.externalPostgres.endpointAddress,
port: spec.externalPostgres.port,
runtimeAccess: spec.externalPostgres.runtimeAccess ?? null,
sslmode: spec.externalPostgres.sslmode,
database: spec.externalPostgres.database,
cloudApi: {
secretName: spec.externalPostgres.cloudApi.secretName,
secretKey: spec.externalPostgres.cloudApi.secretKey,
sourceRef: spec.externalPostgres.cloudApi.sourceRef,
envKey: spec.externalPostgres.cloudApi.envKey,
role: spec.externalPostgres.cloudApi.role,
},
openfga: {
secretName: spec.externalPostgres.openfga.secretName,
secretKey: spec.externalPostgres.openfga.secretKey,
sourceRef: spec.externalPostgres.openfga.sourceRef,
envKey: spec.externalPostgres.openfga.envKey,
authnKey: spec.externalPostgres.openfga.authnKey ?? null,
role: spec.externalPostgres.openfga.role,
schema: spec.externalPostgres.openfga.schema ?? null,
},
valuesPrinted: false,
},
localPostgres: {
shouldRender: spec.runtimeStore?.postgres?.mode !== "platform-service",
expectedAbsent: spec.runtimeStore?.postgres?.mode === "platform-service",
},
};
}