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#[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
178pub 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
185pub 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;