326 lines
15 KiB
TypeScript
326 lines
15 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { buildHwlabCdRemoteCommandForTest } from "./src/hwlab-cd";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function dataOf(payload: JsonRecord): JsonRecord {
|
|
return payload.data as JsonRecord;
|
|
}
|
|
|
|
function runCli(args: string[], env: NodeJS.ProcessEnv = {}): JsonRecord {
|
|
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
|
|
cwd: process.cwd(),
|
|
env: {
|
|
...process.env,
|
|
UNIDESK_HWLAB_CD_TRANSPORT: "local",
|
|
...env,
|
|
},
|
|
encoding: "utf8",
|
|
timeout: 25_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 writeJson(path: string, value: unknown): void {
|
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function makeFakeHwlabRepo(): string {
|
|
const root = join(tmpdir(), `unidesk-hwlab-cd-wrapper-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
mkdirSync(join(root, "scripts"), { recursive: true });
|
|
mkdirSync(join(root, "deploy/k8s/base"), { recursive: true });
|
|
mkdirSync(join(root, "reports/dev-gate"), { recursive: true });
|
|
writeFileSync(join(root, "scripts/dev-cd-apply.mjs"), [
|
|
"const kubeconfigIndex = process.argv.indexOf('--kubeconfig');",
|
|
"process.stdout.write(JSON.stringify({",
|
|
" ok: true,",
|
|
" status: 'pass',",
|
|
" mode: process.argv.includes('--dry-run') ? 'dry-run' : 'status',",
|
|
" command: 'dev-cd-apply',",
|
|
" mutationAttempted: false,",
|
|
" prodTouched: false,",
|
|
" target: {",
|
|
" ref: 'origin/main',",
|
|
" promotionCommit: 'abc1234567890abcdef',",
|
|
" shortCommitId: 'abc1234',",
|
|
" promotionSource: 'deploy/deploy.json',",
|
|
" publishRequired: false,",
|
|
" headCommitId: 'abc1234567890abcdef',",
|
|
" headMatchesTarget: true,",
|
|
" desiredStateCheck: { status: 'pass', summary: { desiredCommitId: 'abc1234', targetConvergence: 'already_promoted' } },",
|
|
" artifactBoundary: { status: 'pass', desiredState: { deployCommitId: 'abc1234', catalogCommitId: 'abc1234', deployCommitMatches: true, catalogCommitMatches: true } },",
|
|
" namespace: 'hwlab-dev'",
|
|
" },",
|
|
" deployJson: { path: 'deploy/deploy.json', commitId: 'abc1234', matchesTarget: true },",
|
|
" artifactCatalog: { path: 'deploy/artifact-catalog.dev.json', commitId: 'abc1234', artifactState: 'published', ciPublished: true, registryVerified: true },",
|
|
" artifactReport: { path: 'reports/dev-gate/dev-artifacts.json', commitId: 'abc1234' },",
|
|
" lock: { status: 'absent' },",
|
|
" liveDelta: { status: 'not_run', reason: '--skip-live-verify' },",
|
|
" kubeconfig: kubeconfigIndex >= 0 ? process.argv[kubeconfigIndex + 1] : null",
|
|
"}, null, 2));",
|
|
].join("\n"));
|
|
writeJson(join(root, "deploy/deploy.json"), { commitId: "abc1234", environment: "dev", namespace: "hwlab-dev", endpoint: "http://74.48.78.17:16667" });
|
|
writeJson(join(root, "deploy/artifact-catalog.dev.json"), { commitId: "abc1234", artifactState: "published", publish: { ciPublished: true, registryVerified: true }, services: [{ id: "hwlab-cloud-api", digest: "sha256:" + "a".repeat(64) }] });
|
|
writeJson(join(root, "reports/dev-gate/dev-artifacts.json"), { commitId: "abc1234", artifactPublish: { sourceCommitId: "abc1234", serviceCount: 1, ciPublished: true, registryVerified: true } });
|
|
writeFileSync(join(root, "deploy/k8s/base/workloads.yaml"), "apiVersion: v1\nkind: List\nitems: []\n");
|
|
spawnSync("git", ["init", "-b", "main"], { cwd: root, encoding: "utf8" });
|
|
spawnSync("git", ["config", "user.email", "test@example.invalid"], { cwd: root, encoding: "utf8" });
|
|
spawnSync("git", ["config", "user.name", "HWLAB CD Test"], { cwd: root, encoding: "utf8" });
|
|
spawnSync("git", ["remote", "add", "origin", "git@github.com:pikasTech/HWLAB.git"], { cwd: root, encoding: "utf8" });
|
|
spawnSync("git", ["add", "."], { cwd: root, encoding: "utf8" });
|
|
spawnSync("git", ["commit", "-m", "fixture"], { cwd: root, encoding: "utf8" });
|
|
spawnSync("git", ["update-ref", "refs/remotes/origin/main", "HEAD"], { cwd: root, encoding: "utf8" });
|
|
writeFileSync(join(root, ".git", "FETCH_HEAD"), "fixture\n");
|
|
return root;
|
|
}
|
|
|
|
function makeFakeBin(mode: "native" | "desktop" | "stale-default" | "wrong-node" | "missing-secret" | "second-plane"): string {
|
|
const bin = join(tmpdir(), `unidesk-hwlab-cd-bin-${process.pid}-${Date.now()}-${mode}-${Math.random().toString(16).slice(2)}`);
|
|
mkdirSync(bin, { recursive: true });
|
|
const explicitContext = mode === "desktop" ? "docker-desktop" : "default";
|
|
const explicitServer = mode === "desktop" ? "https://127.0.0.1:11700" : "https://127.0.0.1:6443";
|
|
const explicitNodes = mode === "desktop" ? "desktop-control-plane" : mode === "wrong-node" ? "d602" : "d601";
|
|
const defaultContext = mode === "stale-default" ? "docker-desktop" : mode === "second-plane" ? "other-k3s" : explicitContext;
|
|
const defaultServer = mode === "stale-default" ? "https://127.0.0.1:11700" : mode === "second-plane" ? "https://10.0.0.2:6443" : explicitServer;
|
|
const defaultNodes = mode === "stale-default" ? "desktop-control-plane" : explicitNodes;
|
|
const defaultNamespaceOk = mode === "second-plane";
|
|
writeFileSync(join(bin, "kubectl"), [
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
"context=" + JSON.stringify(explicitContext),
|
|
"server=" + JSON.stringify(explicitServer),
|
|
"nodes=" + JSON.stringify(explicitNodes),
|
|
"if [[ \"${KUBECONFIG:-}\" == '' ]]; then",
|
|
" context=" + JSON.stringify(defaultContext),
|
|
" server=" + JSON.stringify(defaultServer),
|
|
" nodes=" + JSON.stringify(defaultNodes),
|
|
"fi",
|
|
"if [[ \"$*\" == 'config current-context' ]]; then printf '%s\\n' \"$context\"; exit 0; fi",
|
|
"if [[ \"$*\" == 'config view --minify -o jsonpath={.clusters[0].cluster.server}' ]]; then printf '%s' \"$server\"; exit 0; fi",
|
|
"if [[ \"$*\" == 'get nodes -o jsonpath={range .items[*]}{.metadata.name}{\"\\n\"}{end}' ]]; then printf '%s\\n' \"$nodes\"; exit 0; fi",
|
|
"if [[ \"$*\" == 'get namespace hwlab-dev -o name' ]]; then",
|
|
" if [[ \"${KUBECONFIG:-}\" == '' && " + JSON.stringify(defaultNamespaceOk ? "yes" : "no") + " == 'yes' ]]; then printf 'namespace/hwlab-dev\\n'; exit 0; fi",
|
|
" if [[ \"${KUBECONFIG:-}\" != '' ]]; then printf 'namespace/hwlab-dev\\n'; exit 0; fi",
|
|
" printf 'Error from server (NotFound): namespaces \"hwlab-dev\" not found\\n' >&2; exit 1",
|
|
"fi",
|
|
"if [[ \"$*\" =~ ^-n[[:space:]]+hwlab-dev[[:space:]]+get[[:space:]]+lease[[:space:]]+hwlab-dev-cd-lock[[:space:]]+-o[[:space:]]+json$ ]]; then printf 'Error from server (NotFound): leases.coordination.k8s.io \"hwlab-dev-cd-lock\" not found\\n' >&2; exit 1; fi",
|
|
"if [[ \"$*\" == '-n hwlab-dev get secret hwlab-code-agent-provider -o name' && " + JSON.stringify(mode) + " == 'missing-secret' ]]; then printf 'Error from server (NotFound): secrets \"hwlab-code-agent-provider\" not found\\n' >&2; exit 1; fi",
|
|
"if [[ \"$*\" =~ ^-n\\ hwlab-dev\\ get\\ secret\\ ([^[:space:]]+)\\ -o\\ name$ ]]; then printf 'secret/%s\\n' \"${BASH_REMATCH[1]}\"; exit 0; fi",
|
|
"if [[ \"$*\" == '-n hwlab-dev describe secret hwlab-cloud-api-dev-db' ]]; then printf 'Name: hwlab-cloud-api-dev-db\\nData\\n====\\ndatabase-url: 48 bytes\\n'; exit 0; fi",
|
|
"if [[ \"$*\" == '-n hwlab-dev describe secret hwlab-cloud-api-dev-db-admin' ]]; then printf 'Name: hwlab-cloud-api-dev-db-admin\\nData\\n====\\nadmin-url: 48 bytes\\n'; exit 0; fi",
|
|
"if [[ \"$*\" == '-n hwlab-dev describe secret hwlab-code-agent-provider' ]]; then printf 'Name: hwlab-code-agent-provider\\nData\\n====\\nopenai-api-key: 48 bytes\\n'; exit 0; fi",
|
|
"printf 'unexpected kubectl args: %s\\n' \"$*\" >&2; exit 99",
|
|
].join("\n"));
|
|
spawnSync("chmod", ["+x", join(bin, "kubectl")]);
|
|
return bin;
|
|
}
|
|
|
|
function scopes(data: JsonRecord): string[] {
|
|
return ((data.blockers ?? []) as JsonRecord[]).map((blocker) => String(blocker.scope ?? ""));
|
|
}
|
|
|
|
function withLocalTransport(args: string[]): string[] {
|
|
return [...args, "--transport", "local"];
|
|
}
|
|
|
|
const fakeRepo = makeFakeHwlabRepo();
|
|
const nativeBin = makeFakeBin("native");
|
|
const desktopBin = makeFakeBin("desktop");
|
|
const staleDefaultBin = makeFakeBin("stale-default");
|
|
const wrongNodeBin = makeFakeBin("wrong-node");
|
|
const missingSecretBin = makeFakeBin("missing-secret");
|
|
const secondPlaneBin = makeFakeBin("second-plane");
|
|
|
|
const help = runCli(["hwlab", "help"]);
|
|
assert.equal(help.ok, true);
|
|
assert.equal((help.data as JsonRecord).command, "hwlab cd");
|
|
|
|
const remoteCommand = buildHwlabCdRemoteCommandForTest(withLocalTransport(["cd", "apply", "--env", "dev", "--dry-run"]));
|
|
assert.equal(remoteCommand.includes("scripts/dev-cd-apply.mjs"), true);
|
|
assert.equal(remoteCommand.includes("/etc/rancher/k3s/k3s.yaml"), true);
|
|
assert.equal(remoteCommand.includes("kubectl rollout"), false);
|
|
|
|
const realApply = runCli(["hwlab", "cd", "apply", "--env", "dev", "--hwlab-repo", fakeRepo], {
|
|
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(realApply.ok, false);
|
|
assert.equal(dataOf(realApply).error, "host-commander-only-real-apply");
|
|
assert.equal((dataOf(realApply).safety as JsonRecord).rolloutExecuted, false);
|
|
|
|
const runnerHistoryRepo = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"status",
|
|
"--env",
|
|
"dev",
|
|
"--hwlab-repo",
|
|
"/home/ubuntu/hwlab",
|
|
]), {
|
|
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(runnerHistoryRepo.ok, false);
|
|
assert.equal((dataOf(runnerHistoryRepo).repo as JsonRecord).rejected, true);
|
|
|
|
const applyDryRun = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"apply",
|
|
"--env",
|
|
"dev",
|
|
"--dry-run",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(applyDryRun.ok, true);
|
|
const dryRunData = dataOf(applyDryRun);
|
|
assert.equal(dryRunData.dryRun, true);
|
|
assert.equal(dryRunData.mutation, false);
|
|
assert.equal((dryRunData.safety as JsonRecord).kubectlApplyExecuted, false);
|
|
assert.equal((dryRunData.safety as JsonRecord).rolloutExecuted, false);
|
|
assert.equal((dryRunData.safety as JsonRecord).liveVerifyExecuted, false);
|
|
assert.equal((dryRunData.safety as JsonRecord).cdLockMutated, false);
|
|
assert.equal((dryRunData.safety as JsonRecord).secretValuesPrinted, false);
|
|
assert.equal((dryRunData.remote as JsonRecord).providerId, "D601");
|
|
assert.equal(((dryRunData.remote as JsonRecord).commandCalls as unknown[]).includes("scripts/dev-cd-apply.mjs"), true);
|
|
assert.equal((dryRunData.kubeconfig as JsonRecord).path, "/etc/rancher/k3s/k3s.yaml");
|
|
assert.equal((dryRunData.nodeGuard as JsonRecord).requiredNodePresent, true);
|
|
assert.equal((dryRunData.worktreeGuard as JsonRecord).clean, true);
|
|
assert.equal((dryRunData.secretPreflight as JsonRecord).status, "pass");
|
|
assert.equal((dryRunData.controlledDevCd as JsonRecord).controlledEntrypoint, "scripts/dev-cd-apply.mjs");
|
|
assert.equal(((dryRunData.target as JsonRecord).promotionCommit), "abc1234567890abcdef");
|
|
assert.equal(((dryRunData.promotion as JsonRecord).source), "deploy/deploy.json");
|
|
assert.equal(JSON.stringify(dryRunData).includes("sk-secret"), false);
|
|
|
|
const status = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"status",
|
|
"--env",
|
|
"dev",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(status.ok, true);
|
|
const statusData = dataOf(status);
|
|
assert.equal((statusData.secretPreflight as JsonRecord).status, "skipped");
|
|
assert.equal((statusData.lockState as JsonRecord).status, "absent");
|
|
assert.ok(typeof statusData.reportDumpPath === "string");
|
|
|
|
const staleDefaultOk = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"apply",
|
|
"--env",
|
|
"dev",
|
|
"--dry-run",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${staleDefaultBin}:${process.env.PATH ?? ""}`,
|
|
KUBECONFIG: "",
|
|
});
|
|
assert.equal(staleDefaultOk.ok, true);
|
|
const staleDefaultGuard = dataOf(staleDefaultOk).nodeGuard as JsonRecord;
|
|
assert.equal(staleDefaultGuard.status, "pass");
|
|
assert.equal(staleDefaultGuard.refusal, false);
|
|
assert.equal((staleDefaultGuard.defaultKubectlDiagnostic as JsonRecord).status, "stale-forbidden-default");
|
|
|
|
const desktopRefusal = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"apply",
|
|
"--env",
|
|
"dev",
|
|
"--dry-run",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${desktopBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(desktopRefusal.ok, false);
|
|
const desktopData = dataOf(desktopRefusal);
|
|
assert.equal(desktopData.status, "refused");
|
|
assert.deepEqual((desktopData.nodeGuard as JsonRecord).refusalSignals, ["docker-desktop", "desktop-control-plane", "127.0.0.1:11700"]);
|
|
|
|
const wrongNodeBlocked = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"apply",
|
|
"--env",
|
|
"dev",
|
|
"--dry-run",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${wrongNodeBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(wrongNodeBlocked.ok, false);
|
|
const wrongNodeData = dataOf(wrongNodeBlocked);
|
|
assert.equal((wrongNodeData.nodeGuard as JsonRecord).requiredNodePresent, false);
|
|
assert.equal(scopes(wrongNodeData).includes("d601-native-k3s-guard"), true);
|
|
|
|
const missingSecretBlocked = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"apply",
|
|
"--env",
|
|
"dev",
|
|
"--dry-run",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${missingSecretBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(missingSecretBlocked.ok, false);
|
|
const missingSecretData = dataOf(missingSecretBlocked);
|
|
assert.equal((missingSecretData.secretPreflight as JsonRecord).status, "blocked");
|
|
assert.equal((missingSecretData.controlledDevCd as JsonRecord).status, "skipped");
|
|
assert.equal(scopes(missingSecretData).includes("secretref:hwlab-code-agent-provider/openai-api-key"), true);
|
|
assert.equal(JSON.stringify(missingSecretData).includes("sk-secret"), false);
|
|
|
|
const secondPlaneBlocked = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"apply",
|
|
"--env",
|
|
"dev",
|
|
"--dry-run",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${secondPlaneBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(secondPlaneBlocked.ok, false);
|
|
assert.equal(((dataOf(secondPlaneBlocked).nodeGuard as JsonRecord).defaultKubectlDiagnostic as JsonRecord).secondControlPlaneRisk, true);
|
|
assert.equal(scopes(dataOf(secondPlaneBlocked)).includes("second-hwlab-dev-control-plane"), true);
|
|
|
|
writeFileSync(join(fakeRepo, "dirty.txt"), "dirty\n");
|
|
const dirtyBlocked = runCli(withLocalTransport([
|
|
"hwlab",
|
|
"cd",
|
|
"apply",
|
|
"--env",
|
|
"dev",
|
|
"--dry-run",
|
|
"--hwlab-repo",
|
|
fakeRepo,
|
|
]), {
|
|
PATH: `${nativeBin}:${process.env.PATH ?? ""}`,
|
|
});
|
|
assert.equal(dirtyBlocked.ok, false);
|
|
assert.equal(scopes(dataOf(dirtyBlocked)).includes("hwlab-git-clean"), true);
|
|
|
|
console.log(JSON.stringify({ ok: true, checked: "hwlab-cd-wrapper-contract" }));
|