nginx -t skips the git read maps an activation carries over unchanged
Signed-off-by: dawn <90008@klbr.net>
1177 unmodified linesExpand all117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511177 unmodified linesExpand all profile("two"); assert!(tried(), "a different store tries again"); assert_eq!(active_generation(&env.output), first_id);}
#[test]fn nginx_checks_the_git_read_maps_only_when_they_changed() { let env = setup_test_env(); let tested = env.output.join("nginx-tested"); let nginx = env.output.join("nginx-recorder"); std::fs::create_dir_all(&env.output).unwrap(); std::fs::write( &nginx, format!("#!/bin/sh\necho \"$3\" >> {}\n", tested.display()), ) .unwrap(); std::fs::set_permissions(&nginx, std::os::unix::fs::PermissionsExt::from_mode(0o755)).unwrap(); let config = std::fs::read_to_string(&env.config).unwrap(); std::fs::write( &env.config, config.replace( "nginx_binary = \"true\"", &format!("nginx_binary = \"{}\"", nginx.display()), ), ) .unwrap(); let last_tested = || { let log = std::fs::read_to_string(&tested).unwrap(); log.lines().last().unwrap().to_string() };
assert!(knot_static(&env.config, ["generate"]).status.success()); let first = get_only_generation_dir(&env.output); let first_id = first.file_name().unwrap().to_str().unwrap().to_string(); assert!( knot_static(&env.config, ["activate", &first_id]) .status .success() ); assert!(last_tested().ends_with("/active.conf"), "{}", last_tested());
std::fs::write( env.output.join("owner-handles.json"), serde_json::to_vec(&serde_json::json!({ "version": 1, "entries": { OWNER: { "handle": "ptr.pet", "verified_at_unix": 1_700_000_000u64, "profile": { "description": "checked" }, } } })) .unwrap(), ) .unwrap(); let _ = knot_static(&env.config, ["post-activate"]); assert_ne!(active_generation(&env.output), first_id); assert!( last_tested().ends_with("/runtime/nginx-check.conf"), "the same git read maps aren't tested again: {}", last_tested() );
assert!( knot_static(&env.config, ["activate", &first_id, "--rollback"]) .status .success() ); assert!( last_tested().ends_with("/active.conf"), "a rollback tests it all" );}24 unmodified lines2526272829303132333435363738394041424344454647484950172 unmodified linesExpand all223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256213257258259260261273 unmodified linesExpand all24 unmodified lines ); }}
fn carries_git_maps( config: &ValidatedConfig, previous: Option<&str>, generation_dir: &Path,) -> bool { let Some(previous) = previous else { return false; }; let maps = |dir: &Path| std::fs::read(dir.join("nginx/maps").join(crate::git_xrpc::MAPS_FILE)).ok(); let previous_dir = config.output_root.join("generations").join(previous); generation_dir .join("nginx") .join(crate::nginx::CHECK_FILE) .is_file() && maps(&previous_dir).is_some_and(|before| maps(generation_dir) == Some(before))}
fn abort_activation( config: &ValidatedConfig, previous_active: Option<&[u8]>, active_conf: &Path,172 unmodified linesExpand all
write_active_conf(&active_conf, new_conf_content.as_bytes()) .with_context(|| format!("write active.conf: {}", active_conf.display()))?;
// nginx refuses a bad conf on reload and keeps serving the old generation, so -t checks it // first. git read maps byte-identical to the active generation's passed it then, and their // fragments are immutable, so only the rest is checked again. let t_test = Instant::now(); let carried = !options.rollback && carries_git_maps( &config, previous_generation_name.as_deref(), &generation_dir, ); let checked = if carried { let check_conf = config.output_root.join("runtime/nginx-check.conf"); let check = Block::file() .directive("pid", [pid_path.display()]) .directive( "include", [generation_dir .join("nginx") .join(crate::nginx::CHECK_FILE) .display()], ) .to_string(); crate::cas::atomic_write(&check_conf, check.as_bytes())?; check_conf } else { active_conf.clone() }; let test_res = std::process::Command::new(&config.activate.nginx_binary) .arg("-t") .arg("-c") .arg(&active_conf) .arg(&checked) .output();
match test_res { Ok(output) if !output.status.success() => {273 unmodified linesExpand all524 unmodified linesExpand all525526527528529530530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586524 unmodified linesExpand all ) .block(server), );
let conf = conf.to_string(); let conf_path = nginx_dir.join("knot-static.conf"); crate::cas::atomic_write(&conf_path, conf.to_string().as_bytes()) crate::cas::atomic_write(&conf_path, conf.as_bytes()) .with_context(|| format!("write {}", conf_path.display()))?; emit_check(&conf, &nginx_dir, gen_path)}
/// what `nginx -t` checks when the git read maps are the ones it already accepted: the same conf/// with those maps' heads and none of their fragments, which are most of the test's timefn emit_check(conf: &str, nginx_dir: &Path, gen_path: &str) -> anyhow::Result<()> { let git_maps = std::fs::read_to_string(nginx_dir.join("maps").join(crate::git_xrpc::MAPS_FILE))?; let heads: String = git_maps .lines() .filter(|line| !line.trim_start().starts_with("include ")) .map(|line| format!("{line}\n")) .collect(); let check_dir = nginx_dir.join(CHECK_DIR); std::fs::create_dir_all(&check_dir)?; crate::cas::atomic_write( &check_dir.join(crate::git_xrpc::MAPS_FILE), heads.as_bytes(), )?; let mut maps: Vec<String> = std::fs::read_dir(nginx_dir.join("maps"))? .map(|entry| Ok(entry?.file_name().to_string_lossy().into_owned())) .collect::<std::io::Result<_>>()?; maps.retain(|name| name != crate::git_xrpc::MAPS_FILE); maps.sort(); let glob = format!("include {gen_path}/nginx/maps/*.conf;"); let mut check = String::with_capacity(conf.len()); let mut found = false; for line in conf.lines() { if line.trim() == glob { found = true; let indent = &line[..line.len() - line.trim_start().len()]; for name in &maps { check += &format!("{indent}include {gen_path}/nginx/maps/{name};\n"); } check += &format!( "{indent}include {gen_path}/nginx/{CHECK_DIR}/{};\n", crate::git_xrpc::MAPS_FILE ); } else { check += line; check += "\n"; } } anyhow::ensure!( found, "the conf has no maps include to check without the git read maps" ); crate::cas::atomic_write(&nginx_dir.join(CHECK_FILE), check.as_bytes()) .context("write the conf nginx -t checks")?; Ok(())}
pub(crate) const CHECK_FILE: &str = "knot-static.check.conf";const CHECK_DIR: &str = "check";349 unmodified linesExpand all350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410349 unmodified linesExpand all ] { assert_eq!(get(extra), (404, Some("NotFound".into())), "{extra}"); }}
#[test]#[ignore = "requires nginx; set NGINX_BINARY"]fn the_check_conf_tests_everything_but_the_git_read_fragments() { let knot = knot(&[("README.md", "readme\n")], &[]); let nginx_binary = PathBuf::from(std::env::var_os("NGINX_BINARY").unwrap()); let nginx = knot.generation.join("nginx"); // start_nginx gave the full conf its pid and log, the check conf gets them here let check = nginx.join("tested.conf"); std::fs::write( &check, format!( "pid {};\nerror_log {};\ninclude {};\n", knot.generation.join("tested.pid").display(), knot.generation.join("tested.log").display(), nginx.join(super::super::CHECK_FILE).display() ), ) .unwrap(); let full = nginx.join("knot-static.conf"); let tested = |path: &Path| { let output = std::process::Command::new(&nginx_binary) .arg("-e") .arg(knot.generation.join("tested.log")) .args(["-t", "-c"]) .arg(&path) .output() .unwrap(); output.status.success() }; assert!(tested(&full)); assert!(tested(&check));
let broken = |file: &Path| { let mut permissions = std::fs::metadata(file).unwrap().permissions(); permissions.set_readonly(false); std::fs::set_permissions(file, permissions).unwrap(); let text = std::fs::read_to_string(file).unwrap(); std::fs::write(file, text.clone() + " \"half a row\n").unwrap(); text }; let fragment = std::fs::read_dir(&knot.sealed.maps) .unwrap() .map(|entry| entry.unwrap().path()) .find(|path| path.extension().is_some_and(|ext| ext == "conf")) .unwrap(); let kept = broken(&fragment); assert!( !tested(&full), "a broken fragment fails the full test" ); assert!(tested(&check), "and isn't read by the check"); std::fs::write(&fragment, kept).unwrap();
broken(&nginx.join("maps").join(crate::xrpc::social::MAPS_FILE)); assert!(!tested(&check), "every other map is still checked");}