mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-01 17:58:22 +00:00
feat(admin): expose remote target credential capability state (#6857)
This commit is contained in:
@@ -23,9 +23,9 @@ use crate::admin::storage_api::bucket::metadata::BUCKET_TARGETS_FILE;
|
||||
use crate::admin::storage_api::bucket::metadata_sys;
|
||||
use crate::admin::storage_api::bucket::metadata_sys::get_replication_config;
|
||||
use crate::admin::storage_api::bucket::replication::REMOTE_TARGET_UNSUPPORTED_FIELDS;
|
||||
#[cfg(test)]
|
||||
use crate::admin::storage_api::bucket::replication::REMOTE_TARGET_WRITABLE_FIELDS;
|
||||
use crate::admin::storage_api::bucket::replication::{BucketStats, ReplicationStatusType};
|
||||
#[cfg(test)]
|
||||
use crate::admin::storage_api::bucket::replication::{REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS};
|
||||
use crate::admin::storage_api::bucket::target::{
|
||||
BucketTarget, BucketTargetType, Credentials as TargetCredentials, LatencyStat, duration_from_secs_or_nanos,
|
||||
};
|
||||
@@ -1504,10 +1504,10 @@ impl Operation for ReplicationMrfHandler {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
REMOTE_TARGET_UNSUPPORTED_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS, RemoteTargetCredentialsRequest, RemoteTargetRequest,
|
||||
ReplicationDiffEntry, SUPPORTED_REMOTE_TARGET_API, TargetUpdateOp, build_mrf_response, extract_query_params,
|
||||
parse_remote_target_update_ops, render_mrf_backlog, render_replication_diff, unique_replication_peers,
|
||||
validate_remote_target_tls_settings,
|
||||
REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS, REMOTE_TARGET_UNSUPPORTED_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS,
|
||||
RemoteTargetCredentialsRequest, RemoteTargetRequest, ReplicationDiffEntry, SUPPORTED_REMOTE_TARGET_API, TargetUpdateOp,
|
||||
build_mrf_response, extract_query_params, parse_remote_target_update_ops, render_mrf_backlog, render_replication_diff,
|
||||
unique_replication_peers, validate_remote_target_tls_settings,
|
||||
};
|
||||
use crate::admin::storage_api::bucket::target::{BucketTarget, Credentials as TargetCredentials, LatencyStat};
|
||||
use crate::admin::storage_api::replication::{BucketStats, DurableMrfBacklog, MrfOpKind, MrfReplicateEntry};
|
||||
@@ -2104,12 +2104,20 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn remote_target_request_rejects_unimplemented_fields() {
|
||||
for (field, value) in [
|
||||
("credentials.session_token", serde_json::json!("session-token")),
|
||||
("credentials.expiration", serde_json::json!("2026-01-01T00:00:00Z")),
|
||||
("api", serde_json::json!("s3v2")),
|
||||
("edge", serde_json::json!(true)),
|
||||
("edgeSyncBeforeExpiry", serde_json::json!(true)),
|
||||
for (field, value, historical_field) in [
|
||||
(
|
||||
"credentials.session_token",
|
||||
serde_json::json!("session-token"),
|
||||
Some("credentials.sessionToken"),
|
||||
),
|
||||
(
|
||||
"credentials.expiration",
|
||||
serde_json::json!("2026-01-01T00:00:00Z"),
|
||||
Some("credentials.expiration"),
|
||||
),
|
||||
("api", serde_json::json!("s3v2"), None),
|
||||
("edge", serde_json::json!(true), None),
|
||||
("edgeSyncBeforeExpiry", serde_json::json!(true), None),
|
||||
] {
|
||||
let mut request = valid_remote_target_request();
|
||||
if let Some((credential_field, credential_name)) = field.split_once('.') {
|
||||
@@ -2125,6 +2133,12 @@ mod tests {
|
||||
|
||||
assert!(err.to_string().contains(field));
|
||||
assert!(err.to_string().contains("not supported by this RustFS version"));
|
||||
if let Some(historical_field) = historical_field {
|
||||
assert!(
|
||||
REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS.contains(&historical_field),
|
||||
"rejected field {field} must be advertised as historical-only"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2500,6 +2514,17 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn remote_target_capability_fields_do_not_overlap() {
|
||||
for field in REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS {
|
||||
assert!(
|
||||
!REMOTE_TARGET_WRITABLE_FIELDS.contains(field),
|
||||
"remote target field {field} cannot be both historical-only and writable"
|
||||
);
|
||||
assert!(
|
||||
!REMOTE_TARGET_UNSUPPORTED_FIELDS.contains(field),
|
||||
"remote target field {field} cannot be both historical-only and unsupported"
|
||||
);
|
||||
}
|
||||
|
||||
for field in REMOTE_TARGET_UNSUPPORTED_FIELDS {
|
||||
assert!(
|
||||
!REMOTE_TARGET_WRITABLE_FIELDS.contains(field),
|
||||
|
||||
@@ -24,8 +24,9 @@ use crate::admin::runtime_sources::{
|
||||
DefaultAdminUsecase, QueryServerInfoRequest, current_endpoints_handle, default_admin_usecase, object_store_from_req,
|
||||
};
|
||||
use crate::admin::storage_api::bucket::replication::{
|
||||
REMOTE_TARGET_CAPABILITY_CONTRACT_VERSION, REMOTE_TARGET_UNSUPPORTED_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS,
|
||||
REPLICATION_CAPABILITY_CONTRACT_VERSION, REPLICATION_READ_ONLY_HISTORICAL_FIELDS, REPLICATION_WRITABLE_FIELDS,
|
||||
REMOTE_TARGET_CAPABILITY_CONTRACT_VERSION, REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS, REMOTE_TARGET_UNSUPPORTED_FIELDS,
|
||||
REMOTE_TARGET_WRITABLE_FIELDS, REPLICATION_CAPABILITY_CONTRACT_VERSION, REPLICATION_READ_ONLY_HISTORICAL_FIELDS,
|
||||
REPLICATION_WRITABLE_FIELDS,
|
||||
};
|
||||
use crate::admin::storage_api::cluster::{
|
||||
CapabilityState, CapabilityStatus, ObservabilitySnapshotProvider, TopologySnapshot, TopologySnapshotProvider,
|
||||
@@ -729,6 +730,15 @@ impl ReplicationCapabilities {
|
||||
name,
|
||||
state: ReplicationFieldState::Supported,
|
||||
})
|
||||
.chain(
|
||||
REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|name| ReplicationFieldCapability {
|
||||
name,
|
||||
state: ReplicationFieldState::ReadOnlyHistorical,
|
||||
}),
|
||||
)
|
||||
.chain(
|
||||
REMOTE_TARGET_UNSUPPORTED_FIELDS
|
||||
.iter()
|
||||
@@ -1296,9 +1306,8 @@ mod tests {
|
||||
assert_eq!(response.summary.manual_transition_jobs.state, CapabilityState::Supported);
|
||||
assert_eq!(response.replication.contract_version, 1);
|
||||
assert_eq!(response.replication.bucket_replication.contract_version, 1);
|
||||
// v2: disableProxy moved from unsupported to writable (per-target
|
||||
// read-proxy opt-out reached the admin API).
|
||||
assert_eq!(response.replication.remote_targets.contract_version, 2);
|
||||
// v3: temporary-credential fields are explicitly historical-only.
|
||||
assert_eq!(response.replication.remote_targets.contract_version, 3);
|
||||
assert_eq!(response.replication.bucket_replication.status.state, CapabilityState::Supported);
|
||||
assert_eq!(response.replication.remote_targets.status.state, CapabilityState::Supported);
|
||||
assert_eq!(
|
||||
@@ -1347,6 +1356,17 @@ mod tests {
|
||||
.iter()
|
||||
.any(|field| field.name == "healthCheckDuration" && field.state == super::ReplicationFieldState::Supported)
|
||||
);
|
||||
for name in ["credentials.sessionToken", "credentials.expiration"] {
|
||||
assert!(
|
||||
response
|
||||
.replication
|
||||
.remote_targets
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| field.name == name && field.state == super::ReplicationFieldState::ReadOnlyHistorical),
|
||||
"remote target field {name} must be advertised as historical-only"
|
||||
);
|
||||
}
|
||||
assert_eq!(response.manual_transition_jobs.contract_version, 1);
|
||||
assert_eq!(response.manual_transition_jobs.status.state, CapabilityState::Supported);
|
||||
assert_eq!(response.manual_transition_jobs.modes, ["enqueue_only", "async"]);
|
||||
@@ -1408,7 +1428,7 @@ mod tests {
|
||||
assert_eq!(value["summary"]["manual_transition_jobs"]["state"], "supported");
|
||||
assert_eq!(value["replication"]["contract_version"], 1);
|
||||
assert_eq!(value["replication"]["bucket_replication"]["contract_version"], 1);
|
||||
assert_eq!(value["replication"]["remote_targets"]["contract_version"], 2);
|
||||
assert_eq!(value["replication"]["remote_targets"]["contract_version"], 3);
|
||||
assert_eq!(value["replication"]["bucket_replication"]["status"]["state"], "supported");
|
||||
assert_eq!(value["replication"]["remote_targets"]["status"]["state"], "supported");
|
||||
assert_eq!(
|
||||
@@ -1443,6 +1463,16 @@ mod tests {
|
||||
.iter()
|
||||
.any(|field| field["name"] == "healthCheckDuration" && field["state"] == "supported")
|
||||
);
|
||||
for name in ["credentials.sessionToken", "credentials.expiration"] {
|
||||
assert!(
|
||||
value["replication"]["remote_targets"]["fields"]
|
||||
.as_array()
|
||||
.expect("remote target fields should be an array")
|
||||
.iter()
|
||||
.any(|field| field["name"] == name && field["state"] == "read_only_historical"),
|
||||
"serialized remote target field {name} must be historical-only"
|
||||
);
|
||||
}
|
||||
assert_eq!(value["manual_transition_jobs"]["contract_version"], 1);
|
||||
assert_eq!(value["manual_transition_jobs"]["status"]["state"], "supported");
|
||||
assert_eq!(value["manual_transition_jobs"]["modes"], json!(["enqueue_only", "async"]));
|
||||
|
||||
@@ -443,10 +443,10 @@ pub(crate) mod quota {
|
||||
|
||||
pub(crate) mod replication {
|
||||
pub(crate) use super::ecstore_bucket::replication::{
|
||||
OperatorRuleContract, REMOTE_TARGET_CAPABILITY_CONTRACT_VERSION, REMOTE_TARGET_UNSUPPORTED_FIELDS,
|
||||
REMOTE_TARGET_WRITABLE_FIELDS, REPLICATION_CAPABILITY_CONTRACT_VERSION, REPLICATION_READ_ONLY_HISTORICAL_FIELDS,
|
||||
REPLICATION_WRITABLE_FIELDS, assign_site_replication_rule_priorities, merge_incoming_replication_config,
|
||||
replication_target_arn_deployment_id,
|
||||
OperatorRuleContract, REMOTE_TARGET_CAPABILITY_CONTRACT_VERSION, REMOTE_TARGET_READ_ONLY_HISTORICAL_FIELDS,
|
||||
REMOTE_TARGET_UNSUPPORTED_FIELDS, REMOTE_TARGET_WRITABLE_FIELDS, REPLICATION_CAPABILITY_CONTRACT_VERSION,
|
||||
REPLICATION_READ_ONLY_HISTORICAL_FIELDS, REPLICATION_WRITABLE_FIELDS, assign_site_replication_rule_priorities,
|
||||
merge_incoming_replication_config, replication_target_arn_deployment_id,
|
||||
};
|
||||
pub(crate) type BucketReplicationResyncStatus = super::ecstore_bucket::replication::BucketReplicationResyncStatus;
|
||||
pub(crate) type BucketStats = super::ecstore_bucket::replication::BucketStats;
|
||||
|
||||
Reference in New Issue
Block a user