mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 15:37:02 +00:00
feat(site-replication): support custom TLS peers (#4802)
* feat(madmin): add site replication TLS settings * feat(site-replication): support custom TLS peers * test(site-replication): remove redundant clones * test(site-replication): avoid needless resolver collection
This commit is contained in:
@@ -136,7 +136,7 @@ test-group = 'e2e-reliability'
|
|||||||
# the nightly profile derives its set as "the replication module MINUS this
|
# the nightly profile derives its set as "the replication module MINUS this
|
||||||
# allowlist", so any new replication test lands in nightly by default (never
|
# allowlist", so any new replication test lands in nightly by default (never
|
||||||
# silently unrun) until it is explicitly blessed as fast here. Keep the two
|
# silently unrun) until it is explicitly blessed as fast here. Keep the two
|
||||||
# regexes byte-identical. Count invariant: 20 here + 18 nightly = 38 total
|
# regexes byte-identical. Count invariant: 20 here + 20 nightly = 40 total
|
||||||
# (authority: `cargo nextest list`; docs/testing/e2e-suite-inventory.md).
|
# (authority: `cargo nextest list`; docs/testing/e2e-suite-inventory.md).
|
||||||
# HISTORY (2026-07-11): the 20 fast tests were briefly pulled out of this lane
|
# HISTORY (2026-07-11): the 20 fast tests were briefly pulled out of this lane
|
||||||
# (#4724) because they set a loopback (127.0.0.1) replication target that the
|
# (#4724) because they set a loopback (127.0.0.1) replication target that the
|
||||||
@@ -162,7 +162,7 @@ fail-fast = false
|
|||||||
#
|
#
|
||||||
# * 8 bucket-replication data-plane tests — they PUT/delete objects and poll
|
# * 8 bucket-replication data-plane tests — they PUT/delete objects and poll
|
||||||
# until source and target converge; two replicate over HTTPS.
|
# until source and target converge; two replicate over HTTPS.
|
||||||
# * 9 `_real_dual_node` site-replication tests — each spawns TWO full rustfs
|
# * 11 `_real_dual_node` site-replication tests — each spawns TWO full rustfs
|
||||||
# servers and drives the cross-process site-replication control plane.
|
# servers and drives the cross-process site-replication control plane.
|
||||||
# * 1 `_real_single_node` service-account round-trip test.
|
# * 1 `_real_single_node` service-account round-trip test.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -536,6 +536,7 @@ async fn start_https_rustfs_server(env: &mut RustFSTestEnvironment, tls_dir: &Pa
|
|||||||
.env("RUST_LOG", "rustfs=info,rustfs_notify=debug")
|
.env("RUST_LOG", "rustfs=info,rustfs_notify=debug")
|
||||||
.env("RUSTFS_TLS_PATH", tls_dir)
|
.env("RUSTFS_TLS_PATH", tls_dir)
|
||||||
.env("RUSTFS_CONSOLE_ENABLE", "false")
|
.env("RUSTFS_CONSOLE_ENABLE", "false")
|
||||||
|
.env("RUSTFS_REPLICATION_ALLOW_LOOPBACK_TARGET", "true")
|
||||||
.args([
|
.args([
|
||||||
"--address",
|
"--address",
|
||||||
&env.address,
|
&env.address,
|
||||||
@@ -649,7 +650,7 @@ async fn wait_for_replicated_object_over_https(
|
|||||||
}
|
}
|
||||||
status if tokio::time::Instant::now() < deadline => {
|
status if tokio::time::Instant::now() < deadline => {
|
||||||
let body = response.text().await.unwrap_or_default();
|
let body = response.text().await.unwrap_or_default();
|
||||||
if body.contains("NoSuchKey") || body.contains("NotFound") {
|
if body.contains("NoSuchKey") || body.contains("NoSuchBucket") || body.contains("NotFound") {
|
||||||
sleep(Duration::from_secs(1)).await;
|
sleep(Duration::from_secs(1)).await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2932,6 +2933,178 @@ async fn test_replication_recovers_after_runtime_target_cache_is_cleared() -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_site_replication_allows_self_signed_https_with_skip_tls_verify_real_dual_node() -> TestResult {
|
||||||
|
init_logging();
|
||||||
|
|
||||||
|
let mut source_env = new_replication_source_env().await?;
|
||||||
|
source_env
|
||||||
|
.start_rustfs_server_with_env(vec![], LOOPBACK_REPLICATION_TARGET_ENV)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut target_env = new_replication_https_target_env().await?;
|
||||||
|
let tls_dir = std::path::PathBuf::from(&target_env.temp_dir).join("tls");
|
||||||
|
let target_host = target_env
|
||||||
|
.url
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.split(':')
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| std::io::Error::other("target HTTPS URL missing host"))?
|
||||||
|
.to_string();
|
||||||
|
generate_self_signed_tls_material(&tls_dir, &target_host).await?;
|
||||||
|
start_https_rustfs_server(&mut target_env, &tls_dir).await?;
|
||||||
|
let https_client = insecure_https_client()?;
|
||||||
|
wait_for_https_server_ready(&https_client, &target_env).await?;
|
||||||
|
|
||||||
|
let source_site = PeerSite {
|
||||||
|
name: "source-site".to_string(),
|
||||||
|
endpoint: source_env.url.clone(),
|
||||||
|
access_key: source_env.access_key.clone(),
|
||||||
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let target_site = PeerSite {
|
||||||
|
name: "target-site".to_string(),
|
||||||
|
endpoint: target_env.url.clone(),
|
||||||
|
access_key: target_env.access_key.clone(),
|
||||||
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_error = site_replication_add(&source_env, &[source_site.clone(), target_site.clone()])
|
||||||
|
.await
|
||||||
|
.expect_err("site replication add must reject an untrusted self-signed HTTPS peer");
|
||||||
|
let add_error = add_error.to_string();
|
||||||
|
assert!(
|
||||||
|
add_error.contains("400 Bad Request"),
|
||||||
|
"unexpected untrusted self-signed peer error: {add_error}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
add_error.to_ascii_lowercase().contains("tls") || add_error.to_ascii_lowercase().contains("certificate"),
|
||||||
|
"self-signed peer rejection did not report a TLS certificate error: {add_error}"
|
||||||
|
);
|
||||||
|
let disabled = wait_for_site_replication_disabled(&source_env).await?;
|
||||||
|
assert!(!disabled.enabled && disabled.sites.is_empty());
|
||||||
|
|
||||||
|
let add_status = site_replication_add(
|
||||||
|
&source_env,
|
||||||
|
&[
|
||||||
|
source_site,
|
||||||
|
PeerSite {
|
||||||
|
skip_tls_verify: true,
|
||||||
|
..target_site
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert!(add_status.success, "unexpected site add result: {add_status:?}");
|
||||||
|
let _source_info = wait_for_site_replication_enabled(&source_env, 2).await?;
|
||||||
|
|
||||||
|
let source_client = source_env.create_s3_client();
|
||||||
|
let bucket = "site-repl-self-signed-tls";
|
||||||
|
let key = "self-signed.txt";
|
||||||
|
let body = "site replication over self-signed https";
|
||||||
|
source_client.create_bucket().bucket(bucket).send().await?;
|
||||||
|
source_client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(ByteStream::from(body.as_bytes().to_vec()))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
wait_for_replicated_object_over_https(&https_client, &target_env, bucket, key, body).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_site_replication_allows_private_ca_https_with_ca_cert_pem_real_dual_node() -> TestResult {
|
||||||
|
init_logging();
|
||||||
|
|
||||||
|
let mut source_env = new_replication_source_env().await?;
|
||||||
|
source_env
|
||||||
|
.start_rustfs_server_with_env(vec![], LOOPBACK_REPLICATION_TARGET_ENV)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut target_env = new_replication_https_target_env().await?;
|
||||||
|
let tls_dir = std::path::PathBuf::from(&target_env.temp_dir).join("tls");
|
||||||
|
let target_host = target_env
|
||||||
|
.url
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.split(':')
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| std::io::Error::other("target HTTPS URL missing host"))?
|
||||||
|
.to_string();
|
||||||
|
let ca_cert_pem = generate_private_ca_tls_material(&tls_dir, &target_host).await?;
|
||||||
|
start_https_rustfs_server(&mut target_env, &tls_dir).await?;
|
||||||
|
let https_client = trusted_https_client(&ca_cert_pem)?;
|
||||||
|
wait_for_https_server_ready(&https_client, &target_env).await?;
|
||||||
|
|
||||||
|
let source_site = PeerSite {
|
||||||
|
name: "source-site".to_string(),
|
||||||
|
endpoint: source_env.url.clone(),
|
||||||
|
access_key: source_env.access_key.clone(),
|
||||||
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let target_site = PeerSite {
|
||||||
|
name: "target-site".to_string(),
|
||||||
|
endpoint: target_env.url.clone(),
|
||||||
|
access_key: target_env.access_key.clone(),
|
||||||
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_error = site_replication_add(&source_env, &[source_site.clone(), target_site.clone()])
|
||||||
|
.await
|
||||||
|
.expect_err("site replication add must reject a private CA HTTPS peer without caCertPem");
|
||||||
|
let add_error = add_error.to_string();
|
||||||
|
assert!(
|
||||||
|
add_error.contains("400 Bad Request"),
|
||||||
|
"unexpected untrusted private CA peer error: {add_error}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
add_error.to_ascii_lowercase().contains("tls") || add_error.to_ascii_lowercase().contains("certificate"),
|
||||||
|
"private CA peer rejection did not report a TLS certificate error: {add_error}"
|
||||||
|
);
|
||||||
|
let disabled = wait_for_site_replication_disabled(&source_env).await?;
|
||||||
|
assert!(!disabled.enabled && disabled.sites.is_empty());
|
||||||
|
|
||||||
|
let add_status = site_replication_add(
|
||||||
|
&source_env,
|
||||||
|
&[
|
||||||
|
source_site,
|
||||||
|
PeerSite {
|
||||||
|
ca_cert_pem,
|
||||||
|
..target_site
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert!(add_status.success, "unexpected site add result: {add_status:?}");
|
||||||
|
let _source_info = wait_for_site_replication_enabled(&source_env, 2).await?;
|
||||||
|
|
||||||
|
let source_client = source_env.create_s3_client();
|
||||||
|
let bucket = "site-repl-private-ca-tls";
|
||||||
|
let key = "private-ca.txt";
|
||||||
|
let body = "site replication over private ca https";
|
||||||
|
source_client.create_bucket().bucket(bucket).send().await?;
|
||||||
|
source_client
|
||||||
|
.put_object()
|
||||||
|
.bucket(bucket)
|
||||||
|
.key(key)
|
||||||
|
.body(ByteStream::from(body.as_bytes().to_vec()))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
wait_for_replicated_object_over_https(&https_client, &target_env, bucket, key, body).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
async fn test_site_replication_resync_start_cancel_restart_real_dual_node() -> Result<(), Box<dyn Error + Send + Sync>> {
|
async fn test_site_replication_resync_start_cancel_restart_real_dual_node() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
@@ -2963,12 +3136,14 @@ async fn test_site_replication_resync_start_cancel_restart_real_dual_node() -> R
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3092,18 +3267,21 @@ async fn test_site_replication_edit_and_status_peer_state_real_three_node() -> R
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "relay-site".to_string(),
|
name: "relay-site".to_string(),
|
||||||
endpoint: relay_env.url.clone(),
|
endpoint: relay_env.url.clone(),
|
||||||
access_key: relay_env.access_key.clone(),
|
access_key: relay_env.access_key.clone(),
|
||||||
secret_key: relay_env.secret_key.clone(),
|
secret_key: relay_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3326,12 +3504,14 @@ async fn test_site_replication_remove_all_real_dual_node() -> Result<(), Box<dyn
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3434,12 +3614,14 @@ async fn test_site_replication_state_edit_fresh_and_stale_real_dual_node() -> Re
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3545,12 +3727,14 @@ async fn test_site_replication_replicates_object_with_bucket_versioning_real_dua
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3627,12 +3811,14 @@ async fn test_site_replication_replicates_policy_backed_user_access_real_dual_no
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3713,12 +3899,14 @@ async fn test_site_replication_replicates_group_policy_backed_access_real_dual_n
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3841,12 +4029,14 @@ async fn test_site_replication_replicates_multiple_service_accounts_real_dual_no
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3943,12 +4133,14 @@ async fn test_site_replication_replicates_service_accounts_created_from_sts_sess
|
|||||||
endpoint: source_env.url.clone(),
|
endpoint: source_env.url.clone(),
|
||||||
access_key: source_env.access_key.clone(),
|
access_key: source_env.access_key.clone(),
|
||||||
secret_key: source_env.secret_key.clone(),
|
secret_key: source_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
PeerSite {
|
PeerSite {
|
||||||
name: "target-site".to_string(),
|
name: "target-site".to_string(),
|
||||||
endpoint: target_env.url.clone(),
|
endpoint: target_env.url.clone(),
|
||||||
access_key: target_env.access_key.clone(),
|
access_key: target_env.access_key.clone(),
|
||||||
secret_key: target_env.secret_key.clone(),
|
secret_key: target_env.secret_key.clone(),
|
||||||
|
..Default::default()
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1023,20 +1023,49 @@ fn build_insecure_aws_s3_http_client() -> SharedHttpClient {
|
|||||||
http_client_fn(move |_settings, _components| connector.clone())
|
http_client_fn(move |_settings, _components| connector.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_aws_s3_http_client_from_target_ca_pem(ca_cert_pem: &str) -> Result<SharedHttpClient, BucketTargetError> {
|
fn validate_ca_pem_bundle(ca_cert_pem: &[u8]) -> Result<(), String> {
|
||||||
let certs = rustls_pki_types::CertificateDer::pem_slice_iter(ca_cert_pem.as_bytes())
|
let certs = rustls_pki_types::CertificateDer::pem_slice_iter(ca_cert_pem)
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(|err| BucketTargetError::Io(std::io::Error::other(format!("invalid target CA PEM: {err}"))))?;
|
.map_err(|err| format!("invalid PEM encoding: {err}"))?;
|
||||||
|
|
||||||
if certs.is_empty() {
|
if certs.is_empty() {
|
||||||
return Err(BucketTargetError::Io(std::io::Error::other(
|
return Err("no certificates found".to_string());
|
||||||
"invalid target CA PEM: no certificates found",
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut trust_store = smithy_tls::TrustStore::empty();
|
// Smithy's rustls adapter defers parsing custom certificates and assumes
|
||||||
trust_store.add_pem_certificate(ca_cert_pem.as_bytes());
|
// they are valid when the HTTPS connector is built. Validate every DER
|
||||||
|
// certificate first so malformed configuration is reported rather than
|
||||||
|
// reaching an `expect` in the dependency.
|
||||||
|
let mut validation_store = rustls::RootCertStore::empty();
|
||||||
|
for cert in certs {
|
||||||
|
validation_store
|
||||||
|
.add(cert)
|
||||||
|
.map_err(|err| format!("invalid X.509 certificate: {err}"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_target_ca_pem(ca_cert_pem: &str) -> Result<(), BucketTargetError> {
|
||||||
|
validate_ca_pem_bundle(ca_cert_pem.as_bytes())
|
||||||
|
.map_err(|err| BucketTargetError::Io(std::io::Error::other(format!("invalid target CA PEM: {err}"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compose_replication_trust_store(certificate_bundles: impl IntoIterator<Item = Vec<u8>>) -> (smithy_tls::TrustStore, usize) {
|
||||||
|
// `TrustStore::default()` keeps the platform-native roots enabled. Target
|
||||||
|
// and RUSTFS_TLS_PATH certificates extend that baseline instead of
|
||||||
|
// replacing it with a target-specific trust island.
|
||||||
|
let mut trust_store = smithy_tls::TrustStore::default();
|
||||||
|
let mut custom_bundle_count = 0;
|
||||||
|
for pem in certificate_bundles {
|
||||||
|
trust_store.add_pem_certificate(pem);
|
||||||
|
custom_bundle_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
(trust_store, custom_bundle_count)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_aws_s3_http_client_with_trust_store(trust_store: smithy_tls::TrustStore) -> Result<SharedHttpClient, BucketTargetError> {
|
||||||
let tls_context = smithy_tls::TlsContext::builder()
|
let tls_context = smithy_tls::TlsContext::builder()
|
||||||
.with_trust_store(trust_store)
|
.with_trust_store(trust_store)
|
||||||
.build()
|
.build()
|
||||||
@@ -1048,6 +1077,60 @@ fn build_aws_s3_http_client_from_target_ca_pem(ca_cert_pem: &str) -> Result<Shar
|
|||||||
.build_https())
|
.build_https())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_tls_path_ca_bundles(tls_dir: &Path, trust_leaf_cert_as_ca: bool) -> Vec<Vec<u8>> {
|
||||||
|
let mut certificate_bundles = Vec::new();
|
||||||
|
|
||||||
|
let ca_path = tls_dir.join(RUSTFS_CA_CERT);
|
||||||
|
match tokio::fs::read(&ca_path).await {
|
||||||
|
Ok(pem) => match validate_ca_pem_bundle(&pem) {
|
||||||
|
Ok(()) => certificate_bundles.push(pem),
|
||||||
|
Err(err) => warn!("ignoring invalid custom CA bundle {:?} for replication client: {}", ca_path, err),
|
||||||
|
},
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(e) => warn!("failed to read custom CA bundle {:?} for replication client: {}", ca_path, e),
|
||||||
|
}
|
||||||
|
|
||||||
|
if trust_leaf_cert_as_ca {
|
||||||
|
let leaf_cert_path = tls_dir.join(RUSTFS_TLS_CERT);
|
||||||
|
match tokio::fs::read(&leaf_cert_path).await {
|
||||||
|
Ok(pem) => match validate_ca_pem_bundle(&pem) {
|
||||||
|
Ok(()) => certificate_bundles.push(pem),
|
||||||
|
Err(err) => warn!(
|
||||||
|
"ignoring invalid leaf certificate {:?} for replication client trust store: {}",
|
||||||
|
leaf_cert_path, err
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(e) => warn!("failed to read leaf cert {:?} for replication client trust store: {}", leaf_cert_path, e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
certificate_bundles
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_configured_tls_ca_bundles() -> Vec<Vec<u8>> {
|
||||||
|
let tls_path = rustfs_utils::get_env_str(rustfs_config::ENV_RUSTFS_TLS_PATH, rustfs_config::DEFAULT_RUSTFS_TLS_PATH);
|
||||||
|
if tls_path.is_empty() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
load_tls_path_ca_bundles(
|
||||||
|
Path::new(&tls_path),
|
||||||
|
rustfs_utils::get_env_bool(ENV_TRUST_LEAF_CERT_AS_CA, DEFAULT_TRUST_LEAF_CERT_AS_CA),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_aws_s3_http_client_from_target_ca_pem(ca_cert_pem: &str) -> Result<SharedHttpClient, BucketTargetError> {
|
||||||
|
validate_target_ca_pem(ca_cert_pem)?;
|
||||||
|
|
||||||
|
let mut certificate_bundles = load_configured_tls_ca_bundles().await;
|
||||||
|
certificate_bundles.push(ca_cert_pem.as_bytes().to_vec());
|
||||||
|
let (trust_store, _) = compose_replication_trust_store(certificate_bundles);
|
||||||
|
|
||||||
|
build_aws_s3_http_client_with_trust_store(trust_store)
|
||||||
|
}
|
||||||
|
|
||||||
async fn build_aws_s3_http_client_for_target(target: &BucketTarget) -> Result<Option<SharedHttpClient>, BucketTargetError> {
|
async fn build_aws_s3_http_client_for_target(target: &BucketTarget) -> Result<Option<SharedHttpClient>, BucketTargetError> {
|
||||||
if !target.secure {
|
if !target.secure {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -1058,62 +1141,28 @@ async fn build_aws_s3_http_client_for_target(target: &BucketTarget) -> Result<Op
|
|||||||
}
|
}
|
||||||
|
|
||||||
if has_custom_ca_pem(target) {
|
if has_custom_ca_pem(target) {
|
||||||
return build_aws_s3_http_client_from_target_ca_pem(&target.ca_cert_pem).map(Some);
|
return build_aws_s3_http_client_from_target_ca_pem(&target.ca_cert_pem)
|
||||||
|
.await
|
||||||
|
.map(Some);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(build_aws_s3_http_client_from_tls_path().await)
|
Ok(build_aws_s3_http_client_from_tls_path().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn build_aws_s3_http_client_from_tls_path() -> Option<aws_sdk_s3::config::SharedHttpClient> {
|
async fn build_aws_s3_http_client_from_tls_path() -> Option<aws_sdk_s3::config::SharedHttpClient> {
|
||||||
let tls_path = rustfs_utils::get_env_str(rustfs_config::ENV_RUSTFS_TLS_PATH, rustfs_config::DEFAULT_RUSTFS_TLS_PATH);
|
let certificate_bundles = load_configured_tls_ca_bundles().await;
|
||||||
if tls_path.is_empty() {
|
if certificate_bundles.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tls_dir = Path::new(&tls_path);
|
let (trust_store, _) = compose_replication_trust_store(certificate_bundles);
|
||||||
let mut trust_store = smithy_tls::TrustStore::empty();
|
match build_aws_s3_http_client_with_trust_store(trust_store) {
|
||||||
let mut has_custom_certs = false;
|
Ok(client) => Some(client),
|
||||||
|
|
||||||
let ca_path = tls_dir.join(RUSTFS_CA_CERT);
|
|
||||||
match tokio::fs::read(&ca_path).await {
|
|
||||||
Ok(pem) => {
|
|
||||||
trust_store.add_pem_certificate(pem);
|
|
||||||
has_custom_certs = true;
|
|
||||||
}
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
|
||||||
Err(e) => warn!("failed to read custom CA bundle {:?} for replication client: {}", ca_path, e),
|
|
||||||
}
|
|
||||||
|
|
||||||
if rustfs_utils::get_env_bool(ENV_TRUST_LEAF_CERT_AS_CA, DEFAULT_TRUST_LEAF_CERT_AS_CA) {
|
|
||||||
let leaf_cert_path = tls_dir.join(RUSTFS_TLS_CERT);
|
|
||||||
match tokio::fs::read(&leaf_cert_path).await {
|
|
||||||
Ok(pem) => {
|
|
||||||
trust_store.add_pem_certificate(pem);
|
|
||||||
has_custom_certs = true;
|
|
||||||
}
|
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
|
||||||
Err(e) => warn!("failed to read leaf cert {:?} for replication client trust store: {}", leaf_cert_path, e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !has_custom_certs {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tls_context = match smithy_tls::TlsContext::builder().with_trust_store(trust_store).build() {
|
|
||||||
Ok(ctx) => ctx,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("failed to build AWS SDK TLS context for replication client: {}", e);
|
warn!("failed to build AWS SDK TLS context for replication client: {}", e);
|
||||||
return None;
|
None
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
Some(
|
|
||||||
SmithyHttpClientBuilder::new()
|
|
||||||
.tls_provider(smithy_tls::Provider::rustls(smithy_tls::rustls_provider::CryptoMode::AwsLc))
|
|
||||||
.tls_context(tls_context)
|
|
||||||
.build_https(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_force_path_style(target: &BucketTarget) -> bool {
|
fn should_force_path_style(target: &BucketTarget) -> bool {
|
||||||
@@ -1920,6 +1969,63 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use rcgen::generate_simple_self_signed;
|
use rcgen::generate_simple_self_signed;
|
||||||
|
|
||||||
|
fn spawn_single_request_https_server(cert: &rcgen::CertifiedKey<rcgen::KeyPair>) -> (u16, std::thread::JoinHandle<()>) {
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
ensure_rustls_crypto_provider();
|
||||||
|
let listener = std::net::TcpListener::bind(("127.0.0.1", 0)).expect("test TLS listener should bind");
|
||||||
|
let port = listener
|
||||||
|
.local_addr()
|
||||||
|
.expect("test TLS listener should have an address")
|
||||||
|
.port();
|
||||||
|
let server_config = rustls::ServerConfig::builder()
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(
|
||||||
|
vec![cert.cert.der().clone()],
|
||||||
|
rustls_pki_types::PrivateKeyDer::try_from(cert.signing_key.serialize_der())
|
||||||
|
.expect("test TLS private key should convert"),
|
||||||
|
)
|
||||||
|
.expect("test TLS server config should build");
|
||||||
|
|
||||||
|
let handle = std::thread::spawn(move || {
|
||||||
|
let (stream, _) = listener.accept().expect("test TLS client should connect");
|
||||||
|
stream
|
||||||
|
.set_read_timeout(Some(Duration::from_secs(10)))
|
||||||
|
.expect("test TLS read timeout should configure");
|
||||||
|
stream
|
||||||
|
.set_write_timeout(Some(Duration::from_secs(10)))
|
||||||
|
.expect("test TLS write timeout should configure");
|
||||||
|
let connection = rustls::ServerConnection::new(Arc::new(server_config)).expect("test TLS connection should build");
|
||||||
|
let mut stream = rustls::StreamOwned::new(connection, stream);
|
||||||
|
let mut request = [0_u8; 8192];
|
||||||
|
let _ = stream.read(&mut request).expect("test TLS request should be readable");
|
||||||
|
stream
|
||||||
|
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
|
||||||
|
.expect("test TLS response should be written");
|
||||||
|
stream.flush().expect("test TLS response should flush");
|
||||||
|
});
|
||||||
|
|
||||||
|
(port, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn s3_client_with_http_client(port: u16, http_client: SharedHttpClient) -> S3Client {
|
||||||
|
let credentials = SdkCredentials::builder()
|
||||||
|
.access_key_id("test-access")
|
||||||
|
.secret_access_key("test-secret")
|
||||||
|
.provider_name("bucket_target_tls_test")
|
||||||
|
.build();
|
||||||
|
let config = S3Config::builder()
|
||||||
|
.endpoint_url(format!("https://localhost:{port}"))
|
||||||
|
.credentials_provider(SharedCredentialsProvider::new(credentials))
|
||||||
|
.region(SdkRegion::new("us-east-1"))
|
||||||
|
.force_path_style(true)
|
||||||
|
.behavior_version(aws_sdk_s3::config::BehaviorVersion::latest())
|
||||||
|
.http_client(http_client)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
S3Client::from_conf(config)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn replication_target_versioning_enabled_requires_enabled_status() {
|
fn replication_target_versioning_enabled_requires_enabled_status() {
|
||||||
let enabled = BucketVersioningStatus::Enabled;
|
let enabled = BucketVersioningStatus::Enabled;
|
||||||
@@ -2243,6 +2349,78 @@ mod tests {
|
|||||||
assert_eq!(client.endpoint, "https://192.168.1.10:9000");
|
assert_eq!(client.endpoint, "https://192.168.1.10:9000");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn replication_trust_store_composes_system_global_and_target_roots_for_real_tls() {
|
||||||
|
let tls_dir = tempfile::tempdir().expect("temporary TLS directory should be created");
|
||||||
|
let global_ca =
|
||||||
|
generate_simple_self_signed(vec!["localhost".to_string()]).expect("global CA certificate should generate");
|
||||||
|
let target_ca =
|
||||||
|
generate_simple_self_signed(vec!["localhost".to_string()]).expect("target CA certificate should generate");
|
||||||
|
|
||||||
|
tokio::fs::write(tls_dir.path().join(RUSTFS_CA_CERT), global_ca.cert.pem())
|
||||||
|
.await
|
||||||
|
.expect("global CA bundle should be written");
|
||||||
|
|
||||||
|
let mut certificate_bundles = load_tls_path_ca_bundles(tls_dir.path(), false).await;
|
||||||
|
certificate_bundles.push(target_ca.cert.pem().into_bytes());
|
||||||
|
let (trust_store, _) = compose_replication_trust_store(certificate_bundles);
|
||||||
|
assert!(
|
||||||
|
format!("{trust_store:?}").contains("enable_native_roots: true"),
|
||||||
|
"per-target trust must retain the SDK's platform-native roots"
|
||||||
|
);
|
||||||
|
let http_client = build_aws_s3_http_client_with_trust_store(trust_store).expect("composed TLS client should build");
|
||||||
|
|
||||||
|
let (global_port, global_server) = spawn_single_request_https_server(&global_ca);
|
||||||
|
s3_client_with_http_client(global_port, http_client.clone())
|
||||||
|
.head_bucket()
|
||||||
|
.bucket("test-bucket")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("global RUSTFS_TLS_PATH CA should authenticate its TLS server");
|
||||||
|
global_server.join().expect("global CA TLS server should finish");
|
||||||
|
|
||||||
|
let (target_port, target_server) = spawn_single_request_https_server(&target_ca);
|
||||||
|
s3_client_with_http_client(target_port, http_client)
|
||||||
|
.head_bucket()
|
||||||
|
.bucket("test-bucket")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect("per-target CA should authenticate its TLS server alongside global roots");
|
||||||
|
target_server.join().expect("target CA TLS server should finish");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tls_path_leaf_trust_remains_opt_in() {
|
||||||
|
let tls_dir = tempfile::tempdir().expect("temporary TLS directory should be created");
|
||||||
|
let global_ca =
|
||||||
|
generate_simple_self_signed(vec!["global-ca.example".to_string()]).expect("global CA certificate should generate");
|
||||||
|
let trusted_leaf =
|
||||||
|
generate_simple_self_signed(vec!["leaf.example".to_string()]).expect("trusted leaf certificate should generate");
|
||||||
|
tokio::fs::write(tls_dir.path().join(RUSTFS_CA_CERT), global_ca.cert.pem())
|
||||||
|
.await
|
||||||
|
.expect("global CA bundle should be written");
|
||||||
|
tokio::fs::write(tls_dir.path().join(RUSTFS_TLS_CERT), trusted_leaf.cert.pem())
|
||||||
|
.await
|
||||||
|
.expect("trusted leaf certificate should be written");
|
||||||
|
|
||||||
|
assert_eq!(load_tls_path_ca_bundles(tls_dir.path(), true).await.len(), 2);
|
||||||
|
assert_eq!(load_tls_path_ca_bundles(tls_dir.path(), false).await.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn skip_tls_verify_takes_priority_over_invalid_custom_ca_pem() {
|
||||||
|
let client = build_aws_s3_http_client_for_target(&BucketTarget {
|
||||||
|
secure: true,
|
||||||
|
skip_tls_verify: true,
|
||||||
|
ca_cert_pem: "not a pem".to_string(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("skip verification should bypass custom CA parsing");
|
||||||
|
|
||||||
|
assert!(client.is_some(), "secure targets with skip verification need a custom HTTP client");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn get_remote_target_client_internal_rejects_invalid_custom_ca_pem() {
|
async fn get_remote_target_client_internal_rejects_invalid_custom_ca_pem() {
|
||||||
let sys = BucketTargetSys::default();
|
let sys = BucketTargetSys::default();
|
||||||
@@ -2267,6 +2445,31 @@ mod tests {
|
|||||||
assert!(err.to_string().contains("invalid target CA PEM"));
|
assert!(err.to_string().contains("invalid target CA PEM"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn target_ca_rejects_pem_wrapped_invalid_der_before_smithy_builds() {
|
||||||
|
let err = validate_target_ca_pem("-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n")
|
||||||
|
.expect_err("PEM-wrapped invalid DER must be rejected");
|
||||||
|
|
||||||
|
assert!(err.to_string().contains("invalid target CA PEM"));
|
||||||
|
assert!(err.to_string().contains("invalid X.509 certificate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn invalid_global_ca_is_ignored_without_reaching_smithy() {
|
||||||
|
let tls_dir = tempfile::tempdir().expect("temporary TLS directory should be created");
|
||||||
|
tokio::fs::write(
|
||||||
|
tls_dir.path().join(RUSTFS_CA_CERT),
|
||||||
|
b"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("invalid global CA fixture should be written");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
load_tls_path_ca_bundles(tls_dir.path(), false).await.is_empty(),
|
||||||
|
"invalid global CA must fall back to default roots instead of reaching Smithy's panic path"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// backlog#806-16 regression tests for the rolling one-minute latency window.
|
// backlog#806-16 regression tests for the rolling one-minute latency window.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ use crate::{GroupAddRemove, GroupDesc, SRSvcAccCreate, UserInfo};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::fmt;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
pub const SITE_REPL_API_VERSION: &str = "1";
|
pub const SITE_REPL_API_VERSION: &str = "1";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct PeerSite {
|
pub struct PeerSite {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -30,6 +31,21 @@ pub struct PeerSite {
|
|||||||
pub access_key: String,
|
pub access_key: String,
|
||||||
#[serde(rename = "secretKey", default)]
|
#[serde(rename = "secretKey", default)]
|
||||||
pub secret_key: String,
|
pub secret_key: String,
|
||||||
|
#[serde(rename = "skipTlsVerify", default)]
|
||||||
|
pub skip_tls_verify: bool,
|
||||||
|
#[serde(rename = "caCertPem", default)]
|
||||||
|
pub ca_cert_pem: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for PeerSite {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("PeerSite")
|
||||||
|
.field("name", &self.name)
|
||||||
|
.field("endpoint", &self.endpoint)
|
||||||
|
.field("skip_tls_verify", &self.skip_tls_verify)
|
||||||
|
.field("has_custom_ca", &!self.ca_cert_pem.is_empty())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
@@ -106,7 +122,7 @@ pub enum SyncStatus {
|
|||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct PeerInfo {
|
pub struct PeerInfo {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub endpoint: String,
|
pub endpoint: String,
|
||||||
@@ -122,10 +138,26 @@ pub struct PeerInfo {
|
|||||||
pub replicate_ilm_expiry: bool,
|
pub replicate_ilm_expiry: bool,
|
||||||
#[serde(rename = "objectNamingMode", default, skip_serializing_if = "String::is_empty")]
|
#[serde(rename = "objectNamingMode", default, skip_serializing_if = "String::is_empty")]
|
||||||
pub object_naming_mode: String,
|
pub object_naming_mode: String,
|
||||||
|
#[serde(rename = "skipTlsVerify", default)]
|
||||||
|
pub skip_tls_verify: bool,
|
||||||
|
#[serde(rename = "caCertPem", default)]
|
||||||
|
pub ca_cert_pem: String,
|
||||||
#[serde(rename = "apiVersion", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "apiVersion", skip_serializing_if = "Option::is_none")]
|
||||||
pub api_version: Option<String>,
|
pub api_version: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for PeerInfo {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("PeerInfo")
|
||||||
|
.field("endpoint", &self.endpoint)
|
||||||
|
.field("name", &self.name)
|
||||||
|
.field("deployment_id", &self.deployment_id)
|
||||||
|
.field("skip_tls_verify", &self.skip_tls_verify)
|
||||||
|
.field("has_custom_ca", &!self.ca_cert_pem.is_empty())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct SRPolicyMapping {
|
pub struct SRPolicyMapping {
|
||||||
#[serde(rename = "userOrGroup", default)]
|
#[serde(rename = "userOrGroup", default)]
|
||||||
@@ -1167,3 +1199,107 @@ pub struct SiteNetPerfResult {
|
|||||||
#[serde(rename = "nodeResults", default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(rename = "nodeResults", default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub node_results: Vec<SiteNetPerfNodeResult>,
|
pub node_results: Vec<SiteNetPerfNodeResult>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{PeerInfo, PeerSite};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
const TEST_CA_CERT: &str = "-----BEGIN CERTIFICATE-----\ntest-ca\n-----END CERTIFICATE-----";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_tls_fields_default_when_missing_from_legacy_json() {
|
||||||
|
let site: PeerSite = serde_json::from_value(json!({
|
||||||
|
"name": "site-a",
|
||||||
|
"endpoints": "https://site-a.example.com"
|
||||||
|
}))
|
||||||
|
.expect("legacy PeerSite JSON should deserialize");
|
||||||
|
let peer: PeerInfo = serde_json::from_value(json!({
|
||||||
|
"endpoint": "https://site-a.example.com",
|
||||||
|
"name": "site-a",
|
||||||
|
"deploymentID": "deployment-a"
|
||||||
|
}))
|
||||||
|
.expect("legacy PeerInfo JSON should deserialize");
|
||||||
|
|
||||||
|
assert!(!site.skip_tls_verify);
|
||||||
|
assert_eq!(site.ca_cert_pem, "");
|
||||||
|
assert!(!peer.skip_tls_verify);
|
||||||
|
assert_eq!(peer.ca_cert_pem, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_tls_fields_round_trip_with_exact_json_names() {
|
||||||
|
let site_json = json!({
|
||||||
|
"name": "site-a",
|
||||||
|
"endpoints": "https://site-a.example.com",
|
||||||
|
"accessKey": "access-key",
|
||||||
|
"secretKey": "secret-key",
|
||||||
|
"skipTlsVerify": true,
|
||||||
|
"caCertPem": TEST_CA_CERT
|
||||||
|
});
|
||||||
|
let peer_json = json!({
|
||||||
|
"endpoint": "https://site-a.example.com",
|
||||||
|
"name": "site-a",
|
||||||
|
"deploymentID": "deployment-a",
|
||||||
|
"sync": "unknown",
|
||||||
|
"defaultbandwidth": {
|
||||||
|
"bandwidthLimitPerBucket": 0,
|
||||||
|
"set": false
|
||||||
|
},
|
||||||
|
"replicate-ilm-expiry": false,
|
||||||
|
"objectNamingMode": "path",
|
||||||
|
"skipTlsVerify": true,
|
||||||
|
"caCertPem": TEST_CA_CERT
|
||||||
|
});
|
||||||
|
|
||||||
|
let site: PeerSite = serde_json::from_value(site_json.clone()).expect("PeerSite JSON should deserialize");
|
||||||
|
let peer: PeerInfo = serde_json::from_value(peer_json.clone()).expect("PeerInfo JSON should deserialize");
|
||||||
|
|
||||||
|
assert_eq!(serde_json::to_value(site).expect("PeerSite should serialize"), site_json);
|
||||||
|
assert_eq!(serde_json::to_value(peer).expect("PeerInfo should serialize"), peer_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_tls_false_and_empty_ca_are_still_serialized() {
|
||||||
|
let site = serde_json::to_value(PeerSite::default()).expect("PeerSite should serialize");
|
||||||
|
let peer = serde_json::to_value(PeerInfo::default()).expect("PeerInfo should serialize");
|
||||||
|
|
||||||
|
for value in [site, peer] {
|
||||||
|
let object = value.as_object().expect("peer JSON should be an object");
|
||||||
|
assert_eq!(object.get("skipTlsVerify"), Some(&Value::Bool(false)));
|
||||||
|
assert_eq!(object.get("caCertPem"), Some(&Value::String(String::new())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn peer_debug_output_redacts_secrets_and_ca_contents() {
|
||||||
|
let site = PeerSite {
|
||||||
|
name: "site-a".to_owned(),
|
||||||
|
endpoint: "https://site-a.example.com".to_owned(),
|
||||||
|
access_key: "sensitive-access-key".to_owned(),
|
||||||
|
secret_key: "sensitive-secret-key".to_owned(),
|
||||||
|
skip_tls_verify: true,
|
||||||
|
ca_cert_pem: TEST_CA_CERT.to_owned(),
|
||||||
|
};
|
||||||
|
let peer = PeerInfo {
|
||||||
|
endpoint: "https://site-a.example.com".to_owned(),
|
||||||
|
name: "site-a".to_owned(),
|
||||||
|
deployment_id: "deployment-a".to_owned(),
|
||||||
|
skip_tls_verify: false,
|
||||||
|
ca_cert_pem: TEST_CA_CERT.to_owned(),
|
||||||
|
..PeerInfo::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let site_debug = format!("{site:?}");
|
||||||
|
let peer_debug = format!("{peer:?}");
|
||||||
|
|
||||||
|
assert!(!site_debug.contains("BEGIN CERTIFICATE"));
|
||||||
|
assert!(!site_debug.contains("sensitive-access-key"));
|
||||||
|
assert!(!site_debug.contains("sensitive-secret-key"));
|
||||||
|
assert!(site_debug.contains("skip_tls_verify: true"));
|
||||||
|
assert!(site_debug.contains("has_custom_ca: true"));
|
||||||
|
assert!(!peer_debug.contains("BEGIN CERTIFICATE"));
|
||||||
|
assert!(peer_debug.contains("skip_tls_verify: false"));
|
||||||
|
assert!(peer_debug.contains("has_custom_ca: true"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,18 +3,19 @@
|
|||||||
> Authoritative per-module test counts for the `e2e_test` crate (backlog#1149
|
> Authoritative per-module test counts for the `e2e_test` crate (backlog#1149
|
||||||
> ci-4), generated from `cargo nextest list -p e2e_test`. Regenerate with:
|
> ci-4), generated from `cargo nextest list -p e2e_test`. Regenerate with:
|
||||||
> ```bash
|
> ```bash
|
||||||
> cargo nextest list -p e2e_test | grep "^ " | sed "s/^ *//;s/::.*//" | sort | uniq -c
|
> cargo nextest list -p e2e_test --message-format oneline | awk '{split($2,a,"::"); print a[1]}' | sort | uniq -c
|
||||||
> ```
|
> ```
|
||||||
> Modules marked ✅ are in the PR smoke profile `e2e-smoke`
|
> Modules marked ✅ are in the PR smoke profile `e2e-smoke`
|
||||||
> (`.config/nextest.toml`); admission criteria: `crates/e2e_test/README.md`.
|
> (`.config/nextest.toml`); admission criteria: `crates/e2e_test/README.md`.
|
||||||
> 🌙 marks tests in the scheduled `e2e-repl-nightly` profile (backlog#1147
|
> 🌙 marks tests in the scheduled `e2e-repl-nightly` profile (backlog#1147
|
||||||
> repl-1): `replication_extension_test` splits 20 fast tests into the PR smoke
|
> repl-1): `replication_extension_test` splits 20 fast tests into the PR smoke
|
||||||
> lane and 16 slow / `_real_dual_node` / `_real_single_node` tests into the
|
> lane and 20 slow / `_real_dual_node` / `_real_single_node` tests into the
|
||||||
> nightly lane (`.github/workflows/e2e-replication-nightly.yml`).
|
> nightly lane (`.github/workflows/e2e-replication-nightly.yml`).
|
||||||
> Note: counts exclude `#[ignore]`d tests (nextest lists them separately).
|
> Note: counts exclude `#[ignore]`d tests (nextest lists them separately).
|
||||||
|
|
||||||
| module | tests | PR smoke |
|
| module | tests | PR smoke |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| admin_auth_test | 3 | |
|
||||||
| admin_timeout_regression_test | 1 | |
|
| admin_timeout_regression_test | 1 | |
|
||||||
| anonymous_access_test | 3 | ✅ |
|
| anonymous_access_test | 3 | ✅ |
|
||||||
| archive_download_integrity_test | 13 | |
|
| archive_download_integrity_test | 13 | |
|
||||||
@@ -29,10 +30,12 @@
|
|||||||
| copy_object_version_restore_test | 1 | |
|
| copy_object_version_restore_test | 1 | |
|
||||||
| copy_source_invalid_date_test | 1 | ✅ |
|
| copy_source_invalid_date_test | 1 | ✅ |
|
||||||
| create_bucket_region_test | 2 | ✅ |
|
| create_bucket_region_test | 2 | ✅ |
|
||||||
|
| degraded_read_eof_regression_test | 3 | |
|
||||||
| delete_marker_migration_semantics_test | 2 | ✅ |
|
| delete_marker_migration_semantics_test | 2 | ✅ |
|
||||||
| delete_object_no_content_length_test | 1 | |
|
| delete_object_no_content_length_test | 1 | |
|
||||||
| delete_objects_versioning_test | 2 | ✅ |
|
| delete_objects_versioning_test | 2 | ✅ |
|
||||||
| existing_object_tag_policy_test | 4 | |
|
| existing_object_tag_policy_test | 4 | |
|
||||||
|
| get_codec_streaming_compat_test | 1 | |
|
||||||
| head_object_consistency_test | 1 | ✅ |
|
| head_object_consistency_test | 1 | ✅ |
|
||||||
| head_object_range_test | 1 | ✅ |
|
| head_object_range_test | 1 | ✅ |
|
||||||
| heal_erasure_disk_rebuild_test | 3 | |
|
| heal_erasure_disk_rebuild_test | 3 | |
|
||||||
@@ -47,14 +50,16 @@
|
|||||||
| mc_mirror_small_bucket_test | 1 | |
|
| mc_mirror_small_bucket_test | 1 | |
|
||||||
| multipart_auth_test | 109 | |
|
| multipart_auth_test | 109 | |
|
||||||
| namespace_lock_quorum_test | 2 | |
|
| namespace_lock_quorum_test | 2 | |
|
||||||
|
| negative_sigv4_test | 6 | |
|
||||||
| object_lambda_test | 16 | |
|
| object_lambda_test | 16 | |
|
||||||
| object_lock | 33 | |
|
| object_lock | 33 | |
|
||||||
| overwrite_cleanup_regression_test | 1 | |
|
| overwrite_cleanup_regression_test | 1 | |
|
||||||
|
| presigned_negative_test | 7 | ✅ |
|
||||||
| protocols | 16 | |
|
| protocols | 16 | |
|
||||||
| quota_test | 13 | |
|
| quota_test | 13 | |
|
||||||
| reliability_disk_fault_test | 3 | |
|
| reliability_disk_fault_test | 3 | |
|
||||||
| reliant | 6 | |
|
| reliant | 9 | 3 ✅ |
|
||||||
| replication_extension_test | 38 | 20 ✅ +18 🌙 |
|
| replication_extension_test | 40 | 20 ✅ +20 🌙 |
|
||||||
| security_boundary_test | 4 | |
|
| security_boundary_test | 4 | |
|
||||||
| server_startup_failfast_test | 1 | |
|
| server_startup_failfast_test | 1 | |
|
||||||
| snowball_auto_extract_test | 6 | |
|
| snowball_auto_extract_test | 6 | |
|
||||||
@@ -63,4 +68,4 @@
|
|||||||
| tls_gen | 3 | |
|
| tls_gen | 3 | |
|
||||||
| version_id_regression_test | 10 | ✅ |
|
| version_id_regression_test | 10 | ✅ |
|
||||||
|
|
||||||
**Total listed: 396 tests across 47 modules · PR smoke subset: 83 tests / 18 modules** (17 full modules + 20 of `replication_extension_test`) **· nightly `e2e-repl-nightly`: 18 tests** · generated 2026-07-12.
|
**Total listed: 421 tests across 52 modules · PR smoke subset: 93 tests / 20 modules** (18 full modules + 3 `reliant::lifecycle` tests + 20 of `replication_extension_test`) **· nightly `e2e-repl-nightly`: 20 tests** · generated 2026-07-14.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -160,6 +160,8 @@ mod tests {
|
|||||||
default_bandwidth: BucketBandwidth::default(),
|
default_bandwidth: BucketBandwidth::default(),
|
||||||
replicate_ilm_expiry: false,
|
replicate_ilm_expiry: false,
|
||||||
object_naming_mode: String::new(),
|
object_naming_mode: String::new(),
|
||||||
|
skip_tls_verify: false,
|
||||||
|
ca_cert_pem: String::new(),
|
||||||
api_version: None,
|
api_version: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,6 +206,8 @@ mod tests {
|
|||||||
PeerInfo {
|
PeerInfo {
|
||||||
api_version: Some("v1".to_string()),
|
api_version: Some("v1".to_string()),
|
||||||
replicate_ilm_expiry: true,
|
replicate_ilm_expiry: true,
|
||||||
|
skip_tls_verify: true,
|
||||||
|
ca_cert_pem: "fallback-ca".to_string(),
|
||||||
..peer("remote-http", "http://node-a.example.com:9000")
|
..peer("remote-http", "http://node-a.example.com:9000")
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -226,6 +230,8 @@ mod tests {
|
|||||||
assert_eq!(peer.deployment_id, "remote-http");
|
assert_eq!(peer.deployment_id, "remote-http");
|
||||||
assert_eq!(peer.api_version.as_deref(), Some("v1"));
|
assert_eq!(peer.api_version.as_deref(), Some("v1"));
|
||||||
assert!(peer.replicate_ilm_expiry);
|
assert!(peer.replicate_ilm_expiry);
|
||||||
|
assert!(!peer.skip_tls_verify);
|
||||||
|
assert_eq!(peer.ca_cert_pem, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user