#![cfg(feature = "real-consul")]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, thiserror::Error)]
pub enum ConsulError {
#[error("HTTP error: {0}")]
Http(#[from] reqwest::Error),
#[error("Consul API error: status={status}, body={body}")]
Api { status: u16, body: String },
#[error("Config not found: {0}")]
NotFound(String),
#[error("Invalid response: {0}")]
InvalidResponse(String),
}
#[derive(Debug, Clone)]
pub struct ConsulConfig {
pub endpoint: String,
pub acl_token: Option<String>,
pub datacenter: Option<String>,
pub timeout_secs: u64,
}
impl Default for ConsulConfig {
fn default() -> Self {
Self {
endpoint: "http://127.0.0.1:8500".to_string(),
acl_token: None,
datacenter: None,
timeout_secs: 10,
}
}
}
impl ConsulConfig {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
..Default::default()
}
}
pub fn with_acl_token(mut self, token: impl Into<String>) -> Self {
self.acl_token = Some(token.into());
self
}
pub fn with_datacenter(mut self, dc: impl Into<String>) -> Self {
self.datacenter = Some(dc.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsulKvEntry {
#[serde(rename = "Key")]
pub key: String,
#[serde(rename = "Value")]
pub value: Option<String>,
#[serde(rename = "CreateIndex")]
pub create_index: Option<u64>,
#[serde(rename = "ModifyIndex")]
pub modify_index: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsulServiceRegistration {
#[serde(rename = "ID", skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "Name")]
pub name: String,
#[serde(rename = "Address", skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(rename = "Port", skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
#[serde(rename = "Tags", skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(rename = "Meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, String>>,
}
pub struct ConsulClient {
config: ConsulConfig,
http: reqwest::Client,
}
impl ConsulClient {
pub fn new(config: ConsulConfig) -> Result<Self, ConsulError> {
let http = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(config.timeout_secs))
.build()?;
Ok(Self { config, http })
}
fn build_url(&self, path: &str) -> String {
format!("{}{}", self.config.endpoint, path)
}
fn add_auth(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
if let Some(token) = &self.config.acl_token {
req.header("X-Consul-Token", token)
} else {
req
}
}
fn add_dc(&self, req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
if let Some(dc) = &self.config.datacenter {
req.query(&[("dc", dc)])
} else {
req
}
}
pub async fn get_config(&self, key: &str) -> Result<String, ConsulError> {
let url = self.build_url(&format!("/v1/kv/{}", key));
let req = self.http.get(&url);
let req = self.add_auth(req);
let req = self.add_dc(req);
let resp = req.send().await?;
if resp.status() == reqwest::StatusCode::NOT_FOUND {
return Err(ConsulError::NotFound(key.to_string()));
}
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
let entries: Vec<ConsulKvEntry> = resp.json().await?;
if entries.is_empty() {
return Err(ConsulError::NotFound(key.to_string()));
}
let encoded = entries[0]
.value
.as_ref()
.ok_or_else(|| ConsulError::InvalidResponse("missing Value field".into()))?;
let decoded = base64_decode(encoded)?;
Ok(decoded)
}
pub async fn set_config(&self, key: &str, value: &str) -> Result<(), ConsulError> {
let url = self.build_url(&format!("/v1/kv/{}", key));
let req = self.http.put(&url).body(value.to_string());
let req = self.add_auth(req);
let req = self.add_dc(req);
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
Ok(())
}
pub async fn delete_config(&self, key: &str) -> Result<(), ConsulError> {
let url = self.build_url(&format!("/v1/kv/{}", key));
let req = self.http.delete(&url);
let req = self.add_auth(req);
let req = self.add_dc(req);
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
Ok(())
}
pub async fn list_keys(&self, prefix: &str) -> Result<Vec<String>, ConsulError> {
let url = self.build_url(&format!("/v1/kv/{}?keys", prefix));
let req = self.http.get(&url);
let req = self.add_auth(req);
let req = self.add_dc(req);
let resp = req.send().await?;
if resp.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(Vec::new());
}
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
let keys: Vec<String> = resp.json().await?;
Ok(keys)
}
pub async fn watch(
&self,
key: &str,
last_index: u64,
wait_secs: u64,
) -> Result<(String, u64), ConsulError> {
let url = self.build_url(&format!("/v1/kv/{}", key));
let req = self.http.get(&url).query(&[
("index", last_index.to_string()),
("wait", format!("{}s", wait_secs)),
]);
let req = self.add_auth(req);
let req = self.add_dc(req);
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
let new_index = resp
.headers()
.get("X-Consul-Index")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(last_index);
let entries: Vec<ConsulKvEntry> = resp.json().await?;
if entries.is_empty() {
return Err(ConsulError::NotFound(key.to_string()));
}
let encoded = entries[0]
.value
.as_ref()
.ok_or_else(|| ConsulError::InvalidResponse("missing Value field".into()))?;
let decoded = base64_decode(encoded)?;
Ok((decoded, new_index))
}
pub async fn register_service(
&self,
service: ConsulServiceRegistration,
) -> Result<(), ConsulError> {
let url = self.build_url("/v1/catalog/register");
let body = serde_json::json!({
"Datacenter": self.config.datacenter,
"Node": "sz-orm-auto",
"Address": service.address.as_deref().unwrap_or("127.0.0.1"),
"Service": {
"ID": service.id,
"Service": service.name,
"Address": service.address,
"Port": service.port,
"Tags": service.tags,
"Meta": service.meta,
}
});
let req = self.http.put(&url).json(&body);
let req = self.add_auth(req);
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
Ok(())
}
pub async fn deregister_service(
&self,
service_id: &str,
node: &str,
) -> Result<(), ConsulError> {
let url = self.build_url("/v1/catalog/deregister");
let body = serde_json::json!({
"Datacenter": self.config.datacenter,
"Node": node,
"ServiceID": service_id,
});
let req = self.http.put(&url).json(&body);
let req = self.add_auth(req);
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
Ok(())
}
pub async fn discover_service(
&self,
service_name: &str,
) -> Result<Vec<ConsulServiceInstance>, ConsulError> {
let url = self.build_url(&format!("/v1/health/service/{}", service_name));
let req = self.http.get(&url);
let req = self.add_auth(req);
let req = self.add_dc(req);
let resp = req.send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(ConsulError::Api { status, body });
}
let instances: Vec<ConsulServiceInstance> = resp.json().await?;
Ok(instances)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConsulServiceInstance {
#[serde(rename = "Node")]
pub node: String,
#[serde(rename = "Address")]
pub address: String,
#[serde(rename = "ServiceID")]
pub service_id: String,
#[serde(rename = "ServiceName")]
pub service_name: String,
#[serde(rename = "ServiceAddress")]
pub service_address: String,
#[serde(rename = "ServicePort")]
pub service_port: u16,
#[serde(rename = "ServiceTags", default)]
pub service_tags: Vec<String>,
}
pub struct RealConsulConfigCenter {
client: ConsulClient,
runtime: tokio::runtime::Runtime,
cache: std::collections::HashMap<String, String>,
subscribers: std::collections::HashMap<String, Vec<crate::ConfigChangeCallback>>,
events: std::sync::Mutex<Vec<crate::ConfigChangeEvent>>,
}
impl RealConsulConfigCenter {
pub fn new(config: ConsulConfig) -> Result<Self, ConsulError> {
let client = ConsulClient::new(config)?;
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| ConsulError::InvalidResponse(format!("build runtime: {}", e)))?;
Ok(Self {
client,
runtime,
cache: std::collections::HashMap::new(),
subscribers: std::collections::HashMap::new(),
events: std::sync::Mutex::new(Vec::new()),
})
}
fn notify(&self, key: &str, value: &str, deleted: bool) {
if let Some(callbacks) = self.subscribers.get(key) {
for cb in callbacks {
cb(key, value);
}
}
if let Ok(mut events) = self.events.lock() {
events.push(crate::ConfigChangeEvent {
key: key.to_string(),
value: value.to_string(),
deleted,
});
}
}
pub fn events(&self) -> Vec<crate::ConfigChangeEvent> {
self.events.lock().map(|e| e.clone()).unwrap_or_default()
}
}
impl crate::ConfigCenter for RealConsulConfigCenter {
fn get(&self, key: &str) -> Option<String> {
self.runtime.block_on(self.client.get_config(key)).ok()
}
fn set(&mut self, key: &str, value: &str) {
if let Err(e) = self.runtime.block_on(self.client.set_config(key, value)) {
eprintln!("RealConsulConfigCenter::set error: {}", e);
return;
}
self.cache.insert(key.to_string(), value.to_string());
self.notify(key, value, false);
}
fn delete(&mut self, key: &str) -> bool {
match self.runtime.block_on(self.client.delete_config(key)) {
Ok(()) => {
self.cache.remove(key);
self.notify(key, "", true);
true
}
Err(_) => false,
}
}
fn exists(&self, key: &str) -> bool {
self.runtime.block_on(self.client.get_config(key)).is_ok()
}
fn list(&self) -> Vec<String> {
match self.runtime.block_on(self.client.list_keys("")) {
Ok(keys) => {
let mut sorted = keys;
sorted.sort();
sorted
}
Err(_) => {
let mut keys: Vec<String> = self.cache.keys().cloned().collect();
keys.sort();
keys
}
}
}
fn watch(&self, _key: &str) -> bool {
true
}
fn subscribe(&mut self, key: &str, callback: crate::ConfigChangeCallback) {
self.subscribers
.entry(key.to_string())
.or_default()
.push(callback);
}
}
fn base64_decode(s: &str) -> Result<String, ConsulError> {
use base64::Engine;
let decoded = base64::engine::general_purpose::STANDARD
.decode(s)
.map_err(|e| ConsulError::InvalidResponse(format!("base64 decode error: {}", e)))?;
String::from_utf8(decoded)
.map_err(|e| ConsulError::InvalidResponse(format!("UTF-8 decode error: {}", e)))
}