diff --git a/lib/controllers/lifecycle-controller.js b/lib/controllers/lifecycle-controller.js index 6984fc0..9931555 100644 --- a/lib/controllers/lifecycle-controller.js +++ b/lib/controllers/lifecycle-controller.js @@ -1,10 +1,12 @@ import createServerResources from "../k8s/server-create.js"; import deleteServerResources from "../k8s/server-delete.js"; +import { createServerEntry } from "../database/queries/server-queries.js"; import { sendError } from "../util/ExpressClientError.js"; import { startServerContainer, stopServerContainer, } from "../k8s/server-control.js"; +import { toggleServer } from "../k8s/k8s-server-control.js"; function payloadFilter(req, res) { const serverSpec = req.body; @@ -32,8 +34,10 @@ function checkServerName(serverSpec) { export function createServer(req, res) { if (payloadFilter(req, res) !== "filtered") return; const serverSpec = req.body; - createServerResources(serverSpec) - .then(() => res.sendStatus(200)) + createServerEntry(serverSpec) + .then(() => + createServerResources(serverSpec).then(() => res.sendStatus(200)), + ) .catch(sendError(res)); } @@ -59,7 +63,8 @@ export async function startServer(req, res) { } catch (e) { return sendError(res)(e); } - startServerContainer(serverSpec) + const { name } = serverSpec; + toggleServer(name, true) .then(() => res.sendStatus(200)) .catch(sendError(res)); } @@ -72,7 +77,8 @@ export async function stopServer(req, res) { } catch (e) { return sendError(res)(e); } - stopServerContainer(serverSpec) + const { name } = serverSpec; + toggleServer(name, false) .then(() => res.sendStatus(200)) .catch(sendError(res)); } diff --git a/lib/database/migrations/1_create_servers_table.sql b/lib/database/migrations/1_create_servers_table.sql index 375eafd..5306630 100644 --- a/lib/database/migrations/1_create_servers_table.sql +++ b/lib/database/migrations/1_create_servers_table.sql @@ -1,8 +1,12 @@ -/*CREATE SEQUENCE servers_id_seq; +CREATE SEQUENCE servers_id_seq; CREATE TABLE servers ( id bigint NOT NULL DEFAULT nextval('servers_id_seq') PRIMARY KEY, name varchar(255) DEFAULT NULL, host varchar(255) DEFAULT NULL, + version varchar(63) DEFAULT 'latest', + server_type varchar(63) DEFAULT 'VANILLA', + memory varchar(63) DEFAULT '512', + CONSTRAINT unique_name UNIQUE(name), CONSTRAINT unique_host UNIQUE(host) ); -ALTER SEQUENCE servers_id_seq OWNED BY servers.id;*/ \ No newline at end of file +ALTER SEQUENCE servers_id_seq OWNED BY servers.id; \ No newline at end of file diff --git a/lib/database/queries/server-queries.js b/lib/database/queries/server-queries.js new file mode 100644 index 0000000..239b8a8 --- /dev/null +++ b/lib/database/queries/server-queries.js @@ -0,0 +1,34 @@ +import pg from "../postgres.js"; +import { insertQuery, selectWhereQuery } from "../pg-query.js"; +import ExpressClientError from "../../util/ExpressClientError.js"; +const table = "servers"; + +const asExpressClientError = (e) => { + throw new ExpressClientError({ m: e.message, c: 409 }); +}; + +export async function createServerEntry(serverSpec) { + const { name, host, version, serverType: server_type, memory } = serverSpec; + const q = insertQuery(table, { name, host, version, server_type, memory }); + return pg.query(q).catch(asExpressClientError); +} + +export async function getServerEntry(serverName) { + if (!serverName) asExpressClientError({ message: "Server Name Required!" }); + const q = selectWhereQuery(table, { name: serverName }); + try { + const serverSpecs = await pg.query(q); + if (!serverSpecs.length === 1) + throw Error("Multiple servers found with the same name!"); + const { + name, + host, + version, + server_type: serverType, + memory, + } = serverSpecs[0]; + return { name, host, version, serverType, memory }; + } catch (e) { + asExpressClientError(e); + } +} diff --git a/lib/k8s/configs/containers/ftp-server.yml b/lib/k8s/configs/containers/ftp-server.yml index de6f49e..759bc20 100644 --- a/lib/k8s/configs/containers/ftp-server.yml +++ b/lib/k8s/configs/containers/ftp-server.yml @@ -25,8 +25,8 @@ readinessProbe: timeoutSeconds: 1 resources: requests: - cpu: 500m - memory: 512Mi + cpu: 50m + memory: 64Mi stdin: true terminationMessagePath: /dev/termination-log terminationMessagePolicy: File diff --git a/lib/k8s/configs/containers/minecraft-server.yml b/lib/k8s/configs/containers/minecraft-server.yml index 09bcb90..4034e36 100644 --- a/lib/k8s/configs/containers/minecraft-server.yml +++ b/lib/k8s/configs/containers/minecraft-server.yml @@ -1,15 +1,23 @@ env: + # System Values + - name: JVM_OPTS + - name: JVM_XX_OPTS + - name: OVERRIDE_SERVER_PROPERTIES + value: "false" - name: EULA value: "TRUE" + # Updated at recreation + - name: MEMORY + value: 1024M - name: TYPE value: VANILLA - name: VERSION value: "latest" + # Set at creation but not updated on recreation - name: DIFFICULTY value: easy - name: WHITELIST - name: OPS - - name: ICON - name: MAX_PLAYERS value: "20" - name: MAX_WORLD_SIZE @@ -52,16 +60,8 @@ env: - name: GENERATOR_SETTINGS - name: LEVEL value: world - # - name: MODPACK - # value: https://somemodpack.com - name: ONLINE_MODE value: "true" - - name: MEMORY - value: 1024M - - name: JVM_OPTS - - name: JVM_XX_OPTS - - name: OVERRIDE_SERVER_PROPERTIES - value: "true" - name: ENABLE_RCON value: "true" - name: RCON_PASSWORD @@ -80,7 +80,7 @@ livenessProbe: periodSeconds: 5 successThreshold: 1 timeoutSeconds: 1 -name: changeme-name +name: changeme-name-server ports: - containerPort: 25565 name: minecraft diff --git a/lib/k8s/k8s-server-control.js b/lib/k8s/k8s-server-control.js index fc45560..4e5233d 100644 --- a/lib/k8s/k8s-server-control.js +++ b/lib/k8s/k8s-server-control.js @@ -1,5 +1,12 @@ 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(); @@ -8,6 +15,8 @@ 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 && @@ -93,13 +102,26 @@ export async function getContainers(serverName) { return deployment.spec.template.spec.containers; } +async function containerControl(serverName, 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 serverSpec = await getServerEntry(serverName); + const ftpContainer = depFtp ?? getFtpContainer(serverSpec); + const serverContainer = depServer ?? getCoreServerContainer(serverSpec); + const backupContainer = depBackup ?? getBackupContainer(serverSpec); + if (scaleUp) return [ftpContainer, serverContainer]; + return [ftpContainer]; +} + 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; + deployment.spec.template.spec.containers = await containerControl( + serverName, + deployment, + scaleUp, + ); return k8sDeps.replaceNamespacedDeployment( deployment.metadata.name, namespace, diff --git a/lib/k8s/server-containers.js b/lib/k8s/server-containers.js new file mode 100644 index 0000000..4694828 --- /dev/null +++ b/lib/k8s/server-containers.js @@ -0,0 +1,67 @@ +import fs from "node:fs"; +import path from "node:path"; +import yaml from "js-yaml"; +const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8")); + +export 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; +} + +export function getCoreServerContainer(serverSpec) { + const { name, version, serverType, memory } = serverSpec; + const container = loadYaml("lib/k8s/configs/containers/minecraft-server.yml"); + // Container Updates + container.name = `mcl-${name}-server`; + container.resources.requests.memory = `${memory}Mi`; + + 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("MEMORY", `${memory}M`); + // RCON + const rs = `mcl-${name}-rcon-secret`; + findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs; + return container; +} + +export function getServerContainer(serverSpec) { + const { difficulty, gamemode, motd, maxPlayers, seed, ops, whitelist } = + serverSpec; + const container = getCoreServerContainer(serverSpec); + + const findEnv = (k) => container.env.find(({ name: n }) => n === k); + const updateEnv = (k, v) => (findEnv(k).value = v); + + // Enviornment variables + updateEnv("DIFFICULTY", difficulty); + updateEnv("MODE", gamemode); + updateEnv("MOTD", motd); + updateEnv("MAX_PLAYERS", maxPlayers); + updateEnv("SEED", seed); + updateEnv("OPS", ops); + updateEnv("WHITELIST", whitelist); + + return container; +} + +export function getBackupContainer(serverSpec) { + const container = loadYaml("lib/k8s/configs/containers/minecraft-backup.yml"); + return container; +} diff --git a/lib/k8s/server-control.js b/lib/k8s/server-control.js index f1d868e..63097e6 100644 --- a/lib/k8s/server-control.js +++ b/lib/k8s/server-control.js @@ -47,11 +47,13 @@ export async function getInstances() { const serverInstances = serverDeployments.map((s) => { name = s.metadata.annotations["minecluster.dunemask.net/server-name"]; metrics = null; - started = !!s.spec.replicas; + started = !!s.spec.template.spec.containers.find((c) => + c.name.includes(`mcl-${name}-server`), + ); const pod = podMetricsResponse.items.find(({ metadata: md }) => { return md.labels && md.labels.app && md.labels.app === `mcl-${name}-app`; }); - if (pod) { + if (started && pod) { const podCpus = pod.containers.map( ({ usage }) => parseInt(usage.cpu) / 1_000_000, ); diff --git a/lib/k8s/server-create.js b/lib/k8s/server-create.js index 159811b..d929f1c 100644 --- a/lib/k8s/server-create.js +++ b/lib/k8s/server-create.js @@ -5,6 +5,11 @@ import yaml from "js-yaml"; import fs from "node:fs"; import path from "node:path"; import ExpressClientError from "../util/ExpressClientError.js"; +import { + getFtpContainer, + getServerContainer, + getBackupContainer, +} from "./server-containers.js"; const kc = new k8s.KubeConfig(); kc.loadFromDefault(); @@ -39,72 +44,6 @@ function createServerVolume(serverSpec) { 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");