[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: LEVEL
value: world
- name: MODPACK
# - name: MODPACK
# value: https://somemodpack.com
- name: ONLINE_MODE
value: "true"
- name: MEMORY

View file

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

View file

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

View file

@ -5,15 +5,18 @@ import { ERR } from "../util/logging.js";
const kc = new k8s.KubeConfig();
kc.loadFromDefault();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
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 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)
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)
throw Error(`Multiple pods match the name ${containerName}`);
throw Error(`Multiple pods match the name ${podName}`);
const log = new k8s.Log(kc);
const logStream = new stream.PassThrough();

View file

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

View file

@ -55,8 +55,10 @@ function createServerVolume(serverSpec) {
return volumeYaml;
}
function getFtpContainer() {
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" },
@ -71,7 +73,7 @@ function getFtpContainer() {
return ftpContainer;
}
function createServerDeploy(serverSpec) {
function getServerContainer(serverSpec) {
const {
name,
version,
@ -86,24 +88,16 @@ function createServerDeploy(serverSpec) {
ops,
whitelist,
} = serverSpec;
const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml");
const serverContainer = loadYaml(
"lib/k8s/configs/containers/minecraft-server.yml",
);
const backupContainer = loadYaml(
"lib/k8s/configs/containers/minecraft-backup.yml",
);
const ftpContainer = getFtpContainer();
deployYaml.metadata.name = `mcl-${name}`;
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);
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
updateEnv("TYPE", serverType);
updateEnv("VERSION", version);
@ -116,22 +110,49 @@ function createServerDeploy(serverSpec) {
updateEnv("WHITELIST", whitelist);
updateEnv("MEMORY", `${memory}M`);
if (version !== "VANILLA") delete findEnv("MODPACK").value;
else updateEnv("MODPACK", modpack);
findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name =
`mcl-${name}-rcon-secret`;
// RCON
const rs = `mcl-${name}-rcon-secret`;
findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs;
// 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
deployYaml.spec.template.spec.volumes.find(
({ name }) => name === "datadir",
).persistentVolumeClaim.claimName = `mcl-${name}-volume`;
// Apply Containers
deployYaml.spec.template.spec.containers.push(serverContainer);
deployYaml.spec.template.spec.containers.push(ftpContainer);
// TODO: User control for autostart
deployYaml.spec.replicas = 0;
return deployYaml;
}

View file

@ -4,6 +4,22 @@ import { getServerAssets } from "./k8s-server-control.js";
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) {
const serverSpec = req.body;
if (!serverSpec) return res.sendStatus(400);
@ -16,20 +32,12 @@ export async function listFiles(req, res) {
return res
.status(409)
.send("Service doesn't exist, please contact your hosting provider!");
const client = new ftp.Client(0);
client.ftp.verbose = true;
try {
await client.access({
host: `${server.service.metadata.name}.${namespace}.svc.cluster.local`,
user: "minecluster",
password: "minecluster",
});
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.ftp.verbose = true;
const client = await useFtp(server.service).catch(handleError(res));
if (!client) return;
await client
.list()
.then((f) => res.json(f))
.catch(handleError(res));
client.close();
}

View file

@ -1,6 +1,6 @@
import k8s from "@kubernetes/client-node";
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();
kc.loadFromDefault();
const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
@ -15,7 +15,8 @@ export default async function rconInterface(socket) {
rconRes.body.data["rcon-password"],
"base64",
).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({
host: rconHost,
port: 25575,
@ -25,7 +26,8 @@ export default async function rconInterface(socket) {
try {
await rcon.connect();
} 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;
}