use crate::{agent::AgentState, config::PolicyConfig, model::JwtPolicy, JwtPolicyView};
use ankurah::proto::EntityId;
use ankurah::Context;
use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use tracing::{info, warn};
pub struct PolicyWatcher {
entity_id: EntityId,
watch_handle: JoinHandle<()>,
}
impl PolicyWatcher {
pub async fn start(path: impl AsRef<Path>, context: Context, shared_state: Arc<RwLock<AgentState>>) -> Result<Self, anyhow::Error> {
let path = path.as_ref().to_path_buf();
let json_str = tokio::fs::read_to_string(&path).await?;
let parsed_config: PolicyConfig = serde_json::from_str(&json_str)?;
{
let mut guard = shared_state.write().unwrap_or_else(|e| e.into_inner());
guard.config = parsed_config;
}
let public_key_pem = extract_public_key_pem(&shared_state);
let entity_id = upsert_entity(&context, &json_str, &public_key_pem).await?;
let watch_entity_id = entity_id.clone();
let watch_handle = tokio::spawn(watch_loop(path, context, watch_entity_id, shared_state));
Ok(Self { entity_id, watch_handle })
}
pub fn entity_id(&self) -> &EntityId { &self.entity_id }
pub fn stop(self) { self.watch_handle.abort(); }
pub fn handle(&self) -> &JoinHandle<()> { &self.watch_handle }
}
impl Drop for PolicyWatcher {
fn drop(&mut self) { self.watch_handle.abort(); }
}
fn extract_public_key_pem(shared_state: &Arc<RwLock<AgentState>>) -> String {
let guard = shared_state.read().unwrap_or_else(|e| e.into_inner());
match guard.keys.as_ref() {
Some(keys) => keys.public_key_pem().unwrap_or_default(),
None => String::new(),
}
}
async fn upsert_entity(context: &Context, json_str: &str, public_key_pem: &str) -> Result<EntityId, anyhow::Error> {
let existing: Vec<JwtPolicyView> = context.fetch("true").await?;
if let Some(view) = existing.into_iter().next() {
let trx = context.begin();
let edit = view.edit(&trx)?;
edit.config_json().set(&json_str.to_string())?;
edit.public_key_pem().set(&public_key_pem.to_string())?;
trx.commit().await?;
Ok(view.id())
} else {
let trx = context.begin();
let entity = trx.create(&JwtPolicy { config_json: json_str.to_string(), public_key_pem: public_key_pem.to_string() }).await?;
let id: EntityId = entity.id();
trx.commit().await?;
Ok(id)
}
}
async fn watch_loop(path: PathBuf, context: Context, entity_id: EntityId, shared_state: Arc<RwLock<AgentState>>) {
let (tx, mut rx) = mpsc::channel::<notify::Result<Event>>(64);
let watch_dir = path.parent().unwrap_or_else(|| Path::new(".")).to_path_buf();
let mut watcher = match RecommendedWatcher::new(
move |res| {
let _ = tx.blocking_send(res);
},
notify::Config::default(),
) {
Ok(w) => w,
Err(e) => {
warn!("PolicyWatcher: failed to create fs watcher: {}", e);
return;
}
};
if let Err(e) = watcher.watch(&watch_dir, RecursiveMode::NonRecursive) {
warn!("PolicyWatcher: failed to watch {}: {}", watch_dir.display(), e);
return;
}
info!("PolicyWatcher: watching {} for changes", path.display());
loop {
match rx.recv().await {
Some(Ok(_event)) => {}
Some(Err(e)) => {
warn!("PolicyWatcher: fs watcher error: {}", e);
continue;
}
None => {
break;
}
}
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
while rx.try_recv().is_ok() {}
let json_str = match tokio::fs::read_to_string(&path).await {
Ok(s) => s,
Err(e) => {
warn!("PolicyWatcher: failed to read {}: {}", path.display(), e);
continue;
}
};
let new_config = match serde_json::from_str::<PolicyConfig>(&json_str) {
Ok(c) => c,
Err(e) => {
warn!("PolicyWatcher: invalid PolicyConfig in {}: {}", path.display(), e);
continue;
}
};
info!("PolicyWatcher: detected valid change in {}", path.display());
let public_key_pem = {
let mut guard = shared_state.write().unwrap_or_else(|e| e.into_inner());
guard.config = new_config;
match guard.keys.as_ref() {
Some(keys) => keys.public_key_pem().unwrap_or_default(),
None => String::new(),
}
};
match update_entity(&context, &entity_id, &json_str, &public_key_pem).await {
Ok(()) => info!("PolicyWatcher: updated JwtPolicy entity {}", entity_id),
Err(e) => warn!("PolicyWatcher: failed to update entity: {}", e),
}
}
}
async fn update_entity(context: &Context, entity_id: &EntityId, json_str: &str, public_key_pem: &str) -> Result<(), anyhow::Error> {
let view: JwtPolicyView = context.get::<JwtPolicyView>(entity_id.clone()).await?;
let trx = context.begin();
let edit = view.edit(&trx)?;
edit.config_json().set(&json_str.to_string())?;
edit.public_key_pem().set(&public_key_pem.to_string())?;
trx.commit().await?;
Ok(())
}