Skip to main content

canic_cli/
lib.rs

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