WEB Knobs in API

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-27 21:42:14 +03:00
parent d41a8c3220
commit cf0cd08387
43 changed files with 3399 additions and 196 deletions
+40
View File
@@ -256,6 +256,46 @@ fn reload_applies_hot_change_on_first_observed_snapshot() {
let _ = std::fs::remove_file(path);
}
#[tokio::test]
async fn candidate_watcher_waits_for_activation_and_reconciles_disk() {
let initial_tag = "10101010101010101010101010101010";
let disk_tag = "20202020202020202020202020202020";
let path = temp_config_path("telemt_hot_reload_activation_gate");
write_reload_config(&path, Some(initial_tag), None);
let initial = Arc::new(ProxyConfig::load(&path).unwrap());
write_reload_config(&path, Some(disk_tag), None);
let cancellation = tokio_util::sync::CancellationToken::new();
let (activation_tx, activation_rx) = watch::channel(false);
let (mut config_rx, _log_rx, watcher) = spawn_config_watcher(
path.clone(),
initial,
None,
None,
cancellation.clone(),
Some(activation_rx),
);
let watcher = tokio::spawn(watcher);
tokio::task::yield_now().await;
assert_eq!(
config_rx.borrow().general.ad_tag.as_deref(),
Some(initial_tag)
);
activation_tx.send_replace(true);
tokio::time::timeout(Duration::from_secs(2), config_rx.changed())
.await
.unwrap()
.unwrap();
assert_eq!(
config_rx.borrow_and_update().general.ad_tag.as_deref(),
Some(disk_tag)
);
cancellation.cancel();
watcher.await.unwrap();
let _ = std::fs::remove_file(path);
}
#[test]
fn reload_keeps_hot_apply_when_non_hot_fields_change() {
let initial_tag = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+41 -11
View File
@@ -213,20 +213,47 @@ pub fn spawn_config_watcher(
detected_ip_v4: Option<IpAddr>,
detected_ip_v6: Option<IpAddr>,
cancellation: tokio_util::sync::CancellationToken,
) -> (watch::Receiver<Arc<ProxyConfig>>, watch::Receiver<LogLevel>) {
mut activation: Option<watch::Receiver<bool>>,
) -> (
watch::Receiver<Arc<ProxyConfig>>,
watch::Receiver<LogLevel>,
impl std::future::Future<Output = ()> + Send + 'static,
) {
let initial_level = initial.general.log_level.clone();
let (config_tx, config_rx) = watch::channel(initial);
let (log_tx, log_rx) = watch::channel(initial_level);
let config_path = normalize_watch_path(&config_path);
let initial_loaded = ProxyConfig::load_with_metadata(&config_path).ok();
let initial_manifest = initial_loaded
.as_ref()
.map(|loaded| WatchManifest::from_source_files(&loaded.source_files))
.unwrap_or_else(|| WatchManifest::from_source_files(std::slice::from_ref(&config_path)));
let initial_snapshot_hash = initial_loaded.as_ref().map(|loaded| loaded.rendered_hash);
tokio::spawn(async move {
let task = async move {
if let Some(activation) = activation.as_mut() {
loop {
if *activation.borrow_and_update() {
break;
}
tokio::select! {
result = activation.changed() => {
if result.is_err() {
return;
}
}
_ = cancellation.cancelled() => return,
}
}
}
let initial_loaded = ProxyConfig::load_with_metadata(&config_path).ok();
let initial_manifest = initial_loaded
.as_ref()
.map(|loaded| WatchManifest::from_source_files(&loaded.source_files))
.unwrap_or_else(|| {
WatchManifest::from_source_files(std::slice::from_ref(&config_path))
});
let initial_matches_disk = initial_loaded
.as_ref()
.is_some_and(|loaded| config_equal(config_tx.borrow().as_ref(), &loaded.config));
let initial_snapshot_hash = initial_loaded
.as_ref()
.filter(|_| initial_matches_disk)
.map(|loaded| loaded.rendered_hash);
let (notify_tx, mut notify_rx) = mpsc::channel::<()>(4);
let manifest_state = Arc::new(StdRwLock::new(WatchManifest::default()));
let mut reload_state = ReloadState::new(initial_snapshot_hash);
@@ -304,6 +331,9 @@ pub fn spawn_config_watcher(
if poll_watcher.is_some() {
info!("config watcher: poll watcher active (Docker/NFS safe)");
}
if initial_loaded.is_some() && !initial_matches_disk {
let _ = notify_tx.try_send(());
}
#[cfg(unix)]
let mut sighup = {
@@ -364,7 +394,7 @@ pub fn spawn_config_watcher(
);
}
}
});
};
(config_rx, log_rx)
(config_rx, log_rx, task)
}
+5
View File
@@ -381,6 +381,11 @@ fn validate_vhosts(config: &mut ProxyConfig) -> Result<()> {
validate_decoy(vhost_idx, &vhost.decoy)?;
let mut profiles = HashSet::with_capacity(vhost.profiles.len());
for (profile_idx, profile) in vhost.profiles.iter().enumerate() {
if profile.user.is_empty() || profile.user.len() > 64 {
return config_error(&format!(
"web.vhosts[{vhost_idx}].profiles[{profile_idx}].user must contain 1..64 bytes"
));
}
if !config.access.users.contains_key(&profile.user) {
return config_error(&format!(
"web.vhosts[{vhost_idx}].profiles[{profile_idx}].user references unknown access user `{}`",
@@ -56,6 +56,17 @@ fn web_config_builds_canonical_runtime_snapshot() {
);
}
#[test]
fn web_profile_user_labels_are_bounded_for_runtime_status() {
let user = "a".repeat(65);
let invalid = WEB_CONFIG.replace("alice", &user);
assert!(
load_config_error_from_temp_toml(&invalid)
.contains("web.vhosts[0].profiles[0].user must contain 1..64 bytes")
);
}
#[test]
fn web_carriers_missing_or_false_disable_negotiation() {
let missing = load_config_from_temp_toml(WEB_CONFIG);