[FEATURE] Live Modifications, Host Safety, Minor Tweaks (#19)
Co-authored-by: Dunemask <dunemask@gmail.com> Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/19
This commit is contained in:
parent
0a0f9c8463
commit
fc60df27ac
9 changed files with 154 additions and 31 deletions
2
dist/app.js
vendored
2
dist/app.js
vendored
|
@ -8,4 +8,4 @@ const kc = new k8s.KubeConfig();
|
||||||
kc.loadFromDefault();
|
kc.loadFromDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((e)=>{console.log(e)});
|
main().catch((e)=>{console.error(e)});
|
||||||
|
|
|
@ -9,6 +9,8 @@ import {
|
||||||
import ExpressClientError, { sendError } from "../util/ExpressClientError.js";
|
import ExpressClientError, { sendError } from "../util/ExpressClientError.js";
|
||||||
import { toggleServer } from "../k8s/k8s-server-control.js";
|
import { toggleServer } from "../k8s/k8s-server-control.js";
|
||||||
import { checkAuthorization } from "../database/queries/server-queries.js";
|
import { checkAuthorization } from "../database/queries/server-queries.js";
|
||||||
|
import { WARN } from "../util/logging.js";
|
||||||
|
import modifyServerResources from "../k8s/server-modify.js";
|
||||||
|
|
||||||
const dnsRegex = new RegExp(
|
const dnsRegex = new RegExp(
|
||||||
`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`,
|
`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`,
|
||||||
|
@ -69,6 +71,9 @@ function payloadFilter(req, res) {
|
||||||
return res
|
return res
|
||||||
.status(400)
|
.status(400)
|
||||||
.send("Extra ports must be a list of strings with length of 5!");
|
.send("Extra ports must be a list of strings with length of 5!");
|
||||||
|
if (host !== host.toLowerCase())
|
||||||
|
WARN("CREATE", "Host automatically being lowercasified...");
|
||||||
|
req.body.host = host.toLowerCase();
|
||||||
return "filtered";
|
return "filtered";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,10 +163,15 @@ export async function getServer(req, res) {
|
||||||
export async function modifyServer(req, res) {
|
export async function modifyServer(req, res) {
|
||||||
if (payloadFilter(req, res) !== "filtered") return;
|
if (payloadFilter(req, res) !== "filtered") return;
|
||||||
const serverSpec = req.body;
|
const serverSpec = req.body;
|
||||||
|
if (!!serverSpec.host)
|
||||||
|
WARN(
|
||||||
|
"MODIFY",
|
||||||
|
"Warning, hostname changing is not implimented yet! Please ask the developer if you'd like to see this added!",
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await checkServerId(req.cairoId, serverSpec);
|
await checkServerId(req.cairoId, serverSpec);
|
||||||
const serverEntry = await modifyServerEntry(serverSpec);
|
const serverEntry = await modifyServerEntry(serverSpec);
|
||||||
// await createServerResources(serverEntry);
|
await modifyServerResources(serverEntry);
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sendError(res)(e);
|
sendError(res)(e);
|
||||||
|
|
|
@ -165,7 +165,7 @@ export async function modifyServerEntry(serverSpec) {
|
||||||
id,
|
id,
|
||||||
// ownerCairoId: owner_cairo_id, // DIsabled! If these becomes a reqest, please create a new function!
|
// ownerCairoId: owner_cairo_id, // DIsabled! If these becomes a reqest, please create a new function!
|
||||||
name,
|
name,
|
||||||
host,
|
// host, // TODO: Can only be updated if service name is generic and non descriptive
|
||||||
version,
|
version,
|
||||||
serverType: server_type,
|
serverType: server_type,
|
||||||
cpu, // TODO: Ignored for now by the K8S manifests
|
cpu, // TODO: Ignored for now by the K8S manifests
|
||||||
|
@ -180,28 +180,66 @@ export async function modifyServerEntry(serverSpec) {
|
||||||
backupInterval: backup_interval,
|
backupInterval: backup_interval,
|
||||||
} = serverSpec;
|
} = serverSpec;
|
||||||
|
|
||||||
const q = updateWhereAllQuery(
|
const q =
|
||||||
table,
|
updateWhereAllQuery(
|
||||||
{
|
table,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
// host, // TODO: Can only be updated if service name is generic and non descriptive
|
||||||
|
version,
|
||||||
|
server_type,
|
||||||
|
cpu, // TODO: Ignored for now by the K8S manifests
|
||||||
|
memory,
|
||||||
|
// storage, // DO NOT INCLUDE THIS KEY, Not all storage providers in kubernetes allow for dynamically resizable PVCs
|
||||||
|
extra_ports,
|
||||||
|
backup_enabled,
|
||||||
|
backup_host,
|
||||||
|
backup_bucket_path,
|
||||||
|
backup_id,
|
||||||
|
backup_key,
|
||||||
|
backup_interval,
|
||||||
|
},
|
||||||
|
{ id },
|
||||||
|
) + ` RETURNING *;`;
|
||||||
|
try {
|
||||||
|
const entries = await pg.query(q);
|
||||||
|
const {
|
||||||
name,
|
name,
|
||||||
host,
|
host, // Should always read the database value
|
||||||
|
server_type: serverType,
|
||||||
|
storage,
|
||||||
|
extra_ports: extraPorts,
|
||||||
|
backup_enabled: backupEnabled,
|
||||||
|
backup_host: backupHost,
|
||||||
|
backup_bucket_path: backupPath,
|
||||||
|
backup_id: backupId,
|
||||||
|
backup_key: backupKey,
|
||||||
|
backup_interval: backupInterval,
|
||||||
|
} = entries[0];
|
||||||
|
|
||||||
|
const mclName = getMclName(host, id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name, // Could change
|
||||||
|
mclName, // Shouldn't change
|
||||||
|
id, // Won't change
|
||||||
|
// host, // TODO: Can only be updated if service name is generic and non descriptive
|
||||||
version,
|
version,
|
||||||
server_type,
|
serverType,
|
||||||
cpu, // TODO: Ignored for now by the K8S manifests
|
cpu, // TODO: Ignored for now by the K8S manifests
|
||||||
memory,
|
memory,
|
||||||
// storage, // DO NOT INCLUDE THIS KEY, Not all storage providers in kubernetes allow for dynamically resizable PVCs
|
storage,
|
||||||
extra_ports,
|
extraPorts,
|
||||||
backup_enabled,
|
backupEnabled,
|
||||||
backup_host,
|
backupHost,
|
||||||
backup_bucket_path,
|
backupPath,
|
||||||
backup_id,
|
backupId,
|
||||||
backup_key,
|
backupKey,
|
||||||
backup_interval,
|
backupInterval,
|
||||||
},
|
};
|
||||||
{ id },
|
} catch (e) {
|
||||||
);
|
asExpressClientError(e);
|
||||||
|
}
|
||||||
return pg.query(q);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getServerEntries() {
|
export async function getServerEntries() {
|
||||||
|
|
|
@ -11,8 +11,6 @@ metadata:
|
||||||
namespace: changeme-namespace
|
namespace: changeme-namespace
|
||||||
spec:
|
spec:
|
||||||
internalTrafficPolicy: Cluster
|
internalTrafficPolicy: Cluster
|
||||||
ipFamilies:
|
|
||||||
- IPv4
|
|
||||||
ipFamilyPolicy: SingleStack
|
ipFamilyPolicy: SingleStack
|
||||||
ports: # Programatically add all FTP ports. Port range includes 20, 21, 40000-40001
|
ports: # Programatically add all FTP ports. Port range includes 20, 21, 40000-40001
|
||||||
- name: minecraft
|
- name: minecraft
|
||||||
|
|
|
@ -18,7 +18,7 @@ const namespace = process.env.MCL_SERVER_NAMESPACE;
|
||||||
|
|
||||||
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
|
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
|
||||||
|
|
||||||
function createExtraService(serverSpec) {
|
export function createExtraService(serverSpec) {
|
||||||
const { mclName, id, extraPorts } = serverSpec;
|
const { mclName, id, extraPorts } = serverSpec;
|
||||||
if (!extraPorts) return;
|
if (!extraPorts) return;
|
||||||
const serviceYaml = loadYaml("lib/k8s/configs/extra-svc.yml");
|
const serviceYaml = loadYaml("lib/k8s/configs/extra-svc.yml");
|
||||||
|
@ -49,7 +49,7 @@ function createExtraService(serverSpec) {
|
||||||
return serviceYaml;
|
return serviceYaml;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBackupSecret(serverSpec) {
|
export function createBackupSecret(serverSpec) {
|
||||||
if (!serverSpec.backupEnabled) return; // If backup not defined, don't create RCLONE secret
|
if (!serverSpec.backupEnabled) return; // If backup not defined, don't create RCLONE secret
|
||||||
const { mclName, id, backupId, backupKey, backupHost } = serverSpec;
|
const { mclName, id, backupId, backupKey, backupHost } = serverSpec;
|
||||||
const backupYaml = loadYaml("lib/k8s/configs/backup-secret.yml");
|
const backupYaml = loadYaml("lib/k8s/configs/backup-secret.yml");
|
||||||
|
@ -153,7 +153,7 @@ function createServerDeploy(serverSpec) {
|
||||||
return deployYaml;
|
return deployYaml;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createServerService(serverSpec) {
|
export function createServerService(serverSpec) {
|
||||||
const { mclName, host, id } = serverSpec;
|
const { mclName, host, id } = serverSpec;
|
||||||
const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml");
|
const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml");
|
||||||
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = host;
|
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = host;
|
||||||
|
|
59
lib/k8s/server-modify.js
Normal file
59
lib/k8s/server-modify.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import k8s from "@kubernetes/client-node";
|
||||||
|
import {
|
||||||
|
createExtraService,
|
||||||
|
createBackupSecret,
|
||||||
|
createServerService,
|
||||||
|
} from "./server-create.js";
|
||||||
|
import kc from "./k8s-config.js";
|
||||||
|
import { getServerAssets } from "./k8s-server-control.js";
|
||||||
|
const k8sCore = kc.makeApiClient(k8s.CoreV1Api);
|
||||||
|
const namespace = process.env.MCL_SERVER_NAMESPACE;
|
||||||
|
|
||||||
|
export default async function modifyServerResources(modifySpec) {
|
||||||
|
const { id: serverId } = modifySpec;
|
||||||
|
const serverAssets = await getServerAssets(serverId);
|
||||||
|
const serverService = createServerService(modifySpec);
|
||||||
|
const extraService = createExtraService(modifySpec);
|
||||||
|
const backupSecret = createBackupSecret(modifySpec);
|
||||||
|
const serverResources = [];
|
||||||
|
|
||||||
|
if (!!serverService)
|
||||||
|
// Will Always Exist
|
||||||
|
serverResources.push(
|
||||||
|
k8sCore.replaceNamespacedService(
|
||||||
|
serverAssets.service.metadata.name,
|
||||||
|
namespace,
|
||||||
|
serverService,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!!extraService && !!serverAssets.extraService)
|
||||||
|
// Might not exist
|
||||||
|
serverResources.push(
|
||||||
|
k8sCore.replaceNamespacedService(
|
||||||
|
serverAssets.extraService.metadata.name,
|
||||||
|
namespace,
|
||||||
|
extraService,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
else if (!!extraService)
|
||||||
|
serverResources.push(
|
||||||
|
k8sCore.createNamespacedService(namespace, extraService),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!!backupSecret && !!serverAssets.backupSecret)
|
||||||
|
// Might not exist
|
||||||
|
serverResources.push(
|
||||||
|
k8sCore.replaceNamespacedSecret(
|
||||||
|
serverAssets.backupSecret.metadata.name,
|
||||||
|
namespace,
|
||||||
|
backupSecret,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
else if (!!backupSecret)
|
||||||
|
serverResources.push(
|
||||||
|
k8sCore.createNamespacedSecret(namespace, backupSecret),
|
||||||
|
);
|
||||||
|
|
||||||
|
return await Promise.all(serverResources);
|
||||||
|
}
|
|
@ -3,7 +3,8 @@ import TextField from "@mui/material/TextField";
|
||||||
import Autocomplete from "@mui/material/Autocomplete";
|
import Autocomplete from "@mui/material/Autocomplete";
|
||||||
import Chip from "@mui/material/Chip";
|
import Chip from "@mui/material/Chip";
|
||||||
|
|
||||||
const validatePort = (p) => p !== "25565" && p !== "25575" && p.length < 6;
|
const validatePort = (p) =>
|
||||||
|
p !== "25565" && p !== "25575" && p.length < 6 && parseInt(p) < 60_000;
|
||||||
|
|
||||||
export default function ExtraPortsOption(props) {
|
export default function ExtraPortsOption(props) {
|
||||||
const { extraPorts: initExtraPorts } = props;
|
const { extraPorts: initExtraPorts } = props;
|
||||||
|
@ -30,7 +31,14 @@ export default function ExtraPortsOption(props) {
|
||||||
value={extraPorts}
|
value={extraPorts}
|
||||||
onChange={portChange}
|
onChange={portChange}
|
||||||
freeSolo
|
freeSolo
|
||||||
renderInput={(p) => <TextField {...p} label="Extra Ports" />}
|
renderInput={(p) => (
|
||||||
|
<TextField
|
||||||
|
{...p}
|
||||||
|
label="Extra Ports"
|
||||||
|
helperText="Remember to press enter to add the port!"
|
||||||
|
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
renderTags={(value, getTagProps) =>
|
renderTags={(value, getTagProps) =>
|
||||||
value.map((option, index) => {
|
value.map((option, index) => {
|
||||||
const defaultChipProps = getTagProps({ index });
|
const defaultChipProps = getTagProps({ index });
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
export default function HostOption(props) {
|
export default function HostOption(props) {
|
||||||
const { value, onChange } = props;
|
const { value, onChange, disabled } = props;
|
||||||
|
|
||||||
|
function onTextChange(e) {
|
||||||
|
e.target.value = e.target.value.toLowerCase();
|
||||||
|
onChange(e);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
label="Host"
|
label="Host"
|
||||||
value={value ?? ""}
|
value={value ?? ""}
|
||||||
onChange={onChange}
|
onChange={onTextChange}
|
||||||
helperText="Example: host.mydomain.com"
|
helperText="Example: host.mydomain.com"
|
||||||
FormHelperTextProps={{ sx: { ml: 0 } }}
|
FormHelperTextProps={{ sx: { ml: 0 } }}
|
||||||
required
|
required
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,11 @@ export default function EditCoreOptions(props) {
|
||||||
>
|
>
|
||||||
<FormControl fullWidth sx={{ mt: "2rem", display: "flex", gap: ".5rem" }}>
|
<FormControl fullWidth sx={{ mt: "2rem", display: "flex", gap: ".5rem" }}>
|
||||||
<NameOption value={spec.name} onChange={coreUpdate("name")} />
|
<NameOption value={spec.name} onChange={coreUpdate("name")} />
|
||||||
<HostOption value={spec.host} onChange={coreUpdate("host")} />
|
<HostOption
|
||||||
|
value={spec.host}
|
||||||
|
onChange={coreUpdate("host")}
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
<VersionOption value={spec.version} onChange={coreUpdate("version")} />
|
<VersionOption value={spec.version} onChange={coreUpdate("version")} />
|
||||||
<ServerTypeOption
|
<ServerTypeOption
|
||||||
value={spec.serverType}
|
value={spec.serverType}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue