feat(config): integrate etcd-based configuration management
- Add etcd-client dependency with TLS support - Implement EtcdConfig struct for reading config values with priority: etcd > env > default - Add ServiceRegistry for service discovery registration in etcd - Create from_etcd method in AppConfig for loading SMTP configuration - Update main.rs to use etcd-based config loading with fallback mechanism - Add etcd module with client connection and key-value operations - Modify Dockerfile to use cargo-chef for faster builds - Add docker-compose.yaml for emailks service deployment - Include AGENTS.md with development guidelines and best practices - Add build.sh script for podman-based container building - Update dependencies in Cargo.toml and Cargo.lock
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use emailks::{
|
||||
config::AppConfig, email::EmailSender, pb::email::v1::email_service_server::EmailServiceServer,
|
||||
queue::EmailQueue, server::EmailServiceImpl,
|
||||
config::AppConfig, email::EmailSender, etcd::{EtcdConfig, ServiceRegistry},
|
||||
pb::email::v1::email_service_server::EmailServiceServer, queue::EmailQueue,
|
||||
server::EmailServiceImpl,
|
||||
};
|
||||
use tonic::transport::Server;
|
||||
use tracing::{error, info};
|
||||
@@ -11,18 +12,66 @@ const DEFAULT_QUEUE_CAPACITY: usize = 1_000;
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()),
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "info".into()),
|
||||
)
|
||||
.init();
|
||||
|
||||
let config = AppConfig::from_env()?;
|
||||
info!(?config.smtp.host, port = config.smtp.port, "smtp config loaded");
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
// Phase 1: read etcd endpoints from env (required to bootstrap etcd)
|
||||
let etcd_endpoints: Vec<String> = std::env::var("ETCD_ENDPOINTS")
|
||||
.unwrap_or_else(|_| "http://localhost:2379".to_string())
|
||||
.split(',')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
let etcd_prefix = std::env::var("ETCD_KEY_PREFIX")
|
||||
.unwrap_or_else(|_| "/appks/".to_string());
|
||||
|
||||
// Phase 2: connect etcd, create config overlay (etcd > env > default)
|
||||
let etcd = EtcdConfig::connect(etcd_endpoints, &etcd_prefix).await?;
|
||||
let listen_addr_str = etcd.get("EMAILKS_LISTEN_ADDR", "127.0.0.1:50051").await;
|
||||
|
||||
// Phase 3: register this service so other services (appks) can discover us
|
||||
let registry = ServiceRegistry::new(etcd.client(), &etcd_prefix);
|
||||
registry.register("emailks", &listen_addr_str).await?;
|
||||
|
||||
// Phase 4: load SMTP config — each key: etcd first, then env, then default
|
||||
let smtp_host = etcd.get("APP_SMTP_HOST", "").await;
|
||||
if smtp_host.is_empty() {
|
||||
return Err("APP_SMTP_HOST is required (set via etcd or env)".into());
|
||||
}
|
||||
let smtp_port: u16 = etcd.get_parsed("APP_SMTP_PORT", 587u16).await;
|
||||
let smtp_from_email = etcd.get("APP_SMTP_FROM_EMAIL", "").await;
|
||||
let smtp_from_name = etcd.get("APP_SMTP_FROM_NAME", "EmailKS").await;
|
||||
let smtp_reply_to = etcd.get("APP_SMTP_REPLY_TO", "").await;
|
||||
let smtp_tls = etcd.get("APP_SMTP_TLS", "starttls").await;
|
||||
let smtp_timeout_secs: u64 = etcd.get_parsed("APP_SMTP_TIMEOUT_SECS", 30u64).await;
|
||||
let smtp_allow_request_from: bool = etcd.get_parsed("APP_SMTP_ALLOW_REQUEST_FROM", false).await;
|
||||
let smtp_username = etcd.get("APP_SMTP_USERNAME", "").await;
|
||||
let smtp_password = etcd.get("APP_SMTP_PASSWORD", "").await;
|
||||
let smtp_helo_name = etcd.get("APP_SMTP_HELO_NAME", "").await;
|
||||
|
||||
let queue_capacity: Option<usize> = {
|
||||
let s = etcd.get("APP_SMTP_QUEUE_CAPACITY", "").await;
|
||||
if s.is_empty() { None } else { s.parse().ok() }
|
||||
};
|
||||
|
||||
let config = AppConfig::from_etcd(
|
||||
smtp_host, smtp_port, smtp_username, smtp_password,
|
||||
smtp_from_email, smtp_from_name, smtp_reply_to,
|
||||
smtp_tls, smtp_timeout_secs, smtp_helo_name, smtp_allow_request_from,
|
||||
queue_capacity,
|
||||
&listen_addr_str,
|
||||
)?;
|
||||
|
||||
info!(host = %config.smtp.host, port = config.smtp.port, "smtp config loaded (etcd priority)");
|
||||
|
||||
let sender = EmailSender::new(config.smtp)?;
|
||||
let (queue, worker) = match config.queue_capacity {
|
||||
// `Some(0)` explicitly opts into an unbounded queue (mainly for testing).
|
||||
Some(0) => {
|
||||
info!("creating unbounded queue by explicit configuration");
|
||||
info!("creating unbounded queue");
|
||||
EmailQueue::unbounded()
|
||||
}
|
||||
Some(cap) => {
|
||||
@@ -30,10 +79,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
EmailQueue::bounded(cap)
|
||||
}
|
||||
None => {
|
||||
info!(
|
||||
capacity = DEFAULT_QUEUE_CAPACITY,
|
||||
"creating bounded queue with default capacity"
|
||||
);
|
||||
info!(capacity = DEFAULT_QUEUE_CAPACITY, "creating bounded queue (default)");
|
||||
EmailQueue::bounded(DEFAULT_QUEUE_CAPACITY)
|
||||
}
|
||||
};
|
||||
@@ -60,7 +106,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.await?;
|
||||
|
||||
info!("server stopped");
|
||||
|
||||
if let Err(e) = worker_handle.await {
|
||||
tracing::error!(error = %e, "worker task panicked");
|
||||
}
|
||||
@@ -70,7 +115,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
async fn shutdown_signal() {
|
||||
match tokio::signal::ctrl_c().await {
|
||||
Ok(()) => info!("shutdown signal received, draining..."),
|
||||
Err(err) => error!(%err, "failed to install CTRL+C handler, shutting down"),
|
||||
Ok(()) => info!("shutdown signal received"),
|
||||
Err(err) => error!(%err, "failed to install CTRL+C handler"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user