use chrono::Utc;
use deltalake_aws::constants;
use deltalake_aws::register_handlers;
use deltalake_test::utils::*;
use rand::prelude::*;
use rand::random;
use std::process::{Command, ExitStatus, Stdio};
#[derive(Clone, Debug)]
pub struct S3Integration {
bucket_name: String,
}
impl Default for S3Integration {
fn default() -> Self {
register_handlers(None);
let task_id = format!("{}", rand::rng().next_u64());
Self {
bucket_name: format!("delta-table-{}-{task_id}", Utc::now().timestamp()),
}
}
}
impl StorageIntegration for S3Integration {
fn create_bucket(&self) -> std::io::Result<ExitStatus> {
Self::create_lock_table()?;
let mut child = Command::new("aws")
.args(["s3", "mb", &self.root_uri()])
.spawn()
.expect("aws command is installed");
child.wait()
}
fn bucket_name(&self) -> String {
self.bucket_name.clone()
}
fn root_uri(&self) -> String {
format!("s3://{}", self.bucket_name())
}
fn prepare_env(&self) {
set_env_if_not_set(
constants::LOCK_TABLE_KEY_NAME,
format!("delta_log_it_{}", random::<u16>()),
);
match std::env::var(constants::AWS_ENDPOINT_URL).ok() {
Some(endpoint_url) if endpoint_url.to_lowercase() == "none" => unsafe {
std::env::remove_var(constants::AWS_ENDPOINT_URL)
},
Some(_) => (),
None => unsafe {
std::env::set_var(constants::AWS_ENDPOINT_URL, "http://localhost:4566")
},
}
set_env_if_not_set(constants::AWS_ACCESS_KEY_ID, "deltalake");
set_env_if_not_set(constants::AWS_SECRET_ACCESS_KEY, "weloverust");
set_env_if_not_set(constants::AWS_REGION, "us-east-1");
set_env_if_not_set(constants::AWS_S3_LOCKING_PROVIDER, "dynamodb");
}
fn copy_directory(&self, source: &str, destination: &str) -> std::io::Result<ExitStatus> {
let destination = format!("{}/{destination}", self.root_uri());
let mut child = Command::new("aws")
.args(["s3", "cp", source, &destination, "--recursive"])
.spawn()
.expect("aws command is installed");
child.wait()
}
}
impl S3Integration {
fn delete_bucket(bucket_name: impl AsRef<str>) -> std::io::Result<ExitStatus> {
let mut child = Command::new("aws")
.args(["s3", "rb", bucket_name.as_ref(), "--force"])
.spawn()
.expect("aws command is installed");
child.wait()
}
fn create_dynamodb_table(
table_name: &str,
attr_definitions: &[&str],
key_schema: &[&str],
) -> std::io::Result<ExitStatus> {
let args = [
"dynamodb",
"create-table",
"--table-name",
table_name,
"--provisioned-throughput",
"ReadCapacityUnits=1,WriteCapacityUnits=1",
"--attribute-definitions",
];
let mut child = Command::new("aws")
.args(args)
.args(attr_definitions.iter())
.arg("--key-schema")
.args(key_schema)
.stdout(Stdio::null())
.spawn()
.expect("aws command is installed");
let status = child.wait()?;
Self::wait_for_table(table_name)?;
Ok(status)
}
fn find_subsequence(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack
.windows(needle.len())
.position(|window| window == needle)
}
fn wait_for_table(table_name: &str) -> std::io::Result<()> {
let args = ["dynamodb", "describe-table", "--table-name", table_name];
loop {
let output = Command::new("aws")
.args(args)
.output()
.expect("aws command is installed");
if Self::find_subsequence(&output.stdout, "CREATING".as_bytes()).is_some() {
std::thread::sleep(std::time::Duration::from_millis(200));
continue;
} else {
return Ok(());
}
}
}
pub fn create_lock_table() -> std::io::Result<ExitStatus> {
let table_name =
std::env::var(constants::LOCK_TABLE_KEY_NAME).unwrap_or_else(|_| "delta_log".into());
Self::create_dynamodb_table(
&table_name,
&[
"AttributeName=tablePath,AttributeType=S",
"AttributeName=fileName,AttributeType=S",
],
&[
"AttributeName=tablePath,KeyType=HASH",
"AttributeName=fileName,KeyType=RANGE",
],
)
}
fn delete_dynamodb_table(table_name: &str) -> std::io::Result<ExitStatus> {
let mut child = Command::new("aws")
.args(["dynamodb", "delete-table", "--table-name", table_name])
.stdout(Stdio::null())
.spawn()
.expect("aws command is installed");
child.wait()
}
pub fn delete_lock_table() -> std::io::Result<ExitStatus> {
let table_name =
std::env::var(constants::LOCK_TABLE_KEY_NAME).unwrap_or_else(|_| "delta_log".into());
Self::delete_dynamodb_table(&table_name)
}
}
impl Drop for S3Integration {
fn drop(&mut self) {
Self::delete_bucket(self.root_uri()).expect("Failed to drop bucket");
Self::delete_lock_table().expect("Failed to delete lock table");
}
}