Add exact-identity target storage adapter

This commit is contained in:
ms2k 2026-07-17 02:54:18 +09:00
commit 854e6e1c17

View file

@ -0,0 +1,142 @@
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}/")
}
}