[FEATURE} Adjust error handling and bump versions

This commit is contained in:
Dunemask 2023-12-08 14:19:02 -07:00
parent d47a8c3cc4
commit 360dd32860
19 changed files with 1052 additions and 455 deletions

8
dist/app.js vendored
View file

@ -1,4 +1,3 @@
import stream from "stream";
import k8s from "@kubernetes/client-node"; import k8s from "@kubernetes/client-node";
import Minecluster from "../lib/Minecluster.js"; import Minecluster from "../lib/Minecluster.js";
const mcl = new Minecluster(); const mcl = new Minecluster();
@ -7,11 +6,6 @@ mcl.start();
async function main(){ async function main(){
const kc = new k8s.KubeConfig(); const kc = new k8s.KubeConfig();
kc.loadFromDefault(); kc.loadFromDefault();
/*const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
const res = await k8sApi.listNamespacedPod('mc-garden-default');
const pods = res.body.items.map((vp1) => vp1.metadata.name);
console.log(pods);*/
} }
main().catch((e)=>{console.log(e)}); main().catch((e)=>{console.log(e)});

View file

@ -7,6 +7,7 @@ import { INFO, OK, logInfo } from "./util/logging.js";
// Import Core Modules // Import Core Modules
import buildRoutes from "./server/router.js"; import buildRoutes from "./server/router.js";
import injectSockets from "./server/sockets.js"; import injectSockets from "./server/sockets.js";
import pg from "./database/postgres.js";
// Constants // Constants
const title = "MCL"; const title = "MCL";
@ -23,6 +24,7 @@ export default class Minecluster {
logInfo(fig.textSync(title, "Larry 3D")); logInfo(fig.textSync(title, "Larry 3D"));
INFO("INIT", "Initializing..."); INFO("INIT", "Initializing...");
this.app = express(); this.app = express();
this.pg = pg;
this.server = http.createServer(this.app); this.server = http.createServer(this.app);
this.sockets = injectSockets(this.server, this.jobs); this.sockets = injectSockets(this.server, this.jobs);
this.routes = buildRoutes(this.sockets); this.routes = buildRoutes(this.sockets);
@ -31,11 +33,12 @@ export default class Minecluster {
} }
async _connect() { async _connect() {
// await this.pg.connect(); await this.pg.connect();
} }
start() { start() {
const mcl = this; const mcl = this;
return new Promise(async function init(res) { return new Promise(async function init(res) {
mcl._preinitialize(); mcl._preinitialize();
await mcl._connect(); await mcl._connect();

View file

@ -0,0 +1,8 @@
/*CREATE SEQUENCE servers_id_seq;
CREATE TABLE servers (
id bigint NOT NULL DEFAULT nextval('servers_id_seq') PRIMARY KEY,
name varchar(255) DEFAULT NULL,
host varchar(255) DEFAULT NULL,
CONSTRAINT unique_host UNIQUE(host)
);
ALTER SEQUENCE servers_id_seq OWNED BY servers.id;*/

121
lib/database/pg-query.js Normal file
View file

@ -0,0 +1,121 @@
const buildPostgresEntry = (entry) => {
const pgEntry = { ...entry };
Object.keys(pgEntry).forEach((col) => {
if (pgEntry[col] === undefined) delete pgEntry[col];
});
return pgEntry;
};
export const buildPostgresValue = (jsVar) => {
if (jsVar === null) return "null";
if (typeof jsVar === "string") return buildPostgresString(jsVar);
if (Array.isArray(jsVar) && jsVar.length === 0) return "null";
if (Array.isArray(jsVar) && isTypeArray(jsVar, "string"))
return buildPostgresStringArray(jsVar);
return jsVar;
};
const buildPostgresStringArray = (jsonArray) => {
if (jsonArray.length === 0) return null;
var pgArray = [...jsonArray];
var arrayString = "ARRAY [";
pgArray.forEach((e, i) => (pgArray[i] = `'${e}'`));
arrayString += pgArray.join(",");
arrayString += "]";
return arrayString;
};
const isTypeArray = (jsonArray, type) =>
jsonArray.every((e) => typeof e === type);
const buildPostgresString = (jsonString) =>
(jsonString && `'${jsonString.replaceAll("'", "''")}'`) || null;
export const insertQuery = (table, jsEntry) => {
if (typeof jsEntry !== "object") throw Error("PG Inserts must be objects!");
const entry = buildPostgresEntry(jsEntry);
const cols = Object.keys(entry);
cols.forEach((col, i) => {
entry[col] = buildPostgresValue(entry[col]);
cols[i] = `"${col}"`;
});
var query = `INSERT INTO ${table}(${cols.join(",")})\n`;
query += `VALUES(${Object.values(entry).join(",")})`;
return query;
};
export const deleteQuery = (table, jsEntry) => {
if (typeof jsEntry !== "object")
throw Error("PG Delete conditionals must be an object!");
const entry = buildPostgresEntry(jsEntry);
const cols = Object.keys(entry);
const conditionals = [];
for (var col of cols) {
entry[col] = buildPostgresValue(entry[col]);
if (entry[col] === "null") conditionals.push(`x.${col} IS NULL`);
else conditionals.push(`x.${col}=${entry[col]}`);
}
return `DELETE FROM ${table} x WHERE ${conditionals.join(" AND ")}`;
};
export const onConflictUpdate = (conflicts, updates) => {
if (!Array.isArray(conflicts)) throw Error("PG Conflicts must be an array!");
if (typeof updates !== "object") throw Error("PG Updates must be objects!");
const entry = buildPostgresEntry(updates);
var query = `ON CONFLICT (${conflicts.join(",")}) DO UPDATE SET\n`;
const cols = Object.keys(entry);
for (var col of cols) {
entry[col] = buildPostgresValue(entry[col]);
}
query += cols.map((c) => `${c}=${entry[c]}`).join(",");
return query;
};
export const clearTableQuery = (table) => {
return `TRUNCATE ${table}`;
};
export const selectWhereQuery = (table, jsEntry, joinWith) => {
if (typeof jsEntry !== "object") throw Error("PG Where must be an object!");
const where = buildPostgresEntry(jsEntry);
const cols = Object.keys(where);
var query = `SELECT * FROM ${table} AS x WHERE\n`;
for (var col of cols) {
where[col] = buildPostgresValue(where[col]);
}
return (query += cols.map((c) => `x.${c}=${where[c]}`).join(joinWith));
};
export const updateWhereQuery = (table, updates, wheres, joinWith) => {
if (typeof updates !== "object") throw Error("PG Updates must be an object!");
if (typeof wheres !== "object") throw Error("PG Wheres must be an object!");
const update = buildPostgresEntry(updates);
const where = buildPostgresEntry(wheres);
const updateCols = Object.keys(update);
const whereCols = Object.keys(where);
var query = `UPDATE ${table}\n`;
var updateQuery = updateCols
.map((c) => `${c} = ${buildPostgresValue(update[c])}`)
.join(",");
var whereQuery = whereCols
.map((c) => `${c} = ${buildPostgresValue(where[c])}`)
.join(joinWith);
return (query += `SET ${updateQuery} WHERE ${whereQuery}`);
};
export const updateWhereAnyQuery = (table, updates, wheres) =>
updateWhereQuery(table, updates, wheres, " OR ");
export const updateWhereAllQuery = (table, updates, wheres) =>
updateWhereQuery(table, updates, wheres, " AND ");
export const selectWhereAnyQuery = (table, where) =>
selectWhereQuery(table, where, " OR ");
export const selectWhereAllQuery = (table, where) =>
selectWhereQuery(table, where, " AND ");
export default {
selectWhereAnyQuery,
selectWhereAllQuery,
updateWhereAnyQuery,
updateWhereAllQuery,
insertQuery,
deleteQuery,
buildPostgresValue,
onConflictUpdate,
clearTableQuery,
};

63
lib/database/postgres.js Normal file
View file

@ -0,0 +1,63 @@
// Imports
import path from "node:path";
import { URL } from "node:url";
import { migrate } from "postgres-migrations";
import createPgp from "pg-promise";
import moment from "moment";
import { INFO, WARN, OK, VERB } from "../util/logging.js";
// Environment Variables
const {
MCL_POSTGRES_DATABASE: database,
MCL_POSTGRES_ENABLED: pgEnabled,
MCL_POSTGRES_HOST: host,
MCL_POSTGRES_PASSWORD: password,
MCL_POSTGRES_PORT: port,
MCL_POSTGRES_USER: user,
} = process.env;
// Postgres-promise Configuration
// Ensure dates get saved as UTC date strings
// This prevents the parser from doing strange datetime operations
const pgp = createPgp();
pgp.pg.types.setTypeParser(1114, (str) => moment.utc(str).format());
// Database Config
const dbConfig = {
database: database ?? "minecluster",
user: user ?? "postgres",
password: password ?? "postgres",
host: host ?? "localhost",
port: port ?? 5432,
ensureDatabaseExists: true,
};
const databaseDir = new URL(".", import.meta.url).pathname;
const migrationsDir = path.resolve(databaseDir, "migrations/");
const queryMock = (str) => INFO("POSTGRES MOCK", str);
const connect = (pg) => async () => {
if (pgEnabled === "false") {
WARN("POSTGRES", "Postgres Disabled!");
return { query: queryMock };
}
VERB("POSTGRES", "Migrating...");
await migrate(dbConfig, migrationsDir);
// Override fake methods
const pgInstance = pgp(dbConfig);
for (var k in pgInstance) pg[k] = pgInstance[k];
VERB("POSTGRES", "Migrated Successfully!");
await pg.connect();
VERB("POSTGRES", "Postgres connected Successfully!");
OK("POSTGRES", `Connected to database ${dbConfig.database}!`);
};
const buildPostgres = () => {
var pg = { query: queryMock };
pg.connect = connect(pg);
return pg;
};
export default buildPostgres();

View file

@ -18,7 +18,7 @@ export default async function liveLogging(socket, serverNamespace) {
const log = new k8s.Log(kc); const log = new k8s.Log(kc);
const logStream = new stream.PassThrough(); const logStream = new stream.PassThrough();
logStream.on("data", (chunk) => logStream.on("data", (chunk) =>
socket.emit("push", Buffer.from(chunk).toString()) socket.emit("push", Buffer.from(chunk).toString()),
); );
log log
.log(serverNamespace, mcsPods[0], containerName, logStream, { .log(serverNamespace, mcsPods[0], containerName, logStream, {

View file

@ -8,21 +8,35 @@ const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
const k8sMetrics = new k8s.Metrics(kc); const k8sMetrics = new k8s.Metrics(kc);
const namespace = process.env.MCL_SERVER_NAMESPACE; 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}`);
}
}
export async function startServer(req, res) { export async function startServer(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
if (!serverSpec) return res.sendStatus(400); if (!serverSpec) return res.sendStatus(400);
if (!serverSpec.name) return res.status(400).send("Server name required!"); if (!serverSpec.name) return res.status(400).send("Server name required!");
const { name } = serverSpec; const { name } = serverSpec;
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); const dep = await findDeployment(name);
const dep = deploymentRes.body.items.find(
(i) => i.metadata.name === `mcl-${name}` if (!dep || !dep.spec) return res.status(409).send("Server does not exist!");
);
if (!dep) return res.status(409).send("Server does not exist!");
if (dep.spec.replicas === 1) if (dep.spec.replicas === 1)
return res.status(409).send("Server already started!"); return res.status(409).send("Server already started!");
dep.spec.replicas = 1; dep.spec.replicas = 1;
k8sDeps.replaceNamespacedDeployment(`mcl-${name}`, namespace, dep); k8sDeps
res.sendStatus(200); .replaceNamespacedDeployment(`mcl-${name}`, namespace, dep)
.then(() => res.sendStatus(200))
.catch((e) => {
ERR("SERVER CONTROL", e);
res.status(500).send("Error updating server!");
});
} }
export async function stopServer(req, res) { export async function stopServer(req, res) {
@ -32,7 +46,7 @@ export async function stopServer(req, res) {
const { name } = serverSpec; const { name } = serverSpec;
const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace); const deploymentRes = await k8sDeps.listNamespacedDeployment(namespace);
const dep = deploymentRes.body.items.find( const dep = deploymentRes.body.items.find(
(i) => i.metadata.name === `mcl-${name}` (i) => i.metadata.name === `mcl-${name}`,
); );
if (!dep) return res.status(409).send("Server does not exist!"); if (!dep) return res.status(409).send("Server does not exist!");
if (dep.spec.replicas === 0) if (dep.spec.replicas === 0)
@ -56,7 +70,7 @@ export async function getServers(req, res) {
const podMetricsResponse = await k8sMetrics.getPodMetrics(namespace); const podMetricsResponse = await k8sMetrics.getPodMetrics(namespace);
// TODO Add an annotation and manage using that // TODO Add an annotation and manage using that
const serverDeployments = deployments.filter((d) => const serverDeployments = deployments.filter((d) =>
d.metadata.name.startsWith("mcl-") d.metadata.name.startsWith("mcl-"),
); );
var name, metrics, started; var name, metrics, started;
const servers = serverDeployments.map((s) => { const servers = serverDeployments.map((s) => {
@ -68,10 +82,10 @@ export async function getServers(req, res) {
}); });
if (pod) { if (pod) {
const podCpus = pod.containers.map( const podCpus = pod.containers.map(
({ usage }) => parseInt(usage.cpu) / 1_000_000 ({ usage }) => parseInt(usage.cpu) / 1_000_000,
); );
const podMems = pod.containers.map( const podMems = pod.containers.map(
({ usage }) => parseInt(usage.memory) / 1024 ({ usage }) => parseInt(usage.memory) / 1024,
); );
metrics = { metrics = {
cpu: Math.ceil(podCpus.reduce((a, b) => a + b)), cpu: Math.ceil(podCpus.reduce((a, b) => a + b)),

View file

@ -30,7 +30,7 @@ function payloadFilter(req, res) {
function createRconSecret(serverSpec) { function createRconSecret(serverSpec) {
const { name } = serverSpec; const { name } = serverSpec;
const rconYaml = yaml.load( const rconYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/rcon-secret.yml"), "utf8") fs.readFileSync(path.resolve("lib/k8s/configs/rcon-secret.yml"), "utf8"),
); );
// TODO: Dyamic rconPassword // TODO: Dyamic rconPassword
@ -45,7 +45,7 @@ function createRconSecret(serverSpec) {
function createServerVolume(serverSpec) { function createServerVolume(serverSpec) {
const { name } = serverSpec; const { name } = serverSpec;
const volumeYaml = yaml.load( const volumeYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/server-pvc.yml"), "utf8") fs.readFileSync(path.resolve("lib/k8s/configs/server-pvc.yml"), "utf8"),
); );
volumeYaml.metadata.labels.service = `mcl-${name}-server`; volumeYaml.metadata.labels.service = `mcl-${name}-server`;
volumeYaml.metadata.name = `mcl-${name}-volume`; volumeYaml.metadata.name = `mcl-${name}-volume`;
@ -72,8 +72,8 @@ function createServerDeploy(serverSpec) {
const deployYaml = yaml.load( const deployYaml = yaml.load(
fs.readFileSync( fs.readFileSync(
path.resolve("lib/k8s/configs/server-deployment.yml"), path.resolve("lib/k8s/configs/server-deployment.yml"),
"utf8" "utf8",
) ),
); );
deployYaml.metadata.name = `mcl-${name}`; deployYaml.metadata.name = `mcl-${name}`;
deployYaml.metadata.namespace = namespace; deployYaml.metadata.namespace = namespace;
@ -83,30 +83,25 @@ function createServerDeploy(serverSpec) {
deployYaml.spec.template.spec.containers.splice(0, 1); //TODO: Currently removing backup container deployYaml.spec.template.spec.containers.splice(0, 1); //TODO: Currently removing backup container
const serverContainer = deployYaml.spec.template.spec.containers[0]; const serverContainer = deployYaml.spec.template.spec.containers[0];
const findEnv = (k) => serverContainer.env.find(({ name: n }) => n === k);
const updateEnv = (k, v) => (findEnv.value = v);
// Enviornment variables // Enviornment variables
serverContainer.env.find(({ name: n }) => n === "TYPE").value = serverType; updateEnv("TYPE", serverType);
serverContainer.env.find(({ name: n }) => n === "VERSION").value = version; updateEnv("VERSION", version);
serverContainer.env.find(({ name: n }) => n === "DIFFICULTY").value = updateEnv("DIFFICULTY", difficulty);
difficulty; updateEnv("MODE", gamemode);
serverContainer.env.find(({ name: n }) => n === "MODE").value = gamemode; updateEnv("MOTD", motd);
serverContainer.env.find(({ name: n }) => n === "MOTD").value = motd; updateEnv("MAX_PLAYERS", maxPlayers);
serverContainer.env.find(({ name: n }) => n === "MAX_PLAYERS").value = updateEnv("SEED", seed);
maxPlayers; updateEnv("OPS", ops);
serverContainer.env.find(({ name: n }) => n === "SEED").value = seed; updateEnv("WHITELIST", whitelist);
serverContainer.env.find(({ name: n }) => n === "OPS").value = ops; updateEnv("MEMORY", `${memory}M`);
serverContainer.env.find(({ name: n }) => n === "WHITELIST").value =
whitelist; if (version !== "VANILLA") delete findEnv("MODPACK").value;
serverContainer.env.find( else updateEnv("MODPACK", modpack);
({ name: n }) => n === "MEMORY" findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name =
).value = `${memory}M`; `mcl-${name}-rcon-secret`;
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 // Server Container Name
serverContainer.name = `mcl-${name}`; serverContainer.name = `mcl-${name}`;
// Resources // Resources
@ -114,7 +109,7 @@ function createServerDeploy(serverSpec) {
// serverContainer.resources.limits.memory = `${memory}Mi`; // TODO Allow for limits beyond initial startup // 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`;
deployYaml.spec.template.spec.containers[0] = serverContainer; deployYaml.spec.template.spec.containers[0] = serverContainer;
return deployYaml; return deployYaml;
@ -123,7 +118,7 @@ function createServerDeploy(serverSpec) {
function createServerService(serverSpec) { function createServerService(serverSpec) {
const { name, url } = serverSpec; const { name, url } = serverSpec;
const serviceYaml = yaml.load( const serviceYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/server-svc.yml"), "utf8") fs.readFileSync(path.resolve("lib/k8s/configs/server-svc.yml"), "utf8"),
); );
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = url; serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = url;
serviceYaml.metadata.labels.app = `mcl-${name}-app`; serviceYaml.metadata.labels.app = `mcl-${name}-app`;
@ -136,7 +131,7 @@ function createServerService(serverSpec) {
function createRconService(serverSpec) { function createRconService(serverSpec) {
const { name, url } = serverSpec; const { name, url } = serverSpec;
const rconSvcYaml = yaml.load( const rconSvcYaml = yaml.load(
fs.readFileSync(path.resolve("lib/k8s/configs/rcon-svc.yml"), "utf8") fs.readFileSync(path.resolve("lib/k8s/configs/rcon-svc.yml"), "utf8"),
); );
rconSvcYaml.metadata.labels.app = `mcl-${name}-app`; rconSvcYaml.metadata.labels.app = `mcl-${name}-app`;
rconSvcYaml.metadata.name = `mcl-${name}-rcon`; rconSvcYaml.metadata.name = `mcl-${name}-rcon`;

View file

@ -25,24 +25,24 @@ export default async function deleteServer(req, res) {
// Delete in reverse order // Delete in reverse order
const deleteDeploy = k8sDeps.deleteNamespacedDeployment( const deleteDeploy = k8sDeps.deleteNamespacedDeployment(
`mcl-${serverSpec.name}`, `mcl-${serverSpec.name}`,
namespace namespace,
); );
const deleteService = k8sCore.deleteNamespacedService( const deleteService = k8sCore.deleteNamespacedService(
`mcl-${name}-server`, `mcl-${name}-server`,
namespace namespace,
); );
const deleteRconService = k8sCore.deleteNamespacedService( const deleteRconService = k8sCore.deleteNamespacedService(
`mcl-${name}-rcon`, `mcl-${name}-rcon`,
namespace namespace,
); );
await deleteDeploy.catch(deleteError(res)); await deleteDeploy.catch(deleteError(res));
const deleteRconSecret = k8sCore.deleteNamespacedSecret( const deleteRconSecret = k8sCore.deleteNamespacedSecret(
`mcl-${name}-rcon-secret`, `mcl-${name}-rcon-secret`,
namespace namespace,
); );
const deleteVolume = k8sCore.deleteNamespacedPersistentVolumeClaim( const deleteVolume = k8sCore.deleteNamespacedPersistentVolumeClaim(
`mcl-${name}-volume`, `mcl-${name}-volume`,
namespace namespace,
); );
Promise.all([ Promise.all([
deleteService, deleteService,

17
lib/routes/error-route.js Normal file
View file

@ -0,0 +1,17 @@
export function logErrors(err, req, res, next) {
console.error(err.stack);
next(err);
}
export function clientErrorHandler(err, req, res, next) {
if (req.xhr) {
res.status(500).send({ error: "Something failed!" });
} else {
next(err);
}
}
export function errorHandler(err, req, res, next) {
res.status(500);
res.render("error", { error: err });
}

View file

@ -3,6 +3,6 @@ import path from "path";
const router = Router(); const router = Router();
router.use("/", express.static(path.resolve("./build"))); router.use("/", express.static(path.resolve("./build")));
router.get("/*", (req, res) => router.get("/*", (req, res) =>
res.sendFile(path.resolve("./build/index.html")) res.sendFile(path.resolve("./build/index.html")),
); );
export default router; export default router;

View file

@ -7,7 +7,7 @@ kc.loadFromDefault();
const k8sApi = kc.makeApiClient(k8s.CoreV1Api); const k8sApi = kc.makeApiClient(k8s.CoreV1Api);
// Get Routes // Get Routes
router.get("/available", (req, res) => { router.get("/available", (req, res) => {
return res.json({cpu: 8000, memory: 16000}); return res.json({ cpu: 8000, memory: 16000 });
// TODO Workaround to detect available // TODO Workaround to detect available
k8sApi.listNode().then((nodeRes) => { k8sApi.listNode().then((nodeRes) => {
const nodeAllocatable = nodeRes.body.items.map((i) => i.status.allocatable); const nodeAllocatable = nodeRes.body.items.map((i) => i.status.allocatable);

View file

@ -13,7 +13,7 @@ export default async function rconInterface(socket) {
const rconRes = await k8sCore.readNamespacedSecret(rconSecret, namespace); const rconRes = await k8sCore.readNamespacedSecret(rconSecret, namespace);
const rconPassword = Buffer.from( const rconPassword = Buffer.from(
rconRes.body.data["rcon-password"], rconRes.body.data["rcon-password"],
"base64" "base64",
).toString("utf8"); ).toString("utf8");
const rconHost = `mcl-${socket.mcs.serverName}-rcon`; const rconHost = `mcl-${socket.mcs.serverName}-rcon`;
const rcon = new RconClient({ const rcon = new RconClient({

View file

@ -6,6 +6,11 @@ import vitals from "../routes/vitals-route.js";
import systemRoute from "../routes/system-route.js"; import systemRoute from "../routes/system-route.js";
import serverRoute from "../routes/server-route.js"; import serverRoute from "../routes/server-route.js";
import reactRoute from "../routes/react-route.js"; import reactRoute from "../routes/react-route.js";
import {
logErrors,
clientErrorHandler,
errorHandler,
} from "../routes/error-route.js";
export default function buildRoutes(pg, skio) { export default function buildRoutes(pg, skio) {
const router = express.Router(); const router = express.Router();
@ -18,7 +23,10 @@ export default function buildRoutes(pg, skio) {
// Routes // Routes
router.use("/api/system", systemRoute); router.use("/api/system", systemRoute);
router.use("/api/server", serverRoute); router.use("/api/server", serverRoute);
router.use(["/mcl","/mcl/*"], reactRoute); // Static Build Route router.use(["/mcl", "/mcl/*"], reactRoute); // Static Build Route
/*router.use(logErrors);
router.use(clientErrorHandler);
router.use(errorHandler);*/
return router; return router;
} }

View file

@ -32,4 +32,3 @@ const storage = multerS3({
}); });
export const upload = multer({ storage }); export const upload = multer({ storage });

1096
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -24,30 +24,33 @@
"devDependencies": { "devDependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.12", "@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.12", "@mui/material": "^5.14.20",
"@tanstack/react-query": "^4.36.1", "@tanstack/react-query": "^5.12.2",
"@vitejs/plugin-react": "^4.1.0", "@vitejs/plugin-react": "^4.2.1",
"concurrently": "^8.2.1", "concurrently": "^8.2.2",
"nodemon": "^3.0.1", "nodemon": "^3.0.2",
"prettier": "^3.0.3", "prettier": "^3.1.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^6.16.0", "react-router-dom": "^6.20.1",
"socket.io-client": "^4.7.2", "socket.io-client": "^4.7.2",
"vite": "^4.4.11" "vite": "^5.0.7"
}, },
"dependencies": { "dependencies": {
"@kubernetes/client-node": "^0.19.0", "@kubernetes/client-node": "^0.20.0",
"aws-sdk": "^2.1472.0", "aws-sdk": "^2.1514.0",
"bcrypt": "^5.1.1", "bcrypt": "^5.1.1",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"express": "^4.18.2", "express": "^4.18.2",
"figlet": "^1.6.0", "figlet": "^1.7.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"multer-s3": "^3.0.1", "multer-s3": "^3.0.1",
"rcon-client": "^4.2.3", "pg-promise": "^11.5.4",
"postgres-migrations": "^5.3.0",
"rcon-client": "^4.2.4",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"uuid": "^9.0.1" "uuid": "^9.0.1"
} }

View file

@ -12,16 +12,16 @@ const fetchApiPost = (subPath, json) => async () =>
}).then((res) => res.json()); }).then((res) => res.json());
export const useServerStatus = (server) => export const useServerStatus = (server) =>
useQuery( useQuery({
[`server-status-${server}`], queryKey: [`server-status-${server}`],
fetchApiPost("/server/status", { name: server }), queryFn: fetchApiPost("/server/status", { name: server }),
); });
export const useServerMetrics = (server) => export const useServerMetrics = (server) =>
useQuery( useQuery({
[`server-metrics-${server}`], queryKey: [`server-metrics-${server}`],
fetchApiPost("/server/metrics", { name: server }), queryFn: fetchApiPost("/server/metrics", { name: server }),
{ refetchInterval: 10000 }, refetchInterval: 10000,
); });
export const useStartServer = (server) => export const useStartServer = (server) =>
postJsonApi("/server/start", { name: server }, "server-instances"); postJsonApi("/server/start", { name: server }, "server-instances");
export const useStopServer = (server) => export const useStopServer = (server) =>
@ -31,19 +31,26 @@ export const useDeleteServer = (server) =>
export const useCreateServer = (spec) => export const useCreateServer = (spec) =>
postJsonApi("/server/create", spec, "server-list"); postJsonApi("/server/create", spec, "server-list");
export const useServerList = () => export const useServerList = () =>
useQuery(["server-list"], fetchApi("/server/list")); useQuery({ queryKey: ["server-list"], queryFn: fetchApi("/server/list") });
export const useServerInstances = () => export const useServerInstances = () =>
useQuery(["server-instances"], fetchApi("/server/instances"), { useQuery({
queryKey: ["server-instances"],
queryFn: fetchApi("/server/instances"),
refetchInterval: 5000, refetchInterval: 5000,
}); });
export const useSystemAvailable = () => export const useSystemAvailable = () =>
useQuery(["system-available"], fetchApi("/system/available")); useQuery({
queryKey: ["system-available"],
queryFn: fetchApi("/system/available"),
});
export const useVersionList = () => export const useVersionList = () =>
useQuery(["minecraft-versions"], () => useQuery({
fetch("https://piston-meta.mojang.com/mc/game/version_manifest.json").then( queryKey: ["minecraft-versions"],
(r) => r.json(), queryFn: () =>
), fetch(
); "https://piston-meta.mojang.com/mc/game/version_manifest.json",
).then((r) => r.json()),
});
const postJsonApi = (subPath, body, invalidate, method = "POST") => { const postJsonApi = (subPath, body, invalidate, method = "POST") => {
const qc = useQueryClient(); const qc = useQueryClient();

View file

@ -15,6 +15,7 @@ export default () => {
proxy: { proxy: {
"/api": backendUrl, "/api": backendUrl,
"/socket.io": backendUrl, "/socket.io": backendUrl,
"/healthz": backendUrl,
}, },
hmr: { hmr: {
protocol: process.env.MCL_VITE_DEV_PROTOCOL, protocol: process.env.MCL_VITE_DEV_PROTOCOL,