1use crate::{ProvenanceMap, ProvenanceMapDetailed, ProvenanceRecord};
2use greentic_config_types::{ConfigSource, GreenticConfig};
3use serde::Serialize;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct ExplainReport {
7 pub text: String,
8 pub json: serde_json::Value,
9}
10
11impl ExplainReport {
12 #[allow(clippy::inherent_to_string)]
13 pub fn to_string(&self) -> String {
14 self.text.clone()
15 }
16
17 pub fn to_json(&self) -> serde_json::Value {
18 self.json.clone()
19 }
20}
21
22pub fn explain(
23 config: &GreenticConfig,
24 provenance: &ProvenanceMap,
25 warnings: &[String],
26) -> ExplainReport {
27 let mut lines = Vec::new();
28 lines.push("Configuration:".to_string());
29 lines.push(format!(
30 "- schema_version: {}",
31 config.schema_version.0.as_str()
32 ));
33 let env_prov = provenance.get(&greentic_config_types::ProvenancePath(
34 "environment.env_id".into(),
35 ));
36 lines.push(format!(
37 "- environment.env_id: {:?} ({})",
38 config.environment.env_id,
39 render_source(env_prov)
40 ));
41 lines.push(format!(
42 "- paths.state_dir: {} ({})",
43 config.paths.state_dir.display(),
44 render_source(provenance.get(&greentic_config_types::ProvenancePath(
45 "paths.state_dir".into()
46 )))
47 ));
48 lines.push(format!(
49 "- telemetry.exporter: {:?} ({})",
50 config.telemetry.exporter,
51 render_source(provenance.get(&greentic_config_types::ProvenancePath(
52 "telemetry.exporter".into()
53 )))
54 ));
55 lines.push(format!(
56 "- network.tls_mode: {:?} ({})",
57 config.network.tls_mode,
58 render_source(provenance.get(&greentic_config_types::ProvenancePath(
59 "network.tls_mode".into()
60 )))
61 ));
62 if let Some(deployer) = &config.deployer
63 && let Some(base_domain) = &deployer.base_domain
64 {
65 lines.push(format!(
66 "- deployer.base_domain: {} ({})",
67 base_domain,
68 render_source(provenance.get(&greentic_config_types::ProvenancePath(
69 "deployer.base_domain".into()
70 )))
71 ));
72 }
73 if let Some(services) = &config.services {
74 if let Some(events) = &services.events {
75 lines.push(format!(
76 "- services.events.url: {} ({})",
77 events.url,
78 render_source(provenance.get(&greentic_config_types::ProvenancePath(
79 "services.events.url".into()
80 )))
81 ));
82 }
83 append_service_bindings(&mut lines, services, provenance);
84 }
85 if let Some(runtime) = &config.runtime.admin_endpoints {
86 lines.push(format!(
87 "- runtime.admin_endpoints.secrets_explain_enabled: {} ({})",
88 runtime.secrets_explain_enabled,
89 render_source(provenance.get(&greentic_config_types::ProvenancePath(
90 "runtime.admin_endpoints.secrets_explain_enabled".into()
91 )))
92 ));
93 }
94 if let Some(events) = &config.events {
95 if let Some(backoff) = &events.backoff {
96 lines.push(format!(
97 "- events.backoff.initial_ms: {:?} ({})",
98 backoff.initial_ms,
99 render_source(provenance.get(&greentic_config_types::ProvenancePath(
100 "events.backoff".into()
101 )))
102 ));
103 }
104 if let Some(reconnect) = &events.reconnect {
105 lines.push(format!(
106 "- events.reconnect.enabled: {:?} ({})",
107 reconnect.enabled,
108 render_source(provenance.get(&greentic_config_types::ProvenancePath(
109 "events.reconnect".into()
110 )))
111 ));
112 }
113 }
114
115 if !warnings.is_empty() {
116 lines.push("Warnings:".into());
117 for warning in warnings {
118 lines.push(format!(" - {warning}"));
119 }
120 }
121
122 let json = serde_json::json!({
123 "config": config,
124 "provenance": provenance_as_json(provenance),
125 "warnings": warnings,
126 });
127
128 ExplainReport {
129 text: lines.join("\n"),
130 json,
131 }
132}
133
134pub fn explain_detailed(
135 config: &GreenticConfig,
136 provenance: &ProvenanceMapDetailed,
137 warnings: &[String],
138) -> ExplainReport {
139 let mut lines = Vec::new();
140 lines.push("Configuration:".to_string());
141 lines.push(format!(
142 "- schema_version: {}",
143 config.schema_version.0.as_str()
144 ));
145 lines.push(format!(
146 "- environment.env_id: {:?} ({})",
147 config.environment.env_id,
148 render_record(provenance.get(&greentic_config_types::ProvenancePath(
149 "environment.env_id".into()
150 )))
151 ));
152 lines.push(format!(
153 "- paths.state_dir: {} ({})",
154 config.paths.state_dir.display(),
155 render_record(provenance.get(&greentic_config_types::ProvenancePath(
156 "paths.state_dir".into()
157 )))
158 ));
159 if let Some(services) = &config.services {
160 if let Some(events) = &services.events {
161 lines.push(format!(
162 "- services.events.url: {} ({})",
163 events.url,
164 render_record(provenance.get(&greentic_config_types::ProvenancePath(
165 "services.events.url".into()
166 )))
167 ));
168 }
169 append_service_bindings_detailed(&mut lines, services, provenance);
170 }
171 if let Some(runtime) = &config.runtime.admin_endpoints {
172 lines.push(format!(
173 "- runtime.admin_endpoints.secrets_explain_enabled: {} ({})",
174 runtime.secrets_explain_enabled,
175 render_record(provenance.get(&greentic_config_types::ProvenancePath(
176 "runtime.admin_endpoints.secrets_explain_enabled".into()
177 )))
178 ));
179 }
180 if !warnings.is_empty() {
181 lines.push("Warnings:".into());
182 for warning in warnings {
183 lines.push(format!(" - {warning}"));
184 }
185 }
186
187 let json = serde_json::json!({
188 "config": config,
189 "provenance": provenance_as_json_detailed(provenance),
190 "warnings": warnings,
191 });
192
193 ExplainReport {
194 text: lines.join("\n"),
195 json,
196 }
197}
198
199fn render_source(source: Option<&ConfigSource>) -> String {
200 match source {
201 Some(ConfigSource::Default) => "default".into(),
202 Some(ConfigSource::User) => "user".into(),
203 Some(ConfigSource::Project) => "project".into(),
204 Some(ConfigSource::Environment) => "env".into(),
205 Some(ConfigSource::Cli) => "cli".into(),
206 None => "unknown".into(),
207 }
208}
209
210fn append_service_bindings(
211 lines: &mut Vec<String>,
212 services: &greentic_config_types::ServicesConfig,
213 provenance: &ProvenanceMap,
214) {
215 for (name, entry) in [
216 ("runner", services.runner.as_ref()),
217 ("deployer", services.deployer.as_ref()),
218 ("events_transport", services.events_transport.as_ref()),
219 ("source", services.source.as_ref()),
220 ("publish", services.publish.as_ref()),
221 ("metadata", services.metadata.as_ref()),
222 ("oauth_broker", services.oauth_broker.as_ref()),
223 ] {
224 if let Some(binding) = entry.and_then(|svc| svc.service.as_ref()) {
225 lines.push(format!(
226 "- services.{name}.service.bind_addr: {:?} ({})",
227 binding.bind_addr,
228 render_source(
229 provenance.get(&greentic_config_types::ProvenancePath(format!(
230 "services.{name}.service.bind_addr"
231 )))
232 )
233 ));
234 lines.push(format!(
235 "- services.{name}.service.port: {:?} ({})",
236 binding.port,
237 render_source(
238 provenance.get(&greentic_config_types::ProvenancePath(format!(
239 "services.{name}.service.port"
240 )))
241 )
242 ));
243 lines.push(format!(
244 "- services.{name}.service.public_base_url: {:?} ({})",
245 binding.public_base_url,
246 render_source(
247 provenance.get(&greentic_config_types::ProvenancePath(format!(
248 "services.{name}.service.public_base_url"
249 )))
250 )
251 ));
252 if let Some(metrics) = binding.metrics.as_ref() {
253 lines.push(format!(
254 "- services.{name}.service.metrics.enabled: {:?} ({})",
255 metrics.enabled,
256 render_source(
257 provenance.get(&greentic_config_types::ProvenancePath(format!(
258 "services.{name}.service.metrics.enabled"
259 )))
260 )
261 ));
262 lines.push(format!(
263 "- services.{name}.service.metrics.bind_addr: {:?} ({})",
264 metrics.bind_addr,
265 render_source(
266 provenance.get(&greentic_config_types::ProvenancePath(format!(
267 "services.{name}.service.metrics.bind_addr"
268 )))
269 )
270 ));
271 lines.push(format!(
272 "- services.{name}.service.metrics.port: {:?} ({})",
273 metrics.port,
274 render_source(
275 provenance.get(&greentic_config_types::ProvenancePath(format!(
276 "services.{name}.service.metrics.port"
277 )))
278 )
279 ));
280 lines.push(format!(
281 "- services.{name}.service.metrics.path: {:?} ({})",
282 metrics.path,
283 render_source(
284 provenance.get(&greentic_config_types::ProvenancePath(format!(
285 "services.{name}.service.metrics.path"
286 )))
287 )
288 ));
289 }
290 }
291 }
292}
293
294fn append_service_bindings_detailed(
295 lines: &mut Vec<String>,
296 services: &greentic_config_types::ServicesConfig,
297 provenance: &ProvenanceMapDetailed,
298) {
299 for (name, entry) in [
300 ("runner", services.runner.as_ref()),
301 ("deployer", services.deployer.as_ref()),
302 ("events_transport", services.events_transport.as_ref()),
303 ("source", services.source.as_ref()),
304 ("publish", services.publish.as_ref()),
305 ("metadata", services.metadata.as_ref()),
306 ("oauth_broker", services.oauth_broker.as_ref()),
307 ] {
308 if let Some(binding) = entry.and_then(|svc| svc.service.as_ref()) {
309 lines.push(format!(
310 "- services.{name}.service.bind_addr: {:?} ({})",
311 binding.bind_addr,
312 render_record(
313 provenance.get(&greentic_config_types::ProvenancePath(format!(
314 "services.{name}.service.bind_addr"
315 )))
316 )
317 ));
318 lines.push(format!(
319 "- services.{name}.service.port: {:?} ({})",
320 binding.port,
321 render_record(
322 provenance.get(&greentic_config_types::ProvenancePath(format!(
323 "services.{name}.service.port"
324 )))
325 )
326 ));
327 lines.push(format!(
328 "- services.{name}.service.public_base_url: {:?} ({})",
329 binding.public_base_url,
330 render_record(
331 provenance.get(&greentic_config_types::ProvenancePath(format!(
332 "services.{name}.service.public_base_url"
333 )))
334 )
335 ));
336 if let Some(metrics) = binding.metrics.as_ref() {
337 lines.push(format!(
338 "- services.{name}.service.metrics.enabled: {:?} ({})",
339 metrics.enabled,
340 render_record(
341 provenance.get(&greentic_config_types::ProvenancePath(format!(
342 "services.{name}.service.metrics.enabled"
343 )))
344 )
345 ));
346 lines.push(format!(
347 "- services.{name}.service.metrics.bind_addr: {:?} ({})",
348 metrics.bind_addr,
349 render_record(
350 provenance.get(&greentic_config_types::ProvenancePath(format!(
351 "services.{name}.service.metrics.bind_addr"
352 )))
353 )
354 ));
355 lines.push(format!(
356 "- services.{name}.service.metrics.port: {:?} ({})",
357 metrics.port,
358 render_record(
359 provenance.get(&greentic_config_types::ProvenancePath(format!(
360 "services.{name}.service.metrics.port"
361 )))
362 )
363 ));
364 lines.push(format!(
365 "- services.{name}.service.metrics.path: {:?} ({})",
366 metrics.path,
367 render_record(
368 provenance.get(&greentic_config_types::ProvenancePath(format!(
369 "services.{name}.service.metrics.path"
370 )))
371 )
372 ));
373 }
374 }
375 }
376}
377
378fn render_record(record: Option<&ProvenanceRecord>) -> String {
379 let Some(rec) = record else {
380 return "unknown".into();
381 };
382 let source = render_source(Some(&rec.source));
383 match rec.origin.as_deref() {
384 Some(origin) => format!("{source}@{origin}"),
385 None => source,
386 }
387}
388
389fn provenance_as_json(provenance: &ProvenanceMap) -> serde_json::Value {
390 let map: serde_json::Map<String, serde_json::Value> = provenance
391 .iter()
392 .map(|(k, v)| {
393 (
394 k.0.clone(),
395 serde_json::Value::String(render_source(Some(v))),
396 )
397 })
398 .collect();
399 serde_json::Value::Object(map)
400}
401
402fn provenance_as_json_detailed(provenance: &ProvenanceMapDetailed) -> serde_json::Value {
403 let map: serde_json::Map<String, serde_json::Value> = provenance
404 .iter()
405 .map(|(k, v)| {
406 (
407 k.0.clone(),
408 serde_json::json!({"source": render_source(Some(&v.source)), "origin": v.origin}),
409 )
410 })
411 .collect();
412 serde_json::Value::Object(map)
413}