devops/shuttles/setup-cluster.ts

150 lines
5.1 KiB
TypeScript
Executable File

#!/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 <numMasters:number>", "Number of master nodes", {default: 3})
.action(({masters}) => setupCluster(masters))
.parse(Deno.args);