import { v4 as uuidv4 } from "uuid"; import bcrypt from "bcrypt"; import k8s from "@kubernetes/client-node"; import yaml from "js-yaml"; import fs from "node:fs"; import path from "node:path"; import ExpressClientError from "../util/ExpressClientError.js"; const kc = new k8s.KubeConfig(); kc.loadFromDefault(); const k8sDeps = kc.makeApiClient(k8s.AppsV1Api); const k8sCore = kc.makeApiClient(k8s.CoreV1Api); const namespace = process.env.MCL_SERVER_NAMESPACE; const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8")); function createRconSecret(serverSpec) { const { name } = serverSpec; const rconYaml = loadYaml("lib/k8s/configs/rcon-secret.yml"); // TODO: Dyamic rconPassword const rconPassword = bcrypt.hashSync(uuidv4(), 10); rconYaml.data["rcon-password"] = Buffer.from(rconPassword).toString("base64"); rconYaml.metadata.labels.app = `mcl-${name}-app`; rconYaml.metadata.name = `mcl-${name}-rcon-secret`; rconYaml.metadata.namespace = namespace; rconYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name; return rconYaml; } function createServerVolume(serverSpec) { const { name } = serverSpec; const volumeYaml = loadYaml("lib/k8s/configs/server-pvc.yml"); volumeYaml.metadata.labels.service = `mcl-${name}-server`; volumeYaml.metadata.name = `mcl-${name}-volume`; volumeYaml.metadata.namespace = namespace; volumeYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name; volumeYaml.spec.resources.requests.storage = "1Gi"; // TODO: Changeme return volumeYaml; } function getFtpContainer(serverSpec) { const { name } = serverSpec; const ftpContainer = loadYaml("lib/k8s/configs/containers/ftp-server.yml"); ftpContainer.name = `mcl-${name}-ftp`; const ftpPortList = [ { p: 20, n: "ftp-data" }, { p: 21, n: "ftp-commands" }, ]; for (var p = 40000; p <= 40009; p++) ftpPortList.push({ p, n: `ftp-passive-${p - 40000}` }); ftpContainer.ports = ftpPortList.map(({ p: containerPort, n: name }) => ({ containerPort, name, protocol: "TCP", })); return ftpContainer; } function getServerContainer(serverSpec) { const { name, version, serverType, difficulty, gamemode, memory, motd, maxPlayers, seed, ops, whitelist, } = serverSpec; const container = loadYaml("lib/k8s/configs/containers/minecraft-server.yml"); // Container Updates container.name = `mcl-${name}-server`; container.resources.requests.memory = `${memory}Mi`; // container.resources.limits.memory = `${memory}Mi`; // TODO Allow for limits beyond initial startup const findEnv = (k) => container.env.find(({ name: n }) => n === k); const updateEnv = (k, v) => (findEnv(k).value = v); // Enviornment variables updateEnv("TYPE", serverType); updateEnv("VERSION", version); updateEnv("DIFFICULTY", difficulty); updateEnv("MODE", gamemode); updateEnv("MOTD", motd); updateEnv("MAX_PLAYERS", maxPlayers); updateEnv("SEED", seed); updateEnv("OPS", ops); updateEnv("WHITELIST", whitelist); updateEnv("MEMORY", `${memory}M`); // RCON const rs = `mcl-${name}-rcon-secret`; findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs; return container; } function getBackupContainer(serverSpec) { const container = loadYaml("lib/k8s/configs/containers/minecraft-backup.yml"); return container; } function createServerDeploy(serverSpec) { const { name } = serverSpec; const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml"); const { metadata } = deployYaml; const serverContainer = getServerContainer(serverSpec); const backupContainer = getBackupContainer(serverSpec); const ftpContainer = getFtpContainer(serverSpec); // Configure Metadata; metadata.name = `mcl-${name}`; metadata.namespace = namespace; metadata.annotations["minecluster.dunemask.net/server-name"] = name; deployYaml.metadata = metadata; // Configure Lables & Selectors deployYaml.spec.selector.matchLabels.app = `mcl-${name}-app`; deployYaml.spec.template.metadata.labels.app = `mcl-${name}-app`; // Volumes deployYaml.spec.template.spec.volumes.find( ({ name }) => name === "datadir", ).persistentVolumeClaim.claimName = `mcl-${name}-volume`; // Apply Containers deployYaml.spec.template.spec.containers.push(serverContainer); deployYaml.spec.template.spec.containers.push(ftpContainer); // TODO: User control for autostart deployYaml.spec.replicas = 0; return deployYaml; } function createServerService(serverSpec) { const { name, url } = serverSpec; const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml"); serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = url; serviceYaml.metadata.labels.app = `mcl-${name}-app`; serviceYaml.metadata.name = `mcl-${name}-server`; serviceYaml.metadata.namespace = namespace; serviceYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name; serviceYaml.spec.selector.app = `mcl-${name}-app`; // Apply FTP Port List const ftpPortList = [ { p: 20, n: "ftp-data" }, { p: 21, n: "ftp-commands" }, ]; for (var p = 40000; p <= 40009; p++) ftpPortList.push({ p, n: `ftp-passive-${p - 40000}` }); serviceYaml.spec.ports = ftpPortList.map(({ p: port, n: name }) => ({ port, name, protocol: "TCP", targetPort: port, })); return serviceYaml; } function createRconService(serverSpec) { const { name } = serverSpec; const rconSvcYaml = loadYaml("lib/k8s/configs/rcon-svc.yml"); rconSvcYaml.metadata.labels.app = `mcl-${name}-app`; rconSvcYaml.metadata.name = `mcl-${name}-rcon`; rconSvcYaml.metadata.namespace = namespace; rconSvcYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name; rconSvcYaml.spec.selector.app = `mcl-${name}-app`; return rconSvcYaml; } export default async function createServerResources(serverSpec) { const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); const deployments = deploymentRes.body.items.map((i) => i.metadata.name); if (deployments.includes(`mcl-${serverSpec.name}`)) throw new ExpressClientError({ m: "Server already exists!", c: 409 }); const pvcRes = await k8sCore.listNamespacedPersistentVolumeClaim(namespace); const pvcs = pvcRes.body.items.map((i) => i.metadata.name); if (pvcs.includes(`mcl-${serverSpec.name}-volume`)) throw new ExpressClientError({ m: "Server PVC already exists!", c: 409 }); const rconSecret = createRconSecret(serverSpec); const serverVolume = createServerVolume(serverSpec); const serverDeploy = createServerDeploy(serverSpec); const serverService = createServerService(serverSpec); const rconService = createRconService(serverSpec); k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume); k8sCore.createNamespacedSecret(namespace, rconSecret); k8sCore.createNamespacedService(namespace, serverService); k8sCore.createNamespacedService(namespace, rconService); k8sDeps.createNamespacedDeployment(namespace, serverDeploy); }