Files
pikasTech-unidesk/scripts/src/hwlab-node-control-plane-argo.ts
T
2026-07-22 17:39:49 +02:00

173 lines
8.8 KiB
TypeScript

import { argoDesiredManifest, argoInternalHttpIdentity } from "./hwlab-node-control-plane-argo-model";
import { manifestObjectSummary, remoteJobLogs, remoteJobStateDir, remoteJobStatusScript, runTargetK3s } from "./hwlab-node-control-plane-executor";
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, type ArgoOptions, type ControlPlaneNodeSpec, type ControlPlaneTargetSpec } from "./hwlab-node-control-plane-model";
import {
boolField,
compactCommandResult,
controlPlaneEgressProxySummary,
record,
runtimeHostProxyConfig,
runtimeProxyReady,
sha256Short,
statusScript,
} from "./hwlab-node-control-plane-runtime";
import { argoApplyStartScript, parseRemoteJson } from "./hwlab-node-control-plane-runtime-delivery";
export function argoCommandStatus(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, options: ArgoOptions): Record<string, unknown> {
const result = runTargetK3s(node, statusScript(node, target), options.timeoutSeconds);
const status = record(parseRemoteJson(result.stdout));
const argo = record(record(status.components).argo);
const argoInstall = record(argo.install);
const internalHttp = record(argoInstall.internalHttp);
const reconciliation = record(argoInstall.reconciliation);
const runtimeProxy = record(argoInstall.runtimeProxy);
const jobResult = runTargetK3s(node, remoteJobStatusScript(target, "argo", 0), options.timeoutSeconds);
const jobStatus = parseRemoteJson(jobResult.stdout);
const ok = boolField(argo, "installed")
&& boolField(argo, "projectExists")
&& boolField(argo, "applicationExists")
&& boolField(argoInstall, "crdsReady")
&& boolField(argoInstall, "deploymentsReady")
&& boolField(argoInstall, "statefulSetsReady")
&& boolField(internalHttp, "ready")
&& boolField(reconciliation, "ready")
&& runtimeProxyReady(argoInstall);
return {
ok,
command: "hwlab nodes control-plane infra argo status",
configPath: HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH,
node: node.id,
lane: target.lane,
executionPlane: node.kubeExecution,
mutation: false,
expected: {
namespace: target.argo.namespace,
project: target.argo.projectName,
application: target.argo.applicationName,
install: {
version: target.argo.install.version,
expectedDeploymentCount: target.argo.install.expectedDeployments.length,
expectedStatefulSetCount: target.argo.install.expectedStatefulSets.length,
internalHttp: target.argo.install.internalHttp,
reconciliation: target.argo.install.reconciliation,
},
},
readiness: {
installed: boolField(argo, "installed"),
projectExists: boolField(argo, "projectExists"),
applicationExists: boolField(argo, "applicationExists"),
crdsReady: boolField(argoInstall, "crdsReady"),
deploymentsReady: boolField(argoInstall, "deploymentsReady"),
statefulSetsReady: boolField(argoInstall, "statefulSetsReady"),
internalHttpReady: boolField(internalHttp, "ready"),
reconciliationReady: boolField(reconciliation, "ready"),
runtimeProxyReady: runtimeProxyReady(argoInstall),
},
argo: {
namespace: argo.namespace ?? target.argo.namespace,
installed: argo.installed === true,
projectExists: argo.projectExists === true,
applicationExists: argo.applicationExists === true,
observerRbacReady: record(argo.argoObserverRbac).ready === true,
install: {
crdsReady: argoInstall.crdsReady === true,
deploymentsReady: argoInstall.deploymentsReady === true,
statefulSetsReady: argoInstall.statefulSetsReady === true,
internalHttp: {
enabled: internalHttp.enabled === true,
ready: internalHttp.ready === true,
configMapName: internalHttp.configMapName ?? null,
configMatches: internalHttp.configMatches === true,
deploymentName: internalHttp.deploymentName ?? null,
rolloutMatches: internalHttp.rolloutMatches === true,
},
reconciliation: {
enabled: reconciliation.enabled === true,
ready: reconciliation.ready === true,
configMapName: reconciliation.configMapName ?? null,
timeoutSeconds: reconciliation.timeoutSeconds ?? null,
jitterSeconds: reconciliation.jitterSeconds ?? null,
timeoutMatches: reconciliation.timeoutMatches === true,
jitterMatches: reconciliation.jitterMatches === true,
},
runtimeProxy: { enabled: runtimeProxy.enabled === true, ready: runtimeProxy.ready === true, mode: runtimeProxy.mode ?? null },
},
},
job: typeof jobStatus === "object" && jobStatus !== null ? jobStatus : { parseError: "job status did not return JSON" },
result: {
k3s: { exitCode: result.exitCode, timedOut: result.timedOut, stdoutBytes: Buffer.byteLength(result.stdout), stderrBytes: Buffer.byteLength(result.stderr) },
job: { exitCode: jobResult.exitCode, timedOut: jobResult.timedOut, stdoutBytes: Buffer.byteLength(jobResult.stdout), stderrBytes: Buffer.byteLength(jobResult.stderr) },
},
next: ok
? { infraStatus: `bun scripts/cli.ts hwlab nodes control-plane infra status --node ${node.id} --lane ${target.lane}` }
: { apply: `bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node ${node.id} --lane ${target.lane} --confirm`, logs: `bun scripts/cli.ts hwlab nodes control-plane infra argo logs --node ${node.id} --lane ${target.lane}` },
};
}
export function argoApply(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, options: ArgoOptions): Record<string, unknown> {
if (options.confirm && options.dryRun) throw new Error("argo apply accepts only one of --dry-run or --confirm");
if (!target.argo.install.enabled) throw new Error(`targets.${target.id}.argo.install.enabled=false`);
const dryRun = options.dryRun || !options.confirm;
const desired = argoDesiredManifest(target);
const desiredYaml = `${desired.map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n")}\n`;
const applyPlan = {
namespace: target.argo.namespace,
version: target.argo.install.version,
manifestUrl: target.argo.install.manifestUrl,
fieldManager: target.argo.install.fieldManager,
imageRewrites: target.argo.install.imageRewrites,
preloadImages: target.argo.install.preloadImages,
requiredCrds: target.argo.install.requiredCrds,
desired: manifestObjectSummary(desired),
desiredSha256: sha256Short(desiredYaml),
stateDir: remoteJobStateDir(target, "argo"),
egressProxy: controlPlaneEgressProxySummary(node.egressProxy),
runtimeProxy: runtimeHostProxyConfig(node, target.argo.install.runtimeProxy),
internalHttp: target.argo.install.internalHttp === null ? null : { ...target.argo.install.internalHttp, rolloutIdentity: argoInternalHttpIdentity(target) },
reconciliation: target.argo.install.reconciliation,
};
const applyRunner = argoApplyStartScript(node, target, desiredYaml);
if (dryRun) {
return {
ok: true,
command: "hwlab nodes control-plane infra argo apply",
configPath: HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH,
node: node.id,
lane: target.lane,
executionPlane: node.kubeExecution,
mode: "dry-run",
mutation: false,
applyPlan,
desiredYaml: { objects: desired.length, bytes: Buffer.byteLength(desiredYaml), sha256: sha256Short(desiredYaml), preview: desiredYaml },
runner: { bytes: Buffer.byteLength(applyRunner), sha256: sha256Short(applyRunner) },
next: { confirm: `bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node ${node.id} --lane ${target.lane} --confirm` },
};
}
const result = runTargetK3s(node, applyRunner, options.timeoutSeconds);
const parsed = parseRemoteJson(result.stdout);
return {
ok: result.exitCode === 0,
command: "hwlab nodes control-plane infra argo apply",
configPath: HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH,
node: node.id,
lane: target.lane,
executionPlane: node.kubeExecution,
mode: "confirmed-start",
mutation: result.exitCode === 0,
applyPlan,
start: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 2000) },
result: compactCommandResult(result),
next: {
status: `bun scripts/cli.ts hwlab nodes control-plane infra argo status --node ${node.id} --lane ${target.lane}`,
logs: `bun scripts/cli.ts hwlab nodes control-plane infra argo logs --node ${node.id} --lane ${target.lane}`,
infraStatus: `bun scripts/cli.ts hwlab nodes control-plane infra status --node ${node.id} --lane ${target.lane}`,
},
};
}
export function runArgoControlPlaneCommand(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, options: ArgoOptions): Record<string, unknown> {
if (options.action === "status") return argoCommandStatus(node, target, options);
if (options.action === "logs") return remoteJobLogs(node, target, "argo", options);
return argoApply(node, target, options);
}