embed migrations, fix start, add stop

This commit is contained in:
2026-06-09 17:09:07 +02:00
parent 5136e0bdc3
commit 408530db00
16 changed files with 212 additions and 26 deletions

85
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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');

View File

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

View File

@ -3,6 +3,7 @@ pub mod edit;
pub mod remove;
pub mod ls;
pub mod start;
pub mod stop;
pub mod env_settings;

View File

@ -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::<String>("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(())
}

22
src/cli/stop.rs Normal file
View File

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

View File

@ -2,3 +2,6 @@ mod start;
pub use start::start_server;
pub mod utils;
mod stop;
pub use stop::stop_server;

View File

@ -48,9 +48,10 @@ async fn start_docker_container(docker: &Docker, name: &String) -> Result<String
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?)
Ok(get_container_ip(docker, &container_name).await?)
}
pub async fn start_server(docker: &Docker, pool: &DbPool, server_name: &String) -> Result<(), String> {
use schema::servers::dsl::*;
@ -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(())
}

41
src/dockermgr/stop.rs Normal file
View File

@ -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::<String>), status.eq(ServerStatus::Stopped))).execute(conn).map_err(|e| format!("Failed to set the server state to stopped : {}", e))?;
Ok(())
}

View File

@ -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<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") {
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());
}

View File

@ -6,7 +6,7 @@
/* By: tomoron <tomoron@student.42angouleme.fr> +#+ +:+ +#+ */
/* +#+#+#+#+#+ +#+ */
/* 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<ConnectionManager<SqliteConnection>>;
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(())

19
src/migrations.rs Normal file
View File

@ -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<dyn Error + Send + Sync + 'static>> {
// 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(())
}

View File

@ -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<NaiveDateTime>,
pub container_id: Option<String>,
pub status: ServerStatus,
pub redirect_ip: Option<String>
}
#[derive(Deserialize, Insertable)]
#[derive(Insertable)]
#[diesel(table_name = schema::servers)]
pub struct CreateServer<'a> {
pub name: &'a str,
pub last_login: Option<SystemTime>,
pub container_id: Option<&'a str>,
pub last_login: Option<NaiveDateTime>,
pub status: ServerStatus,
pub redirect_ip: Option<&'a str>
}

View File

@ -4,7 +4,6 @@ diesel::table! {
servers (name) {
name -> Text,
last_login -> Nullable<Timestamp>,
container_id -> Nullable<Text>,
status -> SmallInt,
is_default -> Bool,
redirect_ip -> Nullable<Text>,

View File

@ -32,7 +32,7 @@ impl FromSql<SmallInt, Sqlite> for ServerStatus{
impl ToSql<SmallInt, Sqlite> 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)
}
}