Skip to main content

greentic_operator/
doctor.rs

1use std::path::{Path, PathBuf};
2
3use crate::domains::{self, Domain};
4
5#[derive(Clone, Debug)]
6pub struct DoctorOptions {
7    pub tenant: Option<String>,
8    pub team: Option<String>,
9    pub strict: bool,
10    pub validator_packs: Vec<PathBuf>,
11}
12
13#[derive(Clone, Copy, Debug)]
14pub enum DoctorScope {
15    One(Domain),
16    All,
17}
18
19#[derive(Clone, Debug)]
20pub struct DoctorRun {
21    pub pack_path: PathBuf,
22    pub status: std::process::ExitStatus,
23}
24
25pub fn run_doctor(
26    root: &Path,
27    scope: DoctorScope,
28    options: DoctorOptions,
29    pack_command: &Path,
30) -> anyhow::Result<Vec<DoctorRun>> {
31    let base_dir = doctor_root(root)?;
32    std::fs::create_dir_all(&base_dir)?;
33
34    let domains = match scope {
35        DoctorScope::One(domain) => vec![domain],
36        DoctorScope::All => vec![
37            Domain::Messaging,
38            Domain::Events,
39            Domain::Secrets,
40            Domain::OAuth,
41        ],
42    };
43
44    let mut runs = Vec::new();
45
46    for domain in domains {
47        let provider_packs = domains::discover_provider_packs(root, domain)?;
48        let validators = if !options.validator_packs.is_empty() {
49            options.validator_packs.clone()
50        } else {
51            domains::validator_pack_path(root, domain)
52                .map(|path| vec![path])
53                .unwrap_or_default()
54        };
55
56        for pack in provider_packs {
57            let run = run_doctor_for_pack(
58                root,
59                &base_dir,
60                domain,
61                &pack.path,
62                &pack.pack_id,
63                &validators,
64                options.strict,
65                pack_command,
66            )?;
67            runs.push(run);
68        }
69    }
70
71    if let Some(selection) = demo_packs(root, &options)? {
72        for pack in selection.packs {
73            let run = run_doctor_for_pack(
74                root,
75                &base_dir,
76                Domain::Messaging,
77                &pack,
78                pack.file_name()
79                    .and_then(|name| name.to_str())
80                    .unwrap_or("pack"),
81                &options.validator_packs,
82                options.strict,
83                pack_command,
84            )?;
85            if !run.status.success() {
86                let _ = write_summary(
87                    &base_dir,
88                    "demo",
89                    &format!("doctor failed for demo pack {:?}", pack.display()),
90                );
91            }
92        }
93        let summary = format!(
94            "demo packs validated for tenant={} team={}\n",
95            selection.tenant,
96            selection.team.unwrap_or_else(|| "none".to_string())
97        );
98        write_summary(&base_dir, "demo", &summary)?;
99    }
100
101    Ok(runs)
102}
103
104pub fn build_doctor_args(
105    pack_path: &Path,
106    validator_packs: &[PathBuf],
107    strict: bool,
108) -> Vec<String> {
109    let mut args = vec!["doctor".to_string(), pack_path.display().to_string()];
110    if strict {
111        args.push("--strict".to_string());
112    }
113    for validator in validator_packs {
114        args.push("--validator-pack".to_string());
115        args.push(validator.display().to_string());
116    }
117    args
118}
119
120#[allow(clippy::too_many_arguments)]
121fn run_doctor_for_pack(
122    _root: &Path,
123    base_dir: &Path,
124    domain: Domain,
125    pack_path: &Path,
126    pack_label: &str,
127    validator_packs: &[PathBuf],
128    strict: bool,
129    pack_command: &Path,
130) -> anyhow::Result<DoctorRun> {
131    let run_dir = base_dir.join(domain_name(domain)).join(pack_label);
132    std::fs::create_dir_all(&run_dir)?;
133    let stdout_path = run_dir.join("stdout.txt");
134    let stderr_path = run_dir.join("stderr.txt");
135
136    let stdout = std::fs::File::create(&stdout_path)?;
137    let stderr = std::fs::File::create(&stderr_path)?;
138
139    let args = build_doctor_args(pack_path, validator_packs, strict);
140    let status = std::process::Command::new(pack_command)
141        .args(&args)
142        .stdout(stdout)
143        .stderr(stderr)
144        .status()?;
145
146    let summary = format!("pack: {}\nstatus: {}\n", pack_path.display(), status);
147    write_summary(
148        base_dir,
149        &format!("{}-{}", domain_name(domain), pack_label),
150        &summary,
151    )?;
152
153    if !status.success() {
154        return Err(anyhow::anyhow!(
155            "greentic-pack doctor failed for {}",
156            pack_path.display()
157        ));
158    }
159
160    Ok(DoctorRun {
161        pack_path: pack_path.to_path_buf(),
162        status,
163    })
164}
165
166struct DemoPackSelection {
167    packs: Vec<PathBuf>,
168    tenant: String,
169    team: Option<String>,
170}
171
172fn demo_packs(root: &Path, options: &DoctorOptions) -> anyhow::Result<Option<DemoPackSelection>> {
173    let Some(tenant) = options.tenant.clone() else {
174        return Ok(None);
175    };
176    let manifest = resolved_manifest_path(root, &tenant, options.team.as_deref());
177    if !manifest.exists() {
178        return Ok(None);
179    }
180    let contents = std::fs::read_to_string(manifest)?;
181    let manifest: ResolvedManifest = serde_yaml_bw::from_str(&contents)?;
182    let mut packs = Vec::new();
183    for pack in manifest.packs {
184        if pack.ends_with(".gtpack") {
185            packs.push(root.join(pack));
186        } else {
187            eprintln!("Warning: skipping non-gtpack demo pack {}", pack);
188        }
189    }
190    Ok(Some(DemoPackSelection {
191        packs,
192        tenant,
193        team: options.team.clone(),
194    }))
195}
196
197fn resolved_manifest_path(root: &Path, tenant: &str, team: Option<&str>) -> PathBuf {
198    let filename = match team {
199        Some(team) => format!("{tenant}.{team}.yaml"),
200        None => format!("{tenant}.yaml"),
201    };
202    root.join("state").join("resolved").join(filename)
203}
204
205fn doctor_root(root: &Path) -> anyhow::Result<PathBuf> {
206    let timestamp = std::time::SystemTime::now()
207        .duration_since(std::time::UNIX_EPOCH)
208        .map_err(|err| anyhow::anyhow!("timestamp error: {err}"))?
209        .as_secs();
210    Ok(root
211        .join("state")
212        .join("doctor")
213        .join(format!("{timestamp}")))
214}
215
216fn write_summary(base_dir: &Path, name: &str, contents: &str) -> anyhow::Result<()> {
217    let summary_path = base_dir.join(format!("{name}-summary.txt"));
218    std::fs::write(summary_path, contents)?;
219    Ok(())
220}
221
222fn domain_name(domain: Domain) -> &'static str {
223    match domain {
224        Domain::Messaging => "messaging",
225        Domain::Events => "events",
226        Domain::Secrets => "secrets",
227        Domain::OAuth => "oauth",
228    }
229}
230
231#[derive(Debug, serde::Deserialize)]
232struct ResolvedManifest {
233    packs: Vec<String>,
234}