[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:
dunemask 2024-02-13 05:09:18 +00:00
parent 0a0f9c8463
commit fc60df27ac
9 changed files with 154 additions and 31 deletions

2
dist/app.js vendored
View file

@ -8,4 +8,4 @@ const kc = new k8s.KubeConfig();
kc.loadFromDefault();
}
main().catch((e)=>{console.log(e)});
main().catch((e)=>{console.error(e)});

View file

@ -9,6 +9,8 @@ import {
import ExpressClientError, { sendError } from "../util/ExpressClientError.js";
import { toggleServer } from "../k8s/k8s-server-control.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(
`^([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
.status(400)
.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";
}
@ -158,10 +163,15 @@ export async function getServer(req, res) {
export async function modifyServer(req, res) {
if (payloadFilter(req, res) !== "filtered") return;
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 {
await checkServerId(req.cairoId, serverSpec);
const serverEntry = await modifyServerEntry(serverSpec);
// await createServerResources(serverEntry);
await modifyServerResources(serverEntry);
res.sendStatus(200);
} catch (e) {
sendError(res)(e);

View file

@ -165,7 +165,7 @@ export async function modifyServerEntry(serverSpec) {
id,
// ownerCairoId: owner_cairo_id, // DIsabled! If these becomes a reqest, please create a new function!
name,
host,
// host, // TODO: Can only be updated if service name is generic and non descriptive
version,
serverType: server_type,
cpu, // TODO: Ignored for now by the K8S manifests
@ -180,28 +180,66 @@ export async function modifyServerEntry(serverSpec) {
backupInterval: backup_interval,
} = serverSpec;
const q = updateWhereAllQuery(
table,
{
const q =
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,
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,
server_type,
serverType,
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 },
);
return pg.query(q);
storage,
extraPorts,
backupEnabled,
backupHost,
backupPath,
backupId,
backupKey,
backupInterval,
};
} catch (e) {
asExpressClientError(e);
}
}
export async function getServerEntries() {

View file

@ -11,8 +11,6 @@ metadata:
namespace: changeme-namespace
spec:
internalTrafficPolicy: Cluster
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
ports: # Programatically add all FTP ports. Port range includes 20, 21, 40000-40001
- name: minecraft

View file

@ -18,7 +18,7 @@ const namespace = process.env.MCL_SERVER_NAMESPACE;
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
function createExtraService(serverSpec) {
export function createExtraService(serverSpec) {
const { mclName, id, extraPorts } = serverSpec;
if (!extraPorts) return;
const serviceYaml = loadYaml("lib/k8s/configs/extra-svc.yml");
@ -49,7 +49,7 @@ function createExtraService(serverSpec) {
return serviceYaml;
}
function createBackupSecret(serverSpec) {
export function createBackupSecret(serverSpec) {
if (!serverSpec.backupEnabled) return; // If backup not defined, don't create RCLONE secret
const { mclName, id, backupId, backupKey, backupHost } = serverSpec;
const backupYaml = loadYaml("lib/k8s/configs/backup-secret.yml");
@ -153,7 +153,7 @@ function createServerDeploy(serverSpec) {
return deployYaml;
}
function createServerService(serverSpec) {
export function createServerService(serverSpec) {
const { mclName, host, id } = serverSpec;
const serviceYaml = loadYaml("lib/k8s/configs/server-svc.yml");
serviceYaml.metadata.annotations["ingress.qumine.io/hostname"] = host;

59
lib/k8s/server-modify.js Normal file
View 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);
}

View file

@ -3,7 +3,8 @@ import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
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) {
const { extraPorts: initExtraPorts } = props;
@ -30,7 +31,14 @@ export default function ExtraPortsOption(props) {
value={extraPorts}
onChange={portChange}
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) =>
value.map((option, index) => {
const defaultChipProps = getTagProps({ index });

View file

@ -1,15 +1,21 @@
import TextField from "@mui/material/TextField";
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 (
<TextField
label="Host"
value={value ?? ""}
onChange={onChange}
onChange={onTextChange}
helperText="Example: host.mydomain.com"
FormHelperTextProps={{ sx: { ml: 0 } }}
required
disabled={disabled}
/>
);
}

View file

@ -73,7 +73,11 @@ export default function EditCoreOptions(props) {
>
<FormControl fullWidth sx={{ mt: "2rem", display: "flex", gap: ".5rem" }}>
<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")} />
<ServerTypeOption
value={spec.serverType}