mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-13 16:46:55 +00:00
fix(versioning): reject suspending versioning while a replication config exists (#6006)
PutBucketVersioning with Status=Suspended on a bucket that carries a replication configuration now fails with InvalidBucketState, matching AWS S3 and MinIO. Suspension would start minting null versions that the versioned replication engine can never converge — the state is unreachable on AWS and MinIO, and the nightly acceptance-matrix e2e that tried to exercise it failed every night since it landed (issue #5767). The acceptance-matrix test tail now pins the rejection contract (InvalidBucketState) and verifies a fresh matched PUT still replicates with a real version id after the rejected suspension.
This commit is contained in:
@@ -4235,37 +4235,49 @@ async fn test_bucket_replication_acceptance_matrix_local_dual_targets() -> TestR
|
|||||||
"tag rule with disabled delete-marker replication created a marker: {tagged_state:?}"
|
"tag rule with disabled delete-marker replication created a marker: {tagged_state:?}"
|
||||||
);
|
);
|
||||||
|
|
||||||
set_bucket_versioning(&source_env, source_bucket, BucketVersioningStatus::Suspended).await?;
|
// AWS S3 and MinIO both reject suspending versioning on a bucket that
|
||||||
set_bucket_versioning(&target_env_a, target_bucket_a, BucketVersioningStatus::Suspended).await?;
|
// carries a replication configuration (InvalidBucketState): suspension
|
||||||
let null_put = source_client
|
// would mint null versions that versioned replication can never converge.
|
||||||
|
let suspend_err = source_client
|
||||||
|
.put_bucket_versioning()
|
||||||
|
.bucket(source_bucket)
|
||||||
|
.versioning_configuration(
|
||||||
|
VersioningConfiguration::builder()
|
||||||
|
.status(BucketVersioningStatus::Suspended)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.expect_err("suspending versioning on a replication source must be rejected");
|
||||||
|
assert_eq!(
|
||||||
|
suspend_err.as_service_error().and_then(|error| error.code()),
|
||||||
|
Some("InvalidBucketState"),
|
||||||
|
"suspension on a replication source must fail with InvalidBucketState: {suspend_err:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// The rejected suspension must leave the versioning + replication state
|
||||||
|
// fully intact: a fresh matched PUT still replicates with a real version.
|
||||||
|
let post_reject_put = source_client
|
||||||
.put_object()
|
.put_object()
|
||||||
.bucket(source_bucket)
|
.bucket(source_bucket)
|
||||||
.key("prefix/null.txt")
|
.key("prefix/after-rejected-suspend.txt")
|
||||||
.body(ByteStream::from_static(b"null version"))
|
.body(ByteStream::from_static(b"still replicating"))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
assert!(null_put.version_id().is_none(), "suspended source PUT must create a null version");
|
let post_reject_version_id = post_reject_put
|
||||||
wait_for_replication_state(&target_client_a, target_bucket_a, "null version did not replicate", |state| {
|
.version_id()
|
||||||
state
|
.ok_or("PUT after rejected suspension omitted version ID")?
|
||||||
.iter()
|
.to_string();
|
||||||
.any(|entry| entry.key == "prefix/null.txt" && entry.version_id == "null" && !entry.delete_marker)
|
wait_for_replication_state(
|
||||||
})
|
&target_client_a,
|
||||||
.await?;
|
target_bucket_a,
|
||||||
let null_delete = source_client
|
"replication stopped after rejected versioning suspension",
|
||||||
.delete_object()
|
|state| {
|
||||||
.bucket(source_bucket)
|
state
|
||||||
.key("prefix/null.txt")
|
.iter()
|
||||||
.send()
|
.any(|entry| entry.key == "prefix/after-rejected-suspend.txt" && entry.version_id == post_reject_version_id)
|
||||||
.await?;
|
},
|
||||||
assert!(
|
)
|
||||||
null_delete.version_id().is_none(),
|
|
||||||
"suspended source DELETE must create a null delete marker"
|
|
||||||
);
|
|
||||||
wait_for_replication_state(&target_client_a, target_bucket_a, "null delete marker did not replicate", |state| {
|
|
||||||
state
|
|
||||||
.iter()
|
|
||||||
.any(|entry| entry.key == "prefix/null.txt" && entry.version_id == "null" && entry.delete_marker)
|
|
||||||
})
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -738,6 +738,22 @@ async fn validate_bucket_versioning_update(bucket: &str, config: &VersioningConf
|
|||||||
Err(StorageError::ConfigNotFound) => {}
|
Err(StorageError::ConfigNotFound) => {}
|
||||||
Err(err) => return Err(ApiError::from(err).into()),
|
Err(err) => return Err(ApiError::from(err).into()),
|
||||||
}
|
}
|
||||||
|
// AWS S3 and MinIO both refuse to suspend versioning while a replication
|
||||||
|
// configuration exists: suspension would start minting null versions that
|
||||||
|
// the replication engine (versioned by contract) can never converge.
|
||||||
|
if config.suspended() {
|
||||||
|
match metadata_sys::get_replication_config(bucket).await {
|
||||||
|
Ok(_) => {
|
||||||
|
return Err(S3Error::with_message(
|
||||||
|
S3ErrorCode::InvalidBucketState,
|
||||||
|
"A replication configuration is present on this bucket, bucket wide versioning cannot be suspended."
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(StorageError::ConfigNotFound) => {}
|
||||||
|
Err(err) => return Err(ApiError::from(err).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user