150 lines
5.1 KiB
TypeScript
Executable File
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);
|