greentic_config/
explain.rs

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}