diff --git a/lib/k8s/configs/containers/ftp-server.yml b/lib/k8s/configs/containers/ftp-server.yml new file mode 100644 index 0000000..de6f49e --- /dev/null +++ b/lib/k8s/configs/containers/ftp-server.yml @@ -0,0 +1,36 @@ +env: + - name: FTP_USER + value: "minecluster" + - name: FTP_PASS + value: "minecluster" +image: garethflowers/ftp-server +imagePullPolicy: IfNotPresent +livenessProbe: +exec: + command: ["echo"] +failureThreshold: 20 +initialDelaySeconds: 30 +periodSeconds: 5 +successThreshold: 1 +timeoutSeconds: 1 +name: changeme-name-ftp +ports: [] # Programatically add all the ports for easier readability, Ports include: 20,21,40000-400009 +readinessProbe: + exec: + command: ["echo"] + 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: /home/minecluster + name: datadir diff --git a/lib/k8s/configs/containers/minecraft-backup.yml b/lib/k8s/configs/containers/minecraft-backup.yml new file mode 100644 index 0000000..074025a --- /dev/null +++ b/lib/k8s/configs/containers/minecraft-backup.yml @@ -0,0 +1,63 @@ +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 diff --git a/lib/k8s/configs/containers/minecraft-server.yml b/lib/k8s/configs/containers/minecraft-server.yml new file mode 100644 index 0000000..26b01d2 --- /dev/null +++ b/lib/k8s/configs/containers/minecraft-server.yml @@ -0,0 +1,112 @@ +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 diff --git a/lib/k8s/configs/server-deployment.yml b/lib/k8s/configs/server-deployment.yml index fef67d8..1607944 100644 --- a/lib/k8s/configs/server-deployment.yml +++ b/lib/k8s/configs/server-deployment.yml @@ -19,188 +19,13 @@ spec: 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 + containers: [] dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler - securityContext: - fsGroup: 2000 - runAsUser: 1000 + # securityContext: + # fsGroup: 2000 + # runAsUser: 1000 terminationGracePeriodSeconds: 30 volumes: - name: datadir diff --git a/lib/k8s/configs/server-svc.yml b/lib/k8s/configs/server-svc.yml index a14e769..c7e7fa2 100644 --- a/lib/k8s/configs/server-svc.yml +++ b/lib/k8s/configs/server-svc.yml @@ -14,11 +14,15 @@ spec: ipFamilies: - IPv4 ipFamilyPolicy: SingleStack - ports: + ports: # Programatically add all FTP ports. Port range includes 20, 21, 40000-40001 - name: minecraft port: 25565 protocol: TCP targetPort: minecraft + # - name: ftp-data + # port: 20 + # protocol: TCP + # targetPort: ftp-data selector: app: changeme-app sessionAffinity: None diff --git a/lib/k8s/k8s-server-control.js b/lib/k8s/k8s-server-control.js index 77dae90..7d82bb0 100644 --- a/lib/k8s/k8s-server-control.js +++ b/lib/k8s/k8s-server-control.js @@ -61,11 +61,11 @@ export function getServerAssets(serverName) { const serverAssets = { deployment: deployments[0], service: services.find( - (s) => s.metadata.name === `mcl-${serverName}-rcon`, + (s) => s.metadata.name === `mcl-${serverName}-server`, ), volume: volumes[0], rconService: services.find( - (s) => s.metadata.name === `mcl-${serverName}-server`, + (s) => s.metadata.name === `mcl-${serverName}-rcon`, ), rconSecret: secrets[0], }; diff --git a/lib/k8s/server-create.js b/lib/k8s/server-create.js index 23dfe85..dd7cbbc 100644 --- a/lib/k8s/server-create.js +++ b/lib/k8s/server-create.js @@ -55,6 +55,22 @@ function createServerVolume(serverSpec) { return volumeYaml; } +function getFtpContainer() { + const ftpContainer = loadYaml("lib/k8s/configs/containers/ftp-server.yml"); + const ftpPortList = [ + { p: 20, n: "ftp-data" }, + { p: 21, n: "ftp-commands" }, + ]; + for (var p = 40000; p <= 40009; p++) + ftpPortList.push({ p, n: `ftp-passive-${p - 40000}` }); + ftpContainer.ports = ftpPortList.map(({ p: containerPort, n: name }) => ({ + containerPort, + name, + protocol: "TCP", + })); + return ftpContainer; +} + function createServerDeploy(serverSpec) { const { name, @@ -71,16 +87,21 @@ function createServerDeploy(serverSpec) { whitelist, } = serverSpec; const deployYaml = loadYaml("lib/k8s/configs/server-deployment.yml"); + const serverContainer = loadYaml( + "lib/k8s/configs/containers/minecraft-server.yml", + ); + const backupContainer = loadYaml( + "lib/k8s/configs/containers/minecraft-backup.yml", + ); + const ftpContainer = getFtpContainer(); deployYaml.metadata.name = `mcl-${name}`; + deployYaml.metadata.namespace = namespace; deployYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name; 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]; - const findEnv = (k) => serverContainer.env.find(({ name: n }) => n === k); const updateEnv = (k, v) => (findEnv.value = v); // Enviornment variables @@ -109,7 +130,8 @@ function createServerDeploy(serverSpec) { deployYaml.spec.template.spec.volumes.find( ({ name }) => name === "datadir", ).persistentVolumeClaim.claimName = `mcl-${name}-volume`; - deployYaml.spec.template.spec.containers[0] = serverContainer; + deployYaml.spec.template.spec.containers.push(serverContainer); + deployYaml.spec.template.spec.containers.push(ftpContainer); return deployYaml; } @@ -123,6 +145,20 @@ function createServerService(serverSpec) { serviceYaml.metadata.annotations["minecluster.dunemask.net/server-name"] = name; serviceYaml.spec.selector.app = `mcl-${name}-app`; + + // Apply FTP Port List + const ftpPortList = [ + { p: 20, n: "ftp-data" }, + { p: 21, n: "ftp-commands" }, + ]; + for (var p = 40000; p <= 40009; p++) + ftpPortList.push({ p, n: `ftp-passive-${p - 40000}` }); + serviceYaml.spec.ports = ftpPortList.map(({ p: port, n: name }) => ({ + port, + name, + protocol: "TCP", + targetPort: port, + })); return serviceYaml; } diff --git a/lib/k8s/server-delete.js b/lib/k8s/server-delete.js index db8612e..e620f4b 100644 --- a/lib/k8s/server-delete.js +++ b/lib/k8s/server-delete.js @@ -24,31 +24,27 @@ export default async function deleteServer(req, res) { const { name } = serverSpec; // Ensure deployment exists const server = await getServerAssets(name); - if (!server) return res.status(404).send("No Resources for that server were found!"); - + if (!server) + return res.status(404).send("No Resources for that server were found!"); + // Delete in reverse order - const deleteDeploy = deleteOnExist( - server.deployment, - (name) => k8sDeps.deleteNamespacedDeployment(name, namespace), + const deleteDeploy = deleteOnExist(server.deployment, (name) => + k8sDeps.deleteNamespacedDeployment(name, namespace), ); - const deleteService = deleteOnExist( - server.service, - (name) => k8sCore.deleteNamespacedService(name, namespace), + const deleteService = deleteOnExist(server.service, (name) => + k8sCore.deleteNamespacedService(name, namespace), ); - const deleteRconService = deleteOnExist( - server.rconService, - (name) => k8sCore.deleteNamespacedService(name, namespace), + const deleteRconService = deleteOnExist(server.rconService, (name) => + k8sCore.deleteNamespacedService(name, namespace), ); - if(deleteDeploy) await deleteDeploy.catch(deleteError(res)) + if (deleteDeploy) await deleteDeploy.catch(deleteError(res)); - const deleteRconSecret = deleteOnExist( - server.rconSecret, - (name) => k8sCore.deleteNamespacedSecret(name, namespace), + const deleteRconSecret = deleteOnExist(server.rconSecret, (name) => + k8sCore.deleteNamespacedSecret(name, namespace), ); - const deleteVolume = deleteOnExist( - server.volume, - (name) => k8sCore.deleteNamespacedPersistentVolumeClaim(name, namespace), + const deleteVolume = deleteOnExist(server.volume, (name) => + k8sCore.deleteNamespacedPersistentVolumeClaim(name, namespace), ); Promise.all([ diff --git a/lib/k8s/server-files.js b/lib/k8s/server-files.js new file mode 100644 index 0000000..077158f --- /dev/null +++ b/lib/k8s/server-files.js @@ -0,0 +1,35 @@ +import ftp from "basic-ftp"; +import { ERR } from "../util/logging.js"; +import { getServerAssets } from "./k8s-server-control.js"; + +const namespace = process.env.MCL_SERVER_NAMESPACE; + +export async function listFiles(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 server = await getServerAssets(name); + if (!server) + return res.status(404).send("No Resources for that server were found!"); + if (!server.service) + return res + .status(409) + .send("Service doesn't exist, please contact your hosting provider!"); + const client = new ftp.Client(0); + client.ftp.verbose = true; + try { + await client.access({ + host: `${server.service.metadata.name}.${namespace}.svc.cluster.local`, + user: "minecluster", + password: "minecluster", + }); + const files = await client.list(); + res.json(files); + } catch (err) { + console.log(err); + ERR("SERVER FILES", "Error loading client files:"); + res.status(500).send(err); + } + client.close(); +} diff --git a/lib/routes/files-route.js b/lib/routes/files-route.js new file mode 100644 index 0000000..c326cd9 --- /dev/null +++ b/lib/routes/files-route.js @@ -0,0 +1,7 @@ +import { Router, json as jsonMiddleware } from "express"; +import { listFiles } from "../k8s/server-files.js"; +const router = Router(); +router.use(jsonMiddleware()); +router.post("/list", listFiles); + +export default router; diff --git a/lib/server/router.js b/lib/server/router.js index fdf381a..ccedebb 100644 --- a/lib/server/router.js +++ b/lib/server/router.js @@ -5,6 +5,7 @@ import express from "express"; import vitals from "../routes/vitals-route.js"; import systemRoute from "../routes/system-route.js"; import serverRoute from "../routes/server-route.js"; +import filesRoute from "../routes/files-route.js"; import reactRoute from "../routes/react-route.js"; import { logErrors, @@ -23,6 +24,7 @@ export default function buildRoutes(pg, skio) { // Routes router.use("/api/system", systemRoute); router.use("/api/server", serverRoute); + router.use("/api/files", filesRoute); router.use(["/mcl", "/mcl/*"], reactRoute); // Static Build Route /*router.use(logErrors); router.use(clientErrorHandler); diff --git a/package-lock.json b/package-lock.json index 1b70a3e..86ebdf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@kubernetes/client-node": "^0.20.0", "aws-sdk": "^2.1514.0", + "basic-ftp": "^5.0.4", "bcrypt": "^5.1.1", "chalk": "^5.3.0", "express": "^4.18.2", @@ -3491,6 +3492,14 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/basic-ftp": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz", + "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcrypt": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", diff --git a/package.json b/package.json index e6b3936..6d78841 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "dependencies": { "@kubernetes/client-node": "^0.20.0", "aws-sdk": "^2.1514.0", + "basic-ftp": "^5.0.4", "bcrypt": "^5.1.1", "chalk": "^5.3.0", "express": "^4.18.2",