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