Files
zhenyi c794b818ff feat(config): integrate etcd for service discovery and config management
- Add etcd-client dependency for distributed configuration storage
- Implement EtcdConfig with priority: etcd > environment variables > defaults
- Add ServiceRegistry for service registration with lease keep-alive
- Integrate etcd-based service discovery for appks gRPC connections
- Add service watcher for real-time service instance updates
- Migrate Redis configuration from single URL to cluster node list
- Update Dockerfile with default IMKS_HOST and IMKS_PORT environment variables
- Add etcd bootstrap configuration through environment variables
- Implement Redis cluster URL building with optional authentication
2026-06-11 22:50:38 +08:00

201 lines
6.7 KiB
Rust

//! Server deployment configuration.
//!
//! Reads from environment variables to select adapter (local/redis/nats)
//! and WebTransport settings.
use std::env;
/// Adapter + message bus configuration for multi-node scale-out.
#[derive(Debug, Clone)]
pub struct DeployConfig {
/// "local" | "redis" | "nats"
pub adapter_mode: String,
/// Redis URL for single-node Redis, e.g. `redis://localhost:6379`.
pub redis_url: Option<String>,
/// Whether Redis cluster mode is enabled.
pub redis_cluster_enabled: bool,
/// Redis cluster nodes, comma-separated host:port pairs.
/// Example: "redis1:6379,redis2:6379,redis3:6379"
pub redis_cluster_nodes: String,
/// Redis password (optional).
pub redis_password: String,
/// NATS connection URL (used when adapter_mode = "nats").
pub nats_url: String,
/// Unique server ID for this node.
pub server_id: String,
/// Enable WebTransport server.
pub webtransport_enabled: bool,
/// WebTransport listen port.
pub webtransport_port: u16,
/// TLS certificate path (required for WebTransport).
pub cert_path: String,
/// TLS key path (required for WebTransport).
pub key_path: String,
}
impl DeployConfig {
pub fn from_env() -> Self {
let server_id = env::var("IMKS_SERVER_ID").unwrap_or_else(|_| hostname());
Self {
adapter_mode: env::var("IMKS_ADAPTER").unwrap_or_else(|_| "local".into()),
redis_url: env::var("IMKS_REDIS_URL")
.ok()
.or_else(|| env::var("APP_REDIS_URL").ok())
.filter(|v| !v.trim().is_empty()),
redis_cluster_enabled: env_bool("IMKS_REDIS_CLUSTER_ENABLED")
.or_else(|| env_bool("APP_REDIS_CLUSTER_ENABLED"))
.unwrap_or(false),
redis_cluster_nodes: env::var("IMKS_REDIS_CLUSTER_NODES")
.or_else(|_| env::var("APP_REDIS_CLUSTER_NODES"))
.unwrap_or_default(),
redis_password: env::var("IMKS_REDIS_PASSWORD")
.or_else(|_| env::var("APP_REDIS_PASSWORD"))
.unwrap_or_default(),
nats_url: env::var("IMKS_NATS_URL").unwrap_or_else(|_| "nats://localhost:4222".into()),
server_id,
webtransport_enabled: env::var("IMKS_WT_ENABLED")
.map(|v| v == "true" || v == "1")
.unwrap_or(false),
webtransport_port: env::var("IMKS_WT_PORT")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(3001),
cert_path: env::var("IMKS_WT_CERT_PATH").unwrap_or_default(),
key_path: env::var("IMKS_WT_KEY_PATH").unwrap_or_default(),
}
}
/// Build a redis-rs compatible Redis URL.
///
/// This mirrors appks:
/// - cluster disabled: require `IMKS_REDIS_URL` or `APP_REDIS_URL`
/// - cluster enabled: require `IMKS_REDIS_CLUSTER_NODES` or `APP_REDIS_CLUSTER_NODES`
pub fn redis_url(&self) -> Result<String, String> {
if self.redis_cluster_enabled {
return self.redis_cluster_url();
}
self.redis_url.clone().ok_or_else(|| {
"Redis cluster disabled but IMKS_REDIS_URL/APP_REDIS_URL is not set".into()
})
}
/// Build a redis-cluster URL from cluster_nodes and optional password.
///
/// Produces a redis-rs compatible URL in the format:
/// `redis-cluster://[password@]host1:port1?node=host2:port2&node=host3:port3`
///
/// The first node becomes the URL authority host:port; additional nodes
/// are appended as `node` query parameters.
pub fn redis_cluster_url(&self) -> Result<String, String> {
let auth = if self.redis_password.is_empty() {
String::new()
} else {
format!(":{}@", self.redis_password)
};
let nodes: Vec<String> = self
.redis_cluster_nodes
.split(',')
.map(normalize_redis_cluster_node)
.filter(|s| !s.is_empty())
.collect();
if nodes.is_empty() {
return Err("Redis cluster enabled but IMKS_REDIS_CLUSTER_NODES/APP_REDIS_CLUSTER_NODES is empty".into());
}
if nodes.len() == 1 {
return Ok(format!("redis-cluster://{}{}", auth, nodes[0]));
}
let mut url = format!("redis-cluster://{}{}", auth, nodes[0]);
for (i, node) in nodes.iter().skip(1).enumerate() {
url.push(if i == 0 { '?' } else { '&' });
url.push_str(&format!("node={}", node));
}
Ok(url)
}
}
impl Default for DeployConfig {
fn default() -> Self {
Self::from_env()
}
}
fn env_bool(key: &str) -> Option<bool> {
env::var(key).ok().map(|v| v == "true" || v == "1")
}
fn normalize_redis_cluster_node(node: &str) -> String {
let trimmed = node.trim();
let without_scheme = trimmed
.strip_prefix("redis://")
.or_else(|| trimmed.strip_prefix("rediss://"))
.unwrap_or(trimmed);
let without_auth = without_scheme
.rsplit_once('@')
.map(|(_, host)| host)
.unwrap_or(without_scheme);
without_auth
.split('/')
.next()
.unwrap_or_default()
.to_string()
}
fn hostname() -> String {
env::var("HOSTNAME")
.or_else(|_| env::var("HOST"))
.unwrap_or_else(|_| "imks-node-1".into())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_cluster_nodes_accepts_appks_style_urls() {
assert_eq!(
normalize_redis_cluster_node("redis://127.0.0.1:6380"),
"127.0.0.1:6380"
);
assert_eq!(
normalize_redis_cluster_node("rediss://127.0.0.1:6381/0"),
"127.0.0.1:6381"
);
assert_eq!(
normalize_redis_cluster_node("redis://:secret@127.0.0.1:6382"),
"127.0.0.1:6382"
);
assert_eq!(
normalize_redis_cluster_node("127.0.0.1:6383"),
"127.0.0.1:6383"
);
}
#[test]
fn redis_cluster_url_normalizes_nodes() {
let config = DeployConfig {
adapter_mode: "redis".into(),
redis_url: None,
redis_cluster_enabled: true,
redis_cluster_nodes: "redis://127.0.0.1:6380,redis://127.0.0.1:6381".into(),
redis_password: String::new(),
nats_url: "nats://localhost:4222".into(),
server_id: "test".into(),
webtransport_enabled: false,
webtransport_port: 3001,
cert_path: String::new(),
key_path: String::new(),
};
assert_eq!(
config.redis_cluster_url().unwrap(),
"redis-cluster://127.0.0.1:6380?node=127.0.0.1:6381"
);
}
}