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
This commit is contained in:
+128
-5
@@ -10,6 +10,10 @@ use std::env;
|
||||
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,
|
||||
@@ -35,9 +39,19 @@ impl DeployConfig {
|
||||
|
||||
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")
|
||||
.unwrap_or_else(|_| "localhost:6379,localhost:6380,localhost:6381".into()),
|
||||
redis_password: env::var("IMKS_REDIS_PASSWORD").unwrap_or_default(),
|
||||
.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")
|
||||
@@ -52,15 +66,56 @@ impl DeployConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// Format: redis-cluster://[:password@]host1:port1,host2:port2,...
|
||||
pub fn redis_cluster_url(&self) -> String {
|
||||
///
|
||||
/// 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)
|
||||
};
|
||||
format!("redis-cluster://{}{}", auth, self.redis_cluster_nodes)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +125,76 @@ impl Default for DeployConfig {
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -34,8 +34,7 @@ impl MessageService {
|
||||
let thread_id: Option<Uuid> = Self::parse_optional(payload, "thread_id")?;
|
||||
let reply_to_message_id: Option<Uuid> =
|
||||
Self::parse_optional(payload, "reply_to_message_id")?;
|
||||
let metadata: Option<serde_json::Value> =
|
||||
Self::parse_optional(payload, "metadata")?;
|
||||
let metadata: Option<serde_json::Value> = Self::parse_optional(payload, "metadata")?;
|
||||
let scheduled_at_str: String = Self::parse_field(payload, "scheduled_at")?;
|
||||
|
||||
let scheduled_at: DateTime<Utc> = chrono::DateTime::parse_from_rfc3339(&scheduled_at_str)
|
||||
|
||||
Reference in New Issue
Block a user