#![allow(clippy::result_large_err)]
use aws_sdk_s3::Client;
use aws_sdk_s3::primitives::ByteStream;
use serde_json::Value as JsonValue;
use crate::runtime::executor_utils::{
json_required_str, object_bytes_from_json, reject_oversized_object,
};
use crate::runtime::executors::{
BackendExecutor, BackendHealth, BackendProbe, MutationExecutor, ObjectExecutor, QueryExecutor,
ResourceAdminExecutor, SearchExecutor,
};
pub(crate) struct S3Executor(pub(crate) Client);
impl crate::runtime::backend_context::BackendContextEnforcer for S3Executor {
fn backend_label(&self) -> &str {
"s3"
}
fn enforce(
&self,
ctx: &crate::runtime::backend_context::AppliedContext,
) -> crate::runtime::backend_context::ContextEffect {
if ctx.is_empty() {
return crate::runtime::backend_context::ContextEffect::Advisory {
recorded_in: "no_context_to_apply".into(),
};
}
crate::runtime::backend_context::ContextEffect::Enforced {
mechanism: "key prefix t:<tenant>/p:<project>/ prepended by compile_read/write/delete"
.into(),
}
}
}
impl BackendHealth for S3Executor {
async fn ping(&self) -> Result<(), String> {
Ok(())
}
}
impl QueryExecutor for S3Executor {
async fn query(&self, _request_json: &str) -> Result<String, tonic::Status> {
Err(tonic::Status::failed_precondition(
"s3 does not support generic query",
))
}
}
impl MutationExecutor for S3Executor {
async fn mutate(&self, _request_json: &str) -> Result<String, tonic::Status> {
Err(tonic::Status::failed_precondition(
"s3 mutation is via get_object/put_object",
))
}
}
impl SearchExecutor for S3Executor {
async fn search(&self, _request_json: &str) -> Result<String, tonic::Status> {
Err(tonic::Status::failed_precondition(
"s3 does not support search",
))
}
}
impl ObjectExecutor for S3Executor {
async fn get_object(&self, request_json: &str) -> Result<Vec<u8>, tonic::Status> {
let spec: JsonValue = serde_json::from_str(request_json)
.map_err(|e| tonic::Status::invalid_argument(format!("invalid request json: {e}")))?;
let bucket = json_required_str(&spec, "bucket")?;
let object_key =
json_required_str(&spec, "object_key").or_else(|_| json_required_str(&spec, "key"))?;
let output = self
.0
.get_object()
.bucket(bucket)
.key(object_key)
.send()
.await
.map_err(|err| tonic::Status::unavailable(format!("S3 get_object failed: {err}")))?;
let bytes = output
.body
.collect()
.await
.map_err(|err| tonic::Status::unavailable(format!("S3 body read failed: {err}")))?
.into_bytes()
.to_vec();
Ok(bytes)
}
async fn put_object(
&self,
request_json: &str,
bytes: Vec<u8>,
) -> Result<String, tonic::Status> {
let spec: JsonValue = serde_json::from_str(request_json)
.map_err(|e| tonic::Status::invalid_argument(format!("invalid request json: {e}")))?;
let bucket = json_required_str(&spec, "bucket")?;
let object_key =
json_required_str(&spec, "object_key").or_else(|_| json_required_str(&spec, "key"))?;
let body = if bytes.is_empty() {
object_bytes_from_json(&spec)?
} else {
bytes
};
let body_len = body.len();
reject_oversized_object(body_len)?;
let mut request = self
.0
.put_object()
.bucket(bucket)
.key(object_key)
.body(ByteStream::from(body));
if let Some(content_type) = spec
.get("content_type")
.and_then(JsonValue::as_str)
.filter(|value| !value.trim().is_empty())
{
request = request.content_type(content_type);
}
request
.send()
.await
.map_err(|err| tonic::Status::unavailable(format!("S3 put_object failed: {err}")))?;
Ok(serde_json::json!({
"resource_uri": format!("s3://{bucket}/{object_key}"),
"affected_rows": 1,
"bytes": body_len
})
.to_string())
}
async fn delete_object(&self, request_json: &str) -> Result<(), tonic::Status> {
let spec: JsonValue = serde_json::from_str(request_json)
.map_err(|e| tonic::Status::invalid_argument(format!("invalid request json: {e}")))?;
let bucket = json_required_str(&spec, "bucket")?;
let object_key =
json_required_str(&spec, "object_key").or_else(|_| json_required_str(&spec, "key"))?;
self.0
.delete_object()
.bucket(bucket)
.key(object_key)
.send()
.await
.map(|_| ())
.map_err(|err| tonic::Status::unavailable(format!("S3 delete_object failed: {err}")))
}
}
impl ResourceAdminExecutor for S3Executor {
async fn ensure_resource(
&self,
resource_name: &str,
_spec_json: &str,
) -> Result<(), tonic::Status> {
self.0
.create_bucket()
.bucket(resource_name)
.send()
.await
.map(|_| ())
.or_else(|err| {
let msg = err.to_string();
if msg.contains("BucketAlreadyOwnedByYou") || msg.contains("BucketAlreadyExists") {
Ok(())
} else {
Err(tonic::Status::internal(format!(
"s3 create_bucket failed: {msg}"
)))
}
})
}
async fn drop_resource(&self, resource_name: &str) -> Result<(), tonic::Status> {
self.0
.delete_bucket()
.bucket(resource_name)
.send()
.await
.map(|_| ())
.map_err(|e| tonic::Status::internal(format!("s3 delete_bucket failed: {e}")))
}
async fn list_resources(&self) -> Result<Vec<String>, tonic::Status> {
let resp = self
.0
.list_buckets()
.send()
.await
.map_err(|e| tonic::Status::internal(format!("s3 list_buckets failed: {e}")))?;
Ok(resp
.buckets()
.iter()
.filter_map(|b| b.name())
.map(str::to_owned)
.collect())
}
}
impl BackendExecutor for S3Executor {
async fn transaction(&self, _request_json: &str) -> Result<String, tonic::Status> {
Err(tonic::Status::failed_precondition(
"s3 does not support transactions",
))
}
async fn probe(&self) -> Result<BackendProbe, tonic::Status> {
Ok(BackendProbe {
backend: "s3".to_string(),
instance: None,
ok: true,
error: None,
})
}
}