Skip to main content

tuwunel_admin/server/
regenerate_config.rs

1use 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}