(feature) Update UI & Resource Availability
This commit is contained in:
parent
11d8229eb5
commit
929193d272
44 changed files with 4747 additions and 27 deletions
10
lib/k8s/configs/rcon-secret.yml
Normal file
10
lib/k8s/configs/rcon-secret.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
data:
|
||||
rcon-password: UEphT3V2aGJlQjNvc3M0dElwQU5YTUZrSkltR1RsRVl0ZGx3elFqZjJLdVZrZXNtV0hja1VhUUd3bmZDcElpbA==
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
app: changeme-app-label
|
||||
name: changeme-rcon-secret
|
||||
namespace: changeme-namespace
|
||||
type: Opaque
|
22
lib/k8s/configs/rcon-svc.yml
Normal file
22
lib/k8s/configs/rcon-svc.yml
Normal file
|
@ -0,0 +1,22 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
labels:
|
||||
app: changeme-app
|
||||
name: changeme-rcon
|
||||
namespace: changeme-namespace
|
||||
spec:
|
||||
internalTrafficPolicy: Cluster
|
||||
ipFamilies:
|
||||
- IPv4
|
||||
ipFamilyPolicy: SingleStack
|
||||
ports:
|
||||
- name: rcon
|
||||
port: 25575
|
||||
protocol: TCP
|
||||
targetPort: rcon
|
||||
selector:
|
||||
app: changeme-app
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
215
lib/k8s/configs/server-deployment.yml
Normal file
215
lib/k8s/configs/server-deployment.yml
Normal file
|
@ -0,0 +1,215 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: changeme-name
|
||||
namespace: changeme-namespace
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 10
|
||||
selector:
|
||||
matchLabels:
|
||||
app: changeme-app
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: changeme-app
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: SRC_DIR
|
||||
value: /data
|
||||
- name: BACKUP_NAME
|
||||
value: world
|
||||
- name: INITIAL_DELAY
|
||||
value: 2m
|
||||
- name: BACKUP_INTERVAL
|
||||
value: 24h
|
||||
- name: PRUNE_BACKUPS_DAYS
|
||||
value: "2"
|
||||
- name: PAUSE_IF_NO_PLAYERS
|
||||
value: "true"
|
||||
- name: SERVER_PORT
|
||||
value: "25565"
|
||||
- name: RCON_HOST
|
||||
value: localhost
|
||||
- name: RCON_PORT
|
||||
value: "25575"
|
||||
- name: RCON_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: rcon-password
|
||||
name: changeme-rcon-secret
|
||||
- name: RCON_RETRIES
|
||||
value: "5"
|
||||
- name: RCON_RETRY_INTERVAL
|
||||
value: 10s
|
||||
- name: EXCLUDES
|
||||
value: "*.jar,cache,logs"
|
||||
- name: BACKUP_METHOD
|
||||
value: rclone
|
||||
- name: DEST_DIR
|
||||
value: /backups
|
||||
- name: LINK_LATEST
|
||||
value: "false"
|
||||
- name: TAR_COMPRESS_METHOD
|
||||
value: gzip
|
||||
- name: ZSTD_PARAMETERS
|
||||
value: -3 --long=25 --single-thread
|
||||
- name: RCLONE_REMOTE
|
||||
value: mc-dunemask-net
|
||||
- name: RCLONE_DEST_DIR
|
||||
value: /minecraft-backups/deltasmp-backups
|
||||
- name: RCLONE_COMPRESS_METHOD
|
||||
value: gzip
|
||||
image: itzg/mc-backup:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: mcs-deltasmp-minecraft-mc-backup
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: datadir
|
||||
readOnly: true
|
||||
- mountPath: /backups
|
||||
name: backupdir
|
||||
- mountPath: /config/rclone
|
||||
name: rclone-config
|
||||
- env:
|
||||
- name: EULA
|
||||
value: "TRUE"
|
||||
- name: TYPE
|
||||
value: VANILLA
|
||||
- name: VERSION
|
||||
value: "latest"
|
||||
- name: DIFFICULTY
|
||||
value: easy
|
||||
- name: WHITELIST
|
||||
- name: OPS
|
||||
- name: ICON
|
||||
- name: MAX_PLAYERS
|
||||
value: "20"
|
||||
- name: MAX_WORLD_SIZE
|
||||
value: "10000"
|
||||
- name: ALLOW_NETHER
|
||||
value: "true"
|
||||
- name: ANNOUNCE_PLAYER_ACHIEVEMENTS
|
||||
value: "true"
|
||||
- name: ENABLE_COMMAND_BLOCK
|
||||
value: "true"
|
||||
- name: FORCE_GAMEMODE
|
||||
value: "false"
|
||||
- name: GENERATE_STRUCTURES
|
||||
value: "true"
|
||||
- name: HARDCORE
|
||||
value: "false"
|
||||
- name: MAX_BUILD_HEIGHT
|
||||
value: "256"
|
||||
- name: MAX_TICK_TIME
|
||||
value: "60000"
|
||||
- name: SPAWN_ANIMALS
|
||||
value: "true"
|
||||
- name: SPAWN_MONSTERS
|
||||
value: "true"
|
||||
- name: SPAWN_NPCS
|
||||
value: "true"
|
||||
- name: SPAWN_PROTECTION
|
||||
value: "16"
|
||||
- name: VIEW_DISTANCE
|
||||
value: "10"
|
||||
- name: SEED
|
||||
- name: MODE
|
||||
value: survival
|
||||
- name: MOTD
|
||||
value: §6Minecluster Hosting
|
||||
- name: PVP
|
||||
value: "true"
|
||||
- name: LEVEL_TYPE
|
||||
value: DEFAULT
|
||||
- name: GENERATOR_SETTINGS
|
||||
- name: LEVEL
|
||||
value: world
|
||||
- name: MODPACK
|
||||
- name: ONLINE_MODE
|
||||
value: "true"
|
||||
- name: MEMORY
|
||||
value: 1024M
|
||||
- name: JVM_OPTS
|
||||
- name: JVM_XX_OPTS
|
||||
- name: OVERRIDE_SERVER_PROPERTIES
|
||||
value: "true"
|
||||
- name: ENABLE_RCON
|
||||
value: "true"
|
||||
- name: RCON_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: rcon-password
|
||||
name: changeme-rcon-secret
|
||||
image: itzg/minecraft-server:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mc-health
|
||||
failureThreshold: 20
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
name: changeme-name
|
||||
ports:
|
||||
- containerPort: 25565
|
||||
name: minecraft
|
||||
protocol: TCP
|
||||
- containerPort: 25575
|
||||
name: rcon
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mc-health
|
||||
failureThreshold: 20
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 5
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
stdin: true
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
tty: true
|
||||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: datadir
|
||||
- mountPath: /backups
|
||||
name: backupdir
|
||||
readOnly: true
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext:
|
||||
fsGroup: 2000
|
||||
runAsUser: 1000
|
||||
terminationGracePeriodSeconds: 30
|
||||
volumes:
|
||||
- name: datadir
|
||||
persistentVolumeClaim:
|
||||
claimName: changeme-pvc-name
|
||||
- emptyDir: {}
|
||||
name: backupdir
|
||||
- name: rclone-config
|
||||
secret:
|
||||
defaultMode: 420
|
||||
items:
|
||||
- key: rclone.conf
|
||||
path: rclone.conf
|
||||
secretName: rclone-config
|
13
lib/k8s/configs/server-pvc.yml
Normal file
13
lib/k8s/configs/server-pvc.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
labels:
|
||||
service: changeme-service-name
|
||||
name: changeme-pvc-name
|
||||
namespace: changeme-namespace
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 20Gi
|
24
lib/k8s/configs/server-svc.yml
Normal file
24
lib/k8s/configs/server-svc.yml
Normal file
|
@ -0,0 +1,24 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
annotations:
|
||||
ingress.qumine.io/hostname: changeme-url
|
||||
ingress.qumine.io/portname: minecraft
|
||||
labels:
|
||||
app: changeme-app
|
||||
name: changeme-name
|
||||
namespace: changeme-namespace
|
||||
spec:
|
||||
internalTrafficPolicy: Cluster
|
||||
ipFamilies:
|
||||
- IPv4
|
||||
ipFamilyPolicy: SingleStack
|
||||
ports:
|
||||
- name: minecraft
|
||||
port: 25565
|
||||
protocol: TCP
|
||||
targetPort: minecraft
|
||||
selector:
|
||||
app: changeme-app
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
30
lib/k8s/live-logging.js
Normal file
30
lib/k8s/live-logging.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import stream from "stream";
|
||||
import k8s from "@kubernetes/client-node";
|
||||
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 podResponse = await k8sApi.listNamespacedPod(serverNamespace);
|
||||
const pods = podResponse.body.items.map((vp1) => vp1.metadata.name);
|
||||
const mcsPods = pods.filter((p) => p.startsWith(containerName));
|
||||
if (mcsPods.length === 0)
|
||||
throw Error(`Could not find a pod that starts with ${containerName}`);
|
||||
if (mcsPods.length > 1)
|
||||
throw Error(`Multiple pods match the name ${containerName}`);
|
||||
|
||||
const log = new k8s.Log(kc);
|
||||
const logStream = new stream.PassThrough();
|
||||
logStream.on("data", (chunk) =>
|
||||
socket.emit("push", Buffer.from(chunk).toString())
|
||||
);
|
||||
log
|
||||
.log(serverNamespace, mcsPods[0], containerName, logStream, {
|
||||
follow: true,
|
||||
pretty: false,
|
||||
timestamps: false,
|
||||
})
|
||||
.catch((e) => ERR("K8S", e));
|
||||
}
|
95
lib/k8s/server-control.js
Normal file
95
lib/k8s/server-control.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
import k8s from "@kubernetes/client-node";
|
||||
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;
|
||||
|
||||
export async function startServer(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;
|
||||
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 === 1)
|
||||
return res.status(409).send("Server already started!");
|
||||
dep.spec.replicas = 1;
|
||||
k8sDeps.replaceNamespacedDeployment(`mcl-${name}`, namespace, dep);
|
||||
res.sendStatus(200);
|
||||
}
|
||||
|
||||
export async function stopServer(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;
|
||||
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);
|
||||
}
|
||||
|
||||
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 async function getServers(req, res) {
|
||||
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
|
||||
const deployments = deploymentRes.body.items;
|
||||
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);
|
||||
metrics = null;
|
||||
started = !!s.spec.replicas;
|
||||
const pod = podMetricsResponse.items.find(({ metadata: md }) => {
|
||||
return md.labels && md.labels.app && md.labels.app === `mcl-${name}-app`;
|
||||
});
|
||||
if (pod) {
|
||||
const podCpus = pod.containers.map(
|
||||
({ usage }) => parseInt(usage.cpu) / 1_000_000
|
||||
);
|
||||
const podMems = pod.containers.map(
|
||||
({ usage }) => parseInt(usage.memory) / 1024
|
||||
);
|
||||
metrics = {
|
||||
cpu: Math.ceil(podCpus.reduce((a, b) => a + b)),
|
||||
memory: Math.ceil(podMems.reduce((a, b) => a + b)),
|
||||
};
|
||||
}
|
||||
|
||||
return { name, metrics, started };
|
||||
});
|
||||
var clusterMetrics = { cpu: 0, memory: 0 };
|
||||
if (servers.length > 1) {
|
||||
const clusterCpu = servers
|
||||
.map(({ metrics }) => (metrics ? metrics.cpu : 0))
|
||||
.reduce((a, b) => a + b);
|
||||
const clusterMem = servers
|
||||
.map(({ metrics }) => (metrics ? metrics.memory : 0))
|
||||
.reduce((a, b) => a + b);
|
||||
clusterMetrics = { cpu: clusterCpu, memory: clusterMem };
|
||||
}
|
||||
res.json({ servers, clusterMetrics });
|
||||
}
|
171
lib/k8s/server-create.js
Normal file
171
lib/k8s/server-create.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import bcrypt from "bcrypt";
|
||||
import k8s from "@kubernetes/client-node";
|
||||
import yaml from "js-yaml";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
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";
|
||||
}
|
||||
|
||||
function createRconSecret(serverSpec) {
|
||||
const { name } = serverSpec;
|
||||
const rconYaml = yaml.load(
|
||||
fs.readFileSync(path.resolve("lib/k8s/configs/rcon-secret.yml"), "utf8")
|
||||
);
|
||||
|
||||
// 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;
|
||||
return rconYaml;
|
||||
}
|
||||
|
||||
function createServerVolume(serverSpec) {
|
||||
const { name } = serverSpec;
|
||||
const volumeYaml = yaml.load(
|
||||
fs.readFileSync(path.resolve("lib/k8s/configs/server-pvc.yml"), "utf8")
|
||||
);
|
||||
volumeYaml.metadata.labels.service = `mcl-${name}-server`;
|
||||
volumeYaml.metadata.name = `mcl-${name}-volume`;
|
||||
volumeYaml.metadata.namespace = namespace;
|
||||
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
|
||||
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"
|
||||
).persistentVolumeClaim.claimName = `mcl-${name}-volume`;
|
||||
deployYaml.spec.template.spec.containers[0] = serverContainer;
|
||||
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;
|
||||
serviceYaml.metadata.labels.app = `mcl-${name}-app`;
|
||||
serviceYaml.metadata.name = `mcl-${name}-server`;
|
||||
serviceYaml.metadata.namespace = namespace;
|
||||
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")
|
||||
);
|
||||
rconSvcYaml.metadata.labels.app = `mcl-${name}-app`;
|
||||
rconSvcYaml.metadata.name = `mcl-${name}-rcon`;
|
||||
rconSvcYaml.metadata.namespace = namespace;
|
||||
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;
|
||||
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!");
|
||||
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!");
|
||||
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.createNamespacedSecret(namespace, rconSecret);
|
||||
k8sCore.createNamespacedService(namespace, serverService);
|
||||
k8sCore.createNamespacedService(namespace, rconService);
|
||||
k8sDeps.createNamespacedDeployment(namespace, serverDeploy);
|
||||
|
||||
res.sendStatus(200);
|
||||
}
|
55
lib/k8s/server-delete.js
Normal file
55
lib/k8s/server-delete.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
import k8s from "@kubernetes/client-node";
|
||||
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 namespace = process.env.MCL_SERVER_NAMESPACE;
|
||||
|
||||
const deleteError = (res) => (err) => {
|
||||
res.status(500).send("Error deleting a resource!");
|
||||
ERR("K8S", "An error occurred while deleting a resource", err);
|
||||
};
|
||||
|
||||
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!");
|
||||
// Delete in reverse order
|
||||
const deleteDeploy = k8sDeps.deleteNamespacedDeployment(
|
||||
`mcl-${serverSpec.name}`,
|
||||
namespace
|
||||
);
|
||||
const deleteService = k8sCore.deleteNamespacedService(
|
||||
`mcl-${name}-server`,
|
||||
namespace
|
||||
);
|
||||
const deleteRconService = k8sCore.deleteNamespacedService(
|
||||
`mcl-${name}-rcon`,
|
||||
namespace
|
||||
);
|
||||
await deleteDeploy.catch(deleteError(res));
|
||||
const deleteRconSecret = k8sCore.deleteNamespacedSecret(
|
||||
`mcl-${name}-rcon-secret`,
|
||||
namespace
|
||||
);
|
||||
const deleteVolume = k8sCore.deleteNamespacedPersistentVolumeClaim(
|
||||
`mcl-${name}-volume`,
|
||||
namespace
|
||||
);
|
||||
Promise.all([
|
||||
deleteService,
|
||||
deleteRconService,
|
||||
deleteRconSecret,
|
||||
deleteVolume,
|
||||
])
|
||||
.then(() => res.sendStatus(200))
|
||||
.catch(deleteError(res));
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue