Skip to main content

canic_cli/
lib.rs

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