tuwunel_admin/server/
regenerate_config.rs1use std::path::PathBuf;
2
3use tuwunel_core::{
4 Err, Result,
5 config::{RegenerateOptions, RegenerationSummary, Sources, regenerate_config as regenerate},
6 err, error,
7};
8
9use crate::admin_command;
10
11#[admin_command]
12pub(super) async fn regenerate_config(
13 &self,
14 path: PathBuf,
15 force: bool,
16 include_env: bool,
17 strip_unknown: bool,
18) -> Result {
19 if !path.is_absolute() {
20 return Err!("Configuration regeneration destination must be an absolute path.");
21 }
22
23 let sources = Sources {
24 paths: self.services.server.config_sources.paths.clone(),
25 overrides: None,
26 };
27
28 let regeneration = move || {
29 let options = RegenerateOptions {
30 output: Some(path.as_path()),
31 force,
32 include_env,
33 strip_unknown,
34 };
35
36 regenerate(&sources, options)
37 };
38
39 let task = self
40 .services
41 .server
42 .runtime()
43 .spawn_blocking(regeneration);
44
45 let summary = task
46 .await
47 .inspect_err(|error| error!(?error, "Configuration regeneration task failed"))
48 .map_err(|_| err!("Configuration regeneration failed. Consult the server log."))?
49 .inspect_err(|error| error!(?error, "Configuration regeneration failed"))
50 .map_err(|_| err!("Configuration regeneration failed. Consult the server log."))?;
51
52 let dropped = format_dropped_keys(&summary);
53 let dropped = (!dropped.is_empty())
54 .then_some(dropped.as_str())
55 .unwrap_or("none");
56
57 let warning = (summary.input_count() > 1)
58 .then(|| {
59 format!(
60 "Warning: {} configuration files were collapsed in source order.\n",
61 summary.input_count(),
62 )
63 })
64 .unwrap_or_default();
65
66 write!(
67 self,
68 "Configuration regenerated at `{}`.\n\n{warning}Configured values: {}\nResidue values: \
69 {}\nDropped startup-only migration controls (never emitted): {dropped}",
70 summary.output().display(),
71 summary.configured(),
72 summary.residue(),
73 )
74 .await
75}
76
77fn format_dropped_keys(summary: &RegenerationSummary) -> String {
78 let separator_bytes = summary
79 .dropped_keys()
80 .count()
81 .saturating_sub(1)
82 .saturating_mul(2);
83
84 let capacity = summary
85 .dropped_keys()
86 .map(str::len)
87 .sum::<usize>()
88 .saturating_add(separator_bytes);
89
90 summary
91 .dropped_keys()
92 .fold(String::with_capacity(capacity), |mut dropped, key| {
93 if !dropped.is_empty() {
94 dropped.push_str(", ");
95 }
96
97 dropped.push_str(key);
98 dropped
99 })
100}