UNPKG

@mesh-tech/mesh-cli

Version:

CLI for Mesh platform development utilities

132 lines (131 loc) 4.46 kB
import { execFile, execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; const TAILSCALE_PATHS = [ "tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale", ]; export async function isVpnConnected() { try { const stdout = await execTailscaleCmd(["status", "--json"]); const status = JSON.parse(stdout); return status.BackendState === "Running" && status.Self?.Online === true; } catch { return false; } } const TAILSCALE_SOCKET_PATHS = ["/var/run/tailscale/tailscaled.sock", "/tmp/tailscale.sock"]; export function headscaleDnsConfig(tenant = "mesh", env = "dev", overrides) { return { namespace: `${tenant}-${env}-headscale`, pod: "headscale-0", container: "dns-writer", filePath: "/var/lib/headscale/dns/extra-records.json", ...overrides, }; } function execTailscaleCmd(args) { const socketPath = TAILSCALE_SOCKET_PATHS.find(existsSync); const attempts = []; for (const binary of TAILSCALE_PATHS) { if (socketPath) { attempts.push({ binary, args: ["--socket", socketPath, ...args] }); } attempts.push({ binary, args }); } return new Promise((resolve, reject) => { let index = 0; function tryNext() { if (index >= attempts.length) { reject(new Error("All tailscale binary attempts failed")); return; } const attempt = attempts[index++]; execFile(attempt.binary, attempt.args, (error, stdout) => { if (error) { tryNext(); } else { resolve(stdout); } }); } tryNext(); }); } export async function getTailscaleInfo() { try { const stdout = await execTailscaleCmd(["status", "--json"]); const status = JSON.parse(stdout); if (status.BackendState === "Running" && status.Self?.Online === true && status.Self.HostName && status.Self.TailscaleIPs?.length) { return { hostname: status.Self.HostName, ip: status.Self.TailscaleIPs[0], }; } return null; } catch { return null; } } function dnsWriterExec(cfg, cmd, interactive = false) { return [ "exec", ...(interactive ? ["-i"] : []), "-n", cfg.namespace, cfg.pod, "-c", cfg.container, "--", ...cmd, ]; } export function readDnsRecords(cfg) { try { const raw = execFileSync("kubectl", dnsWriterExec(cfg, ["cat", cfg.filePath]), { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], }); const trimmed = raw.trim(); if (!trimmed || trimmed === "[]") return []; try { return JSON.parse(trimmed); } catch { console.warn(`[vpn] DNS records file contains invalid JSON, treating as empty`); return []; } } catch (err) { const msg = err.message ?? String(err); if (msg.includes("not found") || msg.includes("Unable to connect")) { throw new Error(`Cannot reach Headscale DNS writer (namespace=${cfg.namespace}, pod=${cfg.pod}).\n` + `Verify kubectl context and that the Headscale pod is running:\n` + ` kubectl get pods -n ${cfg.namespace}`); } if (msg.includes("No such file")) { return []; } throw err; } } export function registerDnsRecords(cfg, records) { const json = JSON.stringify(records); try { execFileSync("kubectl", dnsWriterExec(cfg, ["sh", "-c", `cat > '${cfg.filePath}'`], true), { input: json, stdio: ["pipe", "pipe", "pipe"] }); } catch (err) { const msg = err.message ?? String(err); throw new Error(`Failed to write DNS records to Headscale (namespace=${cfg.namespace}, pod=${cfg.pod}).\n` + `${msg.includes("not found") ? "Pod not found. " : ""}` + `Verify kubectl context: kubectl get pods -n ${cfg.namespace}`); } } export function unregisterDnsRecords(cfg, suffix) { try { const records = readDnsRecords(cfg); const filtered = records.filter((r) => !r.name.endsWith(suffix)); registerDnsRecords(cfg, filtered); } catch { } }