From 22cbd41ee95deadbfcd2788b8659f2f5e7574373 Mon Sep 17 00:00:00 2001 From: Zhengchao An Date: Wed, 1 Jul 2026 21:51:26 +0800 Subject: [PATCH] refactor(ecstore): add replication owner bridges (#4141) * refactor(ecstore): add replication owner bridges * fix(ecstore): tighten replication owner guards --- .../ecstore/src/bucket/bucket_target_sys.rs | 23 ++++----- crates/ecstore/src/bucket/migration.rs | 13 ++--- .../ecstore/src/bucket/replication/README.md | 6 +++ crates/ecstore/src/bucket/replication/mod.rs | 5 +- .../replication_migration_bridge.rs | 28 +++++++++++ .../replication_target_config_bridge.rs | 48 +++++++++++++++++++ .../architecture/ecstore-module-split-plan.md | 9 ++++ scripts/check_architecture_migration_rules.sh | 21 ++++++++ 8 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 crates/ecstore/src/bucket/replication/replication_migration_bridge.rs create mode 100644 crates/ecstore/src/bucket/replication/replication_target_config_bridge.rs diff --git a/crates/ecstore/src/bucket/bucket_target_sys.rs b/crates/ecstore/src/bucket/bucket_target_sys.rs index bad9194c9..d1a1a8b74 100644 --- a/crates/ecstore/src/bucket/bucket_target_sys.rs +++ b/crates/ecstore/src/bucket/bucket_target_sys.rs @@ -15,8 +15,7 @@ use crate::bucket::metadata::BucketMetadata; use crate::bucket::metadata_sys::get_bucket_targets_config; use crate::bucket::metadata_sys::get_replication_config; -use crate::bucket::replication::ObjectOpts; -use crate::bucket::replication::ReplicationConfigurationExt; +use crate::bucket::replication::ReplicationTargetConfigBridge; use crate::bucket::target::ARN; use crate::bucket::target::BucketTargetType; use crate::bucket::target::{self, BucketTarget, BucketTargets, Credentials}; @@ -50,7 +49,7 @@ use hyper_util::client::legacy::Client as HyperClient; use hyper_util::rt::{TokioExecutor, TokioTimer}; use reqwest::Client as HttpClient; use rustfs_config::{DEFAULT_TRUST_LEAF_CERT_AS_CA, ENV_TRUST_LEAF_CERT_AS_CA, RUSTFS_CA_CERT, RUSTFS_TLS_CERT}; -use rustfs_filemeta::{ReplicationStatusType, ReplicationType}; +use rustfs_filemeta::ReplicationStatusType; use rustfs_utils::egress::{OutboundUrlError, validate_outbound_url}; use rustfs_utils::http::{ AMZ_BUCKET_REPLICATION_STATUS, AMZ_OBJECT_LOCK_BYPASS_GOVERNANCE, AMZ_OBJECT_LOCK_LEGAL_HOLD, AMZ_OBJECT_LOCK_MODE, @@ -552,19 +551,13 @@ impl BucketTargetSys { if arn.arn_type == BucketTargetType::ReplicationService && let Ok((config, _)) = get_replication_config(bucket).await + && ReplicationTargetConfigBridge::target_is_used_by_rules(&config, arn_str) { - for rule in config.filter_target_arns(&ObjectOpts { - op_type: ReplicationType::All, - ..Default::default() - }) { - if rule == arn_str || config.role == arn_str { - let arn_remotes_map = self.arn_remotes_map.read().await; - if arn_remotes_map.get(arn_str).is_some() { - return Err(BucketTargetError::BucketRemoteRemoveDisallowed { - bucket: bucket.to_string(), - }); - } - } + let arn_remotes_map = self.arn_remotes_map.read().await; + if arn_remotes_map.get(arn_str).is_some() { + return Err(BucketTargetError::BucketRemoteRemoveDisallowed { + bucket: bucket.to_string(), + }); } } diff --git a/crates/ecstore/src/bucket/migration.rs b/crates/ecstore/src/bucket/migration.rs index f70aecd1e..db300fa42 100644 --- a/crates/ecstore/src/bucket/migration.rs +++ b/crates/ecstore/src/bucket/migration.rs @@ -15,7 +15,7 @@ //! Migration of bucket metadata and IAM config from legacy format to RustFS format. use crate::bucket::metadata::BUCKET_METADATA_FILE; -use crate::bucket::replication::{decode_resync_file, encode_resync_file}; +use crate::bucket::replication::ReplicationMigrationBridge; use crate::disk::{BUCKET_META_PREFIX, MIGRATING_META_BUCKET, RUSTFS_META_BUCKET}; use crate::error::Error; use crate::object_api::{GetObjectReader, ObjectInfo, ObjectOptions, PutObjReader}; @@ -179,8 +179,9 @@ fn normalize_bucket_meta_blob(path: &str, data: &[u8]) -> std::result::Result Result { + decode_resync_file(data) + } + + pub(crate) fn encode_resync_status(status: &BucketReplicationResyncStatus) -> Result> { + encode_resync_file(status) + } +} diff --git a/crates/ecstore/src/bucket/replication/replication_target_config_bridge.rs b/crates/ecstore/src/bucket/replication/replication_target_config_bridge.rs new file mode 100644 index 000000000..80230772f --- /dev/null +++ b/crates/ecstore/src/bucket/replication/replication_target_config_bridge.rs @@ -0,0 +1,48 @@ +// Copyright 2024 RustFS Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use s3s::dto::ReplicationConfiguration; + +use super::config::{ObjectOpts, ReplicationConfigurationExt}; +use super::replication_filemeta_boundary::ReplicationType; + +pub(crate) struct ReplicationTargetConfigBridge; + +impl ReplicationTargetConfigBridge { + pub(crate) fn target_is_used_by_rules(config: &ReplicationConfiguration, arn: &str) -> bool { + config + .filter_target_arns(&ObjectOpts { + op_type: ReplicationType::All, + ..Default::default() + }) + .into_iter() + .any(|rule| rule == arn) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn target_config_bridge_matches_role_target() { + let config = ReplicationConfiguration { + role: " arn:target ".to_string(), + rules: Vec::new(), + }; + + assert!(ReplicationTargetConfigBridge::target_is_used_by_rules(&config, "arn:target")); + assert!(!ReplicationTargetConfigBridge::target_is_used_by_rules(&config, "arn:other")); + } +} diff --git a/docs/architecture/ecstore-module-split-plan.md b/docs/architecture/ecstore-module-split-plan.md index fb1bbab0d..16ca88608 100644 --- a/docs/architecture/ecstore-module-split-plan.md +++ b/docs/architecture/ecstore-module-split-plan.md @@ -117,6 +117,9 @@ Current coupling: `ReplicationLifecycleBridge`, while scanner heal paths schedule replication work through `ReplicationScannerBridge`, and app/SetDisks object write/delete paths use `ReplicationObjectBridge`; +- bucket metadata migration and bucket target removal checks use local + replication bridges instead of importing resyncer codec or config helper + internals; - global replication pool/stat initialization still lives with ECStore runtime compatibility state; - modules inside `bucket/replication` use local relative paths rather than the @@ -174,12 +177,18 @@ Required contracts before crate movement: - `ReplicationLifecycleBridge`: lifecycle-originated delete and version-purge scheduling is exposed through the contract type in `crates/ecstore/src/bucket/replication/replication_lifecycle_bridge.rs`. +- `ReplicationMigrationBridge`: persisted resync status decode/encode access + for bucket metadata migration is exposed through the contract type in + `crates/ecstore/src/bucket/replication/replication_migration_bridge.rs`. - `ReplicationObjectBridge`: app and SetDisks object write/delete replication decisions and scheduling are exposed through the contract type in `crates/ecstore/src/bucket/replication/replication_object_bridge.rs`. - `ReplicationScannerBridge`: scanner-originated replication heal scheduling is exposed through the contract type in `crates/ecstore/src/bucket/replication/replication_scanner_bridge.rs`. +- `ReplicationTargetConfigBridge`: bucket target removal checks against + replication target rules are exposed through the contract type in + `crates/ecstore/src/bucket/replication/replication_target_config_bridge.rs`. - `ReplicationFacade`: the current `rustfs_ecstore::api::bucket::replication` compatibility surface is an explicit symbol list guarded against wildcard re-exports while downstream owners migrate to narrower contracts. diff --git a/scripts/check_architecture_migration_rules.sh b/scripts/check_architecture_migration_rules.sh index 6afe7a594..44f43ed63 100755 --- a/scripts/check_architecture_migration_rules.sh +++ b/scripts/check_architecture_migration_rules.sh @@ -210,6 +210,7 @@ REPLICATION_LOCK_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_lock_boundary REPLICATION_METADATA_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_metadata_boundary_bypass_hits.txt" REPLICATION_MSGP_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_msgp_boundary_bypass_hits.txt" REPLICATION_RUNTIME_TYPE_BYPASS_HITS_FILE="${TMP_DIR}/replication_runtime_type_bypass_hits.txt" +REPLICATION_ECSTORE_OWNER_BRIDGE_BYPASS_HITS_FILE="${TMP_DIR}/replication_ecstore_owner_bridge_bypass_hits.txt" REPLICATION_OBJECT_BRIDGE_BYPASS_HITS_FILE="${TMP_DIR}/replication_object_bridge_bypass_hits.txt" REPLICATION_SCANNER_BRIDGE_BYPASS_HITS_FILE="${TMP_DIR}/replication_scanner_bridge_bypass_hits.txt" REPLICATION_STORAGE_BOUNDARY_BYPASS_HITS_FILE="${TMP_DIR}/replication_storage_boundary_bypass_hits.txt" @@ -2531,6 +2532,26 @@ if [[ -s "$REPLICATION_OBJECT_BRIDGE_BYPASS_HITS_FILE" ]]; then report_failure "object replication work entry points must stay behind ReplicationObjectBridge: $(paste -sd '; ' "$REPLICATION_OBJECT_BRIDGE_BYPASS_HITS_FILE")" fi +( + cd "$ROOT_DIR" + { + rg -n --with-filename 'crate::bucket::replication::(decode_resync_file|encode_resync_file|ObjectOpts|ReplicationConfigurationExt)' \ + crates/ecstore/src \ + --glob '*.rs' + rg -n -U --with-filename 'use\s+crate::bucket::replication::\{[^}]*\b(decode_resync_file|encode_resync_file|ObjectOpts|ReplicationConfigurationExt)\b' \ + crates/ecstore/src \ + --glob '*.rs' + rg -n -U --with-filename 'crate::\{[^;]*bucket::replication::\{[^}]*\b(decode_resync_file|encode_resync_file|ObjectOpts|ReplicationConfigurationExt)\b' \ + crates/ecstore/src \ + --glob '*.rs' + } | + rg -v '^crates/ecstore/src/bucket/replication/' || true +) >"$REPLICATION_ECSTORE_OWNER_BRIDGE_BYPASS_HITS_FILE" + +if [[ -s "$REPLICATION_ECSTORE_OWNER_BRIDGE_BYPASS_HITS_FILE" ]]; then + report_failure "ECStore owner modules must use replication bridge contracts instead of internal replication helpers: $(paste -sd '; ' "$REPLICATION_ECSTORE_OWNER_BRIDGE_BYPASS_HITS_FILE")" +fi + ( cd "$ROOT_DIR" find crates/ecstore/src/bucket/replication -type f -name '*.rs' -print0 |