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#[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
195pub 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
202pub 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;