mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
feat(admin): expose ILM expiry status (#5268)
Co-authored-by: heihutu <heihutu@gmail.com>
This commit is contained in:
@@ -226,6 +226,12 @@ struct ApiDiscovery {
|
|||||||
cluster_snapshot: String,
|
cluster_snapshot: String,
|
||||||
#[serde(rename = "extensionsCatalog")]
|
#[serde(rename = "extensionsCatalog")]
|
||||||
extensions_catalog: String,
|
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 {
|
fn console_api_discovery() -> ApiDiscovery {
|
||||||
@@ -234,6 +240,9 @@ fn console_api_discovery() -> ApiDiscovery {
|
|||||||
runtime_capabilities: usecase.runtime_capabilities_route().to_string(),
|
runtime_capabilities: usecase.runtime_capabilities_route().to_string(),
|
||||||
cluster_snapshot: usecase.cluster_snapshot_route().to_string(),
|
cluster_snapshot: usecase.cluster_snapshot_route().to_string(),
|
||||||
extensions_catalog: usecase.extensions_catalog_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.runtime_capabilities, "/rustfs/admin/v4/runtime/capabilities");
|
||||||
assert_eq!(cfg.api.discovery.cluster_snapshot, "/rustfs/admin/v4/cluster/snapshot");
|
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.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]
|
#[tokio::test]
|
||||||
@@ -871,6 +883,12 @@ mod tests {
|
|||||||
assert_eq!(value["api"]["discovery"]["runtimeCapabilities"], "/rustfs/admin/v4/runtime/capabilities");
|
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"]["clusterSnapshot"], "/rustfs/admin/v4/cluster/snapshot");
|
||||||
assert_eq!(value["api"]["discovery"]["extensionsCatalog"], "/rustfs/admin/v4/extensions/catalog");
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ mod tests {
|
|||||||
let _list_remote_target_handler = replication::ListRemoteTargetHandler {};
|
let _list_remote_target_handler = replication::ListRemoteTargetHandler {};
|
||||||
let _remove_remote_target_handler = replication::RemoveRemoteTargetHandler {};
|
let _remove_remote_target_handler = replication::RemoveRemoteTargetHandler {};
|
||||||
let _scanner_status_handler = scanner::ScannerStatusHandler {};
|
let _scanner_status_handler = scanner::ScannerStatusHandler {};
|
||||||
|
let _ilm_expiry_status_handler = scanner::IlmExpiryStatusHandler {};
|
||||||
let _manual_transition_handler = ilm_transition::ManualTransitionRunHandler {};
|
let _manual_transition_handler = ilm_transition::ManualTransitionRunHandler {};
|
||||||
let _manual_transition_status_handler = ilm_transition::ManualTransitionJobStatusHandler {};
|
let _manual_transition_status_handler = ilm_transition::ManualTransitionJobStatusHandler {};
|
||||||
let _manual_transition_cancel_handler = ilm_transition::ManualTransitionJobCancelHandler {};
|
let _manual_transition_cancel_handler = ilm_transition::ManualTransitionJobCancelHandler {};
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use chrono::Utc;
|
|||||||
use http::{HeaderMap, HeaderValue};
|
use http::{HeaderMap, HeaderValue};
|
||||||
use hyper::{Method, StatusCode};
|
use hyper::{Method, StatusCode};
|
||||||
use matchit::Params;
|
use matchit::Params;
|
||||||
use rustfs_common::metrics::ScannerMetricsReport;
|
use rustfs_common::metrics::{ScannerLifecycleExpirySnapshot, ScannerMaintenanceControlSnapshot, ScannerMetricsReport};
|
||||||
use rustfs_credentials::Credentials;
|
use rustfs_credentials::Credentials;
|
||||||
use rustfs_policy::policy::action::{Action, AdminAction};
|
use rustfs_policy::policy::action::{Action, AdminAction};
|
||||||
use s3s::header::CONTENT_TYPE;
|
use s3s::header::CONTENT_TYPE;
|
||||||
@@ -49,6 +49,17 @@ struct ScannerFreshnessStatus {
|
|||||||
reason: Option<&'static str>,
|
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> {
|
fn scanner_disabled_reason(enabled: bool) -> Option<String> {
|
||||||
(!enabled).then(|| format!("disabled by {ENV_SCANNER_ENABLED}"))
|
(!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<()> {
|
pub fn register_scanner_route(r: &mut S3Router<AdminOperation>) -> std::io::Result<()> {
|
||||||
r.insert(
|
r.insert(
|
||||||
Method::GET,
|
Method::GET,
|
||||||
format!("{ADMIN_PREFIX}/v3/scanner/status").as_str(),
|
format!("{ADMIN_PREFIX}/v3/scanner/status").as_str(),
|
||||||
AdminOperation(&ScannerStatusHandler {}),
|
AdminOperation(&ScannerStatusHandler {}),
|
||||||
)?;
|
)?;
|
||||||
|
r.insert(
|
||||||
|
Method::GET,
|
||||||
|
format!("{ADMIN_PREFIX}/v3/ilm/expiry/status").as_str(),
|
||||||
|
AdminOperation(&IlmExpiryStatusHandler {}),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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_enabled"], false);
|
||||||
assert_eq!(encoded["cycle_schedule"]["clean_idle_backoff_multiplier"], 1);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -413,6 +413,12 @@ pub const ADMIN_ROUTE_POLICY_SPECS: &[AdminRouteSpec] = &[
|
|||||||
admin(HttpMethod::Get, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
|
admin(HttpMethod::Get, "/rustfs/admin/v3/config", CONFIG_UPDATE, RouteRiskLevel::High),
|
||||||
admin(HttpMethod::Put, "/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/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::Post, "/rustfs/admin/v3/ilm/transition/run", SET_TIER, RouteRiskLevel::High),
|
||||||
admin(
|
admin(
|
||||||
HttpMethod::Get,
|
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]
|
#[test]
|
||||||
fn route_policy_requires_set_tier_for_manual_transition_routes() {
|
fn route_policy_requires_set_tier_for_manual_transition_routes() {
|
||||||
assert_action(HttpMethod::Post, "/rustfs/admin/v3/ilm/transition/run", SET_TIER);
|
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(Method::PUT, "/v3/tier"),
|
||||||
admin_route_sample(Method::POST, "/v3/tier/{tiername}", "/v3/tier/HOT"),
|
admin_route_sample(Method::POST, "/v3/tier/{tiername}", "/v3/tier/HOT"),
|
||||||
admin_route(Method::POST, "/v3/tier/clear"),
|
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(Method::POST, "/v3/ilm/transition/run"),
|
||||||
admin_route_sample(
|
admin_route_sample(
|
||||||
Method::GET,
|
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::GET, &admin_path("/v3/config"));
|
||||||
assert_route(&router, Method::PUT, &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/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, Method::POST, &admin_path("/v3/ilm/transition/run"));
|
||||||
assert_route(
|
assert_route(
|
||||||
&router,
|
&router,
|
||||||
@@ -1317,6 +1319,7 @@ fn test_admin_alias_paths_match_existing_admin_routes() {
|
|||||||
(Method::GET, compat_admin_alias_path("/v3/config")),
|
(Method::GET, compat_admin_alias_path("/v3/config")),
|
||||||
(Method::PUT, 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/scanner/status")),
|
||||||
|
(Method::GET, compat_admin_alias_path("/v3/ilm/expiry/status")),
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
router.contains_compatible_route(method.clone(), &path),
|
router.contains_compatible_route(method.clone(), &path),
|
||||||
|
|||||||
Reference in New Issue
Block a user