Skip to content

Commit 53798c0

Browse files
housemeheihutu
andauthored
feat(admin): add manual transition job controls (#334)
Co-authored-by: heihutu <heihutu@gmail.com>
1 parent e933226 commit 53798c0

6 files changed

Lines changed: 955 additions & 37 deletions

File tree

crates/cli/src/commands/admin/ilm.rs

Lines changed: 293 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
//! ILM administration commands.
22
33
use clap::{Args, Subcommand};
4-
use rc_core::admin::{AdminApi, ManualTransitionRunRequest, ManualTransitionRunResponse};
4+
use rc_core::Error;
5+
use rc_core::admin::{
6+
AdminApi, ManualTransitionJobResponse, ManualTransitionRunRequest, ManualTransitionRunResponse,
7+
};
58
use serde::Serialize;
9+
use tokio::time::{Duration, Instant, sleep};
610

711
use super::{emit_observability_error, get_admin_client};
812
use crate::exit_code::ExitCode;
@@ -22,6 +26,15 @@ pub enum IlmCommands {
2226
pub enum TransitionCommands {
2327
/// Run bounded lifecycle transition evaluation for existing objects
2428
Run(ManualTransitionRunArgs),
29+
30+
/// Show durable lifecycle transition job status
31+
Status(ManualTransitionJobArgs),
32+
33+
/// Request cancellation for a durable lifecycle transition job
34+
Cancel(ManualTransitionJobArgs),
35+
36+
/// Wait until a durable lifecycle transition job reaches a terminal state
37+
Wait(ManualTransitionWaitArgs),
2538
}
2639

2740
#[derive(Args, Debug)]
@@ -44,6 +57,10 @@ pub struct ManualTransitionRunArgs {
4457
#[arg(long)]
4558
pub dry_run: bool,
4659

60+
/// Start a durable background job and return its job endpoints
61+
#[arg(long = "async")]
62+
pub async_mode: bool,
63+
4764
/// Maximum number of object versions to scan
4865
#[arg(long, default_value_t = 10_000, value_parser = clap::value_parser!(u64).range(1..=MAX_MANUAL_TRANSITION_OBJECTS))]
4966
pub max_objects: u64,
@@ -53,6 +70,32 @@ pub struct ManualTransitionRunArgs {
5370
pub max_duration_seconds: Option<u64>,
5471
}
5572

73+
#[derive(Args, Debug)]
74+
pub struct ManualTransitionJobArgs {
75+
/// Alias name of the server
76+
pub alias: String,
77+
78+
/// Durable manual transition job ID returned by `transition run --async`
79+
pub job_id: String,
80+
}
81+
82+
#[derive(Args, Debug)]
83+
pub struct ManualTransitionWaitArgs {
84+
/// Alias name of the server
85+
pub alias: String,
86+
87+
/// Durable manual transition job ID returned by `transition run --async`
88+
pub job_id: String,
89+
90+
/// Seconds between status polls
91+
#[arg(long, default_value_t = 2, value_parser = clap::value_parser!(u64).range(1..=60))]
92+
pub poll_interval_seconds: u64,
93+
94+
/// Maximum seconds to wait before returning an error
95+
#[arg(long, value_parser = clap::value_parser!(u64).range(1..=86_400))]
96+
pub timeout_seconds: Option<u64>,
97+
}
98+
5699
#[derive(Debug, Serialize)]
57100
struct ManualTransitionRunSuccessOutput<'a> {
58101
schema_version: u8,
@@ -62,11 +105,29 @@ struct ManualTransitionRunSuccessOutput<'a> {
62105
data: &'a ManualTransitionRunResponse,
63106
}
64107

108+
#[derive(Debug, Serialize)]
109+
struct ManualTransitionJobSuccessOutput<'a> {
110+
schema_version: u8,
111+
#[serde(rename = "type")]
112+
output_type: &'static str,
113+
status: &'static str,
114+
data: &'a ManualTransitionJobResponse,
115+
}
116+
65117
pub async fn execute(command: IlmCommands, formatter: &Formatter) -> ExitCode {
66118
match command {
67119
IlmCommands::Transition(TransitionCommands::Run(args)) => {
68120
execute_manual_transition_run(args, formatter).await
69121
}
122+
IlmCommands::Transition(TransitionCommands::Status(args)) => {
123+
execute_manual_transition_status(args, formatter).await
124+
}
125+
IlmCommands::Transition(TransitionCommands::Cancel(args)) => {
126+
execute_manual_transition_cancel(args, formatter).await
127+
}
128+
IlmCommands::Transition(TransitionCommands::Wait(args)) => {
129+
execute_manual_transition_wait(args, formatter).await
130+
}
70131
}
71132
}
72133

@@ -88,7 +149,13 @@ async fn execute_manual_transition_run(
88149
max_duration_seconds: args.max_duration_seconds,
89150
};
90151

91-
match client.run_manual_transition(request).await {
152+
let result = if args.async_mode {
153+
client.run_manual_transition_async(request).await
154+
} else {
155+
client.run_manual_transition(request).await
156+
};
157+
158+
match result {
92159
Ok(response) => {
93160
if formatter.is_json() {
94161
formatter.json(&ManualTransitionRunSuccessOutput {
@@ -112,6 +179,101 @@ async fn execute_manual_transition_run(
112179
}
113180
}
114181

182+
async fn execute_manual_transition_status(
183+
args: ManualTransitionJobArgs,
184+
formatter: &Formatter,
185+
) -> ExitCode {
186+
let client = match get_admin_client(&args.alias, formatter) {
187+
Ok(client) => client,
188+
Err(code) => return code,
189+
};
190+
191+
match client.manual_transition_job_status(&args.job_id).await {
192+
Ok(response) => {
193+
print_manual_transition_job("manual_transition_job_status", &response, formatter);
194+
ExitCode::Success
195+
}
196+
Err(error) => emit_observability_error(
197+
"manual_transition_job_status",
198+
"admin.ilm-transition-job",
199+
"Failed to get manual transition job status",
200+
&error,
201+
formatter,
202+
),
203+
}
204+
}
205+
206+
async fn execute_manual_transition_cancel(
207+
args: ManualTransitionJobArgs,
208+
formatter: &Formatter,
209+
) -> ExitCode {
210+
let client = match get_admin_client(&args.alias, formatter) {
211+
Ok(client) => client,
212+
Err(code) => return code,
213+
};
214+
215+
match client.cancel_manual_transition_job(&args.job_id).await {
216+
Ok(response) => {
217+
print_manual_transition_job("manual_transition_job_cancel", &response, formatter);
218+
ExitCode::Success
219+
}
220+
Err(error) => emit_observability_error(
221+
"manual_transition_job_cancel",
222+
"admin.ilm-transition-job",
223+
"Failed to cancel manual transition job",
224+
&error,
225+
formatter,
226+
),
227+
}
228+
}
229+
230+
async fn execute_manual_transition_wait(
231+
args: ManualTransitionWaitArgs,
232+
formatter: &Formatter,
233+
) -> ExitCode {
234+
let client = match get_admin_client(&args.alias, formatter) {
235+
Ok(client) => client,
236+
Err(code) => return code,
237+
};
238+
239+
let deadline = args
240+
.timeout_seconds
241+
.map(|seconds| Instant::now() + Duration::from_secs(seconds));
242+
let poll_interval = Duration::from_secs(args.poll_interval_seconds);
243+
244+
loop {
245+
match client.manual_transition_job_status(&args.job_id).await {
246+
Ok(response) if is_terminal_job_status(&response.status) => {
247+
print_manual_transition_job("manual_transition_job_wait", &response, formatter);
248+
return wait_terminal_exit_code(&response.status);
249+
}
250+
Ok(_) if deadline.is_some_and(|deadline| Instant::now() >= deadline) => {
251+
let error = Error::General(format!(
252+
"Timed out waiting for manual transition job '{}' to finish",
253+
args.job_id
254+
));
255+
return emit_observability_error(
256+
"manual_transition_job_wait",
257+
"admin.ilm-transition-job",
258+
"Manual transition job wait timed out",
259+
&error,
260+
formatter,
261+
);
262+
}
263+
Ok(_) => sleep(poll_interval).await,
264+
Err(error) => {
265+
return emit_observability_error(
266+
"manual_transition_job_wait",
267+
"admin.ilm-transition-job",
268+
"Failed while waiting for manual transition job",
269+
&error,
270+
formatter,
271+
);
272+
}
273+
}
274+
}
275+
}
276+
115277
fn print_manual_transition_run(response: &ManualTransitionRunResponse, formatter: &Formatter) {
116278
let report = &response.report;
117279
formatter.println(&formatter.style_name("Manual Transition Run"));
@@ -124,6 +286,24 @@ fn print_manual_transition_run(response: &ManualTransitionRunResponse, formatter
124286
"Mode: {}",
125287
formatter.sanitize_text(&response.mode)
126288
));
289+
if let Some(job_id) = &response.job_id {
290+
formatter.println(&format!(
291+
"Job ID: {}",
292+
formatter.sanitize_text(job_id)
293+
));
294+
}
295+
if let Some(status_endpoint) = &response.status_endpoint {
296+
formatter.println(&format!(
297+
"Status: {}",
298+
formatter.sanitize_text(status_endpoint)
299+
));
300+
}
301+
if let Some(cancel_endpoint) = &response.cancel_endpoint {
302+
formatter.println(&format!(
303+
"Cancel: {}",
304+
formatter.sanitize_text(cancel_endpoint)
305+
));
306+
}
127307
formatter.println(&format!(
128308
"Bucket: {}",
129309
formatter.sanitize_text(&report.bucket)
@@ -159,8 +339,119 @@ fn print_manual_transition_run(response: &ManualTransitionRunResponse, formatter
159339
));
160340
formatter.println(&format!("Limit reached: {}", report.truncated_by_limit));
161341
formatter.println(&format!("Duration hit: {}", report.truncated_by_duration));
342+
if let Some(continuation_token) = &report.continuation_token {
343+
formatter.println(&format!(
344+
"Continuation: {}",
345+
formatter.sanitize_text(continuation_token)
346+
));
347+
}
162348
}
163349

164350
fn value_or_all(value: &str) -> &str {
165351
if value.is_empty() { "all" } else { value }
166352
}
353+
354+
fn print_manual_transition_job(
355+
output_type: &'static str,
356+
response: &ManualTransitionJobResponse,
357+
formatter: &Formatter,
358+
) {
359+
if formatter.is_json() {
360+
formatter.json(&ManualTransitionJobSuccessOutput {
361+
schema_version: 3,
362+
output_type,
363+
status: "success",
364+
data: response,
365+
});
366+
return;
367+
}
368+
369+
let report = &response.report;
370+
let queue = &response.queue_snapshot;
371+
formatter.println(&formatter.style_name("Manual Transition Job"));
372+
formatter.println("");
373+
formatter.println(&format!(
374+
"Status: {}",
375+
formatter.sanitize_text(&response.status)
376+
));
377+
formatter.println(&format!(
378+
"Mode: {}",
379+
formatter.sanitize_text(&response.mode)
380+
));
381+
formatter.println(&format!(
382+
"Job ID: {}",
383+
formatter.sanitize_text(&response.job_id)
384+
));
385+
formatter.println(&format!(
386+
"Bucket: {}",
387+
formatter.sanitize_text(&response.bucket)
388+
));
389+
formatter.println(&format!(
390+
"Prefix: {}",
391+
formatter.sanitize_text(value_or_all(&response.prefix))
392+
));
393+
formatter.println(&format!(
394+
"Tier: {}",
395+
formatter.sanitize_text(response.tier.as_deref().map(value_or_all).unwrap_or("all"))
396+
));
397+
formatter.println(&format!("Dry run: {}", response.dry_run));
398+
formatter.println(&format!("Cancel asked: {}", response.cancel_requested));
399+
formatter.println(&format!(
400+
"Created ns: {}",
401+
response.created_at_unix_nanos
402+
));
403+
formatter.println(&format!(
404+
"Updated ns: {}",
405+
response.updated_at_unix_nanos
406+
));
407+
if let Some(completed_at) = response.completed_at_unix_nanos {
408+
formatter.println(&format!("Completed ns: {completed_at}"));
409+
}
410+
if let Some(reason) = &response.failure_reason {
411+
formatter.println(&format!(
412+
"Failure: {}",
413+
formatter.sanitize_text(reason)
414+
));
415+
}
416+
formatter.println("");
417+
formatter.println(&formatter.style_name("Report"));
418+
formatter.println(&format!("Scanned: {}", report.scanned));
419+
formatter.println(&format!("Eligible: {}", report.eligible));
420+
formatter.println(&format!("Enqueued: {}", report.enqueued));
421+
formatter.println(&format!("Completed: {}", report.transition_completed));
422+
formatter.println(&format!("Failed: {}", report.transition_failed));
423+
formatter.println(&format!(
424+
"Already moved: {}",
425+
report.skipped_already_transitioned
426+
));
427+
formatter.println(&format!("Cancelled: {}", report.cancelled));
428+
formatter.println(&format!("Limit reached: {}", report.truncated_by_limit));
429+
formatter.println(&format!("Duration hit: {}", report.truncated_by_duration));
430+
if let Some(continuation_token) = &report.continuation_token {
431+
formatter.println(&format!(
432+
"Continuation: {}",
433+
formatter.sanitize_text(continuation_token)
434+
));
435+
}
436+
formatter.println("");
437+
formatter.println(&formatter.style_name("Queue"));
438+
formatter.println(&format!("Queued: {}", queue.queued));
439+
formatter.println(&format!("Active: {}", queue.active));
440+
formatter.println(&format!("Workers: {}", queue.workers));
441+
formatter.println(&format!("Capacity: {}", queue.queue_capacity));
442+
}
443+
444+
fn is_terminal_job_status(status: &str) -> bool {
445+
matches!(
446+
status,
447+
"completed" | "partial" | "failed" | "cancelled" | "unknown"
448+
)
449+
}
450+
451+
fn wait_terminal_exit_code(status: &str) -> ExitCode {
452+
if matches!(status, "completed" | "partial") {
453+
ExitCode::Success
454+
} else {
455+
ExitCode::GeneralError
456+
}
457+
}

0 commit comments

Comments
 (0)