From 62c966a6bd7887ae88bd110fc64e399abbc32715 Mon Sep 17 00:00:00 2001 From: Dunemask Date: Sun, 17 Dec 2023 12:25:14 -0700 Subject: [PATCH] [FEATURE] Adjust server control --- .../configs/containers/minecraft-server.yml | 3 +- lib/k8s/configs/server-deployment.yml | 2 + lib/k8s/k8s-server-control.js | 26 +++++-- lib/k8s/live-logging.js | 11 ++- lib/k8s/server-control.js | 1 + lib/k8s/server-create.js | 77 ++++++++++++------- lib/k8s/server-files.js | 38 +++++---- lib/server/rcon.js | 8 +- 8 files changed, 109 insertions(+), 57 deletions(-) diff --git a/lib/k8s/configs/containers/minecraft-server.yml b/lib/k8s/configs/containers/minecraft-server.yml index 26b01d2..09bcb90 100644 --- a/lib/k8s/configs/containers/minecraft-server.yml +++ b/lib/k8s/configs/containers/minecraft-server.yml @@ -52,7 +52,8 @@ env: - name: GENERATOR_SETTINGS - name: LEVEL value: world - - name: MODPACK + # - name: MODPACK + # value: https://somemodpack.com - name: ONLINE_MODE value: "true" - name: MEMORY diff --git a/lib/k8s/configs/server-deployment.yml b/lib/k8s/configs/server-deployment.yml index 1607944..0d5d335 100644 --- a/lib/k8s/configs/server-deployment.yml +++ b/lib/k8s/configs/server-deployment.yml @@ -16,6 +16,8 @@ spec: type: Recreate template: metadata: + annotations: + minecluster.dunemask.net/server-name: changeme-server-name labels: app: changeme-app spec: diff --git a/lib/k8s/k8s-server-control.js b/lib/k8s/k8s-server-control.js index 7d82bb0..978fe9e 100644 --- a/lib/k8s/k8s-server-control.js +++ b/lib/k8s/k8s-server-control.js @@ -1,5 +1,5 @@ import k8s from "@kubernetes/client-node"; -import { VERB } from "../util/logging.js"; +import { VERB, ERR } from "../util/logging.js"; const kc = new k8s.KubeConfig(); kc.loadFromDefault(); @@ -13,7 +13,7 @@ const mineclusterManaged = (o) => o.metadata.annotations && o.metadata.annotations["minecluster.dunemask.net/server-name"] !== undefined; -const serverMatch = (serverName) => (o) => +export const serverMatch = (serverName) => (o) => o.metadata.annotations["minecluster.dunemask.net/server-name"] === serverName; export async function getDeployments() { @@ -72,7 +72,7 @@ export function getServerAssets(serverName) { for (var k in serverAssets) if (serverAssets[k]) return serverAssets; // If no assets exist, return nothing }) - .catch((e) => console.log(e)); + .catch((e) => ERR("SERVER ASSETS", e)); } export async function getDeployment(serverName) { @@ -82,13 +82,26 @@ export async function getDeployment(serverName) { s.metadata.annotations["minecluster.dunemask.net/server-name"] === serverName, ); - if (!serverDeployment) { - console.log(servers.map((s) => s.metadata.annotations)); + if (!serverDeployment) throw Error(`MCL Deployment '${serverName}' could not be found!`); - } + return serverDeployment; } +export async function toggleServer(serverName, scaleUp = false) { + const deployment = await getDeployment(serverName); + const { containers } = deployment.spec.template.spec; + const ftpContainer = containers.find((c) => c.name.endsWith("-ftp")); + + res.sendStatus(200); + deployment.spec.template.spec.containers = containers; + return k8sDeps.replaceNamespacedDeployment( + deployment.metadata.name, + namespace, + deployment, + ); +} + export async function scaleDeployment(serverName, scaleUp = false) { const deployment = await getDeployment(serverName); if (deployment.spec.replicas === 1 && scaleUp) @@ -97,6 +110,7 @@ export async function scaleDeployment(serverName, scaleUp = false) { `MCL Deployment '${serverName}' is already scaled! Ignoring scale adjustment.`, ); deployment.spec.replicas = scaleUp ? 1 : 0; + return k8sDeps.replaceNamespacedDeployment( deployment.metadata.name, namespace, diff --git a/lib/k8s/live-logging.js b/lib/k8s/live-logging.js index 93cc273..8a21f58 100644 --- a/lib/k8s/live-logging.js +++ b/lib/k8s/live-logging.js @@ -5,15 +5,18 @@ import { ERR } from "../util/logging.js"; const kc = new k8s.KubeConfig(); kc.loadFromDefault(); const k8sApi = kc.makeApiClient(k8s.CoreV1Api); + export default async function liveLogging(socket, serverNamespace) { - const containerName = `mcl-${socket.mcs.serverName}`; + const { serverName } = socket.mcs; + const podName = `mcl-${serverName}`; + const containerName = `${podName}-server`; const podResponse = await k8sApi.listNamespacedPod(serverNamespace); const pods = podResponse.body.items.map((vp1) => vp1.metadata.name); - const mcsPods = pods.filter((p) => p.startsWith(containerName)); + const mcsPods = pods.filter((p) => p.startsWith(podName)); if (mcsPods.length === 0) - throw Error(`Could not find a pod that starts with ${containerName}`); + throw Error(`Could not find a pod that starts with ${podName}`); if (mcsPods.length > 1) - throw Error(`Multiple pods match the name ${containerName}`); + throw Error(`Multiple pods match the name ${podName}`); const log = new k8s.Log(kc); const logStream = new stream.PassThrough(); diff --git a/lib/k8s/server-control.js b/lib/k8s/server-control.js index 8e14c64..558c77d 100644 --- a/lib/k8s/server-control.js +++ b/lib/k8s/server-control.js @@ -10,6 +10,7 @@ const kc = new k8s.KubeConfig(); kc.loadFromDefault(); const k8sMetrics = new k8s.Metrics(kc); +const k8sDeps = kc.makeApiClient(k8s.AppsV1Api); const namespace = process.env.MCL_SERVER_NAMESPACE; // Gets the all assets for the server diff --git a/lib/k8s/server-create.js b/lib/k8s/server-create.js index dd7cbbc..0486e92 100644 --- a/lib/k8s/server-create.js +++ b/lib/k8s/server-create.js @@ -55,8 +55,10 @@ function createServerVolume(serverSpec) { return volumeYaml; } -function getFtpContainer() { +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" }, @@ -71,7 +73,7 @@ function getFtpContainer() { return ftpContainer; } -function createServerDeploy(serverSpec) { +function getServerContainer(serverSpec) { const { name, version, @@ -86,24 +88,16 @@ function createServerDeploy(serverSpec) { ops, whitelist, } = serverSpec; - const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml"); - const serverContainer = loadYaml( - "lib/k8s/configs/containers/minecraft-server.yml", - ); - const backupContainer = loadYaml( - "lib/k8s/configs/containers/minecraft-backup.yml", - ); - const ftpContainer = getFtpContainer(); - deployYaml.metadata.name = `mcl-${name}`; + 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); - deployYaml.metadata.namespace = namespace; - deployYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = - name; - 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`; - const findEnv = (k) => serverContainer.env.find(({ name: n }) => n === k); - const updateEnv = (k, v) => (findEnv.value = v); // Enviornment variables updateEnv("TYPE", serverType); updateEnv("VERSION", version); @@ -116,22 +110,49 @@ function createServerDeploy(serverSpec) { updateEnv("WHITELIST", whitelist); updateEnv("MEMORY", `${memory}M`); - if (version !== "VANILLA") delete findEnv("MODPACK").value; - else updateEnv("MODPACK", modpack); - findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = - `mcl-${name}-rcon-secret`; + // RCON + const rs = `mcl-${name}-rcon-secret`; + findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs; + // Mods // TODO: remove these once files are managable + /*if (version !== "VANILLA") delete findEnv("MODPACK").value; + else updateEnv("MODPACK", modpack);*/ + + 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`; - // 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`; + + // 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; } diff --git a/lib/k8s/server-files.js b/lib/k8s/server-files.js index 077158f..1e1b13b 100644 --- a/lib/k8s/server-files.js +++ b/lib/k8s/server-files.js @@ -4,6 +4,22 @@ import { getServerAssets } from "./k8s-server-control.js"; const namespace = process.env.MCL_SERVER_NAMESPACE; +export async function useFtp(serverService) { + const { name } = serverService.metadata; + const client = new ftp.Client(); + await client.access({ + host: `${name}.${namespace}.svc.cluster.local`, + user: "minecluster", + password: "minecluster", + }); + return client; +} + +const handleError = (res) => (e) => { + ERR("SERVER FILES", "Error occurred while preforming FTP operation!", e); + res.status(500).send("Error occurred while performing FTP operation!"); +}; + export async function listFiles(req, res) { const serverSpec = req.body; if (!serverSpec) return res.sendStatus(400); @@ -16,20 +32,12 @@ export async function listFiles(req, res) { return res .status(409) .send("Service doesn't exist, please contact your hosting provider!"); - const client = new ftp.Client(0); - client.ftp.verbose = true; - try { - await client.access({ - host: `${server.service.metadata.name}.${namespace}.svc.cluster.local`, - user: "minecluster", - password: "minecluster", - }); - const files = await client.list(); - res.json(files); - } catch (err) { - console.log(err); - ERR("SERVER FILES", "Error loading client files:"); - res.status(500).send(err); - } + // client.ftp.verbose = true; + const client = await useFtp(server.service).catch(handleError(res)); + if (!client) return; + await client + .list() + .then((f) => res.json(f)) + .catch(handleError(res)); client.close(); } diff --git a/lib/server/rcon.js b/lib/server/rcon.js index 81cf46a..82a3def 100644 --- a/lib/server/rcon.js +++ b/lib/server/rcon.js @@ -1,6 +1,6 @@ import k8s from "@kubernetes/client-node"; import { Rcon as RconClient } from "rcon-client"; -import { ERR } from "../util/logging.js"; +import { ERR, WARN } from "../util/logging.js"; const kc = new k8s.KubeConfig(); kc.loadFromDefault(); const k8sCore = kc.makeApiClient(k8s.CoreV1Api); @@ -15,7 +15,8 @@ export default async function rconInterface(socket) { rconRes.body.data["rcon-password"], "base64", ).toString("utf8"); - const rconHost = `mcl-${socket.mcs.serverName}-rcon`; + const { serverName } = socket.mcs; + const rconHost = `mcl-${serverName}-rcon.${namespace}.svc.cluster.local`; const rcon = new RconClient({ host: rconHost, port: 25575, @@ -25,7 +26,8 @@ export default async function rconInterface(socket) { try { await rcon.connect(); } catch (error) { - ERR("RCON", `Could not connect to 'mcl-${socket.mcs.serverName}-rcon'`); + socket.emit("push", "Could not connect RCON Input to server!"); + WARN("RCON", `Could not connect to '${rconHost}'`); } socket.rconClient = rcon; }