feat(gb-infra): Add service management for MinIO, Stalwart, Zitadel, and NGINX with environment variable handling

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-03-15 18:02:21 -03:00
parent bbb1657e42
commit 0473753001
11 changed files with 443 additions and 2 deletions

11
Cargo.lock generated
View file

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

View file

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

13
gb-infra/Cargo.toml Normal file
View file

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

9
gb-infra/src/lib.rs Normal file
View file

@ -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;
}

60
gb-infra/src/manager.rs Normal file
View file

@ -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<Box<dyn Service>>,
}
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>;
}

View file

@ -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<String, String>,
process: Option<std::process::Child>,
}
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(())
}
}

View file

@ -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<String, String>,
process: Option<std::process::Child>,
}
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(())
}
}

View file

@ -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<String, String>,
process: Option<std::process::Child>,
}
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(())
}
}

View file

@ -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<String, String>,
process: Option<std::process::Child>,
}
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(())
}
}

View file

@ -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<String, String>,
process: Option<std::process::Child>,
}
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(())
}
}

0
gb-infra/src/utils.rs Normal file
View file