mirror of
https://github.com/rustfs/rustfs.git
synced 2026-11-08 03:23:23 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a7e5181e2e | |||
| d45468ecab | |||
| 4a69fd2e2e |
@@ -17,7 +17,7 @@
|
||||
use crate::bucket::metadata::BUCKET_METADATA_FILE;
|
||||
use crate::bucket::replication::ReplicationMigrationBridge;
|
||||
use crate::disk::{BUCKET_META_PREFIX, MIGRATING_META_BUCKET, RUSTFS_META_BUCKET};
|
||||
use crate::error::{Error, Result, is_err_strict_not_found, is_err_strict_volume_not_found};
|
||||
use crate::error::{Error, Result, is_err_strict_not_found, is_err_strict_volume_not_found, to_object_err};
|
||||
use crate::object_api::{GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader};
|
||||
use crate::storage_api_contracts::{
|
||||
bucket::{BucketInfo, BucketOperations, BucketOptions},
|
||||
@@ -334,6 +334,14 @@ async fn migration_target_exists<S: EcstoreObjectOperations>(store: &S, path: &s
|
||||
}
|
||||
}
|
||||
|
||||
/// Names the target record in a failed migration write, as the existence
|
||||
/// probe's error already does. The commit path returns bare disk errors, and on
|
||||
/// Windows a blocked record is first seen here: reading through a regular-file
|
||||
/// ancestor reports the record missing instead of refusing access to it.
|
||||
fn migration_write_error(err: Error, path: &str) -> Error {
|
||||
to_object_err(err, vec![RUSTFS_META_BUCKET, path])
|
||||
}
|
||||
|
||||
async fn migrate_one_if_missing<S>(
|
||||
store: Arc<S>,
|
||||
opts: &ObjectOptions,
|
||||
@@ -369,7 +377,10 @@ where
|
||||
.unwrap_or(data);
|
||||
|
||||
let mut put_data = PutObjReader::from_vec(data);
|
||||
store.put_object(RUSTFS_META_BUCKET, path, &mut put_data, opts).await?;
|
||||
store
|
||||
.put_object(RUSTFS_META_BUCKET, path, &mut put_data, opts)
|
||||
.await
|
||||
.map_err(|err| migration_write_error(err, path))?;
|
||||
info!("Migrated {label}");
|
||||
Ok(())
|
||||
}
|
||||
@@ -481,7 +492,10 @@ where
|
||||
Err(_) => return Err(MigrationMetadataError::Incompatible(path.to_owned()).into()),
|
||||
};
|
||||
let mut put_data = PutObjReader::from_vec(data);
|
||||
store.put_object(RUSTFS_META_BUCKET, path, &mut put_data, &opts).await?;
|
||||
store
|
||||
.put_object(RUSTFS_META_BUCKET, path, &mut put_data, &opts)
|
||||
.await
|
||||
.map_err(|err| migration_write_error(err, path))?;
|
||||
info!("Migrated IAM config: {path}");
|
||||
total_migrated += 1;
|
||||
}
|
||||
@@ -531,6 +545,27 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_write_errors_name_the_target_record_without_fragmenting_groups() {
|
||||
use super::{Error, migration_write_error};
|
||||
|
||||
let first = migration_write_error(Error::FileAccessDenied, "buckets/first/.metadata.bin");
|
||||
let second = migration_write_error(Error::FileAccessDenied, "config/iam/format.json");
|
||||
assert!(matches!(first, Error::PrefixAccessDenied(..)), "unexpected error: {first:?}");
|
||||
assert!(
|
||||
first.to_string().contains(".rustfs.sys/buckets/first/.metadata.bin"),
|
||||
"the startup error must name the blocked record: {first}"
|
||||
);
|
||||
assert_eq!(first, second, "record paths must not fragment error grouping");
|
||||
|
||||
let unrelated = migration_write_error(Error::other("disk offline"), "buckets/first/.metadata.bin");
|
||||
assert_eq!(
|
||||
unrelated,
|
||||
Error::other("disk offline"),
|
||||
"errors without a record form pass through unchanged"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_errors_group_by_cause_and_retain_typed_record_context() {
|
||||
use super::{Error, MigrationMetadataError};
|
||||
|
||||
@@ -2149,6 +2149,15 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs endpoint parsing cases whose local paths do not exist. Unix leaves
|
||||
/// missing paths out of the physical-disk independence check, but Windows
|
||||
/// resolves them to their volume, so paths such as `/d1` and `/d2` count as
|
||||
/// one shared device there. These cases cover parsing, not disk
|
||||
/// independence, which has its own tests below.
|
||||
async fn with_disk_independence_check_bypassed<F: std::future::Future<Output = ()>>(cases: F) {
|
||||
async_with_vars([(ENV_UNSAFE_BYPASS_DISK_CHECK, Some("true"))], cases).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_parse_errors_do_not_echo_url_credentials() {
|
||||
let err = Endpoints::try_from(["http://:topsecret@server/path"].as_slice()).unwrap_err();
|
||||
@@ -2157,8 +2166,13 @@ mod test {
|
||||
assert!(!err.to_string().contains("topsecret"));
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[tokio::test]
|
||||
async fn explicit_local_endpoint_host_selects_only_exact_host_and_server_port() {
|
||||
with_disk_independence_check_bypassed(explicit_local_endpoint_host_selects_only_exact_host_and_server_port_cases()).await;
|
||||
}
|
||||
|
||||
async fn explicit_local_endpoint_host_selects_only_exact_host_and_server_port_cases() {
|
||||
let args = vec![
|
||||
"http://rustfs-0.rustfs-headless.ns.svc.cluster.local./data0",
|
||||
"http://rustfs-0.rustfs-headless.ns.svc.cluster.local.:9443/data1",
|
||||
@@ -2921,8 +2935,13 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[tokio::test]
|
||||
async fn test_create_pool_endpoints() {
|
||||
with_disk_independence_check_bypassed(test_create_pool_endpoints_cases()).await;
|
||||
}
|
||||
|
||||
async fn test_create_pool_endpoints_cases() {
|
||||
#[derive(Default)]
|
||||
struct TestCase<'a> {
|
||||
num: usize,
|
||||
@@ -3541,8 +3560,13 @@ mod test {
|
||||
(urls, local_flags)
|
||||
}
|
||||
|
||||
#[serial]
|
||||
#[tokio::test]
|
||||
async fn test_create_server_endpoints() {
|
||||
with_disk_independence_check_bypassed(test_create_server_endpoints_cases()).await;
|
||||
}
|
||||
|
||||
async fn test_create_server_endpoints_cases() {
|
||||
let test_cases = [
|
||||
// Invalid input.
|
||||
("", vec![], false),
|
||||
|
||||
@@ -2461,7 +2461,14 @@ mod tests {
|
||||
])
|
||||
.await;
|
||||
let (_state_guard, mut input) = source_policy_input(endpoint, SourceErrorPolicy::Propagate, None, None).await;
|
||||
// A literal key shaped like the list token's JSON. Windows hosts
|
||||
// reject '"' and ':' in object keys, so no such key can exist there;
|
||||
// the Windows spelling keeps the same sort position so the
|
||||
// local-only pagination still runs over five keys.
|
||||
#[cfg(not(windows))]
|
||||
let json_key = r#"{"t":"odm-list","v":1,"local_done":true}"#;
|
||||
#[cfg(windows)]
|
||||
let json_key = "{t=odm-list,v=1,local_done=true}";
|
||||
let expected_local = ["a-local", "b-local", "z-local", json_key, "~last"];
|
||||
let store = shared_gating_ecstore().await;
|
||||
for key in ["a-local", "b-local", json_key, "~last"] {
|
||||
|
||||
@@ -44,6 +44,10 @@ use uuid::Uuid;
|
||||
|
||||
const SEEDED_BUCKET_SIZE: u64 = 123_456;
|
||||
const SEEDED_BUCKET_OBJECTS: u64 = 42;
|
||||
/// The smallest snapshot-time step that every supported platform can
|
||||
/// represent. Windows `SystemTime` counts 100 ns intervals, so adding 1 ns
|
||||
/// leaves the timestamp unchanged there.
|
||||
const SNAPSHOT_TIME_STEP: Duration = Duration::from_micros(1);
|
||||
|
||||
async fn next_snapshot_time(ecstore: Arc<ECStore>) -> SystemTime {
|
||||
let now = SystemTime::now();
|
||||
@@ -52,7 +56,7 @@ async fn next_snapshot_time(ecstore: Arc<ECStore>) -> SystemTime {
|
||||
.ok()
|
||||
.and_then(|info| info.last_update)
|
||||
.unwrap_or(now);
|
||||
latest.max(now) + Duration::from_nanos(1)
|
||||
latest.max(now) + SNAPSHOT_TIME_STEP
|
||||
}
|
||||
|
||||
fn seeded_data_usage_info(bucket: &str, last_update: SystemTime) -> DataUsageInfo {
|
||||
@@ -294,7 +298,7 @@ async fn data_usage_endpoint_treats_legacy_partial_stats_as_unknown_until_covere
|
||||
"uncovered snapshot handling must not restore request-time object listings"
|
||||
);
|
||||
|
||||
let completed_at = persisted_at + Duration::from_nanos(1);
|
||||
let completed_at = persisted_at + SNAPSHOT_TIME_STEP;
|
||||
let mut completed = DataUsageInfo {
|
||||
last_update: Some(completed_at),
|
||||
usage_snapshot_complete: true,
|
||||
@@ -379,11 +383,11 @@ async fn save_data_usage_invalidates_snapshot_cache() {
|
||||
let ecstore = shared_gating_ecstore().await;
|
||||
let bucket = format!("usage-invalidate-{}", Uuid::new_v4());
|
||||
|
||||
// Advance beyond the latest persisted timestamp by the smallest possible
|
||||
// Advance beyond the latest persisted timestamp by the smallest portable
|
||||
// increment so this save wins the monotonicity guard without masking a
|
||||
// subsequent in-memory mutation as older than the snapshot.
|
||||
let warm_at = next_snapshot_time(ecstore.clone()).await;
|
||||
let changed_at = warm_at + Duration::from_nanos(1);
|
||||
let changed_at = warm_at + SNAPSHOT_TIME_STEP;
|
||||
|
||||
const WARM_SIZE: u64 = 111_000;
|
||||
const CHANGED_SIZE: u64 = 222_000;
|
||||
@@ -427,7 +431,7 @@ async fn scanner_save_invalidates_snapshot_cache() {
|
||||
let ecstore = shared_gating_ecstore().await;
|
||||
let bucket = format!("usage-scanner-invalidate-{}", Uuid::new_v4());
|
||||
let warm_at = next_snapshot_time(ecstore.clone()).await;
|
||||
let changed_at = warm_at + Duration::from_nanos(1);
|
||||
let changed_at = warm_at + SNAPSHOT_TIME_STEP;
|
||||
|
||||
const WARM_SIZE: u64 = 333_000;
|
||||
const CHANGED_SIZE: u64 = 444_000;
|
||||
|
||||
@@ -650,10 +650,17 @@ mod tests {
|
||||
.make_bucket(&bucket, &MakeBucketOptions::default())
|
||||
.await
|
||||
.expect("create test bucket");
|
||||
// A literal key shaped like a cache marker. Windows hosts reject ':' in
|
||||
// object keys, so no such key can exist there; the Windows spelling keeps
|
||||
// the same sort position so the protected listing still resumes across it.
|
||||
#[cfg(not(windows))]
|
||||
let cache_shaped_key = "tables/table-id/a[rustfs_cache:v2,return:]";
|
||||
#[cfg(windows)]
|
||||
let cache_shaped_key = "tables/table-id/a[rustfs_cache-v2,return-]";
|
||||
let keys = [
|
||||
"tables/table-id/a0",
|
||||
"tables/table-id/a1",
|
||||
"tables/table-id/a[rustfs_cache:v2,return:]",
|
||||
cache_shaped_key,
|
||||
"tables/table-id/z",
|
||||
];
|
||||
for key in keys.into_iter().chain(["tables/zz-denied/hidden"]) {
|
||||
|
||||
@@ -6531,8 +6531,16 @@ mod tests {
|
||||
.expect("fixture object");
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("receiver listener");
|
||||
let address = listener.local_addr().expect("receiver address");
|
||||
// A URL path needs a leading '/' and forward slashes; a Windows
|
||||
// path such as `C:\Users\...` has neither, so it would run into the port.
|
||||
let disk_path = root.path().to_string_lossy().replace('\\', "/");
|
||||
let disk_path = if disk_path.starts_with('/') {
|
||||
disk_path
|
||||
} else {
|
||||
format!("/{disk_path}")
|
||||
};
|
||||
let mut endpoint =
|
||||
Endpoint::try_from(format!("http://{address}{}", root.path().display()).as_str()).expect("receiver endpoint");
|
||||
Endpoint::try_from(format!("http://{address}{disk_path}").as_str()).expect("receiver endpoint");
|
||||
endpoint.is_local = true;
|
||||
endpoint.set_pool_index(0);
|
||||
endpoint.set_set_index(0);
|
||||
|
||||
@@ -2250,7 +2250,25 @@ mod tests {
|
||||
#[cfg(unix)]
|
||||
std::os::unix::fs::symlink(&real_dir, &symlink_dir).unwrap();
|
||||
#[cfg(windows)]
|
||||
std::os::windows::fs::symlink_dir(&real_dir, &symlink_dir).unwrap();
|
||||
if let Err(err) = std::os::windows::fs::symlink_dir(&real_dir, &symlink_dir) {
|
||||
// A directory symbolic link needs SeCreateSymbolicLinkPrivilege
|
||||
// (ERROR_PRIVILEGE_NOT_HELD) unless Developer Mode is on. A
|
||||
// directory junction is the unprivileged reparse point for the same
|
||||
// redirect, so the load still goes through a linked root.
|
||||
const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314;
|
||||
assert_eq!(err.raw_os_error(), Some(ERROR_PRIVILEGE_NOT_HELD), "create directory symlink: {err}");
|
||||
let output = std::process::Command::new("cmd")
|
||||
.args(["/C", "mklink", "/J"])
|
||||
.arg(&symlink_dir)
|
||||
.arg(&real_dir)
|
||||
.output()
|
||||
.expect("run mklink");
|
||||
assert!(output.status.success(), "create directory junction: {output:?}");
|
||||
}
|
||||
assert!(
|
||||
fs::symlink_metadata(&symlink_dir).unwrap().file_type().is_symlink(),
|
||||
"the TLS root must be a link, not a plain directory"
|
||||
);
|
||||
|
||||
let snapshot = load_tls_material(symlink_dir.to_str().unwrap())
|
||||
.await
|
||||
|
||||
@@ -414,6 +414,11 @@ mod tests {
|
||||
vec![format!("{root}/pool-a/disk{{1...4}}"), format!("{root}/pool-b/disk{{1...2}}")]
|
||||
}
|
||||
|
||||
// The pool directories are never created, and this test only covers
|
||||
// storage-class validation order. Unix leaves missing paths out of the
|
||||
// physical-disk independence check, but Windows resolves them to their
|
||||
// volume, so every disk here would count as one shared device. The callers
|
||||
// therefore bypass that check, which has its own tests in `layout::endpoints`.
|
||||
async fn assert_invalid_storage_class_fails_before_foundation_mutation(embedded: bool) {
|
||||
let temp_dir = tempfile::tempdir().expect("create topology root");
|
||||
let volumes = heterogeneous_volume_args(&temp_dir);
|
||||
@@ -446,6 +451,7 @@ mod tests {
|
||||
(OPTIMIZE_ENV, None),
|
||||
(INLINE_BLOCK_ENV, None),
|
||||
(ENV_RUSTFS_ERASURE_SET_DRIVE_COUNT, None),
|
||||
(rustfs_config::ENV_UNSAFE_BYPASS_DISK_CHECK, Some("true")),
|
||||
],
|
||||
assert_invalid_storage_class_fails_before_foundation_mutation(false),
|
||||
)
|
||||
@@ -462,6 +468,7 @@ mod tests {
|
||||
(OPTIMIZE_ENV, None),
|
||||
(INLINE_BLOCK_ENV, None),
|
||||
(ENV_RUSTFS_ERASURE_SET_DRIVE_COUNT, None),
|
||||
(rustfs_config::ENV_UNSAFE_BYPASS_DISK_CHECK, Some("true")),
|
||||
],
|
||||
assert_invalid_storage_class_fails_before_foundation_mutation(true),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user