canic-cli 0.58.2

Operator CLI for Canic fleet setup, builds, evidence, catalog, backup, and restore workflows
Documentation
use crate::{
    cli::clap::{flag_arg, parse_matches, path_option, string_option, value_arg},
    cli::defaults::{default_icp, local_network},
    cli::globals::{internal_icp_arg, internal_network_arg},
    cycles::CyclesCommandError,
};
use clap::Command as ClapCommand;
use std::{ffi::OsString, path::PathBuf};

const DEFAULT_SINCE_SECONDS: u64 = 24 * 60 * 60;
const DEFAULT_LIMIT: u64 = 1_000;

const COMMAND_NAME: &str = "cycles";
const DEPLOYMENT_ARG: &str = "deployment";
const JSON_ARG: &str = "json";
const LIMIT_ARG: &str = "limit";
const OUT_ARG: &str = "out";
const SINCE_ARG: &str = "since";
const SUBTREE_ARG: &str = "subtree";
const VERBOSE_ARG: &str = "verbose";

///
/// CyclesOptions
///

#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) struct CyclesOptions {
    pub(super) deployment: String,
    pub(super) subtree: Option<String>,
    pub(super) since_seconds: u64,
    pub(super) limit: u64,
    pub(super) json: bool,
    pub(super) verbose: bool,
    pub(super) out: Option<PathBuf>,
    pub(super) network: String,
    pub(super) icp: String,
}

impl CyclesOptions {
    pub(super) fn parse_info<I>(args: I) -> Result<Self, CyclesCommandError>
    where
        I: IntoIterator<Item = OsString>,
    {
        let matches = parse_matches(info_cycles_command(), args)
            .map_err(|_| CyclesCommandError::Usage(info_usage()))?;
        Self::from_matches(&matches)
    }

    fn from_matches(matches: &clap::ArgMatches) -> Result<Self, CyclesCommandError> {
        let since_seconds = string_option(matches, SINCE_ARG)
            .map(|value| parse_duration(&value))
            .transpose()?
            .unwrap_or(DEFAULT_SINCE_SECONDS);
        let limit = string_option(matches, LIMIT_ARG)
            .and_then(|value| value.parse::<u64>().ok())
            .filter(|limit| *limit > 0)
            .unwrap_or(DEFAULT_LIMIT);

        Ok(Self {
            deployment: string_option(matches, DEPLOYMENT_ARG).expect("clap requires deployment"),
            subtree: string_option(matches, SUBTREE_ARG),
            since_seconds,
            limit,
            json: matches.get_flag(JSON_ARG),
            verbose: matches.get_flag(VERBOSE_ARG),
            out: path_option(matches, OUT_ARG),
            network: string_option(matches, "network").unwrap_or_else(local_network),
            icp: string_option(matches, "icp").unwrap_or_else(default_icp),
        })
    }
}

fn parse_duration(value: &str) -> Result<u64, CyclesCommandError> {
    let value = value.trim();
    let digits = value
        .chars()
        .take_while(char::is_ascii_digit)
        .collect::<String>();
    let suffix = value[digits.len()..].trim();
    let amount = digits
        .parse::<u64>()
        .map_err(|_| CyclesCommandError::InvalidDuration(value.to_string()))?;
    let multiplier = match suffix {
        "s" | "" => 1,
        "m" => 60,
        "h" => 60 * 60,
        "d" => 24 * 60 * 60,
        _ => return Err(CyclesCommandError::InvalidDuration(value.to_string())),
    };
    amount
        .checked_mul(multiplier)
        .filter(|seconds| *seconds > 0)
        .ok_or_else(|| CyclesCommandError::InvalidDuration(value.to_string()))
}

pub(super) fn info_usage() -> String {
    render_usage(info_cycles_command)
}

fn info_cycles_command() -> ClapCommand {
    cycles_command_with_bin_name("canic info cycles")
}

fn cycles_command_with_bin_name(bin_name: &'static str) -> ClapCommand {
    ClapCommand::new(COMMAND_NAME)
        .bin_name(bin_name)
        .about("Summarize installed deployment cycle history")
        .disable_help_flag(true)
        .arg(
            value_arg(DEPLOYMENT_ARG)
                .value_name(DEPLOYMENT_ARG)
                .required(true)
                .help("Installed deployment target name to inspect"),
        )
        .arg(
            value_arg(SINCE_ARG)
                .long(SINCE_ARG)
                .value_name("duration")
                .help("Cycle history window; defaults to 24h"),
        )
        .arg(
            value_arg(SUBTREE_ARG)
                .long(SUBTREE_ARG)
                .value_name("name-or-principal")
                .help("Summarize one subtree anchored at a unique role name or canister principal"),
        )
        .arg(
            value_arg(LIMIT_ARG)
                .long(LIMIT_ARG)
                .value_name("entries")
                .help("Maximum tracker samples to fetch per canister; defaults to 1000"),
        )
        .arg(flag_arg(JSON_ARG).long(JSON_ARG))
        .arg(
            flag_arg(VERBOSE_ARG).long(VERBOSE_ARG).short('v').help(
                "Show diagnostic columns such as canister id, history, topups, and net total",
            ),
        )
        .arg(value_arg(OUT_ARG).long(OUT_ARG).value_name("file"))
        .arg(internal_network_arg())
        .arg(internal_icp_arg())
}

fn render_usage(command: fn() -> ClapCommand) -> String {
    let mut command = command();
    command.render_help().to_string()
}