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