11//! ILM administration commands.
22
33use 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+ } ;
58use serde:: Serialize ;
9+ use tokio:: time:: { Duration , Instant , sleep} ;
610
711use super :: { emit_observability_error, get_admin_client} ;
812use crate :: exit_code:: ExitCode ;
@@ -22,6 +26,15 @@ pub enum IlmCommands {
2226pub 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 ) ]
57100struct 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+
65117pub 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+
115277fn 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
164350fn 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