use bollard::Docker; use bollard::container::{ Config as ContainerConfig, CreateContainerOptions, ListContainersOptions, RemoveContainerOptions, StartContainerOptions, StopContainerOptions, }; use bollard::image::CreateImageOptions; use bollard::models::HostConfig; use futures_util::StreamExt; use std::collections::HashMap; use crate::config::Config; use crate::models::Instance; #[derive(Clone)] pub struct DockerManager { client: Docker, config: Config, } #[derive(Debug, Clone)] pub enum ContainerStatus { Running, Stopped, Starting, NotFound, Error(String), } impl DockerManager { pub fn new(config: Config) -> Result { let client = Docker::connect_with_local_defaults()?; Ok(Self { client, config }) } pub async fn container_status(&self) -> ContainerStatus { let mut filters = HashMap::new(); filters.insert("name".to_string(), vec!["mc".to_string()]); let options = ListContainersOptions { all: true, filters, ..Default::default() }; match self.client.list_containers(Some(options)).await { Ok(containers) => { if containers.is_empty() { ContainerStatus::NotFound } else { match containers[0].state.as_deref() { Some("running") => ContainerStatus::Running, Some("exited") | Some("stopped") => ContainerStatus::Stopped, _ => ContainerStatus::Starting, } } } Err(e) => ContainerStatus::Error(e.to_string()), } } pub async fn start_container(&self, instance: &Instance) -> Result<(), String> { tracing::info!("Pulling itzg/minecraft-server:latest image..."); self.pull_image().await?; tracing::info!("Creating container for instance '{}'", instance.name); self.recreate_container(instance).await?; tracing::info!("Starting container..."); self.start_existing().await } async fn pull_image(&self) -> Result<(), String> { let options = CreateImageOptions { from_image: "itzg/minecraft-server", tag: "latest", ..Default::default() }; let mut stream = self.client.create_image(Some(options), None, None); while let Some(result) = stream.next().await { match result { Ok(_info) => {} Err(e) => return Err(format!("Image pull failed: {e}")), } } Ok(()) } async fn recreate_container(&self, instance: &Instance) -> Result<(), String> { // Remove existing container if present let remove_options = RemoveContainerOptions { force: true, ..Default::default() }; match self .client .remove_container("mc", Some(remove_options)) .await { Ok(_) => tracing::info!("Removed existing container"), Err(e) => tracing::debug!("No existing container to remove: {e}"), } let server_dir = self .config .server_directory .canonicalize() .unwrap_or_else(|_| self.config.server_directory.clone()) .display() .to_string(); let instance_data = format!("{server_dir}/instances/{}/data", instance.name); let mut env_strings: Vec = vec![ "EULA=TRUE".into(), format!("VERSION={}", instance.version), format!("TYPE={}", instance.server_type), format!("MEMORY={}M", instance.memory_mb), format!("MAX_PLAYERS={}", instance.max_players), format!("DIFFICULTY={}", instance.difficulty), "FORCE_GAMEMODE=true".into(), "SPAWN_PROTECTION=0".into(), "ENABLE_WHITELIST=true".into(), "PAUSE_WHEN_EMPTY_SECONDS=30".into(), "ONLINE_MODE=false".into(), "TZ=Europe/Warsaw".into(), "USE_AIKAR_FLAGS=true".into(), "ENABLE_RCON=true".into(), format!("RCON_PASSWORD={}", instance.rcon_password), "RCON_PORT=25575".into(), format!("MOTD={}", instance.motd), format!("GAMEMODE={}", instance.gamemode), format!("LEVEL_TYPE={}", instance.level_type), ]; if instance.pvp { env_strings.push("PVP=true".into()); } else { env_strings.push("PVP=false".into()); } if let Some(ref seed) = instance.world_seed { if !seed.is_empty() { env_strings.push(format!("LEVEL_SEED={seed}")); } } let env_refs: Vec<&str> = env_strings.iter().map(|s| s.as_str()).collect(); let mut exposed_ports: HashMap<&str, HashMap<(), ()>> = HashMap::new(); exposed_ports.insert("25565/tcp", HashMap::new()); exposed_ports.insert("25575/tcp", HashMap::new()); let mut port_bindings = HashMap::new(); port_bindings.insert( "25565/tcp".to_string(), Some(vec![bollard::models::PortBinding { host_ip: None, host_port: Some("25565".to_string()), }]), ); port_bindings.insert( "25575/tcp".to_string(), Some(vec![bollard::models::PortBinding { host_ip: None, host_port: Some("25575".to_string()), }]), ); let host_config = HostConfig { binds: Some(vec![format!("{instance_data}:/data")]), port_bindings: Some(port_bindings), ..Default::default() }; let container_config = ContainerConfig { image: Some("itzg/minecraft-server:latest"), tty: Some(true), env: Some(env_refs), exposed_ports: Some(exposed_ports), host_config: Some(host_config), ..Default::default() }; let options = CreateContainerOptions { name: "mc", ..Default::default() }; self.client .create_container(Some(options), container_config) .await .map_err(|e| format!("Failed to create container: {e}"))?; tracing::info!("Container 'mc' created successfully"); Ok(()) } async fn start_existing(&self) -> Result<(), String> { self.client .start_container("mc", None::>) .await .map_err(|e| format!("Failed to start container: {e}"))?; Ok(()) } pub async fn stop_container(&self) -> Result<(), String> { let options = StopContainerOptions { t: 30 }; self.client .stop_container("mc", Some(options)) .await .map_err(|e| format!("Failed to stop container: {e}"))?; Ok(()) } }