From 398d16f58c810df88df5adb02e29c2f09c694198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=94=90=E5=B0=8F=E9=B8=AD?= Date: Sat, 15 Aug 2026 09:41:23 +0800 Subject: [PATCH] fix(ecstore): mint bucket-target ARNs in the madmin arn:minio partition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 | 16 ++++++++++++++-- rustfs/src/admin/handlers/site_replication.rs | 5 ++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/crates/ecstore/src/bucket/target/arn.rs b/crates/ecstore/src/bucket/target/arn.rs index 6419d99bf..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")); } diff --git a/rustfs/src/admin/handlers/site_replication.rs b/rustfs/src/admin/handlers/site_replication.rs index 471a624d4..88f11cb98 100644 --- a/rustfs/src/admin/handlers/site_replication.rs +++ b/rustfs/src/admin/handlers/site_replication.rs @@ -14413,7 +14413,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