use std::fmt; use crate::error::TargetError; /// Connection settings for one S3-compatible target store. #[derive(Clone, Eq, PartialEq)] pub struct TargetStoreConfig { /// Stable target-store identity used in authorization and object identities. pub target_store_id: String, pub endpoint: String, pub bucket: String, pub region: String, pub access_key_id: String, pub secret_access_key: String, /// Optional operator root under the bucket. Empty means bucket root. pub root: String, } impl fmt::Debug for TargetStoreConfig { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("TargetStoreConfig") .field("target_store_id", &self.target_store_id) .field("endpoint", &self.endpoint) .field("bucket", &self.bucket) .field("region", &self.region) .field("access_key_id", &self.access_key_id) .field("secret_access_key", &"REDACTED") .field("root", &self.root) .finish() } } impl TargetStoreConfig { /// Load fixture or deployment settings from environment variables. /// /// Recognized variables: `REMORA_TARGET_STORE_ID` (or `REMORA_TARGET_PROVIDER`), /// `REMORA_TARGET_ENDPOINT`, `REMORA_TARGET_BUCKET`, `REMORA_TARGET_REGION`, /// `REMORA_TARGET_ACCESS_KEY`, `REMORA_TARGET_SECRET_KEY`, /// `REMORA_TARGET_EXCLUSIVE_WRITER`, and optional `REMORA_TARGET_ROOT`. pub fn from_env() -> Result { let target_store_id = std::env::var("REMORA_TARGET_STORE_ID") .ok() .filter(|value| !value.is_empty()) .or_else(|| { std::env::var("REMORA_TARGET_PROVIDER") .ok() .filter(|value| !value.is_empty()) }) .ok_or_else(|| { TargetError::validation( "REMORA_TARGET_STORE_ID or REMORA_TARGET_PROVIDER is required", ) })?; let endpoint = required_env("REMORA_TARGET_ENDPOINT")?; let bucket = required_env("REMORA_TARGET_BUCKET")?; let region = std::env::var("REMORA_TARGET_REGION") .ok() .filter(|value| !value.is_empty()) .unwrap_or_else(|| "us-east-1".to_owned()); let access_key_id = required_env("REMORA_TARGET_ACCESS_KEY")?; let secret_access_key = required_env("REMORA_TARGET_SECRET_KEY")?; let root = std::env::var("REMORA_TARGET_ROOT") .ok() .filter(|value| !value.is_empty()) .unwrap_or_default(); let exclusive_writer = required_env("REMORA_TARGET_EXCLUSIVE_WRITER")? .parse::() .map_err(|_| { TargetError::validation("REMORA_TARGET_EXCLUSIVE_WRITER must be true or false") })?; if !exclusive_writer { return Err(TargetError::validation( "target reserved namespace must have Remora as its exclusive writer", )); } Self::new_exclusive( target_store_id, endpoint, bucket, region, access_key_id, secret_access_key, root, ) } pub fn new_exclusive( target_store_id: impl Into, endpoint: impl Into, bucket: impl Into, region: impl Into, access_key_id: impl Into, secret_access_key: impl Into, root: impl Into, ) -> Result { let target_store_id = target_store_id.into(); let endpoint = endpoint.into().trim_end_matches('/').to_owned(); let bucket = bucket.into(); let region = region.into(); let access_key_id = access_key_id.into(); let secret_access_key = secret_access_key.into(); let root = normalize_root(root.into()); if target_store_id.is_empty() { return Err(TargetError::validation("target store id is required")); } if endpoint.is_empty() { return Err(TargetError::validation("target endpoint is required")); } if bucket.is_empty() { return Err(TargetError::validation("target bucket is required")); } if access_key_id.is_empty() || secret_access_key.is_empty() { return Err(TargetError::validation("target credentials are required")); } Ok(Self { target_store_id, endpoint, bucket, region, access_key_id, secret_access_key, root, }) } } fn required_env(name: &str) -> Result { std::env::var(name) .ok() .filter(|value| !value.is_empty()) .ok_or_else(|| TargetError::validation(format!("{name} is required"))) } fn normalize_root(root: String) -> String { let trimmed = root.trim_matches('/'); if trimmed.is_empty() { String::new() } else { format!("/{trimmed}/") } }