From 9608ac4d066f667b9daeac9c3b254e153c46ca90 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 26 Jun 2026 13:10:02 +0000 Subject: [PATCH] feat: add k3s build proxy benchmark --- .../egress-proxy-benchmarks.yaml | 36 + ...-01060309-cross-node-proxy-ci-benchmark.md | 49 ++ scripts/src/help.ts | 1 + scripts/src/platform-infra-egress-proxy.ts | 4 +- .../src/platform-infra-k3s-build-benchmark.ts | 769 ++++++++++++++++++ scripts/src/platform-infra/entry.ts | 1 + 6 files changed, 859 insertions(+), 1 deletion(-) create mode 100644 config/platform-infra/egress-proxy-benchmarks.yaml create mode 100644 project-management/PJ2026-01/specs/PJ2026-01060309-cross-node-proxy-ci-benchmark.md create mode 100644 scripts/src/platform-infra-k3s-build-benchmark.ts diff --git a/config/platform-infra/egress-proxy-benchmarks.yaml b/config/platform-infra/egress-proxy-benchmarks.yaml new file mode 100644 index 00000000..c6add5ee --- /dev/null +++ b/config/platform-infra/egress-proxy-benchmarks.yaml @@ -0,0 +1,36 @@ +version: 1 +kind: platform-infra-egress-proxy-benchmarks + +metadata: + owner: unidesk + relatedIssues: + - 1032 + +profiles: + no-mirror-600m: + enabled: true + workload: k3s-build + description: Generic k3s build benchmark with no registry/source mirror and at least 600 MiB output payload. + image: docker.io/library/debian:bookworm + imagePullPolicy: Always + payloadMiB: 600 + timeoutSeconds: 3600 + ttlSecondsAfterFinished: 3600 + noMirror: + apt: system-default + npmRegistry: https://registry.npmjs.org/ + pipIndexUrl: https://pypi.org/simple + registryMirror: forbidden + aptPackages: + - build-essential + - ca-certificates + - curl + - git + - make + - pkg-config + - xz-utils + dependencyDownload: + enabled: true + url: https://speed.cloudflare.com/__down?bytes=67108864 + chunks: 8 + expectedMiB: 512 diff --git a/project-management/PJ2026-01/specs/PJ2026-01060309-cross-node-proxy-ci-benchmark.md b/project-management/PJ2026-01/specs/PJ2026-01060309-cross-node-proxy-ci-benchmark.md new file mode 100644 index 00000000..76ea92ac --- /dev/null +++ b/project-management/PJ2026-01/specs/PJ2026-01060309-cross-node-proxy-ci-benchmark.md @@ -0,0 +1,49 @@ +# PJ2026-01060309 Cross Node Proxy K3s Build Benchmark + +## Scope + +This SPEC replaces the earlier HWLAB full-CI dependency for pikasTech/unidesk#1032. The benchmark target is now a generic k3s build workload that can run on any platform-infra target with an enabled egress proxy. It must not require HWLAB runtime namespaces, Tekton Pipelines, Argo CD, git-mirror, or HWLAB source repositories. + +The first profile is `no-mirror-600m` in `config/platform-infra/egress-proxy-benchmarks.yaml`. It runs a fresh k3s Job per node, uses an emptyDir workspace, forbids benchmark reuse, keeps registry/source mirror settings out of the workload, and produces at least 600 MiB of build output. The profile also downloads an explicit no-mirror dependency payload so proxyserver traffic can be observed during the run. + +## Architecture + +`platform-infra egress-proxy k3s-build-benchmark` is the coordinator. It reads platform-infra targets from `config/platform-infra/sub2api.yaml`, reads workload profiles from `config/platform-infra/egress-proxy-benchmarks.yaml`, renders one Kubernetes Job per target, and uses `trans sh -- ...` as the short control path. + +The workload Job runs in the target platform-infra namespace and receives proxy environment variables that point at the YAML-declared `sub2api-egress-proxy` service. The existing `platform-infra egress-proxy traffic` sampler remains the proxyserver-side observability source. Benchmark status may include a bounded traffic sample, but raw proxy credentials and Secret values are never printed. + +## Data Model + +Stable benchmark records contain: + +- `target`: platform-infra target id such as `D601` or `D518`. +- `profile`: benchmark profile id, initially `no-mirror-600m`. +- `runId` and `jobName`: k3s Job identity. +- `image`, `payloadMiB`, `dependencyDownload.expectedMiB`, `noMirror`: profile facts from YAML. +- `state`: `pending`, `running`, `succeeded`, `failed`, or `missing`. +- `startedAt`, `completedAt`, `durationSeconds`: Job timing. +- `outputMiB`, `downloadMiB`: workload evidence parsed from Job logs. +- `proxy`: service, port, source fingerprint, and values-redacted marker. +- `traffic`: optional proxyserver sample with window bytes, rate, cumulative bytes, top client, and top destination. +- `failureFamily`: `none`, `job-missing`, `image-pull`, `apt-download`, `dependency-download`, `payload-too-small`, `timeout`, `proxy-unavailable`, or `unknown`. + +## CLI Sequence + +1. `--dry-run` resolves all requested targets and profiles, then prints a bounded table of target, route, namespace, proxy service, payload size, expected dependency download, and no-mirror policy. +2. `--confirm` creates one unique k3s Job per target and returns immediately with run id, Job name, status command, logs command, and traffic command. +3. `status` reads the newest matching Job per target, parses the bounded Job log result marker, and prints a cross-node table. +4. `status --traffic-sample-seconds N` additionally samples the target proxyserver loopback Clash API for N seconds per node and includes rate/window/cumulative traffic columns. +5. `logs` prints bounded Job log tails for drill-down. + +## Boundaries + +This benchmark is not a HWLAB application CI result and must not be reported as PipelineRun timing. It measures a generic k3s build workload under the node-local platform-infra egress proxy. It is valid for cross-node proxy path and build egress performance comparison because every target runs the same YAML profile and the same Job template. + +The workload must stay no-mirror: do not rewrite apt sources to regional mirrors, do not use npm mirror registries, do not configure Docker registry mirrors, and do not reuse a previous Job, PVC, or dependency cache. The configured profile must keep output payload at or above 500 MiB. + +## Acceptance + +- `bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark --targets D601,D518 --profile no-mirror-600m --dry-run` prints two target plans or structured blockers. +- `--confirm` starts D601 and D518 Jobs without waiting for completion. +- `status --traffic-sample-seconds 15` prints cross-node build status plus proxyserver traffic evidence. +- Final issue closeout includes a table with target, profile, job, state, duration, output MiB, dependency download MiB, traffic window/rate/cumulative, top client, top destination, and failure family. diff --git a/scripts/src/help.ts b/scripts/src/help.ts index e0d81a7e..9883baf1 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -657,6 +657,7 @@ function platformInfraHelpSummary(): unknown { "bun scripts/cli.ts platform-infra egress-proxy benchmark --target D601 --profile no-mirror --confirm", "bun scripts/cli.ts platform-infra egress-proxy benchmark-status --target D601 --profile no-mirror", "bun scripts/cli.ts platform-infra egress-proxy traffic --target D601 --sample-seconds 15", + "bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark --targets D601,D518 --profile no-mirror-600m --dry-run", "bun scripts/cli.ts platform-infra langbot plan", "bun scripts/cli.ts platform-infra langbot apply --confirm", "bun scripts/cli.ts platform-infra langbot status", diff --git a/scripts/src/platform-infra-egress-proxy.ts b/scripts/src/platform-infra-egress-proxy.ts index fb2b7325..fd2a47a9 100644 --- a/scripts/src/platform-infra-egress-proxy.ts +++ b/scripts/src/platform-infra-egress-proxy.ts @@ -6,6 +6,7 @@ import { egressBenchmarkCompactResult, egressBenchmarkDryRun, egressBenchmarkSta import { egressProxyTrafficCompactResult, egressProxyTrafficScript, type EgressProxyTrafficSpec } from "./egress-proxy-traffic"; import { readSub2ApiConfig } from "./platform-infra/config"; import { resolveTarget } from "./platform-infra/manifest"; +import { runK3sBuildBenchmarkCommand } from "./platform-infra-k3s-build-benchmark"; type BenchmarkAction = "benchmark" | "benchmark-status" | "benchmark-logs"; type EgressProxyAction = BenchmarkAction | "traffic"; @@ -33,6 +34,7 @@ interface TrafficOptions { type EgressProxyOptions = BenchmarkOptions | TrafficOptions; export async function runPlatformInfraEgressProxyCommand(_config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { + if (args[0] === "k3s-build-benchmark") return runK3sBuildBenchmarkCommand(args.slice(1)); const options = parseEgressProxyOptions(args); if (options.action === "traffic") { const spec = platformTrafficSpec(options); @@ -148,7 +150,7 @@ function platformTrafficSpec(options: TrafficOptions): EgressProxyTrafficSpec { function parseEgressProxyOptions(args: string[]): EgressProxyOptions { const actionRaw = args[0] ?? "benchmark"; if (!isEgressProxyAction(actionRaw)) { - throw new Error("platform-infra egress-proxy usage: benchmark|benchmark-status|benchmark-logs|traffic --target D601|D518 [--profile no-mirror] [--sample-seconds N]"); + throw new Error("platform-infra egress-proxy usage: benchmark|benchmark-status|benchmark-logs|traffic|k3s-build-benchmark --target D601|D518 [--profile no-mirror|no-mirror-600m] [--sample-seconds N]"); } const action = actionRaw; const rest = args.slice(1); diff --git a/scripts/src/platform-infra-k3s-build-benchmark.ts b/scripts/src/platform-infra-k3s-build-benchmark.ts new file mode 100644 index 00000000..1e625d67 --- /dev/null +++ b/scripts/src/platform-infra-k3s-build-benchmark.ts @@ -0,0 +1,769 @@ +// SPEC: PJ2026-01060309 cross-node-proxy-ci-benchmark draft-2026-06-26-k3s-build-benchmark. +// Responsibility: Generic platform-infra k3s build benchmark coordinator with proxyserver traffic evidence. +import { readFileSync } from "node:fs"; +import { rootPath } from "./config"; +import { runCommand, type CommandResult } from "./command"; +import type { RenderedCliResult } from "./output"; +import { egressProxyTrafficScript, type EgressProxyTrafficSpec } from "./egress-proxy-traffic"; +import { readSub2ApiConfig } from "./platform-infra/config"; +import type { Sub2ApiTargetConfig } from "./platform-infra/entry"; +import { resolveTarget } from "./platform-infra/manifest"; + +const BENCHMARK_CONFIG_PATH = "config/platform-infra/egress-proxy-benchmarks.yaml"; +const BENCHMARK_APP = "unidesk-k3s-build-benchmark"; + +type K3sBuildAction = "start" | "status" | "logs"; + +interface K3sBuildBenchmarkOptions { + action: K3sBuildAction; + targets: readonly string[]; + profile: string; + confirm: boolean; + dryRun: boolean; + timeoutSeconds: number; + tailLines: number; + trafficSampleSeconds: number; +} + +interface K3sBuildBenchmarkProfile { + id: string; + enabled: boolean; + workload: "k3s-build"; + description: string; + image: string; + imagePullPolicy: "Always" | "IfNotPresent" | "Never"; + payloadMiB: number; + timeoutSeconds: number; + ttlSecondsAfterFinished: number; + noMirror: { + apt: string; + npmRegistry: string; + pipIndexUrl: string; + registryMirror: "forbidden"; + }; + aptPackages: readonly string[]; + dependencyDownload: { + enabled: boolean; + url: string; + chunks: number; + expectedMiB: number; + }; +} + +interface K3sBuildBenchmarkConfig { + version: number; + kind: string; + metadata: { owner: string; relatedIssues: readonly number[] }; + profiles: Record; +} + +interface TargetPlan { + ok: boolean; + targetId: string; + target?: Sub2ApiTargetConfig; + profile?: K3sBuildBenchmarkProfile; + blocker?: string; + detail?: string; +} + +interface TargetStatus { + ok: boolean; + targetId: string; + profile: string; + state: string; + jobName: string; + runId: string; + startedAt: string; + completedAt: string; + durationSeconds: number | null; + outputMiB: number | null; + downloadMiB: number | null; + payloadMiB: number | null; + failureFamily: string; + logTail: string; + traffic?: TrafficSummary; +} + +interface TrafficSummary { + ok: boolean; + reason: string; + windowBytes: number; + rateBps: number; + processTotalBytes: number; + topClient: string; + topDestination: string; +} + +export function runK3sBuildBenchmarkCommand(args: string[]): RenderedCliResult { + const options = parseK3sBuildBenchmarkOptions(args); + const config = readK3sBuildBenchmarkConfig(); + const plans = resolvePlans(config, options); + if (options.action === "start" && options.dryRun) return renderDryRun(plans, options); + if (options.action === "start") return startBenchmarks(plans, options); + return statusBenchmarks(plans, options); +} + +function parseK3sBuildBenchmarkOptions(args: string[]): K3sBuildBenchmarkOptions { + const first = args[0]; + const action: K3sBuildAction = first === "status" || first === "logs" ? first : "start"; + const rest = action === "start" ? args : args.slice(1); + if (first === "--help" || first === "-h" || first === "help") { + throw new Error("platform-infra egress-proxy k3s-build-benchmark usage: k3s-build-benchmark [status|logs] --targets D601,D518 --profile no-mirror-600m [--dry-run|--confirm]"); + } + const confirm = rest.includes("--confirm"); + const explicitDryRun = rest.includes("--dry-run"); + if (confirm && explicitDryRun) throw new Error("k3s-build-benchmark accepts only one of --confirm or --dry-run"); + return { + action, + targets: parseTargetsOption(rest), + profile: option(rest, "--profile") ?? "no-mirror-600m", + confirm, + dryRun: action === "start" ? explicitDryRun || !confirm : false, + timeoutSeconds: positiveIntOption(rest, "--timeout-seconds", 60, 60), + tailLines: positiveIntOption(rest, "--tail-lines", action === "logs" ? 160 : 40, 400), + trafficSampleSeconds: positiveIntOption(rest, "--traffic-sample-seconds", 0, 45), + }; +} + +function parseTargetsOption(args: string[]): readonly string[] { + const raw = option(args, "--targets") ?? option(args, "--target") ?? "D601"; + if (raw === "all") { + const sub2api = readSub2ApiConfig(); + return sub2api.targets + .filter((target) => target.enabled && target.egressProxy?.enabled === true) + .map((target) => target.id); + } + const targets = raw.split(",").map((item) => item.trim()).filter(Boolean); + if (targets.length === 0) throw new Error("--targets must contain at least one target id"); + return [...new Set(targets)]; +} + +function resolvePlans(config: K3sBuildBenchmarkConfig, options: K3sBuildBenchmarkOptions): readonly TargetPlan[] { + const sub2api = readSub2ApiConfig(); + const profile = config.profiles[options.profile]; + return options.targets.map((targetId): TargetPlan => { + if (profile === undefined) return { ok: false, targetId, blocker: "profile-missing", detail: `${BENCHMARK_CONFIG_PATH}.profiles.${options.profile} is missing` }; + if (!profile.enabled) return { ok: false, targetId, profile, blocker: "profile-disabled", detail: `${BENCHMARK_CONFIG_PATH}.profiles.${profile.id}.enabled=false` }; + if (profile.payloadMiB < 500) return { ok: false, targetId, profile, blocker: "payload-too-small", detail: `${BENCHMARK_CONFIG_PATH}.profiles.${profile.id}.payloadMiB must be >= 500` }; + try { + const target = resolveTarget(sub2api, targetId); + if (target.egressProxy === null || !target.egressProxy.enabled) return { ok: false, targetId: target.id, target, profile, blocker: "egress-proxy-disabled", detail: `config/platform-infra/sub2api.yaml target ${target.id} has no enabled egressProxy` }; + return { ok: true, targetId: target.id, target, profile }; + } catch (error) { + return { ok: false, targetId, profile, blocker: "target-missing", detail: error instanceof Error ? error.message : String(error) }; + } + }); +} + +function startBenchmarks(plans: readonly TargetPlan[], options: K3sBuildBenchmarkOptions): RenderedCliResult { + const rows = plans.map((plan) => { + if (!plan.ok || plan.target === undefined || plan.profile === undefined) { + return { ...plan, started: false, state: "blocked", jobName: "-", runId: "-", result: null }; + } + const runId = `k3sbuild-${Date.now().toString(36)}-${plan.target.id.toLowerCase()}`; + const jobName = benchmarkJobName(plan.target, plan.profile, runId); + const manifest = benchmarkJobManifest(plan.target, plan.profile, runId, jobName); + const result = runTrans(plan.target.route, startScript(manifest, plan.target, plan.profile, runId, jobName), options.timeoutSeconds); + const parsed = parseJson(result.stdout); + return { + ...plan, + started: result.exitCode === 0, + state: result.exitCode === 0 ? "started" : "failed", + jobName, + runId, + result: typeof parsed === "object" && parsed !== null ? parsed : { stdoutPreview: result.stdout.slice(0, 1200), stderrPreview: result.stderr.slice(-1200), exitCode: result.exitCode }, + }; + }); + const ok = rows.every((row) => row.ok && row.started); + const tableRows = rows.map((row) => [ + row.targetId, + row.profile?.id ?? options.profile, + row.state, + row.jobName, + row.runId, + row.ok ? `status/logs/traffic` : `${row.blocker}: ${row.detail}`, + ]); + return { + ok, + command: "platform-infra egress-proxy k3s-build-benchmark", + contentType: "text/plain", + renderedText: [ + "PLATFORM-INFRA K3S BUILD BENCHMARK START", + "", + ...table(["TARGET", "PROFILE", "STATE", "JOB", "RUN", "NEXT"], tableRows), + "", + "NEXT", + ` bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark status --targets ${rows.map((row) => row.targetId).join(",")} --profile ${options.profile}`, + ` bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark status --targets ${rows.map((row) => row.targetId).join(",")} --profile ${options.profile} --traffic-sample-seconds 15`, + ` bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark logs --targets ${rows.map((row) => row.targetId).join(",")} --profile ${options.profile}`, + "", + "Disclosure: start is fire-and-forget; the k3s Job uses emptyDir and a unique Job name, so benchmark work is not reused.", + ].join("\n"), + }; +} + +function statusBenchmarks(plans: readonly TargetPlan[], options: K3sBuildBenchmarkOptions): RenderedCliResult { + const statuses = plans.map((plan): TargetStatus => { + if (!plan.ok || plan.target === undefined || plan.profile === undefined) { + return blockedStatus(plan, options.profile); + } + const result = runTrans(plan.target.route, statusScript(plan.target, plan.profile, options.tailLines), options.timeoutSeconds); + const parsed = parseJson(result.stdout); + const status = normalizeStatus(plan, parsed, result); + if (options.trafficSampleSeconds > 0) status.traffic = sampleTraffic(plan.target, options.trafficSampleSeconds, options.timeoutSeconds); + return status; + }); + const ok = statuses.every((status) => status.ok || options.action === "logs"); + const rows = statuses.map((status) => [ + status.targetId, + status.profile, + status.state, + status.jobName, + status.durationSeconds === null ? "-" : `${status.durationSeconds}s`, + status.outputMiB === null ? "-" : `${status.outputMiB}MiB`, + status.downloadMiB === null ? "-" : `${status.downloadMiB}MiB`, + status.traffic === undefined ? "-" : bytes(status.traffic.windowBytes), + status.traffic === undefined ? "-" : rate(status.traffic.rateBps), + status.traffic === undefined ? "-" : bytes(status.traffic.processTotalBytes), + status.traffic === undefined ? "-" : status.traffic.topClient, + status.traffic === undefined ? "-" : status.traffic.topDestination, + status.failureFamily, + ]); + const logSections = options.action === "logs" + ? statuses.flatMap((status) => ["", `LOG ${status.targetId} ${status.jobName}`, status.logTail || "-"]) + : []; + return { + ok, + command: `platform-infra egress-proxy k3s-build-benchmark ${options.action}`, + contentType: "text/plain", + renderedText: [ + "PLATFORM-INFRA K3S BUILD BENCHMARK STATUS", + "", + ...table(["TARGET", "PROFILE", "STATE", "JOB", "DURATION", "OUTPUT", "DOWNLOAD", "TRAFFIC_WINDOW", "TRAFFIC_RATE", "PROXY_CUM", "TOP_CLIENT", "TOP_DEST", "FAILURE"], rows), + ...logSections, + "", + "NEXT", + ` bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark status --targets ${statuses.map((status) => status.targetId).join(",")} --profile ${options.profile} --traffic-sample-seconds 15`, + ` bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark logs --targets ${statuses.map((status) => status.targetId).join(",")} --profile ${options.profile}`, + "", + "Disclosure: traffic columns are proxyserver-side samples only when --traffic-sample-seconds is set; Secret/proxy values are redacted.", + ].join("\n"), + }; +} + +function renderDryRun(plans: readonly TargetPlan[], options: K3sBuildBenchmarkOptions): RenderedCliResult { + const rows = plans.map((plan) => [ + plan.targetId, + plan.profile?.id ?? options.profile, + plan.ok ? "ok" : `blocked:${plan.blocker}`, + plan.target?.route ?? "-", + plan.target?.namespace ?? "-", + plan.target?.egressProxy?.serviceName ?? "-", + plan.profile === undefined ? "-" : `${plan.profile.payloadMiB}MiB`, + plan.profile === undefined ? "-" : `${plan.profile.dependencyDownload.expectedMiB}MiB`, + plan.detail ?? "no-mirror emptyDir unique-job", + ]); + return { + ok: plans.every((plan) => plan.ok), + command: "platform-infra egress-proxy k3s-build-benchmark", + contentType: "text/plain", + renderedText: [ + "PLATFORM-INFRA K3S BUILD BENCHMARK DRY-RUN", + "", + ...table(["TARGET", "PROFILE", "STATUS", "ROUTE", "NAMESPACE", "PROXY", "PAYLOAD", "DOWNLOAD", "DETAIL"], rows), + "", + "NEXT", + ` bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark --targets ${plans.map((plan) => plan.targetId).join(",")} --profile ${options.profile} --confirm`, + "", + `Config: ${BENCHMARK_CONFIG_PATH}`, + ].join("\n"), + }; +} + +function benchmarkJobManifest(target: Sub2ApiTargetConfig, profile: K3sBuildBenchmarkProfile, runId: string, jobName: string): Record { + const proxy = target.egressProxy; + if (proxy === null) throw new Error(`target ${target.id} has no egressProxy`); + const proxyUrl = `http://${proxy.serviceName}.${target.namespace}.svc.cluster.local:${proxy.listenPort}`; + const noProxy = proxy.noProxy.join(","); + return { + apiVersion: "batch/v1", + kind: "Job", + metadata: { + name: jobName, + namespace: target.namespace, + labels: benchmarkLabels(target, profile, runId), + annotations: { + "unidesk.ai/no-mirror": JSON.stringify(profile.noMirror), + "unidesk.ai/payload-mib": String(profile.payloadMiB), + "unidesk.ai/dependency-download-mib": String(profile.dependencyDownload.expectedMiB), + }, + }, + spec: { + backoffLimit: 0, + activeDeadlineSeconds: profile.timeoutSeconds, + ttlSecondsAfterFinished: profile.ttlSecondsAfterFinished, + template: { + metadata: { labels: benchmarkLabels(target, profile, runId) }, + spec: { + restartPolicy: "Never", + containers: [{ + name: "build", + image: profile.image, + imagePullPolicy: profile.imagePullPolicy, + command: ["/bin/sh", "-lc"], + args: [workloadScript(profile)], + env: [ + { name: "HTTP_PROXY", value: proxyUrl }, + { name: "HTTPS_PROXY", value: proxyUrl }, + { name: "ALL_PROXY", value: proxyUrl }, + { name: "http_proxy", value: proxyUrl }, + { name: "https_proxy", value: proxyUrl }, + { name: "all_proxy", value: proxyUrl }, + { name: "NO_PROXY", value: noProxy }, + { name: "no_proxy", value: noProxy }, + { name: "DEBIAN_FRONTEND", value: "noninteractive" }, + { name: "NPM_CONFIG_REGISTRY", value: profile.noMirror.npmRegistry }, + { name: "PIP_INDEX_URL", value: profile.noMirror.pipIndexUrl }, + { name: "BENCHMARK_TARGET", value: target.id }, + { name: "BENCHMARK_PROFILE", value: profile.id }, + { name: "BENCHMARK_RUN_ID", value: runId }, + { name: "PAYLOAD_MIB", value: String(profile.payloadMiB) }, + { name: "DOWNLOAD_URL", value: profile.dependencyDownload.url }, + { name: "DOWNLOAD_CHUNKS", value: String(profile.dependencyDownload.chunks) }, + { name: "DOWNLOAD_EXPECTED_MIB", value: String(profile.dependencyDownload.expectedMiB) }, + { name: "APT_PACKAGES", value: profile.aptPackages.join(" ") }, + ], + volumeMounts: [{ name: "work", mountPath: "/work" }], + }], + volumes: [{ name: "work", emptyDir: { sizeLimit: "3Gi" } }], + }, + }, + }, + }; +} + +function workloadScript(profile: K3sBuildBenchmarkProfile): string { + return `set -eu +started_epoch="$(date +%s)" +started_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +work=/work/k3s-build-benchmark +download_dir="$work/download" +build_dir="$work/build" +output_dir="$work/output" +mkdir -p "$download_dir" "$build_dir" "$output_dir" +printf 'UNIDESK_K3S_BUILD_BENCHMARK_EVENT target=%s profile=%s run=%s payloadMiB=%s expectedDownloadMiB=%s noMirror=true\\n' "$BENCHMARK_TARGET" "$BENCHMARK_PROFILE" "$BENCHMARK_RUN_ID" "$PAYLOAD_MIB" "$DOWNLOAD_EXPECTED_MIB" +if grep -R -E 'npmmirror|daocloud|aliyun|tuna|ustc' /etc/apt/sources.list /etc/apt/sources.list.d >/tmp/mirror-check.out 2>/dev/null; then + cat /tmp/mirror-check.out >&2 + echo "unexpected apt mirror in base image" >&2 + exit 42 +fi +apt-get -o Acquire::http::No-Cache=true -o Acquire::https::No-Cache=true update +apt-get -o Acquire::http::No-Cache=true -o Acquire::https::No-Cache=true install -y --no-install-recommends $APT_PACKAGES +cat > "$build_dir/bench.c" <<'C' +#include +#include +int main(void) { + uint64_t x = 1469598103934665603ULL; + for (uint64_t i = 0; i < 12000000ULL; ++i) { + x ^= i; + x *= 1099511628211ULL; + } + printf("%llu\\n", (unsigned long long)x); + return 0; +} +C +cc -O2 "$build_dir/bench.c" -o "$build_dir/bench" +"$build_dir/bench" > "$output_dir/compile-result.txt" +if [ "${profile.dependencyDownload.enabled ? "1" : "0"}" = "1" ]; then + i=1 + while [ "$i" -le "$DOWNLOAD_CHUNKS" ]; do + curl -fL --retry 2 --connect-timeout 15 --max-time 240 "$DOWNLOAD_URL" -o "$download_dir/chunk-$i.bin" + i=$((i + 1)) + done +fi +download_mib="$(du -sm "$download_dir" | awk '{print $1}')" +rm -rf "$download_dir" +dd if=/dev/zero of="$output_dir/payload.bin" bs=1M count="$PAYLOAD_MIB" status=none +sha256sum "$output_dir/payload.bin" > "$output_dir/payload.sha256" +output_mib="$(du -sm "$output_dir" | awk '{print $1}')" +if [ "$output_mib" -lt 500 ]; then + echo "payload-too-small outputMiB=$output_mib" >&2 + exit 43 +fi +completed_epoch="$(date +%s)" +completed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)" +duration_seconds=$((completed_epoch - started_epoch)) +printf 'UNIDESK_K3S_BUILD_BENCHMARK_RESULT {"ok":true,"target":"%s","profile":"%s","runId":"%s","startedAt":"%s","completedAt":"%s","durationSeconds":%s,"payloadMiB":%s,"downloadMiB":%s,"downloadExpectedMiB":%s,"outputMiB":%s,"noMirror":true,"aptMirror":"system-default","npmRegistry":"%s","pipIndexUrl":"%s"}\\n' "$BENCHMARK_TARGET" "$BENCHMARK_PROFILE" "$BENCHMARK_RUN_ID" "$started_at" "$completed_at" "$duration_seconds" "$PAYLOAD_MIB" "$download_mib" "$DOWNLOAD_EXPECTED_MIB" "$output_mib" "$NPM_CONFIG_REGISTRY" "$PIP_INDEX_URL" +`; +} + +function benchmarkLabels(target: Sub2ApiTargetConfig, profile: K3sBuildBenchmarkProfile, runId: string): Record { + return { + "app.kubernetes.io/name": BENCHMARK_APP, + "app.kubernetes.io/part-of": "platform-infra", + "app.kubernetes.io/managed-by": "unidesk", + "unidesk.ai/benchmark": "k3s-build", + "unidesk.ai/benchmark-profile": profile.id, + "unidesk.ai/runtime-node": target.id.toLowerCase(), + "unidesk.ai/run-id": runId, + }; +} + +function startScript(manifest: Record, target: Sub2ApiTargetConfig, profile: K3sBuildBenchmarkProfile, runId: string, jobName: string): string { + const yaml = `${Bun.YAML.stringify(manifest).trim()}\n`; + const encoded = Buffer.from(yaml, "utf8").toString("base64"); + return ` +set -eu +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +manifest="$tmp/k3s-build-benchmark.yaml" +printf '%s' '${encoded}' | base64 -d > "$manifest" +kubectl apply -f "$manifest" >/dev/null +printf '{"ok":true,"jobName":"%s","namespace":"%s","target":"%s","runId":"%s","profile":"%s"}\\n' ${shQuote(jobName)} ${shQuote(target.namespace)} ${shQuote(target.id)} ${shQuote(runId)} ${shQuote(profile.id)} +`; +} + +function statusScript(target: Sub2ApiTargetConfig, profile: K3sBuildBenchmarkProfile, tailLines: number): string { + const selector = `app.kubernetes.io/name=${BENCHMARK_APP},unidesk.ai/benchmark-profile=${profile.id},unidesk.ai/runtime-node=${target.id.toLowerCase()}`; + return ` +set -eu +python3 - ${shQuote(target.namespace)} ${shQuote(selector)} ${shQuote(String(tailLines))} <<'PY' +import json, re, subprocess, sys +namespace, selector, tail_lines_raw = sys.argv[1:4] +tail_lines = int(tail_lines_raw) + +def kubectl(args): + return subprocess.run(["kubectl", "-n", namespace, *args], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + +jobs_result = kubectl(["get", "jobs", "-l", selector, "-o", "json"]) +if jobs_result.returncode != 0: + print(json.dumps({"ok": False, "reason": "kubectl-jobs-failed", "stderr": jobs_result.stderr[-2000:]}, ensure_ascii=False)) + sys.exit(0) +jobs = json.loads(jobs_result.stdout or "{}").get("items", []) +if not jobs: + print(json.dumps({"ok": False, "reason": "job-missing", "state": "missing"}, ensure_ascii=False)) + sys.exit(0) +jobs.sort(key=lambda item: item.get("metadata", {}).get("creationTimestamp", "")) +job = jobs[-1] +meta = job.get("metadata", {}) +status = job.get("status", {}) +job_name = meta.get("name") or "-" +labels = meta.get("labels", {}) +pods_result = kubectl(["get", "pods", "-l", "job-name=" + job_name, "-o", "json"]) +pods = json.loads(pods_result.stdout or "{}").get("items", []) if pods_result.returncode == 0 else [] +pods.sort(key=lambda item: item.get("metadata", {}).get("creationTimestamp", "")) +pod_name = pods[-1].get("metadata", {}).get("name") if pods else None +logs = "" +if pod_name: + logs_result = kubectl(["logs", pod_name, "--tail", str(tail_lines)]) + logs = logs_result.stdout if logs_result.returncode == 0 else logs_result.stderr +full_logs = "" +if pod_name: + full_result = kubectl(["logs", pod_name, "--tail", "800"]) + full_logs = full_result.stdout if full_result.returncode == 0 else "" +match = None +for line in reversed(full_logs.splitlines()): + if line.startswith("UNIDESK_K3S_BUILD_BENCHMARK_RESULT "): + try: + match = json.loads(line.split(" ", 1)[1]) + except Exception: + match = None + break +conditions = status.get("conditions") or [] +failed = any(item.get("type") == "Failed" and item.get("status") == "True" for item in conditions) +succeeded = status.get("succeeded", 0) > 0 +active = status.get("active", 0) > 0 +if succeeded: + state = "succeeded" +elif failed: + state = "failed" +elif active: + state = "running" +else: + state = "pending" +failure_family = "none" if state == "succeeded" else "unknown" +tail_text = (full_logs or logs)[-4000:] +if state == "missing": + failure_family = "job-missing" +elif "ImagePullBackOff" in tail_text or "ErrImagePull" in tail_text: + failure_family = "image-pull" +elif "apt-get" in tail_text and ("Failed" in tail_text or "Unable to" in tail_text): + failure_family = "apt-download" +elif "curl:" in tail_text: + failure_family = "dependency-download" +elif "payload-too-small" in tail_text: + failure_family = "payload-too-small" +elif failed: + failure_family = "failed" +payload = { + "ok": state == "succeeded", + "state": state, + "jobName": job_name, + "runId": labels.get("unidesk.ai/run-id") or "-", + "profile": labels.get("unidesk.ai/benchmark-profile") or "-", + "startedAt": status.get("startTime") or (match or {}).get("startedAt"), + "completedAt": status.get("completionTime") or (match or {}).get("completedAt"), + "result": match, + "failureFamily": failure_family, + "logTail": logs[-4000:], +} +print(json.dumps(payload, ensure_ascii=False)) +PY +`; +} + +function sampleTraffic(target: Sub2ApiTargetConfig, sampleSeconds: number, timeoutSeconds: number): TrafficSummary { + const spec = trafficSpec(target); + const result = runTrans(spec.route, egressProxyTrafficScript(spec, sampleSeconds), timeoutSeconds); + const parsed = parseJson(result.stdout); + const data = typeof parsed === "object" && parsed !== null ? parsed as Record : {}; + const totals = record(data.totals); + const clients = arrayRecords(data.clients); + const topClient = clients[0] ?? {}; + const destinations = arrayRecords(topClient.topDestinations); + return { + ok: result.exitCode === 0 && data.ok !== false, + reason: text(data.reason, result.exitCode === 0 ? "ok" : "traffic-failed"), + windowBytes: number(totals.clientWindowTotalBytes), + rateBps: number(totals.clientTotalBps), + processTotalBytes: number(totals.processTotalBytes), + topClient: text(topClient.client), + topDestination: text(destinations[0]?.destination), + }; +} + +function trafficSpec(target: Sub2ApiTargetConfig): EgressProxyTrafficSpec { + const proxy = target.egressProxy; + if (proxy === null) throw new Error(`target ${target.id} has no egressProxy`); + return { + scope: "platform-infra", + targetId: target.id, + route: target.route, + namespace: target.namespace, + deploymentName: proxy.deploymentName, + serviceName: proxy.serviceName, + port: proxy.listenPort, + sourceType: proxy.sourceType, + sourceRef: proxy.sourceRef, + sourceConfigRef: proxy.sourceConfigRef, + sourceFingerprint: proxy.sourceFingerprint, + }; +} + +function normalizeStatus(plan: TargetPlan, parsed: unknown, result: CommandResult): TargetStatus { + const data = typeof parsed === "object" && parsed !== null ? parsed as Record : {}; + const jobResult = record(data.result); + const status: TargetStatus = { + ok: result.exitCode === 0 && data.ok === true, + targetId: plan.targetId, + profile: text(data.profile, plan.profile?.id ?? "-"), + state: text(data.state, result.exitCode === 0 ? "unknown" : "failed"), + jobName: text(data.jobName), + runId: text(data.runId), + startedAt: text(data.startedAt), + completedAt: text(data.completedAt), + durationSeconds: nullableNumber(jobResult.durationSeconds), + outputMiB: nullableNumber(jobResult.outputMiB), + downloadMiB: nullableNumber(jobResult.downloadMiB), + payloadMiB: nullableNumber(jobResult.payloadMiB), + failureFamily: text(data.failureFamily, data.ok === true ? "none" : text(data.reason, "unknown")), + logTail: text(data.logTail, result.stderr.slice(-2000)), + }; + return status; +} + +function blockedStatus(plan: TargetPlan, profile: string): TargetStatus { + return { + ok: false, + targetId: plan.targetId, + profile, + state: "blocked", + jobName: "-", + runId: "-", + startedAt: "-", + completedAt: "-", + durationSeconds: null, + outputMiB: null, + downloadMiB: null, + payloadMiB: plan.profile?.payloadMiB ?? null, + failureFamily: plan.blocker ?? "blocked", + logTail: plan.detail ?? "", + }; +} + +function benchmarkJobName(target: Sub2ApiTargetConfig, profile: K3sBuildBenchmarkProfile, runId: string): string { + const base = `k3s-build-${target.id.toLowerCase()}-${profile.id}-${runId.replace(/^k3sbuild-/u, "")}`.toLowerCase().replace(/[^a-z0-9-]/gu, "-"); + return base.slice(0, 63).replace(/-+$/u, ""); +} + +function readK3sBuildBenchmarkConfig(): K3sBuildBenchmarkConfig { + const raw = asRecord(Bun.YAML.parse(readFileSync(rootPath(BENCHMARK_CONFIG_PATH), "utf8")) as unknown, BENCHMARK_CONFIG_PATH); + const version = integerField(raw, "version", BENCHMARK_CONFIG_PATH); + const kind = stringField(raw, "kind", BENCHMARK_CONFIG_PATH); + if (kind !== "platform-infra-egress-proxy-benchmarks") throw new Error(`${BENCHMARK_CONFIG_PATH}.kind must be platform-infra-egress-proxy-benchmarks`); + const metadataRaw = asRecord(raw.metadata, "metadata"); + const profilesRaw = asRecord(raw.profiles, "profiles"); + const profiles = Object.fromEntries(Object.entries(profilesRaw).map(([id, value]) => [id, profileSpec(id, asRecord(value, `profiles.${id}`))])); + return { + version, + kind, + metadata: { + owner: stringField(metadataRaw, "owner", "metadata"), + relatedIssues: integerArrayField(metadataRaw, "relatedIssues", "metadata"), + }, + profiles, + }; +} + +function profileSpec(id: string, raw: Record): K3sBuildBenchmarkProfile { + const workload = stringField(raw, "workload", `profiles.${id}`); + if (workload !== "k3s-build") throw new Error(`profiles.${id}.workload must be k3s-build`); + const imagePullPolicy = stringField(raw, "imagePullPolicy", `profiles.${id}`); + if (imagePullPolicy !== "Always" && imagePullPolicy !== "IfNotPresent" && imagePullPolicy !== "Never") throw new Error(`profiles.${id}.imagePullPolicy must be Always, IfNotPresent, or Never`); + const noMirror = asRecord(raw.noMirror, `profiles.${id}.noMirror`); + const registryMirror = stringField(noMirror, "registryMirror", `profiles.${id}.noMirror`); + if (registryMirror !== "forbidden") throw new Error(`profiles.${id}.noMirror.registryMirror must be forbidden`); + const dependencyDownload = asRecord(raw.dependencyDownload, `profiles.${id}.dependencyDownload`); + return { + id, + enabled: booleanField(raw, "enabled", `profiles.${id}`), + workload, + description: stringField(raw, "description", `profiles.${id}`), + image: stringField(raw, "image", `profiles.${id}`), + imagePullPolicy, + payloadMiB: integerField(raw, "payloadMiB", `profiles.${id}`), + timeoutSeconds: integerField(raw, "timeoutSeconds", `profiles.${id}`), + ttlSecondsAfterFinished: integerField(raw, "ttlSecondsAfterFinished", `profiles.${id}`), + noMirror: { + apt: stringField(noMirror, "apt", `profiles.${id}.noMirror`), + npmRegistry: stringField(noMirror, "npmRegistry", `profiles.${id}.noMirror`), + pipIndexUrl: stringField(noMirror, "pipIndexUrl", `profiles.${id}.noMirror`), + registryMirror, + }, + aptPackages: stringArrayField(raw, "aptPackages", `profiles.${id}`), + dependencyDownload: { + enabled: booleanField(dependencyDownload, "enabled", `profiles.${id}.dependencyDownload`), + url: stringField(dependencyDownload, "url", `profiles.${id}.dependencyDownload`), + chunks: integerField(dependencyDownload, "chunks", `profiles.${id}.dependencyDownload`), + expectedMiB: integerField(dependencyDownload, "expectedMiB", `profiles.${id}.dependencyDownload`), + }, + }; +} + +function runTrans(route: string, script: string, timeoutSeconds: number): CommandResult { + return runCommand(["/root/.local/bin/trans", route, "sh", "--", script], rootPath(), { timeoutMs: timeoutSeconds * 1000 }); +} + +function shQuote(value: string): string { + return `'${value.replace(/'/gu, `'\\''`)}'`; +} + +function option(args: string[], name: string): string | null { + const index = args.indexOf(name); + if (index === -1) return null; + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`); + return value; +} + +function positiveIntOption(args: string[], name: string, defaultValue: number, maxValue: number): number { + const raw = option(args, name); + if (raw === null) return defaultValue; + const value = Number.parseInt(raw, 10); + if (!Number.isInteger(value) || value < 0 || value > maxValue) throw new Error(`${name} must be an integer from 0 to ${maxValue}`); + return value; +} + +function parseJson(textValue: string): unknown { + const trimmed = textValue.trim(); + if (trimmed.length === 0) return null; + try { return JSON.parse(trimmed); } catch { + const start = trimmed.indexOf("{"); + const end = trimmed.lastIndexOf("}"); + if (start >= 0 && end > start) { + try { return JSON.parse(trimmed.slice(start, end + 1)); } catch {} + } + } + return null; +} + +function asRecord(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); + return value as Record; +} + +function stringField(obj: Record, 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`); + return value; +} + +function booleanField(obj: Record, key: string, path: string): boolean { + const value = obj[key]; + if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`); + return value; +} + +function integerField(obj: Record, key: string, path: string): number { + const value = obj[key]; + if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`${path}.${key} must be a non-negative integer`); + return value; +} + +function integerArrayField(obj: Record, key: string, path: string): number[] { + const value = obj[key]; + if (!Array.isArray(value) || value.some((item) => typeof item !== "number" || !Number.isInteger(item))) throw new Error(`${path}.${key} must be an integer array`); + return [...value] as number[]; +} + +function stringArrayField(obj: Record, key: string, path: string): string[] { + const value = obj[key]; + if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${path}.${key} must be a non-empty string array`); + return [...value] as string[]; +} + +function table(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 record(value: unknown): Record { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; +} + +function arrayRecords(value: unknown): Array> { + return Array.isArray(value) ? value.map(record) : []; +} + +function text(value: unknown, fallback = "-"): string { + if (value === undefined || value === null || value === "") return fallback; + return String(value); +} + +function number(value: unknown): number { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : 0; +} + +function nullableNumber(value: unknown): number | null { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; +} + +function bytes(value: unknown): string { + const parsed = number(value); + if (parsed <= 0) return "0 B"; + const units = ["B", "KiB", "MiB", "GiB", "TiB"]; + let scaled = parsed; + let unit = 0; + while (scaled >= 1024 && unit < units.length - 1) { + scaled /= 1024; + unit += 1; + } + return `${scaled >= 10 || unit === 0 ? scaled.toFixed(0) : scaled.toFixed(1)} ${units[unit]}`; +} + +function rate(value: unknown): string { + return `${bytes(value)}/s`; +} diff --git a/scripts/src/platform-infra/entry.ts b/scripts/src/platform-infra/entry.ts index d4a172f2..1b13a682 100644 --- a/scripts/src/platform-infra/entry.ts +++ b/scripts/src/platform-infra/entry.ts @@ -328,6 +328,7 @@ export function platformInfraHelp(): unknown { "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-image status", "bun scripts/cli.ts platform-infra sub2api codex-pool sentinel-probe --account unidesk-codex-hy --confirm", "bun scripts/cli.ts platform-infra egress-proxy traffic --target D601 --sample-seconds 15", + "bun scripts/cli.ts platform-infra egress-proxy k3s-build-benchmark --targets D601,D518 --profile no-mirror-600m --dry-run", "bun scripts/cli.ts platform-infra langbot plan [--target G14]", "bun scripts/cli.ts platform-infra langbot apply [--target G14] --confirm", "bun scripts/cli.ts platform-infra langbot status [--target G14] [--full|--raw]",