From 5136e0bdc37bac141464e8737e406507425702fe Mon Sep 17 00:00:00 2001 From: tomoron Date: Tue, 9 Jun 2026 00:42:50 +0200 Subject: [PATCH] add docker container creation (add broken since sqlite change) --- Cargo.lock | 1 + Cargo.toml | 1 + src/cli/add.rs | 8 ++-- src/cli/env_settings.rs | 2 - src/cli/mod.rs | 1 + src/cli/start.rs | 20 +++++++++ src/config.rs | 6 ++- src/docker/start.rs | 3 -- src/{docker => dockermgr}/mod.rs | 2 + src/dockermgr/start.rs | 75 ++++++++++++++++++++++++++++++++ src/dockermgr/utils.rs | 67 ++++++++++++++++++++++++++++ src/lib.rs | 6 --- src/main.rs | 18 +++++--- 13 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 src/cli/start.rs delete mode 100644 src/docker/start.rs rename src/{docker => dockermgr}/mod.rs (71%) create mode 100644 src/dockermgr/start.rs create mode 100644 src/dockermgr/utils.rs delete mode 100644 src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9b37092..407d980 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -604,6 +604,7 @@ dependencies = [ "clap", "diesel", "diesel-enum", + "futures-util", "json", "num-derive", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index b84a6eb..14810c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ chrono = "0.4.44" clap = "4.6.1" diesel = { version = "2.3.9", features = ["sqlite", "r2d2", "chrono"] } diesel-enum = "0.2.1" +futures-util = "0.3.32" json = "0.12.4" num-derive = "0.4.2" num-traits = "0.2.19" diff --git a/src/cli/add.rs b/src/cli/add.rs index 1a3e5a9..9f467b0 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -4,8 +4,7 @@ use crate::status::ServerStatus; use crate::utils::{ validate_name, extract_archive, move_all_elements}; use diesel::prelude::*; -use crate::{DbPool, models}; -use crate::schema; +use crate::{DbPool, models}; use crate::schema; use tokio::{fs::{File, create_dir}, io::AsyncWriteExt}; @@ -27,7 +26,7 @@ pub fn create_subcommand() -> Command { pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{ -// use schema::servers::dsl::*; + use schema::servers::dsl::*; let config = crate::Config::load(); let server_name = arg.get_one::("NAME").unwrap(); @@ -65,6 +64,7 @@ pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{ redirect_ip: None }; -// insert_into(schema::servers::dsl::servers).values(&new_server).execute(conn).map_err(|e| format!("Failed to insert in db, error : {:?}", e))?; +// diesel::insert_into(schema::servers::dsl::servers).values(&new_server).execute(conn).map_err(|e| format!("Failed to insert in db, error : {:?}", e))?; +// TODO: fix weird error Ok(()) } diff --git a/src/cli/env_settings.rs b/src/cli/env_settings.rs index d53740c..0c0b349 100644 --- a/src/cli/env_settings.rs +++ b/src/cli/env_settings.rs @@ -96,5 +96,3 @@ pub fn update_env(arguments: &ArgMatches, prev_env: String) -> String{ settings_to_env(merged) } -// START COMMAND : -// docker run --rm --env-file ./env -u 1000:100 -it --volume $PWD/server:/data itzg/minecraft-server diff --git a/src/cli/mod.rs b/src/cli/mod.rs index e7aadbe..190c0ea 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2,6 +2,7 @@ pub mod add; pub mod edit; pub mod remove; pub mod ls; +pub mod start; pub mod env_settings; diff --git a/src/cli/start.rs b/src/cli/start.rs new file mode 100644 index 0000000..7ebe68f --- /dev/null +++ b/src/cli/start.rs @@ -0,0 +1,20 @@ +use bollard::Docker; +use clap::Arg; +use clap::{Command, ArgMatches}; + +use crate::DbPool; + +use crate::dockermgr; + +pub fn create_subcommand() -> Command { + Command::new("start") + .about("manually start a server (and set last login datetime to now)") + .arg(Arg::new("NAME").required(true)) +} + +pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: Docker) -> Result<(), String> { + let server_name = arguments.get_one::("NAME").unwrap(); + dockermgr::start_server(&docker, pool, server_name).await?; + + Ok(()) +} diff --git a/src/config.rs b/src/config.rs index 2ba54f6..e2ca39d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,11 +1,13 @@ pub struct Config { - pub config_path: String + pub config_path: String, + pub docker_image: String, } impl Config { pub fn load() -> Self { Self { - config_path: "/home/tom/desktop/rust/yo_mama/servers".to_string() // that's my program I do what I want + config_path: "/home/tom/desktop/rust/yo_mama/servers".to_string(), // that's my program I do what I want + docker_image: "itzg/minecraft-server".to_string() } } } diff --git a/src/docker/start.rs b/src/docker/start.rs deleted file mode 100644 index 1e52853..0000000 --- a/src/docker/start.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub async fn start_server() -> Result { - Ok("".to_string()) -} diff --git a/src/docker/mod.rs b/src/dockermgr/mod.rs similarity index 71% rename from src/docker/mod.rs rename to src/dockermgr/mod.rs index cbe2574..0a70def 100644 --- a/src/docker/mod.rs +++ b/src/dockermgr/mod.rs @@ -1,2 +1,4 @@ mod start; pub use start::start_server; + +pub mod utils; diff --git a/src/dockermgr/start.rs b/src/dockermgr/start.rs new file mode 100644 index 0000000..e0c7208 --- /dev/null +++ b/src/dockermgr/start.rs @@ -0,0 +1,75 @@ +use bollard::{Docker, plugin::{ContainerCreateBody, HostConfig, Mount, MountType}, query_parameters::CreateContainerOptionsBuilder}; +use diesel::{prelude::*, result::Error::NotFound}; + +use crate::{schema, models, status::ServerStatus}; + +use crate::{DbPool, dockermgr::utils::{self, get_container_ip}}; + +async fn start_docker_container(docker: &Docker, name: &String) -> Result { + utils::check_image(&docker).await?; + + let container_name = format!("minecraft-{}", name); + + if let Some(_) = utils::get_container(&docker, &container_name).await { + return Err("Container already exists".to_string()) + } + + let config_path = crate::Config::load().config_path; + + let env_file = std::fs::read_to_string(config_path.clone() + "/" + name + "/env").map_err(|e| format!("Failed to read env file {}", e))?; + + let env = env_file.split("\n").filter_map(|x| if x.len() == 0 { None } else { Some(x.to_string()) } ).collect(); + + let mut mount = Mount::default(); + mount.typ = Some(MountType::BIND); + mount.target = Some("/data".to_string()); + mount.source = Some(config_path + "/" + name + "/server"); + mount.read_only = Some(false); + + let mut host_config = HostConfig::default(); + host_config.mounts = Some(vec![mount]); + host_config.auto_remove = Some(true); + + let mut create_config = ContainerCreateBody::default(); + create_config.tty = Some(true); + create_config.open_stdin = Some(true); + create_config.user = Some("1000:100".to_string()); + create_config.stop_timeout = Some(60); + create_config.host_config = Some(host_config); + create_config.image = Some(crate::Config::load().docker_image + ":latest"); + create_config.env = Some(env); + + let options = CreateContainerOptionsBuilder::default() + .name(&container_name) + .build(); + + docker.create_container(Some(options), create_config).await.map_err(|e| format!("failed to create the container : {}", e))?; + println!("created container"); + + println!("starting container"); + docker.start_container(&container_name, None).await.map_err(|e| format!("Failed to start the container : {}", e))?; + Ok(get_container_ip(docker, name).await?) +} + +pub async fn start_server(docker: &Docker, pool: &DbPool, server_name: &String) -> Result<(), String> { + use schema::servers::dsl::*; + + let conn = &mut pool.get().unwrap(); + + let server = match servers.select(models::Servers::as_select()).filter(name.eq(server_name)).first(conn) { + Err(NotFound) => { return Err("This server does not exist".to_string()); }, + Err(e) => { return Err(format!("Failed to get server from db : {}", e)) }, + Ok(server) => { server } + }; + + if server.status == ServerStatus::Starting { + return Err("Server is starting, please wait".to_string()); + } + + diesel::update(servers).filter(name.eq(server_name)).set(status.eq(ServerStatus::Starting)).execute(conn).map_err(|e| format!("Failed to set server to starting : {}", e))?; + + let ip = start_docker_container(docker, server_name).await?; + + diesel::update(servers).filter(name.eq(server_name)).set((status.eq(ServerStatus::Starting), redirect_ip.eq(ip.to_string()))).execute(conn).map_err(|e| format!("Failed to set server to starting : {}", e))?; + Ok(()) +} diff --git a/src/dockermgr/utils.rs b/src/dockermgr/utils.rs new file mode 100644 index 0000000..3c2cf67 --- /dev/null +++ b/src/dockermgr/utils.rs @@ -0,0 +1,67 @@ +use std::collections::HashMap; + +use bollard::{Docker, plugin::{ContainerInspectResponse, ContainerSummary, ImageId}, query_parameters::{CreateImageOptionsBuilder, ListContainersOptionsBuilder, ListImagesOptionsBuilder}}; + +use futures_util::stream::StreamExt; +use tokio::task; + +pub async fn check_image(docker: &Docker) -> Result<(), String> { + let mut filters = HashMap::new(); + let container_name = crate::Config::load().docker_image; + filters.insert("reference", vec![&container_name]); + let options = ListImagesOptionsBuilder::default() + .all(true) + .filters(&filters) + .build(); + + + let images = docker.list_images(Some(options)).await.map_err(|e| format!("failed to check if image exists, error : {:?}", e))?; + if images.len() >= 1 { + return Ok(()); + } + + let options = CreateImageOptionsBuilder::default() + .from_image(&container_name) + .tag("latest") + .build(); + + + println!("pulling {} from docker hub ... (this might take a while and I'm too lazy to do an Interface to show the progress, if you want progress info, pull it yourself on with `docker pull {}`)", container_name, container_name); + + let mut pull_result = docker.create_image(Some(options), None, None); + + while let Some(update) = pull_result.next().await { + if let Err(e) = update { + return Err(format!("Failed to pull the image from docker hub, error : {:?}", e)); + } + } + println!("pulled successfully"); + + Ok(()) +} + +pub async fn get_container(docker: &Docker, name: &String) -> Option { + println!("get {}", name); + + let result = docker.inspect_container(name, None).await.map_err(|e| format!("failed to retreive container : {:?}", e)); + match result { + Err(_) => { None }, + Ok(container) => { Some(container) } + } +} + +pub async fn get_container_ip(docker: &Docker, name: &String) -> Result{ + let mut retries = 20; + + while retries > 0 { + let container = get_container(docker, name).await.unwrap(); + if let Some(network) = container.network_settings.unwrap().networks.unwrap().get("bridge") { + if let Some(ip) = &network.ip_address { + return Ok(ip.to_string()); + } + } + async_std::task::sleep(std::time::Duration::from_millis(500)).await; + retries -= 1; + } + Ok("can't get container ip".to_string()) +} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 51bc2c4..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -use bollard::Docker; - -use std::sync::LazyLock; - -pub static mut DOCKER: LazyLock = LazyLock::new(|| Docker::connect_with_local_defaults().expect("Failed to connect to the docker socket") ); - diff --git a/src/main.rs b/src/main.rs index 402ac40..3622a12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ /* By: tomoron +#+ +:+ +#+ */ /* +#+#+#+#+#+ +#+ */ /* Created: 2026/05/29 21:22:17 by tomoron #+# #+# */ -/* Updated: 2026/06/06 19:39:47 by tomoron ### ########.fr */ +/* Updated: 2026/06/09 00:05:23 by tomoron ### ########.fr */ /* */ /* ************************************************************************** */ @@ -20,7 +20,7 @@ pub mod models; pub mod status; -pub mod docker; +pub mod dockermgr; mod cli; use cli::*; @@ -28,20 +28,22 @@ use cli::*; mod config; pub use config::Config; +use clap::Command; + pub type DbPool = Pool>; +use bollard::Docker; + fn get_connection_pool() -> DbPool { - let database_url = "sqlite:///".to_string() + &Config::load().config_path + "/db.sqlite"; + let database_url = "sqlite://".to_string() + &Config::load().config_path + "/db.sqlite"; let manager = ConnectionManager::::new(&database_url); Pool::builder() .build(manager) - .unwrap_or_else(|_| panic!("Error creating pool for {}", database_url)) + .unwrap_or_else(|e| panic!("Error creating pool for {}\nerror: {:?}", database_url, e)) } -use clap::Command; - fn cli() -> Command { Command::new("dmm") .about("Docker Minecraft server Manager cli") @@ -56,12 +58,14 @@ fn cli() -> Command { .subcommand(remove::create_subcommand()) .subcommand(edit::create_subcommand()) .subcommand(ls::create_subcommand()) + .subcommand(start::create_subcommand()) } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), String> { let arg = cli().get_matches(); let pool = get_connection_pool(); + let docker = Docker::connect_with_defaults().expect("Failed to connect to local docker"); match arg.subcommand() { Some(("serverMode", _)) => { mc_socket_listen(pool).await.map_err(|_| "socket error".to_string())?; }, @@ -69,6 +73,8 @@ async fn main() -> Result<(), String> { Some(("remove", sub_matches)) => { remove::subcommand(sub_matches, pool).await?; }, Some(("ls", sub_matches)) => { ls::subcommand(sub_matches, pool).await?; }, Some(("edit", sub_matches)) => { edit::subcommand(sub_matches, pool).await?; }, + + Some(("start", sub_matches)) => { start::subcommand(sub_matches, &pool, docker).await?; }, _ => {panic!("subcommand not implemented")} } Ok(())