diff --git a/Cargo.lock b/Cargo.lock index 407d980..edb0095 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,6 +561,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "diesel_migrations" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + [[package]] name = "diesel_table_macro_syntax" version = "0.3.0" @@ -604,6 +615,7 @@ dependencies = [ "clap", "diesel", "diesel-enum", + "diesel_migrations", "futures-util", "json", "num-derive", @@ -1300,6 +1312,27 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "migrations_internals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "migrations_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1667,6 +1700,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1904,6 +1946,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + [[package]] name = "tower-service" version = "0.3.3" @@ -2207,6 +2280,18 @@ dependencies = [ "windows-link", ] +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 14810c6..e08d641 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" +diesel_migrations = "2.3.2" futures-util = "0.3.32" json = "0.12.4" num-derive = "0.4.2" diff --git a/migrations/2026-05-28-142613-0000_servers/up.sql b/migrations/2026-05-28-142613-0000_servers/up.sql index 4dadf3d..edfa6df 100644 --- a/migrations/2026-05-28-142613-0000_servers/up.sql +++ b/migrations/2026-05-28-142613-0000_servers/up.sql @@ -1,10 +1,9 @@ CREATE TABLE "servers" ( "name" varchar(255) UNIQUE NOT NULL PRIMARY KEY, "last_login" timestamp, - "container_id" varchar(30), "status" INT2 NOT NULL DEFAULT 0, "is_default" bool NOT NULL DEFAULT false, "redirect_ip" varchar(50) ); -insert into servers (name, last_login, container_id, status, is_default, redirect_ip) values ('potato', datetime(), null, 1, false, 'play.hypixel.net:25565'); +insert into servers (name, last_login, status, is_default, redirect_ip) values ('potato', datetime(), 4, false, 'play.hypixel.net:25565'); diff --git a/src/cli/add.rs b/src/cli/add.rs index 9f467b0..a86f217 100644 --- a/src/cli/add.rs +++ b/src/cli/add.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use clap::{ArgMatches, Command, arg}; use crate::cli::env_settings; use crate::status::ServerStatus; @@ -9,7 +10,7 @@ use crate::{DbPool, models}; use crate::schema; use tokio::{fs::{File, create_dir}, io::AsyncWriteExt}; use std::path::Path; -use std::time::SystemTime; + pub fn create_subcommand() -> Command { let mut arguments = vec![ @@ -56,15 +57,13 @@ pub async fn subcommand(arg: &ArgMatches, pool: DbPool) -> Result<(), String>{ std::fs::remove_dir_all(path.to_string() + "tmp").map_err(|e| format!("failed to remove the tmp dir, reason : {}", e))?; } - let _new_server = models::CreateServer { + let new_server = models::CreateServer { name: server_name, - last_login: Some(SystemTime::now()), - container_id: None, + last_login: Some(Utc::now().naive_utc()), status: ServerStatus::Stopped, redirect_ip: None }; -// 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 + diesel::insert_into(servers).values(&new_server).execute(conn).map_err(|e| format!("Failed to insert in db, error : {:?}", e))?; Ok(()) } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 190c0ea..65063f9 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -3,6 +3,7 @@ pub mod edit; pub mod remove; pub mod ls; pub mod start; +pub mod stop; pub mod env_settings; diff --git a/src/cli/start.rs b/src/cli/start.rs index 7ebe68f..795b769 100644 --- a/src/cli/start.rs +++ b/src/cli/start.rs @@ -1,11 +1,14 @@ use bollard::Docker; +use chrono::Utc; use clap::Arg; use clap::{Command, ArgMatches}; -use crate::DbPool; +use crate::{ DbPool, schema }; use crate::dockermgr; +use diesel::prelude::*; + pub fn create_subcommand() -> Command { Command::new("start") .about("manually start a server (and set last login datetime to now)") @@ -13,8 +16,11 @@ pub fn create_subcommand() -> Command { } pub async fn subcommand(arguments: &ArgMatches, pool: &DbPool, docker: Docker) -> Result<(), String> { + use schema::servers::dsl::*; let server_name = arguments.get_one::("NAME").unwrap(); dockermgr::start_server(&docker, pool, server_name).await?; + + diesel::update(servers).filter(name.eq(server_name)).set(last_login.eq(Utc::now().naive_utc())).execute(&mut pool.get().unwrap()).map_err(|e| format!("Failed to set the last login date: {}", e))?; Ok(()) } diff --git a/src/cli/stop.rs b/src/cli/stop.rs new file mode 100644 index 0000000..3d940af --- /dev/null +++ b/src/cli/stop.rs @@ -0,0 +1,22 @@ +use bollard::Docker; +use clap::Arg; +use clap::{Command, ArgMatches}; + +use crate::DbPool; + +use crate::dockermgr; + +pub fn create_subcommand() -> Command { + Command::new("stop") + .about("manually stop a server") + .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(); + println!("Stopping container"); + dockermgr::stop_server(&docker, pool, server_name).await?; + println!("container stopped"); + + Ok(()) +} diff --git a/src/dockermgr/mod.rs b/src/dockermgr/mod.rs index 0a70def..275dfd5 100644 --- a/src/dockermgr/mod.rs +++ b/src/dockermgr/mod.rs @@ -2,3 +2,6 @@ mod start; pub use start::start_server; pub mod utils; + +mod stop; +pub use stop::stop_server; diff --git a/src/dockermgr/start.rs b/src/dockermgr/start.rs index e0c7208..0de0ff0 100644 --- a/src/dockermgr/start.rs +++ b/src/dockermgr/start.rs @@ -48,13 +48,14 @@ async fn start_docker_container(docker: &Docker, name: &String) -> Result Result<(), String> { use schema::servers::dsl::*; - let conn = &mut pool.get().unwrap(); + 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()); }, @@ -66,10 +67,14 @@ pub async fn start_server(docker: &Docker, pool: &DbPool, server_name: &String) return Err("Server is starting, please wait".to_string()); } + if server.status != ServerStatus::Stopped { + return Err(format!("Invalid server state({}), refusing to start", server.status)); + } + 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))?; + diesel::update(servers).filter(name.eq(server_name)).set((status.eq(ServerStatus::Running), redirect_ip.eq(ip.to_string() + ":25565"))).execute(conn).map_err(|e| format!("Failed to set server to starting : {}", e))?; Ok(()) } diff --git a/src/dockermgr/stop.rs b/src/dockermgr/stop.rs new file mode 100644 index 0000000..ecfe970 --- /dev/null +++ b/src/dockermgr/stop.rs @@ -0,0 +1,41 @@ +use bollard::{Docker, query_parameters::StopContainerOptionsBuilder}; +use diesel::result::Error::NotFound; + +use diesel::prelude::*; + +use crate::{ DbPool, models, schema, status::ServerStatus }; + + +async fn stop_container(docker: &Docker, name: &String) -> Result<(), String> { + let container_name = format!("minecraft-{}", name); + + let options = StopContainerOptionsBuilder::new() + .t(60) + .build(); + + docker.stop_container(&container_name, Some(options)).await.map_err(|e| format!("Failed to stop the container : {}", e))?; + Ok(()) +} + +pub async fn stop_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::Running { + return Err(format!("Invalid server state ({}), refusing to stop the server", server.status)); + } + + diesel::update(servers).filter(name.eq(server_name)).set(status.eq(ServerStatus::Stopping)).execute(conn).map_err(|e| format!("Failed to set the server state to stopping : {}", e))?; + + stop_container(docker, server_name).await?; + + diesel::update(servers).filter(name.eq(server_name)).set((redirect_ip.eq(None::), status.eq(ServerStatus::Stopped))).execute(conn).map_err(|e| format!("Failed to set the server state to stopped : {}", e))?; + Ok(()) +} diff --git a/src/dockermgr/utils.rs b/src/dockermgr/utils.rs index 3c2cf67..b5763fc 100644 --- a/src/dockermgr/utils.rs +++ b/src/dockermgr/utils.rs @@ -1,9 +1,12 @@ use std::collections::HashMap; -use bollard::{Docker, plugin::{ContainerInspectResponse, ContainerSummary, ImageId}, query_parameters::{CreateImageOptionsBuilder, ListContainersOptionsBuilder, ListImagesOptionsBuilder}}; +use bollard::{ + Docker, + plugin::{ContainerInspectResponse}, + query_parameters::{CreateImageOptionsBuilder, ListImagesOptionsBuilder} +}; use futures_util::stream::StreamExt; -use tokio::task; pub async fn check_image(docker: &Docker) -> Result<(), String> { let mut filters = HashMap::new(); @@ -54,8 +57,9 @@ pub async fn get_container_ip(docker: &Docker, name: &String) -> Result 0 { - let container = get_container(docker, name).await.unwrap(); - if let Some(network) = container.network_settings.unwrap().networks.unwrap().get("bridge") { + let container = get_container(docker, name).await; + + if let Some(container) = container && let Some(network) = container.network_settings.unwrap().networks.unwrap().get("bridge") { if let Some(ip) = &network.ip_address { return Ok(ip.to_string()); } diff --git a/src/main.rs b/src/main.rs index 3622a12..c12d985 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/09 00:05:23 by tomoron ### ########.fr */ +/* Updated: 2026/06/09 17:04:32 by tomoron ### ########.fr */ /* */ /* ************************************************************************** */ @@ -34,6 +34,8 @@ pub type DbPool = Pool>; use bollard::Docker; +mod migrations; + fn get_connection_pool() -> DbPool { let database_url = "sqlite://".to_string() + &Config::load().config_path + "/db.sqlite"; @@ -59,12 +61,16 @@ fn cli() -> Command { .subcommand(edit::create_subcommand()) .subcommand(ls::create_subcommand()) .subcommand(start::create_subcommand()) + .subcommand(stop::create_subcommand()) } #[tokio::main(flavor = "current_thread")] async fn main() -> Result<(), String> { let arg = cli().get_matches(); let pool = get_connection_pool(); + + migrations::run_migrations(&pool).map_err(|e| format!("Failed to run migrations : {}", e))?; + let docker = Docker::connect_with_defaults().expect("Failed to connect to local docker"); match arg.subcommand() { @@ -75,6 +81,7 @@ async fn main() -> Result<(), String> { Some(("edit", sub_matches)) => { edit::subcommand(sub_matches, pool).await?; }, Some(("start", sub_matches)) => { start::subcommand(sub_matches, &pool, docker).await?; }, + Some(("stop", sub_matches)) => { stop::subcommand(sub_matches, &pool, docker).await?; }, _ => {panic!("subcommand not implemented")} } Ok(()) diff --git a/src/migrations.rs b/src/migrations.rs new file mode 100644 index 0000000..58ae8cf --- /dev/null +++ b/src/migrations.rs @@ -0,0 +1,19 @@ +use std::error::Error; + +use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; + +use crate::DbPool; +pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); + + +pub fn run_migrations(pool: &DbPool) -> Result<(), Box> { + + // This will run the necessary migrations. + // + // See the documentation for `MigrationHarness` for + // all available methods. + let connection = &mut pool.get().unwrap(); + connection.run_pending_migrations(MIGRATIONS)?; + + Ok(()) +} diff --git a/src/models.rs b/src/models.rs index 6c98669..b139693 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,11 +1,8 @@ use std::fmt; -use std::time::SystemTime; use chrono::{DateTime, NaiveDateTime, TimeZone, Utc}; use diesel::prelude::*; -use serde::Deserialize; - use crate::status::ServerStatus; use crate::schema; @@ -16,18 +13,16 @@ use crate::schema; pub struct Servers { pub name: String, pub last_login: Option, - pub container_id: Option, pub status: ServerStatus, pub redirect_ip: Option } -#[derive(Deserialize, Insertable)] +#[derive(Insertable)] #[diesel(table_name = schema::servers)] pub struct CreateServer<'a> { pub name: &'a str, - pub last_login: Option, - pub container_id: Option<&'a str>, + pub last_login: Option, pub status: ServerStatus, pub redirect_ip: Option<&'a str> } diff --git a/src/schema.rs b/src/schema.rs index 50c00b2..10534b7 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -4,7 +4,6 @@ diesel::table! { servers (name) { name -> Text, last_login -> Nullable, - container_id -> Nullable, status -> SmallInt, is_default -> Bool, redirect_ip -> Nullable, diff --git a/src/status.rs b/src/status.rs index e8d6632..a904948 100644 --- a/src/status.rs +++ b/src/status.rs @@ -32,7 +32,7 @@ impl FromSql for ServerStatus{ impl ToSql for ServerStatus { fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> diesel::serialize::Result { let value: i16 = self.clone() as i16; - out.set_value(value.to_le_bytes().to_vec()); + out.set_value(value as i32); Ok(diesel::serialize::IsNull::No) } }