Skip to main content

horto_os_ui_shared/ops/
doctor.rs

1//! Local host readiness checks for status and pre-apply diagnostics.
2
3use crate::context::{is_root, HostContext};
4use crate::kits::docker;
5use serde::{Deserialize, Serialize};
6
7/// Boolean readiness probes (flattened in JSON).
8#[allow(clippy::struct_excessive_bools)]
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DoctorFlags {
11    /// Process is effective uid 0.
12    pub is_root: bool,
13    /// `sudo` is on `PATH`.
14    pub has_sudo: bool,
15    /// `docker` is on `PATH`.
16    pub docker_present: bool,
17    /// [`HostPaths::active_setup`](crate::paths::HostPaths::active_setup) exists as a directory.
18    pub active_setup_dir: bool,
19    /// Full IoT-LAN env file is present.
20    pub full_env: bool,
21    /// Minimal setup env file is present.
22    pub minimal_env: bool,
23    /// Docker stacks directory exists.
24    pub docker_dir: bool,
25    /// Backup root directory exists.
26    pub backup_dir: bool,
27}
28
29/// Boolean probes and free-form notes from [`doctor`].
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct DoctorReport {
32    /// Flattened probe flags (same JSON field names as before nesting).
33    #[serde(flatten)]
34    pub flags: DoctorFlags,
35    /// Operator-facing warnings (missing root, docker, env files, …).
36    pub notes: Vec<String>,
37}
38
39impl std::ops::Deref for DoctorReport {
40    type Target = DoctorFlags;
41
42    fn deref(&self) -> &Self::Target {
43        &self.flags
44    }
45}
46
47/// Probe the host using `ctx.paths` and return a [`DoctorReport`].
48#[must_use]
49pub fn doctor(ctx: &HostContext) -> DoctorReport {
50    let mut notes = Vec::new();
51    let root = is_root();
52    if !root {
53        notes.push("Not running as root; apply mode needs sudo.".into());
54    }
55    let has_sudo = which::which("sudo").is_ok();
56    let docker_present = docker::docker_available();
57    if !docker_present {
58        notes.push("docker binary not found on PATH.".into());
59    }
60    let active_setup_dir = ctx.paths.active_setup.is_dir();
61    let full_env = ctx.paths.full_env_file().is_file();
62    let minimal_env = ctx.paths.minimal_env_file().is_file();
63    let os_conf = ctx.paths.os_configuration_file().is_file();
64    let docker_dir = ctx.paths.docker.is_dir();
65    let backup_dir = ctx.paths.backup.is_dir();
66    if !full_env && !minimal_env && !os_conf {
67        notes.push(
68            "No active env yet (os-configuration.env, iot-lan_conf.env, or minimal_setup_vars.env)."
69                .into(),
70        );
71    }
72    DoctorReport {
73        flags: DoctorFlags {
74            is_root: root,
75            has_sudo,
76            docker_present,
77            active_setup_dir,
78            full_env,
79            minimal_env,
80            docker_dir,
81            backup_dir,
82        },
83        notes,
84    }
85}