[FEATURE] Adjust server control

This commit is contained in:
Dunemask 2023-12-17 12:25:14 -07:00
parent 7348b07352
commit 62c966a6bd
8 changed files with 109 additions and 57 deletions

View file

@ -52,7 +52,8 @@ env:
- name: GENERATOR_SETTINGS - name: GENERATOR_SETTINGS
- name: LEVEL - name: LEVEL
value: world value: world
- name: MODPACK # - name: MODPACK
# value: https://somemodpack.com
- name: ONLINE_MODE - name: ONLINE_MODE
value: "true" value: "true"
- name: MEMORY - name: MEMORY

View file

@ -16,6 +16,8 @@ spec:
type: Recreate type: Recreate
template: template:
metadata: metadata:
annotations:
minecluster.dunemask.net/server-name: changeme-server-name
labels: labels:
app: changeme-app app: changeme-app
spec: spec:

View file

@ -1,5 +1,5 @@
import k8s from "@kubernetes/client-node"; import k8s from "@kubernetes/client-node";
import { VERB } from "../util/logging.js"; import { VERB, ERR } from "../util/logging.js";
const kc = new k8s.KubeConfig(); const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
@ -13,7 +13,7 @@ const mineclusterManaged = (o) =>
o.metadata.annotations && o.metadata.annotations &&
o.metadata.annotations["minecluster.dunemask.net/server-name"] !== undefined; o.metadata.annotations["minecluster.dunemask.net/server-name"] !== undefined;
const serverMatch = (serverName) => (o) => export const serverMatch = (serverName) => (o) =>
o.metadata.annotations["minecluster.dunemask.net/server-name"] === serverName; o.metadata.annotations["minecluster.dunemask.net/server-name"] === serverName;
export async function getDeployments() { export async function getDeployments() {
@ -72,7 +72,7 @@ export function getServerAssets(serverName) {
for (var k in serverAssets) if (serverAssets[k]) return serverAssets; for (var k in serverAssets) if (serverAssets[k]) return serverAssets;
// If no assets exist, return nothing // If no assets exist, return nothing
}) })
.catch((e) => console.log(e)); .catch((e) => ERR("SERVER ASSETS", e));
} }
export async function getDeployment(serverName) { export async function getDeployment(serverName) {
@ -82,13 +82,26 @@ export async function getDeployment(serverName) {
s.metadata.annotations["minecluster.dunemask.net/server-name"] === s.metadata.annotations["minecluster.dunemask.net/server-name"] ===
serverName, serverName,
); );
if (!serverDeployment) { if (!serverDeployment)
console.log(servers.map((s) => s.metadata.annotations));
throw Error(`MCL Deployment '${serverName}' could not be found!`); throw Error(`MCL Deployment '${serverName}' could not be found!`);
}
return serverDeployment; return serverDeployment;
} }
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;
return k8sDeps.replaceNamespacedDeployment(
deployment.metadata.name,
namespace,
deployment,
);
}
export async function scaleDeployment(serverName, scaleUp = false) { export async function scaleDeployment(serverName, scaleUp = false) {
const deployment = await getDeployment(serverName); const deployment = await getDeployment(serverName);
if (deployment.spec.replicas === 1 && scaleUp) if (deployment.spec.replicas === 1 && scaleUp)
@ -97,6 +110,7 @@ export async function scaleDeployment(serverName, scaleUp = false) {
`MCL Deployment '${serverName}' is already scaled! Ignoring scale adjustment.`, `MCL Deployment '${serverName}' is already scaled! Ignoring scale adjustment.`,
); );
deployment.spec.replicas = scaleUp ? 1 : 0; deployment.spec.replicas = scaleUp ? 1 : 0;
return k8sDeps.replaceNamespacedDeployment( return k8sDeps.replaceNamespacedDeployment(
deployment.metadata.name, deployment.metadata.name,
namespace, namespace,

View file

@ -5,15 +5,18 @@ import { ERR } from "../util/logging.js";
const kc = new k8s.KubeConfig(); const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api); const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
export default async function liveLogging(socket, serverNamespace) { export default async function liveLogging(socket, serverNamespace) {
const containerName = `mcl-${socket.mcs.serverName}`; const { serverName } = socket.mcs;
const podName = `mcl-${serverName}`;
const containerName = `${podName}-server`;
const podResponse = await k8sApi.listNamespacedPod(serverNamespace); const podResponse = await k8sApi.listNamespacedPod(serverNamespace);
const pods = podResponse.body.items.map((vp1) => vp1.metadata.name); const pods = podResponse.body.items.map((vp1) => vp1.metadata.name);
const mcsPods = pods.filter((p) => p.startsWith(containerName)); const mcsPods = pods.filter((p) => p.startsWith(podName));
if (mcsPods.length === 0) if (mcsPods.length === 0)
throw Error(`Could not find a pod that starts with ${containerName}`); throw Error(`Could not find a pod that starts with ${podName}`);
if (mcsPods.length > 1) if (mcsPods.length > 1)
throw Error(`Multiple pods match the name ${containerName}`); throw Error(`Multiple pods match the name ${podName}`);
const log = new k8s.Log(kc); const log = new k8s.Log(kc);
const logStream = new stream.PassThrough(); const logStream = new stream.PassThrough();

View file

@ -10,6 +10,7 @@ const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
const k8sMetrics = new k8s.Metrics(kc); const k8sMetrics = new k8s.Metrics(kc);
const k8sDeps = kc.makeApiClient(k8s.AppsV1Api);
const namespace = process.env.MCL_SERVER_NAMESPACE; const namespace = process.env.MCL_SERVER_NAMESPACE;
// Gets the all assets for the server // Gets the all assets for the server

View file

@ -55,8 +55,10 @@ function createServerVolume(serverSpec) {
return volumeYaml; return volumeYaml;
} }
function getFtpContainer() { function getFtpContainer(serverSpec) {
const { name } = 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`;
const ftpPortList = [ const ftpPortList = [
{ p: 20, n: "ftp-data" }, { p: 20, n: "ftp-data" },
{ p: 21, n: "ftp-commands" }, { p: 21, n: "ftp-commands" },
@ -71,7 +73,7 @@ function getFtpContainer() {
return ftpContainer; return ftpContainer;
} }
function createServerDeploy(serverSpec) { function getServerContainer(serverSpec) {
const { const {
name, name,
version, version,
@ -86,24 +88,16 @@ function createServerDeploy(serverSpec) {
ops, ops,
whitelist, whitelist,
} = serverSpec; } = serverSpec;
const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml"); const container = loadYaml("lib/k8s/configs/containers/minecraft-server.yml");
const serverContainer = loadYaml(
"lib/k8s/configs/containers/minecraft-server.yml", // Container Updates
); container.name = `mcl-${name}-server`;
const backupContainer = loadYaml( container.resources.requests.memory = `${memory}Mi`;
"lib/k8s/configs/containers/minecraft-backup.yml", // container.resources.limits.memory = `${memory}Mi`; // TODO Allow for limits beyond initial startup
);
const ftpContainer = getFtpContainer(); const findEnv = (k) => container.env.find(({ name: n }) => n === k);
deployYaml.metadata.name = `mcl-${name}`; const updateEnv = (k, v) => (findEnv(k).value = v);
deployYaml.metadata.namespace = namespace;
deployYaml.metadata.annotations["minecluster.dunemask.net/server-name"] =
name;
deployYaml.spec.replicas = 0; // TODO: User control for autostart
deployYaml.spec.selector.matchLabels.app = `mcl-${name}-app`;
deployYaml.spec.template.metadata.labels.app = `mcl-${name}-app`;
const findEnv = (k) => serverContainer.env.find(({ name: n }) => n === k);
const updateEnv = (k, v) => (findEnv.value = v);
// Enviornment variables // Enviornment variables
updateEnv("TYPE", serverType); updateEnv("TYPE", serverType);
updateEnv("VERSION", version); updateEnv("VERSION", version);
@ -116,22 +110,49 @@ function createServerDeploy(serverSpec) {
updateEnv("WHITELIST", whitelist); updateEnv("WHITELIST", whitelist);
updateEnv("MEMORY", `${memory}M`); updateEnv("MEMORY", `${memory}M`);
if (version !== "VANILLA") delete findEnv("MODPACK").value; // RCON
else updateEnv("MODPACK", modpack); const rs = `mcl-${name}-rcon-secret`;
findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs;
`mcl-${name}-rcon-secret`; // Mods // TODO: remove these once files are managable
/*if (version !== "VANILLA") delete findEnv("MODPACK").value;
else updateEnv("MODPACK", modpack);*/
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");
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`;
// 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 // 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-${name}-volume`;
// Apply Containers
deployYaml.spec.template.spec.containers.push(serverContainer); deployYaml.spec.template.spec.containers.push(serverContainer);
deployYaml.spec.template.spec.containers.push(ftpContainer); deployYaml.spec.template.spec.containers.push(ftpContainer);
// TODO: User control for autostart
deployYaml.spec.replicas = 0;
return deployYaml; return deployYaml;
} }

View file

@ -4,6 +4,22 @@ import { getServerAssets } from "./k8s-server-control.js";
const namespace = process.env.MCL_SERVER_NAMESPACE; const namespace = process.env.MCL_SERVER_NAMESPACE;
export async function useFtp(serverService) {
const { name } = serverService.metadata;
const client = new ftp.Client();
await client.access({
host: `${name}.${namespace}.svc.cluster.local`,
user: "minecluster",
password: "minecluster",
});
return client;
}
const handleError = (res) => (e) => {
ERR("SERVER FILES", "Error occurred while preforming FTP operation!", e);
res.status(500).send("Error occurred while performing FTP operation!");
};
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);
@ -16,20 +32,12 @@ export async function listFiles(req, res) {
return res return res
.status(409) .status(409)
.send("Service doesn't exist, please contact your hosting provider!"); .send("Service doesn't exist, please contact your hosting provider!");
const client = new ftp.Client(0); // client.ftp.verbose = true;
client.ftp.verbose = true; const client = await useFtp(server.service).catch(handleError(res));
try { if (!client) return;
await client.access({ await client
host: `${server.service.metadata.name}.${namespace}.svc.cluster.local`, .list()
user: "minecluster", .then((f) => res.json(f))
password: "minecluster", .catch(handleError(res));
});
const files = await client.list();
res.json(files);
} catch (err) {
console.log(err);
ERR("SERVER FILES", "Error loading client files:");
res.status(500).send(err);
}
client.close(); client.close();
} }

View file

@ -1,6 +1,6 @@
import k8s from "@kubernetes/client-node"; import k8s from "@kubernetes/client-node";
import { Rcon as RconClient } from "rcon-client"; import { Rcon as RconClient } from "rcon-client";
import { ERR } from "../util/logging.js"; import { ERR, WARN } from "../util/logging.js";
const kc = new k8s.KubeConfig(); const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
const k8sCore = kc.makeApiClient(k8s.CoreV1Api); const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
@ -15,7 +15,8 @@ export default async function rconInterface(socket) {
rconRes.body.data["rcon-password"], rconRes.body.data["rcon-password"],
"base64", "base64",
).toString("utf8"); ).toString("utf8");
const rconHost = `mcl-${socket.mcs.serverName}-rcon`; const { serverName } = socket.mcs;
const rconHost = `mcl-${serverName}-rcon.${namespace}.svc.cluster.local`;
const rcon = new RconClient({ const rcon = new RconClient({
host: rconHost, host: rconHost,
port: 25575, port: 25575,
@ -25,7 +26,8 @@ export default async function rconInterface(socket) {
try { try {
await rcon.connect(); await rcon.connect();
} catch (error) { } catch (error) {
ERR("RCON", `Could not connect to 'mcl-${socket.mcs.serverName}-rcon'`); socket.emit("push", "Could not connect RCON Input to server!");
WARN("RCON", `Could not connect to '${rconHost}'`);
} }
socket.rconClient = rcon; socket.rconClient = rcon;
} }