use std::fmt;
use std::sync::Arc;
use aws_config::profile::ProfileFileCredentialsProvider;
use aws_config::profile::region::ProfileFileRegionProvider;
use aws_config::sts::AssumeRoleProvider;
use aws_types::region::Region;
use aws_types::sdk_config::SharedCredentialsProvider;
use bytes::Bytes;
use futures::StreamExt;
use object_store::aws::{AmazonS3Builder, AwsCredential};
use object_store::local::LocalFileSystem;
use object_store::path::Path as StorePath;
use object_store::{
Attribute, AttributeValue, Attributes, CredentialProvider, ObjectStore, ObjectStoreExt,
PutOptions, PutPayload,
};
use super::error::DrainError;
use super::uri::DestinationUri;
pub const LIST_LIMIT: usize = 10_000;
#[allow(deprecated)]
pub(super) type ProfileFiles = aws_config::profile::profile_file::ProfileFiles;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PutMeta {
pub content_type: Option<String>,
pub content_encoding: Option<String>,
}
impl PutMeta {
pub fn gzipped_text() -> Self {
Self {
content_type: Some("text/plain".to_string()),
content_encoding: Some("gzip".to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ObjectMeta {
pub key: String,
pub size: u64,
pub last_modified_unix: i64,
}
#[async_trait::async_trait]
pub trait LogDestination: Send + Sync + fmt::Debug {
async fn put(&self, key: &str, body: Bytes, meta: PutMeta) -> Result<(), DrainError>;
async fn head(&self, key: &str) -> Result<Option<ObjectMeta>, DrainError>;
async fn get(&self, key: &str) -> Result<Option<Bytes>, DrainError>;
async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, DrainError>;
fn cache_namespace(&self) -> &str;
}
pub struct ObjectStoreDestination {
store: Arc<dyn ObjectStore>,
prefix: String,
label: String,
supports_attributes: bool,
cache_namespace: String,
}
impl fmt::Debug for ObjectStoreDestination {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ObjectStoreDestination")
.field("destination", &self.label)
.finish()
}
}
impl ObjectStoreDestination {
pub async fn connect(uri: &DestinationUri) -> Result<Self, DrainError> {
let cache_namespace = uri.cache_namespace();
match uri {
DestinationUri::File { path } => {
std::fs::create_dir_all(path).map_err(|source| DrainError::Io {
path: path.clone(),
source,
})?;
let store = LocalFileSystem::new_with_prefix(path).map_err(|e| DrainError::Io {
path: path.clone(),
source: std::io::Error::other(e),
})?;
Ok(Self {
store: Arc::new(store),
prefix: String::new(),
label: format!("file://{}", path.display()),
supports_attributes: false,
cache_namespace,
})
}
DestinationUri::S3 {
bucket,
prefix,
region,
profile,
role_arn,
} => {
Self::connect_s3(
bucket,
prefix,
S3AuthRequest {
region: region.as_deref(),
profile: profile.as_deref(),
role_arn: role_arn.as_deref(),
profile_files: None,
},
cache_namespace,
)
.await
}
}
}
async fn connect_s3(
bucket: &str,
prefix: &str,
auth: S3AuthRequest<'_>,
cache_namespace: String,
) -> Result<Self, DrainError> {
let label = format!("s3://{bucket}/{prefix}");
let resolved = resolve_s3_auth(&label, auth).await?;
let store = AmazonS3Builder::new()
.with_bucket_name(bucket)
.with_region(&resolved.region)
.with_credentials(Arc::new(AwsChainCredentials {
inner: resolved.provider,
}))
.build()
.map_err(|source| DrainError::Transport {
op: "connect",
key: label.clone(),
source,
})?;
Ok(Self {
store: Arc::new(store),
prefix: prefix.to_string(),
label,
supports_attributes: true,
cache_namespace,
})
}
fn absolute(&self, key: &str) -> String {
if self.prefix.is_empty() {
key.to_string()
} else {
format!("{}/{}", self.prefix, key)
}
}
fn convert(meta: object_store::ObjectMeta) -> ObjectMeta {
ObjectMeta {
key: meta.location.as_ref().to_string(),
size: meta.size,
last_modified_unix: meta.last_modified.timestamp(),
}
}
}
#[async_trait::async_trait]
impl LogDestination for ObjectStoreDestination {
async fn put(&self, key: &str, body: Bytes, meta: PutMeta) -> Result<(), DrainError> {
let absolute = self.absolute(key);
let path = StorePath::from(absolute.as_str());
let mut attributes = Attributes::new();
if self.supports_attributes {
if let Some(ct) = meta.content_type {
attributes.insert(Attribute::ContentType, AttributeValue::from(ct));
}
if let Some(ce) = meta.content_encoding {
attributes.insert(Attribute::ContentEncoding, AttributeValue::from(ce));
}
}
let options = PutOptions {
attributes,
..PutOptions::default()
};
self.store
.put_opts(&path, PutPayload::from_bytes(body), options)
.await
.map_err(|source| DrainError::Transport {
op: "put",
key: absolute,
source,
})?;
Ok(())
}
async fn head(&self, key: &str) -> Result<Option<ObjectMeta>, DrainError> {
let absolute = self.absolute(key);
let path = StorePath::from(absolute.as_str());
match self.store.head(&path).await {
Ok(meta) => Ok(Some(Self::convert(meta))),
Err(object_store::Error::NotFound { .. }) => Ok(None),
Err(source) => Err(DrainError::Transport {
op: "head",
key: absolute,
source,
}),
}
}
async fn get(&self, key: &str) -> Result<Option<Bytes>, DrainError> {
let absolute = self.absolute(key);
let path = StorePath::from(absolute.as_str());
let result = match self.store.get(&path).await {
Ok(result) => result,
Err(object_store::Error::NotFound { .. }) => return Ok(None),
Err(source) => {
return Err(DrainError::Transport {
op: "get",
key: absolute,
source,
});
}
};
let bytes = result
.bytes()
.await
.map_err(|source| DrainError::Transport {
op: "get",
key: absolute,
source,
})?;
Ok(Some(bytes))
}
async fn list(&self, prefix: &str) -> Result<Vec<ObjectMeta>, DrainError> {
let absolute = self.absolute(prefix);
let path = StorePath::from(absolute.as_str());
let mut stream = self.store.list(Some(&path));
let mut out = Vec::new();
while let Some(next) = stream.next().await {
let meta = next.map_err(|source| DrainError::Transport {
op: "list",
key: absolute.clone(),
source,
})?;
out.push(Self::convert(meta));
if out.len() >= LIST_LIMIT {
tracing::warn!(
prefix = %absolute,
limit = LIST_LIMIT,
"log-drain list hit its entry cap; results truncated"
);
break;
}
}
Ok(out)
}
fn cache_namespace(&self) -> &str {
&self.cache_namespace
}
}
pub(super) struct S3AuthRequest<'a> {
pub region: Option<&'a str>,
pub profile: Option<&'a str>,
pub role_arn: Option<&'a str>,
pub profile_files: Option<&'a ProfileFiles>,
}
pub(super) struct S3Auth {
pub region: String,
pub provider: SharedCredentialsProvider,
}
impl fmt::Debug for S3Auth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("S3Auth")
.field("region", &self.region)
.finish()
}
}
pub(super) async fn resolve_s3_auth(
label: &str,
auth: S3AuthRequest<'_>,
) -> Result<S3Auth, DrainError> {
let (identity_region, base) = match auth.profile {
Some(name) => resolve_profile_identity(name, auth.profile_files).await,
None => resolve_default_chain(label).await?,
};
let region = auth
.region
.map(str::to_string)
.or(identity_region)
.ok_or_else(|| DrainError::Credentials {
uri: label.to_string(),
source: "no AWS region: none in the URI's `?region=`, none from the \
credential chain or the named profile (set AWS_REGION, give the \
profile a `region`, or add `?region=` to the URI)"
.into(),
})?;
let provider = match auth.role_arn {
None => base,
Some(arn) => assume_role(arn, ®ion, base).await,
};
Ok(S3Auth { region, provider })
}
async fn resolve_profile_identity(
name: &str,
files: Option<&ProfileFiles>,
) -> (Option<String>, SharedCredentialsProvider) {
let mut credentials = ProfileFileCredentialsProvider::builder().profile_name(name);
let mut region = ProfileFileRegionProvider::builder().profile_name(name);
if let Some(files) = files {
credentials = credentials.profile_files(files.clone());
region = region.profile_files(files.clone());
}
use aws_config::meta::region::ProvideRegion;
let region = region
.build()
.region()
.await
.map(|r| r.as_ref().to_string());
(region, SharedCredentialsProvider::new(credentials.build()))
}
async fn resolve_default_chain(
label: &str,
) -> Result<(Option<String>, SharedCredentialsProvider), DrainError> {
let sdk_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.load()
.await;
let region = sdk_config.region().map(|r| r.as_ref().to_string());
let provider = sdk_config
.credentials_provider()
.ok_or_else(|| DrainError::Credentials {
uri: label.to_string(),
source: "the AWS default provider chain supplied no credentials provider".into(),
})?;
Ok((region, provider))
}
async fn assume_role(
arn: &str,
region: &str,
base: SharedCredentialsProvider,
) -> SharedCredentialsProvider {
let sts_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(Region::new(region.to_string()))
.credentials_provider(base)
.load()
.await;
SharedCredentialsProvider::new(
AssumeRoleProvider::builder(arn)
.configure(&sts_config)
.build()
.await,
)
}
#[derive(Debug)]
struct AwsChainCredentials {
inner: SharedCredentialsProvider,
}
#[async_trait::async_trait]
impl CredentialProvider for AwsChainCredentials {
type Credential = AwsCredential;
async fn get_credential(&self) -> object_store::Result<Arc<AwsCredential>> {
use aws_credential_types::provider::ProvideCredentials;
let creds = self.inner.provide_credentials().await.map_err(|e| {
object_store::Error::Unauthenticated {
path: "aws-credential-chain".to_string(),
source: Box::new(e),
}
})?;
Ok(Arc::new(AwsCredential {
key_id: creds.access_key_id().to_string(),
secret_key: creds.secret_access_key().to_string(),
token: creds.session_token().map(str::to_string),
}))
}
}