diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..8caa093f --- /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 fc0d17c9..35e9a228 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 654328b0..88a45422 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 65ab4956..1cb918d3 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 eb299c9c..a47ed142 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 ab52e432..cf59197a 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 00000000..c7c5958a --- /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 00000000..bad536d5 --- /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 81a7b82f..794e8871 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 2f34332e..44c89680 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 f05b7d95..aa2c29e1 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 00000000..026dbe69 --- /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 d9aa4790..e1de3f2e 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 c4470c07..92977b96 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")