use std::collections::HashMap;
use std::path::Path;
use foundation_db::core::state::{FileStateStore, StateStore};
use foundation_db::core::state::types::ResourceState;
use foundation_db::core::errors::StorageError;
use serde::{Deserialize, Serialize};
use crate::config::ProxyConfig;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PersistedProxyState {
pub backend_states: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cert_pem: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_pem: Option<String>,
pub cert_renewed_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_hash: Option<String>,
}
pub struct ProxyStateStore {
store: FileStateStore,
}
const PROXY_STATE_ID: &str = "proxy_state";
impl ProxyStateStore {
pub fn new(state_dir: &Path, domain: &str) -> Self {
Self {
store: FileStateStore::new(state_dir, "foundation_proxy", domain),
}
}
pub fn load(&self) -> Result<Option<PersistedProxyState>, StorageError> {
let stream = self.store.get(PROXY_STATE_ID)?;
for item in stream {
match item {
foundation_core::valtron::ThreadedValue::Value(Ok(Some(resource))) => {
return serde_json::from_value(resource.output)
.map(Some)
.map_err(|e| StorageError::Serialization(e.to_string()));
}
foundation_core::valtron::ThreadedValue::Value(Ok(None)) => return Ok(None),
foundation_core::valtron::ThreadedValue::Value(Err(e)) => return Err(e),
_ => {}
}
}
Ok(None)
}
pub fn save(&self, state: &PersistedProxyState) -> Result<(), StorageError> {
let data = serde_json::to_value(state)
.map_err(|e| StorageError::Serialization(e.to_string()))?;
let resource = ResourceState {
id: PROXY_STATE_ID.to_string(),
kind: "proxy_state".to_string(),
provider: "foundation_proxy".to_string(),
status: foundation_db::core::state::types::StateStatus::Created,
environment: None,
config_hash: String::new(),
output: data,
config_snapshot: serde_json::Value::Null,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
let stream = self.store.set(PROXY_STATE_ID, &resource)?;
for item in stream {
if let foundation_core::valtron::ThreadedValue::Value(Err(e)) = item {
return Err(e);
}
}
Ok(())
}
pub fn save_backend_state(
&self,
service: &str,
backend_url: &str,
state: &str,
) -> Result<(), StorageError> {
let mut persisted = self.load()?.unwrap_or_default();
persisted
.backend_states
.insert(format!("{service}/{backend_url}"), state.to_string());
self.save(&persisted)
}
pub fn compute_config_hash(config: &ProxyConfig) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
config.domain.hash(&mut hasher);
config.public_ip.hash(&mut hasher);
for svc in &config.services {
svc.name.hash(&mut hasher);
svc.host.hash(&mut hasher);
for be in &svc.backends {
be.url.hash(&mut hasher);
}
}
format!("{:016x}", hasher.finish())
}
pub fn save_tls_cert(&self, cert_pem: &str, key_pem: &str) -> Result<(), StorageError> {
let mut state = self.load()?.unwrap_or_default();
state.cert_pem = Some(cert_pem.to_string());
state.key_pem = Some(key_pem.to_string());
state.cert_renewed_at = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
);
self.save(&state)
}
}