seaplane_cli/cli/cmds/locks/
list.rs

1use clap::{ArgMatches, Command};
2
3use crate::{
4    api::LocksReq,
5    cli::cmds::locks::{common, common::SeaplaneLocksCommonArgMatches, CliCommand},
6    context::{Ctx, LocksCtx},
7    error::{CliError, CliErrorKind, Result},
8    ops::locks::{self, ListedLock, LockName},
9    printer::OutputFormat,
10};
11
12static OUTPUT_PAGE_SIZE: usize = 10;
13
14static LONG_ABOUT: &str = "Get information around currently held locks.
15
16There are 3 ways to list locks with this command:
17- Omit the LOCK_NAME argument to list all locks
18- Use a single lock name as the argument, without a trailing slash, this will list only that single lock
19- Use a lock name followed by a trailing slash to list all locks under that directory
20
21Locknames will be displayed in base64 encoded format by default because they may contain
22arbitrary binary data. Using --decode to output the decoded values instead.";
23
24#[derive(Copy, Clone, Debug)]
25pub struct SeaplaneLocksList;
26
27impl SeaplaneLocksList {
28    pub fn command() -> Command {
29        Command::new("list")
30            .visible_alias("ls")
31            .about("Get information around currently held locks")
32            .long_about(LONG_ABOUT)
33            .arg(
34                arg!(lock_name = ["LOCK_NAME"] !required)
35                    .help("The name of a lock. If omitted, all locks are shown. Append a trailing slash to list directory contents"),
36            )
37            .arg(common::base64().requires("lock_name"))
38            .args(common::display_args())
39    }
40}
41
42fn run_one_info(ctx: &mut Ctx) -> Result<()> {
43    let locksctx = ctx.locks_ctx.get_or_init();
44    let lock_name = locksctx.lock_name.as_ref().unwrap();
45    let model_name = lock_name.to_model();
46
47    let mut req = LocksReq::new(ctx)?;
48    req.set_name(model_name)?;
49
50    let resp = req.get_lock_info()?;
51    let out = ListedLock::from(resp);
52
53    match ctx.args.out_format {
54        OutputFormat::Json => cli_println!("{}", serde_json::to_string(&out)?),
55        OutputFormat::Table => locks::print_lock_table(!locksctx.no_header, vec![out], ctx)?,
56    };
57
58    Ok(())
59}
60
61/// Looks up all held locks within this directory, using the root directory if `dir_name` is None.
62fn run_dir_info(ctx: &mut Ctx, dir_name: Option<LockName>) -> Result<()> {
63    let mut last_key = None;
64    let dir = dir_name.map(|d| d.to_model());
65    let mut headers = !ctx.locks_ctx.get_or_init().no_header;
66    let mut table_page = Vec::with_capacity(OUTPUT_PAGE_SIZE);
67
68    loop {
69        let mut req = LocksReq::new(ctx)?;
70        let page = req.get_page(last_key, dir.clone())?;
71
72        // We use the regular paging interface rather than
73        // get_all_pages so that we don't have to store
74        // all of the locks in memory at once.
75        for info in page.locks {
76            let out = ListedLock::from(info);
77            match ctx.args.out_format {
78                OutputFormat::Json => cli_println!("{}", serde_json::to_string(&out)?),
79                OutputFormat::Table => {
80                    table_page.push(out);
81                    if table_page.len() >= OUTPUT_PAGE_SIZE {
82                        locks::print_lock_table(headers, table_page.drain(..), ctx)?;
83                        headers = false;
84                    }
85                }
86            }
87        }
88
89        if let Some(next_key) = page.next {
90            last_key = Some(next_key);
91        } else {
92            if !table_page.is_empty() {
93                locks::print_lock_table(headers, table_page, ctx)?;
94            }
95
96            break;
97        }
98    }
99
100    Ok(())
101}
102
103impl CliCommand for SeaplaneLocksList {
104    fn run(&self, ctx: &mut Ctx) -> Result<()> {
105        let locksctx = ctx.locks_ctx.get_or_init();
106
107        // Check if the lock argument is root, directory or single lock
108        match &locksctx.lock_name {
109            // If there's no lock name given it's an "all locks" query
110            None => run_dir_info(ctx, None),
111            Some(name) => {
112                // We need to at least peek at the final character of the decoded lock name to
113                // determine if its a directory query or not.
114                let mut decoded_lock_name = name
115                    .name
116                    .decoded()
117                    .expect("decoding of a string we encoded shouldn't ever fail");
118
119                if *decoded_lock_name
120                    .last()
121                    .expect("Lock name should hold something else it'd be None")
122                    == b'/'
123                {
124                    // The SDK expects a lock name without the trailing slash for getting a
125                    // directory, so we remove the `/`
126                    decoded_lock_name.pop();
127                    run_dir_info(ctx, Some(LockName::from_name_unencoded(decoded_lock_name)))
128                } else {
129                    run_one_info(ctx)
130                }
131            }
132        }
133    }
134
135    fn update_ctx(&self, matches: &ArgMatches, ctx: &mut Ctx) -> Result<()> {
136        ctx.locks_ctx
137            .init(LocksCtx::from_locks_common(&SeaplaneLocksCommonArgMatches(matches))?);
138
139        ctx.args.out_format = matches.get_one("format").copied().unwrap_or_default();
140        let mut locksctx = ctx.locks_ctx.get_mut().unwrap();
141        locksctx.base64 = matches.get_flag("base64");
142        locksctx.decode = matches.get_flag("decode");
143        locksctx.no_header = matches.get_flag("no-header");
144
145        if locksctx.decode && ctx.args.out_format != OutputFormat::Table {
146            let format_arg = format!("--format {}", ctx.args.out_format);
147            return Err(CliError::from(CliErrorKind::ConflictingArguments(
148                "--decode".to_owned(),
149                format_arg,
150            )));
151        }
152
153        Ok(())
154    }
155}