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 { getFtpContainer, getServerContainer, getBackupContainer, } from "./server-containers.js"; import kc from "./k8s-config.js"; 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 createExtraService(serverSpec) { const { mclName, id, extraPorts } = serverSpec; if (!extraPorts) return; const serviceYaml = loadYaml("lib/k8s/configs/extra-svc.yml"); serviceYaml.metadata.labels.app = `mcl-${mclName}-app`; serviceYaml.metadata.name = `mcl-${mclName}-extra`; serviceYaml.metadata.namespace = namespace; serviceYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; serviceYaml.spec.selector.app = `mcl-${mclName}-app`; // Port List: const portList = extraPorts.map((p) => ({ port: parseInt(p), name: `mcl-extra-${p}`, })); const tcpPorts = portList.map(({ port, name }) => ({ port, name: `${name}-tcp`, protocol: "TCP", targetPort: port, })); const udpPorts = portList.map(({ port, name }) => ({ port, name: `${name}-udp`, protocol: "UDP", targetPort: port, })); serviceYaml.spec.ports = [...tcpPorts, ...udpPorts]; return serviceYaml; } function createBackupSecret(serverSpec) { if (!serverSpec.backupEnabled) return; // If backup not defined, don't create RCLONE secret const { mclName, id, backupId, backupKey, backupHost } = serverSpec; const backupYaml = loadYaml("lib/k8s/configs/backup-secret.yml"); backupYaml.metadata.labels.app = `mcl-${mclName}-app`; backupYaml.metadata.name = `mcl-${mclName}-backup-secret`; backupYaml.metadata.namespace = namespace; backupYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; const rcloneConfig = [ `[${mclName}-backup]`, "type = s3", "provider = Minio", "env_auth = false", `access_key_id = ${backupId}`, `secret_access_key = ${backupKey}`, `endpoint = ${backupHost}`, `acl = private`, ].join("\n"); backupYaml.data["rclone.conf"] = Buffer.from(rcloneConfig).toString("base64"); return backupYaml; } function createRconSecret(serverSpec) { const { mclName, id } = 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-${mclName}-app`; rconYaml.metadata.name = `mcl-${mclName}-rcon-secret`; rconYaml.metadata.namespace = namespace; rconYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; return rconYaml; } function createServerVolume(serverSpec) { const { mclName, id, storage } = serverSpec; if (!storage) return; const volumeYaml = loadYaml("lib/k8s/configs/server-pvc.yml"); volumeYaml.metadata.labels.service = `mcl-${mclName}-server`; volumeYaml.metadata.name = `mcl-${mclName}-volume`; volumeYaml.metadata.namespace = namespace; volumeYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; volumeYaml.spec.resources.requests.storage = `${storage}Gi`; return volumeYaml; } function createServerDeploy(serverSpec) { const { mclName, id, backupEnabled, storage } = 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-${mclName}`; metadata.namespace = namespace; metadata.annotations["minecluster.dunemask.net/id"] = id; deployYaml.metadata = metadata; deployYaml.spec.template.spec.terminationGracePeriodSeconds = 1; // Configure Lables & Selectors deployYaml.spec.selector.matchLabels.app = `mcl-${mclName}-app`; deployYaml.spec.template.metadata.labels.app = `mcl-${mclName}-app`; deployYaml.spec.template.metadata.annotations["minecluster.dunemask.net/id"] = id; // Volumes if (!!storage) { const dvi = deployYaml.spec.template.spec.volumes.findIndex( ({ name }) => name === "datadir", ); delete deployYaml.spec.template.spec.volumes[dvi].emptyDir; deployYaml.spec.template.spec.volumes[dvi] = { ...deployYaml.spec.template.spec.volumes[dvi], persistentVolumeClaim: { claimName: `mcl-${mclName}-volume`, }, }; } // Backups if (backupEnabled) { deployYaml.spec.template.spec.volumes.push({ name: "rclone-config", secret: { defaultMode: 420, items: [{ key: "rclone.conf", path: "rclone.conf" }], secretName: `mcl-${mclName}-backup-secret`, }, }); } // Apply Containers TODO: User control for autostart // deployYaml.spec.template.spec.containers.push(serverContainer); deployYaml.spec.template.spec.containers.push(ftpContainer); deployYaml.spec.replicas = 1; return deployYaml; } function createServerService(serverSpec) { const { mclName, host, id } = serverSpec; const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml"); serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = host; serviceYaml.metadata.annotations["mc-router.itzg.me/externalServerName"] = host; serviceYaml.metadata.labels.app = `mcl-${mclName}-app`; serviceYaml.metadata.name = `mcl-${mclName}-server`; serviceYaml.metadata.namespace = namespace; serviceYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; serviceYaml.spec.selector.app = `mcl-${mclName}-app`; // Port List: const serverPortList = [{ p: 25565, n: "minecraft" }]; // 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}` }); const portList = [...serverPortList, ...ftpPortList]; serviceYaml.spec.ports = portList.map(({ p: port, n: name }) => ({ port, name, protocol: "TCP", targetPort: port, })); return serviceYaml; } function createRconService(createSpec) { const { id, mclName } = createSpec; const rconSvcYaml = loadYaml("lib/k8s/configs/rcon-svc.yml"); rconSvcYaml.metadata.labels.app = `mcl-${mclName}-app`; rconSvcYaml.metadata.name = `mcl-${mclName}-rcon`; rconSvcYaml.metadata.namespace = namespace; rconSvcYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; rconSvcYaml.spec.selector.app = `mcl-${mclName}-app`; return rconSvcYaml; } export default async function createServerResources(createSpec) { const backupSecret = createBackupSecret(createSpec); const rconSecret = createRconSecret(createSpec); const serverVolume = createServerVolume(createSpec); const serverDeploy = createServerDeploy(createSpec); const serverService = createServerService(createSpec); const rconService = createRconService(createSpec); const extraService = createExtraService(createSpec); const serverResources = []; if (!!serverVolume) serverResources.push( k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume), ); if (!!extraService) serverResources.push( k8sCore.createNamespacedService(namespace, extraService), ); if (!!backupSecret) serverResources.push( k8sCore.createNamespacedSecret(namespace, backupSecret), ); serverResources.push(k8sCore.createNamespacedSecret(namespace, rconSecret)); serverResources.push( k8sCore.createNamespacedService(namespace, serverService), ); serverResources.push(k8sCore.createNamespacedService(namespace, rconService)); serverResources.push( k8sDeps.createNamespacedDeployment(namespace, serverDeploy), ); return await Promise.all(serverResources); }