From e26668e62c8dbb9dc710ed4bc0c03766e371d970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Sun, 16 Aug 2026 10:28:45 +0800 Subject: [PATCH] fix(ecstore): mint bucket-target ARNs in the madmin arn:minio partition (#6128) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(ecstore): pin madmin-compatible ARN partition contract Red-light evidence for backlog#1675 P1-7: madmin-go's ParseARN hard-rejects any ARN that does not start with 'arn:minio:', while RustFS generates and only accepts 'arn:rustfs:'. mc/madmin tooling therefore cannot decode RustFS remote-target listings, and MinIO-era replication configs are rejected as StaleTarget when re-registered. The new tests pin the target contract (generate arn:minio:, parse both partitions, reject unknown partitions) and fail against the current single-partition gate. * fix(ecstore): mint bucket-target ARNs in the madmin arn:minio partition madmin-go's ParseARN hard-rejects any partition other than 'arn:minio:', so native mc/madmin tooling could not decode RustFS remote-target listings, and re-registering a MinIO-era replication config failed its StaleTarget check against freshly minted arn:rustfs: targets (backlog#1675 P1-7, route A). - ARN Display now emits 'arn:minio:'; FromStr accepts a {minio, rustfs} partition whitelist (the legacy partition stays readable forever for persisted bucket-targets.json / replication configs). The whitelist is the only structural gate — BucketTargetType::from_str never fails — so it deliberately rejects foreign partitions such as arn:aws:. - No data migration: every runtime match between targets, rules and stats keys is full-string equality, so existing arn:rustfs: targets keep matching their persisted rules; site replication already preserves MinIO-era ARNs on reconcile (pinned by existing tests). - Rolling upgrade note: upgrade all cluster nodes before creating new remote targets — a not-yet-upgraded node rejects remove-remote-target for a freshly minted arn:minio: ARN with BucketRemoteArnInvalid. - Out of scope: notification/SQS ARNs (crates/targets) keep the arn:rustfs:sqs: partition; they have their own compatibility story. --- crates/ecstore/src/bucket/target/arn.rs | 56 +++++++++++++++++-- rustfs/src/admin/handlers/site_replication.rs | 5 +- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/crates/ecstore/src/bucket/target/arn.rs b/crates/ecstore/src/bucket/target/arn.rs index 0bf0f175a..e680daca9 100644 --- a/crates/ecstore/src/bucket/target/arn.rs +++ b/crates/ecstore/src/bucket/target/arn.rs @@ -40,7 +40,14 @@ impl ARN { impl Display for ARN { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "arn:rustfs:{}:{}:{}:{}", self.arn_type, self.region, self.id, self.bucket) + // The `minio` partition is deliberate: madmin-go's ParseARN + // hard-rejects any other partition, so native mc/madmin tooling can + // only decode remote-target ARNs minted in this form (backlog#1675 + // P1-7). Legacy `arn:rustfs:` ARNs persisted by older releases stay + // readable via the FromStr whitelist below; runtime matching between + // targets and replication rules is by full-string equality, so mixed + // partitions coexist safely. + write!(f, "arn:minio:{}:{}:{}:{}", self.arn_type, self.region, self.id, self.bucket) } } @@ -48,7 +55,12 @@ impl FromStr for ARN { type Err = std::io::Error; fn from_str(s: &str) -> Result { - if !s.starts_with("arn:rustfs:") { + // Partition whitelist, not just an `arn:` check: `BucketTargetType:: + // from_str(...).unwrap_or_default()` below never fails, so this is + // the only structural gate rejecting foreign ARNs. `arn:rustfs:` is + // the legacy partition and must stay accepted forever (persisted + // bucket-targets.json / replication configs from older releases). + if !s.starts_with("arn:minio:") && !s.starts_with("arn:rustfs:") { return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid ARN format")); } @@ -101,14 +113,50 @@ mod tests { } /// RustFS commonly generates ARNs with an empty region: - /// `arn:rustfs:replication:::`. + /// `arn:minio:replication:::`. #[test] fn from_str_handles_empty_region_segment() { - let parsed = ARN::from_str("arn:rustfs:replication::depl-123:bucket-a").expect("valid ARN must parse"); + let parsed = ARN::from_str("arn:minio:replication::depl-123:bucket-a").expect("valid ARN must parse"); assert_eq!(parsed.arn_type, BucketTargetType::ReplicationService); assert_eq!(parsed.region, "", "region segment is empty in this form"); assert_eq!(parsed.id, "depl-123"); assert_eq!(parsed.bucket, "bucket-a"); } + + /// madmin-go's `ParseARN` hard-rejects anything that does not start with + /// `arn:minio:`, so generated ARNs must use the `minio` partition or the + /// native mc/madmin tooling cannot decode remote-target listings. + #[test] + fn display_emits_minio_partition() { + let arn = ARN::new( + BucketTargetType::ReplicationService, + "depl-123".to_string(), + String::new(), + "bucket-a".to_string(), + ); + + assert_eq!(arn.to_string(), "arn:minio:replication::depl-123:bucket-a"); + } + + /// Persisted bucket-targets.json files from older RustFS releases carry + /// `arn:rustfs:` ARNs; the legacy partition must stay parseable forever. + #[test] + fn from_str_accepts_legacy_rustfs_partition() { + let parsed = ARN::from_str("arn:rustfs:replication:us-east-1:depl-123:bucket-a").expect("legacy ARN must parse"); + + assert_eq!(parsed.arn_type, BucketTargetType::ReplicationService); + assert_eq!(parsed.region, "us-east-1"); + assert_eq!(parsed.id, "depl-123"); + assert_eq!(parsed.bucket, "bucket-a"); + } + + /// The partition whitelist is the only structural gate: `BucketTargetType:: + /// from_str(...).unwrap_or_default()` never fails, so any 6-segment string + /// would otherwise parse as `type=None`. + #[test] + fn from_str_rejects_unknown_partition() { + assert!(ARN::from_str("arn:aws:replication::depl-123:bucket-a").is_err()); + assert!(ARN::from_str("not-an-arn").is_err()); + } } diff --git a/rustfs/src/admin/handlers/site_replication.rs b/rustfs/src/admin/handlers/site_replication.rs index 495db4284..d9604fe6f 100644 --- a/rustfs/src/admin/handlers/site_replication.rs +++ b/rustfs/src/admin/handlers/site_replication.rs @@ -15305,7 +15305,10 @@ mod tests { assert!(!target.secure); assert_eq!(target.target_bucket, "photos"); assert_eq!(target.deployment_id, "remote"); - assert_eq!(target.arn, "arn:rustfs:replication::remote:photos"); + // Freshly minted ARNs use the `minio` partition so madmin-go tooling + // can parse them; legacy `arn:rustfs:` targets are preserved as-is + // (see the MinIO-era preservation test below). + assert_eq!(target.arn, "arn:minio:replication::remote:photos"); assert_eq!(target.region, "us-east-1"); let credentials = target .credentials