feat: harden runtime capability snapshots (#3784)

* feat(admin): expose runtime capability snapshots

* feat(runtime): refine workload admission snapshots

* test(ci): align architecture migration checks

* build(deps): bump memmap2 for RUSTSEC-2026-0186

* build(deps): refresh cargo deny lockfile
This commit is contained in:
houseme
2026-06-23 18:10:49 +08:00
committed by GitHub
parent fd2ab38581
commit 46775ae019
13 changed files with 646 additions and 91 deletions
+334
View File
@@ -16,14 +16,22 @@ use super::metrics;
use crate::admin::auth::validate_admin_request;
use crate::admin::router::{AdminOperation, Operation, S3Router};
use crate::app::admin_usecase::{DefaultAdminUsecase, QueryServerInfoRequest};
use crate::app::context::resolve_endpoints_handle;
use crate::auth::{check_key_valid, get_session_token};
use crate::runtime_capabilities::{EndpointTopologySnapshotProvider, RustFsObservabilitySnapshotProvider};
use crate::server::{ADMIN_PREFIX, RemoteAddr};
use crate::workload_admission::workload_admission_registry_snapshot;
use http::{HeaderMap, HeaderValue};
use hyper::{Method, StatusCode};
use matchit::Params;
use rustfs_concurrency::WorkloadAdmissionRegistrySnapshot;
use rustfs_policy::policy::action::{Action, AdminAction, S3Action};
use rustfs_storage_api::{
CapabilityState, CapabilityStatus, ObservabilitySnapshotProvider, TopologySnapshot, TopologySnapshotProvider,
};
use s3s::header::CONTENT_TYPE;
use s3s::{Body, S3Error, S3ErrorCode, S3Request, S3Response, S3Result, s3_error};
use serde::Serialize;
use tracing::{error, info, warn};
const LOG_COMPONENT_ADMIN_API: &str = "admin_api";
@@ -31,6 +39,10 @@ const LOG_SUBSYSTEM_SYSTEM_ADMIN: &str = "system_admin";
const EVENT_ADMIN_REQUEST_REJECTED: &str = "admin_request_rejected";
const EVENT_ADMIN_REQUEST_FAILED: &str = "admin_request_failed";
const EVENT_ADMIN_RESPONSE_EMITTED: &str = "admin_response_emitted";
const OBSERVABILITY_SUMMARY_RESOLVED: &str = "observability summary resolved from provider snapshots";
const TOPOLOGY_SUMMARY_RESOLVED: &str = "topology summary resolved from capability snapshot";
const TOPOLOGY_SNAPSHOT_NOT_AVAILABLE: &str = "endpoint topology is not available before storage endpoint pools initialize";
pub(crate) const RUNTIME_CAPABILITIES_ROUTE_SUFFIX: &str = "/v4/runtime/capabilities";
macro_rules! log_system_request_rejected {
($operation:expr, $reason:expr) => {
@@ -117,6 +129,12 @@ pub fn register_system_route(r: &mut S3Router<AdminOperation>) -> std::io::Resul
AdminOperation(&metrics::MetricsHandler {}),
)?;
r.insert(
Method::GET,
format!("{}{}", ADMIN_PREFIX, RUNTIME_CAPABILITIES_ROUTE_SUFFIX).as_str(),
AdminOperation(&RuntimeCapabilitiesHandler {}),
)?;
Ok(())
}
pub struct ServiceHandle {}
@@ -225,6 +243,176 @@ impl Operation for StorageInfoHandler {
pub struct DataUsageInfoHandler {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RuntimeCapabilitiesSummary {
pub observability: CapabilityStatus,
pub userspace_profiling: CapabilityStatus,
pub memory_sampling: CapabilityStatus,
pub platform: CapabilityStatus,
pub topology: CapabilityStatus,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct RuntimeCapabilitiesResponse {
pub summary: RuntimeCapabilitiesSummary,
pub observability: rustfs_storage_api::ObservabilitySnapshot,
pub workload_admission: WorkloadAdmissionRegistrySnapshot,
pub topology: Option<TopologySnapshot>,
pub topology_status: CapabilityStatus,
}
pub struct RuntimeCapabilitiesHandler {}
pub(crate) async fn build_runtime_capabilities_response()
-> Result<RuntimeCapabilitiesResponse, rustfs_storage_api::CapabilitySnapshotError> {
let observability_provider = RustFsObservabilitySnapshotProvider;
let observability = observability_provider.observability_snapshot().await?;
let workload_admission = workload_admission_registry_snapshot();
let (topology, topology_status) = if let Some(endpoint_pools) = resolve_endpoints_handle() {
let topology_provider = EndpointTopologySnapshotProvider::new(endpoint_pools);
let topology = topology_provider.topology_snapshot().await?;
(Some(topology), CapabilityStatus::supported())
} else {
(None, CapabilityStatus::unknown().with_reason(TOPOLOGY_SNAPSHOT_NOT_AVAILABLE))
};
let summary = build_runtime_capabilities_summary(&observability, topology.as_ref(), &topology_status);
Ok(RuntimeCapabilitiesResponse {
summary,
observability,
workload_admission,
topology,
topology_status,
})
}
fn build_runtime_capabilities_summary(
observability: &rustfs_storage_api::ObservabilitySnapshot,
topology: Option<&TopologySnapshot>,
topology_status: &CapabilityStatus,
) -> RuntimeCapabilitiesSummary {
let userspace_profiling = summarize_named_capability_statuses(
[
("cpu", &observability.userspace_profiling.cpu),
("memory", &observability.userspace_profiling.memory),
("continuous_cpu", &observability.userspace_profiling.continuous_cpu),
("periodic_cpu", &observability.userspace_profiling.periodic_cpu),
],
"userspace profiling",
);
let memory_sampling = summarize_named_capability_statuses(
[
("process", &observability.memory_sampling.process),
("system", &observability.memory_sampling.system),
("cgroup", &observability.memory_sampling.cgroup),
],
"memory sampling",
);
let platform = summarize_named_capability_statuses(
[
("allocator", &observability.platform.allocator),
("ebpf", &observability.platform.ebpf),
("numa", &observability.platform.numa),
],
"platform support",
);
let observability_status = if [userspace_profiling.state, memory_sampling.state, platform.state]
.into_iter()
.any(|state| state == CapabilityState::Unknown)
{
CapabilityStatus::unknown().with_reason(OBSERVABILITY_SUMMARY_RESOLVED)
} else {
CapabilityStatus::supported().with_reason(OBSERVABILITY_SUMMARY_RESOLVED)
};
let topology_summary = topology.map_or_else(
|| topology_status.clone(),
|topology| {
summarize_named_capability_statuses(
[
("profiling", &topology.capabilities.profiling),
("numa", &topology.capabilities.numa),
("failure_domain_labels", &topology.capabilities.failure_domain_labels),
("media_labels", &topology.capabilities.media_labels),
],
"topology capability",
)
.with_reason(TOPOLOGY_SUMMARY_RESOLVED)
},
);
RuntimeCapabilitiesSummary {
observability: observability_status,
userspace_profiling,
memory_sampling,
platform,
topology: topology_summary,
}
}
fn summarize_named_capability_statuses<const N: usize>(
statuses: [(&'static str, &CapabilityStatus); N],
subject: &'static str,
) -> CapabilityStatus {
let unknown = statuses
.iter()
.filter_map(|(name, status)| (status.state == CapabilityState::Unknown).then_some(*name))
.collect::<Vec<_>>();
if !unknown.is_empty() {
return CapabilityStatus::unknown().with_reason(format!("{subject} unresolved fields: {}", unknown.join(", ")));
}
let supported_or_disabled = statuses
.iter()
.any(|(_, status)| matches!(status.state, CapabilityState::Supported | CapabilityState::Disabled));
if supported_or_disabled {
return CapabilityStatus::supported();
}
let unsupported = statuses.iter().map(|(name, _)| *name).collect::<Vec<_>>();
CapabilityStatus::unsupported().with_reason(format!("{subject} unsupported fields: {}", unsupported.join(", ")))
}
#[async_trait::async_trait]
impl Operation for RuntimeCapabilitiesHandler {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let Some(input_cred) = req.credentials else {
log_system_request_rejected!("runtime_capabilities", "missing_credentials");
return Err(s3_error!(InvalidRequest, "get cred failed"));
};
let (cred, owner) =
check_key_valid(get_session_token(&req.uri, &req.headers).unwrap_or_default(), &input_cred.access_key).await?;
let remote_addr = req.extensions.get::<Option<RemoteAddr>>().and_then(|opt| opt.map(|a| a.0));
validate_admin_request(
&req.headers,
&cred,
owner,
false,
vec![Action::AdminAction(AdminAction::ServerInfoAdminAction)],
remote_addr,
)
.await?;
let response = build_runtime_capabilities_response().await.map_err(|err| {
log_system_request_failed!("runtime_capabilities", "build_runtime_capabilities_failed", err);
S3Error::with_message(S3ErrorCode::InternalError, "failed to build runtime capabilities snapshot")
})?;
let data = serde_json::to_vec(&response).map_err(|e| {
log_system_request_failed!("runtime_capabilities", "serialize_runtime_capabilities_failed", e);
S3Error::with_message(S3ErrorCode::InternalError, "failed to serialize runtime capabilities snapshot")
})?;
let mut header = HeaderMap::new();
header.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
log_system_response_emitted!("runtime_capabilities");
Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header))
}
}
#[async_trait::async_trait]
impl Operation for DataUsageInfoHandler {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
@@ -265,3 +453,149 @@ impl Operation for DataUsageInfoHandler {
Ok(S3Response::with_headers((StatusCode::OK, Body::from(data)), header))
}
}
#[cfg(test)]
mod tests {
use super::{
OBSERVABILITY_SUMMARY_RESOLVED, TOPOLOGY_SNAPSHOT_NOT_AVAILABLE, TOPOLOGY_SUMMARY_RESOLVED,
build_runtime_capabilities_response, build_runtime_capabilities_summary,
};
use rustfs_concurrency::{AdmissionState, WorkloadClass};
use rustfs_storage_api::{
CapabilityState, CapabilityStatus, MemorySamplingState, ObservabilitySnapshot, PlatformSupport, TopologyCapabilities,
TopologySnapshot, UserspaceProfilingCapability,
};
#[tokio::test]
async fn runtime_capabilities_response_reports_missing_topology_before_storage_init() {
let response = build_runtime_capabilities_response()
.await
.expect("runtime capabilities response should build");
assert_eq!(response.topology, None);
assert_eq!(response.topology_status.state, CapabilityState::Unknown);
assert_eq!(response.topology_status.reason.as_deref(), Some(TOPOLOGY_SNAPSHOT_NOT_AVAILABLE));
assert_eq!(response.summary.topology.state, CapabilityState::Unknown);
assert_eq!(response.summary.topology.reason.as_deref(), Some(TOPOLOGY_SNAPSHOT_NOT_AVAILABLE));
assert_eq!(response.observability.platform.os.as_deref(), Some(std::env::consts::OS));
assert_eq!(
response
.workload_admission
.get(WorkloadClass::ForegroundRead)
.map(|snapshot| snapshot.state),
Some(AdmissionState::Open)
);
assert_eq!(
response
.workload_admission
.get(WorkloadClass::ForegroundWrite)
.map(|snapshot| snapshot.state),
Some(AdmissionState::Disabled)
);
assert_eq!(response.workload_admission.entries().len(), WorkloadClass::REQUIRED.len());
}
#[test]
fn runtime_capabilities_summary_marks_observability_supported_when_groups_are_resolved() {
let observability = ObservabilitySnapshot {
runtime_telemetry: CapabilityStatus::supported(),
userspace_profiling: UserspaceProfilingCapability {
cpu: CapabilityStatus::supported(),
memory: CapabilityStatus::unsupported().with_reason("platform"),
continuous_cpu: CapabilityStatus::supported(),
periodic_cpu: CapabilityStatus::disabled(),
},
memory_sampling: MemorySamplingState {
process: CapabilityStatus::supported(),
system: CapabilityStatus::supported(),
cgroup: CapabilityStatus::unsupported().with_reason("platform"),
},
platform: PlatformSupport {
target_triple: Some("x86_64-unknown-linux-gnu".to_owned()),
os: Some("linux".to_owned()),
arch: Some("x86_64".to_owned()),
allocator: CapabilityStatus::supported(),
ebpf: CapabilityStatus::unsupported().with_reason("platform"),
numa: CapabilityStatus::unsupported().with_reason("platform"),
},
};
let topology = TopologySnapshot {
pools: Vec::new(),
capabilities: TopologyCapabilities {
profiling: CapabilityStatus::supported(),
numa: CapabilityStatus::unsupported().with_reason("platform"),
failure_domain_labels: CapabilityStatus::supported(),
media_labels: CapabilityStatus::unsupported().with_reason("not reported"),
},
};
let summary = build_runtime_capabilities_summary(&observability, Some(&topology), &CapabilityStatus::supported());
assert_eq!(summary.observability.state, CapabilityState::Supported);
assert_eq!(summary.observability.reason.as_deref(), Some(OBSERVABILITY_SUMMARY_RESOLVED));
assert_eq!(summary.userspace_profiling.state, CapabilityState::Supported);
assert_eq!(summary.memory_sampling.state, CapabilityState::Supported);
assert_eq!(summary.platform.state, CapabilityState::Supported);
assert_eq!(summary.topology.state, CapabilityState::Supported);
assert_eq!(summary.topology.reason.as_deref(), Some(TOPOLOGY_SUMMARY_RESOLVED));
}
#[test]
fn runtime_capabilities_summary_marks_unknown_groups_when_snapshot_is_not_fully_wired() {
let observability = ObservabilitySnapshot {
runtime_telemetry: CapabilityStatus::supported(),
userspace_profiling: UserspaceProfilingCapability {
cpu: CapabilityStatus::unknown().with_reason("not wired"),
memory: CapabilityStatus::unsupported().with_reason("platform"),
continuous_cpu: CapabilityStatus::supported(),
periodic_cpu: CapabilityStatus::disabled(),
},
memory_sampling: MemorySamplingState {
process: CapabilityStatus::supported(),
system: CapabilityStatus::supported(),
cgroup: CapabilityStatus::unknown().with_reason("not wired"),
},
platform: PlatformSupport {
target_triple: Some("x86_64-unknown-linux-gnu".to_owned()),
os: Some("linux".to_owned()),
arch: Some("x86_64".to_owned()),
allocator: CapabilityStatus::supported(),
ebpf: CapabilityStatus::unknown().with_reason("not wired"),
numa: CapabilityStatus::unknown().with_reason("not wired"),
},
};
let topology = TopologySnapshot {
pools: Vec::new(),
capabilities: TopologyCapabilities {
profiling: CapabilityStatus::supported(),
numa: CapabilityStatus::unknown().with_reason("not wired"),
failure_domain_labels: CapabilityStatus::unknown().with_reason("not reported"),
media_labels: CapabilityStatus::unknown().with_reason("not reported"),
},
};
let summary = build_runtime_capabilities_summary(&observability, Some(&topology), &CapabilityStatus::supported());
assert_eq!(summary.observability.state, CapabilityState::Unknown);
assert_eq!(summary.userspace_profiling.state, CapabilityState::Unknown);
assert_eq!(summary.memory_sampling.state, CapabilityState::Unknown);
assert_eq!(summary.platform.state, CapabilityState::Unknown);
assert_eq!(summary.topology.state, CapabilityState::Unknown);
assert!(
summary
.userspace_profiling
.reason
.as_deref()
.unwrap_or_default()
.contains("cpu")
);
assert!(
summary
.topology
.reason
.as_deref()
.unwrap_or_default()
.contains(TOPOLOGY_SUMMARY_RESOLVED)
);
}
}