mirror of
https://github.com/tale/headplane.git
synced 2026-10-26 06:30:12 +00:00
135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
// pnpm harness up [scenario] [--version 0.29.5] [--headplane] | pnpm harness list
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { randomBytes } from "node:crypto";
|
|
import { mkdtemp, readdir, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { basename, join } from "node:path";
|
|
import { parseArgs } from "node:util";
|
|
|
|
import { stringify } from "yaml";
|
|
|
|
import { HARNESS_LABEL } from "./headscale";
|
|
import { applyScenario, type Scenario } from "./scenario";
|
|
import { createTailnet } from "./tailnet";
|
|
import { HEADSCALE_VERSIONS, type HeadscaleVersion, LATEST_HEADSCALE } from "./versions";
|
|
|
|
const scenariosDir = join(import.meta.dirname, "scenarios");
|
|
|
|
const { positionals, values } = parseArgs({
|
|
allowPositionals: true,
|
|
options: {
|
|
version: { type: "string", default: LATEST_HEADSCALE },
|
|
headplane: { type: "boolean", default: false },
|
|
port: { type: "string", default: "3001" },
|
|
},
|
|
});
|
|
|
|
const [command = "up", name = "homelab"] = positionals;
|
|
|
|
async function loadScenario(scenario: string): Promise<Scenario> {
|
|
const module = await import(join(scenariosDir, `${scenario}.ts`)).catch(() => undefined);
|
|
if (!module) {
|
|
throw new Error(`No scenario named ${scenario}. Run \`pnpm harness list\` to see them.`);
|
|
}
|
|
|
|
return module.default;
|
|
}
|
|
|
|
if (command === "list") {
|
|
for (const file of (await readdir(scenariosDir)).sort()) {
|
|
const scenario = await loadScenario(basename(file, ".ts"));
|
|
console.log(`${basename(file, ".ts").padEnd(12)} ${scenario.description}`);
|
|
}
|
|
|
|
process.exit(0);
|
|
}
|
|
|
|
if (command !== "up") {
|
|
console.error(`Unknown command ${command}. Use \`up\` or \`list\`.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const version = values.version as HeadscaleVersion;
|
|
if (!HEADSCALE_VERSIONS.includes(version)) {
|
|
console.error(
|
|
`Headscale ${version} isn't supported. Pick one of ${HEADSCALE_VERSIONS.join(", ")}.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const scenario = await loadScenario(name);
|
|
console.log(`Starting ${name} on Headscale ${version}: ${scenario.description}`);
|
|
|
|
const tailnet = await createTailnet({
|
|
version,
|
|
headscaleConfig: scenario.headscaleConfig,
|
|
records: scenario.records,
|
|
});
|
|
let headplane: ReturnType<typeof spawn> | undefined;
|
|
|
|
async function shutdown() {
|
|
console.log("\nStopping the tailnet...");
|
|
headplane?.kill("SIGTERM");
|
|
await tailnet.dispose();
|
|
process.exit(0);
|
|
}
|
|
|
|
process.on("SIGINT", shutdown);
|
|
process.on("SIGTERM", shutdown);
|
|
|
|
await applyScenario(tailnet, scenario);
|
|
|
|
const dataDir = await mkdtemp(join(tmpdir(), "hp-harness-headplane-"));
|
|
const configPath = join(dataDir, "config.yaml");
|
|
await writeFile(
|
|
configPath,
|
|
stringify({
|
|
server: {
|
|
host: "127.0.0.1",
|
|
port: Number(values.port),
|
|
cookie_secret: randomBytes(16).toString("hex"),
|
|
cookie_secure: false,
|
|
data_path: dataDir,
|
|
},
|
|
headscale: {
|
|
url: tailnet.server.url,
|
|
api_key: tailnet.server.apiKey,
|
|
tls_cert_path: tailnet.server.certPath,
|
|
config_path: tailnet.server.configPath,
|
|
dns_records_path: tailnet.server.recordsPath,
|
|
},
|
|
integration: {
|
|
docker: {
|
|
enabled: true,
|
|
container_label: `${HARNESS_LABEL}=${tailnet.id}`,
|
|
socket: process.env.DOCKER_HOST ?? "unix:///var/run/docker.sock",
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
console.log(`
|
|
Headscale ${tailnet.server.url}
|
|
API key ${tailnet.server.apiKey}
|
|
CA cert ${tailnet.server.certPath}
|
|
Config ${tailnet.server.configPath}
|
|
Headplane ${configPath}
|
|
Exec docker exec ${tailnet.server.container.getId().slice(0, 12)} headscale <command>
|
|
`);
|
|
|
|
if (values.headplane) {
|
|
headplane = spawn("pnpm", ["exec", "react-router", "dev", "--port", values.port!], {
|
|
stdio: "inherit",
|
|
env: { ...process.env, HEADPLANE_CONFIG_PATH: configPath },
|
|
});
|
|
|
|
console.log(`Headplane http://127.0.0.1:${values.port}/admin (sign in with the API key)\n`);
|
|
} else {
|
|
console.log(`Run Headplane against it with:
|
|
HEADPLANE_CONFIG_PATH=${configPath} pnpm exec react-router dev --port ${values.port}
|
|
`);
|
|
}
|
|
|
|
console.log("Press Ctrl-C to stop.");
|