#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts"; import { delay } from "https://deno.land/std/async/mod.ts"; const alpineImage = "alpine/edge/cloud" const alpineConfig = ['--profile', 'cloud-init-alpine'] const archImage = "archlinux/current/cloud" const archConfig = ['--profile', 'cloud-init-arch'] const image = archImage const config = archConfig const findIP4 = (name: string, nodeList: any) => { const ip4 = nodeList?.find((n) => n.name === name)?.state?.network?.eth0?.addresses?.find((n) => n.family === 'inet')?.address; return ip4; } const setupCluster = async (numMasters: number) => { const hostname = await Deno.run({ cmd: ["hostnamectl", "hostname"], stdout: "piped", }).output().then((output) => new TextDecoder().decode(output).trim()); const user = await Deno.run({ cmd: ["whoami"], stdout: "piped", }).output().then((output) => new TextDecoder().decode(output).trim()); const sshKeyPubFileName = `/home/${user}/.ssh/nl.fourlights.${hostname}.pub`; const sshKeyPrivateFileName = `/home/${user}/.ssh/nl.fourlights.${hostname}`; // Step 1: Create Low-Resource Profile (if not exists) const profileExists = await Deno.run({ cmd: ["incus", "profile", "show", "low-resource"], stdout: "null", stderr: "null", }).status().then((status) => status.success); if (!profileExists) { await Deno.run({ cmd: ["incus", "profile", "create", "low-resource"], }).status(); await Deno.run({ cmd: ["incus", "profile", "set", "low-resource", "limits.cpu=1", "limits.memory=512MB"], }).status(); await Deno.run({ cmd: ["incus", "profile", "device", "add", "low-resource", "root", "disk", "pool=default", "path=/"], }).status(); await Deno.run({ cmd: ["incus", "profile", "device", "add", "low-resource", "eth-0", "nic", "network=incusbr0"], }).status(); console.log("✅ Low-resource profile created."); } else { console.log("⏩ Low-resource profile already exists."); } const sshKey = await Deno.readTextFile(sshKeyPubFileName); // Step 3: Launch VMs (if not already running) for (let i = 1; i <= numMasters; i++) { const vmName = `k3s-master${i}`; const vmExists = await Deno.run({ cmd: ["incus", "list", vmName, "--format", "csv"], stdout: "piped", }).output().then((output) => new TextDecoder().decode(output).trim() !== ""); if (!vmExists) { await Deno.run({ cmd: ["incus", "launch", `images:${image}`, vmName, "--profile", "low-resource", "-c", "user.timezone=\"Europe/Amsterdam\"", "-c", `user.ssh_key=\"${sshKey}\"`, ...config], }).status(); console.log(`✅ VM ${vmName} launched.`); } else { console.log(`⏩ VM ${vmName} already exists.`); } } // Step 4: Install k3sup (if not installed) const k3supInstalled = await Deno.run({ cmd: ["which", "k3sup"], stdout: "null", stderr: "null", }).status().then((status) => status.success); if (!k3supInstalled) { await Deno.run({ cmd: ["sh", "-c", "curl -sLS https://get.k3sup.dev | sh"], }).status(); console.log("✅ k3sup installed."); } else { console.log("⏩ k3sup already installed."); } // Step 5: Bootstrap First Master Node (if not already bootstrapped) let firstMasterIP; let nodes; while (firstMasterIP === undefined) { nodes = await Deno.run({ cmd: ["incus", "list", "--format", "json"], stdout: "piped", }).output().then((output) => JSON.parse(new TextDecoder().decode(output))); firstMasterIP = findIP4('k3s-master1', nodes) await delay(1000) } const kubeconfigExists = await Deno.stat("./kubeconfig").then(() => true).catch(() => false); if (!kubeconfigExists) { await Deno.run({ cmd: ["k3sup", "install", "--ip", firstMasterIP, "--user", "picard", "--cluster", "--ssh-key", sshKeyPrivateFileName], }).status(); console.log("✅ First master node bootstrapped."); } else { console.log("⏩ First master node already bootstrapped."); } // Step 6: Join Additional Master Nodes (if not already joined) for (let i = 2; i <= numMasters; i++) { const vmName = `k3s-master${i}`; const vmIP = findIP4(vmName, nodes) const joined = await Deno.run({ cmd: ["kubectl", "get", "nodes", vmName], stdout: "null", stderr: "null", }).status().then((status) => status.success); if (!joined) { await Deno.run({ cmd: ["k3sup", "join", "--ip", vmIP, "--server-ip", firstMasterIP, "--user", "picard", "--ssh-key", sshKeyPrivateFileName], }).status(); console.log(`✅ VM ${vmName} joined the cluster.`); } else { console.log(`⏩ VM ${vmName} already joined the cluster.`); } } console.log("🚀 HA k3s cluster setup complete!"); }; await new Command() .name("setup-k3s-cluster") .version("0.1.0") .description("Automate the setup of an HA k3s cluster using incus and k3sup") .option("-m, --masters ", "Number of master nodes", {default: 3}) .action(({masters}) => setupCluster(masters)) .parse(Deno.args);