cellos_ctl/cmd/version.rs
1//! `cellctl version` — print client version, then attempt the server version.
2//!
3//! The client version is printed unconditionally on its own line so an operator
4//! always sees what they have locally even when the server is unreachable.
5//! After that, we attempt the server query and:
6//!
7//! - on success, print `server: cellos-server <ver> (<url>)` and exit 0;
8//! - on transport OR HTTP error, print `error: unable to connect to the server: <reason>`
9//! to stderr and return a [`CtlError::api`] so the process exits non-zero.
10//!
11//! This matches `kubectl version`, which also exits non-zero when it cannot
12//! reach the API server. See SMOKE-TEST report Finding #2.
13
14use serde_json::Value;
15
16use crate::client::CellosClient;
17use crate::exit::{CtlError, CtlResult};
18
19pub async fn run(client: &CellosClient) -> CtlResult<()> {
20 println!("client: cellctl {}", env!("CARGO_PKG_VERSION"));
21
22 match client.get_json::<Value>("/v1/version").await {
23 Ok(v) => {
24 let svr = v
25 .get("version")
26 .and_then(|x| x.as_str())
27 .unwrap_or("unknown");
28 println!("server: cellos-server {svr} ({})", client.base_url());
29 Ok(())
30 }
31 Err(e) => {
32 // Propagate as an API error so the process exits with code 2 —
33 // scripts and CI gates that branch on $? get an honest signal
34 // instead of a false-success. `CtlError::exit` formats this as
35 // `cellctl: api: unable to connect to the server: <reason>` on
36 // stderr, matching the existing error-output contract.
37 Err(CtlError::api(format!(
38 "unable to connect to the server: {e}"
39 )))
40 }
41 }
42}