import k8s from "@kubernetes/client-node"; import yaml from "js-yaml"; import { VERB, ERR } from "../util/logging.js"; import { getServerEntry } from "../database/queries/server-queries.js"; import { getFtpContainer, getCoreServerContainer, getBackupContainer, } from "./server-containers.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")); const mineclusterManaged = (o) => o.metadata && o.metadata.annotations && o.metadata.annotations["minecluster.dunemask.net/id"] !== undefined; export const serverMatch = (serverId) => (o) => o.metadata.annotations["minecluster.dunemask.net/id"] === serverId; export async function getDeployments() { const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); const serverDeployments = deploymentRes.body.items.filter(mineclusterManaged); return serverDeployments; } export async function getServices() { const serviceRes = await k8sCore.listNamespacedService(namespace); const serverServices = serviceRes.body.items.filter(mineclusterManaged); return serverServices; } export async function getSecrets() { const secretRes = await k8sCore.listNamespacedSecret(namespace); const serverSecrets = secretRes.body.items.filter(mineclusterManaged); return serverSecrets; } export async function getVolumes() { const volumeRes = await k8sCore.listNamespacedPersistentVolumeClaim(namespace); const serverVolumes = volumeRes.body.items.filter(mineclusterManaged); return serverVolumes; } export function getServerAssets(serverId) { const serverFilter = serverMatch(serverId); return Promise.all([ getDeployments(), getServices(), getSecrets(), getVolumes(), ]) .then(([deps, svcs, scrts, vols]) => { const deployments = deps.filter(serverFilter); const services = svcs.filter(serverFilter); const secrets = scrts.filter(serverFilter); const volumes = vols.filter(serverFilter); if (deployments.length > 1) throw Error("Deployment filter broken!"); if (volumes.length > 1) throw Error("Volume filter broken!"); if (secrets.length > 2) throw Error("Secrets broken!"); const serverAssets = { deployment: deployments[0], service: services.find((s) => s.metadata.name.endsWith("-server")), volume: volumes[0], rconService: services.find((s) => s.metadata.name.endsWith("-rcon")), rconSecret: secrets.find((s) => s.metadata.name.endsWith("-rcon-secret"), ), backupSecret: secrets.find((s) => s.metadata.name.endsWith("-backup-secret"), ), extraService: services.find((s) => s.metadata.name.endsWith("-extra")), }; for (var k in serverAssets) if (serverAssets[k]) return serverAssets; // If no assets exist, return nothing }) .catch((e) => ERR("SERVER ASSETS", e)); } export async function getDeployment(serverId) { const servers = await getDeployments(); const serverDeployment = servers.find( (s) => s.metadata.annotations["minecluster.dunemask.net/id"] === serverId, ); if (!serverDeployment) throw Error(`MCL Deployment with ID '${serverId}' could not be found!`); return serverDeployment; } export async function getContainers(serverId) { const deployment = await getDeployment(serverId); return deployment.spec.template.spec.containers; } async function containerControl(serverSpec, deployment, scaleUp) { const { containers } = deployment.spec.template.spec; const depFtp = containers.find((c) => c.name.endsWith("-ftp")); const depServer = containers.find((c) => c.name.endsWith("-server")); const depBackup = containers.find((c) => c.name.endsWith("-backup")); const ftpContainer = depFtp ?? getFtpContainer(serverSpec); const serverContainer = depServer ?? getCoreServerContainer(serverSpec); const backupContainer = depBackup ?? getBackupContainer(serverSpec); if (scaleUp && serverSpec.backupEnabled) return [ftpContainer, serverContainer, backupContainer]; else if (scaleUp) return [ftpContainer, serverContainer]; return [ftpContainer]; } export function terminationControl(containers) { return containers.length > 1 ? 30 /*seconds*/ : 1 /*seconds */; } export async function toggleServer(serverId, scaleUp = false) { const [deployment, serverSpec] = await Promise.all([ getDeployment(serverId), getServerEntry(serverId), ]); const containers = await containerControl(serverSpec, deployment, scaleUp); const ts = terminationControl(containers); // Speed up container termination if not running a server deployment.spec.template.spec.terminationGracePeriodSeconds = ts; deployment.spec.template.spec.containers = containers; return k8sDeps.replaceNamespacedDeployment( deployment.metadata.name, namespace, deployment, ); }