This commit is contained in:
Mateusz Gruszczyński committed 2026-10-09 13:11:33 +02:00
1 parent f87eec7287
commit dd8c195600
38 files changed
+5491 -4682

No files matched your search

+20 -16
View File
@@ -48,24 +48,28 @@ pub fn compile_with_active(
config.compression.validate()?;
config.server_header.validate()?;
let forwarding = config.forwarding.compile()?;
ensure!(
config.web_services.len() <= 128,
"too many Web Services (max 128)"
);
ensure!(config.php_fpm_profiles.len() <= 128, "too many PHP-FPM profiles (max 128)");
let mut php_profiles = HashMap::new();
for profile in &mut config.php_fpm_profiles {
profile.validate().with_context(|| format!("PHP-FPM profile {}", profile.id))?;
ensure!(php_profiles.insert(profile.id.clone(), profile.php.clone()).is_none(),
"duplicate PHP-FPM profile id");
}
ensure!(config.web_services.len() <= 128, "too many Web Services (max 128)");
let mut web_services = HashMap::new();
for service in &mut config.web_services {
service
.validate()
.with_context(|| format!("Web Service {}", service.id))?;
let compiled = Arc::new(
service
.compile()
.with_context(|| format!("Web Service {}", service.id))?,
);
ensure!(
web_services.insert(service.id.clone(), compiled).is_none(),
"duplicate Web Service id"
);
service.validate().with_context(|| format!("Web Service {}", service.id))?;
// A profile changes effective runtime settings, not the saved standalone
// PHP configuration. Detaching a profile restores local settings exactly.
let mut effective = service.clone();
if let Some(id) = &service.php_profile_id {
effective.php = php_profiles.get(id).with_context(|| {
format!("Web Service {} references unknown PHP-FPM profile {}", service.id, id)
})?.clone();
effective.validate().with_context(|| format!("Web Service {} PHP-FPM profile {}", service.id, id))?;
}
let compiled = Arc::new(effective.compile().with_context(|| format!("Web Service {}", service.id))?);
ensure!(web_services.insert(service.id.clone(), compiled).is_none(), "duplicate Web Service id");
}
ensure!(config.vhosts.len() <= 64, "too many vhosts (max 64)");
let mut vhost_ids = HashSet::new();
+2
View File
@@ -67,6 +67,8 @@ pub struct Config {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub web_services: Vec<crate::web_server::Service>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub php_fpm_profiles: Vec<crate::web_server::PhpFpmProfile>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub vhosts: Vec<crate::vhost::Vhost>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub host_policies: Vec<HostPolicy>,
+1
View File
@@ -1,4 +1,5 @@
mod tests {
include!("tests/part_01.rs");
include!("tests/part_02.rs");
include!("tests/part_03.rs");
}
+1
View File
@@ -480,3 +480,4 @@ fn backend_diagnostic_flags_compile_to_frontend() {
assert!(frontend.backend_header);
assert!(frontend.backend_timing_header);
}
+73
View File
@@ -0,0 +1,73 @@
#[test]
fn shared_php_fpm_profile_is_reused_and_detach_restores_local_configuration() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join("index.php"), b"<?php echo 'ok';").unwrap();
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
config.php_fpm_profiles.push(crate::web_server::PhpFpmProfile {
id: "php-default".into(),
name: "Default PHP pool".into(),
php: crate::web_server::Php {
enabled: true,
endpoint: "tcp:127.0.0.1:9000".into(),
..Default::default()
},
});
let service = crate::web_server::Service {
id: "site-one".into(),
name: "Site One".into(),
document_root: tmp.path().to_path_buf(),
php_profile_id: Some("php-default".into()),
// Standalone settings remain distinct and disabled.
php: crate::web_server::Php::default(),
..Default::default()
};
let mut second = service.clone();
second.id = "site-two".into();
second.name = "Site Two".into();
config.web_services.extend([service, second]);
config.vhosts[0].source = crate::vhost::Source::WebServer {
web_service_id: "site-one".into(),
};
config.vhosts[0].routes.clear();
config.vhosts[0].backends.clear();
let snapshot = compile(config, 1, &policy()).unwrap();
for id in ["site-one", "site-two"] {
let compiled = snapshot.web_php_services.get(id).unwrap();
assert!(compiled.php.is_some(), "{id} must use the shared FPM profile");
}
assert_eq!(snapshot.envelope.config.php_fpm_profiles.len(), 1);
assert!(!snapshot.envelope.config.web_services[0].php.enabled);
assert!(!snapshot.envelope.config.web_services[1].php.enabled);
let serialized = serde_json::to_string(&snapshot.envelope.config).unwrap();
let mut detached: Config = serde_json::from_str(&serialized).unwrap();
detached.web_services[0].php_profile_id = None;
let snapshot = compile(detached, 2, &policy()).unwrap();
assert!(snapshot.web_php_services.get("site-one").unwrap().php.is_none());
assert!(snapshot.web_php_services.get("site-two").unwrap().php.is_some());
}
#[test]
fn shared_php_fpm_profile_reference_and_duplicate_ids_are_rejected() {
let tmp = tempfile::TempDir::new().unwrap();
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
config.web_services.push(crate::web_server::Service {
id: "site".into(),
name: "Site".into(),
document_root: tmp.path().to_path_buf(),
php_profile_id: Some("missing".into()),
..Default::default()
});
let err = compile(config.clone(), 1, &policy()).unwrap_err().to_string();
assert!(err.contains("unknown PHP-FPM profile"), "{err}");
config.web_services[0].php_profile_id = None;
let profile = crate::web_server::PhpFpmProfile {
id: "duplicate".into(),
name: "Duplicate".into(),
php: Default::default(),
};
config.php_fpm_profiles = vec![profile.clone(), profile];
let err = compile(config, 1, &policy()).unwrap_err().to_string();
assert!(err.contains("duplicate PHP-FPM profile"), "{err}");
}