use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use super::StorageConfig;
use crate::io::is_truthy;
use crate::{Error, ErrorKind, Result};
pub const S3_ENDPOINT: &str = "s3.endpoint";
pub const S3_ACCESS_KEY_ID: &str = "s3.access-key-id";
pub const S3_SECRET_ACCESS_KEY: &str = "s3.secret-access-key";
pub const S3_SESSION_TOKEN: &str = "s3.session-token";
pub const S3_REGION: &str = "s3.region";
pub const CLIENT_REGION: &str = "client.region";
pub const S3_PATH_STYLE_ACCESS: &str = "s3.path-style-access";
pub const S3_SSE_TYPE: &str = "s3.sse.type";
pub const S3_SSE_KEY: &str = "s3.sse.key";
pub const S3_SSE_MD5: &str = "s3.sse.md5";
pub const S3_ASSUME_ROLE_ARN: &str = "client.assume-role.arn";
pub const S3_ASSUME_ROLE_EXTERNAL_ID: &str = "client.assume-role.external-id";
pub const S3_ASSUME_ROLE_SESSION_NAME: &str = "client.assume-role.session-name";
pub const S3_ALLOW_ANONYMOUS: &str = "s3.allow-anonymous";
pub const S3_DISABLE_EC2_METADATA: &str = "s3.disable-ec2-metadata";
pub const S3_DISABLE_CONFIG_LOAD: &str = "s3.disable-config-load";
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, TypedBuilder)]
pub struct S3Config {
#[builder(default, setter(strip_option, into))]
pub endpoint: Option<String>,
#[builder(default, setter(strip_option, into))]
pub access_key_id: Option<String>,
#[builder(default, setter(strip_option, into))]
pub secret_access_key: Option<String>,
#[builder(default, setter(strip_option, into))]
pub session_token: Option<String>,
#[builder(default, setter(strip_option, into))]
pub region: Option<String>,
#[builder(default)]
pub enable_virtual_host_style: bool,
#[builder(default, setter(strip_option, into))]
pub server_side_encryption: Option<String>,
#[builder(default, setter(strip_option, into))]
pub server_side_encryption_aws_kms_key_id: Option<String>,
#[builder(default, setter(strip_option, into))]
pub server_side_encryption_customer_algorithm: Option<String>,
#[builder(default, setter(strip_option, into))]
pub server_side_encryption_customer_key: Option<String>,
#[builder(default, setter(strip_option, into))]
pub server_side_encryption_customer_key_md5: Option<String>,
#[builder(default, setter(strip_option, into))]
pub role_arn: Option<String>,
#[builder(default, setter(strip_option, into))]
pub external_id: Option<String>,
#[builder(default, setter(strip_option, into))]
pub role_session_name: Option<String>,
#[builder(default)]
pub allow_anonymous: bool,
#[builder(default)]
pub disable_ec2_metadata: bool,
#[builder(default)]
pub disable_config_load: bool,
}
impl TryFrom<&StorageConfig> for S3Config {
type Error = crate::Error;
fn try_from(config: &StorageConfig) -> Result<Self> {
let props = config.props();
let mut cfg = S3Config::default();
if let Some(endpoint) = props.get(S3_ENDPOINT) {
cfg.endpoint = Some(endpoint.clone());
}
if let Some(access_key_id) = props.get(S3_ACCESS_KEY_ID) {
cfg.access_key_id = Some(access_key_id.clone());
}
if let Some(secret_access_key) = props.get(S3_SECRET_ACCESS_KEY) {
cfg.secret_access_key = Some(secret_access_key.clone());
}
if let Some(session_token) = props.get(S3_SESSION_TOKEN) {
cfg.session_token = Some(session_token.clone());
}
if let Some(region) = props.get(S3_REGION) {
cfg.region = Some(region.clone());
}
if let Some(region) = props.get(CLIENT_REGION) {
cfg.region = Some(region.clone());
}
if let Some(path_style_access) = props.get(S3_PATH_STYLE_ACCESS) {
cfg.enable_virtual_host_style = !is_truthy(path_style_access.to_lowercase().as_str());
}
if let Some(arn) = props.get(S3_ASSUME_ROLE_ARN) {
cfg.role_arn = Some(arn.clone());
}
if let Some(external_id) = props.get(S3_ASSUME_ROLE_EXTERNAL_ID) {
cfg.external_id = Some(external_id.clone());
}
if let Some(session_name) = props.get(S3_ASSUME_ROLE_SESSION_NAME) {
cfg.role_session_name = Some(session_name.clone());
}
let s3_sse_key = props.get(S3_SSE_KEY).cloned();
if let Some(sse_type) = props.get(S3_SSE_TYPE) {
match sse_type.to_lowercase().as_str() {
"none" => {}
"s3" => {
cfg.server_side_encryption = Some("AES256".to_string());
}
"kms" => {
cfg.server_side_encryption = Some("aws:kms".to_string());
cfg.server_side_encryption_aws_kms_key_id = s3_sse_key;
}
"custom" => {
cfg.server_side_encryption_customer_algorithm = Some("AES256".to_string());
cfg.server_side_encryption_customer_key = s3_sse_key;
cfg.server_side_encryption_customer_key_md5 = props.get(S3_SSE_MD5).cloned();
}
_ => {
return Err(Error::new(
ErrorKind::DataInvalid,
format!(
"Invalid {S3_SSE_TYPE}: {sse_type}. Expected one of (custom, kms, s3, none)"
),
));
}
}
}
if let Some(allow_anonymous) = props.get(S3_ALLOW_ANONYMOUS)
&& is_truthy(allow_anonymous.to_lowercase().as_str())
{
cfg.allow_anonymous = true;
}
if let Some(disable_ec2_metadata) = props.get(S3_DISABLE_EC2_METADATA)
&& is_truthy(disable_ec2_metadata.to_lowercase().as_str())
{
cfg.disable_ec2_metadata = true;
}
if let Some(disable_config_load) = props.get(S3_DISABLE_CONFIG_LOAD)
&& is_truthy(disable_config_load.to_lowercase().as_str())
{
cfg.disable_config_load = true;
}
Ok(cfg)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_s3_config_builder() {
let config = S3Config::builder()
.region("us-east-1")
.access_key_id("my-access-key")
.secret_access_key("my-secret-key")
.endpoint("http://localhost:9000")
.build();
assert_eq!(config.region.as_deref(), Some("us-east-1"));
assert_eq!(config.access_key_id.as_deref(), Some("my-access-key"));
assert_eq!(config.secret_access_key.as_deref(), Some("my-secret-key"));
assert_eq!(config.endpoint.as_deref(), Some("http://localhost:9000"));
}
#[test]
fn test_s3_config_from_storage_config() {
let storage_config = StorageConfig::new()
.with_prop(S3_REGION, "us-east-1")
.with_prop(S3_ACCESS_KEY_ID, "my-access-key")
.with_prop(S3_SECRET_ACCESS_KEY, "my-secret-key")
.with_prop(S3_ENDPOINT, "http://localhost:9000");
let s3_config = S3Config::try_from(&storage_config).unwrap();
assert_eq!(s3_config.region.as_deref(), Some("us-east-1"));
assert_eq!(s3_config.access_key_id.as_deref(), Some("my-access-key"));
assert_eq!(
s3_config.secret_access_key.as_deref(),
Some("my-secret-key")
);
assert_eq!(s3_config.endpoint.as_deref(), Some("http://localhost:9000"));
}
#[test]
fn test_s3_config_client_region_precedence() {
let storage_config = StorageConfig::new()
.with_prop(S3_REGION, "us-east-1")
.with_prop(CLIENT_REGION, "eu-west-1");
let s3_config = S3Config::try_from(&storage_config).unwrap();
assert_eq!(s3_config.region.as_deref(), Some("eu-west-1"));
}
#[test]
fn test_s3_config_path_style_access() {
let storage_config = StorageConfig::new().with_prop(S3_PATH_STYLE_ACCESS, "true");
let s3_config = S3Config::try_from(&storage_config).unwrap();
assert!(!s3_config.enable_virtual_host_style);
}
#[test]
fn test_s3_config_sse_kms() {
let storage_config = StorageConfig::new()
.with_prop(S3_SSE_TYPE, "kms")
.with_prop(S3_SSE_KEY, "my-kms-key-id");
let s3_config = S3Config::try_from(&storage_config).unwrap();
assert_eq!(s3_config.server_side_encryption.as_deref(), Some("aws:kms"));
assert_eq!(
s3_config.server_side_encryption_aws_kms_key_id.as_deref(),
Some("my-kms-key-id")
);
}
#[test]
fn test_s3_config_allow_anonymous() {
let storage_config = StorageConfig::new().with_prop(S3_ALLOW_ANONYMOUS, "true");
let s3_config = S3Config::try_from(&storage_config).unwrap();
assert!(s3_config.allow_anonymous);
}
}