add docker container creation (add broken since sqlite change)

This commit is contained in:
2026-06-09 00:42:50 +02:00
parent 410ca2eeef
commit 5136e0bdc3
13 changed files with 187 additions and 23 deletions

1
Cargo.lock generated
View File

@ -604,6 +604,7 @@ dependencies = [
"clap", "clap",
"diesel", "diesel",
"diesel-enum", "diesel-enum",
"futures-util",
"json", "json",
"num-derive", "num-derive",
"num-traits", "num-traits",

View File

@ -10,6 +10,7 @@ chrono = "0.4.44"
clap = "4.6.1" clap = "4.6.1"
diesel = { version = "2.3.9", features = ["sqlite", "r2d2", "chrono"] } diesel = { version = "2.3.9", features = ["sqlite", "r2d2", "chrono"] }
diesel-enum = "0.2.1" diesel-enum = "0.2.1"
futures-util = "0.3.32"
json = "0.12.4" json = "0.12.4"
num-derive = "0.4.2" num-derive = "0.4.2"
num-traits = "0.2.19" num-traits = "0.2.19"

View File

@ -4,8 +4,7 @@ use crate::status::ServerStatus;
use crate::utils::{ validate_name, extract_archive, move_all_elements}; use crate::utils::{ validate_name, extract_archive, move_all_elements};
use diesel::prelude::*; use diesel::prelude::*;
use crate::{DbPool, models}; use crate::{DbPool, models}; use crate::schema;
use crate::schema;
use tokio::{fs::{File, create_dir}, io::AsyncWriteExt}; 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>{ pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{
// use schema::servers::dsl::*; use schema::servers::dsl::*;
let config = crate::Config::load(); let config = crate::Config::load();
let server_name = arg.get_one::<String>("NAME").unwrap(); let server_name = arg.get_one::<String>("NAME").unwrap();
@ -65,6 +64,7 @@ pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{
redirect_ip: None 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(()) Ok(())
} }

View File

@ -96,5 +96,3 @@ pub fn update_env(arguments: &ArgMatches, prev_env: String) -> String{
settings_to_env(merged) settings_to_env(merged)
} }
// START COMMAND :
// docker run --rm --env-file ./env -u 1000:100 -it --volume $PWD/server:/data itzg/minecraft-server

View File

@ -2,6 +2,7 @@ pub mod add;
pub mod edit; pub mod edit;
pub mod remove; pub mod remove;
pub mod ls; pub mod ls;
pub mod start;
pub mod env_settings; pub mod env_settings;

20
src/cli/start.rs Normal file
View File

@ -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::<String>("NAME").unwrap();
dockermgr::start_server(&docker, pool, server_name).await?;
Ok(())
}

View File

@ -1,11 +1,13 @@
pub struct Config { pub struct Config {
pub config_path: String pub config_path: String,
pub docker_image: String,
} }
impl Config { impl Config {
pub fn load() -> Self { pub fn load() -> Self {
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()
} }
} }
} }

View File

@ -1,3 +0,0 @@
pub async fn start_server() -> Result<String, String> {
Ok("".to_string())
}

View File

@ -1,2 +1,4 @@
mod start; mod start;
pub use start::start_server; pub use start::start_server;
pub mod utils;

75
src/dockermgr/start.rs Normal file
View File

@ -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<String, String> {
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(())
}

67
src/dockermgr/utils.rs Normal file
View File

@ -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<ContainerInspectResponse> {
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<String, String>{
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())
}

View File

@ -1,6 +0,0 @@
use bollard::Docker;
use std::sync::LazyLock;
pub static mut DOCKER: LazyLock<Docker> = LazyLock::new(|| Docker::connect_with_local_defaults().expect("Failed to connect to the docker socket") );

View File

@ -6,7 +6,7 @@
/* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */ /* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */ /* +#+#+#+#+#+ +#+ */
/* Created: 2026/05/29 21:22:17 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 status;
pub mod docker; pub mod dockermgr;
mod cli; mod cli;
use cli::*; use cli::*;
@ -28,20 +28,22 @@ use cli::*;
mod config; mod config;
pub use config::Config; pub use config::Config;
use clap::Command;
pub type DbPool = Pool<ConnectionManager<SqliteConnection>>; pub type DbPool = Pool<ConnectionManager<SqliteConnection>>;
use bollard::Docker;
fn get_connection_pool() -> DbPool { 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::<SqliteConnection>::new(&database_url); let manager = ConnectionManager::<SqliteConnection>::new(&database_url);
Pool::builder() Pool::builder()
.build(manager) .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 { fn cli() -> Command {
Command::new("dmm") Command::new("dmm")
.about("Docker Minecraft server Manager cli") .about("Docker Minecraft server Manager cli")
@ -56,12 +58,14 @@ fn cli() -> Command {
.subcommand(remove::create_subcommand()) .subcommand(remove::create_subcommand())
.subcommand(edit::create_subcommand()) .subcommand(edit::create_subcommand())
.subcommand(ls::create_subcommand()) .subcommand(ls::create_subcommand())
.subcommand(start::create_subcommand())
} }
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), String> { async fn main() -> Result<(), String> {
let arg = cli().get_matches(); let arg = cli().get_matches();
let pool = get_connection_pool(); let pool = get_connection_pool();
let docker = Docker::connect_with_defaults().expect("Failed to connect to local docker");
match arg.subcommand() { match arg.subcommand() {
Some(("serverMode", _)) => { mc_socket_listen(pool).await.map_err(|_| "socket error".to_string())?; }, 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(("remove", sub_matches)) => { remove::subcommand(sub_matches, pool).await?; },
Some(("ls", sub_matches)) => { ls::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(("edit", sub_matches)) => { edit::subcommand(sub_matches, pool).await?; },
Some(("start", sub_matches)) => { start::subcommand(sub_matches, &pool, docker).await?; },
_ => {panic!("subcommand not implemented")} _ => {panic!("subcommand not implemented")}
} }
Ok(()) Ok(())