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"; 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; function payloadFilter(req, res) { const serverSpec = req.body; if (!serverSpec) return res.sendStatus(400); const { name, url, version, serverType, difficulty, gamemode, memory } = serverSpec; if (!name) return res.status(400).send("Server name is required!"); if (!url) return res.status(400).send("Server url is required!"); if (!version) return res.status(400).send("Server version is required!"); if (!difficulty) return res.status(400).send("Server difficulty is required!"); if (!serverType) return res.status(400).send("Server type is required!"); if (!gamemode) return res.status(400).send("Server Gamemode is required!"); if (!memory) return res.status(400).send("Memory is required!"); req.body.name = req.body.name.toLowerCase(); return "filtered"; } function createRconSecret(serverSpec) { const { name } = serverSpec; const rconYaml = yaml.load( fs.readFileSync(path.resolve("lib/k8s/configs/rcon-secret.yml"), "utf8") ); // 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; return rconYaml; } function createServerVolume(serverSpec) { const { name } = serverSpec; const volumeYaml = yaml.load( fs.readFileSync(path.resolve("lib/k8s/configs/server-pvc.yml"), "utf8") ); volumeYaml.metadata.labels.service = `mcl-${name}-server`; volumeYaml.metadata.name = `mcl-${name}-volume`; volumeYaml.metadata.namespace = namespace; volumeYaml.spec.resources.requests.storage = "1Gi"; // TODO: Changeme return volumeYaml; } function createServerDeploy(serverSpec) { const { name, version, serverType, difficulty, gamemode, memory, motd, maxPlayers, seed, modpack, ops, whitelist, } = serverSpec; const deployYaml = yaml.load( fs.readFileSync( path.resolve("lib/k8s/configs/server-deployment.yml"), "utf8" ) ); deployYaml.metadata.name = `mcl-${name}`; deployYaml.metadata.namespace = namespace; deployYaml.spec.replicas = 0; // TODO: User control for autostart deployYaml.spec.selector.matchLabels.app = `mcl-${name}-app`; deployYaml.spec.template.metadata.labels.app = `mcl-${name}-app`; deployYaml.spec.template.spec.containers.splice(0, 1); //TODO: Currently removing backup container const serverContainer = deployYaml.spec.template.spec.containers[0]; // Enviornment variables serverContainer.env.find(({ name: n }) => n === "TYPE").value = serverType; serverContainer.env.find(({ name: n }) => n === "VERSION").value = version; serverContainer.env.find(({ name: n }) => n === "DIFFICULTY").value = difficulty; serverContainer.env.find(({ name: n }) => n === "MODE").value = gamemode; serverContainer.env.find(({ name: n }) => n === "MOTD").value = motd; serverContainer.env.find(({ name: n }) => n === "MAX_PLAYERS").value = maxPlayers; serverContainer.env.find(({ name: n }) => n === "SEED").value = seed; serverContainer.env.find(({ name: n }) => n === "OPS").value = ops; serverContainer.env.find(({ name: n }) => n === "WHITELIST").value = whitelist; serverContainer.env.find( ({ name: n }) => n === "MEMORY" ).value = `${memory}M`; if (version !== "VANILLA") delete serverContainer.env.find(({ name: n }) => n === "MODPACK").value; else serverContainer.env.find(({ name: n }) => n === "MODPACK").value = modpack; serverContainer.env.find( ({ name }) => name === "RCON_PASSWORD" ).valueFrom.secretKeyRef.name = `mcl-${name}-rcon-secret`; // Server Container Name serverContainer.name = `mcl-${name}`; // Resources serverContainer.resources.requests.memory = `${memory}Mi`; // serverContainer.resources.limits.memory = `${memory}Mi`; // TODO Allow for limits beyond initial startup // Volumes deployYaml.spec.template.spec.volumes.find( ({ name }) => name === "datadir" ).persistentVolumeClaim.claimName = `mcl-${name}-volume`; deployYaml.spec.template.spec.containers[0] = serverContainer; return deployYaml; } function createServerService(serverSpec) { const { name, url } = serverSpec; const serviceYaml = yaml.load( fs.readFileSync(path.resolve("lib/k8s/configs/server-svc.yml"), "utf8") ); 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.spec.selector.app = `mcl-${name}-app`; return serviceYaml; } function createRconService(serverSpec) { const { name, url } = serverSpec; const rconSvcYaml = yaml.load( fs.readFileSync(path.resolve("lib/k8s/configs/rcon-svc.yml"), "utf8") ); rconSvcYaml.metadata.labels.app = `mcl-${name}-app`; rconSvcYaml.metadata.name = `mcl-${name}-rcon`; rconSvcYaml.metadata.namespace = namespace; rconSvcYaml.spec.selector.app = `mcl-${name}-app`; return rconSvcYaml; } export default async function createServer(req, res) { if (payloadFilter(req, res) !== "filtered") return; const serverSpec = req.body; const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); const deployments = deploymentRes.body.items.map((i) => i.metadata.name); if (deployments.includes(`mcl-${serverSpec.name}`)) return res.status(409).send("Server already exists!"); const pvcRes = await k8sCore.listNamespacedPersistentVolumeClaim(namespace); const pvcs = pvcRes.body.items.map((i) => i.metadata.name); if (pvcs.includes(`mcl-${serverSpec.name}-volume`)) return res.status(409).send("Server PVC already exists!"); 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); res.sendStatus(200); }