[FEATURE] Server service containerization

This commit is contained in:
Dunemask 2023-12-19 11:39:01 -07:00
parent 22bf905415
commit 12d198456c
9 changed files with 165 additions and 91 deletions

View file

@ -1,10 +1,12 @@
import createServerResources from "../k8s/server-create.js"; import createServerResources from "../k8s/server-create.js";
import deleteServerResources from "../k8s/server-delete.js"; import deleteServerResources from "../k8s/server-delete.js";
import { createServerEntry } from "../database/queries/server-queries.js";
import { sendError } from "../util/ExpressClientError.js"; import { sendError } from "../util/ExpressClientError.js";
import { import {
startServerContainer, startServerContainer,
stopServerContainer, stopServerContainer,
} from "../k8s/server-control.js"; } from "../k8s/server-control.js";
import { toggleServer } from "../k8s/k8s-server-control.js";
function payloadFilter(req, res) { function payloadFilter(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
@ -32,8 +34,10 @@ function checkServerName(serverSpec) {
export function createServer(req, res) { export function createServer(req, res) {
if (payloadFilter(req, res) !== "filtered") return; if (payloadFilter(req, res) !== "filtered") return;
const serverSpec = req.body; const serverSpec = req.body;
createServerResources(serverSpec) createServerEntry(serverSpec)
.then(() => res.sendStatus(200)) .then(() =>
createServerResources(serverSpec).then(() => res.sendStatus(200)),
)
.catch(sendError(res)); .catch(sendError(res));
} }
@ -59,7 +63,8 @@ export async function startServer(req, res) {
} catch (e) { } catch (e) {
return sendError(res)(e); return sendError(res)(e);
} }
startServerContainer(serverSpec) const { name } = serverSpec;
toggleServer(name, true)
.then(() => res.sendStatus(200)) .then(() => res.sendStatus(200))
.catch(sendError(res)); .catch(sendError(res));
} }
@ -72,7 +77,8 @@ export async function stopServer(req, res) {
} catch (e) { } catch (e) {
return sendError(res)(e); return sendError(res)(e);
} }
stopServerContainer(serverSpec) const { name } = serverSpec;
toggleServer(name, false)
.then(() => res.sendStatus(200)) .then(() => res.sendStatus(200))
.catch(sendError(res)); .catch(sendError(res));
} }

View file

@ -1,8 +1,12 @@
/*CREATE SEQUENCE servers_id_seq; CREATE SEQUENCE servers_id_seq;
CREATE TABLE servers ( CREATE TABLE servers (
id bigint NOT NULL DEFAULT nextval('servers_id_seq') PRIMARY KEY, id bigint NOT NULL DEFAULT nextval('servers_id_seq') PRIMARY KEY,
name varchar(255) DEFAULT NULL, name varchar(255) DEFAULT NULL,
host 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) CONSTRAINT unique_host UNIQUE(host)
); );
ALTER SEQUENCE servers_id_seq OWNED BY servers.id;*/ ALTER SEQUENCE servers_id_seq OWNED BY servers.id;

View file

@ -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);
}
}

View file

@ -25,8 +25,8 @@ readinessProbe:
timeoutSeconds: 1 timeoutSeconds: 1
resources: resources:
requests: requests:
cpu: 500m cpu: 50m
memory: 512Mi memory: 64Mi
stdin: true stdin: true
terminationMessagePath: /dev/termination-log terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File terminationMessagePolicy: File

View file

@ -1,15 +1,23 @@
env: env:
# System Values
- name: JVM_OPTS
- name: JVM_XX_OPTS
- name: OVERRIDE_SERVER_PROPERTIES
value: "false"
- name: EULA - name: EULA
value: "TRUE" value: "TRUE"
# Updated at recreation
- name: MEMORY
value: 1024M
- name: TYPE - name: TYPE
value: VANILLA value: VANILLA
- name: VERSION - name: VERSION
value: "latest" value: "latest"
# Set at creation but not updated on recreation
- name: DIFFICULTY - name: DIFFICULTY
value: easy value: easy
- name: WHITELIST - name: WHITELIST
- name: OPS - name: OPS
- name: ICON
- name: MAX_PLAYERS - name: MAX_PLAYERS
value: "20" value: "20"
- name: MAX_WORLD_SIZE - name: MAX_WORLD_SIZE
@ -52,16 +60,8 @@ env:
- name: GENERATOR_SETTINGS - name: GENERATOR_SETTINGS
- name: LEVEL - name: LEVEL
value: world value: world
# - name: MODPACK
# value: https://somemodpack.com
- name: ONLINE_MODE - name: ONLINE_MODE
value: "true" value: "true"
- name: MEMORY
value: 1024M
- name: JVM_OPTS
- name: JVM_XX_OPTS
- name: OVERRIDE_SERVER_PROPERTIES
value: "true"
- name: ENABLE_RCON - name: ENABLE_RCON
value: "true" value: "true"
- name: RCON_PASSWORD - name: RCON_PASSWORD
@ -80,7 +80,7 @@ livenessProbe:
periodSeconds: 5 periodSeconds: 5
successThreshold: 1 successThreshold: 1
timeoutSeconds: 1 timeoutSeconds: 1
name: changeme-name name: changeme-name-server
ports: ports:
- containerPort: 25565 - containerPort: 25565
name: minecraft name: minecraft

View file

@ -1,5 +1,12 @@
import k8s from "@kubernetes/client-node"; import k8s from "@kubernetes/client-node";
import yaml from "js-yaml";
import { VERB, ERR } from "../util/logging.js"; 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(); const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
@ -8,6 +15,8 @@ const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
const namespace = process.env.MCL_SERVER_NAMESPACE; const namespace = process.env.MCL_SERVER_NAMESPACE;
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
const mineclusterManaged = (o) => const mineclusterManaged = (o) =>
o.metadata && o.metadata &&
o.metadata.annotations && o.metadata.annotations &&
@ -93,13 +102,26 @@ export async function getContainers(serverName) {
return deployment.spec.template.spec.containers; 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) { export async function toggleServer(serverName, scaleUp = false) {
const deployment = await getDeployment(serverName); const deployment = await getDeployment(serverName);
const { containers } = deployment.spec.template.spec; deployment.spec.template.spec.containers = await containerControl(
const ftpContainer = containers.find((c) => c.name.endsWith("-ftp")); serverName,
deployment,
res.sendStatus(200); scaleUp,
deployment.spec.template.spec.containers = containers; );
return k8sDeps.replaceNamespacedDeployment( return k8sDeps.replaceNamespacedDeployment(
deployment.metadata.name, deployment.metadata.name,
namespace, namespace,

View file

@ -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;
}

View file

@ -47,11 +47,13 @@ export async function getInstances() {
const serverInstances = serverDeployments.map((s) => { const serverInstances = serverDeployments.map((s) => {
name = s.metadata.annotations["minecluster.dunemask.net/server-name"]; name = s.metadata.annotations["minecluster.dunemask.net/server-name"];
metrics = null; 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 }) => { const pod = podMetricsResponse.items.find(({ metadata: md }) => {
return md.labels && md.labels.app && md.labels.app === `mcl-${name}-app`; return md.labels && md.labels.app && md.labels.app === `mcl-${name}-app`;
}); });
if (pod) { if (started && pod) {
const podCpus = pod.containers.map( const podCpus = pod.containers.map(
({ usage }) => parseInt(usage.cpu) / 1_000_000, ({ usage }) => parseInt(usage.cpu) / 1_000_000,
); );

View file

@ -5,6 +5,11 @@ import yaml from "js-yaml";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import ExpressClientError from "../util/ExpressClientError.js"; import ExpressClientError from "../util/ExpressClientError.js";
import {
getFtpContainer,
getServerContainer,
getBackupContainer,
} from "./server-containers.js";
const kc = new k8s.KubeConfig(); const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
@ -39,72 +44,6 @@ function createServerVolume(serverSpec) {
return volumeYaml; 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) { function createServerDeploy(serverSpec) {
const { name } = serverSpec; const { name } = serverSpec;
const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml"); const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml");