Add exact-identity target storage adapter
This commit is contained in:
parent
f569707f97
commit
854e6e1c17
1 changed files with 3263 additions and 41 deletions
142
crates/remora-target/src/config.rs
Normal file
142
crates/remora-target/src/config.rs
Normal 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}/")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue