Skip to main content

canic_cli/
lib.rs

1mod apps;
2mod auth;
3mod backup;
4mod blob_storage;
5mod build;
6mod cli;
7mod cycles;
8mod deploy;
9mod diagnostic;
10mod endpoints;
11mod evidence;
12mod evidence_support;
13mod info;
14mod info_env;
15mod info_subnets;
16mod inspect;
17mod install;
18mod list;
19mod medic;
20mod metrics;
21mod network;
22mod output;
23mod replica;
24mod restore;
25mod scaffold;
26mod state;
27mod status;
28mod support;
29#[cfg(test)]
30mod test_support;
31mod token;
32
33use crate::cli::{
34    argv::trace_if_enabled,
35    clap::parse_matches,
36    globals::{
37        DISPATCH_ARGS, apply_global_environment, apply_global_icp, global_environment_conflict,
38        misplaced_global_option,
39    },
40};
41use clap::error::ErrorKind;
42pub use cli::top_level_command;
43use std::ffi::OsString;
44use thiserror::Error as ThisError;
45
46const VERSION_TEXT: &str = concat!("canic ", env!("CARGO_PKG_VERSION"));
47
48///
49/// CliError
50///
51
52#[derive(Debug, ThisError)]
53pub enum CliError {
54    #[error("backup: {0}")]
55    Backup(#[source] Box<backup::BackupCommandError>),
56
57    #[error("auth: {0}")]
58    Auth(#[source] Box<auth::AuthCommandError>),
59
60    #[error("blob-storage: {0}")]
61    BlobStorage(#[source] Box<blob_storage::BlobStorageCommandError>),
62
63    #[error(transparent)]
64    Clap(#[from] clap::Error),
65
66    #[error("build: {0}")]
67    Build(#[from] build::BuildCommandError),
68
69    #[error("cycles: {0}")]
70    Cycles(#[source] Box<cycles::CyclesCommandError>),
71
72    #[error("deploy: {0}")]
73    Deploy(#[from] deploy::DeployCommandError),
74
75    #[error("diagnostic: {0}")]
76    Diagnostic(#[from] diagnostic::DiagnosticCommandError),
77
78    #[error("evidence: {0}")]
79    Evidence(#[from] evidence::EvidenceCommandError),
80
81    #[error("install: {0}")]
82    Install(#[from] install::InstallCommandError),
83
84    #[error("inspect: {0}")]
85    Inspect(#[source] Box<inspect::InspectCommandError>),
86
87    #[error("info: {0}")]
88    Info(#[from] info::InfoCommandError),
89
90    #[error("medic: {0}")]
91    Medic(#[from] medic::MedicCommandError),
92
93    #[error("network: {0}")]
94    Network(#[from] network::NetworkCommandError),
95
96    #[error("app: {0}")]
97    Apps(#[source] Box<apps::AppCommandError>),
98
99    #[error("state: {0}")]
100    State(#[from] state::StateCommandError),
101
102    #[error("scaffold: {0}")]
103    Scaffold(#[from] scaffold::ScaffoldCommandError),
104
105    #[error("restore: {0}")]
106    Restore(#[from] restore::RestoreCommandError),
107
108    #[error("replica: {0}")]
109    Replica(#[from] replica::ReplicaCommandError),
110
111    #[error("status: {0}")]
112    Status(#[from] status::StatusCommandError),
113
114    #[error("token: {0}")]
115    Token(#[source] Box<token::TokenCommandError>),
116}
117
118impl From<backup::BackupCommandError> for CliError {
119    fn from(error: backup::BackupCommandError) -> Self {
120        Self::Backup(Box::new(error))
121    }
122}
123
124impl From<auth::AuthCommandError> for CliError {
125    fn from(error: auth::AuthCommandError) -> Self {
126        Self::Auth(Box::new(error))
127    }
128}
129
130impl From<blob_storage::BlobStorageCommandError> for CliError {
131    fn from(error: blob_storage::BlobStorageCommandError) -> Self {
132        Self::BlobStorage(Box::new(error))
133    }
134}
135
136impl From<cycles::CyclesCommandError> for CliError {
137    fn from(error: cycles::CyclesCommandError) -> Self {
138        Self::Cycles(Box::new(error))
139    }
140}
141
142impl From<inspect::InspectCommandError> for CliError {
143    fn from(error: inspect::InspectCommandError) -> Self {
144        Self::Inspect(Box::new(error))
145    }
146}
147
148impl From<apps::AppCommandError> for CliError {
149    fn from(error: apps::AppCommandError) -> Self {
150        Self::Apps(Box::new(error))
151    }
152}
153
154impl From<token::TokenCommandError> for CliError {
155    fn from(error: token::TokenCommandError) -> Self {
156        Self::Token(Box::new(error))
157    }
158}
159
160/// Run the CLI from process arguments.
161pub fn run_from_env() -> Result<(), CliError> {
162    let argv = std::env::args_os().collect::<Vec<_>>();
163    trace_if_enabled(&argv);
164    run(argv.into_iter().skip(1))
165}
166
167/// Run the CLI from an argument iterator.
168pub fn run<I>(args: I) -> Result<(), CliError>
169where
170    I: IntoIterator<Item = OsString>,
171{
172    let args = args.into_iter().collect::<Vec<_>>();
173    if let Some(option) = misplaced_global_option(&args) {
174        let error = top_level_command().error(
175            ErrorKind::UnknownArgument,
176            format!("unexpected argument '{option}' found; put it before the command"),
177        );
178        return Err(CliError::Clap(error));
179    }
180    let matches = match parse_matches(top_level_command(), args) {
181        Ok(matches) => matches,
182        Err(error) if !error.use_stderr() => {
183            let _ = error.print();
184            return Ok(());
185        }
186        Err(error) => return Err(CliError::Clap(error)),
187    };
188    let global_icp = cli::clap::string_option(&matches, "icp");
189    let global_environment = cli::clap::string_option(&matches, "environment");
190
191    let (command, subcommand_matches) = matches
192        .subcommand()
193        .unwrap_or_else(|| unreachable!("Clap requires one top-level command"));
194    let mut tail = subcommand_matches
195        .get_many::<OsString>(DISPATCH_ARGS)
196        .map(|values| values.cloned().collect::<Vec<_>>())
197        .unwrap_or_default();
198    if let Some(conflict) =
199        global_environment_conflict(command, &tail, global_environment.as_deref())
200    {
201        return Err(CliError::Clap(
202            top_level_command().error(ErrorKind::InvalidValue, conflict.to_string()),
203        ));
204    }
205    apply_global_icp(command, &mut tail, global_icp);
206    apply_global_environment(command, &mut tail, global_environment);
207    let tail = tail.into_iter();
208
209    match command {
210        "app" => apps::run(tail).map_err(CliError::from),
211        "auth" => auth::run(tail).map_err(CliError::from),
212        "backup" => backup::run(tail).map_err(CliError::from),
213        "blob-storage" => blob_storage::run(tail).map_err(CliError::from),
214        "build" => build::run(tail).map_err(CliError::from),
215        "cycles" => cycles::run(tail).map_err(CliError::from),
216        "deploy" => deploy::run(tail).map_err(CliError::from),
217        "diagnostic" => diagnostic::run(tail).map_err(CliError::from),
218        "evidence" => evidence::run(tail).map_err(CliError::from),
219        "info" => info::run(tail).map_err(CliError::from),
220        "inspect" => inspect::run(tail).map_err(CliError::from),
221        "install" => install::run(tail).map_err(CliError::from),
222        "medic" => medic::run(tail).map_err(CliError::from),
223        "network" => network::run(tail).map_err(CliError::from),
224        "replica" => replica::run(tail).map_err(CliError::from),
225        "restore" => restore::run(tail).map_err(CliError::from),
226        "scaffold" => scaffold::run(tail).map_err(CliError::from),
227        "state" => state::run(tail).map_err(CliError::from),
228        "status" => status::run(tail).map_err(CliError::from),
229        "token" => token::run(tail).map_err(CliError::from),
230        _ => unreachable!("top-level dispatch command only defines known commands"),
231    }
232}
233
234#[must_use]
235pub const fn version_text() -> &'static str {
236    VERSION_TEXT
237}
238
239#[must_use]
240pub fn render_cli_error(error: &CliError) -> String {
241    match error {
242        CliError::BlobStorage(err) => err.json_error_report().unwrap_or_else(|| error.to_string()),
243        CliError::Build(build::BuildCommandError::Clap(err)) | CliError::Clap(err) => {
244            err.to_string().trim_end().to_string()
245        }
246        CliError::Deploy(err) if err.suppress_stderr() => String::new(),
247        CliError::Inspect(err) if err.suppress_stderr() => String::new(),
248        CliError::Medic(err) if err.suppress_stderr() => String::new(),
249        CliError::State(err) if err.suppress_stderr() => String::new(),
250        _ => error.to_string(),
251    }
252}
253
254#[must_use]
255pub fn cli_error_exit_code(err: &CliError) -> i32 {
256    match err {
257        CliError::Auth(err) => i32::from(err.exit_code()),
258        CliError::BlobStorage(err) => i32::from(err.exit_code()),
259        CliError::Build(err) => err.exit_code(),
260        CliError::Clap(err) => err.exit_code(),
261        CliError::Deploy(err) => i32::from(err.exit_code()),
262        CliError::Info(err) => i32::from(err.exit_code()),
263        CliError::Inspect(err) => i32::from(err.exit_code()),
264        CliError::Medic(err) => i32::from(err.exit_code()),
265        CliError::State(err) => i32::from(err.exit_code()),
266        _ => 1,
267    }
268}
269
270#[cfg(test)]
271mod tests;