Skip to main content

canic_cli/
lib.rs

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