feat: add yaml egress proxy benchmark
This commit is contained in:
@@ -2,6 +2,9 @@ import { createHash } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./config";
|
||||
import { runCommand, type CommandResult } from "./command";
|
||||
import { egressBenchmarkCompactResult, egressBenchmarkDryRun, egressBenchmarkStartScript, egressBenchmarkStatusScript, type EgressBenchmarkSpec } from "./egress-proxy-benchmark";
|
||||
import { resolveEgressProxySourceRef } from "./egress-proxy-sources";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { fingerprintSecretValues, readEnvSourceFile, requiredEnvValue } from "./secrets";
|
||||
|
||||
export const HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH = "config/hwlab-node-control-plane.yaml";
|
||||
@@ -9,6 +12,7 @@ export const HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH = "config/hwlab-node-control-p
|
||||
type InfraAction = "plan" | "status" | "apply";
|
||||
type ToolsImageAction = "status" | "build" | "logs";
|
||||
type ArgoAction = "status" | "apply" | "logs";
|
||||
type EgressBenchmarkAction = "benchmark" | "status" | "logs";
|
||||
|
||||
interface InfraOptions {
|
||||
action: InfraAction;
|
||||
@@ -39,15 +43,31 @@ interface ArgoOptions {
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
interface EgressBenchmarkOptions {
|
||||
action: EgressBenchmarkAction;
|
||||
node: string;
|
||||
lane: string;
|
||||
profile: "no-mirror";
|
||||
dryRun: boolean;
|
||||
confirm: boolean;
|
||||
samples: number;
|
||||
sampleTimeoutSeconds: number;
|
||||
timeoutSeconds: number;
|
||||
tailLines: number;
|
||||
}
|
||||
|
||||
interface ControlPlaneEgressProxySpec {
|
||||
mode: "k8s-service-cluster-ip";
|
||||
clientName: string;
|
||||
namespace: string;
|
||||
serviceName: string;
|
||||
port: number;
|
||||
sourceConfigRef: string | null;
|
||||
sourceFingerprint: string | null;
|
||||
sourceRef: string;
|
||||
sourceKey: string;
|
||||
sourceType: "subscription-url";
|
||||
sourceType: "subscription-url" | "master-shadowsocks";
|
||||
preferredOutbound: "vless-reality" | "hysteria2" | null;
|
||||
noProxy: readonly string[];
|
||||
}
|
||||
|
||||
@@ -180,7 +200,7 @@ interface ControlPlaneConfig {
|
||||
targets: readonly ControlPlaneTargetSpec[];
|
||||
}
|
||||
|
||||
export function runHwlabNodeControlPlaneInfra(args: string[]): Record<string, unknown> {
|
||||
export function runHwlabNodeControlPlaneInfra(args: string[]): Record<string, unknown> | RenderedCliResult {
|
||||
if (args[0] === "tools-image") {
|
||||
const options = parseToolsImageOptions(args.slice(1));
|
||||
const { config, node, target } = controlPlaneContext(options.node, options.lane);
|
||||
@@ -191,6 +211,11 @@ export function runHwlabNodeControlPlaneInfra(args: string[]): Record<string, un
|
||||
const { config, node, target } = controlPlaneContext(options.node, options.lane);
|
||||
return runArgoCommand(config, node, target, options);
|
||||
}
|
||||
if (args[0] === "egress-benchmark") {
|
||||
const options = parseEgressBenchmarkOptions(args.slice(1));
|
||||
const { config, node, target } = controlPlaneContext(options.node, options.lane);
|
||||
return runEgressBenchmarkCommand(config, node, target, options);
|
||||
}
|
||||
const options = parseInfraOptions(args);
|
||||
const { config, node, target } = controlPlaneContext(options.node, options.lane);
|
||||
|
||||
@@ -228,6 +253,8 @@ export function hwlabNodeControlPlaneInfraHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra argo apply --node D601 --lane v03 --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra argo logs --node D601 --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra egress-benchmark --node D601 --lane v03 --profile no-mirror --confirm",
|
||||
"bun scripts/cli.ts hwlab nodes control-plane infra egress-benchmark status --node D601 --lane v03 --profile no-mirror",
|
||||
],
|
||||
g14Consistency: "D601 target fields mirror the existing G14 runtime lane control-plane vocabulary: source branch, gitops branch/path, Pipeline, PipelineRun prefix, ServiceAccount, Argo Application, and git-mirror read/write/sync/flush status concepts.",
|
||||
};
|
||||
@@ -402,6 +429,116 @@ function runArgoCommand(_config: ControlPlaneConfig, node: ControlPlaneNodeSpec,
|
||||
return argoApply(node, target, options);
|
||||
}
|
||||
|
||||
function runEgressBenchmarkCommand(_config: ControlPlaneConfig, node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, options: EgressBenchmarkOptions): Record<string, unknown> | RenderedCliResult {
|
||||
const spec = controlPlaneEgressBenchmarkSpec(node, target, options);
|
||||
if (options.action === "status" || options.action === "logs") {
|
||||
const result = runTransK3s(node.kubeRoute, egressBenchmarkStatusScript(spec, options.tailLines), options.timeoutSeconds);
|
||||
const parsed = parseRemoteJson(result.stdout);
|
||||
const status = record(record(parsed).status);
|
||||
const state = renderCell(status.state, "unknown");
|
||||
return renderControlPlaneBenchmarkResult({
|
||||
ok: result.exitCode === 0 && (options.action === "logs" || state !== "failed"),
|
||||
command: `hwlab nodes control-plane infra egress-benchmark ${options.action}`,
|
||||
configPath: HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH,
|
||||
node: node.id,
|
||||
lane: target.lane,
|
||||
mutation: false,
|
||||
job: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 2000) },
|
||||
result: egressBenchmarkCompactResult(result),
|
||||
});
|
||||
}
|
||||
if (options.dryRun) {
|
||||
return renderControlPlaneBenchmarkResult({
|
||||
ok: true,
|
||||
command: "hwlab nodes control-plane infra egress-benchmark",
|
||||
configPath: HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH,
|
||||
node: node.id,
|
||||
lane: target.lane,
|
||||
mode: "dry-run",
|
||||
mutation: false,
|
||||
plan: egressBenchmarkDryRun(spec),
|
||||
next: { confirm: `bun scripts/cli.ts hwlab nodes control-plane infra egress-benchmark --node ${node.id} --lane ${target.lane} --profile ${options.profile} --confirm` },
|
||||
});
|
||||
}
|
||||
const result = runTransK3s(node.kubeRoute, egressBenchmarkStartScript(spec), options.timeoutSeconds);
|
||||
const parsed = parseRemoteJson(result.stdout);
|
||||
return renderControlPlaneBenchmarkResult({
|
||||
ok: result.exitCode === 0,
|
||||
command: "hwlab nodes control-plane infra egress-benchmark",
|
||||
configPath: HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH,
|
||||
node: node.id,
|
||||
lane: target.lane,
|
||||
mode: "async-job",
|
||||
mutation: result.exitCode === 0,
|
||||
start: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 2000) },
|
||||
result: egressBenchmarkCompactResult(result),
|
||||
});
|
||||
}
|
||||
|
||||
function controlPlaneEgressBenchmarkSpec(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, options: EgressBenchmarkOptions): EgressBenchmarkSpec {
|
||||
const proxy = node.egressProxy;
|
||||
if (proxy === null) throw new Error(`nodes.${node.id}.egressProxy is required for egress-benchmark`);
|
||||
return {
|
||||
scope: "hwlab-control-plane",
|
||||
targetId: `${node.id}/${target.lane}`,
|
||||
route: node.kubeRoute,
|
||||
namespace: proxy.namespace,
|
||||
serviceName: proxy.serviceName,
|
||||
port: proxy.port,
|
||||
noProxy: proxy.noProxy,
|
||||
sourceType: proxy.sourceType,
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourceConfigRef: proxy.sourceConfigRef,
|
||||
sourceFingerprint: proxy.sourceFingerprint,
|
||||
profile: options.profile,
|
||||
samples: options.samples,
|
||||
sampleTimeoutSeconds: options.sampleTimeoutSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
function renderControlPlaneBenchmarkResult(result: Record<string, unknown>): RenderedCliResult {
|
||||
const start = record(result.start);
|
||||
const job = record(result.job);
|
||||
const status = record(job.status);
|
||||
const plan = record(result.plan);
|
||||
const next = record(result.next);
|
||||
const rows = Array.isArray(status.rows) ? status.rows.map(record) : [];
|
||||
const statusText = renderCell(status.state, result.ok === false ? "failed" : "ok");
|
||||
const logTail = typeof job.logTail === "string" ? job.logTail.trimEnd() : "";
|
||||
const node = renderCell(result.node);
|
||||
const lane = renderCell(result.lane);
|
||||
const target = `${node}/${lane}`;
|
||||
const profile = renderCell(result.profile ?? plan.profile ?? status.profile, "no-mirror");
|
||||
const statusCommand = renderCell(start.statusCommand, `bun scripts/cli.ts hwlab nodes control-plane infra egress-benchmark status --node ${node} --lane ${lane} --profile ${profile}`);
|
||||
const logsCommand = renderCell(start.logsCommand, `bun scripts/cli.ts hwlab nodes control-plane infra egress-benchmark logs --node ${node} --lane ${lane} --profile ${profile}`);
|
||||
const lines = [
|
||||
"HWLAB CONTROL-PLANE EGRESS BENCHMARK",
|
||||
"",
|
||||
...renderTable(["TARGET", "PROFILE", "MODE", "STATUS"], [[target, profile, renderCell(result.mode ?? optionsModeFromCommand(result.command)), statusText]]),
|
||||
"",
|
||||
rows.length === 0 ? "RESULTS\n-" : [
|
||||
"RESULTS",
|
||||
...renderTable(["TEST", "SUCCESS", "SAMPLES", "P50", "P95", "FAILURES"], rows.map((row) => [
|
||||
renderCell(row.test),
|
||||
renderCell(row.success),
|
||||
renderCell(row.samples),
|
||||
renderCell(row.p50Ms),
|
||||
renderCell(row.p95Ms),
|
||||
JSON.stringify(row.failureFamilies ?? {}),
|
||||
])),
|
||||
].join("\n"),
|
||||
...(logTail.length === 0 ? [] : ["", "LOG TAIL", logTail]),
|
||||
"",
|
||||
"NEXT",
|
||||
` ${renderCell(next.confirm, "")}`,
|
||||
` ${statusCommand}`,
|
||||
` ${logsCommand}`,
|
||||
"",
|
||||
"Disclosure: default output is bounded; Secret/proxy source values are not printed.",
|
||||
].filter((line) => line !== " ");
|
||||
return { ok: result.ok !== false, command: renderCell(result.command, "hwlab nodes control-plane infra egress-benchmark"), renderedText: lines.join("\n"), contentType: "text/plain" };
|
||||
}
|
||||
|
||||
function toolsImageCommandStatus(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, options: ToolsImageOptions): Record<string, unknown> {
|
||||
const registry = toolsImageStatus(node, target, options.timeoutSeconds);
|
||||
const jobResult = runTransK3s(node.kubeRoute, remoteJobStatusScript(target, "tools-image", options.tailLines), options.timeoutSeconds);
|
||||
@@ -437,7 +574,7 @@ function toolsImageBuild(node: ControlPlaneNodeSpec, target: ControlPlaneTargetS
|
||||
buildNetwork: target.tekton.toolsImage.buildNetwork,
|
||||
publicBaseImages: target.tekton.toolsImage.publicBaseImages,
|
||||
nodeLocalRegistryOutputOnly: true,
|
||||
egressProxy: node.egressProxy,
|
||||
egressProxy: controlPlaneEgressProxySummary(node.egressProxy),
|
||||
stateDir: remoteJobStateDir(target, "tools-image"),
|
||||
};
|
||||
if (dryRun) {
|
||||
@@ -536,7 +673,7 @@ function argoApply(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec, o
|
||||
desired: manifestObjectSummary(desired),
|
||||
desiredSha256: sha256Short(desiredYaml),
|
||||
stateDir: remoteJobStateDir(target, "argo"),
|
||||
egressProxy: node.egressProxy,
|
||||
egressProxy: controlPlaneEgressProxySummary(node.egressProxy),
|
||||
};
|
||||
if (dryRun) {
|
||||
return {
|
||||
@@ -628,6 +765,34 @@ function parseArgoOptions(args: string[]): ArgoOptions {
|
||||
};
|
||||
}
|
||||
|
||||
function parseEgressBenchmarkOptions(args: string[]): EgressBenchmarkOptions {
|
||||
const first = args[0];
|
||||
const action: EgressBenchmarkAction = first === "status" || first === "logs"
|
||||
? first
|
||||
: "benchmark";
|
||||
const effectiveArgs = action === "benchmark" ? args : args.slice(1);
|
||||
if (first === "--help" || first === "-h" || first === "help") {
|
||||
throw new Error("infra egress-benchmark usage: egress-benchmark [status|logs] --node NODE --lane vNN --profile no-mirror [--dry-run|--confirm]");
|
||||
}
|
||||
const profileRaw = optionValue(effectiveArgs, "--profile") ?? "no-mirror";
|
||||
if (profileRaw !== "no-mirror") throw new Error("egress-benchmark --profile currently supports no-mirror");
|
||||
const confirm = effectiveArgs.includes("--confirm");
|
||||
const explicitDryRun = effectiveArgs.includes("--dry-run");
|
||||
if (confirm && explicitDryRun) throw new Error("egress-benchmark accepts only one of --confirm or --dry-run");
|
||||
return {
|
||||
action,
|
||||
node: requiredOption(effectiveArgs, "--node"),
|
||||
lane: requiredOption(effectiveArgs, "--lane"),
|
||||
profile: profileRaw,
|
||||
confirm,
|
||||
dryRun: action === "benchmark" ? explicitDryRun || !confirm : false,
|
||||
samples: positiveIntegerOption(effectiveArgs, "--samples", 5, 20),
|
||||
sampleTimeoutSeconds: positiveIntegerOption(effectiveArgs, "--sample-timeout-seconds", 240, 900),
|
||||
timeoutSeconds: positiveIntegerOption(effectiveArgs, "--timeout-seconds", 60, 60),
|
||||
tailLines: positiveIntegerOption(effectiveArgs, "--tail-lines", 80, 1000),
|
||||
};
|
||||
}
|
||||
|
||||
function readControlPlaneConfig(): ControlPlaneConfig {
|
||||
const parsed = asRecord(Bun.YAML.parse(readFileSync(rootPath(HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH), "utf8")) as unknown, HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH);
|
||||
const version = numberField(parsed, "version", HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH);
|
||||
@@ -846,21 +1011,45 @@ function execStartPreField(raw: unknown, path: string): readonly (readonly strin
|
||||
function egressProxySpec(raw: Record<string, unknown>, path: string): ControlPlaneEgressProxySpec {
|
||||
const mode = stringField(raw, "mode", path);
|
||||
if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be k8s-service-cluster-ip`);
|
||||
const sourceType = stringField(raw, "sourceType", path);
|
||||
if (sourceType !== "subscription-url") throw new Error(`${path}.sourceType must be subscription-url`);
|
||||
const sourceConfigRef = optionalStringField(raw, "sourceConfigRef", path) ?? null;
|
||||
const source = sourceConfigRef === null ? null : resolveEgressProxySourceRef(sourceConfigRef, `${HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH}.${path}.sourceConfigRef`);
|
||||
const sourceType = source?.sourceType ?? stringField(raw, "sourceType", path);
|
||||
if (sourceType !== "subscription-url" && sourceType !== "master-shadowsocks") throw new Error(`${path}.sourceType must be subscription-url or master-shadowsocks`);
|
||||
if (raw.sourceType !== undefined && raw.sourceType !== sourceType) throw new Error(`${path}.sourceType must match sourceConfigRef value ${sourceType}`);
|
||||
const preferredOutbound = source?.preferredOutbound ?? (raw.preferredOutbound === undefined ? null : preferredOutboundField(raw, "preferredOutbound", path));
|
||||
if (source !== null && source.preferredOutbound !== null && raw.preferredOutbound !== undefined && raw.preferredOutbound !== source.preferredOutbound) {
|
||||
throw new Error(`${path}.preferredOutbound must match sourceConfigRef value ${source.preferredOutbound}`);
|
||||
}
|
||||
if (source !== null) {
|
||||
if (raw.sourceRef !== undefined && raw.sourceRef !== source.sourceRef) throw new Error(`${path}.sourceRef must match sourceConfigRef value ${source.sourceRef}`);
|
||||
if (raw.sourceKey !== undefined && raw.sourceKey !== source.sourceKey) throw new Error(`${path}.sourceKey must match sourceConfigRef value ${source.sourceKey}`);
|
||||
}
|
||||
const sourceRef = source?.sourceRef ?? stringField(raw, "sourceRef", path);
|
||||
const sourceKey = source?.sourceKey ?? stringField(raw, "sourceKey", path);
|
||||
validateSourceRef(sourceRef, `${path}.sourceRef`);
|
||||
validateEnvKey(sourceKey, `${path}.sourceKey`);
|
||||
return {
|
||||
mode,
|
||||
clientName: stringField(raw, "clientName", path),
|
||||
namespace: stringField(raw, "namespace", path),
|
||||
serviceName: stringField(raw, "serviceName", path),
|
||||
port: positiveConfigIntegerField(raw, "port", path),
|
||||
sourceRef: stringField(raw, "sourceRef", path),
|
||||
sourceKey: stringField(raw, "sourceKey", path),
|
||||
sourceConfigRef,
|
||||
sourceFingerprint: source?.fingerprint ?? null,
|
||||
sourceRef,
|
||||
sourceKey,
|
||||
sourceType,
|
||||
preferredOutbound,
|
||||
noProxy: stringArrayField(raw, "noProxy", path),
|
||||
};
|
||||
}
|
||||
|
||||
function preferredOutboundField(raw: Record<string, unknown>, key: string, path: string): "vless-reality" | "hysteria2" {
|
||||
const value = stringField(raw, key, path);
|
||||
if (value !== "vless-reality" && value !== "hysteria2") throw new Error(`${path}.${key} must be vless-reality or hysteria2`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function gitMirrorEgressProxySpec(raw: Record<string, unknown>, path: string): ControlPlaneGitMirrorEgressProxySpec {
|
||||
const mode = stringField(raw, "mode", path);
|
||||
if (mode !== "node-global" && mode !== "direct") throw new Error(`${path}.mode must be node-global or direct`);
|
||||
@@ -1297,11 +1486,14 @@ function gitMirrorProxyPrelude(node: ControlPlaneNodeSpec, target: ControlPlaneT
|
||||
const proxyPort = useDirect || node.egressProxy === null ? 0 : node.egressProxy.port;
|
||||
const proxyUrl = useDirect ? "" : `http://${proxyHost}:${proxyPort}`;
|
||||
const noProxy = useDirect || node.egressProxy === null ? "" : node.egressProxy.noProxy.join(",");
|
||||
const proxySource = useDirect || node.egressProxy === null
|
||||
? "sourceType=direct sourceRef=- sourceFingerprint=-"
|
||||
: `sourceType=${node.egressProxy.sourceType} sourceRef=${node.egressProxy.sourceRef} sourceFingerprint=${node.egressProxy.sourceFingerprint ?? "-"}`;
|
||||
const proxySummary = useDirect
|
||||
? `git-mirror-egress-proxy mode=direct required=false transport=${transport.mode} source=yaml`
|
||||
: transport.mode === "https"
|
||||
? `git-mirror-egress-proxy client=${node.egressProxy?.clientName} mode=node-global required=${proxyRequired ? "true" : "false"} host=${proxyHost} port=${proxyPort} transport=https proxy=HTTP_PROXY authSecret=${transport.tokenSecretName} authKey=${transport.tokenSecretKey} authSourceRef=${transport.tokenSourceRef} source=yaml`
|
||||
: `git-mirror-egress-proxy client=${node.egressProxy?.clientName} mode=node-global required=${proxyRequired ? "true" : "false"} host=${proxyHost} port=${proxyPort} transport=ssh ssh=GIT_SSH-wrapper source=yaml`;
|
||||
? `git-mirror-egress-proxy client=${node.egressProxy?.clientName} mode=node-global required=${proxyRequired ? "true" : "false"} host=${proxyHost} port=${proxyPort} transport=https proxy=HTTP_PROXY authSecret=${transport.tokenSecretName} authKey=${transport.tokenSecretKey} authSourceRef=${transport.tokenSourceRef} ${proxySource} source=yaml`
|
||||
: `git-mirror-egress-proxy client=${node.egressProxy?.clientName} mode=node-global required=${proxyRequired ? "true" : "false"} host=${proxyHost} port=${proxyPort} transport=ssh ssh=GIT_SSH-wrapper ${proxySource} source=yaml`;
|
||||
const proxyCommand = useDirect ? "" : `ProxyCommand=node /tmp/hwlab-github-proxy-connect.cjs ${proxyHost} ${proxyPort} %h %p`;
|
||||
const common = [
|
||||
`printf '%s\\n' ${shQuote(proxySummary)} >&2`,
|
||||
@@ -1574,7 +1766,7 @@ function planSummary(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec)
|
||||
runtimeNamespace: target.runtimeNamespace,
|
||||
k3sNodeConfig: k3sNodeConfigPlan(node),
|
||||
registry: node.registry.endpoint,
|
||||
egressProxy: node.egressProxy,
|
||||
egressProxy: controlPlaneEgressProxySummary(node.egressProxy),
|
||||
sourceBranch: target.source.branch,
|
||||
gitopsBranch: target.gitops.branch,
|
||||
gitopsPath: target.gitops.path,
|
||||
@@ -1615,6 +1807,7 @@ function expectedSummary(node: ControlPlaneNodeSpec, target: ControlPlaneTargetS
|
||||
deploymentReplicas: target.gitMirror.deploymentReplicas,
|
||||
syncConfigMap: target.gitMirror.syncConfigMapName,
|
||||
egressProxy: target.gitMirror.egressProxy,
|
||||
effectiveEgressProxy: gitMirrorEffectiveEgressProxySummary(node, target),
|
||||
githubTransport: gitMirrorGithubTransportSummary(target.gitMirror.githubTransport),
|
||||
statusSummaryKeys: ["localSource", "githubSource", "localGitops", "githubGitops", "pendingFlush", "flushNeeded", "githubInSync"],
|
||||
},
|
||||
@@ -1670,6 +1863,45 @@ function k3sDropInContent(spec: ControlPlaneK3sNodeSpec): string {
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function controlPlaneEgressProxySummary(proxy: ControlPlaneEgressProxySpec | null): Record<string, unknown> | null {
|
||||
if (proxy === null) return null;
|
||||
return {
|
||||
mode: proxy.mode,
|
||||
clientName: proxy.clientName,
|
||||
namespace: proxy.namespace,
|
||||
serviceName: proxy.serviceName,
|
||||
port: proxy.port,
|
||||
sourceConfigRef: proxy.sourceConfigRef,
|
||||
sourceType: proxy.sourceType,
|
||||
sourceRef: proxy.sourceRef,
|
||||
sourceKey: proxy.sourceKey,
|
||||
sourceFingerprint: proxy.sourceFingerprint,
|
||||
preferredOutbound: proxy.preferredOutbound,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function gitMirrorEffectiveEgressProxySummary(node: ControlPlaneNodeSpec, target: ControlPlaneTargetSpec): Record<string, unknown> {
|
||||
const config = target.gitMirror.egressProxy;
|
||||
if (config === null || config.mode === "direct") {
|
||||
return {
|
||||
mode: "direct",
|
||||
required: false,
|
||||
transport: target.gitMirror.githubTransport.mode,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const proxy = node.egressProxy;
|
||||
return {
|
||||
mode: config.mode,
|
||||
required: config.required,
|
||||
transport: target.gitMirror.githubTransport.mode,
|
||||
ready: proxy !== null,
|
||||
nodeProxy: controlPlaneEgressProxySummary(proxy),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function gitMirrorGithubTransportSummary(transport: ControlPlaneGitMirrorGithubTransportSpec): Record<string, unknown> {
|
||||
if (transport.mode === "ssh") return { mode: "ssh", valuesPrinted: false };
|
||||
return {
|
||||
@@ -1694,6 +1926,7 @@ function statusScript(nodeSpec: ControlPlaneNodeSpec, target: ControlPlaneTarget
|
||||
const argoStatefulSets = shellJsonArray(target.argo.install.expectedStatefulSets);
|
||||
const k3s = nodeSpec.k3s;
|
||||
const k3sDropIn = k3s === null ? "" : k3sDropInContent(k3s);
|
||||
const gitMirrorEgressProxyJson = JSON.stringify(gitMirrorEffectiveEgressProxySummary(nodeSpec, target));
|
||||
return `
|
||||
set +e
|
||||
node=${shQuote(target.node)}
|
||||
@@ -1712,6 +1945,7 @@ github_token_secret=${shQuote(target.gitMirror.githubTransport.mode === "https"
|
||||
github_token_key=${shQuote(target.gitMirror.githubTransport.mode === "https" ? target.gitMirror.githubTransport.tokenSecretKey : "")}
|
||||
github_token_source_ref=${shQuote(target.gitMirror.githubTransport.mode === "https" ? target.gitMirror.githubTransport.tokenSourceRef : "")}
|
||||
github_token_source_key=${shQuote(target.gitMirror.githubTransport.mode === "https" ? target.gitMirror.githubTransport.tokenSourceKey : "")}
|
||||
gitmirror_egress_proxy_json=${shQuote(gitMirrorEgressProxyJson)}
|
||||
pipeline=${shQuote(target.tekton.pipelineName)}
|
||||
service_account=${shQuote(target.tekton.serviceAccountName)}
|
||||
argo_ns=${shQuote(target.argo.namespace)}
|
||||
@@ -1845,7 +2079,7 @@ print(json.dumps({"crds": crds, "deployments": deploy, "statefulSets": sts, "crd
|
||||
PY
|
||||
argo_fragment=$(cat /tmp/hwlab-node-status-fragments.json 2>/dev/null || printf '{}')
|
||||
cat <<JSON
|
||||
{"observedAt":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","node":"$node","lane":"$lane","components":{"k3sNodeConfig":$k3s_fragment,"tekton":{"installed":$(kubectl get crd pipelines.tekton.dev pipelineruns.tekton.dev >/dev/null 2>&1 && printf true || printf false),"controllerReady":$(deploy_ready tekton-pipelines tekton-pipelines-controller),"webhookReady":$(deploy_ready tekton-pipelines tekton-pipelines-webhook)},"ciNamespace":{"name":"$ci_ns","exists":$(exists_ns "$ci_ns"),"serviceAccountExists":$(exists_res "$ci_ns" serviceaccount "$service_account"),"pipelineExists":$(exists_res "$ci_ns" pipeline "$pipeline")},"gitMirror":{"namespace":"$gitmirror_ns","namespaceExists":$(exists_ns "$gitmirror_ns"),"readDeploymentReady":$(deploy_ready "$gitmirror_ns" "$read_deploy"),"writeDeploymentReady":$(deploy_ready "$gitmirror_ns" "$write_deploy"),"readServiceExists":$(exists_res "$gitmirror_ns" service "$read_svc"),"writeServiceExists":$(exists_res "$gitmirror_ns" service "$write_svc"),"readEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$read_svc"),"writeEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$write_svc"),"cachePvcExists":$(exists_res "$gitmirror_ns" pvc "$cache_pvc"),"cacheHostPath":"$cache_host_path","cacheHostPathReady":$cache_host_path_ready,"githubTransport":$github_transport_json,"summary":{"localSource":null,"githubSource":null,"localGitops":null,"githubGitops":null,"pendingFlush":null,"flushNeeded":null,"githubInSync":null}},"argo":{"namespace":"$argo_ns","namespaceExists":$(exists_ns "$argo_ns"),"installed":$(kubectl get crd applications.argoproj.io appprojects.argoproj.io >/dev/null 2>&1 && printf true || printf false),"projectExists":$(kubectl -n "$argo_ns" get appproject "$argo_project" >/dev/null 2>&1 && printf true || printf false),"applicationExists":$(kubectl -n "$argo_ns" get application "$argo_app" >/dev/null 2>&1 && printf true || printf false),"install":$argo_fragment},"registry":{"endpoint":"$registry","ready":$registry_ready,"toolsImage":"$tools_image","toolsImageReady":$tools_image_ready},"runtimeNamespace":{"name":"$runtime_ns","exists":$(exists_ns "$runtime_ns")}}}
|
||||
{"observedAt":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","node":"$node","lane":"$lane","components":{"k3sNodeConfig":$k3s_fragment,"tekton":{"installed":$(kubectl get crd pipelines.tekton.dev pipelineruns.tekton.dev >/dev/null 2>&1 && printf true || printf false),"controllerReady":$(deploy_ready tekton-pipelines tekton-pipelines-controller),"webhookReady":$(deploy_ready tekton-pipelines tekton-pipelines-webhook)},"ciNamespace":{"name":"$ci_ns","exists":$(exists_ns "$ci_ns"),"serviceAccountExists":$(exists_res "$ci_ns" serviceaccount "$service_account"),"pipelineExists":$(exists_res "$ci_ns" pipeline "$pipeline")},"gitMirror":{"namespace":"$gitmirror_ns","namespaceExists":$(exists_ns "$gitmirror_ns"),"readDeploymentReady":$(deploy_ready "$gitmirror_ns" "$read_deploy"),"writeDeploymentReady":$(deploy_ready "$gitmirror_ns" "$write_deploy"),"readServiceExists":$(exists_res "$gitmirror_ns" service "$read_svc"),"writeServiceExists":$(exists_res "$gitmirror_ns" service "$write_svc"),"readEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$read_svc"),"writeEndpointsReady":$(endpoint_ready "$gitmirror_ns" "$write_svc"),"cachePvcExists":$(exists_res "$gitmirror_ns" pvc "$cache_pvc"),"cacheHostPath":"$cache_host_path","cacheHostPathReady":$cache_host_path_ready,"egressProxy":$gitmirror_egress_proxy_json,"githubTransport":$github_transport_json,"summary":{"localSource":null,"githubSource":null,"localGitops":null,"githubGitops":null,"pendingFlush":null,"flushNeeded":null,"githubInSync":null}},"argo":{"namespace":"$argo_ns","namespaceExists":$(exists_ns "$argo_ns"),"installed":$(kubectl get crd applications.argoproj.io appprojects.argoproj.io >/dev/null 2>&1 && printf true || printf false),"projectExists":$(kubectl -n "$argo_ns" get appproject "$argo_project" >/dev/null 2>&1 && printf true || printf false),"applicationExists":$(kubectl -n "$argo_ns" get application "$argo_app" >/dev/null 2>&1 && printf true || printf false),"install":$argo_fragment},"registry":{"endpoint":"$registry","ready":$registry_ready,"toolsImage":"$tools_image","toolsImageReady":$tools_image_ready},"runtimeNamespace":{"name":"$runtime_ns","exists":$(exists_ns "$runtime_ns")}}}
|
||||
JSON
|
||||
`;
|
||||
}
|
||||
@@ -2509,6 +2743,23 @@ function record(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function renderTable(headers: string[], rows: string[][]): string[] {
|
||||
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index]?.length ?? 0)));
|
||||
const render = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd();
|
||||
return [render(headers), ...rows.map(render)];
|
||||
}
|
||||
|
||||
function renderCell(value: unknown, fallback = "-"): string {
|
||||
if (value === undefined || value === null || value === "") return fallback;
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function optionsModeFromCommand(command: unknown): string {
|
||||
const value = String(command ?? "");
|
||||
if (value.endsWith(" status") || value.endsWith(" logs")) return "status";
|
||||
return "benchmark";
|
||||
}
|
||||
|
||||
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = obj[key];
|
||||
if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
|
||||
@@ -2600,10 +2851,17 @@ function requiredOption(args: string[], name: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
|
||||
function optionValue(args: string[], name: string): string | null {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return defaultValue;
|
||||
const raw = args[index + 1];
|
||||
if (index === -1) return null;
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--") || value.length === 0) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
|
||||
const raw = optionValue(args, name);
|
||||
if (raw === null) return defaultValue;
|
||||
const value = Number(raw);
|
||||
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer`);
|
||||
return Math.min(value, maxValue);
|
||||
|
||||
Reference in New Issue
Block a user