From 0473753001ba458ee62ed800f053ef028dd3e060 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sat, 15 Mar 2025 18:02:21 -0300 Subject: [PATCH] feat(gb-infra): Add service management for MinIO, Stalwart, Zitadel, and NGINX with environment variable handling --- Cargo.lock | 11 ++++ Cargo.toml | 5 +- gb-infra/Cargo.toml | 13 +++++ gb-infra/src/lib.rs | 9 +++ gb-infra/src/manager.rs | 60 +++++++++++++++++++ gb-infra/src/services/minio.rs | 54 +++++++++++++++++ gb-infra/src/services/nginx.rs | 83 +++++++++++++++++++++++++++ gb-infra/src/services/postgresql.rs | 89 +++++++++++++++++++++++++++++ gb-infra/src/services/stalwart.rs | 58 +++++++++++++++++++ gb-infra/src/services/zitadel.rs | 63 ++++++++++++++++++++ gb-infra/src/utils.rs | 0 11 files changed, 443 insertions(+), 2 deletions(-) create mode 100644 gb-infra/Cargo.toml create mode 100644 gb-infra/src/lib.rs create mode 100644 gb-infra/src/manager.rs create mode 100644 gb-infra/src/services/minio.rs create mode 100644 gb-infra/src/services/nginx.rs create mode 100644 gb-infra/src/services/postgresql.rs create mode 100644 gb-infra/src/services/stalwart.rs create mode 100644 gb-infra/src/services/zitadel.rs create mode 100644 gb-infra/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 5994d8a..b2e0d21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2926,6 +2926,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "gb-infra" +version = "0.1.0" +dependencies = [ + "ctrlc", + "dotenv", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "gb-llm" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a7fd4c4..cb90b99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ members = [ "gb-document", "gb-file", "gb-llm", - "gb-calendar", + "gb-calendar", "gb-infra", ] [workspace.package] @@ -41,6 +41,7 @@ parking_lot = "0.12" bytes = "1.0" log = "0.4" env_logger = "0.10" +ctrlc = "3.2" # Web framework and servers axum = { version = "0.7.9", features = ["ws", "multipart"] } @@ -136,4 +137,4 @@ docx = "1.1" zip = "0.6" [workspace.metadata] -msrv = "1.70.0" \ No newline at end of file +msrv = "1.70.0" diff --git a/gb-infra/Cargo.toml b/gb-infra/Cargo.toml new file mode 100644 index 0000000..aa75cb1 --- /dev/null +++ b/gb-infra/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "gb-infra" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +dotenv = { workspace = true } +ctrlc = { workspace = true } +tokio = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } diff --git a/gb-infra/src/lib.rs b/gb-infra/src/lib.rs new file mode 100644 index 0000000..f987a8a --- /dev/null +++ b/gb-infra/src/lib.rs @@ -0,0 +1,9 @@ +pub mod manager; +pub mod utils; +pub mod services { + pub mod minio; + pub mod nginx; + pub mod postgresql; + pub mod stalwart; + pub mod zitadel; +} \ No newline at end of file diff --git a/gb-infra/src/manager.rs b/gb-infra/src/manager.rs new file mode 100644 index 0000000..d125d38 --- /dev/null +++ b/gb-infra/src/manager.rs @@ -0,0 +1,60 @@ +use crate::services::{zitadel, stalwart, minio, postgresql, nginx}; +use dotenv::dotenv; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +pub struct ServiceManager { + services: Vec>, +} + +impl ServiceManager { + pub fn new() -> Self { + dotenv().ok(); + ServiceManager { + services: vec![ + Box::new(zitadel::Zitadel::new()), + Box::new(stalwart::Stalwart::new()), + Box::new(minio::MinIO::new()), + Box::new(postgresql::PostgreSQL::new()), + Box::new(nginx::NGINX::new()), + ], + } + } + + pub fn start(&mut self) { + for service in &mut self.services { + service.start().unwrap(); + } + } + + pub fn stop(&mut self) { + for service in &mut self.services { + service.stop().unwrap(); + } + } + + pub fn run(&mut self) { + self.start(); + let running = Arc::new(Mutex::new(true)); + let running_clone = Arc::clone(&running); + + ctrlc::set_handler(move || { + println!("Exiting service manager..."); + let mut running = running_clone.lock().unwrap(); + *running = false; + }) + .expect("Failed to set Ctrl+C handler."); + + while *running.lock().unwrap() { + thread::sleep(Duration::from_secs(1)); + } + + self.stop(); + } +} + +pub trait Service { + fn start(&mut self) -> Result<(), String>; + fn stop(&mut self) -> Result<(), String>; +} diff --git a/gb-infra/src/services/minio.rs b/gb-infra/src/services/minio.rs new file mode 100644 index 0000000..b46bdd4 --- /dev/null +++ b/gb-infra/src/services/minio.rs @@ -0,0 +1,54 @@ +use crate::manager::Service; +use std::env; +use std::process::Command; +use std::collections::HashMap; +use dotenv::dotenv; + +pub struct MinIO { + env_vars: HashMap, + process: Option, +} + +impl MinIO { + pub fn new() -> Self { + dotenv().ok(); + let env_vars = vec![ + "MINIO_ROOT_USER", + "MINIO_ROOT_PASSWORD", + "MINIO_VOLUMES", + "MINIO_ADDRESS", + ] + .into_iter() + .filter_map(|key| env::var(key).ok().map(|value| (key.to_string(), value))) + .collect(); + + MinIO { + env_vars, + process: None, + } + } +} + +impl Service for MinIO { + fn start(&mut self) -> Result<(), String> { + if self.process.is_some() { + return Err("MinIO is already running.".to_string()); + } + + let mut command = Command::new("/opt/gbo/bin/minio"); + for (key, value) in &self.env_vars { + command.env(key, value); + } + + self.process = Some(command.spawn().map_err(|e| e.to_string())?); + Ok(()) + } + + fn stop(&mut self) -> Result<(), String> { + if let Some(mut child) = self.process.take() { + child.kill().map_err(|e| e.to_string())?; + child.wait().map_err(|e| e.to_string())?; + } + Ok(()) + } +} diff --git a/gb-infra/src/services/nginx.rs b/gb-infra/src/services/nginx.rs new file mode 100644 index 0000000..48a7be1 --- /dev/null +++ b/gb-infra/src/services/nginx.rs @@ -0,0 +1,83 @@ +use crate::manager::Service; +use std::env; +use std::process::Command; +use std::collections::HashMap; +use dotenv::dotenv; + +pub struct NGINX { + env_vars: HashMap, + process: Option, +} + +impl NGINX { + pub fn new() -> Self { + dotenv().ok(); + let env_vars = vec![ + "NGINX_ERROR_LOG", + "NGINX_ACCESS_LOG", + ] + .into_iter() + .filter_map(|key| env::var(key).ok().map(|value| (key.to_string(), value))) + .collect(); + + NGINX { + env_vars, + process: None, + } + } +} + +impl Service for NGINX { + fn start(&mut self) -> Result<(), String> { + if self.process.is_some() { + return Err("NGINX is already running.".to_string()); + } + + // Configure NGINX logs + let error_log = self.env_vars.get("NGINX_ERROR_LOG").unwrap(); + let access_log = self.env_vars.get("NGINX_ACCESS_LOG").unwrap(); + + // Update NGINX configuration + let nginx_conf = format!( + r#" +error_log {} debug; +access_log {}; +events {{}} +http {{ + server {{ + listen 80; + server_name localhost; + location / {{ + root /var/www/html; + }} + }} +}} +"#, + error_log, access_log + ); + + // Write the configuration to /etc/nginx/nginx.conf + std::fs::write("/etc/nginx/nginx.conf", nginx_conf).map_err(|e| e.to_string())?; + + // Start NGINX + let mut command = Command::new("nginx"); + self.process = Some(command.spawn().map_err(|e| e.to_string())?); + Ok(()) + } + + fn stop(&mut self) -> Result<(), String> { + if let Some(mut child) = self.process.take() { + child.kill().map_err(|e| e.to_string())?; + child.wait().map_err(|e| e.to_string())?; + } + + // Stop NGINX + Command::new("nginx") + .arg("-s") + .arg("stop") + .status() + .map_err(|e| e.to_string())?; + + Ok(()) + } +} \ No newline at end of file diff --git a/gb-infra/src/services/postgresql.rs b/gb-infra/src/services/postgresql.rs new file mode 100644 index 0000000..c29ac99 --- /dev/null +++ b/gb-infra/src/services/postgresql.rs @@ -0,0 +1,89 @@ +use crate::manager::Service; +use std::env; +use std::process::Command; +use std::collections::HashMap; +use dotenv::dotenv; + +pub struct PostgreSQL { + env_vars: HashMap, + process: Option, +} + +impl PostgreSQL { + pub fn new() -> Self { + dotenv().ok(); + let env_vars = vec![ + "POSTGRES_DATA_DIR", + "POSTGRES_PORT", + "POSTGRES_USER", + "POSTGRES_PASSWORD", + ] + .into_iter() + .filter_map(|key| env::var(key).ok().map(|value| (key.to_string(), value))) + .collect(); + + PostgreSQL { + env_vars, + process: None, + } + } +} + +impl Service for PostgreSQL { + fn start(&mut self) -> Result<(), String> { + if self.process.is_some() { + return Err("PostgreSQL is already running.".to_string()); + } + + // Initialize PostgreSQL data directory if it doesn't exist + let data_dir = self.env_vars.get("POSTGRES_DATA_DIR").unwrap(); + if !std::path::Path::new(data_dir).exists() { + Command::new("sudo") + .arg("-u") + .arg("postgres") + .arg("/usr/lib/postgresql/14/bin/initdb") + .arg("-D") + .arg(data_dir) + .status() + .map_err(|e| e.to_string())?; + } + + // Start PostgreSQL + let mut command = Command::new("sudo"); + command + .arg("-u") + .arg("postgres") + .arg("/usr/lib/postgresql/14/bin/pg_ctl") + .arg("start") + .arg("-D") + .arg(data_dir); + + for (key, value) in &self.env_vars { + command.env(key, value); + } + + self.process = Some(command.spawn().map_err(|e| e.to_string())?); + Ok(()) + } + + fn stop(&mut self) -> Result<(), String> { + if let Some(mut child) = self.process.take() { + child.kill().map_err(|e| e.to_string())?; + child.wait().map_err(|e| e.to_string())?; + } + + // Stop PostgreSQL + let data_dir = self.env_vars.get("POSTGRES_DATA_DIR").unwrap(); + Command::new("sudo") + .arg("-u") + .arg("postgres") + .arg("/usr/lib/postgresql/14/bin/pg_ctl") + .arg("stop") + .arg("-D") + .arg(data_dir) + .status() + .map_err(|e| e.to_string())?; + + Ok(()) + } +} \ No newline at end of file diff --git a/gb-infra/src/services/stalwart.rs b/gb-infra/src/services/stalwart.rs new file mode 100644 index 0000000..89dff63 --- /dev/null +++ b/gb-infra/src/services/stalwart.rs @@ -0,0 +1,58 @@ +use crate::manager::Service; +use std::env; +use std::process::Command; +use std::collections::HashMap; +use dotenv::dotenv; + +pub struct Stalwart { + env_vars: HashMap, + process: Option, +} + +impl Stalwart { + pub fn new() -> Self { + dotenv().ok(); + let env_vars = vec![ + "STALWART_LOG_LEVEL", + "STALWART_OAUTH_PROVIDER", + "STALWART_OAUTH_CLIENT_ID", + "STALWART_OAUTH_CLIENT_SECRET", + "STALWART_OAUTH_AUTHORIZATION_ENDPOINT", + "STALWART_OAUTH_TOKEN_ENDPOINT", + "STALWART_OAUTH_USERINFO_ENDPOINT", + "STALWART_OAUTH_SCOPE", + ] + .into_iter() + .filter_map(|key| env::var(key).ok().map(|value| (key.to_string(), value))) + .collect(); + + Stalwart { + env_vars, + process: None, + } + } +} + +impl Service for Stalwart { + fn start(&mut self) -> Result<(), String> { + if self.process.is_some() { + return Err("Stalwart Mail is already running.".to_string()); + } + + let mut command = Command::new("/opt/gbo/bin/stalwart"); + for (key, value) in &self.env_vars { + command.env(key, value); + } + + self.process = Some(command.spawn().map_err(|e| e.to_string())?); + Ok(()) + } + + fn stop(&mut self) -> Result<(), String> { + if let Some(mut child) = self.process.take() { + child.kill().map_err(|e| e.to_string())?; + child.wait().map_err(|e| e.to_string())?; + } + Ok(()) + } +} diff --git a/gb-infra/src/services/zitadel.rs b/gb-infra/src/services/zitadel.rs new file mode 100644 index 0000000..ce5e22b --- /dev/null +++ b/gb-infra/src/services/zitadel.rs @@ -0,0 +1,63 @@ +use crate::manager::Service; +use std::env; +use std::process::Command; +use std::collections::HashMap; +use dotenv::dotenv; + +pub struct Zitadel { + env_vars: HashMap, + process: Option, +} + +impl Zitadel { + pub fn new() -> Self { + dotenv().ok(); + let env_vars = vec![ + "ZITADEL_DEFAULTINSTANCE_INSTANCENAME", + "ZITADEL_DEFAULTINSTANCE_ORG_NAME", + "ZITADEL_DATABASE_POSTGRES_HOST", + "ZITADEL_DATABASE_POSTGRES_PORT", + "ZITADEL_DATABASE_POSTGRES_DATABASE", + "ZITADEL_DATABASE_POSTGRES_USER_USERNAME", + "ZITADEL_DATABASE_POSTGRES_USER_PASSWORD", + "ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE", + "ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE", + "ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME", + "ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD", + "ZITADEL_EXTERNALSECURE", + "ZITADEL_MASTERKEY", + ] + .into_iter() + .filter_map(|key| env::var(key).ok().map(|value| (key.to_string(), value))) + .collect(); + + Zitadel { + env_vars, + process: None, + } + } +} + +impl Service for Zitadel { + fn start(&mut self) -> Result<(), String> { + if self.process.is_some() { + return Err("Zitadel is already running.".to_string()); + } + + let mut command = Command::new("/opt/gbo/bin/zitadel"); + for (key, value) in &self.env_vars { + command.env(key, value); + } + + self.process = Some(command.spawn().map_err(|e| e.to_string())?); + Ok(()) + } + + fn stop(&mut self) -> Result<(), String> { + if let Some(mut child) = self.process.take() { + child.kill().map_err(|e| e.to_string())?; + child.wait().map_err(|e| e.to_string())?; + } + Ok(()) + } +} diff --git a/gb-infra/src/utils.rs b/gb-infra/src/utils.rs new file mode 100644 index 0000000..e69de29