feat(admin): expose ILM expiry status (#5268)

Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
houseme
2026-07-26 13:32:28 +08:00
committed by GitHub
parent 29b4a98e74
commit 7667b7aaf8
5 changed files with 128 additions and 1 deletions
+18
View File
@@ -226,6 +226,12 @@ struct ApiDiscovery {
cluster_snapshot: String,
#[serde(rename = "extensionsCatalog")]
extensions_catalog: String,
#[serde(rename = "ilmExpiryStatus")]
ilm_expiry_status: String,
#[serde(rename = "ilmTransitionRun")]
ilm_transition_run: String,
#[serde(rename = "ilmTransitionJob")]
ilm_transition_job: String,
}
fn console_api_discovery() -> ApiDiscovery {
@@ -234,6 +240,9 @@ fn console_api_discovery() -> ApiDiscovery {
runtime_capabilities: usecase.runtime_capabilities_route().to_string(),
cluster_snapshot: usecase.cluster_snapshot_route().to_string(),
extensions_catalog: usecase.extensions_catalog_route().to_string(),
ilm_expiry_status: format!("{RUSTFS_ADMIN_PREFIX}/ilm/expiry/status"),
ilm_transition_run: format!("{RUSTFS_ADMIN_PREFIX}/ilm/transition/run"),
ilm_transition_job: format!("{RUSTFS_ADMIN_PREFIX}/ilm/transition/jobs/{{job_id}}"),
}
}
@@ -853,6 +862,9 @@ mod tests {
assert_eq!(cfg.api.discovery.runtime_capabilities, "/rustfs/admin/v4/runtime/capabilities");
assert_eq!(cfg.api.discovery.cluster_snapshot, "/rustfs/admin/v4/cluster/snapshot");
assert_eq!(cfg.api.discovery.extensions_catalog, "/rustfs/admin/v4/extensions/catalog");
assert_eq!(cfg.api.discovery.ilm_expiry_status, "/rustfs/admin/v3/ilm/expiry/status");
assert_eq!(cfg.api.discovery.ilm_transition_run, "/rustfs/admin/v3/ilm/transition/run");
assert_eq!(cfg.api.discovery.ilm_transition_job, "/rustfs/admin/v3/ilm/transition/jobs/{job_id}");
}
#[tokio::test]
@@ -871,6 +883,12 @@ mod tests {
assert_eq!(value["api"]["discovery"]["runtimeCapabilities"], "/rustfs/admin/v4/runtime/capabilities");
assert_eq!(value["api"]["discovery"]["clusterSnapshot"], "/rustfs/admin/v4/cluster/snapshot");
assert_eq!(value["api"]["discovery"]["extensionsCatalog"], "/rustfs/admin/v4/extensions/catalog");
assert_eq!(value["api"]["discovery"]["ilmExpiryStatus"], "/rustfs/admin/v3/ilm/expiry/status");
assert_eq!(value["api"]["discovery"]["ilmTransitionRun"], "/rustfs/admin/v3/ilm/transition/run");
assert_eq!(
value["api"]["discovery"]["ilmTransitionJob"],
"/rustfs/admin/v3/ilm/transition/jobs/{job_id}"
);
}
#[test]
+1
View File
@@ -120,6 +120,7 @@ mod tests {
let _list_remote_target_handler = replication::ListRemoteTargetHandler {};
let _remove_remote_target_handler = replication::RemoveRemoteTargetHandler {};
let _scanner_status_handler = scanner::ScannerStatusHandler {};
let _ilm_expiry_status_handler = scanner::IlmExpiryStatusHandler {};
let _manual_transition_handler = ilm_transition::ManualTransitionRunHandler {};
let _manual_transition_status_handler = ilm_transition::ManualTransitionJobStatusHandler {};
let _manual_transition_cancel_handler = ilm_transition::ManualTransitionJobCancelHandler {};
+94 -1
View File
@@ -22,7 +22,7 @@ use chrono::Utc;
use http::{HeaderMap, HeaderValue};
use hyper::{Method, StatusCode};
use matchit::Params;
use rustfs_common::metrics::ScannerMetricsReport;
use rustfs_common::metrics::{ScannerLifecycleExpirySnapshot, ScannerMaintenanceControlSnapshot, ScannerMetricsReport};
use rustfs_credentials::Credentials;
use rustfs_policy::policy::action::{Action, AdminAction};
use s3s::header::CONTENT_TYPE;
@@ -49,6 +49,17 @@ struct ScannerFreshnessStatus {
reason: Option<&'static str>,
}
#[derive(Debug, Serialize)]
struct IlmExpiryStatusResponse {
enabled: bool,
disabled_reason: Option<String>,
freshness: ScannerFreshnessStatus,
lifecycle_expiry: ScannerLifecycleExpirySnapshot,
maintenance_control: ScannerMaintenanceControlSnapshot,
current_cycle_lifecycle_expiry_actions: u64,
last_cycle_lifecycle_expiry_actions: u64,
}
fn scanner_disabled_reason(enabled: bool) -> Option<String> {
(!enabled).then(|| format!("disabled by {ENV_SCANNER_ENABLED}"))
}
@@ -110,12 +121,35 @@ fn scanner_status_response(
}
}
fn ilm_expiry_status_response(
enabled: bool,
metrics: ScannerMetricsReport,
runtime_config: rustfs_scanner::runtime_config::ScannerRuntimeConfigStatus,
cycle_schedule: rustfs_scanner::ScannerCycleScheduleStatus,
) -> IlmExpiryStatusResponse {
let freshness = scanner_freshness_status(&metrics, &runtime_config, cycle_schedule.effective_interval_seconds());
IlmExpiryStatusResponse {
enabled,
disabled_reason: scanner_disabled_reason(enabled),
freshness,
lifecycle_expiry: metrics.lifecycle_expiry,
maintenance_control: metrics.maintenance_control,
current_cycle_lifecycle_expiry_actions: metrics.current_cycle_lifecycle_expiry_actions,
last_cycle_lifecycle_expiry_actions: metrics.last_cycle_lifecycle_expiry_actions,
}
}
pub fn register_scanner_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
r.insert(
Method::GET,
format!("{ADMIN_PREFIX}/v3/scanner/status").as_str(),
AdminOperation(&ScannerStatusHandler {}),
)?;
r.insert(
Method::GET,
format!("{ADMIN_PREFIX}/v3/ilm/expiry/status").as_str(),
AdminOperation(&IlmExpiryStatusHandler {}),
)?;
Ok(())
}
@@ -172,6 +206,25 @@ impl Operation for ScannerStatusHandler {
}
}
pub struct IlmExpiryStatusHandler {}
#[async_trait::async_trait]
impl Operation for IlmExpiryStatusHandler {
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
let _cred = validate_scanner_status_request(&req).await?;
let enabled = scanner_enabled_from_env();
let metrics = current_scanner_metrics_report().await;
let runtime_config = rustfs_scanner::scanner_runtime_config_status();
let cycle_schedule = rustfs_scanner::scanner_cycle_schedule_status();
let response = ilm_expiry_status_response(enabled, metrics, runtime_config, cycle_schedule);
let body = serde_json::to_vec(&response).map_err(|err| {
S3Error::with_message(S3ErrorCode::InternalError, format!("failed to encode ILM expiry status: {err}"))
})?;
json_response(body)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -244,4 +297,44 @@ mod tests {
assert_eq!(encoded["cycle_schedule"]["clean_idle_backoff_enabled"], false);
assert_eq!(encoded["cycle_schedule"]["clean_idle_backoff_multiplier"], 1);
}
#[test]
fn ilm_expiry_status_serializes_lifecycle_expiry_contract() {
let metrics = ScannerMetricsReport {
lifecycle_expiry: ScannerLifecycleExpirySnapshot {
current_queue_capacity: 32,
current_queued: 7,
current_active: 2,
current_workers: 4,
queue_missed: 3,
scanner_queued: 17,
scanner_missed: 5,
scanner_blocked: 11,
scanner_not_enqueued: 13,
delete_failed: 19,
},
maintenance_control: ScannerMaintenanceControlSnapshot {
primary_control: "expiry_backlog".to_string(),
..Default::default()
},
current_cycle_lifecycle_expiry_actions: 23,
last_cycle_lifecycle_expiry_actions: 29,
..Default::default()
};
let response = ilm_expiry_status_response(
true,
metrics,
rustfs_scanner::scanner_runtime_config_status(),
rustfs_scanner::ScannerCycleScheduleStatus::default(),
);
let encoded = serde_json::to_value(response).expect("ILM expiry status should serialize");
assert_eq!(encoded["lifecycle_expiry"]["current_queue_capacity"].as_u64(), Some(32));
assert_eq!(encoded["lifecycle_expiry"]["scanner_blocked"].as_u64(), Some(11));
assert_eq!(encoded["lifecycle_expiry"]["scanner_not_enqueued"].as_u64(), Some(13));
assert_eq!(encoded["lifecycle_expiry"]["delete_failed"].as_u64(), Some(19));
assert_eq!(encoded["maintenance_control"]["primary_control"].as_str(), Some("expiry_backlog"));
assert_eq!(encoded["current_cycle_lifecycle_expiry_actions"].as_u64(), Some(23));
assert_eq!(encoded["last_cycle_lifecycle_expiry_actions"].as_u64(), Some(29));
}
}
+12
View File
@@ -413,6 +413,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
admin(HttpMethod::Get, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
admin(HttpMethod::Put, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
admin(HttpMethod::Get, "/rustfs/admin/v3/scanner/status", SERVER_INFO, RouteRiskLevel::Sensitive),
admin(
HttpMethod::Get,
"/rustfs/admin/v3/ilm/expiry/status",
SERVER_INFO,
RouteRiskLevel::Sensitive,
),
admin(HttpMethod::Post, "/rustfs/admin/v3/ilm/transition/run", SET_TIER, RouteRiskLevel::High),
admin(
HttpMethod::Get,
@@ -1812,6 +1818,12 @@ mod tests {
}
}
#[test]
fn route_policy_allows_server_info_for_ilm_expiry_status() {
assert_action(HttpMethod::Get, "/rustfs/admin/v3/ilm/expiry/status", SERVER_INFO);
assert_not_action(HttpMethod::Get, "/rustfs/admin/v3/ilm/expiry/status", SET_TIER);
}
#[test]
fn route_policy_requires_set_tier_for_manual_transition_routes() {
assert_action(HttpMethod::Post, "/rustfs/admin/v3/ilm/transition/run", SET_TIER);
@@ -195,6 +195,7 @@ fn expected_admin_route_matrix() -> Vec<RouteMatrixEntry> {
admin_route(Method::PUT, "/v3/tier"),
admin_route_sample(Method::POST, "/v3/tier/{tiername}", "/v3/tier/HOT"),
admin_route(Method::POST, "/v3/tier/clear"),
admin_route(Method::GET, "/v3/ilm/expiry/status"),
admin_route(Method::POST, "/v3/ilm/transition/run"),
admin_route_sample(
Method::GET,
@@ -843,6 +844,7 @@ fn test_register_routes_cover_representative_admin_paths() {
assert_route(&router, Method::GET, &admin_path("/v3/config"));
assert_route(&router, Method::PUT, &admin_path("/v3/config"));
assert_route(&router, Method::GET, &admin_path("/v3/scanner/status"));
assert_route(&router, Method::GET, &admin_path("/v3/ilm/expiry/status"));
assert_route(&router, Method::POST, &admin_path("/v3/ilm/transition/run"));
assert_route(
&router,
@@ -1317,6 +1319,7 @@ fn test_admin_alias_paths_match_existing_admin_routes() {
(Method::GET, compat_admin_alias_path("/v3/config")),
(Method::PUT, compat_admin_alias_path("/v3/config")),
(Method::GET, compat_admin_alias_path("/v3/scanner/status")),
(Method::GET, compat_admin_alias_path("/v3/ilm/expiry/status")),
] {
assert!(
router.contains_compatible_route(method.clone(), &path),