[FEATURE] Storage adjustments and minor tweaks (#9)

Co-authored-by: Dunemask <dunemask@gmail.com>
Reviewed-on: https://gitea.dunemask.dev/elysium/minecluster/pulls/9
This commit is contained in:
dunemask 2024-01-24 16:39:57 +00:00
parent 3d73f69678
commit 43c4409498
13 changed files with 121 additions and 39 deletions

View file

@ -15,8 +15,14 @@ const dnsRegex = new RegExp(
function backupPayloadFilter(req, res) { function backupPayloadFilter(req, res) {
const serverSpec = req.body; const serverSpec = req.body;
const { backupHost, backupBucket, backupId, backupKey, backupInterval } = const {
serverSpec; storage,
backupHost,
backupBucket,
backupId,
backupKey,
backupInterval,
} = serverSpec;
// TODO: Impliment non creation time backups // TODO: Impliment non creation time backups
if ( if (
!!backupHost || !!backupHost ||
@ -25,6 +31,8 @@ function backupPayloadFilter(req, res) {
!!backupKey || !!backupKey ||
!!backupInterval !!backupInterval
) { ) {
if (storage === 0)
return res.status(400).send("Backups cannot be used if storage is zero!");
// If any keys are required, all are required // If any keys are required, all are required
if ( if (
!( !(

View file

@ -7,6 +7,7 @@ CREATE TABLE servers (
server_type varchar(63) DEFAULT 'VANILLA', server_type varchar(63) DEFAULT 'VANILLA',
cpu varchar(63) DEFAULT '500', cpu varchar(63) DEFAULT '500',
memory varchar(63) DEFAULT '512', memory varchar(63) DEFAULT '512',
storage varchar(63) DEFAULT NULL,
backup_enabled BOOLEAN DEFAULT FALSE, backup_enabled BOOLEAN DEFAULT FALSE,
backup_host varchar(255) DEFAULT NULL, backup_host varchar(255) DEFAULT NULL,
backup_bucket_path varchar(255) DEFAULT NULL, backup_bucket_path varchar(255) DEFAULT NULL,

View file

@ -20,7 +20,9 @@ export async function createServerEntry(serverSpec) {
host, host,
version, version,
serverType: server_type, serverType: server_type,
cpu, // TODO: Ignored for now by the K8S manifests
memory, memory,
storage: storage_val,
extraPorts: extra_ports, extraPorts: extra_ports,
backupHost: backup_host, backupHost: backup_host,
backupBucket: backup_bucket_path, backupBucket: backup_bucket_path,
@ -28,12 +30,15 @@ export async function createServerEntry(serverSpec) {
backupKey: backup_key, backupKey: backup_key,
backupInterval: backup_interval, backupInterval: backup_interval,
} = serverSpec; } = serverSpec;
var q = insertQuery(table, { var q = insertQuery(table, {
name, name,
host, host,
version, version,
server_type, server_type,
cpu, // TODO: Ignored for now by the K8S manifests
memory, memory,
storage: !storage_val || storage_val === "0" ? null : storage_val, // 0, undefined, null, or "0" becomes null
extra_ports, extra_ports,
backup_enabled: !!backup_interval ? true : null, // We already verified the payload, so any backup key will work backup_enabled: !!backup_interval ? true : null, // We already verified the payload, so any backup key will work
backup_host, backup_host,
@ -51,7 +56,9 @@ export async function createServerEntry(serverSpec) {
host, host,
version, version,
server_type: serverType, server_type: serverType,
cpu, // TODO: Ignored for now by the K8S manifests
memory, memory,
storage,
extra_ports: extraPorts, extra_ports: extraPorts,
backup_enabled: backupEnabled, backup_enabled: backupEnabled,
backup_host: backupHost, backup_host: backupHost,
@ -68,7 +75,9 @@ export async function createServerEntry(serverSpec) {
host, host,
version, version,
serverType, serverType,
cpu, // TODO: Ignored for now by the K8S manifests
memory, memory,
storage,
extraPorts, extraPorts,
backupEnabled, backupEnabled,
backupHost, backupHost,
@ -102,7 +111,9 @@ export async function getServerEntry(serverId) {
host, host,
version, version,
server_type: serverType, server_type: serverType,
cpu, // TODO: Ignored for now by the K8S manifests
memory, memory,
storage,
extra_ports: extraPorts, extra_ports: extraPorts,
backup_enabled: backupEnabled, backup_enabled: backupEnabled,
backup_host: backupHost, backup_host: backupHost,
@ -119,7 +130,9 @@ export async function getServerEntry(serverId) {
host, host,
version, version,
serverType, serverType,
cpu, // TODO: Ignored for now by the K8S manifests
memory, memory,
storage,
extraPorts, extraPorts,
backupEnabled, backupEnabled,
backupHost, backupHost,
@ -140,7 +153,9 @@ export async function modifyServerEntry(serverSpec) {
host, host,
version, version,
serverType: server_type, serverType: server_type,
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
extraPorts: extra_ports, extraPorts: extra_ports,
backupEnabled: backup_enabled, backupEnabled: backup_enabled,
backupHost: backup_host, backupHost: backup_host,
@ -157,7 +172,9 @@ export async function modifyServerEntry(serverSpec) {
host, host,
version, version,
server_type, server_type,
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
extra_ports, extra_ports,
backup_enabled, backup_enabled,
backup_host, backup_host,

View file

@ -30,11 +30,13 @@ spec:
# runAsUser: 1000 # runAsUser: 1000
terminationGracePeriodSeconds: 30 terminationGracePeriodSeconds: 30
volumes: volumes:
- name: datadir - emptyDir: {}
persistentVolumeClaim: name: datadir
claimName: changeme-pvc-name
- emptyDir: {} - emptyDir: {}
name: backupdir name: backupdir
# - name: datadir
# persistentVolumeClaim:
# claimName: changeme-pvc-name
# - name: rclone-config # - name: rclone-config
# secret: # secret:
# defaultMode: 420 # defaultMode: 420

View file

@ -4,7 +4,7 @@ import yaml from "js-yaml";
const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8")); const loadYaml = (f) => yaml.load(fs.readFileSync(path.resolve(f), "utf8"));
export function getFtpContainer(serverSpec) { export function getFtpContainer(serverSpec) {
const { mclName } = serverSpec; const { mclName, storage } = serverSpec;
const ftpContainer = loadYaml("lib/k8s/configs/containers/ftp-server.yml"); const ftpContainer = loadYaml("lib/k8s/configs/containers/ftp-server.yml");
ftpContainer.name = `mcl-${mclName}-ftp`; ftpContainer.name = `mcl-${mclName}-ftp`;
const ftpPortList = [ const ftpPortList = [
@ -18,11 +18,12 @@ export function getFtpContainer(serverSpec) {
name, name,
protocol: "TCP", protocol: "TCP",
})); }));
if (!storage) delete ftpContainer.volumeMounts;
return ftpContainer; return ftpContainer;
} }
export function getCoreServerContainer(serverSpec) { export function getCoreServerContainer(serverSpec) {
const { mclName, version, serverType, memory } = serverSpec; const { mclName, version, serverType, memory, storage } = serverSpec;
const container = loadYaml("lib/k8s/configs/containers/minecraft-server.yml"); const container = loadYaml("lib/k8s/configs/containers/minecraft-server.yml");
// Container Updates // Container Updates
container.name = `mcl-${mclName}-server`; container.name = `mcl-${mclName}-server`;
@ -38,12 +39,21 @@ export function getCoreServerContainer(serverSpec) {
// RCON // RCON
const rs = `mcl-${mclName}-rcon-secret`; const rs = `mcl-${mclName}-rcon-secret`;
findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs; findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs;
if (!storage) delete container.volumeMounts;
return container; return container;
} }
export function getServerContainer(serverSpec) { export function getServerContainer(serverSpec) {
const { difficulty, gamemode, motd, maxPlayers, seed, ops, whitelist } = const {
serverSpec; difficulty,
gamemode,
motd,
maxPlayers,
seed,
ops,
whitelist,
storage,
} = serverSpec;
const container = getCoreServerContainer(serverSpec); const container = getCoreServerContainer(serverSpec);
const findEnv = (k) => container.env.find(({ name: n }) => n === k); const findEnv = (k) => container.env.find(({ name: n }) => n === k);
@ -57,12 +67,13 @@ export function getServerContainer(serverSpec) {
updateEnv("SEED", seed); updateEnv("SEED", seed);
updateEnv("OPS", ops); updateEnv("OPS", ops);
updateEnv("WHITELIST", whitelist); */ updateEnv("WHITELIST", whitelist); */
if (!storage) delete container.volumeMounts;
return container; return container;
} }
export function getBackupContainer(serverSpec) { export function getBackupContainer(serverSpec) {
const { mclName, backupEnabled, backupPath } = serverSpec; const { mclName, backupEnabled, backupPath, storage } = serverSpec;
const container = loadYaml("lib/k8s/configs/containers/minecraft-backup.yml"); const container = loadYaml("lib/k8s/configs/containers/minecraft-backup.yml");
if (!backupEnabled) return; if (!backupEnabled) return;
const findEnv = (k) => container.env.find(({ name: n }) => n === k); const findEnv = (k) => container.env.find(({ name: n }) => n === k);
@ -73,6 +84,7 @@ export function getBackupContainer(serverSpec) {
// RCON // RCON
const rs = `mcl-${mclName}-rcon-secret`; const rs = `mcl-${mclName}-rcon-secret`;
findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs; findEnv("RCON_PASSWORD").valueFrom.secretKeyRef.name = rs;
if (!storage) delete container.volumeMounts;
return container; return container;
} }

View file

@ -86,18 +86,19 @@ function createRconSecret(serverSpec) {
} }
function createServerVolume(serverSpec) { function createServerVolume(serverSpec) {
const { mclName, id } = serverSpec; const { mclName, id, storage } = serverSpec;
if (!storage) return;
const volumeYaml = loadYaml("lib/k8s/configs/server-pvc.yml"); const volumeYaml = loadYaml("lib/k8s/configs/server-pvc.yml");
volumeYaml.metadata.labels.service = `mcl-${mclName}-server`; volumeYaml.metadata.labels.service = `mcl-${mclName}-server`;
volumeYaml.metadata.name = `mcl-${mclName}-volume`; volumeYaml.metadata.name = `mcl-${mclName}-volume`;
volumeYaml.metadata.namespace = namespace; volumeYaml.metadata.namespace = namespace;
volumeYaml.metadata.annotations["minecluster.dunemask.net/id"] = id; volumeYaml.metadata.annotations["minecluster.dunemask.net/id"] = id;
volumeYaml.spec.resources.requests.storage = "5Gi"; // TODO: Changeme volumeYaml.spec.resources.requests.storage = `${storage}Gi`;
return volumeYaml; return volumeYaml;
} }
function createServerDeploy(serverSpec) { function createServerDeploy(serverSpec) {
const { mclName, id, backupEnabled } = serverSpec; const { mclName, id, backupEnabled, storage } = serverSpec;
const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml"); const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml");
const { metadata } = deployYaml; const { metadata } = deployYaml;
const serverContainer = getServerContainer(serverSpec); const serverContainer = getServerContainer(serverSpec);
@ -119,9 +120,18 @@ function createServerDeploy(serverSpec) {
id; id;
// Volumes // Volumes
deployYaml.spec.template.spec.volumes.find( if (!!storage) {
const dvi = deployYaml.spec.template.spec.volumes.findIndex(
({ name }) => name === "datadir", ({ name }) => name === "datadir",
).persistentVolumeClaim.claimName = `mcl-${mclName}-volume`; );
delete deployYaml.spec.template.spec.volumes[dvi].emptyDir;
deployYaml.spec.template.spec.volumes[dvi] = {
...deployYaml.spec.template.spec.volumes[dvi],
persistentVolumeClaim: {
claimName: `mcl-${mclName}-volume`,
},
};
}
// Backups // Backups
if (backupEnabled) { if (backupEnabled) {
@ -194,6 +204,7 @@ export default async function createServerResources(createSpec) {
const rconService = createRconService(createSpec); const rconService = createRconService(createSpec);
const extraService = createExtraService(createSpec); const extraService = createExtraService(createSpec);
const serverResources = []; const serverResources = [];
if (!!serverVolume)
serverResources.push( serverResources.push(
k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume), k8sCore.createNamespacedPersistentVolumeClaim(namespace, serverVolume),
); );

View file

@ -6,7 +6,7 @@ export default function BackupHostOption(props) {
<TextField <TextField
label="Backup Host" label="Backup Host"
onChange={onChange} onChange={onChange}
value={value} value={value ?? ""}
helperText="Example: s3.mydomain.com" helperText="Example: s3.mydomain.com"
FormHelperTextProps={{ sx: { ml: 0 } }} FormHelperTextProps={{ sx: { ml: 0 } }}
required required

View file

@ -5,7 +5,7 @@ export default function BackupIdOption(props) {
return ( return (
<TextField <TextField
label="S3 Access Key ID" label="S3 Access Key ID"
value={value} value={value ?? ""}
onChange={onChange} onChange={onChange}
helperText="Example: s3-access-key-id" helperText="Example: s3-access-key-id"
FormHelperTextProps={{ sx: { ml: 0 } }} FormHelperTextProps={{ sx: { ml: 0 } }}

View file

@ -1,10 +1,11 @@
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
export default function BackupKeyOption(props) { export default function BackupKeyOption(props) {
const { onChange } = props; const { value, onChange } = props;
return ( return (
<TextField <TextField
label="S3 Access Key" label="S3 Access Key"
value={value ?? ""}
onChange={onChange} onChange={onChange}
helperText="Example: s3-access-key" helperText="Example: s3-access-key"
FormHelperTextProps={{ sx: { ml: 0 } }} FormHelperTextProps={{ sx: { ml: 0 } }}

View file

@ -0,0 +1,26 @@
import TextField from "@mui/material/TextField";
import MenuItem from "@mui/material/MenuItem";
const maxStorageSupported = 80;
export const storageOptions = new Array(2 * maxStorageSupported)
.fill(0)
.map((v, i) => (i + 1) * 0.5);
export default function StorageOption(props) {
const { value, onChange } = props;
return (
<TextField
label="Storage"
onChange={onChange}
value={value ?? null}
select
required
SelectProps={{ MenuProps: { sx: { maxHeight: "20rem" } } }}
>
<MenuItem value={0}>No Storage</MenuItem>
{storageOptions.map((o, i) => (
<MenuItem value={o} key={i}>{`${o} GB`}</MenuItem>
))}
</TextField>
);
}

View file

@ -22,7 +22,7 @@ export default class RconSocket {
onRconError(v) { onRconError(v) {
this.rconLive = false; this.rconLive = false;
console.log("Server sent" + v); console.log("Server sent: ", v);
} }
onConnect() { onConnect() {

View file

@ -22,6 +22,7 @@ import MemoryOption, {
memoryOptions, memoryOptions,
} from "@mcl/components/server-options/MemoryOption.jsx"; } from "@mcl/components/server-options/MemoryOption.jsx";
import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.jsx"; import ExtraPortsOption from "@mcl/components/server-options/ExtraPortsOption.jsx";
import StorageOption from "@mcl/components/server-options/StorageOption.jsx";
import BackupHostOption from "@mcl/components/server-options/BackupHostOption.jsx"; import BackupHostOption from "@mcl/components/server-options/BackupHostOption.jsx";
import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.jsx"; import BackupBucketOption from "@mcl/components/server-options/BackupBucketOption.jsx";
@ -36,6 +37,7 @@ const defaultServer = {
serverType: serverTypeOptions[0], serverType: serverTypeOptions[0],
cpu: cpuOptions[0], cpu: cpuOptions[0],
memory: memoryOptions[2], // 1.5GB memory: memoryOptions[2], // 1.5GB
storage: 0,
extraPorts: [], extraPorts: [],
}; };
@ -99,7 +101,9 @@ export default function CreateCoreOptions() {
/> />
<CpuOption value={spec.cpu} onChange={coreUpdate("cpu")} /> <CpuOption value={spec.cpu} onChange={coreUpdate("cpu")} />
<MemoryOption value={spec.memory} onChange={coreUpdate("memory")} /> <MemoryOption value={spec.memory} onChange={coreUpdate("memory")} />
<StorageOption value={spec.storage} onChange={coreUpdate("storage")} />
<ExtraPortsOption onChange={updateSpec} /> <ExtraPortsOption onChange={updateSpec} />
{spec.storage !== 0 && (
<FormControlLabel <FormControlLabel
control={ control={
<Switch <Switch
@ -112,7 +116,8 @@ export default function CreateCoreOptions() {
labelPlacement="start" labelPlacement="start"
sx={{ mr: "auto" }} sx={{ mr: "auto" }}
/> />
{backupEnabled && ( )}
{backupEnabled && spec.storage !== 0 && (
<FormControl <FormControl
fullWidth fullWidth
sx={{ mt: "2rem", display: "flex", gap: ".5rem" }} sx={{ mt: "2rem", display: "flex", gap: ".5rem" }}

View file

@ -35,6 +35,7 @@ export default function EditCoreOptions(props) {
const { serverId } = props; const { serverId } = props;
const [spec, setSpec] = useState(); const [spec, setSpec] = useState();
const modifyServer = useModifyServer(spec); const modifyServer = useModifyServer(spec);
const nav = useNavigate();
const { isLoading, data: serverBlueprint } = useGetServer(serverId); const { isLoading, data: serverBlueprint } = useGetServer(serverId);
useEffect(() => setSpec(serverBlueprint), [serverBlueprint]); useEffect(() => setSpec(serverBlueprint), [serverBlueprint]);
@ -47,9 +48,7 @@ export default function EditCoreOptions(props) {
const coreUpdate = (attr) => (e) => updateSpec(attr, e.target.value); const coreUpdate = (attr) => (e) => updateSpec(attr, e.target.value);
const upsertSpec = () => { const upsertSpec = () => modifyServer().then(() => nav("/"));
modifyServer(spec);
};
const toggleBackupEnabled = () => const toggleBackupEnabled = () =>
updateSpec("backupEnabled", !spec.backupEnabled); updateSpec("backupEnabled", !spec.backupEnabled);