mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-07 12:35:54 +00:00
fix(replication): send an integrity header on Object Lock PUTs (#7097)
* fix(replication): send an integrity header on Object Lock replication PUTs AWS S3, MinIO and most compatible targets reject a PutObject that carries x-amz-object-lock-* headers unless it also carries Content-MD5 or an x-amz-checksum-* header. Since rustfs#6895 the replication client sends plain signed payloads with no SDK checksum, so every replicated object with a retention period or legal hold failed against such targets. TargetClient::put_object now decides per request through the pure rustfs_replication::object_lock_put_integrity: a plaintext single-part object whose source ETag is its MD5 gets Content-MD5 derived from the ETag (no body pass, framing unchanged); a multipart-layout ETag, managed SSE or SSE-C passthrough falls back to an SDK CRC32; a forwarded source checksum or an unlocked PUT is left alone. The outbound target matrix flips its two KnownFailing(rustfs#7082) cells to Completed and every Completed cell now asserts that a locked PutObject carried an integrity header. Fixes rustfs#7082. * test(e2e): keep the matrix expectation table clippy-clean under -D warnings The CI lint runs cargo clippy --all-targets -- -D warnings. With every cell green the single-arm match tripped match_single_binding and the unused KnownFailing variant tripped dead_code, and the target-client tests tripped field_reassign_with_default. Drive the expectation table from a KNOWN_FAILING_CELLS constant (so the variant stays live and adding a red cell is a one-line entry), build the test options as struct literals, and refresh the e2e-repl-nightly selection digest for the renamed table test.
This commit is contained in:
@@ -202,19 +202,21 @@ enum Expectation {
|
||||
KnownFailing(&'static str),
|
||||
}
|
||||
|
||||
/// The single source of truth for what every cell must do today. A fix that
|
||||
/// turns a `KnownFailing` cell green must flip it here in the same PR; the
|
||||
/// test refuses an unexpected pass so the table cannot go stale silently.
|
||||
/// The cells that are red today, each pinned to the open issue that owns it.
|
||||
/// This is the single source of truth: a fix that turns a cell green must
|
||||
/// remove its entry in the same PR, and [`check_known_failing_cell`] refuses
|
||||
/// an unexpected pass so the table cannot go stale silently. rustfs#7082
|
||||
/// (Retention and LegalHold against the checksum-requiring target) lived
|
||||
/// here until the replication PUT started carrying a Content-MD5 derived
|
||||
/// from the source ETag.
|
||||
const KNOWN_FAILING_CELLS: &[(TargetMode, ObjectShape, &str)] = &[];
|
||||
|
||||
fn expectation(mode: TargetMode, shape: ObjectShape) -> Expectation {
|
||||
match (mode, shape) {
|
||||
// rustfs#7082: the replication PUT carries the lock headers but no
|
||||
// Content-MD5 / x-amz-checksum-* since rustfs#6895 switched the SDK
|
||||
// to plain payloads; AWS-compatible Object Lock targets reject it.
|
||||
(TargetMode::RequireChecksumWithObjectLock, ObjectShape::Retention | ObjectShape::LegalHold) => {
|
||||
Expectation::KnownFailing("rustfs#7082")
|
||||
}
|
||||
_ => Expectation::Completed,
|
||||
}
|
||||
KNOWN_FAILING_CELLS
|
||||
.iter()
|
||||
.find(|(known_mode, known_shape, _)| *known_mode == mode && *known_shape == shape)
|
||||
.map(|(_, _, issue)| Expectation::KnownFailing(issue))
|
||||
.unwrap_or(Expectation::Completed)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -237,28 +239,27 @@ async fn matrix_mint_own_version_ids_target() -> TestResult {
|
||||
run_row(TargetMode::MintOwnVersionIds).await
|
||||
}
|
||||
|
||||
/// The expectation table must name every mode and shape exactly once, so a
|
||||
/// new row or column cannot be added without deciding what it does.
|
||||
/// Every known-red entry must name a real cell and an issue, and the lookup
|
||||
/// must round-trip, so a stale or mistyped entry cannot silently pin nothing.
|
||||
#[test]
|
||||
fn expectation_table_covers_every_cell() {
|
||||
for mode in TargetMode::ALL {
|
||||
for shape in ObjectShape::ALL {
|
||||
let _ = expectation(mode, shape);
|
||||
}
|
||||
fn known_failing_table_names_real_cells() {
|
||||
for (mode, shape, issue) in KNOWN_FAILING_CELLS {
|
||||
assert!(
|
||||
TargetMode::ALL.contains(mode) && ObjectShape::ALL.contains(shape),
|
||||
"{mode:?}/{shape:?} is not a matrix cell"
|
||||
);
|
||||
assert!(
|
||||
issue.starts_with("rustfs#") || issue.starts_with("rustfs/backlog#"),
|
||||
"{issue} must name an open issue"
|
||||
);
|
||||
assert_eq!(expectation(*mode, *shape), Expectation::KnownFailing(issue));
|
||||
}
|
||||
let known_failing: Vec<_> = TargetMode::ALL
|
||||
let red_cells = TargetMode::ALL
|
||||
.iter()
|
||||
.flat_map(|mode| ObjectShape::ALL.iter().map(move |shape| (*mode, *shape)))
|
||||
.filter(|(mode, shape)| matches!(expectation(*mode, *shape), Expectation::KnownFailing(_)))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
known_failing,
|
||||
vec![
|
||||
(TargetMode::RequireChecksumWithObjectLock, ObjectShape::Retention),
|
||||
(TargetMode::RequireChecksumWithObjectLock, ObjectShape::LegalHold),
|
||||
],
|
||||
"every known-red cell is listed here on purpose; update this list together with the expectation table"
|
||||
);
|
||||
.count();
|
||||
assert_eq!(red_cells, KNOWN_FAILING_CELLS.len());
|
||||
}
|
||||
|
||||
async fn run_row(mode: TargetMode) -> TestResult {
|
||||
@@ -389,6 +390,17 @@ async fn check_completed_cell(
|
||||
)
|
||||
.into());
|
||||
}
|
||||
// rustfs#7082 contract: every PutObject that carries Object Lock
|
||||
// parameters also carries Content-MD5 or an x-amz-checksum-* header,
|
||||
// whatever the target's own policy is.
|
||||
if let Some(bare) = uploads.iter().find(|record| {
|
||||
record.operation == FakeTargetOperation::PutObject
|
||||
&& record.transport.object_lock_params
|
||||
&& record.transport.content_md5.is_none()
|
||||
&& record.transport.checksum_headers.is_empty()
|
||||
}) {
|
||||
return Err(format!("a locked PutObject went out without any integrity header (rustfs#7082): {bare:?}").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user