[FEATURE] Basic System with file manager (#4)

Co-authored-by: dunemask <dunemask@gmail.com>
Co-authored-by: Dunemask <dunemask@gmail.com>
Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/4
This commit is contained in:
dunemask 2023-12-20 03:20:04 +00:00
parent 8fb5b34c77
commit 4f19cf19d9
62 changed files with 5910 additions and 1190 deletions

View file

@ -4,158 +4,130 @@ import k8s from "@kubernetes/client-node";
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();
const k8sDeps = kc.makeApiClient(k8s.AppsV1Api);
const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
const namespace = process.env.MCL_SERVER_NAMESPACE;
function payloadFilter(req, res) {
const serverSpec = req.body;
if (!serverSpec) return res.sendStatus(400);
const { name, url, version, serverType, difficulty, gamemode, memory } =
serverSpec;
if (!name) return res.status(400).send("Server name is required!");
if (!url) return res.status(400).send("Server url is required!");
if (!version) return res.status(400).send("Server version is required!");
if (!difficulty)
return res.status(400).send("Server difficulty is required!");
if (!serverType) return res.status(400).send("Server type is required!");
if (!gamemode) return res.status(400).send("Server Gamemode is required!");
if (!memory) return res.status(400).send("Memory is required!");
req.body.name = req.body.name.toLowerCase();
return "filtered";
}
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
function createRconSecret(serverSpec) {
const { name } = serverSpec;
const rconYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/rcon-secret.yml"), "utf8")
);
const rconYaml = loadYaml("lib/k8s/configs/rcon-secret.yml");
// TODO: Dyamic rconPassword
const rconPassword = bcrypt.hashSync(uuidv4(), 10);
rconYaml.data["rcon-password"] = Buffer.from(rconPassword).toString("base64");
rconYaml.metadata.labels.app = `mcl-${name}-app`;
rconYaml.metadata.name = `mcl-${name}-rcon-secret`;
rconYaml.metadata.namespace = namespace;
rconYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name;
return rconYaml;
}
function createServerVolume(serverSpec) {
const { name } = serverSpec;
const volumeYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/server-pvc.yml"), "utf8")
);
const volumeYaml = loadYaml("lib/k8s/configs/server-pvc.yml");
volumeYaml.metadata.labels.service = `mcl-${name}-server`;
volumeYaml.metadata.name = `mcl-${name}-volume`;
volumeYaml.metadata.namespace = namespace;
volumeYaml.metadata.annotations["minecluster.dunemask.net/server-name"] =
name;
volumeYaml.spec.resources.requests.storage = "1Gi"; // TODO: Changeme
return volumeYaml;
}
function createServerDeploy(serverSpec) {
const {
name,
version,
serverType,
difficulty,
gamemode,
memory,
motd,
maxPlayers,
seed,
modpack,
ops,
whitelist,
} = serverSpec;
const deployYaml = yaml.load(
fs.readFileSync(
path.resolve("lib/k8s/configs/server-deployment.yml"),
"utf8"
)
);
deployYaml.metadata.name = `mcl-${name}`;
deployYaml.metadata.namespace = namespace;
deployYaml.spec.replicas = 0; // TODO: User control for autostart
const { name, host } = 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`;
deployYaml.spec.template.spec.containers.splice(0, 1); //TODO: Currently removing backup container
const serverContainer = deployYaml.spec.template.spec.containers[0];
// Enviornment variables
serverContainer.env.find(({ name: n }) => n === "TYPE").value = serverType;
serverContainer.env.find(({ name: n }) => n === "VERSION").value = version;
serverContainer.env.find(({ name: n }) => n === "DIFFICULTY").value =
difficulty;
serverContainer.env.find(({ name: n }) => n === "MODE").value = gamemode;
serverContainer.env.find(({ name: n }) => n === "MOTD").value = motd;
serverContainer.env.find(({ name: n }) => n === "MAX_PLAYERS").value =
maxPlayers;
serverContainer.env.find(({ name: n }) => n === "SEED").value = seed;
serverContainer.env.find(({ name: n }) => n === "OPS").value = ops;
serverContainer.env.find(({ name: n }) => n === "WHITELIST").value =
whitelist;
serverContainer.env.find(
({ name: n }) => n === "MEMORY"
).value = `${memory}M`;
if (version !== "VANILLA")
delete serverContainer.env.find(({ name: n }) => n === "MODPACK").value;
else
serverContainer.env.find(({ name: n }) => n === "MODPACK").value = modpack;
serverContainer.env.find(
({ name }) => name === "RCON_PASSWORD"
).valueFrom.secretKeyRef.name = `mcl-${name}-rcon-secret`;
// 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"
({ name }) => name === "datadir",
).persistentVolumeClaim.claimName = `mcl-${name}-volume`;
deployYaml.spec.template.spec.containers[0] = serverContainer;
// Apply Containers TODO: User control for autostart
deployYaml.spec.template.spec.containers.push(serverContainer);
deployYaml.spec.template.spec.containers.push(ftpContainer);
deployYaml.spec.replicas = 1;
return deployYaml;
}
function createServerService(serverSpec) {
const { name, url } = serverSpec;
const serviceYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/server-svc.yml"), "utf8")
);
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = url;
const { name, host } = serverSpec;
const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml");
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = host;
serviceYaml.metadata.annotations["mc-router.itzg.me/externalServerName"] =
host;
serviceYaml.metadata.labels.app = `mcl-${name}-app`;
serviceYaml.metadata.name = `mcl-${name}-server`;
serviceYaml.metadata.namespace = namespace;
serviceYaml.metadata.annotations["minecluster.dunemask.net/server-name"] =
name;
serviceYaml.spec.selector.app = `mcl-${name}-app`;
// Port List:
const serverPortList = [{ p: 25565, n: "minecraft" }];
// Apply FTP Port List
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}` });
const portList = [...serverPortList, ...ftpPortList];
serviceYaml.spec.ports = portList.map(({ p: port, n: name }) => ({
port,
name,
protocol: "TCP",
targetPort: port,
}));
return serviceYaml;
}
function createRconService(serverSpec) {
const { name, url } = serverSpec;
const rconSvcYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/rcon-svc.yml"), "utf8")
);
const { name } = serverSpec;
const rconSvcYaml = loadYaml("lib/k8s/configs/rcon-svc.yml");
rconSvcYaml.metadata.labels.app = `mcl-${name}-app`;
rconSvcYaml.metadata.name = `mcl-${name}-rcon`;
rconSvcYaml.metadata.namespace = namespace;
rconSvcYaml.metadata.annotations["minecluster.dunemask.net/server-name"] =
name;
rconSvcYaml.spec.selector.app = `mcl-${name}-app`;
return rconSvcYaml;
}
export default async function createServer(req, res) {
if (payloadFilter(req, res) !== "filtered") return;
const serverSpec = req.body;
export default async function createServerResources(serverSpec) {
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
const deployments = deploymentRes.body.items.map((i) => i.metadata.name);
if (deployments.includes(`mcl-${serverSpec.name}`))
return res.status(409).send("Server already exists!");
throw new ExpressClientError({ m: "Server already exists!", c: 409 });
const pvcRes = await k8sCore.listNamespacedPersistentVolumeClaim(namespace);
const pvcs = pvcRes.body.items.map((i) => i.metadata.name);
if (pvcs.includes(`mcl-${serverSpec.name}-volume`))
return res.status(409).send("Server PVC already exists!");
throw new ExpressClientError({ m: "Server PVC already exists!", c: 409 });
const rconSecret = createRconSecret(serverSpec);
const serverVolume = createServerVolume(serverSpec);
const serverDeploy = createServerDeploy(serverSpec);
@ -166,6 +138,4 @@ export default async function createServer(req, res) {
k8sCore.createNamespacedService(namespace, serverService);
k8sCore.createNamespacedService(namespace, rconService);
k8sDeps.createNamespacedDeployment(namespace, serverDeploy);
res.sendStatus(200);
}