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}