[FEATURE] Server service containerization
This commit is contained in:
parent
22bf905415
commit
12d198456c
9 changed files with 165 additions and 91 deletions
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
34
lib/database/queries/server-queries.js
Normal file
34
lib/database/queries/server-queries.js
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
67
lib/k8s/server-containers.js
Normal file
67
lib/k8s/server-containers.js
Normal 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;
|
||||||
|
}
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue