Skip to content

Commit 370c983

Browse files
committed
Initial work for running the game servers in docker
1 parent d67d32f commit 370c983

14 files changed

Lines changed: 316 additions & 195 deletions

File tree

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.gradle
2+
**/build
3+
.git
4+
.idea

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# 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.
2-
FROM gradle:jdk21-alpine as build
2+
FROM gradle:jdk25-alpine as build
33

44
COPY . .
55
RUN gradle clean :bot:build :plugin-paper:build --no-daemon

bot/build.gradle.kts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,26 @@ dependencies {
2020
implementation("de.chojo", "cjda-util", "2.14.5+jda-6.3.0") {
2121
exclude(module = "opus-java")
2222
}
23+
24+
implementation("net.dv8tion", "JDA", "6.4.1")
25+
2326
implementation(libs.javalin.bundle)
2427
implementation(libs.javalin.openapi)
2528
implementation(libs.javalin.swagger)
2629
annotationProcessor(libs.javalin.annotation)
2730
implementation("net.lingala.zip4j", "zip4j", "2.11.6")
2831

32+
// config
33+
implementation("dev.chojo", "ocular", "2.2.1")
34+
implementation("tools.jackson.dataformat:jackson-dataformat-yaml:3.1.4")
35+
2936
// database
3037
implementation(libs.bundles.sadu)
3138
implementation("org.postgresql", "postgresql", "42.7.11")
3239

40+
// docker api
41+
implementation(libs.bundles.docker)
42+
3343
// Logging
3444
implementation(libs.bundles.logging)
3545
implementation("club.minnced", "discord-webhooks", "0.8.4")

bot/src/main/java/de/chojo/gamejam/Bot.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ private void buildLocale() {
155155
.addLanguage(DiscordLocale.GERMAN)
156156
.withLanguageProvider(guild -> Optional.ofNullable(guilds.guild(guild).settings().locale())
157157
.map(DiscordLocale::from))
158+
.withGuildLocaleCodeProvider((guild, s) -> Optional.empty())
158159
.build();
159160
}
160161

bot/src/main/java/de/chojo/gamejam/configuration/ConfigFile.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@
66

77
package de.chojo.gamejam.configuration;
88

9-
import de.chojo.gamejam.configuration.elements.Api;
10-
import de.chojo.gamejam.configuration.elements.BaseSettings;
11-
import de.chojo.gamejam.configuration.elements.Database;
12-
import de.chojo.gamejam.configuration.elements.Plugins;
13-
import de.chojo.gamejam.configuration.elements.ServerManagement;
14-
import de.chojo.gamejam.configuration.elements.ServerTemplate;
9+
import de.chojo.gamejam.configuration.elements.*;
1510

1611
@SuppressWarnings("FieldMayBeFinal")
1712
public class ConfigFile {
1813
private BaseSettings baseSettings = new BaseSettings();
1914
private Database database = new Database();
2015
private Api api = new Api();
2116
private ServerManagement serverManagement = new ServerManagement();
17+
18+
private Docker docker = new Docker();
2219
private Plugins plugins = new Plugins();
20+
21+
public void setServerTemplate(ServerTemplate serverTemplate) {
22+
this.serverTemplate = serverTemplate;
23+
}
24+
2325
private ServerTemplate serverTemplate = new ServerTemplate();
2426

2527
public BaseSettings baseSettings() {
@@ -38,6 +40,10 @@ public ServerManagement serverManagement(){
3840
return serverManagement;
3941
}
4042

43+
public Docker docker() {
44+
return docker;
45+
}
46+
4147
public Plugins plugins() {
4248
return plugins;
4349
}

bot/src/main/java/de/chojo/gamejam/configuration/Configuration.java

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@
66

77
package de.chojo.gamejam.configuration;
88

9-
import de.chojo.gamejam.configuration.elements.Api;
10-
import de.chojo.gamejam.configuration.elements.BaseSettings;
11-
import de.chojo.gamejam.configuration.elements.Database;
12-
import de.chojo.gamejam.configuration.elements.Plugins;
13-
import de.chojo.gamejam.configuration.elements.ServerManagement;
14-
import de.chojo.gamejam.configuration.elements.ServerTemplate;
15-
import de.chojo.jdautil.configuration.BaseConfiguration;
16-
17-
public class Configuration extends BaseConfiguration<ConfigFile> {
9+
import de.chojo.gamejam.configuration.elements.*;
10+
import dev.chojo.ocular.Configurations;
11+
import dev.chojo.ocular.dataformats.YamlDataFormat;
12+
import dev.chojo.ocular.key.Key;
13+
14+
import java.nio.file.Path;
15+
import java.util.List;
16+
17+
public class Configuration extends Configurations<ConfigFile> {
18+
public static final Key<ConfigFile> MAIN = Key.builder(Path.of("config.yml"), ConfigFile::new).build();
19+
1820
private Configuration() {
19-
super(new ConfigFile());
21+
super(Path.of("config"), MAIN, List.of(new YamlDataFormat()), Configuration.class.getClassLoader(), null);
2022
}
2123

2224
public static Configuration create() {
@@ -27,26 +29,30 @@ public static Configuration create() {
2729

2830

2931
public Database database() {
30-
return config().database();
32+
return main().database();
33+
}
34+
35+
public Docker docker() {
36+
return main().docker();
3137
}
3238

3339
public BaseSettings baseSettings() {
34-
return config().baseSettings();
40+
return main().baseSettings();
3541
}
3642

3743
public Api api() {
38-
return config().api();
44+
return main().api();
3945
}
4046

4147
public ServerManagement serverManagement() {
42-
return config().serverManagement();
48+
return main().serverManagement();
4349
}
4450

4551
public Plugins plugins() {
46-
return config().plugins();
52+
return main().plugins();
4753
}
4854

4955
public ServerTemplate serverTemplate() {
50-
return config().serverTemplate();
56+
return main().serverTemplate();
5157
}
5258
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* SPDX-License-Identifier: AGPL-3.0-only
3+
*
4+
* Copyright (C) 2022 DevCord Team and Contributor
5+
*/
6+
7+
package de.chojo.gamejam.configuration.elements;
8+
9+
10+
@SuppressWarnings({"FieldMayBeFinal", "FieldCanBeLocal"})
11+
public class Docker {
12+
private String host = "unix:///var/run/docker.sock";
13+
private String certPath = "/home/user/.docker";
14+
private boolean tlsVerify = true;
15+
private String registryUsername;
16+
private String registryPassword;
17+
private String registryEmail;
18+
private String registryUrl;
19+
20+
public String getHost() {
21+
return host;
22+
}
23+
24+
public String getCertPath() {
25+
return certPath;
26+
}
27+
28+
public boolean isTlsVerify() {
29+
return tlsVerify;
30+
}
31+
32+
public String getRegistryUsername() {
33+
return registryUsername;
34+
}
35+
36+
public String getRegistryPassword() {
37+
return registryPassword;
38+
}
39+
40+
public String getRegistryEmail() {
41+
return registryEmail;
42+
}
43+
44+
public String getRegistryUrl() {
45+
return registryUrl;
46+
}
47+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* SPDX-License-Identifier: AGPL-3.0-only
3+
*
4+
* Copyright (C) 2022 DevCord Team and Contributor
5+
*/
6+
7+
package de.chojo.gamejam.server;
8+
9+
import com.github.dockerjava.api.DockerClient;
10+
import com.github.dockerjava.api.model.*;
11+
import com.github.dockerjava.core.DefaultDockerClientConfig;
12+
import com.github.dockerjava.core.DockerClientConfig;
13+
import com.github.dockerjava.core.DockerClientImpl;
14+
import com.github.dockerjava.httpclient5.ApacheDockerHttpClient;
15+
import com.github.dockerjava.transport.DockerHttpClient;
16+
import de.chojo.gamejam.configuration.elements.Docker;
17+
import org.slf4j.Logger;
18+
19+
import java.io.IOException;
20+
import java.nio.file.Path;
21+
import java.time.Duration;
22+
import java.util.List;
23+
import java.util.Optional;
24+
25+
import static org.slf4j.LoggerFactory.getLogger;
26+
27+
public class DockerService {
28+
private DockerClientConfig dockerClientConfig;
29+
private DockerClient dockerClient;
30+
private static final Logger log = getLogger(DockerService.class);
31+
private static final String DOCKER_IMAGE = "itzg/minecraft-server:latest";
32+
private static final String DOCKER_VOLUME_DATA_DIR = "/data";
33+
34+
public DockerService(Docker dockerConfig) {
35+
this.dockerClientConfig = DefaultDockerClientConfig
36+
.createDefaultConfigBuilder()
37+
.withDockerHost(dockerConfig.getHost())
38+
.withDockerCertPath(dockerConfig.getCertPath())
39+
.withDockerTlsVerify(dockerConfig.isTlsVerify())
40+
.withRegistryUsername(dockerConfig.getRegistryUsername())
41+
.withRegistryPassword(dockerConfig.getRegistryPassword())
42+
.withRegistryEmail(dockerConfig.getRegistryEmail())
43+
.withRegistryUrl(dockerConfig.getRegistryUrl())
44+
.build();
45+
}
46+
47+
public void initDockerClient() {
48+
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
49+
.dockerHost(dockerClientConfig.getDockerHost())
50+
.sslConfig(dockerClientConfig.getSSLConfig())
51+
.maxConnections(100)
52+
.connectionTimeout(Duration.ofSeconds(30))
53+
.responseTimeout(Duration.ofSeconds(45))
54+
.build();
55+
56+
dockerClient = DockerClientImpl.getInstance(dockerClientConfig, httpClient);
57+
}
58+
59+
public void shutdown() throws IOException {
60+
dockerClient.close();
61+
}
62+
63+
public void provisionServer(int teamId) {
64+
log.info("Provisioning server for team {}", teamId);
65+
dockerClient.createVolumeCmd()
66+
.withName(volumeName(teamId))
67+
.exec();
68+
69+
HostConfig hostConfig = HostConfig.newHostConfig()
70+
.withBinds(new Bind(volumeName(teamId), new Volume(DOCKER_VOLUME_DATA_DIR)));
71+
72+
dockerClient.createContainerCmd(DOCKER_IMAGE)
73+
.withName(containerName(teamId))
74+
.withHostConfig(hostConfig)
75+
.exec();
76+
log.info("Server provisioned for team with container name {} and volume name {}", containerName(teamId), volumeName(teamId));
77+
}
78+
79+
public void destroyServer(int teamId) {
80+
log.info("Destroying server for team {}", teamId);
81+
dockerClient.removeContainerCmd(containerName(teamId)).exec();
82+
dockerClient.removeVolumeCmd(volumeName(teamId)).exec();
83+
}
84+
85+
public void startServer(int teamId) {
86+
log.info("Starting server for team {}", teamId);
87+
dockerClient.startContainerCmd(containerName(teamId)).exec();
88+
}
89+
90+
public void stopServer(int teamId) {
91+
log.info("Stopping server for team {}", teamId);
92+
dockerClient.stopContainerCmd(containerName(teamId)).exec();
93+
}
94+
95+
public void restartServer(int teamId) {
96+
dockerClient.restartContainerCmd(containerName(teamId)).exec();
97+
}
98+
99+
public boolean running(int teamId) {
100+
return dockerClient.listContainersCmd()
101+
.withShowAll(true)
102+
.withNameFilter(List.of(containerName(teamId)))
103+
.exec()
104+
.stream()
105+
.anyMatch(container -> container.getState().equals("running"));
106+
}
107+
108+
public void sendCommand(int teamId, String command) {
109+
dockerClient.execCreateCmd(containerName(teamId))
110+
.withCmd(String.format("mc rcon-cli %s", command))
111+
.exec();
112+
}
113+
114+
public boolean exists(int teamId) {
115+
return dockerClient.listContainersCmd()
116+
.withShowAll(true)
117+
.exec()
118+
.stream()
119+
.anyMatch(container -> container.getId().startsWith(containerName(teamId)));
120+
}
121+
122+
public Optional<Container> container(int teamId) {
123+
return dockerClient.listContainersCmd()
124+
.withShowAll(true)
125+
.withNameFilter(List.of(containerName(teamId)))
126+
.exec()
127+
.stream()
128+
.findFirst();
129+
}
130+
131+
public void copyArchiveToContainer(int teamId, Path source, Path destination) {
132+
dockerClient.copyArchiveToContainerCmd(containerName(teamId))
133+
.withHostResource(source.toString())
134+
.withRemotePath(destination.toString())
135+
.exec();
136+
}
137+
138+
private String volumeName(int teamId) {
139+
return "plugin-jam-team-" + teamId;
140+
}
141+
142+
public String containerName(int teamId) {
143+
return "plugin-jam-team-" + teamId;
144+
}
145+
}

bot/src/main/java/de/chojo/gamejam/server/ServerService.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public class ServerService implements Runnable {
3636
private Teams teams;
3737
private final Configuration configuration;
3838
private final Stack<Integer> freePorts = new Stack<>();
39+
private final DockerService dockerService;
3940

4041
public static ServerService create(ScheduledExecutorService executorService, Configuration configuration) {
4142
var serverService = new ServerService(configuration);
@@ -47,6 +48,8 @@ private ServerService(Configuration configuration) {
4748
this.configuration = configuration;
4849
IntStream.rangeClosed(configuration.serverManagement().minPort(), configuration.serverManagement().maxPort())
4950
.forEach(freePorts::add);
51+
this.dockerService = new DockerService(configuration.docker());
52+
this.dockerService.initDockerClient();
5053
}
5154

5255
public void shutdown() {
@@ -123,7 +126,7 @@ public CompletableFuture<Boolean> syncVelocity() {
123126
}
124127
var team = optTeam.get();
125128
log.info("Registered server for team {} with id {}", team.meta().name(), team.id());
126-
var teamServer = new TeamServer(this, team, configuration, registration.port(), registration.apiPort());
129+
var teamServer = new TeamServer(this, dockerService, team, configuration, registration.port(), registration.apiPort());
127130
teamServer.running(true);
128131
server.put(team, teamServer);
129132
freePorts.removeElement(registration.apiPort());
@@ -134,7 +137,7 @@ public CompletableFuture<Boolean> syncVelocity() {
134137
}
135138

136139
public TeamServer get(Team team) {
137-
return server.computeIfAbsent(team, key -> new TeamServer(this, key, configuration, nextPort(), nextPort()));
140+
return server.computeIfAbsent(team, key -> new TeamServer(this, dockerService, key, configuration, nextPort(), nextPort()));
138141
}
139142

140143
private int nextPort() {

0 commit comments

Comments
 (0)