From 370c983834d1fc74f1a5fe7e844115fb443f77ac Mon Sep 17 00:00:00 2001 From: TheZexquex Date: Sat, 6 Jun 2026 18:39:34 +0200 Subject: [PATCH 1/5] Initial work for running the game servers in docker --- .dockerignore | 4 + Dockerfile | 2 +- bot/build.gradle.kts | 10 ++ bot/src/main/java/de/chojo/gamejam/Bot.java | 1 + .../gamejam/configuration/ConfigFile.java | 18 +- .../gamejam/configuration/Configuration.java | 38 +++-- .../configuration/elements/Docker.java | 47 +++++ .../chojo/gamejam/server/DockerService.java | 145 ++++++++++++++++ .../chojo/gamejam/server/ServerService.java | 7 +- .../de/chojo/gamejam/server/TeamServer.java | 161 +++--------------- bot/src/main/resources/log4j2.xml | 30 +--- dev.Dockerfile | 34 ++++ dev.compose.yml | 9 +- settings.gradle.kts | 5 + 14 files changed, 316 insertions(+), 195 deletions(-) create mode 100644 .dockerignore create mode 100644 bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java create mode 100644 bot/src/main/java/de/chojo/gamejam/server/DockerService.java create mode 100644 dev.Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8caa093 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.gradle +**/build +.git +.idea \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fc0d17c..35e9a22 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # This dockerfile is far from best practice and is a pure "Make it work" approach. Please do not use it as a reference of any kind. -FROM gradle:jdk21-alpine as build +FROM gradle:jdk25-alpine as build COPY . . RUN gradle clean :bot:build :plugin-paper:build --no-daemon diff --git a/bot/build.gradle.kts b/bot/build.gradle.kts index 654328b..88a4542 100644 --- a/bot/build.gradle.kts +++ b/bot/build.gradle.kts @@ -20,16 +20,26 @@ dependencies { implementation("de.chojo", "cjda-util", "2.14.5+jda-6.3.0") { exclude(module = "opus-java") } + + implementation("net.dv8tion", "JDA", "6.4.1") + implementation(libs.javalin.bundle) implementation(libs.javalin.openapi) implementation(libs.javalin.swagger) annotationProcessor(libs.javalin.annotation) implementation("net.lingala.zip4j", "zip4j", "2.11.6") + // config + implementation("dev.chojo", "ocular", "2.2.1") + implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.1.4") + // database implementation(libs.bundles.sadu) implementation("org.postgresql", "postgresql", "42.7.11") + // docker api + implementation(libs.bundles.docker) + // Logging implementation(libs.bundles.logging) implementation("club.minnced", "discord-webhooks", "0.8.4") diff --git a/bot/src/main/java/de/chojo/gamejam/Bot.java b/bot/src/main/java/de/chojo/gamejam/Bot.java index 65ab495..1cb918d 100644 --- a/bot/src/main/java/de/chojo/gamejam/Bot.java +++ b/bot/src/main/java/de/chojo/gamejam/Bot.java @@ -155,6 +155,7 @@ private void buildLocale() { .addLanguage(DiscordLocale.GERMAN) .withLanguageProvider(guild -> Optional.ofNullable(guilds.guild(guild).settings().locale()) .map(DiscordLocale::from)) + .withGuildLocaleCodeProvider((guild, s) -> Optional.empty()) .build(); } diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java b/bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java index eb299c9..a47ed14 100644 --- a/bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java +++ b/bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java @@ -6,12 +6,7 @@ package de.chojo.gamejam.configuration; -import de.chojo.gamejam.configuration.elements.Api; -import de.chojo.gamejam.configuration.elements.BaseSettings; -import de.chojo.gamejam.configuration.elements.Database; -import de.chojo.gamejam.configuration.elements.Plugins; -import de.chojo.gamejam.configuration.elements.ServerManagement; -import de.chojo.gamejam.configuration.elements.ServerTemplate; +import de.chojo.gamejam.configuration.elements.*; @SuppressWarnings("FieldMayBeFinal") public class ConfigFile { @@ -19,7 +14,14 @@ public class ConfigFile { private Database database = new Database(); private Api api = new Api(); private ServerManagement serverManagement = new ServerManagement(); + + private Docker docker = new Docker(); private Plugins plugins = new Plugins(); + + public void setServerTemplate(ServerTemplate serverTemplate) { + this.serverTemplate = serverTemplate; + } + private ServerTemplate serverTemplate = new ServerTemplate(); public BaseSettings baseSettings() { @@ -38,6 +40,10 @@ public ServerManagement serverManagement(){ return serverManagement; } + public Docker docker() { + return docker; + } + public Plugins plugins() { return plugins; } diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java b/bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java index ab52e43..cf59197 100644 --- a/bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java +++ b/bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java @@ -6,17 +6,19 @@ package de.chojo.gamejam.configuration; -import de.chojo.gamejam.configuration.elements.Api; -import de.chojo.gamejam.configuration.elements.BaseSettings; -import de.chojo.gamejam.configuration.elements.Database; -import de.chojo.gamejam.configuration.elements.Plugins; -import de.chojo.gamejam.configuration.elements.ServerManagement; -import de.chojo.gamejam.configuration.elements.ServerTemplate; -import de.chojo.jdautil.configuration.BaseConfiguration; - -public class Configuration extends BaseConfiguration { +import de.chojo.gamejam.configuration.elements.*; +import dev.chojo.ocular.Configurations; +import dev.chojo.ocular.dataformats.YamlDataFormat; +import dev.chojo.ocular.key.Key; + +import java.nio.file.Path; +import java.util.List; + +public class Configuration extends Configurations { + public static final Key MAIN = Key.builder(Path.of("config.yml"), ConfigFile::new).build(); + private Configuration() { - super(new ConfigFile()); + super(Path.of("config"), MAIN, List.of(new YamlDataFormat()), Configuration.class.getClassLoader(), null); } public static Configuration create() { @@ -27,26 +29,30 @@ public static Configuration create() { public Database database() { - return config().database(); + return main().database(); + } + + public Docker docker() { + return main().docker(); } public BaseSettings baseSettings() { - return config().baseSettings(); + return main().baseSettings(); } public Api api() { - return config().api(); + return main().api(); } public ServerManagement serverManagement() { - return config().serverManagement(); + return main().serverManagement(); } public Plugins plugins() { - return config().plugins(); + return main().plugins(); } public ServerTemplate serverTemplate() { - return config().serverTemplate(); + return main().serverTemplate(); } } diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java new file mode 100644 index 0000000..c7c5958 --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.configuration.elements; + + +@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) +public class Docker { + private String host = "unix:///var/run/docker.sock"; + private String certPath = "/home/user/.docker"; + private boolean tlsVerify = true; + private String registryUsername; + private String registryPassword; + private String registryEmail; + private String registryUrl; + + public String getHost() { + return host; + } + + public String getCertPath() { + return certPath; + } + + public boolean isTlsVerify() { + return tlsVerify; + } + + public String getRegistryUsername() { + return registryUsername; + } + + public String getRegistryPassword() { + return registryPassword; + } + + public String getRegistryEmail() { + return registryEmail; + } + + public String getRegistryUrl() { + return registryUrl; + } +} \ No newline at end of file diff --git a/bot/src/main/java/de/chojo/gamejam/server/DockerService.java b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java new file mode 100644 index 0000000..bad536d --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java @@ -0,0 +1,145 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.server; + +import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.*; +import com.github.dockerjava.core.DefaultDockerClientConfig; +import com.github.dockerjava.core.DockerClientConfig; +import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; +import com.github.dockerjava.transport.DockerHttpClient; +import de.chojo.gamejam.configuration.elements.Docker; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import static org.slf4j.LoggerFactory.getLogger; + +public class DockerService { + private DockerClientConfig dockerClientConfig; + private DockerClient dockerClient; + private static final Logger log = getLogger(DockerService.class); + private static final String DOCKER_IMAGE = "itzg/minecraft-server:latest"; + private static final String DOCKER_VOLUME_DATA_DIR = "/data"; + + public DockerService(Docker dockerConfig) { + this.dockerClientConfig = DefaultDockerClientConfig + .createDefaultConfigBuilder() + .withDockerHost(dockerConfig.getHost()) + .withDockerCertPath(dockerConfig.getCertPath()) + .withDockerTlsVerify(dockerConfig.isTlsVerify()) + .withRegistryUsername(dockerConfig.getRegistryUsername()) + .withRegistryPassword(dockerConfig.getRegistryPassword()) + .withRegistryEmail(dockerConfig.getRegistryEmail()) + .withRegistryUrl(dockerConfig.getRegistryUrl()) + .build(); + } + + public void initDockerClient() { + DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder() + .dockerHost(dockerClientConfig.getDockerHost()) + .sslConfig(dockerClientConfig.getSSLConfig()) + .maxConnections(100) + .connectionTimeout(Duration.ofSeconds(30)) + .responseTimeout(Duration.ofSeconds(45)) + .build(); + + dockerClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient); + } + + public void shutdown() throws IOException { + dockerClient.close(); + } + + public void provisionServer(int teamId) { + log.info("Provisioning server for team {}", teamId); + dockerClient.createVolumeCmd() + .withName(volumeName(teamId)) + .exec(); + + HostConfig hostConfig = HostConfig.newHostConfig() + .withBinds(new Bind(volumeName(teamId), new Volume(DOCKER_VOLUME_DATA_DIR))); + + dockerClient.createContainerCmd(DOCKER_IMAGE) + .withName(containerName(teamId)) + .withHostConfig(hostConfig) + .exec(); + log.info("Server provisioned for team with container name {} and volume name {}", containerName(teamId), volumeName(teamId)); + } + + public void destroyServer(int teamId) { + log.info("Destroying server for team {}", teamId); + dockerClient.removeContainerCmd(containerName(teamId)).exec(); + dockerClient.removeVolumeCmd(volumeName(teamId)).exec(); + } + + public void startServer(int teamId) { + log.info("Starting server for team {}", teamId); + dockerClient.startContainerCmd(containerName(teamId)).exec(); + } + + public void stopServer(int teamId) { + log.info("Stopping server for team {}", teamId); + dockerClient.stopContainerCmd(containerName(teamId)).exec(); + } + + public void restartServer(int teamId) { + dockerClient.restartContainerCmd(containerName(teamId)).exec(); + } + + public boolean running(int teamId) { + return dockerClient.listContainersCmd() + .withShowAll(true) + .withNameFilter(List.of(containerName(teamId))) + .exec() + .stream() + .anyMatch(container -> container.getState().equals("running")); + } + + public void sendCommand(int teamId, String command) { + dockerClient.execCreateCmd(containerName(teamId)) + .withCmd(String.format("mc rcon-cli %s", command)) + .exec(); + } + + public boolean exists(int teamId) { + return dockerClient.listContainersCmd() + .withShowAll(true) + .exec() + .stream() + .anyMatch(container -> container.getId().startsWith(containerName(teamId))); + } + + public Optional container(int teamId) { + return dockerClient.listContainersCmd() + .withShowAll(true) + .withNameFilter(List.of(containerName(teamId))) + .exec() + .stream() + .findFirst(); + } + + public void copyArchiveToContainer(int teamId, Path source, Path destination) { + dockerClient.copyArchiveToContainerCmd(containerName(teamId)) + .withHostResource(source.toString()) + .withRemotePath(destination.toString()) + .exec(); + } + + private String volumeName(int teamId) { + return "plugin-jam-team-" + teamId; + } + + public String containerName(int teamId) { + return "plugin-jam-team-" + teamId; + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/server/ServerService.java b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java index 81a7b82..794e887 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/ServerService.java +++ b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java @@ -36,6 +36,7 @@ public class ServerService implements Runnable { private Teams teams; private final Configuration configuration; private final Stack freePorts = new Stack<>(); + private final DockerService dockerService; public static ServerService create(ScheduledExecutorService executorService, Configuration configuration) { var serverService = new ServerService(configuration); @@ -47,6 +48,8 @@ private ServerService(Configuration configuration) { this.configuration = configuration; IntStream.rangeClosed(configuration.serverManagement().minPort(), configuration.serverManagement().maxPort()) .forEach(freePorts::add); + this.dockerService = new DockerService(configuration.docker()); + this.dockerService.initDockerClient(); } public void shutdown() { @@ -123,7 +126,7 @@ public CompletableFuture syncVelocity() { } var team = optTeam.get(); log.info("Registered server for team {} with id {}", team.meta().name(), team.id()); - var teamServer = new TeamServer(this, team, configuration, registration.port(), registration.apiPort()); + var teamServer = new TeamServer(this, dockerService, team, configuration, registration.port(), registration.apiPort()); teamServer.running(true); server.put(team, teamServer); freePorts.removeElement(registration.apiPort()); @@ -134,7 +137,7 @@ public CompletableFuture syncVelocity() { } public TeamServer get(Team team) { - return server.computeIfAbsent(team, key -> new TeamServer(this, key, configuration, nextPort(), nextPort())); + return server.computeIfAbsent(team, key -> new TeamServer(this, dockerService, key, configuration, nextPort(), nextPort())); } private int nextPort() { diff --git a/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java index 2f34332..44c8968 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java +++ b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java @@ -11,12 +11,10 @@ import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; import de.chojo.gamejam.util.Mapper; import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; -import de.chojo.jdautil.util.Futures; import de.chojo.jdautil.wrapper.EventContext; import de.chojo.pluginjam.payload.RequestsPayload; import de.chojo.pluginjam.payload.StatsPayload; import net.dv8tion.jda.api.entities.MessageEmbed; -import net.lingala.zip4j.ZipFile; import org.slf4j.Logger; import java.io.File; @@ -31,7 +29,6 @@ import java.nio.file.StandardCopyOption; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Optional; @@ -45,6 +42,7 @@ public class TeamServer { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss.SSSS"); private static final Logger log = getLogger(TeamServer.class); private final ServerService serverService; + private final DockerService dockerService; private final Team team; private final Configuration configuration; private final int port; @@ -72,8 +70,9 @@ public class TeamServer { private boolean running; - public TeamServer(ServerService serverService, Team team, Configuration configuration, int port, int apiPort) { + public TeamServer(ServerService serverService, DockerService dockerService, Team team, Configuration configuration, int port, int apiPort) { this.serverService = serverService; + this.dockerService = dockerService; this.team = team; this.configuration = configuration; this.port = port; @@ -85,7 +84,7 @@ public boolean running() { } public boolean exists() { - return serverDir().toFile().exists(); + return dockerService.exists(team.id()); } /** @@ -96,6 +95,7 @@ public boolean exists() { */ public boolean setup() throws IOException { if (exists()) return false; + dockerService.provisionServer(team.id()); log.info("Setting up server of team {}", team); writeTemplate(); return true; @@ -159,49 +159,20 @@ private void writeTemplate() throws IOException { */ public boolean purge() throws IOException { if (!exists()) return false; - if(running()) stop().join(); + if(running()) { + log.info("Stopping server for team {}", team); + stop().join(); + } log.info("Purging server of team {}", team); - return deleteDirectory(serverDir()); + dockerService.destroyServer(team.id()); + return true; } public boolean start() { if (!exists() || running()) return false; - var server = configuration.serverManagement(); - var command = new ArrayList(); - command.add("screen"); - command.add("-dmS"); - command.add(screenName()); - command.add("java"); - command.add("-Xmx%dM".formatted(server.memory())); - command.add("-Xms%dM".formatted(server.memory())); - command.addAll(AIKAR); - command.addAll(server.parameter()); - command.add("-Dpluginjam.port=" + server.velocityPort()); - command.add("-Dpluginjam.host=" + server.getVelocityHost()); - command.add("-Dpluginjam.team.id=" + team.id()); - command.add("-Dpluginjam.team.name=" + teamName()); - command.add("-Djavalin.port=" + apiPort); - command.add("-Dcom.mojang.eula.agree=true"); - command.add("--enable-preview"); - command.add("-jar"); - command.add("server.jar"); - command.add("--max-players"); - command.add(String.valueOf(server.maxPlayers())); - command.add("--nogui"); - command.add("--port"); - command.add(String.valueOf(port)); log.info("Starting server server of team {}", team); - try { - new ProcessBuilder() - .directory(serverDir().toFile()) - .command(command) - .redirectOutput(ProcessBuilder.Redirect.to(processLogFile("start"))) - .start(); - running = true; - } catch (IOException e) { - log.error("Could not start server", e); - return false; - } + dockerService.startServer(team.id()); + running = true; return true; } @@ -218,50 +189,16 @@ public CompletableFuture stop(boolean restart) { return CompletableFuture.completedFuture(null); } running = false; - try { - CompletableFuture future = new ProcessBuilder() - .directory(new File("").toPath().toAbsolutePath().toFile()) - .command("./wait.sh", screenName()) - .redirectOutput(ProcessBuilder.Redirect.to(processLogFile("stop"))) - .start() - .onExit() - .whenComplete(Futures.whenComplete( - exit -> { - log.info("Stopped server of team {}", team); - serverService.stopped(this, restart); - }, - err -> log.error("Could not stop server {}", team)) - ) - .thenApply(r -> null); - send("stop"); - log.info("Stopping server of team {}", team); - return future; - } catch (IOException e) { - log.error("Failed to build process builder", e); - throw new RuntimeException(e); - } + CompletableFuture future = CompletableFuture.runAsync(() -> { + dockerService.stopServer(team.id()); + }); + log.info("Stopping server of team {}", team); + return future; } public void send(String command) { log.info("Sending command \"{}\" to server of team {}.", command, team); - try { - new ProcessBuilder() - .directory(serverDir().toFile()) - .redirectOutput(ProcessBuilder.Redirect.to(processLogFile("send"))) - .command(List.of( - "screen", - "-S", - screenName(), - "-p", - "0", - "-X", - "stuff", - "%s^M".formatted(command) - )) - .start(); - } catch (IOException e) { - throw new RuntimeException(e); - } + dockerService.sendCommand(team.id(), command); } private File processLogFile(String type) { @@ -284,10 +221,6 @@ private String teamName() { return team.meta().name().toLowerCase().replace(" ", "_"); } - private String screenName() { - return "team_%d_%d".formatted(team.id(), port); - } - public Team team() { return team; } @@ -315,60 +248,8 @@ public Path logFile() { public boolean replaceWorld(Path newWorld) { log.info("Replacing world"); - var worldDir = serverDir().resolve("world"); - var tempWorld = serverDir().resolve("t_world"); - - try (var zip = new ZipFile(newWorld.toFile())) { - log.info("Extracting zip file"); - zip.extractAll(tempWorld.toAbsolutePath().toString()); - } catch (IOException e) { - log.info("Failed to extract zip file", e); - return false; - } - - var copyWorld = tempWorld; - var dirFiles = List.of(tempWorld.toFile().listFiles()); - - var dirOffstet = 3; - - if (dirFiles.size() == 1) { - log.info("No world data found"); - copyWorld = tempWorld.resolve(dirFiles.get(0).getName()); - dirOffstet++; - } - - if (!copyWorld.resolve("region").toFile().exists()) { - log.warn("No region directory."); - return false; - } - - log.info("Deleting old world"); - if (!deleteDirectory(worldDir)) { - return false; - } - - if (copyWorld.resolve("session.lock").toFile().exists()) { - log.info("Found session lock. Deleting."); - copyWorld.resolve("session.lock").toFile().delete(); - } - - log.info("Copy new world data."); - try (var files = Files.walk(copyWorld)) { - Files.createDirectories(worldDir); - for (var sourceTarget : files.toList()) { - // skip root dir - if (sourceTarget.getNameCount() == dirOffstet) continue; - var filePath = sourceTarget.subpath(dirOffstet, sourceTarget.getNameCount()); - var serverTarget = worldDir.resolve(filePath); - Files.copy(sourceTarget, serverTarget, StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - log.error("Could not copy world", e); - return false; - } - - log.info("Cleaning up temp world"); - return deleteDirectory(tempWorld); + dockerService.copyArchiveToContainer(team.id(), newWorld, serverDir()); + return true; } public boolean deleteDirectory(Path path) { diff --git a/bot/src/main/resources/log4j2.xml b/bot/src/main/resources/log4j2.xml index f05b7d9..aa2c29e 100644 --- a/bot/src/main/resources/log4j2.xml +++ b/bot/src/main/resources/log4j2.xml @@ -1,5 +1,5 @@ - + @@ -16,28 +16,6 @@ - - - - - - - - - - - - - - - - - - - @@ -45,13 +23,9 @@ - + - - - - diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 0000000..026dbe6 --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,34 @@ +FROM gradle:jdk25-alpine as build +WORKDIR /home/gradle + +COPY --chown=gradle:gradle settings.gradle* build.gradle* gradle.properties* ./ +COPY --chown=gradle:gradle bot/build.gradle* ./bot/ +COPY --chown=gradle:gradle plugin-paper/build.gradle* ./plugin-paper/ + +RUN gradle dependencies --no-daemon || true + +COPY --chown=gradle:gradle . . + +RUN gradle :bot:build :plugin-paper:build --no-daemon + +# We use a jammy image because we need some more stuff than alpine provides +FROM eclipse-temurin:25-jammy as runtime +WORKDIR /app + +# Setting up the bot +COPY --from=build /home/gradle/bot/build/libs/bot-*-all.jar ./bot.jar +RUN mkdir plugins +RUN mkdir servers +RUN mkdir template +RUN mkdir template/plugins +COPY docker/resources/bot/wait.sh . +# Copy the plugin jam plugin into the template. +COPY --from=build /home/gradle/plugin-paper/build/libs/plugin-paper-*-all.jar ./bot/template/plugins/pluginjam.jar + +COPY docker/resources/docker-entrypoint.sh . + +EXPOSE 8080 + +HEALTHCHECK CMD curl --fail http://localhost:8080/swagger-ui || exit 1 + +ENTRYPOINT ["bash", "docker-entrypoint.sh"] diff --git a/dev.compose.yml b/dev.compose.yml index d9aa479..e1de3f2 100644 --- a/dev.compose.yml +++ b/dev.compose.yml @@ -1,17 +1,22 @@ services: bot: - build: . + build: + context: . + dockerfile: dev.Dockerfile volumes: - ./data/bot/config:/app/config - ./data/bot/plugins:/app/plugins - ./data/bot/servers:/app/servers - ./data/bot/template:/app/template + - /var/run/docker.sock:/var/run/docker.sock + group_add: + - 131 database: image: postgres:16 environment: POSTGRES_PASSWORD: postgres ports: - - 5433:5432 + - "127.0.0.1:5433:5432" velocity: image: itzg/mc-proxy:latest tty: true diff --git a/settings.gradle.kts b/settings.gradle.kts index c4470c0..92977b9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,11 @@ dependencyResolutionManagement { library("log4j-slf4j2", "org.apache.logging.log4j", "log4j-slf4j2-impl").versionRef("log4j") bundle("logging", listOf("slf4j", "log4j-core", "log4j-slf4j2")) + version("docker", "3.7.1") + library("docker-api", "com.github.docker-java", "docker-java-core").versionRef("docker") + library("docker-transport", "com.github.docker-java", "docker-java-transport-httpclient5").versionRef("docker") + bundle("docker", listOf("docker-api", "docker-transport")) + version("javalin", "7.2.2") library("javalin-core", "io.javalin", "javalin").versionRef("javalin") From d8ee5a34e59cc9cd2615db3903c7cf7bfc753c08 Mon Sep 17 00:00:00 2001 From: Nora <46890129+RainbowDashLabs@users.noreply.github.com> Date: Sat, 6 Jun 2026 19:02:09 +0200 Subject: [PATCH 2/5] Fix logging --- bot/src/main/resources/log4j2.xml | 4 ++-- dev.Dockerfile | 3 +++ dev.compose.yml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/bot/src/main/resources/log4j2.xml b/bot/src/main/resources/log4j2.xml index aa2c29e..79a4c7c 100644 --- a/bot/src/main/resources/log4j2.xml +++ b/bot/src/main/resources/log4j2.xml @@ -1,10 +1,10 @@ - + diff --git a/dev.Dockerfile b/dev.Dockerfile index 026dbe6..f202382 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -29,6 +29,9 @@ COPY docker/resources/docker-entrypoint.sh . EXPOSE 8080 +COPY bot/src/main/resources/log4j2.xml config/log4j2.xml +RUN touch config/config.yml + HEALTHCHECK CMD curl --fail http://localhost:8080/swagger-ui || exit 1 ENTRYPOINT ["bash", "docker-entrypoint.sh"] diff --git a/dev.compose.yml b/dev.compose.yml index e1de3f2..daa2ee9 100644 --- a/dev.compose.yml +++ b/dev.compose.yml @@ -4,7 +4,7 @@ services: context: . dockerfile: dev.Dockerfile volumes: - - ./data/bot/config:/app/config + - ./data/bot/config/config.yml:/app/config/config.yml - ./data/bot/plugins:/app/plugins - ./data/bot/servers:/app/servers - ./data/bot/template:/app/template From 08d0206e29a390e3eb2ee002816b3c48b74db4d2 Mon Sep 17 00:00:00 2001 From: TheZexquex Date: Sat, 6 Jun 2026 19:10:33 +0200 Subject: [PATCH 3/5] Fix check if server exists --- bot/src/main/java/de/chojo/gamejam/server/DockerService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/src/main/java/de/chojo/gamejam/server/DockerService.java b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java index bad536d..72fe3a4 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/DockerService.java +++ b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java @@ -114,9 +114,11 @@ public void sendCommand(int teamId, String command) { public boolean exists(int teamId) { return dockerClient.listContainersCmd() .withShowAll(true) + .withNameFilter(List.of(containerName(teamId))) .exec() .stream() - .anyMatch(container -> container.getId().startsWith(containerName(teamId))); + .findAny() + .isPresent(); } public Optional container(int teamId) { From 4dd813b3faf341b259dbb045242b2254eadd097f Mon Sep 17 00:00:00 2001 From: TheZexquex Date: Mon, 8 Jun 2026 19:41:06 +0200 Subject: [PATCH 4/5] Switch some more things to docker (stats, console commands, ...) --- .../java/de/chojo/gamejam/api/v1/Server.java | 4 +- .../commands/server/configure/MaxPlayers.java | 3 +- .../commands/server/configure/Message.java | 2 +- .../server/configure/SpectatorOverflow.java | 3 +- .../commands/server/configure/Whitelist.java | 3 +- .../gamejam/commands/server/process/Log.java | 20 ++--- .../commands/server/process/Restart.java | 4 +- .../gamejam/commands/server/process/Stop.java | 2 +- .../handler/restart/RestartAll.java | 4 +- .../handler/restart/RestartTeam.java | 2 +- .../serveradmin/handler/stop/StopAll.java | 4 +- .../serveradmin/handler/stop/StopTeam.java | 2 +- .../configuration/elements/Docker.java | 7 +- .../configuration/elements/Plugins.java | 5 ++ .../chojo/gamejam/server/DockerService.java | 84 +++++++++++++++-- .../chojo/gamejam/server/ServerService.java | 6 +- .../de/chojo/gamejam/server/TeamServer.java | 89 +++++++------------ compose.yml | 12 +++ dev.compose.yml | 13 +++ 19 files changed, 173 insertions(+), 96 deletions(-) diff --git a/bot/src/main/java/de/chojo/gamejam/api/v1/Server.java b/bot/src/main/java/de/chojo/gamejam/api/v1/Server.java index 2287ac1..43567bf 100644 --- a/bot/src/main/java/de/chojo/gamejam/api/v1/Server.java +++ b/bot/src/main/java/de/chojo/gamejam/api/v1/Server.java @@ -107,9 +107,9 @@ private void handle(@NotNull Context ctx) { ctx.status(HttpStatus.ACCEPTED); String restart = ctx.queryParam("restart"); - if ("true".equals(restart) && teamServer.running()) { + if ("true".equals(restart) && teamServer.isRunning()) { teamServer.restart(); - } else if (teamServer.running()) { + } else if (teamServer.isRunning()) { teamServer.send("say Plugin Updated"); } } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/MaxPlayers.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/MaxPlayers.java index 8008c8f..d85e6b2 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/MaxPlayers.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/MaxPlayers.java @@ -7,7 +7,6 @@ package de.chojo.gamejam.commands.server.configure; import de.chojo.gamejam.commands.server.Server; -import de.chojo.gamejam.server.TeamServer; import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; import de.chojo.jdautil.util.Futures; import de.chojo.jdautil.wrapper.EventContext; @@ -37,7 +36,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont .POST(HttpRequest.BodyPublishers.ofString(String.valueOf(event.getOption("amount").getAsInt()))) .build(); - if (!teamServer.running()) { + if (!teamServer.isRunning()) { event.reply(context.localize("error.servernotrunning")).queue(); return; } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Message.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Message.java index 9491627..aa124fd 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Message.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Message.java @@ -37,7 +37,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var teamServer = optServer.get(); - if (!teamServer.running()) { + if (!teamServer.isRunning()) { event.reply(context.localize("error.servernotrunning")).queue(); return; } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/SpectatorOverflow.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/SpectatorOverflow.java index b7a987f..7829237 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/SpectatorOverflow.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/SpectatorOverflow.java @@ -7,7 +7,6 @@ package de.chojo.gamejam.commands.server.configure; import de.chojo.gamejam.commands.server.Server; -import de.chojo.gamejam.server.TeamServer; import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; import de.chojo.jdautil.util.Futures; import de.chojo.jdautil.wrapper.EventContext; @@ -34,7 +33,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var teamServer = optServer.get(); - if (!teamServer.running()) { + if (!teamServer.isRunning()) { event.reply(context.localize("error.servernotrunning")).queue(); return; } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Whitelist.java b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Whitelist.java index 8568d95..dd17ff2 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Whitelist.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/configure/Whitelist.java @@ -7,7 +7,6 @@ package de.chojo.gamejam.commands.server.configure; import de.chojo.gamejam.commands.server.Server; -import de.chojo.gamejam.server.TeamServer; import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; import de.chojo.jdautil.util.Futures; import de.chojo.jdautil.wrapper.EventContext; @@ -34,7 +33,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var teamServer = optServer.get(); - if (!teamServer.running()) { + if (!teamServer.isRunning()) { event.reply(context.localize("error.servernotrunning")).queue(); return; } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java index 60d0d89..ffa9719 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java @@ -12,7 +12,10 @@ import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.utils.FileUpload; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; public class Log implements SlashHandler { @@ -27,16 +30,13 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var optServer = server.getServer(event, context); if(optServer.isEmpty())return; var teamServer = optServer.get(); - var logFile = teamServer.logFile(); - String content; - try { - content = Files.readString(logFile); - } catch (IOException e) { - content = ""; + var logs = teamServer.logs(0); + var content = logs.substring(Math.max(logs.length() - 1950, 0)); + try(InputStream inputStream = new ByteArrayInputStream(logs.getBytes(StandardCharsets.UTF_8))) { + event.reply("```log%n%s%n```".formatted(content)) + .addFiles(FileUpload.fromData(inputStream, "latest.log")) + .queue(); + } catch (IOException _) { } - content = content.substring(Math.max(content.length() - 1950, 0)); - event.reply("```log%n%s%n```".formatted(content)) - .addFiles(FileUpload.fromData(logFile, "latest.log")) - .queue(); } } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Restart.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Restart.java index 487b31b..14542bb 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Restart.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Restart.java @@ -24,8 +24,8 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont if(optServer.isEmpty())return; var teamServer = optServer.get(); if (teamServer.exists()) { - teamServer.stop(true) - .thenRun(() -> event.getHook().editOriginal(context.localize("command.server.process.restart.message.restarted")).queue()); + teamServer.stop(); + event.getHook().editOriginal(context.localize("command.server.process.restart.message.restarted")).queue(); event.reply(context.localize("command.server.process.restart.message.restarting")).queue(); } } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Stop.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Stop.java index 9e28f91..fc7cb3f 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Stop.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Stop.java @@ -25,7 +25,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var teamServer = optServer.get(); if (teamServer.exists()) { event.reply(context.localize("command.server.process.stop.message.stopping")).queue(); - teamServer.stop(false).thenRun(() -> event.getHook().editOriginal( + teamServer.stop().thenRun(() -> event.getHook().editOriginal( context.localize("command.server.process.stop.message.stopped")).queue()); } } diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartAll.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartAll.java index 8472b89..7b35cd1 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartAll.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartAll.java @@ -34,9 +34,9 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var count = jam.teams().teams().stream() .map(serverService::get) .filter(server -> { - var running = server.running(); + var running = server.isRunning(); if (running) { - server.stop(true); + server.stop(); } return running; }) diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartTeam.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartTeam.java index e712f51..57190df 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartTeam.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/restart/RestartTeam.java @@ -42,7 +42,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont } var started = optTeam.map(serverService::get).map(server -> { - var running = server.running(); + var running = server.isRunning(); server.restart(); return running; }).orElse(false); diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopAll.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopAll.java index c6939e3..bdafd25 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopAll.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopAll.java @@ -34,8 +34,8 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var count = jam.teams().teams().stream() .map(serverService::get) .map(server -> { - var running = server.running(); - server.stop(false); + var running = server.isRunning(); + server.stop(); return running; }) .filter(v -> v) diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopTeam.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopTeam.java index f6d25f8..cbf77bb 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopTeam.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/stop/StopTeam.java @@ -42,7 +42,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont } var started = optTeam.map(serverService::get).map(server -> { - var running = server.running(); + var running = server.isRunning(); server.stop(); return running; }).orElse(false); diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java index c7c5958..3471c18 100644 --- a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java @@ -6,7 +6,6 @@ package de.chojo.gamejam.configuration.elements; - @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public class Docker { private String host = "unix:///var/run/docker.sock"; @@ -16,6 +15,7 @@ public class Docker { private String registryPassword; private String registryEmail; private String registryUrl; + private String networkName = "plugin-jam-network"; public String getHost() { return host; @@ -44,4 +44,9 @@ public String getRegistryEmail() { public String getRegistryUrl() { return registryUrl; } + + public String getNetworkName() { + return networkName; + } + } \ No newline at end of file diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Plugins.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Plugins.java index 7ea8a94..db7a00b 100644 --- a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Plugins.java +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Plugins.java @@ -16,11 +16,16 @@ @SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"}) public class Plugins { private String pluginDir = "plugins"; + private List defaultPlugins = List.of(); public String pluginDir() { return pluginDir; } + public List defaultPlugins() { + return defaultPlugins; + } + private Path pluginPath() { return Path.of(pluginDir); } diff --git a/bot/src/main/java/de/chojo/gamejam/server/DockerService.java b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java index 72fe3a4..312b3e1 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/DockerService.java +++ b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java @@ -11,9 +11,11 @@ import com.github.dockerjava.core.DefaultDockerClientConfig; import com.github.dockerjava.core.DockerClientConfig; import com.github.dockerjava.core.DockerClientImpl; +import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; import com.github.dockerjava.transport.DockerHttpClient; import de.chojo.gamejam.configuration.elements.Docker; +import de.chojo.gamejam.configuration.elements.Plugins; import org.slf4j.Logger; import java.io.IOException; @@ -30,8 +32,12 @@ public class DockerService { private static final Logger log = getLogger(DockerService.class); private static final String DOCKER_IMAGE = "itzg/minecraft-server:latest"; private static final String DOCKER_VOLUME_DATA_DIR = "/data"; + private final String networkName; + private final String pluginUrls; - public DockerService(Docker dockerConfig) { + public DockerService(Docker dockerConfig, Plugins pluginsConfig) { + this.networkName = dockerConfig.getNetworkName(); + this.pluginUrls = String.join(",", pluginsConfig.defaultPlugins()); this.dockerClientConfig = DefaultDockerClientConfig .createDefaultConfigBuilder() .withDockerHost(dockerConfig.getHost()) @@ -54,6 +60,20 @@ public void initDockerClient() { .build(); dockerClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient); + ensureNetwork(); + } + + private void ensureNetwork() { + var networks = dockerClient.listNetworksCmd() + .withNameFilter(networkName) + .exec(); + if (networks.stream().noneMatch(n -> n.getName().equals(networkName))) { + dockerClient.createNetworkCmd() + .withName(networkName) + .withDriver("bridge") + .exec(); + log.info("Created docker network {}", networkName); + } } public void shutdown() throws IOException { @@ -69,8 +89,11 @@ public void provisionServer(int teamId) { HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(volumeName(teamId), new Volume(DOCKER_VOLUME_DATA_DIR))); + hostConfig.withNetworkMode(networkName); + dockerClient.createContainerCmd(DOCKER_IMAGE) .withName(containerName(teamId)) + .withEnv("EULA=TRUE", "TYPE=PAPER", "VERSION=26.1.2", String.format("PLUGINS=%s", pluginUrls)) .withHostConfig(hostConfig) .exec(); log.info("Server provisioned for team with container name {} and volume name {}", containerName(teamId), volumeName(teamId)); @@ -96,7 +119,7 @@ public void restartServer(int teamId) { dockerClient.restartContainerCmd(containerName(teamId)).exec(); } - public boolean running(int teamId) { + public boolean isRunning(int teamId) { return dockerClient.listContainersCmd() .withShowAll(true) .withNameFilter(List.of(containerName(teamId))) @@ -106,11 +129,31 @@ public boolean running(int teamId) { } public void sendCommand(int teamId, String command) { - dockerClient.execCreateCmd(containerName(teamId)) - .withCmd(String.format("mc rcon-cli %s", command)) - .exec(); + var container = container(teamId); + if (container.isEmpty()) { + log.error("Container not found for team {}", teamId); + return; + } + var execId = dockerClient.execCreateCmd(container.get().getId()) + .withAttachStdout(true) + .withAttachStderr(true) + .withCmd("rcon-cli", command) + .exec() + .getId(); + try { + dockerClient.execStartCmd(execId) + .exec(new ResultCallback.Adapter() { + @Override + public void onNext(Frame frame) { + log.info("rcon-cli response for team {}: {}", teamId, new String(frame.getPayload())); + } + }) + .awaitCompletion(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Interrupted while sending command to team {}", teamId, e); + } } - public boolean exists(int teamId) { return dockerClient.listContainersCmd() .withShowAll(true) @@ -130,6 +173,35 @@ public Optional container(int teamId) { .findFirst(); } + public String logs(int teamId, int tail) { + var callback = new ResultCallback.Adapter() { + private final StringBuilder logs = new StringBuilder(); + + @Override + public void onNext(Frame frame) { + logs.append(new String(frame.getPayload())); + } + + public String getLogs() { + return logs.toString(); + } + }; + + try { + dockerClient.logContainerCmd(containerName(teamId)) + .withStdOut(true) + .withStdErr(true) + .withTail(tail) + .exec(callback) + .awaitCompletion(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Interrupted while retrieving logs for team {}", teamId, e); + } + + return callback.getLogs(); + } + public void copyArchiveToContainer(int teamId, Path source, Path destination) { dockerClient.copyArchiveToContainerCmd(containerName(teamId)) .withHostResource(source.toString()) diff --git a/bot/src/main/java/de/chojo/gamejam/server/ServerService.java b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java index 794e887..dd7d73d 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/ServerService.java +++ b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java @@ -48,7 +48,7 @@ private ServerService(Configuration configuration) { this.configuration = configuration; IntStream.rangeClosed(configuration.serverManagement().minPort(), configuration.serverManagement().maxPort()) .forEach(freePorts::add); - this.dockerService = new DockerService(configuration.docker()); + this.dockerService = new DockerService(configuration.docker(), configuration.plugins()); this.dockerService.initDockerClient(); } @@ -61,7 +61,7 @@ public void shutdown() { @Override public void run() { for (var value : server.values()) { - if (!value.running()) continue; + if (!value.isRunning()) continue; try { value.serverRequests() .ifPresent(server -> { @@ -127,7 +127,7 @@ public CompletableFuture syncVelocity() { var team = optTeam.get(); log.info("Registered server for team {} with id {}", team.meta().name(), team.id()); var teamServer = new TeamServer(this, dockerService, team, configuration, registration.port(), registration.apiPort()); - teamServer.running(true); + //teamServer.running(true); server.put(team, teamServer); freePorts.removeElement(registration.apiPort()); freePorts.removeElement(registration.port()); diff --git a/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java index 44c8968..a11defd 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java +++ b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java @@ -30,7 +30,6 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Comparator; -import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -41,34 +40,13 @@ public class TeamServer { private static final HttpClient http = HttpClient.newHttpClient(); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss.SSSS"); private static final Logger log = getLogger(TeamServer.class); + private static final int DEFAULT_API_PORT = 30000; private final ServerService serverService; private final DockerService dockerService; private final Team team; private final Configuration configuration; private final int port; private final int apiPort; - private static final List AIKAR = List.of( - "-XX:+ParallelRefProcEnabled", - "-XX:MaxGCPauseMillis=200", - "-XX:+UnlockExperimentalVMOptions", - "-XX:+DisableExplicitGC", - "-XX:+AlwaysPreTouch", - "-XX:G1NewSizePercent=30", - "-XX:G1MaxNewSizePercent=40", "-XX:G1HeapRegionSize=8M", - "-XX:G1ReservePercent=20", - "-XX:G1HeapWastePercent=5", - "-XX:G1MixedGCCountTarget=4", - "-XX:InitiatingHeapOccupancyPercent=15", - "-XX:G1MixedGCLiveThresholdPercent=90", - "-XX:G1RSetUpdatingPauseTimePercent=5", - "-XX:SurvivorRatio=32", - "-XX:+PerfDisableSharedMem", - "-XX:MaxTenuringThreshold=1", - "-Dusing.aikars.flags=https://mcflags.emc.gs", - "-Daikars.new.flags=true" - ); - private boolean running; - public TeamServer(ServerService serverService, DockerService dockerService, Team team, Configuration configuration, int port, int apiPort) { this.serverService = serverService; @@ -79,8 +57,8 @@ public TeamServer(ServerService serverService, DockerService dockerService, Team this.apiPort = apiPort; } - public boolean running() { - return running; + public boolean isRunning() { + return dockerService.isRunning(team.id()); } public boolean exists() { @@ -159,9 +137,9 @@ private void writeTemplate() throws IOException { */ public boolean purge() throws IOException { if (!exists()) return false; - if(running()) { + if (isRunning()) { log.info("Stopping server for team {}", team); - stop().join(); + stop(); } log.info("Purging server of team {}", team); dockerService.destroyServer(team.id()); @@ -169,31 +147,24 @@ public boolean purge() throws IOException { } public boolean start() { - if (!exists() || running()) return false; + if (!exists() || isRunning()) return false; log.info("Starting server server of team {}", team); dockerService.startServer(team.id()); - running = true; return true; } - public CompletableFuture stop() { - return stop(false); - } - - public CompletableFuture restart() { - return stop(true); + public CompletableFuture restart() { + return CompletableFuture.runAsync(() -> { + dockerService.restartServer(team.id()); + log.info("Starting server of team {}", team); + }); } - public CompletableFuture stop(boolean restart) { - if (!running) { - return CompletableFuture.completedFuture(null); - } - running = false; - CompletableFuture future = CompletableFuture.runAsync(() -> { + public CompletableFuture stop() { + return CompletableFuture.runAsync(() -> { dockerService.stopServer(team.id()); + log.info("Stopping server of team {}", team); }); - log.info("Stopping server of team {}", team); - return future; } public void send(String command) { @@ -236,14 +207,15 @@ public int apiPort() { @Override public String toString() { return "TeamServer{" + - "team=" + team + - ", port=" + port + - ", apiPort=" + apiPort + - '}'; + "team=" + team + + ", port=" + port + + ", apiPort=" + apiPort + + '}'; } - public Path logFile() { - return serverDir().resolve("logs").resolve("latest.log"); + public String logs(int tail) { + + return dockerService.logs(team.id(), tail); } public boolean replaceWorld(Path newWorld) { @@ -252,6 +224,7 @@ public boolean replaceWorld(Path newWorld) { return true; } + //TODO: replace with docker version public boolean deleteDirectory(Path path) { try (var files = Files.walk(path)) { files.sorted(Comparator.reverseOrder()) @@ -294,7 +267,7 @@ public CompletableFuture detailStatus(EventContext context) { if (!exists()) { builder.setDescription("teamserver.message.detailstatus.nonexisting.description"); } else { - if (running()) { + if (isRunning()) { builder.setDescription("teamserver.message.detailstatus.existing.description") .addField("word.ports", "$word.server$: %d%n$word.api$: %d".formatted(port, apiPort), true); stats().ifPresent(stats -> { @@ -318,7 +291,7 @@ public CompletableFuture detailStatus(EventContext context) { public String status() { var status = statusEmoji(); var ports = ""; - if (exists() && running()) { + if (exists() && isRunning()) { ports = "Server: %s Api: %s".formatted(port, apiPort); } return "%s %s %s".formatted(status, team, ports); @@ -347,23 +320,23 @@ public Optional stats() { } public HttpRequest.Builder requestBuilder(String path) { - return HttpRequest.newBuilder(URI.create("http://localhost:%d/%s".formatted(apiPort(), path))); + return HttpRequest.newBuilder(URI.create("http://%s:%d/%s".formatted(containerName(), DEFAULT_API_PORT, path))); } public HttpRequest.Builder requestBuilder(String path, String query) { - return HttpRequest.newBuilder(URI.create("http://localhost:%d/%s?%s".formatted(apiPort(), path, query))); + return HttpRequest.newBuilder(URI.create("http://%s:%d/%s?%s".formatted(containerName(), DEFAULT_API_PORT, path, query))); } - public HttpClient http() { - return http; + public String containerName() { + return dockerService.containerName(team.id()); } - public void running(boolean running) { - this.running = running; + public HttpClient http() { + return http; } private String statusEmoji() { - if (exists() && running()) return "🟢"; + if (exists() && isRunning()) return "🟢"; if (exists()) return "🟡"; return "🔴"; } diff --git a/compose.yml b/compose.yml index fd3f0bd..4188cbf 100644 --- a/compose.yml +++ b/compose.yml @@ -6,8 +6,11 @@ services: - ./data/bot/plugins:/app/plugins - ./data/bot/servers:/app/servers - ./data/bot/template:/app/template + - /var/run/docker.sock:/var/run/docker.sock depends_on: - + networks: + - plugin-jam-network velocity: image: itzg/mc-proxy:latest tty: true @@ -18,6 +21,8 @@ services: volumes: - ./data/velocity/config:/config - ./data/velocity/plugins:/plugins + networks: + - plugin-jam-network lobby: image: itzg/minecraft-server:latest stdin_open: true @@ -28,3 +33,10 @@ services: MEMORY: 2G volumes: - ./data/lobby:/data + networks: + - plugin-jam-network + +networks: + plugin-jam-network: + name: plugin-jam-network + driver: bridge diff --git a/dev.compose.yml b/dev.compose.yml index daa2ee9..18f4b6f 100644 --- a/dev.compose.yml +++ b/dev.compose.yml @@ -11,12 +11,16 @@ services: - /var/run/docker.sock:/var/run/docker.sock group_add: - 131 + networks: + - plugin-jam-network database: image: postgres:16 environment: POSTGRES_PASSWORD: postgres ports: - "127.0.0.1:5433:5432" + networks: + - plugin-jam-network velocity: image: itzg/mc-proxy:latest tty: true @@ -27,6 +31,8 @@ services: volumes: - ./data/velocity/config:/config - ./data/velocity/plugins:/plugins + networks: + - plugin-jam-network lobby: image: itzg/minecraft-server:latest stdin_open: true @@ -37,3 +43,10 @@ services: MEMORY: 2G volumes: - ./data/lobby:/data + networks: + - plugin-jam-network + +networks: + plugin-jam-network: + name: plugin-jam-network + driver: bridge From df3e36fe457221766fcb09163dba765aed4a1b25 Mon Sep 17 00:00:00 2001 From: TheZexquex Date: Tue, 9 Jun 2026 22:16:36 +0200 Subject: [PATCH 5/5] Custom docker images for the team servers and velocity, more refactoring and removing unused code. --- Dockerfile => Dockerfile.bot | 0 dev.Dockerfile => Dockerfile.bot.dev | 3 +- Dockerfile.mc | 15 +++ Dockerfile.velocity | 14 +++ bot/src/main/java/de/chojo/gamejam/Bot.java | 2 +- .../jamadmin/handler/jam/JamStart.java | 2 +- .../gamejam/commands/server/process/Log.java | 2 +- .../commands/serveradmin/ServerAdmin.java | 8 -- .../handler/refresh/RefreshAll.java | 43 ------- .../handler/refresh/RefreshTeam.java | 68 ----------- .../configuration/elements/Docker.java | 28 ++++- .../de/chojo/gamejam/message/EmbedHelper.java | 47 ++++++++ .../chojo/gamejam/server/DockerService.java | 30 +++-- .../chojo/gamejam/server/ServerService.java | 57 +-------- .../de/chojo/gamejam/server/TeamServer.java | 113 ++---------------- dev.compose.yml => compose.dev.yml | 23 +++- compose.yml | 16 ++- docker/docker-compose.yml | 28 ----- docker/resources/docker-entrypoint.sh | 2 +- 19 files changed, 163 insertions(+), 338 deletions(-) rename Dockerfile => Dockerfile.bot (100%) rename dev.Dockerfile => Dockerfile.bot.dev (92%) create mode 100644 Dockerfile.mc create mode 100644 Dockerfile.velocity delete mode 100644 bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshAll.java delete mode 100644 bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshTeam.java create mode 100644 bot/src/main/java/de/chojo/gamejam/message/EmbedHelper.java rename dev.compose.yml => compose.dev.yml (67%) delete mode 100644 docker/docker-compose.yml diff --git a/Dockerfile b/Dockerfile.bot similarity index 100% rename from Dockerfile rename to Dockerfile.bot diff --git a/dev.Dockerfile b/Dockerfile.bot.dev similarity index 92% rename from dev.Dockerfile rename to Dockerfile.bot.dev index f202382..4b25a08 100644 --- a/dev.Dockerfile +++ b/Dockerfile.bot.dev @@ -29,8 +29,7 @@ COPY docker/resources/docker-entrypoint.sh . EXPOSE 8080 -COPY bot/src/main/resources/log4j2.xml config/log4j2.xml -RUN touch config/config.yml +COPY bot/src/main/resources/log4j2.xml log4j2.xml HEALTHCHECK CMD curl --fail http://localhost:8080/swagger-ui || exit 1 diff --git a/Dockerfile.mc b/Dockerfile.mc new file mode 100644 index 0000000..5c05908 --- /dev/null +++ b/Dockerfile.mc @@ -0,0 +1,15 @@ +FROM gradle:jdk25-alpine AS build +WORKDIR /home/gradle +COPY --chown=gradle:gradle settings.gradle* build.gradle* gradle.properties* ./ +COPY --chown=gradle:gradle bot/build.gradle* ./bot/ +COPY --chown=gradle:gradle plugin-api/build.gradle* ./plugin-api/ +COPY --chown=gradle:gradle plugin-paper/build.gradle* ./plugin-paper/ +COPY --chown=gradle:gradle plugin-velocity/build.gradle* ./plugin-velocity/ +RUN mkdir -p plugin-paper/Readme.md && \ + gradle dependencies --no-daemon || true +COPY --chown=gradle:gradle plugin-api ./plugin-api +COPY --chown=gradle:gradle plugin-paper ./plugin-paper +RUN gradle :plugin-paper:shadowJar --no-daemon + +FROM itzg/minecraft-server:stable-java25 +COPY --from=build /home/gradle/plugin-paper/build/libs/plugin-paper-*-all.jar /plugins/pluginjam.jar diff --git a/Dockerfile.velocity b/Dockerfile.velocity new file mode 100644 index 0000000..c87e992 --- /dev/null +++ b/Dockerfile.velocity @@ -0,0 +1,14 @@ +FROM gradle:jdk25-alpine AS build +WORKDIR /home/gradle +COPY --chown=gradle:gradle settings.gradle* build.gradle* gradle.properties* ./ +COPY --chown=gradle:gradle bot/build.gradle* ./bot/ +COPY --chown=gradle:gradle plugin-api/build.gradle* ./plugin-api/ +COPY --chown=gradle:gradle plugin-paper/build.gradle* ./plugin-paper/ +COPY --chown=gradle:gradle plugin-velocity/build.gradle* ./plugin-velocity/ +RUN mkdir -p plugin-paper/Readme.md && \ + gradle dependencies --no-daemon || true +COPY --chown=gradle:gradle plugin-api ./plugin-api +COPY --chown=gradle:gradle plugin-velocity ./plugin-velocity +RUN gradle :plugin-velocity:shadowJar --no-daemon +FROM itzg/mc-proxy:latest +COPY --from=build /home/gradle/plugin-velocity/build/libs/plugin-velocity-*-all.jar /plugins/pluginjam.jar diff --git a/bot/src/main/java/de/chojo/gamejam/Bot.java b/bot/src/main/java/de/chojo/gamejam/Bot.java index 1cb918d..de929ea 100644 --- a/bot/src/main/java/de/chojo/gamejam/Bot.java +++ b/bot/src/main/java/de/chojo/gamejam/Bot.java @@ -243,7 +243,7 @@ private void initDb() throws IOException, SQLException { } private void initServer() throws IOException { - serverService = ServerService.create(createScheduledExecutor("Server ping", 1), configuration); + serverService = ServerService.create(configuration); var templateDir = Path.of(configuration.serverTemplate().templateDir()); var serverDir = Path.of(configuration.serverManagement().serverDir()); diff --git a/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamStart.java b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamStart.java index 32296d6..ee317ca 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamStart.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/jamadmin/handler/jam/JamStart.java @@ -32,7 +32,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont event.reply(context.localize("command.start.message.activated")) .setEphemeral(true) .queue(); - + return; } event.reply(context.localize("error.noupcomingjam")).setEphemeral(true) .queue(); diff --git a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java index ffa9719..68e4641 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/server/process/Log.java @@ -30,7 +30,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event, EventContext cont var optServer = server.getServer(event, context); if(optServer.isEmpty())return; var teamServer = optServer.get(); - var logs = teamServer.logs(0); + var logs = teamServer.logs(); var content = logs.substring(Math.max(logs.length() - 1950, 0)); try(InputStream inputStream = new ByteArrayInputStream(logs.getBytes(StandardCharsets.UTF_8))) { event.reply("```log%n%s%n```".formatted(content)) diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/ServerAdmin.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/ServerAdmin.java index 6a58922..a633f9a 100644 --- a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/ServerAdmin.java +++ b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/ServerAdmin.java @@ -9,8 +9,6 @@ import de.chojo.gamejam.commands.serveradmin.handler.SyncVelocity; import de.chojo.gamejam.commands.serveradmin.handler.info.Detailed; import de.chojo.gamejam.commands.serveradmin.handler.info.Short; -import de.chojo.gamejam.commands.serveradmin.handler.refresh.RefreshAll; -import de.chojo.gamejam.commands.serveradmin.handler.refresh.RefreshTeam; import de.chojo.gamejam.commands.serveradmin.handler.restart.RestartAll; import de.chojo.gamejam.commands.serveradmin.handler.restart.RestartTeam; import de.chojo.gamejam.commands.serveradmin.handler.start.StartAll; @@ -47,12 +45,6 @@ public ServerAdmin(Guilds guilds, ServerService serverService) { .subCommand(SubCommand.of("team", "command.serveradmin.stop.team.description") .handler(new StopTeam(serverService, guilds)) .argument(Argument.text("team", "command.serveradmin.stop.team.options.team.description").asRequired().withAutoComplete()))) - .group(Group.of("refresh", "command.serveradmin.refresh.description") - .subCommand(SubCommand.of("all", "command.serveradmin.refresh.all.description") - .handler(new RefreshAll(serverService, guilds))) - .subCommand(SubCommand.of("team", "command.serveradmin.refresh.team.description") - .handler(new RefreshTeam(serverService, guilds)) - .argument(Argument.text("team", "command.serveradmin.refresh.team.options.team.description").asRequired().withAutoComplete()))) .group(Group.of("info", "command.serveradmin.info.description") .subCommand(SubCommand.of("short", "command.serveradmin.info.short.description") .handler(new Short(serverService, guilds))) diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshAll.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshAll.java deleted file mode 100644 index 01078f7..0000000 --- a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshAll.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-License-Identifier: AGPL-3.0-only - * - * Copyright (C) 2022 DevCord Team and Contributor - */ - -package de.chojo.gamejam.commands.serveradmin.handler.refresh; - -import de.chojo.gamejam.data.access.Guilds; -import de.chojo.gamejam.server.ServerService; -import de.chojo.gamejam.server.TeamServer; -import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; -import de.chojo.jdautil.localization.util.Replacement; -import de.chojo.jdautil.wrapper.EventContext; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; - -public class RefreshAll implements SlashHandler { - private final ServerService serverService; - private final Guilds guilds; - - public RefreshAll(ServerService serverService, Guilds guilds) { - this.serverService = serverService; - this.guilds = guilds; - } - - @Override - public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { - var currentJam = guilds.guild(event).jams().getCurrentJam(); - if (currentJam.isEmpty()) { - event.reply(context.localize("error.noactivejam")).queue(); - return; - } - var jam = currentJam.get(); - - var count = jam.teams().teams().stream() - .map(serverService::get) - .map(TeamServer::refresh) - .filter(v -> v) - .count(); - event.reply(context.localize("command.serveradmin.refresh.refreshall.message.refreshed", - Replacement.create("AMOUNT", count))).queue(); - } -} diff --git a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshTeam.java b/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshTeam.java deleted file mode 100644 index 520b5a2..0000000 --- a/bot/src/main/java/de/chojo/gamejam/commands/serveradmin/handler/refresh/RefreshTeam.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-License-Identifier: AGPL-3.0-only - * - * Copyright (C) 2022 DevCord Team and Contributor - */ - -package de.chojo.gamejam.commands.serveradmin.handler.refresh; - -import de.chojo.gamejam.data.access.Guilds; -import de.chojo.gamejam.server.ServerService; -import de.chojo.gamejam.server.TeamServer; -import de.chojo.jdautil.interactions.slash.structure.handler.SlashHandler; -import de.chojo.jdautil.localization.util.Replacement; -import de.chojo.jdautil.wrapper.EventContext; -import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; -import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; - -import java.util.Collections; - -public class RefreshTeam implements SlashHandler { - private final ServerService serverService; - private final Guilds guilds; - - public RefreshTeam(ServerService serverService, Guilds guilds) { - this.serverService = serverService; - this.guilds = guilds; - } - - @Override - public void onSlashCommand(SlashCommandInteractionEvent event, EventContext context) { - var currentJam = guilds.guild(event).jams().getCurrentJam(); - if (currentJam.isEmpty()) { - event.reply(context.localize("error.noactivejam")).queue(); - return; - } - var jam = currentJam.get(); - - var optTeam = jam.teams().byName(event.getOption("team").getAsString()); - - if (optTeam.isEmpty()) { - event.reply(context.localize("error.unkownteam")).queue(); - return; - } - - var started = optTeam.map(serverService::get) - .map(TeamServer::refresh) - .orElse(false); - if (started) { - event.reply(context.localize("command.serveradmin.refresh.refreshteam.message.refreshed", - Replacement.create("TEAM", optTeam.get()))).queue(); - } else { - event.reply(context.localize("command.serveradmin.refresh.refreshteam.message.failed", - Replacement.create("TEAM", optTeam.get()))).queue(); - } - } - - @Override - public void onAutoComplete(CommandAutoCompleteInteractionEvent event, EventContext context) { - var guild = guilds.guild(event); - var option = event.getFocusedOption(); - if ("team".equals(option.getName())) { - var choices = guild.jams().nextOrCurrent() - .map(jam -> jam.teams().completeTeam(option.getValue())) - .orElse(Collections.emptyList()); - event.replyChoices(choices).queue(); - } - } -} diff --git a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java index 3471c18..99d8542 100644 --- a/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java +++ b/bot/src/main/java/de/chojo/gamejam/configuration/elements/Docker.java @@ -10,12 +10,13 @@ public class Docker { private String host = "unix:///var/run/docker.sock"; private String certPath = "/home/user/.docker"; - private boolean tlsVerify = true; + private boolean tlsVerify = false; private String registryUsername; private String registryPassword; private String registryEmail; private String registryUrl; private String networkName = "plugin-jam-network"; + private String teamServerImage = "plugin-jam-mc-server:latest"; public String getHost() { return host; @@ -29,24 +30,39 @@ public boolean isTlsVerify() { return tlsVerify; } - public String getRegistryUsername() { + public String host() { + return host; + } + + public String certPath() { + return certPath; + } + + public boolean tlsVerify() { + return tlsVerify; + } + + public String registryUsername() { return registryUsername; } - public String getRegistryPassword() { + public String registryPassword() { return registryPassword; } - public String getRegistryEmail() { + public String registryEmail() { return registryEmail; } - public String getRegistryUrl() { + public String registryUrl() { return registryUrl; } - public String getNetworkName() { + public String networkName() { return networkName; } + public String teamServerImage() { + return teamServerImage; + } } \ No newline at end of file diff --git a/bot/src/main/java/de/chojo/gamejam/message/EmbedHelper.java b/bot/src/main/java/de/chojo/gamejam/message/EmbedHelper.java new file mode 100644 index 0000000..378512e --- /dev/null +++ b/bot/src/main/java/de/chojo/gamejam/message/EmbedHelper.java @@ -0,0 +1,47 @@ +/* + * SPDX-License-Identifier: AGPL-3.0-only + * + * Copyright (C) 2022 DevCord Team and Contributor + */ + +package de.chojo.gamejam.message; + +import de.chojo.gamejam.server.TeamServer; +import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; +import de.chojo.jdautil.wrapper.EventContext; +import net.dv8tion.jda.api.entities.MessageEmbed; + +import java.util.concurrent.CompletableFuture; + +public class EmbedHelper { + + public static CompletableFuture embedDetailedStatus(TeamServer teamServer, EventContext context) { + return CompletableFuture.supplyAsync(() -> { + + var builder = new LocalizedEmbedBuilder(context.guildLocalizer()) + .setTitle("%s #%d | %s".formatted(teamServer.statusEmoji(), teamServer.team().id(), teamServer.team().meta().name())); + if (!teamServer.exists()) { + builder.setDescription("teamserver.message.detailstatus.nonexisting.description"); + } else { + if (teamServer.isRunning()) { + builder.setDescription("teamserver.message.detailstatus.existing.description"); + + var serverStats = teamServer.stats(); + serverStats.ifPresent(stats -> { + var memory = stats.memory(); + builder.addField("word.memory", "$word.used$ %d%n$word.total$: %d%n$word.max$: %d".formatted(memory.usedMb(), memory.totalMb(), memory.maxMb()), true) + .addField("word.tps", "1 $word.min$: %.2f%n5 $word.min$: %.2f%n 15 $word.min$: %.2f%n$word.averageticktime$ %.2f".formatted( + stats.tps()[0], stats.tps()[1], stats.tps()[2], stats.averageTickTime()), true) + .addField("word.players", String.valueOf(stats.onlinePlayers()), true) + .addField("word.system", "$word.activethreads$: %d".formatted(stats.activeThreads()), true); + }); + } else { + builder.setDescription("word.serversetup") + .addField("word.ports", "word.notrunning", true); + } + } + + return builder.build(); + }); + } +} diff --git a/bot/src/main/java/de/chojo/gamejam/server/DockerService.java b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java index 312b3e1..38f2206 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/DockerService.java +++ b/bot/src/main/java/de/chojo/gamejam/server/DockerService.java @@ -27,26 +27,25 @@ import static org.slf4j.LoggerFactory.getLogger; public class DockerService { - private DockerClientConfig dockerClientConfig; + private final Docker dockerConfig; + private final DockerClientConfig dockerClientConfig; private DockerClient dockerClient; private static final Logger log = getLogger(DockerService.class); - private static final String DOCKER_IMAGE = "itzg/minecraft-server:latest"; private static final String DOCKER_VOLUME_DATA_DIR = "/data"; - private final String networkName; private final String pluginUrls; public DockerService(Docker dockerConfig, Plugins pluginsConfig) { - this.networkName = dockerConfig.getNetworkName(); + this.dockerConfig = dockerConfig; this.pluginUrls = String.join(",", pluginsConfig.defaultPlugins()); this.dockerClientConfig = DefaultDockerClientConfig .createDefaultConfigBuilder() .withDockerHost(dockerConfig.getHost()) .withDockerCertPath(dockerConfig.getCertPath()) .withDockerTlsVerify(dockerConfig.isTlsVerify()) - .withRegistryUsername(dockerConfig.getRegistryUsername()) - .withRegistryPassword(dockerConfig.getRegistryPassword()) - .withRegistryEmail(dockerConfig.getRegistryEmail()) - .withRegistryUrl(dockerConfig.getRegistryUrl()) + .withRegistryUsername(dockerConfig.registryUsername()) + .withRegistryPassword(dockerConfig.registryPassword()) + .withRegistryEmail(dockerConfig.registryEmail()) + .withRegistryUrl(dockerConfig.registryUrl()) .build(); } @@ -65,14 +64,14 @@ public void initDockerClient() { private void ensureNetwork() { var networks = dockerClient.listNetworksCmd() - .withNameFilter(networkName) + .withNameFilter(dockerConfig.networkName()) .exec(); - if (networks.stream().noneMatch(n -> n.getName().equals(networkName))) { + if (networks.stream().noneMatch(n -> n.getName().equals(dockerConfig.networkName()))) { dockerClient.createNetworkCmd() - .withName(networkName) + .withName(dockerConfig.networkName()) .withDriver("bridge") .exec(); - log.info("Created docker network {}", networkName); + log.info("Created docker network {}", dockerConfig.networkName()); } } @@ -89,9 +88,9 @@ public void provisionServer(int teamId) { HostConfig hostConfig = HostConfig.newHostConfig() .withBinds(new Bind(volumeName(teamId), new Volume(DOCKER_VOLUME_DATA_DIR))); - hostConfig.withNetworkMode(networkName); + hostConfig.withNetworkMode(dockerConfig.networkName()); - dockerClient.createContainerCmd(DOCKER_IMAGE) + dockerClient.createContainerCmd(dockerConfig.teamServerImage()) .withName(containerName(teamId)) .withEnv("EULA=TRUE", "TYPE=PAPER", "VERSION=26.1.2", String.format("PLUGINS=%s", pluginUrls)) .withHostConfig(hostConfig) @@ -173,7 +172,7 @@ public Optional container(int teamId) { .findFirst(); } - public String logs(int teamId, int tail) { + public String logs(int teamId) { var callback = new ResultCallback.Adapter() { private final StringBuilder logs = new StringBuilder(); @@ -191,7 +190,6 @@ public String getLogs() { dockerClient.logContainerCmd(containerName(teamId)) .withStdOut(true) .withStdErr(true) - .withTail(tail) .exec(callback) .awaitCompletion(); } catch (InterruptedException e) { diff --git a/bot/src/main/java/de/chojo/gamejam/server/ServerService.java b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java index dd7d73d..6101e21 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/ServerService.java +++ b/bot/src/main/java/de/chojo/gamejam/server/ServerService.java @@ -22,7 +22,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Stack; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -30,24 +29,19 @@ import static org.slf4j.LoggerFactory.getLogger; -public class ServerService implements Runnable { +public class ServerService { private static final Logger log = getLogger(ServerService.class); private final Map server = new HashMap<>(); private Teams teams; private final Configuration configuration; - private final Stack freePorts = new Stack<>(); private final DockerService dockerService; - public static ServerService create(ScheduledExecutorService executorService, Configuration configuration) { - var serverService = new ServerService(configuration); - executorService.scheduleAtFixedRate(serverService, 10, 10, TimeUnit.SECONDS); - return serverService; + public static ServerService create(Configuration configuration) { + return new ServerService(configuration); } private ServerService(Configuration configuration) { this.configuration = configuration; - IntStream.rangeClosed(configuration.serverManagement().minPort(), configuration.serverManagement().maxPort()) - .forEach(freePorts::add); this.dockerService = new DockerService(configuration.docker(), configuration.plugins()); this.dockerService.initDockerClient(); } @@ -58,30 +52,10 @@ public void shutdown() { }); } - @Override - public void run() { - for (var value : server.values()) { - if (!value.isRunning()) continue; - try { - value.serverRequests() - .ifPresent(server -> { - if (server.restart()) { - log.info("Server of team {} requested restart", value.team()); - value.restart(); - } - }); - } catch (RuntimeException e) { - log.error("Could not reach server {}", value); - } - } - } - public CompletableFuture syncVelocity() { return CompletableFuture.supplyAsync(() -> { log.info("Syncing server with velocity instance."); - freePorts.clear(); - IntStream.rangeClosed(configuration.serverManagement().minPort(), configuration.serverManagement().maxPort()) - .forEach(freePorts::add); + var velocityPort = configuration.serverManagement().velocityPort(); var velocityHost = configuration.serverManagement().getVelocityHost(); var httpClient = HttpClient.newHttpClient(); @@ -126,34 +100,15 @@ public CompletableFuture syncVelocity() { } var team = optTeam.get(); log.info("Registered server for team {} with id {}", team.meta().name(), team.id()); - var teamServer = new TeamServer(this, dockerService, team, configuration, registration.port(), registration.apiPort()); - //teamServer.running(true); + var teamServer = new TeamServer(dockerService, team, configuration); server.put(team, teamServer); - freePorts.removeElement(registration.apiPort()); - freePorts.removeElement(registration.port()); } return true; }); } public TeamServer get(Team team) { - return server.computeIfAbsent(team, key -> new TeamServer(this, dockerService, key, configuration, nextPort(), nextPort())); - } - - private int nextPort() { - if (!freePorts.isEmpty()) { - return freePorts.pop(); - } - throw new RuntimeException("Ports exhausted"); - } - - void stopped(TeamServer server, boolean restart) { - this.server.remove(server.team()); - freePorts.push(server.port()); - freePorts.push(server.apiPort()); - if (restart) { - get(server.team()).start(); - } + return server.computeIfAbsent(team, key -> new TeamServer(dockerService, key, configuration)); } public void inject(Teams teams) { diff --git a/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java index a11defd..cef7626 100644 --- a/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java +++ b/bot/src/main/java/de/chojo/gamejam/server/TeamServer.java @@ -9,6 +9,8 @@ import com.fasterxml.jackson.core.JsonProcessingException; import de.chojo.gamejam.configuration.Configuration; import de.chojo.gamejam.data.dao.guild.jams.jam.teams.Team; +import de.chojo.gamejam.data.dao.guild.jams.jam.user.JamUser; +import de.chojo.gamejam.message.EmbedHelper; import de.chojo.gamejam.util.Mapper; import de.chojo.jdautil.localization.util.LocalizedEmbedBuilder; import de.chojo.jdautil.wrapper.EventContext; @@ -41,20 +43,14 @@ public class TeamServer { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss.SSSS"); private static final Logger log = getLogger(TeamServer.class); private static final int DEFAULT_API_PORT = 30000; - private final ServerService serverService; private final DockerService dockerService; private final Team team; private final Configuration configuration; - private final int port; - private final int apiPort; - public TeamServer(ServerService serverService, DockerService dockerService, Team team, Configuration configuration, int port, int apiPort) { - this.serverService = serverService; + public TeamServer(DockerService dockerService, Team team, Configuration configuration) { this.dockerService = dockerService; this.team = team; this.configuration = configuration; - this.port = port; - this.apiPort = apiPort; } public boolean isRunning() { @@ -75,60 +71,9 @@ public boolean setup() throws IOException { if (exists()) return false; dockerService.provisionServer(team.id()); log.info("Setting up server of team {}", team); - writeTemplate(); return true; } - /** - * Refresh the files of the server present in the template. This is basically a new setup without purging the data beforehand. - *

- * Files with the same name will be overridden - * - * @return true when the refresh was successful - */ - public boolean refresh() { - log.info("Refreshing template files of server {}", team); - try { - writeTemplate(); - } catch (IOException e) { - log.error("Could not refresh template", e); - return false; - } - return true; - } - - private void writeTemplate() throws IOException { - var serverDir = serverDir(); - Files.createDirectories(serverDir); - - var sourceDir = Path.of(configuration.serverTemplate().templateDir()); - var symlinks = configuration.serverTemplate().symLinks() - .stream() - .map(sourceDir::resolve) - .collect(Collectors.toSet()); - try (var files = Files.walk(sourceDir)) { - for (var sourceTarget : files.toList()) { - // skip root dir - if (sourceTarget.getNameCount() == 1) continue; - var filePath = sourceTarget.subpath(1, sourceTarget.getNameCount()); - var serverTarget = serverDir.resolve(filePath); - if (symlinks.contains(sourceTarget)) { - // Not really required since the current and new symlink are probably equal, but the creation will fail otherwise. - if (serverTarget.toFile().isFile() && serverTarget.toFile().delete()) { - log.debug("Deleted old version of file {}", serverTarget); - } - Files.createSymbolicLink(serverTarget, sourceTarget.toAbsolutePath()); - } else { - // ignore already existing directories - if (sourceTarget.toFile().isDirectory() && serverTarget.toFile().exists()) { - continue; - } - Files.copy(sourceTarget, serverTarget, StandardCopyOption.REPLACE_EXISTING); - } - } - } - } - /** * Delete all the server data. * @@ -196,26 +141,16 @@ public Team team() { return team; } - public int port() { - return port; - } - - public int apiPort() { - return apiPort; - } - @Override public String toString() { return "TeamServer{" + "team=" + team + - ", port=" + port + - ", apiPort=" + apiPort + '}'; } - public String logs(int tail) { + public String logs() { - return dockerService.logs(team.id(), tail); + return dockerService.logs(team.id()); } public boolean replaceWorld(Path newWorld) { @@ -259,41 +194,9 @@ public Path world() { return plugins; } - public CompletableFuture detailStatus(EventContext context) { - return CompletableFuture.supplyAsync(() -> { - - var builder = new LocalizedEmbedBuilder(context.guildLocalizer()) - .setTitle("%s #%d | %s".formatted(statusEmoji(), team.id(), team.meta().name())); - if (!exists()) { - builder.setDescription("teamserver.message.detailstatus.nonexisting.description"); - } else { - if (isRunning()) { - builder.setDescription("teamserver.message.detailstatus.existing.description") - .addField("word.ports", "$word.server$: %d%n$word.api$: %d".formatted(port, apiPort), true); - stats().ifPresent(stats -> { - var memory = stats.memory(); - builder.addField("word.memory", "$word.used$ %d%n$word.total$: %d%n$word.max$: %d".formatted(memory.usedMb(), memory.totalMb(), memory.maxMb()), true) - .addField("word.tps", "1 $word.min$: %.2f%n5 $word.min$: %.2f%n 15 $word.min$: %.2f%n$word.averageticktime$ %.2f".formatted( - stats.tps()[0], stats.tps()[1], stats.tps()[2], stats.averageTickTime()), true) - .addField("word.players", String.valueOf(stats.onlinePlayers()), true) - .addField("word.system", "$word.activethreads$: %d".formatted(stats.activeThreads()), true); - }); - } else { - builder.setDescription("word.serversetup") - .addField("word.ports", "word.notrunning", true); - } - } - - return builder.build(); - }); - } - public String status() { var status = statusEmoji(); var ports = ""; - if (exists() && isRunning()) { - ports = "Server: %s Api: %s".formatted(port, apiPort); - } return "%s %s %s".formatted(status, team, ports); } @@ -335,7 +238,7 @@ public HttpClient http() { return http; } - private String statusEmoji() { + public String statusEmoji() { if (exists() && isRunning()) return "🟢"; if (exists()) return "🟡"; return "🔴"; @@ -360,4 +263,8 @@ public Optional serverRequests() { throw new RuntimeException(e); } } + + public CompletableFuture detailStatus(EventContext context) { + return EmbedHelper.embedDetailedStatus(this, context); + } } diff --git a/dev.compose.yml b/compose.dev.yml similarity index 67% rename from dev.compose.yml rename to compose.dev.yml index 18f4b6f..a2666a8 100644 --- a/dev.compose.yml +++ b/compose.dev.yml @@ -1,10 +1,19 @@ services: + mc-server-image: + build: + context: . + dockerfile: Dockerfile.mc + image: plugin-jam-mc-server + entrypoint: ["echo", "image built"] bot: build: context: . - dockerfile: dev.Dockerfile + dockerfile: Dockerfile.bot.dev + depends_on: + mc-server-image: + condition: service_completed_successfully volumes: - - ./data/bot/config/config.yml:/app/config/config.yml + - ./data/bot/config:/app/config - ./data/bot/plugins:/app/plugins - ./data/bot/servers:/app/servers - ./data/bot/template:/app/template @@ -21,8 +30,12 @@ services: - "127.0.0.1:5433:5432" networks: - plugin-jam-network + volumes: + - ./data/db:/var/lib/postgresql/data velocity: - image: itzg/mc-proxy:latest + build: + context: . + dockerfile: Dockerfile.velocity tty: true environment: EULA: "TRUE" @@ -30,7 +43,6 @@ services: JVM_DD_OPTS: javalin.port=30000 volumes: - ./data/velocity/config:/config - - ./data/velocity/plugins:/plugins networks: - plugin-jam-network lobby: @@ -38,8 +50,9 @@ services: stdin_open: true tty: true environment: - VERSION: 1.21 + VERSION: 26.1.2 TYPE: PAPER + EULA: "TRUE" MEMORY: 2G volumes: - ./data/lobby:/data diff --git a/compose.yml b/compose.yml index 4188cbf..9337343 100644 --- a/compose.yml +++ b/compose.yml @@ -1,18 +1,27 @@ services: + mc-server-image: + build: + context: . + dockerfile: Dockerfile.mc + image: plugin-jam-mc-server + entrypoint: ["echo", "image built"] bot: image: ghcr.io/devcordde/plugin-jam-bot:latest + depends_on: + mc-server-image: + condition: service_completed_successfully volumes: - ./data/bot/config:/app/config - ./data/bot/plugins:/app/plugins - ./data/bot/servers:/app/servers - ./data/bot/template:/app/template - /var/run/docker.sock:/var/run/docker.sock - depends_on: - - networks: - plugin-jam-network velocity: - image: itzg/mc-proxy:latest + build: + context: . + dockerfile: Dockerfile.velocity tty: true environment: EULA: "TRUE" @@ -20,7 +29,6 @@ services: JVM_DD_OPTS: javalin.port=30000 volumes: - ./data/velocity/config:/config - - ./data/velocity/plugins:/plugins networks: - plugin-jam-network lobby: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 5d35830..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,28 +0,0 @@ -version: v3 - -services: - bot: - image: game-jam-bot - build: - dockerfile: ../bot/Dockerfile - context: .. - networks: - - plugin-jam - depends_on: - - database - ports: - - 8888:8888 - database: - image: postgres:18.4 - restart: always - user: postgres - environment: - POSTGRES_PASSWORD: "changeme" - POSTGRES_USER: "postgres" - POSTGRES_DB: "db" - networks: - - plugin-jam -networks: - plugin-jam: - name: "plugin-jam" - external: false diff --git a/docker/resources/docker-entrypoint.sh b/docker/resources/docker-entrypoint.sh index f08937b..ef4b3cd 100755 --- a/docker/resources/docker-entrypoint.sh +++ b/docker/resources/docker-entrypoint.sh @@ -1,2 +1,2 @@ #!/bin/sh -exec java -Xms256m -Xmx2048m -Dbot.config=config/config.json -Dlog4j.configurationFile=config/log4j2.xml -Dcjda.localisation.error.name=false -jar ./bot.jar +exec java -Xms256m -Xmx2048m -Dlog4j.configurationFile=log4j2.xml -Dcjda.localisation.error.name=false -jar ./bot.jar \ No newline at end of file