142 lines
5 KiB
Rust
142 lines
5 KiB
Rust
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<Self, TargetError> {
|
|
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::<bool>()
|
|
.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<String>,
|
|
endpoint: impl Into<String>,
|
|
bucket: impl Into<String>,
|
|
region: impl Into<String>,
|
|
access_key_id: impl Into<String>,
|
|
secret_access_key: impl Into<String>,
|
|
root: impl Into<String>,
|
|
) -> Result<Self, TargetError> {
|
|
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<String, TargetError> {
|
|
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}/")
|
|
}
|
|
}
|