feat: add HWLAB dev CD CLI wrapper
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function runCli(args: string[], env: NodeJS.ProcessEnv = {}): JsonRecord {
|
||||
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
|
||||
cwd: process.cwd(),
|
||||
env: { ...process.env, ...env },
|
||||
encoding: "utf8",
|
||||
timeout: 20_000,
|
||||
});
|
||||
assert.equal(result.stderr, "", `stderr should be empty: ${result.stderr}`);
|
||||
assert.notEqual(result.stdout.trim(), "", "CLI must not produce empty output");
|
||||
const parsed = JSON.parse(result.stdout) as JsonRecord;
|
||||
if (result.status !== 0) {
|
||||
assert.equal(parsed.ok, false, `nonzero CLI should return ok=false: ${result.stdout}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function makeFakeHwlabRepo(): string {
|
||||
const root = join(tmpdir(), `unidesk-hwlab-cd-wrapper-${process.pid}-${Date.now()}`);
|
||||
mkdirSync(join(root, "scripts"), { recursive: true });
|
||||
writeFileSync(join(root, "scripts/dev-cd-apply.mjs"), "process.stdout.write(JSON.stringify({ok:true}))\n");
|
||||
writeFileSync(join(root, "scripts/dev-deploy-apply.mjs"), [
|
||||
"const dryRun = process.argv.includes('--dry-run');",
|
||||
"const kubeconfigIndex = process.argv.indexOf('--kubeconfig');",
|
||||
"process.stdout.write(JSON.stringify({",
|
||||
" reportVersion: 'v1',",
|
||||
" status: dryRun ? 'pass' : 'blocked',",
|
||||
" commitId: 'abc1234',",
|
||||
" namespace: 'hwlab-dev',",
|
||||
" endpoint: 'http://74.48.78.17:16667',",
|
||||
" blockers: [],",
|
||||
" devDeployApply: {",
|
||||
" conclusion: { status: 'ready', blockerCount: 0 },",
|
||||
" artifactPlan: { expectedArtifactCommit: 'abc1234', deployCommitId: 'abc1234', catalogCommitId: 'abc1234', published: true, registryVerified: true, imageCount: 13, requiredServiceCount: 13, unpublishedServices: [] },",
|
||||
" applyBoundary: { currentMode: 'dry-run', defaultNoWrite: true, mutationAttempted: false, mutationAllowed: false, kubeconfigSource: 'flag:--kubeconfig', writeScope: 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl apply -k deploy/k8s/dev', noWriteScope: 'server-side dry-run only', forbiddenActions: ['prod-deploy'] },",
|
||||
" applyStep: { status: 'pass', command: 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl apply --dry-run=server -k deploy/k8s/dev', mutationAttempted: false },",
|
||||
" manualCommands: { status: 'ready' }",
|
||||
" },",
|
||||
" kubeconfig: kubeconfigIndex >= 0 ? process.argv[kubeconfigIndex + 1] : null",
|
||||
"}, null, 2));",
|
||||
].join("\n"));
|
||||
writeFileSync(join(root, "scripts/deploy-desired-state-plan.mjs"), [
|
||||
"process.stdout.write(JSON.stringify({",
|
||||
" kind: 'hwlab-deploy-desired-state-plan',",
|
||||
" status: 'pass',",
|
||||
" summary: { desiredCommitId: 'abc1234', desiredImageTag: 'abc1234', artifactState: 'published', ciPublished: true, registryVerified: true, services: 13, workloadContainers: 13, diagnostics: 0, blockers: 0, targetConvergence: 'not_requested' }",
|
||||
"}, null, 2));",
|
||||
].join("\n"));
|
||||
return root;
|
||||
}
|
||||
|
||||
function makeFakeBin(mode: "native" | "desktop"): string {
|
||||
const bin = join(tmpdir(), `unidesk-hwlab-cd-bin-${process.pid}-${Date.now()}-${mode}`);
|
||||
mkdirSync(bin, { recursive: true });
|
||||
const context = mode === "desktop" ? "docker-desktop" : "default";
|
||||
const server = mode === "desktop" ? "https://127.0.0.1:11700" : "https://127.0.0.1:6443";
|
||||
const nodes = mode === "desktop" ? "desktop-control-plane" : "d601";
|
||||
writeFileSync(join(bin, "kubectl"), [
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
"printf 'KUBECONFIG=%s\\n' \"${KUBECONFIG:-}\" >&2",
|
||||
"if [[ \"$*\" == 'config current-context' ]]; then printf '%s\\n' " + JSON.stringify(context) + "; exit 0; fi",
|
||||
"if [[ \"$*\" == 'config view --minify -o jsonpath={.clusters[0].cluster.server}' ]]; then printf '%s' " + JSON.stringify(server) + "; exit 0; fi",
|
||||
"if [[ \"$*\" == 'get nodes -o jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}' ]]; then printf '%s\\n' " + JSON.stringify(nodes) + "; exit 0; fi",
|
||||
"if [[ \"$*\" == '-n hwlab-dev get lease hwlab-dev-cd-lock -o json' ]]; then printf 'Error from server (NotFound): leases.coordination.k8s.io \"hwlab-dev-cd-lock\" not found\\n' >&2; exit 1; fi",
|
||||
"printf '{}\\n'",
|
||||
].join("\n"));
|
||||
spawnSync("chmod", ["+x", join(bin, "kubectl")]);
|
||||
return bin;
|
||||
}
|
||||
|
||||
const fakeRepo = makeFakeHwlabRepo();
|
||||
const nativeBin = makeFakeBin("native");
|
||||
const desktopBin = makeFakeBin("desktop");
|
||||
const liveBody = "data:application/json,%7B%22serviceId%22%3A%22hwlab-cloud-web%22%2C%22environment%22%3A%22dev%22%2C%22status%22%3A%22ok%22%2C%22revision%22%3A%22abc1234%22%7D";
|
||||
const apiBody = "data:application/json,%7B%22serviceId%22%3A%22hwlab-cloud-api%22%2C%22environment%22%3A%22dev%22%2C%22status%22%3A%22ok%22%2C%22revision%22%3A%22abc1234%22%7D";
|
||||
|
||||
const help = runCli(["hwlab", "help"]);
|
||||
assert.equal(help.ok, true);
|
||||
assert.equal((help.data as JsonRecord).command, "hwlab cd");
|
||||
|
||||
const applyDryRun = runCli([
|
||||
"hwlab",
|
||||
"cd",
|
||||
"apply",
|
||||
"--env",
|
||||
"dev",
|
||||
"--dry-run",
|
||||
"--hwlab-repo",
|
||||
fakeRepo,
|
||||
], {
|
||||
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
||||
});
|
||||
assert.equal(applyDryRun.ok, true);
|
||||
const dryRunData = applyDryRun.data as JsonRecord;
|
||||
assert.equal(dryRunData.dryRun, true);
|
||||
assert.equal(dryRunData.mutation, false);
|
||||
assert.equal(((dryRunData.d601NativeK3sGuard as JsonRecord).injectedEnv as JsonRecord).KUBECONFIG, "/etc/rancher/k3s/k3s.yaml");
|
||||
assert.equal((dryRunData.controlledDryRun as JsonRecord).commandOk, true);
|
||||
assert.equal(((dryRunData.hostCommanderOnlyLiveApply as JsonRecord).commandShape as unknown[]).includes("scripts/dev-cd-apply.mjs"), true);
|
||||
|
||||
const realApply = runCli([
|
||||
"hwlab",
|
||||
"cd",
|
||||
"apply",
|
||||
"--env",
|
||||
"dev",
|
||||
"--hwlab-repo",
|
||||
fakeRepo,
|
||||
], {
|
||||
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
||||
});
|
||||
assert.equal(realApply.ok, false);
|
||||
assert.equal((realApply.data as JsonRecord).error, "host-commander-only-real-apply");
|
||||
|
||||
const status = runCli([
|
||||
"hwlab",
|
||||
"cd",
|
||||
"status",
|
||||
"--env",
|
||||
"dev",
|
||||
"--hwlab-repo",
|
||||
fakeRepo,
|
||||
], {
|
||||
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
||||
UNIDESK_HWLAB_CD_TEST_FRONTEND_LIVE_URL: liveBody,
|
||||
UNIDESK_HWLAB_CD_TEST_API_LIVE_URL: apiBody,
|
||||
});
|
||||
assert.equal(status.ok, true);
|
||||
const statusData = status.data as JsonRecord;
|
||||
assert.equal(((statusData.d601NativeK3sGuard as JsonRecord).injectedEnv as JsonRecord).KUBECONFIG, "/etc/rancher/k3s/k3s.yaml");
|
||||
assert.equal((statusData.liveRevisions as JsonRecord).status, "observed");
|
||||
assert.ok(typeof statusData.dumpDir === "string" && String(statusData.dumpDir).includes(".state/hwlab-cd"));
|
||||
|
||||
const desktopRefusal = runCli([
|
||||
"hwlab",
|
||||
"cd",
|
||||
"apply",
|
||||
"--env",
|
||||
"dev",
|
||||
"--dry-run",
|
||||
"--hwlab-repo",
|
||||
fakeRepo,
|
||||
], {
|
||||
PATH: `${desktopBin}:${process.env.PATH ?? ""}`,
|
||||
});
|
||||
assert.equal(desktopRefusal.ok, false);
|
||||
assert.equal((desktopRefusal.data as JsonRecord).error, "native-k3s-guard-refused");
|
||||
assert.deepEqual((desktopRefusal.data as JsonRecord).d601NativeK3sGuard && ((desktopRefusal.data as JsonRecord).d601NativeK3sGuard as JsonRecord).refusalSignals, ["docker-desktop", "desktop-control-plane", "127.0.0.1:11700"]);
|
||||
|
||||
console.log(JSON.stringify({ ok: true, checked: "hwlab-cd-wrapper-contract" }));
|
||||
Reference in New Issue
Block a user