Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions crates/cli/src/commands/admin/ilm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//! ILM administration commands.

use clap::{Args, Subcommand};
use rc_core::admin::{AdminApi, ManualTransitionRunRequest, ManualTransitionRunResponse};
use serde::Serialize;

use super::{emit_observability_error, get_admin_client};
use crate::exit_code::ExitCode;
use crate::output::Formatter;

const MAX_MANUAL_TRANSITION_OBJECTS: u64 = 100_000;

#[derive(Subcommand, Debug)]
pub enum IlmCommands {
/// Manage lifecycle transition operations
#[command(subcommand)]
Transition(TransitionCommands),
}

#[derive(Subcommand, Debug)]
pub enum TransitionCommands {
/// Run bounded lifecycle transition evaluation for existing objects
Run(ManualTransitionRunArgs),
}

#[derive(Args, Debug)]
pub struct ManualTransitionRunArgs {
/// Alias name of the server
pub alias: String,

/// Bucket to evaluate
pub bucket: String,

/// Limit evaluation to this object key prefix
#[arg(long, default_value = "")]
pub prefix: String,

/// Limit evaluation to lifecycle transitions targeting this storage tier
#[arg(long)]
pub tier: Option<String>,

/// Report eligible objects without enqueueing transition tasks
#[arg(long)]
pub dry_run: bool,

/// Maximum number of object versions to scan
#[arg(long, default_value_t = 10_000, value_parser = clap::value_parser!(u64).range(1..=MAX_MANUAL_TRANSITION_OBJECTS))]
pub max_objects: u64,
}

#[derive(Debug, Serialize)]
struct ManualTransitionRunSuccessOutput<'a> {
schema_version: u8,
#[serde(rename = "type")]
output_type: &'static str,
status: &'static str,
data: &'a ManualTransitionRunResponse,
}

pub async fn execute(command: IlmCommands, formatter: &Formatter) -> ExitCode {
match command {
IlmCommands::Transition(TransitionCommands::Run(args)) => {
execute_manual_transition_run(args, formatter).await
}
}
}

async fn execute_manual_transition_run(
args: ManualTransitionRunArgs,
formatter: &Formatter,
) -> ExitCode {
let client = match get_admin_client(&args.alias, formatter) {
Ok(client) => client,
Err(code) => return code,
};

let request = ManualTransitionRunRequest {
bucket: args.bucket,
prefix: args.prefix,
tier: args.tier,
dry_run: args.dry_run,
max_objects: args.max_objects,
};

match client.run_manual_transition(request).await {
Ok(response) => {
if formatter.is_json() {
formatter.json(&ManualTransitionRunSuccessOutput {
schema_version: 3,
output_type: "manual_transition_run",
status: "success",
data: &response,
});
} else {
print_manual_transition_run(&response, formatter);
}
ExitCode::Success
}
Err(error) => emit_observability_error(
"manual_transition_run",
"admin.ilm-transition-run",
"Failed to run manual transition",
&error,
formatter,
),
}
}

fn print_manual_transition_run(response: &ManualTransitionRunResponse, formatter: &Formatter) {
let report = &response.report;
formatter.println(&formatter.style_name("Manual Transition Run"));
formatter.println("");
formatter.println(&format!(
"State: {}",
formatter.sanitize_text(&response.state)
));
formatter.println(&format!(
"Mode: {}",
formatter.sanitize_text(&response.mode)
));
formatter.println(&format!(
"Bucket: {}",
formatter.sanitize_text(&report.bucket)
));
formatter.println(&format!(
"Prefix: {}",
formatter.sanitize_text(value_or_all(&report.prefix))
));
formatter.println(&format!(
"Tier: {}",
formatter.sanitize_text(report.tier.as_deref().map(value_or_all).unwrap_or("all"))
));
formatter.println(&format!("Dry run: {}", report.dry_run));
formatter.println(&format!(
"Lifecycle: {}",
report.lifecycle_config_found
));
formatter.println(&format!("Scanned: {}", report.scanned));
formatter.println(&format!("Eligible: {}", report.eligible));
formatter.println(&format!("Enqueued: {}", report.enqueued));
formatter.println(&format!("Dry-run count: {}", report.dry_run_eligible));
formatter.println(&format!(
"Not due: {}",
report.skipped_not_transition
));
formatter.println(&format!(
"Queue pressure: {}",
report.skipped_queue_full + report.skipped_queue_closed + report.skipped_queue_timeout
));
formatter.println(&format!("Truncated: {}", report.truncated_by_limit));
}

fn value_or_all(value: &str) -> &str {
if value.is_empty() { "all" } else { value }
}
39 changes: 39 additions & 0 deletions crates/cli/src/commands/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod diagnostics;
mod expand;
mod group;
mod heal;
mod ilm;
mod info;
mod kms;
mod metrics;
Expand Down Expand Up @@ -57,6 +58,10 @@ pub enum AdminCommands {
#[command(subcommand)]
Scanner(scanner::ScannerCommands),

/// Manage lifecycle transition operations
#[command(subcommand)]
Ilm(ilm::IlmCommands),

/// Display cluster information (servers, disks, usage)
#[command(subcommand)]
Info(info::InfoCommands),
Expand Down Expand Up @@ -121,6 +126,7 @@ pub async fn execute(cmd: AdminCommands, output_config: OutputConfig) -> ExitCod
AdminCommands::Metrics(args) => metrics::execute(args, &formatter).await,
AdminCommands::Kms(kms_cmd) => kms::execute(kms_cmd, &formatter).await,
AdminCommands::Scanner(scanner_cmd) => scanner::execute(scanner_cmd, &formatter).await,
AdminCommands::Ilm(ilm_cmd) => ilm::execute(ilm_cmd, &formatter).await,
AdminCommands::Info(info_cmd) => info::execute(info_cmd, &formatter).await,
AdminCommands::Heal(heal_cmd) => heal::execute(heal_cmd, &formatter).await,
AdminCommands::Pool(pool_cmd) => pool::execute(pool_cmd, &formatter).await,
Expand Down Expand Up @@ -303,6 +309,39 @@ mod tests {
}
}

#[test]
fn test_parse_admin_ilm_transition_run() {
let cli = TestCli::parse_from([
"rc",
"ilm",
"transition",
"run",
"local",
"photos",
"--prefix",
"logs/",
"--tier",
"COLDTIER",
"--dry-run",
"--max-objects",
"25",
]);

match cli.command {
AdminCommands::Ilm(ilm::IlmCommands::Transition(ilm::TransitionCommands::Run(
args,
))) => {
assert_eq!(args.alias, "local");
assert_eq!(args.bucket, "photos");
assert_eq!(args.prefix, "logs/");
assert_eq!(args.tier.as_deref(), Some("COLDTIER"));
assert!(args.dry_run);
assert_eq!(args.max_objects, 25);
}
_ => panic!("Unexpected ILM transition run command"),
}
}

#[test]
fn test_parse_admin_kms_commands() {
let status = TestCli::parse_from(["rc", "kms", "status", "local"]);
Expand Down
Loading