refactor(ecstore): migrate huaweicloud/tencent warm backends to shared S3 constructor (#6775)

refactor(ecstore): huaweicloud/tencent reuse the shared S3 constructor

Migrates the Huaweicloud and Tencent tier warm backends onto the shared
S3-compatible constructor (backlog#2040). Also makes the shared
constructor's outbound-URL validation injectable per provider
(S3CompatibleWarmBackendParams::validate_endpoint) so it can centralize
rustfs/rustfs#6764's SSRF check for the providers that don't need an
exception, while accommodating rustfs/rustfs#6773's RustFS-specific
debug-only loopback opt-in without weakening the other six providers.

Updates scripts/error-other-format-baseline.txt: the one ::other(format!)
call site moves from the two per-provider files into the new shared
call site in warm_backend.rs (net call-site count unchanged).

Refs rustfs/backlog#2042
This commit is contained in:
Zhengchao An
2026-08-28 21:12:55 +08:00
committed by GitHub
parent 847fbd2a8b
commit ed66b0a04d
3 changed files with 48 additions and 163 deletions
@@ -19,78 +19,38 @@
#![allow(clippy::all)]
use std::collections::HashMap;
use std::sync::Arc;
use crate::services::tier::{
tier_config::TierHuaweicloud,
warm_backend::{WarmBackend, WarmBackendGetOpts, build_transition_put_options},
warm_backend::{
S3CompatibleWarmBackendParams, WarmBackend, WarmBackendGetOpts, build_transition_put_options,
new_s3_compatible_warm_backend, optimal_part_size,
},
warm_backend_s3::WarmBackendS3,
};
use rustfs_s3_client::{
admin_handler_utils::AdminError,
api_put_object::PutObjectOptions,
credentials::{Credentials, SignatureType, Static, Value},
transition_api::{BucketLookupType, Options, ReadCloser, ReaderImpl, TransitionClient, TransitionCore},
};
use rustfs_s3_client::transition_api::{BucketLookupType, ReadCloser, ReaderImpl};
use rustfs_utils::egress::validate_outbound_url;
use tracing::warn;
const MAX_MULTIPART_PUT_OBJECT_SIZE: i64 = 1024 * 1024 * 1024 * 1024 * 5;
const MAX_PARTS_COUNT: i64 = 10000;
const _MAX_PART_SIZE: i64 = 1024 * 1024 * 1024 * 5;
const MIN_PART_SIZE: i64 = 1024 * 1024 * 128;
pub struct WarmBackendHuaweicloud(WarmBackendS3);
impl WarmBackendHuaweicloud {
pub async fn new(conf: &TierHuaweicloud, tier: &str) -> Result<Self, std::io::Error> {
if conf.access_key == "" || conf.secret_key == "" {
return Err(std::io::Error::other("both access and secret keys are required"));
}
if conf.bucket == "" {
return Err(std::io::Error::other("no bucket name was provided"));
}
let u = match url::Url::parse(&conf.endpoint) {
Ok(u) => u,
Err(e) => {
return Err(std::io::Error::other(e.to_string()));
}
};
validate_outbound_url(&u).map_err(|err| std::io::Error::other(format!("tier endpoint is not allowed: {err}")))?;
let creds = Credentials::new(Static(Value {
access_key_id: conf.access_key.clone(),
secret_access_key: conf.secret_key.clone(),
session_token: "".to_string(),
signer_type: SignatureType::SignatureV4,
..Default::default()
}));
let opts = Options {
creds,
secure: u.scheme() == "https",
region: conf.region.clone(),
bucket_lookup: BucketLookupType::BucketLookupDNS,
..Default::default()
};
let scheme = u.scheme();
let default_port = if scheme == "https" { 443 } else { 80 };
let host = u
.host_str()
.ok_or_else(|| std::io::Error::other("Invalid endpoint URL: missing host"))?;
let client =
TransitionClient::new(&format!("{}:{}", host, u.port().unwrap_or(default_port)), opts, "huaweicloud").await?;
let client = Arc::new(client);
let core = TransitionCore(Arc::clone(&client));
Ok(Self(WarmBackendS3 {
client,
core,
bucket: conf.bucket.clone(),
prefix: conf.prefix.strip_suffix("/").unwrap_or(&conf.prefix).to_owned(),
storage_class: "".to_string(),
}))
Ok(Self(
new_s3_compatible_warm_backend(S3CompatibleWarmBackendParams {
endpoint: &conf.endpoint,
access_key: &conf.access_key,
secret_key: &conf.secret_key,
bucket: &conf.bucket,
prefix: &conf.prefix,
region: &conf.region,
bucket_lookup: BucketLookupType::BucketLookupDNS,
provider_tag: "huaweicloud",
validate_endpoint: validate_outbound_url,
})
.await?,
))
}
}
@@ -103,7 +63,7 @@ impl WarmBackend for WarmBackendHuaweicloud {
length: i64,
meta: HashMap<String, String>,
) -> Result<String, std::io::Error> {
let part_size = optimal_part_size(length)?;
let part_size = optimal_part_size(length, MIN_PART_SIZE)?;
let client = self.0.client.clone();
let res = client
.put_object(&self.0.bucket, &self.0.get_dest(object), r, length, &{
@@ -134,32 +94,15 @@ impl WarmBackend for WarmBackendHuaweicloud {
}
}
fn optimal_part_size(object_size: i64) -> Result<i64, std::io::Error> {
let mut object_size = object_size;
if object_size == -1 {
object_size = MAX_MULTIPART_PUT_OBJECT_SIZE;
}
if object_size > MAX_MULTIPART_PUT_OBJECT_SIZE {
return Err(std::io::Error::other("entity too large"));
}
let configured_part_size = MIN_PART_SIZE;
let mut part_size_flt = object_size as f64 / MAX_PARTS_COUNT as f64;
part_size_flt = (part_size_flt as f64 / configured_part_size as f64).ceil() * configured_part_size as f64;
let part_size = part_size_flt as i64;
if part_size == 0 {
return Ok(MIN_PART_SIZE);
}
Ok(part_size)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::tier::tier_config::TierHuaweicloud;
/// The SSRF guard itself is exercised once, generically, in
/// `warm_backend::tests` (see backlog#2040/backlog#2041 and
/// rustfs/rustfs#6764) — this test only pins that this provider's
/// production constructor really is wired through that shared path.
#[tokio::test]
async fn new_rejects_loopback_endpoint_before_network_setup() {
let conf = TierHuaweicloud {
@@ -19,77 +19,38 @@
#![allow(clippy::all)]
use std::collections::HashMap;
use std::sync::Arc;
use crate::services::tier::{
tier_config::TierTencent,
warm_backend::{WarmBackend, WarmBackendGetOpts, build_transition_put_options},
warm_backend::{
S3CompatibleWarmBackendParams, WarmBackend, WarmBackendGetOpts, build_transition_put_options,
new_s3_compatible_warm_backend, optimal_part_size,
},
warm_backend_s3::WarmBackendS3,
};
use rustfs_s3_client::{
admin_handler_utils::AdminError,
api_put_object::PutObjectOptions,
credentials::{Credentials, SignatureType, Static, Value},
transition_api::{BucketLookupType, Options, ReadCloser, ReaderImpl, TransitionClient, TransitionCore},
};
use rustfs_s3_client::transition_api::{BucketLookupType, ReadCloser, ReaderImpl};
use rustfs_utils::egress::validate_outbound_url;
use tracing::warn;
const MAX_MULTIPART_PUT_OBJECT_SIZE: i64 = 1024 * 1024 * 1024 * 1024 * 5;
const MAX_PARTS_COUNT: i64 = 10000;
const _MAX_PART_SIZE: i64 = 1024 * 1024 * 1024 * 5;
const MIN_PART_SIZE: i64 = 1024 * 1024 * 128;
pub struct WarmBackendTencent(WarmBackendS3);
impl WarmBackendTencent {
pub async fn new(conf: &TierTencent, tier: &str) -> Result<Self, std::io::Error> {
if conf.access_key == "" || conf.secret_key == "" {
return Err(std::io::Error::other("both access and secret keys are required"));
}
if conf.bucket == "" {
return Err(std::io::Error::other("no bucket name was provided"));
}
let u = match url::Url::parse(&conf.endpoint) {
Ok(u) => u,
Err(e) => {
return Err(std::io::Error::other(e.to_string()));
}
};
validate_outbound_url(&u).map_err(|err| std::io::Error::other(format!("tier endpoint is not allowed: {err}")))?;
let creds = Credentials::new(Static(Value {
access_key_id: conf.access_key.clone(),
secret_access_key: conf.secret_key.clone(),
session_token: "".to_string(),
signer_type: SignatureType::SignatureV4,
..Default::default()
}));
let opts = Options {
creds,
secure: u.scheme() == "https",
region: conf.region.clone(),
bucket_lookup: BucketLookupType::BucketLookupDNS,
..Default::default()
};
let scheme = u.scheme();
let default_port = if scheme == "https" { 443 } else { 80 };
let host = u
.host_str()
.ok_or_else(|| std::io::Error::other("Invalid endpoint URL: missing host"))?;
let client = TransitionClient::new(&format!("{}:{}", host, u.port().unwrap_or(default_port)), opts, "tencent").await?;
let client = Arc::new(client);
let core = TransitionCore(Arc::clone(&client));
Ok(Self(WarmBackendS3 {
client,
core,
bucket: conf.bucket.clone(),
prefix: conf.prefix.strip_suffix("/").unwrap_or(&conf.prefix).to_owned(),
storage_class: "".to_string(),
}))
Ok(Self(
new_s3_compatible_warm_backend(S3CompatibleWarmBackendParams {
endpoint: &conf.endpoint,
access_key: &conf.access_key,
secret_key: &conf.secret_key,
bucket: &conf.bucket,
prefix: &conf.prefix,
region: &conf.region,
bucket_lookup: BucketLookupType::BucketLookupDNS,
provider_tag: "tencent",
validate_endpoint: validate_outbound_url,
})
.await?,
))
}
}
@@ -102,7 +63,7 @@ impl WarmBackend for WarmBackendTencent {
length: i64,
meta: HashMap<String, String>,
) -> Result<String, std::io::Error> {
let part_size = optimal_part_size(length)?;
let part_size = optimal_part_size(length, MIN_PART_SIZE)?;
let client = self.0.client.clone();
let res = client
.put_object(&self.0.bucket, &self.0.get_dest(object), r, length, &{
@@ -133,32 +94,15 @@ impl WarmBackend for WarmBackendTencent {
}
}
fn optimal_part_size(object_size: i64) -> Result<i64, std::io::Error> {
let mut object_size = object_size;
if object_size == -1 {
object_size = MAX_MULTIPART_PUT_OBJECT_SIZE;
}
if object_size > MAX_MULTIPART_PUT_OBJECT_SIZE {
return Err(std::io::Error::other("entity too large"));
}
let configured_part_size = MIN_PART_SIZE;
let mut part_size_flt = object_size as f64 / MAX_PARTS_COUNT as f64;
part_size_flt = (part_size_flt as f64 / configured_part_size as f64).ceil() * configured_part_size as f64;
let part_size = part_size_flt as i64;
if part_size == 0 {
return Ok(MIN_PART_SIZE);
}
Ok(part_size)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::tier::tier_config::TierTencent;
/// The SSRF guard itself is exercised once, generically, in
/// `warm_backend::tests` (see backlog#2040/backlog#2041 and
/// rustfs/rustfs#6764) — this test only pins that this provider's
/// production constructor really is wired through that shared path.
#[tokio::test]
async fn new_rejects_loopback_endpoint_before_network_setup() {
let conf = TierTencent {
-2
View File
@@ -56,12 +56,10 @@
1|crates/ecstore/src/services/tier/tier_config.rs
1|crates/ecstore/src/services/tier/warm_backend.rs
2|crates/ecstore/src/services/tier/warm_backend_gcs.rs
1|crates/ecstore/src/services/tier/warm_backend_huaweicloud.rs
1|crates/ecstore/src/services/tier/warm_backend_minio.rs
1|crates/ecstore/src/services/tier/warm_backend_r2.rs
1|crates/ecstore/src/services/tier/warm_backend_rustfs.rs
1|crates/ecstore/src/services/tier/warm_backend_s3.rs
1|crates/ecstore/src/services/tier/warm_backend_tencent.rs
1|crates/ecstore/src/services/tier/warm_backend_wasabi.rs
7|crates/ecstore/src/set_disk/core/io_primitives.rs
1|crates/ecstore/src/set_disk/mod.rs