[FEATURE] Adjust resource flow
This commit is contained in:
parent
360dd32860
commit
61bf66c5c1
11 changed files with 202 additions and 86 deletions
|
@ -3,6 +3,8 @@ data:
|
|||
rcon-password: UEphT3V2aGJlQjNvc3M0dElwQU5YTUZrSkltR1RsRVl0ZGx3elFqZjJLdVZrZXNtV0hja1VhUUd3bmZDcElpbA==
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
minecluster.dunemask.net/server-name: changeme-server-name
|
||||
labels:
|
||||
app: changeme-app-label
|
||||
name: changeme-rcon-secret
|
||||
|
|
|
@ -2,6 +2,7 @@ apiVersion: v1
|
|||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
minecluster.dunemask.net/server-name: changeme-server-name
|
||||
labels:
|
||||
app: changeme-app
|
||||
name: changeme-rcon
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
minecluster.dunemask.net/server-name: changeme-server-name
|
||||
name: changeme-name
|
||||
namespace: changeme-namespace
|
||||
spec:
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
annotations:
|
||||
minecluster.dunemask.net/server-name: changeme-server-name
|
||||
labels:
|
||||
service: changeme-service-name
|
||||
name: changeme-pvc-name
|
||||
|
|
|
@ -4,6 +4,7 @@ metadata:
|
|||
annotations:
|
||||
ingress.qumine.io/hostname: changeme-url
|
||||
ingress.qumine.io/portname: minecraft
|
||||
minecluster.dunemask.net/server-name: changeme-server-name
|
||||
labels:
|
||||
app: changeme-app
|
||||
name: changeme-name
|
||||
|
|
105
lib/k8s/k8s-server-control.js
Normal file
105
lib/k8s/k8s-server-control.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
import k8s from "@kubernetes/client-node";
|
||||
import { VERB } from "../util/logging.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;
|
||||
|
||||
const mineclusterManaged = (o) =>
|
||||
o.metadata &&
|
||||
o.metadata.annotations &&
|
||||
o.metadata.annotations["minecluster.dunemask.net/server-name"] !== undefined;
|
||||
|
||||
const serverMatch = (serverName) => (o) =>
|
||||
o.metadata.annotations["minecluster.dunemask.net/server-name"] === serverName;
|
||||
|
||||
export async function getDeployments() {
|
||||
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
|
||||
const serverDeployments = deploymentRes.body.items.filter(mineclusterManaged);
|
||||
return serverDeployments;
|
||||
}
|
||||
|
||||
export async function getServices() {
|
||||
const serviceRes = await k8sCore.listNamespacedService(namespace);
|
||||
const serverServices = serviceRes.body.items.filter(mineclusterManaged);
|
||||
return serverServices;
|
||||
}
|
||||
|
||||
export async function getSecrets() {
|
||||
const secretRes = await k8sCore.listNamespacedSecret(namespace);
|
||||
const serverSecrets = secretRes.body.items.filter(mineclusterManaged);
|
||||
return serverSecrets;
|
||||
}
|
||||
|
||||
export async function getVolumes() {
|
||||
const volumeRes =
|
||||
await k8sCore.listNamespacedPersistentVolumeClaim(namespace);
|
||||
const serverVolumes = volumeRes.body.items.filter(mineclusterManaged);
|
||||
return serverVolumes;
|
||||
}
|
||||
|
||||
export function getServerAssets(serverName) {
|
||||
const serverFilter = serverMatch(serverName);
|
||||
return Promise.all([
|
||||
getDeployments(),
|
||||
getServices(),
|
||||
getSecrets(),
|
||||
getVolumes(),
|
||||
])
|
||||
.then(([deps, svcs, scrts, vols]) => {
|
||||
const deployments = deps.filter(serverFilter);
|
||||
const services = svcs.filter(serverFilter);
|
||||
const secrets = scrts.filter(serverFilter);
|
||||
const volumes = vols.filter(serverFilter);
|
||||
|
||||
if (deployments.length > 1) throw Error("Deployment filter broken!");
|
||||
if (volumes.length > 1) throw Error("Volume filter broken!");
|
||||
if (secrets.length > 1) throw Error("Secrets broken!");
|
||||
const serverAssets = {
|
||||
deployment: deployments[0],
|
||||
service: services.find(
|
||||
(s) => s.metadata.name === `mcl-${serverName}-rcon`,
|
||||
),
|
||||
volume: volumes[0],
|
||||
rconService: services.find(
|
||||
(s) => s.metadata.name === `mcl-${serverName}-server`,
|
||||
),
|
||||
rconSecret: secrets[0],
|
||||
};
|
||||
for (var k in serverAssets) if (serverAssets[k]) return serverAssets;
|
||||
// If no assets exist, return nothing
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
}
|
||||
|
||||
export async function getDeployment(serverName) {
|
||||
const servers = await getDeployments();
|
||||
const serverDeployment = servers.find(
|
||||
(s) =>
|
||||
s.metadata.annotations["minecluster.dunemask.net/server-name"] ===
|
||||
serverName,
|
||||
);
|
||||
if (!serverDeployment) {
|
||||
console.log(servers.map((s) => s.metadata.annotations));
|
||||
throw Error(`MCL Deployment '${serverName}' could not be found!`);
|
||||
}
|
||||
return serverDeployment;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
|
@ -1,22 +1,26 @@
|
|||
import k8s from "@kubernetes/client-node";
|
||||
import {
|
||||
getDeployment,
|
||||
getDeployments,
|
||||
getServerAssets,
|
||||
scaleDeployment,
|
||||
} from "./k8s-server-control.js";
|
||||
import { ERR } from "../util/logging.js";
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromDefault();
|
||||
|
||||
const k8sDeps = kc.makeApiClient(k8s.AppsV1Api);
|
||||
const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
|
||||
const k8sMetrics = new k8s.Metrics(kc);
|
||||
const namespace = process.env.MCL_SERVER_NAMESPACE;
|
||||
|
||||
async function findDeployment(serverName) {
|
||||
try {
|
||||
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
|
||||
return deploymentRes.body.items.find(
|
||||
(i) => i.metadata.name === `mcl-${serverName}`,
|
||||
);
|
||||
} catch (e) {
|
||||
ERR("SERVER CONTROL", `Error finding deployment: mcl-${serverName}`);
|
||||
}
|
||||
// Gets the all assets for the server
|
||||
export async function getServer(req, res) {
|
||||
const serverSpec = req.body;
|
||||
if (!serverSpec) return res.sendStatus(400);
|
||||
if (!serverSpec.name) return res.status(400).send("Server name required!");
|
||||
const { name } = serverSpec;
|
||||
getServerAssets(name)
|
||||
.then((server) => res.status(200).json(server))
|
||||
.catch((e) => res.status(500).send(e));
|
||||
}
|
||||
|
||||
export async function startServer(req, res) {
|
||||
|
@ -24,19 +28,13 @@ export async function startServer(req, res) {
|
|||
if (!serverSpec) return res.sendStatus(400);
|
||||
if (!serverSpec.name) return res.status(400).send("Server name required!");
|
||||
const { name } = serverSpec;
|
||||
const dep = await findDeployment(name);
|
||||
|
||||
if (!dep || !dep.spec) return res.status(409).send("Server does not exist!");
|
||||
if (dep.spec.replicas === 1)
|
||||
return res.status(409).send("Server already started!");
|
||||
dep.spec.replicas = 1;
|
||||
k8sDeps
|
||||
.replaceNamespacedDeployment(`mcl-${name}`, namespace, dep)
|
||||
.then(() => res.sendStatus(200))
|
||||
.catch((e) => {
|
||||
ERR("SERVER CONTROL", e);
|
||||
res.status(500).send("Error updating server!");
|
||||
});
|
||||
try {
|
||||
await scaleDeployment(name, true);
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
ERR("SERVER CONTROL", e);
|
||||
res.status(500).send(`Error updating server '${name}'!`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function stopServer(req, res) {
|
||||
|
@ -44,34 +42,28 @@ export async function stopServer(req, res) {
|
|||
if (!serverSpec) return res.sendStatus(400);
|
||||
if (!serverSpec.name) return res.status(400).send("Server name required!");
|
||||
const { name } = serverSpec;
|
||||
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
|
||||
const dep = deploymentRes.body.items.find(
|
||||
(i) => i.metadata.name === `mcl-${name}`,
|
||||
);
|
||||
if (!dep) return res.status(409).send("Server does not exist!");
|
||||
if (dep.spec.replicas === 0)
|
||||
return res.status(409).send("Server already stopped!");
|
||||
dep.spec.replicas = 0;
|
||||
k8sDeps.replaceNamespacedDeployment(`mcl-${name}`, namespace, dep);
|
||||
res.sendStatus(200);
|
||||
try {
|
||||
await scaleDeployment(name, false);
|
||||
res.sendStatus(200);
|
||||
} catch (e) {
|
||||
ERR("SERVER CONTROL", e);
|
||||
res.status(500).send(`Error updating server '${name}'!`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function serverList(req, res) {
|
||||
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
|
||||
const deployments = deploymentRes.body.items.map((i) => i.metadata.name);
|
||||
// TODO Add an annotation and manage using that
|
||||
const serverDeployments = deployments.filter((d) => d.startsWith("mcl-"));
|
||||
res.json(serverDeployments.map((sd) => sd.substring(4)));
|
||||
export function serverList(req, res) {
|
||||
getDeployments()
|
||||
.then((sd) => res.json(sd.map((s) => s.metadata.name.substring(4))))
|
||||
.catch((e) => {
|
||||
ERR("SERVER CONTROL", e);
|
||||
res.status(500).send("Couldn't get server list");
|
||||
});
|
||||
}
|
||||
|
||||
export async function getServers(req, res) {
|
||||
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
|
||||
const deployments = deploymentRes.body.items;
|
||||
const serverDeployments = await getDeployments();
|
||||
const podMetricsResponse = await k8sMetrics.getPodMetrics(namespace);
|
||||
// TODO Add an annotation and manage using that
|
||||
const serverDeployments = deployments.filter((d) =>
|
||||
d.metadata.name.startsWith("mcl-"),
|
||||
);
|
||||
|
||||
var name, metrics, started;
|
||||
const servers = serverDeployments.map((s) => {
|
||||
name = s.metadata.name.substring(4);
|
||||
|
|
|
@ -10,6 +10,8 @@ const k8sDeps = kc.makeApiClient(k8s.AppsV1Api);
|
|||
const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
|
||||
const namespace = process.env.MCL_SERVER_NAMESPACE;
|
||||
|
||||
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
|
||||
|
||||
function payloadFilter(req, res) {
|
||||
const serverSpec = req.body;
|
||||
if (!serverSpec) return res.sendStatus(400);
|
||||
|
@ -29,9 +31,7 @@ function payloadFilter(req, res) {
|
|||
|
||||
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);
|
||||
|
@ -39,17 +39,18 @@ function createRconSecret(serverSpec) {
|
|||
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;
|
||||
}
|
||||
|
@ -69,14 +70,11 @@ function createServerDeploy(serverSpec) {
|
|||
ops,
|
||||
whitelist,
|
||||
} = serverSpec;
|
||||
const deployYaml = yaml.load(
|
||||
fs.readFileSync(
|
||||
path.resolve("lib/k8s/configs/server-deployment.yml"),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml");
|
||||
deployYaml.metadata.name = `mcl-${name}`;
|
||||
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`;
|
||||
|
@ -117,25 +115,25 @@ function createServerDeploy(serverSpec) {
|
|||
|
||||
function createServerService(serverSpec) {
|
||||
const { name, url } = serverSpec;
|
||||
const serviceYaml = yaml.load(
|
||||
fs.readFileSync(path.resolve("lib/k8s/configs/server-svc.yml"), "utf8"),
|
||||
);
|
||||
const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml");
|
||||
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = url;
|
||||
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`;
|
||||
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 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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import k8s from "@kubernetes/client-node";
|
||||
import { ERR } from "../util/logging.js";
|
||||
import { getServerAssets } from "./k8s-server-control.js";
|
||||
const kc = new k8s.KubeConfig();
|
||||
kc.loadFromDefault();
|
||||
|
||||
|
@ -12,38 +13,47 @@ const deleteError = (res) => (err) => {
|
|||
ERR("K8S", "An error occurred while deleting a resource", err);
|
||||
};
|
||||
|
||||
async function deleteOnExist(o, fn) {
|
||||
if (!o) return;
|
||||
return fn(o.metadata.name);
|
||||
}
|
||||
|
||||
export default async function deleteServer(req, res) {
|
||||
const serverSpec = req.body;
|
||||
if (!serverSpec) return res.sendStatus(400);
|
||||
if (!serverSpec.name) return res.status(400).send("Server name required!");
|
||||
const { name } = serverSpec;
|
||||
// Ensure deployment exists
|
||||
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 does not exist!");
|
||||
const server = await getServerAssets(name);
|
||||
if (!server) return res.status(404).send("No Resources for that server were found!");
|
||||
// Delete in reverse order
|
||||
const deleteDeploy = k8sDeps.deleteNamespacedDeployment(
|
||||
`mcl-${serverSpec.name}`,
|
||||
namespace,
|
||||
const deleteDeploy = deleteOnExist(
|
||||
server.deployment,
|
||||
(name)=> k8sDeps.deleteNamespacedDeployment(name, namespace),
|
||||
);
|
||||
const deleteService = k8sCore.deleteNamespacedService(
|
||||
`mcl-${name}-server`,
|
||||
namespace,
|
||||
|
||||
const deleteService = deleteOnExist(
|
||||
server.service,
|
||||
(name) => k8sCore.deleteNamespacedService(name, namespace),
|
||||
);
|
||||
const deleteRconService = k8sCore.deleteNamespacedService(
|
||||
`mcl-${name}-rcon`,
|
||||
namespace,
|
||||
const deleteRconService = deleteOnExist(
|
||||
server.rconService,
|
||||
(name) => k8sCore.deleteNamespacedService(name, namespace),
|
||||
);
|
||||
await deleteDeploy.catch(deleteError(res));
|
||||
const deleteRconSecret = k8sCore.deleteNamespacedSecret(
|
||||
`mcl-${name}-rcon-secret`,
|
||||
namespace,
|
||||
try {
|
||||
await deleteDeploy;
|
||||
} catch (e) {
|
||||
return deleteError(res)(e);
|
||||
}
|
||||
const deleteRconSecret = deleteOnExist(
|
||||
server.rconSecret,
|
||||
(name)=> k8sCore.deleteNamespacedSecret(name, namespace),
|
||||
);
|
||||
const deleteVolume = k8sCore.deleteNamespacedPersistentVolumeClaim(
|
||||
`mcl-${name}-volume`,
|
||||
namespace,
|
||||
const deleteVolume = deleteOnExist(
|
||||
server.volume,
|
||||
(name)=>k8sCore.deleteNamespacedPersistentVolumeClaim(name, namespace),
|
||||
);
|
||||
|
||||
Promise.all([
|
||||
deleteService,
|
||||
deleteRconService,
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
stopServer,
|
||||
serverList,
|
||||
getServers,
|
||||
getServer,
|
||||
} from "../k8s/server-control.js";
|
||||
import createServer from "../k8s/server-create.js";
|
||||
import deleteServer from "../k8s/server-delete.js";
|
||||
|
@ -14,6 +15,7 @@ router.post("/create", createServer);
|
|||
router.delete("/delete", deleteServer);
|
||||
router.post("/start", startServer);
|
||||
router.post("/stop", stopServer);
|
||||
// router.post("/get", getServer) // SHOULD BE DISABLED EXCEPT FOR DEBUGGING;
|
||||
router.get("/list", serverList);
|
||||
router.get("/instances", getServers);
|
||||
export default router;
|
||||
|
|
|
@ -12,12 +12,11 @@ import { useCreateServer, useVersionList } from "@mcl/queries";
|
|||
|
||||
const defaultServer = {
|
||||
version: "latest",
|
||||
name: "example",
|
||||
serverType: "VANILLA",
|
||||
difficulty: "easy",
|
||||
maxPlayers: "5",
|
||||
gamemode: "survival",
|
||||
memory: "1024",
|
||||
memory: "512",
|
||||
motd: `\\u00A7e\\u00A7ka\\u00A7l\\u00A7aMine\\u00A76Cluster\\u00A7r\\u00A78\\u00A7b\\u00A7ka`,
|
||||
};
|
||||
|
||||
|
@ -115,7 +114,9 @@ export default function Create() {
|
|||
<TextField
|
||||
label="Name"
|
||||
onChange={coreUpdate("name")}
|
||||
helperText="Example: My Survival World"
|
||||
defaultValue={spec.name}
|
||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue