Skip to main content

xianyun_cli/commands/
doctor.rs

1//! `caproni doctor`: run built-in diagnostics to help troubleshoot Caproni.
2//!
3//! Port of Go's `cmd/doctor` package.
4
5use std::sync::Arc;
6
7use anyhow::Context as _;
8use clap::Args;
9use xianyun_backends::kubectl;
10use xianyun_cluster::driver::State;
11use xianyun_core::config::caproni::load_from_context;
12use xianyun_core::config::global::Context;
13use xianyun_deploy::diagnostics::base::Meta;
14use xianyun_deploy::diagnostics::builtin;
15use xianyun_executor::exec::CommandRunner;
16
17#[derive(Args, Debug)]
18pub struct DoctorArgs {}
19
20pub fn run(_args: &DoctorArgs, ctx: &Context) -> anyhow::Result<()> {
21    run_doctor(ctx)
22}
23
24/// Port of `doctor.runDoctor`: loads the configuration, resolves the
25/// cluster manager, runs every built-in diagnostic (skipping
26/// cluster-dependent ones when the cluster is not ready), and reports the
27/// results.
28///
29/// # Errors
30///
31/// Returns an error if configuration loading, cluster manager creation,
32/// getting the cluster status, or getting the kubeconfig (once the
33/// cluster is known to be ready) fails.
34pub fn run_doctor(ctx: &Context) -> anyhow::Result<()> {
35    crate::drivers::ensure_registered();
36
37    let container = load_from_context(ctx).context("loading configuration")?;
38
39    let manager = xianyun_cluster::registry::new_manager(
40        container.caproni.cluster.clone(),
41        container.global.profile_name.clone(),
42    )
43    .context("creating cluster manager")?;
44
45    let status = manager.status().context("getting cluster status")?;
46
47    let is_cluster_up = status.state == Some(State::Ready);
48
49    let kubectl_backend: Option<Arc<dyn kubectl::Backend + Send + Sync>> = if is_cluster_up {
50        let kubeconfig = manager.get_kubeconfig().context("getting kubeconfig")?;
51        let executor: Arc<dyn CommandRunner + Send + Sync> =
52            Arc::new(xianyun_executor::exec::Client::new());
53        Some(Arc::new(kubectl::Client::with_config(
54            executor,
55            &kubeconfig,
56        )))
57    } else {
58        tracing::warn!(
59            status = ?status.state,
60            "cluster is not ready, running limited set of diagnostics"
61        );
62        None
63    };
64
65    let meta = Meta {
66        config: Some(container.caproni.clone()),
67        kubectl: kubectl_backend,
68    };
69
70    let runner = xianyun_deploy::diagnostics::Runner::new(builtin::all());
71    let results = runner.run(meta, is_cluster_up);
72
73    let mut errors = Vec::new();
74    let mut warnings = Vec::new();
75
76    for result in results {
77        if result.err.is_some() {
78            errors.push(result);
79        } else if !result.output.is_empty() {
80            warnings.push(result);
81        }
82    }
83
84    if errors.is_empty() && warnings.is_empty() {
85        println!("Caproni is healthy.");
86        return Ok(());
87    }
88
89    for result in &errors {
90        println!("ERROR: {}", result.name);
91        println!("{}", "=".repeat(result.name.len() + 7));
92        println!("{}", result.err.as_deref().unwrap_or_default());
93        println!();
94    }
95
96    for result in &warnings {
97        println!("WARN: {}", result.name);
98        println!("{}", "=".repeat(result.name.len() + 6));
99        println!("{}", result.output);
100        println!();
101    }
102
103    let exit_code = if errors.is_empty() { 1 } else { 2 };
104    Err(crate::exitcode::CliExit::new(exit_code).into())
105}