[REV] Switch to use IDS over server names

This commit is contained in:
Dunemask 2023-12-22 14:45:49 -07:00
parent e94aca7c96
commit 91587f66b2
21 changed files with 196 additions and 221 deletions

View file

@ -10,7 +10,7 @@ import { sendError } from "../util/ExpressClientError.js";
export async function listFiles(req, res) { export async function listFiles(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
if (!serverSpec) return res.sendStatus(400); if (!serverSpec) return res.sendStatus(400);
if (!serverSpec.host) return res.status(400).send("Server name required!"); if (!serverSpec.id) return res.status(400).send("Server id missing!");
listServerFiles(serverSpec) listServerFiles(serverSpec)
.then((f) => { .then((f) => {
const fileData = f.map((fi, i) => ({ const fileData = f.map((fi, i) => ({
@ -29,7 +29,7 @@ export async function listFiles(req, res) {
export async function createFolder(req, res) { export async function createFolder(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
if (!serverSpec) return res.sendStatus(400); if (!serverSpec) return res.sendStatus(400);
if (!serverSpec.host) return res.status(400).send("Server name required!"); if (!serverSpec.id) return res.status(400).send("Server id missing!");
if (!serverSpec.path) return res.status(400).send("Path required!"); if (!serverSpec.path) return res.status(400).send("Path required!");
createServerFolder(serverSpec) createServerFolder(serverSpec)
.then(() => res.sendStatus(200)) .then(() => res.sendStatus(200))
@ -39,7 +39,7 @@ export async function createFolder(req, res) {
export async function deleteItem(req, res) { export async function deleteItem(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
if (!serverSpec) return res.sendStatus(400); if (!serverSpec) return res.sendStatus(400);
if (!serverSpec.host) return res.status(400).send("Server name required!"); if (!serverSpec.id) return res.status(400).send("Server id missing!");
if (!serverSpec.path) return res.status(400).send("Path required!"); if (!serverSpec.path) return res.status(400).send("Path required!");
if (serverSpec.isDir === undefined || serverSpec.isDir === null) if (serverSpec.isDir === undefined || serverSpec.isDir === null)
return res.status(400).send("IsDIr required!"); return res.status(400).send("IsDIr required!");
@ -50,7 +50,7 @@ export async function deleteItem(req, res) {
export async function uploadItem(req, res) { export async function uploadItem(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
if (!serverSpec.host) return res.status(400).send("Server name required!"); if (!serverSpec.id) return res.status(400).send("Server id missing!");
if (!serverSpec.path) return res.status(400).send("Path required!"); if (!serverSpec.path) return res.status(400).send("Path required!");
uploadServerItem(serverSpec, req.file) uploadServerItem(serverSpec, req.file)
.then(() => res.sendStatus(200)) .then(() => res.sendStatus(200))
@ -59,7 +59,7 @@ export async function uploadItem(req, res) {
export async function getItem(req, res) { export async function getItem(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
if (!serverSpec.host) return res.status(400).send("Server name required!"); if (!serverSpec.id) return res.status(400).send("Server id missing!");
if (!serverSpec.path) return res.status(400).send("Path required!"); if (!serverSpec.path) return res.status(400).send("Path required!");
getServerItem(serverSpec, res) getServerItem(serverSpec, res)
.then(({ ds, ftpTransfer }) => { .then(({ ds, ftpTransfer }) => {

View file

@ -8,33 +8,35 @@ import {
import { sendError } from "../util/ExpressClientError.js"; import { sendError } from "../util/ExpressClientError.js";
import { toggleServer } from "../k8s/k8s-server-control.js"; import { toggleServer } from "../k8s/k8s-server-control.js";
const dnsRegex = new RegExp(
`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`,
);
function payloadFilter(req, res) { function payloadFilter(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
if (!serverSpec) return res.sendStatus(400); if (!serverSpec) return res.sendStatus(400);
const { name, host, version, serverType, memory } = const { name, host, version, serverType, memory } = serverSpec;
serverSpec;
if (!name) return res.status(400).send("Server name is required!"); if (!name) return res.status(400).send("Server name is required!");
if (!host) return res.status(400).send("Server host is required!"); if (!host) return res.status(400).send("Server host is required!");
if (!dnsRegex.test(host)) return res.status(400).send("Hostname invalid!");
if (!version) return res.status(400).send("Server version is required!"); if (!version) return res.status(400).send("Server version is required!");
if (!serverType) return res.status(400).send("Server type is required!"); if (!serverType) return res.status(400).send("Server type is required!");
if (!memory) return res.status(400).send("Memory is required!"); if (!memory) return res.status(400).send("Memory is required!");
return "filtered"; return "filtered";
} }
function checkServerHost(serverSpec) { function checkServerId(serverSpec) {
if (!serverSpec) throw new ExpressClientError({ c: 400 }); if (!serverSpec) throw new ExpressClientError({ c: 400 });
if (!serverSpec.host) if (!serverSpec.id)
throw new ExpressClientError({ c: 400, m: "Server name required!" }); throw new ExpressClientError({ c: 400, m: "Server id missing!" });
} }
export async function createServer(req, res) { export async function createServer(req, res) {
if (payloadFilter(req, res) !== "filtered") return; if (payloadFilter(req, res) !== "filtered") return;
const serverSpec = req.body; const serverSpec = req.body;
try { try {
const serverSpecs = await getServerEntry(serverSpec.id); const serverEntry = await createServerEntry(serverSpec);
if (serverSpecs.length !== 0) throw Error("Server already exists in DB!"); await createServerResources(serverEntry);
await createServerResources(serverSpec);
await createServerEntry(serverSpec);
res.sendStatus(200); res.sendStatus(200);
} catch (e) { } catch (e) {
sendError(res)(e); sendError(res)(e);
@ -45,7 +47,7 @@ export async function deleteServer(req, res) {
// Ensure spec is safe // Ensure spec is safe
const serverSpec = req.body; const serverSpec = req.body;
try { try {
checkServerHost(serverSpec); checkServerId(serverSpec);
} catch (e) { } catch (e) {
return sendError(res)(e); return sendError(res)(e);
} }
@ -60,12 +62,12 @@ export async function startServer(req, res) {
// Ensure spec is safe // Ensure spec is safe
const serverSpec = req.body; const serverSpec = req.body;
try { try {
checkServerHost(serverSpec); checkServerId(serverSpec);
} catch (e) { } catch (e) {
return sendError(res)(e); return sendError(res)(e);
} }
const { name } = serverSpec; const { id } = serverSpec;
toggleServer(name, true) toggleServer(id, true)
.then(() => res.sendStatus(200)) .then(() => res.sendStatus(200))
.catch(sendError(res)); .catch(sendError(res));
} }
@ -74,12 +76,12 @@ export async function stopServer(req, res) {
// Ensure spec is safe // Ensure spec is safe
const serverSpec = req.body; const serverSpec = req.body;
try { try {
checkServerHost(serverSpec); checkServerId(serverSpec);
} catch (e) { } catch (e) {
return sendError(res)(e); return sendError(res)(e);
} }
const { name } = serverSpec; const { id } = serverSpec;
toggleServer(name, false) toggleServer(id, false)
.then(() => res.sendStatus(200)) .then(() => res.sendStatus(200))
.catch(sendError(res)); .catch(sendError(res));
} }

View file

@ -9,32 +9,47 @@ const asExpressClientError = (e) => {
export async function createServerEntry(serverSpec) { export async function createServerEntry(serverSpec) {
const { name, host, version, serverType: server_type, memory } = serverSpec; const { name, host, version, serverType: server_type, memory } = serverSpec;
const q = insertQuery(table, { name, host, version, server_type, memory }); var q = insertQuery(table, { name, host, version, server_type, memory });
q += "\n RETURNING *";
try {
const entries = await pg.query(q);
const {
id,
name,
host,
version,
server_type: serverType,
memory,
} = entries[0];
return { name, id, host, version, serverType, memory };
} catch (e) {
asExpressClientError(e);
}
}
export async function deleteServerEntry(serverId) {
if (!serverId) asExpressClientError({ message: "Server ID Required!" });
const q = deleteQuery(table, { id: serverId });
return pg.query(q).catch(asExpressClientError); return pg.query(q).catch(asExpressClientError);
} }
export async function deleteServerEntry(serverName) { export async function getServerEntry(serverId) {
if (!serverName) asExpressClientError({ message: "Server Name Required!" }); if (!serverId) asExpressClientError({ message: "Server ID Required!" });
const q = deleteQuery(table, { name: serverName }); const q = selectWhereQuery(table, { id: serverId });
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 { try {
const serverSpecs = await pg.query(q); const serverSpecs = await pg.query(q);
if (serverSpecs.length === 0) return []; if (serverSpecs.length === 0) return [];
if (!serverSpecs.length === 1) if (!serverSpecs.length === 1)
throw Error("Multiple servers found with the same name!"); throw Error("Multiple servers found with the same name!");
const { const {
id,
name, name,
host, host,
version, version,
server_type: serverType, server_type: serverType,
memory, memory,
} = serverSpecs[0]; } = serverSpecs[0];
return { name, host, version, serverType, memory }; return { name, id, host, version, serverType, memory };
} catch (e) { } catch (e) {
asExpressClientError(e); asExpressClientError(e);
} }

View file

@ -4,7 +4,7 @@ data:
kind: Secret kind: Secret
metadata: metadata:
annotations: annotations:
minecluster.dunemask.net/server-name: changeme-server-name minecluster.dunemask.net/id: changeme-server-id
labels: labels:
app: changeme-app-label app: changeme-app-label
name: changeme-rcon-secret name: changeme-rcon-secret

View file

@ -2,7 +2,7 @@ apiVersion: v1
kind: Service kind: Service
metadata: metadata:
annotations: annotations:
minecluster.dunemask.net/server-name: changeme-server-name minecluster.dunemask.net/id: changeme-server-id
labels: labels:
app: changeme-app app: changeme-app
name: changeme-rcon name: changeme-rcon

View file

@ -2,7 +2,7 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
annotations: annotations:
minecluster.dunemask.net/server-name: changeme-server-name minecluster.dunemask.net/id: changeme-server-id
name: changeme-name name: changeme-name
namespace: changeme-namespace namespace: changeme-namespace
spec: spec:
@ -17,7 +17,7 @@ spec:
template: template:
metadata: metadata:
annotations: annotations:
minecluster.dunemask.net/server-name: changeme-server-name minecluster.dunemask.net/id: changeme-server-id
labels: labels:
app: changeme-app app: changeme-app
spec: spec:

View file

@ -2,7 +2,7 @@ apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
annotations: annotations:
minecluster.dunemask.net/server-name: changeme-server-name minecluster.dunemask.net/id: changeme-server-id
labels: labels:
service: changeme-service-name service: changeme-service-name
name: changeme-pvc-name name: changeme-pvc-name

View file

@ -4,7 +4,7 @@ metadata:
annotations: annotations:
ingress.qumine.io/hostname: changeme-url ingress.qumine.io/hostname: changeme-url
ingress.qumine.io/portname: minecraft ingress.qumine.io/portname: minecraft
minecluster.dunemask.net/server-name: changeme-server-name minecluster.dunemask.net/id: changeme-server-id
labels: labels:
app: changeme-app app: changeme-app
name: changeme-name name: changeme-name

View file

@ -20,10 +20,10 @@ 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 &&
o.metadata.annotations["minecluster.dunemask.net/server-name"] !== undefined; o.metadata.annotations["minecluster.dunemask.net/id"] !== undefined;
export const serverMatch = (serverName) => (o) => export const serverMatch = (serverId) => (o) =>
o.metadata.annotations["minecluster.dunemask.net/server-name"] === serverName; o.metadata.annotations["minecluster.dunemask.net/id"] === serverId;
export async function getDeployments() { export async function getDeployments() {
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
@ -50,8 +50,9 @@ export async function getVolumes() {
return serverVolumes; return serverVolumes;
} }
export function getServerAssets(serverName) { export function getServerAssets(serverId) {
const serverFilter = serverMatch(serverName); const serverFilter = serverMatch(serverId);
console.log(serverId);
return Promise.all([ return Promise.all([
getDeployments(), getDeployments(),
getServices(), getServices(),
@ -69,13 +70,9 @@ export function getServerAssets(serverName) {
if (secrets.length > 1) throw Error("Secrets broken!"); if (secrets.length > 1) throw Error("Secrets broken!");
const serverAssets = { const serverAssets = {
deployment: deployments[0], deployment: deployments[0],
service: services.find( service: services.find((s) => s.metadata.name.endsWith("-server")),
(s) => s.metadata.name === `mcl-${serverName}-server`,
),
volume: volumes[0], volume: volumes[0],
rconService: services.find( rconService: services.find((s) => s.metadata.name.endsWith("-rcon")),
(s) => s.metadata.name === `mcl-${serverName}-rcon`,
),
rconSecret: secrets[0], rconSecret: secrets[0],
}; };
for (var k in serverAssets) if (serverAssets[k]) return serverAssets; for (var k in serverAssets) if (serverAssets[k]) return serverAssets;
@ -84,30 +81,29 @@ export function getServerAssets(serverName) {
.catch((e) => ERR("SERVER ASSETS", e)); .catch((e) => ERR("SERVER ASSETS", e));
} }
export async function getDeployment(serverName) { export async function getDeployment(serverId) {
const servers = await getDeployments(); const servers = await getDeployments();
console.log(servers.map(({ metadata }) => metadata.annotations));
const serverDeployment = servers.find( const serverDeployment = servers.find(
(s) => (s) => s.metadata.annotations["minecluster.dunemask.net/id"] === serverId,
s.metadata.annotations["minecluster.dunemask.net/server-name"] ===
serverName,
); );
if (!serverDeployment) if (!serverDeployment)
throw Error(`MCL Deployment '${serverName}' could not be found!`); throw Error(`MCL Deployment with ID '${serverId}' could not be found!`);
return serverDeployment; return serverDeployment;
} }
export async function getContainers(serverName) { export async function getContainers(serverId) {
const deployment = await getDeployment(serverName); const deployment = await getDeployment(serverId);
return deployment.spec.template.spec.containers; return deployment.spec.template.spec.containers;
} }
async function containerControl(serverName, deployment, scaleUp) { async function containerControl(serverId, deployment, scaleUp) {
const { containers } = deployment.spec.template.spec; const { containers } = deployment.spec.template.spec;
const depFtp = containers.find((c) => c.name.endsWith("-ftp")); const depFtp = containers.find((c) => c.name.endsWith("-ftp"));
const depServer = containers.find((c) => c.name.endsWith("-server")); const depServer = containers.find((c) => c.name.endsWith("-server"));
const depBackup = containers.find((c) => c.name.endsWith("-backup")); const depBackup = containers.find((c) => c.name.endsWith("-backup"));
const serverSpec = await getServerEntry(serverName); const serverSpec = await getServerEntry(serverId);
const ftpContainer = depFtp ?? getFtpContainer(serverSpec); const ftpContainer = depFtp ?? getFtpContainer(serverSpec);
const serverContainer = depServer ?? getCoreServerContainer(serverSpec); const serverContainer = depServer ?? getCoreServerContainer(serverSpec);
const backupContainer = depBackup ?? getBackupContainer(serverSpec); const backupContainer = depBackup ?? getBackupContainer(serverSpec);
@ -115,10 +111,10 @@ async function containerControl(serverName, deployment, scaleUp) {
return [ftpContainer]; return [ftpContainer];
} }
export async function toggleServer(serverName, scaleUp = false) { export async function toggleServer(serverId, scaleUp = false) {
const deployment = await getDeployment(serverName); const deployment = await getDeployment(serverId);
deployment.spec.template.spec.containers = await containerControl( deployment.spec.template.spec.containers = await containerControl(
serverName, serverId,
deployment, deployment,
scaleUp, scaleUp,
); );
@ -128,19 +124,3 @@ export async function toggleServer(serverName, scaleUp = false) {
deployment, deployment,
); );
} }
export async function scaleDeployment(serverName, scaleUp = false) {
const deployment = await getDeployment(serverName);
if (deployment.spec.replicas === 1 && scaleUp)
return VERB(
"KSC",
`MCL Deployment '${serverName}' is already scaled! Ignoring scale adjustment.`,
);
deployment.spec.replicas = scaleUp ? 1 : 0;
return k8sDeps.replaceNamespacedDeployment(
deployment.metadata.name,
namespace,
deployment,
);
}

View file

@ -4,9 +4,9 @@ import yaml from "js-yaml";
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8")); const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
export function getFtpContainer(serverSpec) { export function getFtpContainer(serverSpec) {
const { name } = serverSpec; const { mclName } = serverSpec;
const ftpContainer = loadYaml("lib/k8s/configs/containers/ftp-server.yml"); const ftpContainer = loadYaml("lib/k8s/configs/containers/ftp-server.yml");
ftpContainer.name = `mcl-${name}-ftp`; ftpContainer.name = `mcl-${mclName}-ftp`;
const ftpPortList = [ const ftpPortList = [
{ p: 20, n: "ftp-data" }, { p: 20, n: "ftp-data" },
{ p: 21, n: "ftp-commands" }, { p: 21, n: "ftp-commands" },
@ -22,10 +22,10 @@ export function getFtpContainer(serverSpec) {
} }
export function getCoreServerContainer(serverSpec) { export function getCoreServerContainer(serverSpec) {
const { name, version, serverType, memory } = serverSpec; const { mclName, version, serverType, memory } = serverSpec;
const container = loadYaml("lib/k8s/configs/containers/minecraft-server.yml"); const container = loadYaml("lib/k8s/configs/containers/minecraft-server.yml");
// Container Updates // Container Updates
container.name = `mcl-${name}-server`; container.name = `mcl-${mclName}-server`;
container.resources.requests.memory = `${memory}Mi`; container.resources.requests.memory = `${memory}Mi`;
const findEnv = (k) => container.env.find(({ name: n }) => n === k); const findEnv = (k) => container.env.find(({ name: n }) => n === k);
@ -36,7 +36,7 @@ export function getCoreServerContainer(serverSpec) {
updateEnv("VERSION", version); updateEnv("VERSION", version);
updateEnv("MEMORY", `${memory}M`); updateEnv("MEMORY", `${memory}M`);
// RCON // RCON
const rs = `mcl-${name}-rcon-secret`; const rs = `mcl-${mclName}-rcon-secret`;
findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs; findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs;
return container; return container;
} }
@ -50,13 +50,13 @@ export function getServerContainer(serverSpec) {
const updateEnv = (k, v) => (findEnv(k).value = v); const updateEnv = (k, v) => (findEnv(k).value = v);
// Enviornment variables // Enviornment variables
updateEnv("DIFFICULTY", difficulty); /*updateEnv("DIFFICULTY", difficulty);
updateEnv("MODE", gamemode); updateEnv("MODE", gamemode);
updateEnv("MOTD", motd); updateEnv("MOTD", motd);
updateEnv("MAX_PLAYERS", maxPlayers); updateEnv("MAX_PLAYERS", maxPlayers);
updateEnv("SEED", seed); updateEnv("SEED", seed);
updateEnv("OPS", ops); updateEnv("OPS", ops);
updateEnv("WHITELIST", whitelist); updateEnv("WHITELIST", whitelist); */
return container; return container;
} }

View file

@ -1,66 +1,19 @@
import k8s from "@kubernetes/client-node"; import k8s from "@kubernetes/client-node";
import { import { getDeployments } from "./k8s-server-control.js";
getDeployment,
getDeployments,
getServerAssets,
scaleDeployment,
} from "./k8s-server-control.js";
import { ERR } from "../util/logging.js";
import ExpressClientError from "../util/ExpressClientError.js";
const kc = new k8s.KubeConfig(); const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
const k8sMetrics = new k8s.Metrics(kc); const k8sMetrics = new k8s.Metrics(kc);
const namespace = process.env.MCL_SERVER_NAMESPACE; const namespace = process.env.MCL_SERVER_NAMESPACE;
export async function startServerContainer(serverSpec) { function getServerMetrics(podMetricsRes, serverId, serverAvailable) {
const { name } = serverSpec; const pod = podMetricsRes.items.find(({ metadata: md }) => {
try { return (
await scaleDeployment(name, true); md.annotations &&
} catch (e) { md.annotations["minecluster.dunemask.net/id"] === serverId
ERR("SERVER CONTROL", e);
throw new ExpressClientError({
c: 500,
m: `Error updating server '${name}'!\n`,
});
}
}
export async function stopServerContainer(serverSpec) {
const { name } = serverSpec;
try {
await scaleDeployment(name, false);
} catch (e) {
ERR("SERVER CONTROL", e);
throw new ExpressClientError({
c: 500,
m: `Error updating server '${name}'!`,
});
}
}
export async function getInstances() {
const serverDeployments = await getDeployments();
const podMetricsResponse = await k8sMetrics.getPodMetrics(namespace);
var name, metrics, services, serverAvailable, ftpAvailable;
const serverInstances = serverDeployments.map((s) => {
name = s.metadata.annotations["minecluster.dunemask.net/server-name"];
metrics = null;
const { containers } = s.spec.template.spec;
services = containers.map(({ name }) => name.split("-").pop());
const serverStatusList = s.status.conditions.map(
({ type: statusType, status: sts }) => ({ statusType, sts }),
); );
const deploymentAvailable =
serverStatusList.find(
(ss) => ss.statusType === "Available" && ss.sts === "True",
) !== undefined;
serverAvailable = services.includes(`server`) && deploymentAvailable;
ftpAvailable = services.includes("ftp") && deploymentAvailable;
const pod = podMetricsResponse.items.find(({ metadata: md }) => {
return md.labels && md.labels.app && md.labels.app === `mcl-${name}-app`;
}); });
if (serverAvailable && pod) { if (serverAvailable && 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,
@ -73,7 +26,37 @@ export async function getInstances() {
memory: Math.ceil(podMems.reduce((a, b) => a + b)), memory: Math.ceil(podMems.reduce((a, b) => a + b)),
}; };
} }
return { name, metrics, services, serverAvailable, ftpAvailable }; }
export async function getInstances() {
const serverDeployments = await getDeployments();
const podMetricsRes = await k8sMetrics.getPodMetrics(namespace);
var name, serverId, metrics, services, serverAvailable, ftpAvailable;
const serverInstances = serverDeployments.map((s) => {
serverId = s.metadata.annotations["minecluster.dunemask.net/id"];
name = s.metadata.name;
metrics = null;
const { containers } = s.spec.template.spec;
services = containers.map(({ name }) => name.split("-").pop());
const serverStatusList = s.status.conditions.map(
({ type: statusType, status: sts }) => ({ statusType, sts }),
);
const deploymentAvailable =
serverStatusList.find(
(ss) => ss.statusType === "Available" && ss.sts === "True",
) !== undefined;
serverAvailable = services.includes(`server`) && deploymentAvailable;
ftpAvailable = services.includes("ftp") && deploymentAvailable;
metrics = getServerMetrics(podMetricsRes, serverId, serverAvailable);
return {
name,
id: serverId,
metrics,
services,
serverAvailable,
ftpAvailable,
};
}); });
return serverInstances; return serverInstances;
} }

View file

@ -4,7 +4,7 @@ import k8s from "@kubernetes/client-node";
import yaml from "js-yaml"; 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 { import {
getFtpContainer, getFtpContainer,
getServerContainer, getServerContainer,
@ -20,32 +20,31 @@ const namespace = process.env.MCL_SERVER_NAMESPACE;
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8")); const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
function createRconSecret(serverSpec) { function createRconSecret(serverSpec) {
const { name } = serverSpec; const { mclName, id } = serverSpec;
const rconYaml = loadYaml("lib/k8s/configs/rcon-secret.yml"); const rconYaml = loadYaml("lib/k8s/configs/rcon-secret.yml");
// TODO: Dyamic rconPassword // TODO: Dyamic rconPassword
const rconPassword = bcrypt.hashSync(uuidv4(), 10); const rconPassword = bcrypt.hashSync(uuidv4(), 10);
rconYaml.data["rcon-password"] = Buffer.from(rconPassword).toString("base64"); rconYaml.data["rcon-password"] = Buffer.from(rconPassword).toString("base64");
rconYaml.metadata.labels.app = `mcl-${name}-app`; rconYaml.metadata.labels.app = `mcl-${mclName}-app`;
rconYaml.metadata.name = `mcl-${name}-rcon-secret`; rconYaml.metadata.name = `mcl-${mclName}-rcon-secret`;
rconYaml.metadata.namespace = namespace; rconYaml.metadata.namespace = namespace;
rconYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name; rconYaml.metadata.annotations["minecluster.dunemask.net/id"] = id;
return rconYaml; return rconYaml;
} }
function createServerVolume(serverSpec) { function createServerVolume(serverSpec) {
const { name } = serverSpec; const { mclName, id } = serverSpec;
const volumeYaml = loadYaml("lib/k8s/configs/server-pvc.yml"); const volumeYaml = loadYaml("lib/k8s/configs/server-pvc.yml");
volumeYaml.metadata.labels.service = `mcl-${name}-server`; volumeYaml.metadata.labels.service = `mcl-${mclName}-server`;
volumeYaml.metadata.name = `mcl-${name}-volume`; volumeYaml.metadata.name = `mcl-${mclName}-volume`;
volumeYaml.metadata.namespace = namespace; volumeYaml.metadata.namespace = namespace;
volumeYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = volumeYaml.metadata.annotations["minecluster.dunemask.net/id"] = id;
name;
volumeYaml.spec.resources.requests.storage = "1Gi"; // TODO: Changeme volumeYaml.spec.resources.requests.storage = "1Gi"; // TODO: Changeme
return volumeYaml; return volumeYaml;
} }
function createServerDeploy(serverSpec) { function createServerDeploy(serverSpec) {
const { name, host } = serverSpec; const { mclName, id } = serverSpec;
const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml"); const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml");
const { metadata } = deployYaml; const { metadata } = deployYaml;
const serverContainer = getServerContainer(serverSpec); const serverContainer = getServerContainer(serverSpec);
@ -53,19 +52,21 @@ function createServerDeploy(serverSpec) {
const ftpContainer = getFtpContainer(serverSpec); const ftpContainer = getFtpContainer(serverSpec);
// Configure Metadata; // Configure Metadata;
metadata.name = `mcl-${name}`; metadata.name = `mcl-${mclName}`;
metadata.namespace = namespace; metadata.namespace = namespace;
metadata.annotations["minecluster.dunemask.net/server-name"] = name; metadata.annotations["minecluster.dunemask.net/id"] = id;
deployYaml.metadata = metadata; deployYaml.metadata = metadata;
// Configure Lables & Selectors // Configure Lables & Selectors
deployYaml.spec.selector.matchLabels.app = `mcl-${name}-app`; deployYaml.spec.selector.matchLabels.app = `mcl-${mclName}-app`;
deployYaml.spec.template.metadata.labels.app = `mcl-${name}-app`; deployYaml.spec.template.metadata.labels.app = `mcl-${mclName}-app`;
deployYaml.spec.template.metadata.annotations["minecluster.dunemask.net/id"] =
id;
// Volumes // Volumes
deployYaml.spec.template.spec.volumes.find( deployYaml.spec.template.spec.volumes.find(
({ name }) => name === "datadir", ({ name }) => name === "datadir",
).persistentVolumeClaim.claimName = `mcl-${name}-volume`; ).persistentVolumeClaim.claimName = `mcl-${mclName}-volume`;
// Apply Containers TODO: User control for autostart // Apply Containers TODO: User control for autostart
deployYaml.spec.template.spec.containers.push(serverContainer); deployYaml.spec.template.spec.containers.push(serverContainer);
@ -75,17 +76,16 @@ function createServerDeploy(serverSpec) {
} }
function createServerService(serverSpec) { function createServerService(serverSpec) {
const { name, host } = serverSpec; const { mclName, host, id } = serverSpec;
const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml"); const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml");
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = host; serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = host;
serviceYaml.metadata.annotations["mc-router.itzg.me/externalServerName"] = serviceYaml.metadata.annotations["mc-router.itzg.me/externalServerName"] =
host; host;
serviceYaml.metadata.labels.app = `mcl-${name}-app`; serviceYaml.metadata.labels.app = `mcl-${mclName}-app`;
serviceYaml.metadata.name = `mcl-${name}-server`; serviceYaml.metadata.name = `mcl-${mclName}-server`;
serviceYaml.metadata.namespace = namespace; serviceYaml.metadata.namespace = namespace;
serviceYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = serviceYaml.metadata.annotations["minecluster.dunemask.net/id"] = id;
name; serviceYaml.spec.selector.app = `mcl-${mclName}-app`;
serviceYaml.spec.selector.app = `mcl-${name}-app`;
// Port List: // Port List:
const serverPortList = [{ p: 25565, n: "minecraft" }]; const serverPortList = [{ p: 25565, n: "minecraft" }];
@ -107,32 +107,26 @@ function createServerService(serverSpec) {
return serviceYaml; return serviceYaml;
} }
function createRconService(serverSpec) { function createRconService(createSpec) {
const { name } = serverSpec; const { id, mclName } = createSpec;
const rconSvcYaml = loadYaml("lib/k8s/configs/rcon-svc.yml"); const rconSvcYaml = loadYaml("lib/k8s/configs/rcon-svc.yml");
rconSvcYaml.metadata.labels.app = `mcl-${name}-app`; rconSvcYaml.metadata.labels.app = `mcl-${mclName}-app`;
rconSvcYaml.metadata.name = `mcl-${name}-rcon`; rconSvcYaml.metadata.name = `mcl-${mclName}-rcon`;
rconSvcYaml.metadata.namespace = namespace; rconSvcYaml.metadata.namespace = namespace;
rconSvcYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = rconSvcYaml.metadata.annotations["minecluster.dunemask.net/id"] = id;
name; rconSvcYaml.spec.selector.app = `mcl-${mclName}-app`;
rconSvcYaml.spec.selector.app = `mcl-${name}-app`;
return rconSvcYaml; return rconSvcYaml;
} }
export default async function createServerResources(serverSpec) { export default async function createServerResources(createSpec) {
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); createSpec.mclName = `${createSpec.host.replaceAll(".", "-")}-${
const deployments = deploymentRes.body.items.map((i) => i.metadata.name); createSpec.id
if (deployments.includes(`mcl-${serverSpec.name}`)) }`;
throw new ExpressClientError({ m: "Server already exists!", c: 409 }); const rconSecret = createRconSecret(createSpec);
const pvcRes = await k8sCore.listNamespacedPersistentVolumeClaim(namespace); const serverVolume = createServerVolume(createSpec);
const pvcs = pvcRes.body.items.map((i) => i.metadata.name); const serverDeploy = createServerDeploy(createSpec);
if (pvcs.includes(`mcl-${serverSpec.name}-volume`)) const serverService = createServerService(createSpec);
throw new ExpressClientError({ m: "Server PVC already exists!", c: 409 }); const rconService = createRconService(createSpec);
const rconSecret = createRconSecret(serverSpec);
const serverVolume = createServerVolume(serverSpec);
const serverDeploy = createServerDeploy(serverSpec);
const serverService = createServerService(serverSpec);
const rconService = createRconService(serverSpec);
k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume); k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume);
k8sCore.createNamespacedSecret(namespace, rconSecret); k8sCore.createNamespacedSecret(namespace, rconSecret);
k8sCore.createNamespacedService(namespace, serverService); k8sCore.createNamespacedService(namespace, serverService);

View file

@ -22,9 +22,9 @@ function deleteOnExist(o, fn) {
} }
export default async function deleteServerResources(serverSpec) { export default async function deleteServerResources(serverSpec) {
const { name } = serverSpec; const { id } = serverSpec;
// Ensure deployment exists // Ensure deployment exists
const server = await getServerAssets(name); const server = await getServerAssets(id);
if (!server) if (!server)
throw new ExpressClientError({ throw new ExpressClientError({
c: 404, c: 404,

View file

@ -34,8 +34,8 @@ export async function getFtpClient(serverService) {
} }
export async function useServerFtp(serverSpec, fn) { export async function useServerFtp(serverSpec, fn) {
const { name } = serverSpec; const { id } = serverSpec;
const server = await getServerAssets(name); const server = await getServerAssets(id);
if (!server) if (!server)
throw new ExpressClientError({ throw new ExpressClientError({
c: 404, c: 404,

View file

@ -33,14 +33,14 @@ export default function MineclusterFiles(props) {
], ],
[], [],
); );
const { server: serverName } = props; const { server: serverId } = props;
const inputRef = useRef(null); const inputRef = useRef(null);
const [dirStack, setDirStack] = useState(["."]); const [dirStack, setDirStack] = useState(["."]);
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const updateFiles = () => { const updateFiles = () => {
const dir = dirStack.join("/"); const dir = dirStack.join("/");
getServerFiles(serverName, dir).then((f) => { getServerFiles(serverId, dir).then((f) => {
const files = f.map((fi) => ({ ...fi, id: `${dir}/${fi.name}` })); const files = f.map((fi) => ({ ...fi, id: `${dir}/${fi.name}` }));
setFiles(files ?? []); setFiles(files ?? []);
}); });
@ -70,13 +70,13 @@ export default function MineclusterFiles(props) {
function createFolder() { function createFolder() {
const name = prompt("What is the name of the new folder?"); const name = prompt("What is the name of the new folder?");
const path = [...dirStack, name].join("/"); const path = [...dirStack, name].join("/");
createServerFolder(serverName, path).then(updateFiles); createServerFolder(serverId, path).then(updateFiles);
} }
function deleteItems(files) { function deleteItems(files) {
Promise.all( Promise.all(
files.map((f) => files.map((f) =>
deleteServerItem(serverName, [...dirStack, f.name].join("/"), f.isDir), deleteServerItem(serverId, [...dirStack, f.name].join("/"), f.isDir),
), ),
) )
.catch((e) => console.error("Error deleting some files!", e)) .catch((e) => console.error("Error deleting some files!", e))
@ -94,7 +94,7 @@ export default function MineclusterFiles(props) {
async function uploadFile(file) { async function uploadFile(file) {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("name", serverName); formData.append("id", serverId);
formData.append("path", [...dirStack, name].join("/")); formData.append("path", [...dirStack, name].join("/"));
await fetch("/api/files/upload", { await fetch("/api/files/upload", {
method: "POST", method: "POST",
@ -105,7 +105,7 @@ export default function MineclusterFiles(props) {
async function downloadFiles(files) { async function downloadFiles(files) {
Promise.all( Promise.all(
files.map((f) => files.map((f) =>
getServerItem(serverName, f.name, [...dirStack, f.name].join("/")), getServerItem(serverId, f.name, [...dirStack, f.name].join("/")),
), ),
) )
.then(() => console.log("Done downloading files!")) .then(() => console.log("Done downloading files!"))

View file

@ -16,7 +16,8 @@ export function useRconDialog(isOpen = false) {
} }
export default function RconDialog(props) { export default function RconDialog(props) {
const { serverName, open, dialogToggle } = props; const { server, open, dialogToggle } = props;
const { name: serverName, id: serverId } = server ?? {};
const theme = useTheme(); const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.down("sm")); const fullScreen = useMediaQuery(theme.breakpoints.down("sm"));
return ( return (
@ -33,7 +34,7 @@ export default function RconDialog(props) {
<Toolbar sx={{ display: { sm: "none" } }} /> <Toolbar sx={{ display: { sm: "none" } }} />
<DialogTitle>RCON - {serverName}</DialogTitle> <DialogTitle>RCON - {serverName}</DialogTitle>
<DialogContent> <DialogContent>
<RconView serverName={serverName} /> <RconView serverId={serverId} />
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button autoFocus onClick={dialogToggle}> <Button autoFocus onClick={dialogToggle}>

View file

@ -1,7 +1,7 @@
import { io } from "socket.io-client"; import { io } from "socket.io-client";
export default class RconSocket { export default class RconSocket {
constructor(logUpdate, serverName) { constructor(logUpdate, serverId) {
(this.sk = io("/", { query: { serverName } })), (this.logs = []); (this.sk = io("/", { query: { serverId } })), (this.logs = []);
this.logUpdate = logUpdate; this.logUpdate = logUpdate;
this.sk.on("push", this.onPush.bind(this)); this.sk.on("push", this.onPush.bind(this));
this.sk.on("connect", this.onConnect.bind(this)); this.sk.on("connect", this.onConnect.bind(this));

View file

@ -6,14 +6,14 @@ import RconSocket from "./RconSocket.js";
import "@mcl/css/rcon.css"; import "@mcl/css/rcon.css";
export default function RconView(props) { export default function RconView(props) {
const { serverName } = props; const { serverId } = props;
const logsRef = useRef(0); const logsRef = useRef(0);
const [cmd, setCmd] = useState(""); const [cmd, setCmd] = useState("");
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [rcon, setRcon] = useState({}); const [rcon, setRcon] = useState({});
const updateCmd = (e) => setCmd(e.target.value); const updateCmd = (e) => setCmd(e.target.value);
useEffect(function () { useEffect(function () {
setRcon(new RconSocket(setLogs, serverName)); setRcon(new RconSocket(setLogs, serverId));
return () => { return () => {
if (rcon && typeof rcon.disconnect === "function") rcon.disconnect(); if (rcon && typeof rcon.disconnect === "function") rcon.disconnect();
}; };

View file

@ -19,10 +19,10 @@ import { Link } from "react-router-dom";
export default function ServerCard(props) { export default function ServerCard(props) {
const { server, openRcon } = props; const { server, openRcon } = props;
const { name, metrics, ftpAvailable, serverAvailable, services } = server; const { name, id, metrics, ftpAvailable, serverAvailable, services } = server;
const startServer = useStartServer(name); const startServer = useStartServer(id);
const stopServer = useStopServer(name); const stopServer = useStopServer(id);
const deleteServer = useDeleteServer(name); const deleteServer = useDeleteServer(id);
function toggleRcon() { function toggleRcon() {
if (!services.includes("server")) return; if (!services.includes("server")) return;
openRcon(); openRcon();
@ -113,7 +113,7 @@ export default function ServerCard(props) {
aria-label="Edit" aria-label="Edit"
size="large" size="large"
component={Link} component={Link}
to={`/mcl/edit?server=${name}`} to={`/mcl/edit?server=${id}`}
> >
<EditIcon /> <EditIcon />
</IconButton> </IconButton>
@ -122,7 +122,7 @@ export default function ServerCard(props) {
aria-label="Files" aria-label="Files"
size="large" size="large"
component={Link} component={Link}
to={`/mcl/files?server=${name}`} to={`/mcl/files?server=${id}`}
disabled={!services.includes("ftp")} disabled={!services.includes("ftp")}
> >
<FolderIcon /> <FolderIcon />

View file

@ -50,10 +50,10 @@ export default function Home() {
<Box className="servers"> <Box className="servers">
{!isLoading && {!isLoading &&
servers.map((s, k) => ( servers.map((s, k) => (
<ServerCard key={k} server={s} openRcon={openRcon(s.name)} /> <ServerCard key={k} server={s} openRcon={openRcon(s)} />
))} ))}
</Box> </Box>
<RconDialog open={rdOpen} dialogToggle={rconToggle} serverName={server} /> <RconDialog open={rdOpen} dialogToggle={rconToggle} server={server} />
<Button <Button
component={Link} component={Link}
to="/mcl/create" to="/mcl/create"

View file

@ -20,38 +20,38 @@ const fetchApiPost = (subPath, json) => async () =>
body: JSON.stringify(json), body: JSON.stringify(json),
}).then((res) => res.json()); }).then((res) => res.json());
export const useServerStatus = (server) => export const useServerStatus = (serverId) =>
useQuery({ useQuery({
queryKey: [`server-status-${server}`], queryKey: [`server-status-${serverId}`],
queryFn: fetchApiPost("/server/status", { name: server }), queryFn: fetchApiPost("/server/status", { id: serverId }),
}); });
export const useServerMetrics = (server) => export const useServerMetrics = (serverId) =>
useQuery({ useQuery({
queryKey: [`server-metrics-${server}`], queryKey: [`server-metrics-${serverId}`],
queryFn: fetchApiPost("/server/metrics", { name: server }), queryFn: fetchApiPost("/server/metrics", { id: serverId }),
refetchInterval: 10000, refetchInterval: 10000,
}); });
export const useStartServer = (server) => export const useStartServer = (serverId) =>
postJsonApi("/server/start", { name: server }, "server-instances"); postJsonApi("/server/start", { id: serverId }, "server-instances");
export const useStopServer = (server) => export const useStopServer = (serverId) =>
postJsonApi("/server/stop", { name: server }, "server-instances"); postJsonApi("/server/stop", { id: serverId }, "server-instances");
export const useDeleteServer = (server) => export const useDeleteServer = (serverId) =>
postJsonApi("/server/delete", { name: server }, "server-instances", "DELETE"); postJsonApi("/server/delete", { id: serverId }, "server-instances", "DELETE");
export const useCreateServer = (spec) => export const useCreateServer = (spec) =>
postJsonApi("/server/create", spec, "server-list"); postJsonApi("/server/create", spec, "server-list");
export const getServerFiles = async (server, path) => export const getServerFiles = async (serverId, path) =>
fetchApiCore("/files/list", { name: server, path }, "POST", true); fetchApiCore("/files/list", { id: serverId, path }, "POST", true);
export const createServerFolder = async (server, path) => export const createServerFolder = async (serverId, path) =>
fetchApiCore("/files/folder", { fetchApiCore("/files/folder", {
name: server, id: serverId,
path, path,
}); });
export const deleteServerItem = async (server, path, isDir) => export const deleteServerItem = async (serverId, path, isDir) =>
fetchApiCore("/files/item", { name: server, path, isDir }, "DELETE"); fetchApiCore("/files/item", { id: serverId, path, isDir }, "DELETE");
export const getServerItem = async (server, name, path) => export const getServerItem = async (serverId, name, path) =>
fetchApiCore("/files/item", { name: server, path }) fetchApiCore("/files/item", { id: serverId, path })
.then((resp) => .then((resp) =>
resp.status === 200 resp.status === 200
? resp.blob() ? resp.blob()