Skip to content

Commit 1a05724

Browse files
committed
Duplicate some of the tests utils in binary crate
Signed-off-by: Evgeny (Zhenia) Dolgii <[email protected]>
1 parent f1b05a8 commit 1a05724

File tree

7 files changed

+227
-0
lines changed

7 files changed

+227
-0
lines changed

Cargo.lock

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mountpoint-s3/Cargo.toml

+9
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,21 @@ regex = "1.11.1"
2121

2222
[dev-dependencies]
2323
mountpoint-s3-client = { path = "../mountpoint-s3-client", features = ["mock"] }
24+
mountpoint-s3-fuser = { path = "../mountpoint-s3-fuser" }
2425
assert_cmd = "2.0.16"
2526
assert_fs = "1.1.2"
27+
aws-config = "1.5.15"
28+
aws-credential-types = "1.2.1"
29+
aws-sdk-s3 = "1.71.0"
2630
futures = { version = "0.3.31", features = ["thread-pool"] }
31+
nix = { version = "0.29.0" }
2732
predicates = "3.1.3"
33+
rand = "0.8.5"
34+
rand_chacha = "0.3.1"
2835
serde = { version = "1.0.217", features = ["derive"] }
2936
test-case = "3.3.1"
37+
tempfile = "3.15.0"
38+
tokio = { version = "1.43.0" }
3039
tracing = { version = "0.1.41", features = ["log"] }
3140

3241
[build-dependencies]

mountpoint-s3/tests/common/creds.rs

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use aws_config::sts::AssumeRoleProvider;
2+
use aws_config::Region;
3+
use aws_credential_types::provider::ProvideCredentials;
4+
use aws_credential_types::Credentials;
5+
use crate::common::s3::get_test_region;
6+
7+
/// Detect if running on GitHub Actions (GHA) and if so,
8+
/// emit masking string to avoid credentials accidentally being printed.
9+
fn mask_aws_creds_if_on_gha(credentials: &Credentials) {
10+
if std::env::var_os("GITHUB_ACTIONS").is_some() {
11+
// GitHub Actions aren't aware of these credential strings since we're sourcing them inside the tests.
12+
// If we think we're in GitHub Actions environment, register each in stdout.
13+
// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#masking-a-value-in-a-log
14+
println!("::add-mask::{}", credentials.access_key_id());
15+
println!("::add-mask::{}", credentials.secret_access_key());
16+
if let Some(token) = credentials.session_token() {
17+
println!("::add-mask::{}", token);
18+
}
19+
}
20+
}
21+
22+
pub async fn get_sdk_default_chain_creds() -> Credentials {
23+
use aws_config::default_provider::credentials::DefaultCredentialsChain;
24+
use aws_credential_types::provider::ProvideCredentials;
25+
26+
let sdk_provider = DefaultCredentialsChain::builder().build().await;
27+
let credentials = sdk_provider
28+
.provide_credentials()
29+
.await
30+
.expect("default chain credentials should be available");
31+
mask_aws_creds_if_on_gha(&credentials);
32+
credentials
33+
}
34+
35+
pub fn get_subsession_iam_role() -> String {
36+
std::env::var("S3_SUBSESSION_IAM_ROLE").expect("Set S3_SUBSESSION_IAM_ROLE to run integration tests")
37+
}
38+
39+
/// Takes the provided IAM Policy and assumes the configured subsession role,
40+
/// applying the given policy to scope down the permissions available.
41+
///
42+
/// This method works by making an AWS Security Token Service (STS) AssumeRole call providing the policy request field.
43+
/// The permissions are the intersection of the identity-based policy of the principal creating the session
44+
/// and the session policies. This means that the subsession role must already have any permissions you wish to use -
45+
/// this method can only reduce the scope of permissions.
46+
///
47+
/// See the [session policies section of the AWS IAM User Guide][session_policies] for more detail.
48+
///
49+
/// [session_policies]: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#policies_session
50+
pub async fn get_scoped_down_credentials<Policy: AsRef<str>>(policy: Policy) -> Credentials {
51+
let provider = AssumeRoleProvider::builder(get_subsession_iam_role())
52+
.region(Region::new(get_test_region()))
53+
.session_name("test_mountpoint-s3")
54+
.policy(policy.as_ref())
55+
.build()
56+
.await;
57+
let credentials = provider
58+
.provide_credentials()
59+
.await
60+
.expect("default chain credentials should be available");
61+
mask_aws_creds_if_on_gha(&credentials);
62+
credentials
63+
}

mountpoint-s3/tests/common/fuse.rs

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use std::ffi::OsStr;
2+
use std::fs::{File, ReadDir};
3+
use std::os::fd::AsRawFd;
4+
use std::path::Path;
5+
use std::sync::Arc;
6+
7+
use nix::fcntl::{self, FdFlag};
8+
9+
use fuser::{Mount, MountOption};
10+
11+
/// Open `/dev/fuse` and call `mount` syscall with given `mount_point`.
12+
///
13+
/// The mount is automatically unmounted when the returned [Mount] is dropped.
14+
pub fn mount_for_passing_fuse_fd(mount_point: &Path, options: &[MountOption]) -> (Arc<File>, Mount) {
15+
let (file, mount) = Mount::new(mount_point, options).unwrap();
16+
17+
// fuser sets `FD_CLOEXEC` (i.e., close-on-exec) flag on the file descriptor in its libfuse3 implementation.
18+
// Since we're forking the process in some of our test cases, this prevents child process to inherit the FUSE fd and causes our tests to fail.
19+
// Here we're clearing this flag if its set on the FUSE fd.
20+
let fd = file.as_raw_fd();
21+
let mut flags = FdFlag::from_bits_retain(fcntl::fcntl(fd, fcntl::F_GETFD).unwrap());
22+
if flags.contains(FdFlag::FD_CLOEXEC) {
23+
flags.remove(FdFlag::FD_CLOEXEC);
24+
let _ = fcntl::fcntl(fd, fcntl::F_SETFD(flags)).unwrap();
25+
}
26+
27+
(file, mount)
28+
}
29+
30+
/// Take a `read_dir` iterator and return the entry names
31+
pub fn read_dir_to_entry_names(read_dir_iter: ReadDir) -> Vec<String> {
32+
read_dir_iter
33+
.map(|entry| {
34+
let entry = entry.expect("no io err during readdir");
35+
let entry_path = entry.path();
36+
let name = entry_path
37+
.file_name()
38+
.and_then(OsStr::to_str)
39+
.expect("path should end with valid unicode file or dir name");
40+
name.to_owned()
41+
})
42+
.collect::<Vec<_>>()
43+
}

mountpoint-s3/tests/common/mod.rs

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
pub mod creds;
2+
pub mod fuse;
3+
pub mod s3;
4+
5+
use std::future::Future;
6+
7+
pub fn tokio_block_on<F: Future>(future: F) -> F::Output {
8+
let runtime = tokio::runtime::Builder::new_current_thread()
9+
.enable_io()
10+
.enable_time()
11+
.build()
12+
.unwrap();
13+
runtime.block_on(future)
14+
}

mountpoint-s3/tests/common/s3.rs

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use aws_sdk_s3::primitives::ByteStream;
2+
use aws_config::{BehaviorVersion, Region};
3+
use rand::RngCore;
4+
use rand_chacha::rand_core::OsRng;
5+
use crate::tokio_block_on;
6+
7+
pub fn get_test_region() -> String {
8+
std::env::var("S3_REGION").expect("Set S3_REGION to run integration tests")
9+
}
10+
11+
pub fn get_standard_bucket() -> String {
12+
std::env::var("S3_BUCKET_NAME").expect("Set S3_BUCKET_NAME to run integration tests")
13+
}
14+
15+
pub fn get_test_bucket_forbidden() -> String {
16+
std::env::var("S3_FORBIDDEN_BUCKET_NAME").expect("Set S3_FORBIDDEN_BUCKET_NAME to run integration tests")
17+
}
18+
19+
pub fn get_test_kms_key_id() -> String {
20+
std::env::var("KMS_TEST_KEY_ID").expect("Set KMS_TEST_KEY_ID to run integration tests")
21+
}
22+
23+
// Get a region other than what configured in S3_REGION
24+
pub fn get_non_test_region() -> String {
25+
match get_test_region().as_str() {
26+
"us-east-1" => String::from("us-west-2"),
27+
_ => String::from("us-east-1"),
28+
}
29+
}
30+
31+
/// Optional config for testing against a custom endpoint url
32+
pub fn get_test_endpoint_url() -> Option<String> {
33+
if cfg!(feature = "s3express_tests") {
34+
std::env::var("S3_EXPRESS_ONE_ZONE_ENDPOINT_URL")
35+
.ok()
36+
.filter(|str| !str.is_empty())
37+
} else {
38+
std::env::var("S3_ENDPOINT_URL").ok().filter(|str| !str.is_empty())
39+
}
40+
}
41+
42+
pub async fn get_test_sdk_client(region: &str) -> aws_sdk_s3::Client {
43+
let mut sdk_config = aws_config::defaults(BehaviorVersion::latest()).region(Region::new(region.to_owned()));
44+
if let Some(endpoint_url) = get_test_endpoint_url() {
45+
sdk_config = sdk_config.endpoint_url(endpoint_url);
46+
}
47+
aws_sdk_s3::Client::new(&sdk_config.load().await)
48+
}
49+
50+
pub fn create_objects(bucket: &str, prefix: &str, region: &str, key: &str, value: &[u8]) {
51+
let sdk_client = tokio_block_on(get_test_sdk_client(region));
52+
let full_key = format!("{prefix}{key}");
53+
tokio_block_on(
54+
sdk_client
55+
.put_object()
56+
.bucket(bucket)
57+
.key(full_key)
58+
.body(ByteStream::from(value.to_vec()))
59+
.send(),
60+
)
61+
.unwrap();
62+
}
63+
64+
pub fn get_test_bucket_and_prefix(test_name: &str) -> (String, String) {
65+
let bucket = get_test_bucket();
66+
let prefix = get_test_prefix(test_name);
67+
68+
(bucket, prefix)
69+
}
70+
71+
pub fn get_test_prefix(test_name: &str) -> String {
72+
// Generate a random nonce to make sure this prefix is truly unique
73+
let nonce = OsRng.next_u64();
74+
75+
// Prefix always has a trailing "/" to keep meaning in sync with the S3 API.
76+
let prefix = std::env::var("S3_BUCKET_TEST_PREFIX").unwrap_or(String::from("mountpoint-test/"));
77+
assert!(prefix.ends_with('/'), "S3_BUCKET_TEST_PREFIX should end in '/'");
78+
79+
format!("{prefix}{test_name}/{nonce}/")
80+
}
81+
82+
pub fn get_test_bucket() -> String {
83+
#[cfg(not(feature = "s3express_tests"))]
84+
return get_standard_bucket();
85+
#[cfg(feature = "s3express_tests")]
86+
return get_express_bucket();
87+
}

mountpoint-s3/tests/fork_test.rs

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ use std::{path::PathBuf, process::Command};
1818
use tempfile::NamedTempFile;
1919
use test_case::test_case;
2020

21+
mod common;
22+
2123
use crate::common::creds::{get_sdk_default_chain_creds, get_subsession_iam_role};
2224
use crate::common::fuse::{mount_for_passing_fuse_fd, read_dir_to_entry_names};
2325
use crate::common::s3::{

0 commit comments

Comments
 (0)