summaryrefslogtreecommitdiffstats
path: root/src/docker.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-07-12 19:38:03 +0200
committersalmonik <salmonik@tuta.io>2026-07-12 19:38:03 +0200
commitae7ef10b67737f08a86743f687bca30a12e9e17a (patch)
tree056539ab76b801559dc8d07ee6599220e2bd8e07 /src/docker.rs
parentarchitecture (diff)
downloadminecraft-ui-ae7ef10b67737f08a86743f687bca30a12e9e17a.tar.gz
minecraft-ui-ae7ef10b67737f08a86743f687bca30a12e9e17a.zip
first stable
Diffstat (limited to '')
-rw-r--r--src/docker.rs213
1 files changed, 213 insertions, 0 deletions
diff --git a/src/docker.rs b/src/docker.rs
new file mode 100644
index 0000000..b18bb47
--- /dev/null
+++ b/src/docker.rs
@@ -0,0 +1,213 @@
1use bollard::Docker;
2use bollard::container::{
3 Config as ContainerConfig, CreateContainerOptions, ListContainersOptions,
4 RemoveContainerOptions, StartContainerOptions, StopContainerOptions,
5};
6use bollard::image::CreateImageOptions;
7use bollard::models::HostConfig;
8use futures_util::StreamExt;
9use std::collections::HashMap;
10
11use crate::config::Config;
12use crate::models::Instance;
13
14#[derive(Clone)]
15pub struct DockerManager {
16 client: Docker,
17 config: Config,
18}
19
20#[derive(Debug, Clone)]
21pub enum ContainerStatus {
22 Running,
23 Stopped,
24 Starting,
25 NotFound,
26 Error(String),
27}
28
29impl DockerManager {
30 pub fn new(config: Config) -> Result<Self, bollard::errors::Error> {
31 let client = Docker::connect_with_local_defaults()?;
32 Ok(Self { client, config })
33 }
34
35 pub async fn container_status(&self) -> ContainerStatus {
36 let mut filters = HashMap::new();
37 filters.insert("name".to_string(), vec!["mc".to_string()]);
38
39 let options = ListContainersOptions {
40 all: true,
41 filters,
42 ..Default::default()
43 };
44
45 match self.client.list_containers(Some(options)).await {
46 Ok(containers) => {
47 if containers.is_empty() {
48 ContainerStatus::NotFound
49 } else {
50 match containers[0].state.as_deref() {
51 Some("running") => ContainerStatus::Running,
52 Some("exited") | Some("stopped") => ContainerStatus::Stopped,
53 _ => ContainerStatus::Starting,
54 }
55 }
56 }
57 Err(e) => ContainerStatus::Error(e.to_string()),
58 }
59 }
60
61 pub async fn start_container(&self, instance: &Instance) -> Result<(), String> {
62 tracing::info!("Pulling itzg/minecraft-server:latest image...");
63 self.pull_image().await?;
64 tracing::info!("Creating container for instance '{}'", instance.name);
65 self.recreate_container(instance).await?;
66 tracing::info!("Starting container...");
67 self.start_existing().await
68 }
69
70 async fn pull_image(&self) -> Result<(), String> {
71 let options = CreateImageOptions {
72 from_image: "itzg/minecraft-server",
73 tag: "latest",
74 ..Default::default()
75 };
76
77 let mut stream = self.client.create_image(Some(options), None, None);
78
79 while let Some(result) = stream.next().await {
80 match result {
81 Ok(_info) => {}
82 Err(e) => return Err(format!("Image pull failed: {e}")),
83 }
84 }
85 Ok(())
86 }
87
88 async fn recreate_container(&self, instance: &Instance) -> Result<(), String> {
89 // Remove existing container if present
90 let remove_options = RemoveContainerOptions {
91 force: true,
92 ..Default::default()
93 };
94 match self
95 .client
96 .remove_container("mc", Some(remove_options))
97 .await
98 {
99 Ok(_) => tracing::info!("Removed existing container"),
100 Err(e) => tracing::debug!("No existing container to remove: {e}"),
101 }
102
103 let server_dir = self
104 .config
105 .server_directory
106 .canonicalize()
107 .unwrap_or_else(|_| self.config.server_directory.clone())
108 .display()
109 .to_string();
110 let instance_data = format!("{server_dir}/instances/{}/data", instance.name);
111
112 let mut env_strings: Vec<String> = vec![
113 "EULA=TRUE".into(),
114 format!("VERSION={}", instance.version),
115 format!("TYPE={}", instance.server_type),
116 format!("MEMORY={}M", instance.memory_mb),
117 format!("MAX_PLAYERS={}", instance.max_players),
118 format!("DIFFICULTY={}", instance.difficulty),
119 "FORCE_GAMEMODE=true".into(),
120 "SPAWN_PROTECTION=0".into(),
121 "ENABLE_WHITELIST=true".into(),
122 "PAUSE_WHEN_EMPTY_SECONDS=30".into(),
123 "ONLINE_MODE=false".into(),
124 "TZ=Europe/Warsaw".into(),
125 "USE_AIKAR_FLAGS=true".into(),
126 "ENABLE_RCON=true".into(),
127 format!("RCON_PASSWORD={}", instance.rcon_password),
128 "RCON_PORT=25575".into(),
129 format!("MOTD={}", instance.motd),
130 format!("GAMEMODE={}", instance.gamemode),
131 format!("LEVEL_TYPE={}", instance.level_type),
132 ];
133
134 if instance.pvp {
135 env_strings.push("PVP=true".into());
136 } else {
137 env_strings.push("PVP=false".into());
138 }
139
140 if let Some(ref seed) = instance.world_seed {
141 if !seed.is_empty() {
142 env_strings.push(format!("LEVEL_SEED={seed}"));
143 }
144 }
145
146 let env_refs: Vec<&str> = env_strings.iter().map(|s| s.as_str()).collect();
147
148 let mut exposed_ports: HashMap<&str, HashMap<(), ()>> = HashMap::new();
149 exposed_ports.insert("25565/tcp", HashMap::new());
150 exposed_ports.insert("25575/tcp", HashMap::new());
151
152 let mut port_bindings = HashMap::new();
153 port_bindings.insert(
154 "25565/tcp".to_string(),
155 Some(vec![bollard::models::PortBinding {
156 host_ip: None,
157 host_port: Some("25565".to_string()),
158 }]),
159 );
160 port_bindings.insert(
161 "25575/tcp".to_string(),
162 Some(vec![bollard::models::PortBinding {
163 host_ip: None,
164 host_port: Some("25575".to_string()),
165 }]),
166 );
167
168 let host_config = HostConfig {
169 binds: Some(vec![format!("{instance_data}:/data")]),
170 port_bindings: Some(port_bindings),
171 ..Default::default()
172 };
173
174 let container_config = ContainerConfig {
175 image: Some("itzg/minecraft-server:latest"),
176 tty: Some(true),
177 env: Some(env_refs),
178 exposed_ports: Some(exposed_ports),
179 host_config: Some(host_config),
180 ..Default::default()
181 };
182
183 let options = CreateContainerOptions {
184 name: "mc",
185 ..Default::default()
186 };
187
188 self.client
189 .create_container(Some(options), container_config)
190 .await
191 .map_err(|e| format!("Failed to create container: {e}"))?;
192
193 tracing::info!("Container 'mc' created successfully");
194 Ok(())
195 }
196
197 async fn start_existing(&self) -> Result<(), String> {
198 self.client
199 .start_container("mc", None::<StartContainerOptions<&str>>)
200 .await
201 .map_err(|e| format!("Failed to start container: {e}"))?;
202 Ok(())
203 }
204
205 pub async fn stop_container(&self) -> Result<(), String> {
206 let options = StopContainerOptions { t: 30 };
207 self.client
208 .stop_container("mc", Some(options))
209 .await
210 .map_err(|e| format!("Failed to stop container: {e}"))?;
211 Ok(())
212 }
213}