240 lines
8 KiB
Rust
240 lines
8 KiB
Rust
use std::collections::HashMap;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use anyhow::Result;
|
|
use chainfire_client::Client;
|
|
use serde::{Deserialize, Serialize};
|
|
use tokio::sync::RwLock;
|
|
use tracing::{info, warn};
|
|
|
|
const PHOTON_PREFIX: &str = "photoncloud";
|
|
const CACHE_TTL: Duration = Duration::from_secs(30);
|
|
const POLICY_CACHE_TTL: Duration = Duration::from_secs(30);
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ServiceInstance {
|
|
pub instance_id: String,
|
|
pub service: String,
|
|
pub node_id: String,
|
|
pub ip: String,
|
|
pub port: u16,
|
|
#[serde(default)]
|
|
pub mesh_port: Option<u16>,
|
|
#[serde(default)]
|
|
pub version: Option<String>,
|
|
#[serde(default)]
|
|
pub state: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MtlsPolicy {
|
|
pub policy_id: String,
|
|
#[serde(default)]
|
|
pub environment: Option<String>,
|
|
pub source_service: String,
|
|
pub target_service: String,
|
|
#[serde(default)]
|
|
pub mtls_required: Option<bool>,
|
|
#[serde(default)]
|
|
pub mode: Option<String>,
|
|
}
|
|
|
|
struct CachedInstances {
|
|
instances: Vec<ServiceInstance>,
|
|
updated_at: Instant,
|
|
}
|
|
|
|
struct CachedPolicy {
|
|
policy: MtlsPolicy,
|
|
updated_at: Instant,
|
|
}
|
|
|
|
pub struct ServiceDiscovery {
|
|
chainfire_endpoint: String,
|
|
cluster_id: String,
|
|
cache: Arc<RwLock<HashMap<String, CachedInstances>>>,
|
|
policy_cache: Arc<RwLock<HashMap<String, CachedPolicy>>>,
|
|
}
|
|
|
|
impl ServiceDiscovery {
|
|
pub fn new(chainfire_endpoint: String, cluster_id: String) -> Self {
|
|
Self {
|
|
chainfire_endpoint,
|
|
cluster_id,
|
|
cache: Arc::new(RwLock::new(HashMap::new())),
|
|
policy_cache: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
|
|
pub async fn resolve_service(&self, service_name: &str) -> Result<Vec<ServiceInstance>> {
|
|
// キャッシュをチェック
|
|
{
|
|
let cache = self.cache.read().await;
|
|
if let Some(cached) = cache.get(service_name) {
|
|
if cached.updated_at.elapsed() < CACHE_TTL {
|
|
return Ok(cached.instances.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Chainfireから取得
|
|
let instances = self.fetch_instances_from_chainfire(service_name).await?;
|
|
|
|
// キャッシュを更新
|
|
{
|
|
let mut cache = self.cache.write().await;
|
|
cache.insert(
|
|
service_name.to_string(),
|
|
CachedInstances {
|
|
instances: instances.clone(),
|
|
updated_at: Instant::now(),
|
|
},
|
|
);
|
|
}
|
|
|
|
Ok(instances)
|
|
}
|
|
|
|
async fn fetch_instances_from_chainfire(&self, service_name: &str) -> Result<Vec<ServiceInstance>> {
|
|
let mut client = Client::connect(self.chainfire_endpoint.clone()).await?;
|
|
let prefix = format!(
|
|
"{}instances/{}/",
|
|
cluster_prefix(&self.cluster_id),
|
|
service_name
|
|
);
|
|
let prefix_bytes = prefix.as_bytes();
|
|
|
|
let (kvs, _) = client.scan_prefix(prefix_bytes, 0).await?;
|
|
let mut instances = Vec::new();
|
|
|
|
for (_, value, _) in kvs {
|
|
match serde_json::from_slice::<ServiceInstance>(&value) {
|
|
Ok(inst) => {
|
|
// 状態が "healthy" または未設定のもののみ返す
|
|
if inst.state.as_deref().unwrap_or("healthy") == "healthy" {
|
|
instances.push(inst);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
warn!(error = %e, "failed to parse ServiceInstance from Chainfire");
|
|
}
|
|
}
|
|
}
|
|
|
|
info!(
|
|
service = %service_name,
|
|
count = instances.len(),
|
|
"resolved service instances from Chainfire"
|
|
);
|
|
|
|
Ok(instances)
|
|
}
|
|
|
|
pub async fn get_mtls_policy(
|
|
&self,
|
|
source_service: &str,
|
|
target_service: &str,
|
|
) -> Result<Option<MtlsPolicy>> {
|
|
let policy_key = format!(
|
|
"{}-{}",
|
|
source_service, target_service
|
|
);
|
|
|
|
// キャッシュをチェック
|
|
{
|
|
let cache = self.policy_cache.read().await;
|
|
if let Some(cached) = cache.get(&policy_key) {
|
|
if cached.updated_at.elapsed() < POLICY_CACHE_TTL {
|
|
return Ok(Some(cached.policy.clone()));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Chainfireから取得
|
|
let mut client = Client::connect(self.chainfire_endpoint.clone()).await?;
|
|
let prefix = format!(
|
|
"{}mtls/policies/",
|
|
cluster_prefix(&self.cluster_id)
|
|
);
|
|
let prefix_bytes = prefix.as_bytes();
|
|
|
|
let (kvs, _) = client.scan_prefix(prefix_bytes, 0).await?;
|
|
|
|
for (_, value, _) in kvs {
|
|
match serde_json::from_slice::<MtlsPolicy>(&value) {
|
|
Ok(policy) => {
|
|
if policy.source_service == source_service && policy.target_service == target_service {
|
|
// キャッシュに保存
|
|
let mut cache = self.policy_cache.write().await;
|
|
cache.insert(
|
|
policy_key.clone(),
|
|
CachedPolicy {
|
|
policy: policy.clone(),
|
|
updated_at: Instant::now(),
|
|
},
|
|
);
|
|
return Ok(Some(policy));
|
|
}
|
|
}
|
|
Err(e) => {
|
|
warn!(error = %e, "failed to parse MtlsPolicy from Chainfire");
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
pub async fn start_background_refresh(&self) {
|
|
let endpoint = self.chainfire_endpoint.clone();
|
|
let cluster_id = self.cluster_id.clone();
|
|
let cache = Arc::clone(&self.cache);
|
|
let policy_cache = Arc::clone(&self.policy_cache);
|
|
|
|
tokio::spawn(async move {
|
|
let mut interval = tokio::time::interval(Duration::from_secs(60));
|
|
loop {
|
|
interval.tick().await;
|
|
|
|
// 全サービスのインスタンスをリフレッシュ
|
|
if let Ok(mut client) = Client::connect(endpoint.clone()).await {
|
|
let prefix = format!("{}instances/", cluster_prefix(&cluster_id));
|
|
if let Ok((kvs, _)) = client.scan_prefix(prefix.as_bytes(), 0).await {
|
|
let mut service_map: HashMap<String, Vec<ServiceInstance>> = HashMap::new();
|
|
for (_key, value, _) in kvs {
|
|
if let Ok(inst) = serde_json::from_slice::<ServiceInstance>(&value) {
|
|
if inst.state.as_deref().unwrap_or("healthy") == "healthy" {
|
|
service_map
|
|
.entry(inst.service.clone())
|
|
.or_insert_with(Vec::new)
|
|
.push(inst);
|
|
}
|
|
}
|
|
}
|
|
let mut cache_guard = cache.write().await;
|
|
for (service, instances) in service_map {
|
|
cache_guard.insert(
|
|
service,
|
|
CachedInstances {
|
|
instances,
|
|
updated_at: Instant::now(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ポリシーキャッシュはTTLベースでクリア
|
|
{
|
|
let mut policy_guard = policy_cache.write().await;
|
|
policy_guard.retain(|_, cached| cached.updated_at.elapsed() < POLICY_CACHE_TTL);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
fn cluster_prefix(cluster_id: &str) -> String {
|
|
format!("{}/clusters/{}/", PHOTON_PREFIX, cluster_id)
|
|
}
|