cellos_ctl/exit.rs
1//! Exit-code discipline — kubectl/POSIX-aligned, aligned with CellOS engineering doctrine.
2//!
3//! All non-success exits in `cellctl` MUST go through `CtlError::exit()` so the contract
4//! between the CLI and shell scripts/CI is stable:
5//!
6//! | code | meaning |
7//! |------|----------------------------------------------------|
8//! | 0 | success |
9//! | 1 | usage / config error (bad flag, missing file, …) |
10//! | 2 | API error (cellos-server returned 4xx/5xx, network)|
11//! | 3 | validation error (local schema check failed) |
12//!
13//! Errors are always written to **stderr**. Machine-readable output (JSON, names)
14//! always goes to **stdout** so it can be piped through `jq`, `xargs`, etc.
15
16use std::fmt;
17use std::process;
18
19/// Structured CLI error carrying both a message and the doctrine-mandated exit code.
20///
21/// `status` carries the HTTP status code when the error originated from a real
22/// HTTP response with a non-success status. Transport / network errors leave
23/// `status` as `None` so callers can distinguish "the server said no" from
24/// "we never reached the server". See SMOKE-TEST report Finding #1:
25/// `cellctl diff` must only treat HTTP 404 as "would create", not every
26/// `ErrorKind::Api`.
27#[derive(Debug)]
28pub struct CtlError {
29 pub kind: ErrorKind,
30 pub message: String,
31 pub status: Option<u16>,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum ErrorKind {
36 /// Usage / config error — bad CLI invocation, missing config, malformed YAML on read.
37 Usage = 1,
38 /// API error — cellos-server returned an error status, or the network call failed.
39 Api = 2,
40 /// Validation error — local schema check failed before the API call.
41 Validation = 3,
42}
43
44impl ErrorKind {
45 pub fn code(self) -> i32 {
46 self as i32
47 }
48}
49
50impl fmt::Display for ErrorKind {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Usage => f.write_str("usage"),
54 Self::Api => f.write_str("api"),
55 Self::Validation => f.write_str("validation"),
56 }
57 }
58}
59
60impl CtlError {
61 pub fn usage(msg: impl Into<String>) -> Self {
62 Self {
63 kind: ErrorKind::Usage,
64 message: msg.into(),
65 status: None,
66 }
67 }
68 pub fn api(msg: impl Into<String>) -> Self {
69 Self {
70 kind: ErrorKind::Api,
71 message: msg.into(),
72 status: None,
73 }
74 }
75 pub fn validation(msg: impl Into<String>) -> Self {
76 Self {
77 kind: ErrorKind::Validation,
78 message: msg.into(),
79 status: None,
80 }
81 }
82
83 /// Attach an HTTP status code. Use this in client error paths that map
84 /// a real HTTP response into a `CtlError` so downstream callers
85 /// (e.g. `cellctl diff`'s 404→"would create" branch) can distinguish
86 /// "server returned 404" from "transport failure".
87 pub fn with_status(mut self, status: u16) -> Self {
88 self.status = Some(status);
89 self
90 }
91
92 /// Emit the error to stderr and exit with the doctrine-mandated code.
93 /// Never returns.
94 pub fn exit(&self) -> ! {
95 eprintln!("cellctl: {}: {}", self.kind, self.message);
96 process::exit(self.kind.code())
97 }
98}
99
100impl fmt::Display for CtlError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{}: {}", self.kind, self.message)
103 }
104}
105
106impl std::error::Error for CtlError {}
107
108/// Convenience: convert anyhow errors into a generic API error.
109impl From<anyhow::Error> for CtlError {
110 fn from(e: anyhow::Error) -> Self {
111 Self::api(e.to_string())
112 }
113}
114
115impl From<reqwest::Error> for CtlError {
116 fn from(e: reqwest::Error) -> Self {
117 Self::api(format!("http: {e}"))
118 }
119}
120
121impl From<std::io::Error> for CtlError {
122 fn from(e: std::io::Error) -> Self {
123 Self::usage(format!("io: {e}"))
124 }
125}
126
127impl From<serde_yaml::Error> for CtlError {
128 fn from(e: serde_yaml::Error) -> Self {
129 Self::validation(format!("yaml: {e}"))
130 }
131}
132
133impl From<serde_json::Error> for CtlError {
134 fn from(e: serde_json::Error) -> Self {
135 Self::api(format!("json: {e}"))
136 }
137}
138
139pub type CtlResult<T> = Result<T, CtlError>;