Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
9.4 kB · 264 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265//! validated, atomic generation activation (packet §16).
use std::path::Path;use std::sync::Arc;
use anyhow::Context as _;
use crate::config::ValidatedConfig;use crate::manifest::GenerationManifest;
fn rollback_active_conf(previous: Option<&[u8]>, active_conf: &Path) { if let Some(bytes) = previous { let tmp = active_conf.with_extension(format!("rollback.tmp.{}", std::process::id())); if std::fs::write(&tmp, bytes).is_ok() { let _ = std::fs::rename(&tmp, active_conf); } } else { let _ = std::fs::remove_file(active_conf); }}
#[derive(Debug, Clone)]pub struct ActivationResult { pub generation: String, pub changed_repos: Vec<String>,}
pub async fn activate(config: Arc<ValidatedConfig>, generation: &str) -> anyhow::Result<ActivationResult> { let _output_lock = crate::output::lock(&config.output_root)?; anyhow::ensure!( generation.len() == 64 && generation.bytes().all(|b| b.is_ascii_hexdigit()), "generation must be a 64-character hexadecimal id" );
let generation_dir = config.output_root.join("generations").join(generation); anyhow::ensure!( generation_dir.exists(), "generation directory does not exist: {}", generation_dir.display() );
let manifest = GenerationManifest::load(&generation_dir) .with_context(|| format!("load manifest from {}", generation_dir.display()))?;
anyhow::ensure!( manifest.generation_id == generation, "manifest generation_id ({}) does not match requested generation ({generation})", manifest.generation_id );
let computed_id = manifest.compute_id(); anyhow::ensure!( computed_id == manifest.generation_id, "manifest generation_id ({}) does not match computed semantic hash ({computed_id})", manifest.generation_id );
// Verify referenced CAS objects exist (routes only; git_export_hash is // an input fingerprint, its bytes live in the generation www/_git tree) let cas = crate::cas::Cas::open(config.output_root.join("cas")); for response in manifest.routes.values() { let digest = crate::cas::Digest::from_hex(&response.body_hash) .with_context(|| format!("invalid CAS hex hash: {}", response.body_hash))?; anyhow::ensure!( cas.has(&digest), "missing referenced CAS object: {}", response.body_hash ); } for (index, _) in manifest .repos .iter() .enumerate() .filter(|(_, repo)| repo.git_export_hash.is_some()) { let export = generation_dir .join("www/_git") .join(format!("r_{index:03}")); anyhow::ensure!( export.exists(), "missing git export for r_{index:03} in generation" ); }
let responses_dir = generation_dir.join("www/_responses"); for (route_key, response) in &manifest.routes { let hex = &response.body_hash; let sharded = (hex.len() >= 4).then(|| responses_dir.join(&hex[..2]).join(&hex[2..])); let flat = responses_dir.join(hex); let exists = sharded.as_ref().is_some_and(|p| p.exists()) || flat.exists(); anyhow::ensure!( exists, "missing www response file for route '{route_key}' (body hash: {hex})" ); }
let nginx_conf = generation_dir.join("nginx/knot-static.conf"); anyhow::ensure!( nginx_conf.is_file(), "generation is missing its nginx configuration; regenerate it" );
let active_conf = config.output_root.join("active.conf"); let previous_active = active_conf .exists() .then(|| std::fs::read(&active_conf).context("read existing active.conf")) .transpose()?;
let previous_manifest = std::fs::read_link(config.output_root.join("current")) .ok() .and_then(|link| { let id = link.file_name()?.to_str()?; GenerationManifest::load(&config.output_root.join("generations").join(id)).ok() });
let nginx_conf_canon = std::fs::canonicalize(&nginx_conf).unwrap_or(nginx_conf); let pid_path = config.output_root.join("runtime/nginx.pid"); let new_conf_content = format!( "pid {};\ninclude {};\n", pid_path.display(), nginx_conf_canon.display() ); let tmp_active = config .output_root .join(format!("active.conf.tmp.{}", std::process::id()));
std::fs::write(&tmp_active, &new_conf_content) .with_context(|| format!("write tmp active.conf: {}", tmp_active.display()))?; std::fs::rename(&tmp_active, &active_conf) .with_context(|| format!("rename to active.conf: {}", active_conf.display()))?;
let test_res = std::process::Command::new(&config.activate.nginx_binary) .arg("-t") .arg("-c") .arg(&active_conf) .output();
match test_res { Ok(output) if !output.status.success() => { rollback_active_conf(previous_active.as_deref(), &active_conf); let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); anyhow::bail!( "nginx configuration test failed (-t):\nstderr: {}\nstdout: {}", stderr.trim(), stdout.trim() ); } Err(err) => { rollback_active_conf(previous_active.as_deref(), &active_conf); anyhow::bail!( "failed to execute nginx binary '{}': {err}", config.activate.nginx_binary ); } _ => {} }
if let Some(reload_cmd) = &config.activate.reload_command { let reload_res = std::process::Command::new("sh") .arg("-c") .arg(reload_cmd) .output();
match reload_res { Ok(output) if !output.status.success() => { rollback_active_conf(previous_active.as_deref(), &active_conf); let stderr = String::from_utf8_lossy(&output.stderr); let stdout = String::from_utf8_lossy(&output.stdout); anyhow::bail!( "nginx reload command failed:\nstderr: {}\nstdout: {}", stderr.trim(), stdout.trim() ); } Err(err) => { rollback_active_conf(previous_active.as_deref(), &active_conf); anyhow::bail!("failed to execute reload command '{reload_cmd}': {err}"); } _ => {} } }
let current_link = config.output_root.join("current"); let tmp_link = config .output_root .join(format!("current.tmp.{}", std::process::id())); let _ = std::fs::remove_file(&tmp_link);
let rel_target = Path::new("generations").join(generation); #[cfg(unix)] { std::os::unix::fs::symlink(&rel_target, &tmp_link).with_context(|| { format!("symlink {} -> {}", tmp_link.display(), rel_target.display()) })?; std::fs::rename(&tmp_link, ¤t_link) .with_context(|| format!("rename symlink to {}", current_link.display()))?; }
// fingerprint changed → repos whose records and mirrors need updating let previous_fingerprints: std::collections::HashMap<&str, &str> = previous_manifest .as_ref() .map(|m| { m.repos .iter() .map(|repo| (repo.repo_did.as_str(), repo.fingerprint.as_str())) .collect() }) .unwrap_or_default(); let changed_repos: Vec<String> = manifest .repos .iter() .filter(|repo| { previous_fingerprints.get(repo.repo_did.as_str()) != Some(&repo.fingerprint.as_str()) }) .map(|repo| repo.repo_did.clone()) .collect();
// GC is mark-and-sweep across retained manifests; run offline via `knot-static gc`, not on every activation.
eprintln!( "activated generation {} (repos: {}, routes: {})", generation, manifest.repos.len(), manifest.routes.len() );
Ok(ActivationResult { generation: generation.to_string(), changed_repos, })}
/// post-activation network tasks: mirror resync pings + PDS record publishing./// these run concurrently and independently of the generation switch.pub async fn post_activate(config: Arc<ValidatedConfig>, changed_repos: &[String]) { if changed_repos.is_empty() { return; }
let repo_dids: Vec<knot_types::RepoDid> = changed_repos .iter() .filter_map(|r| knot_types::RepoDid::new(r.clone()).ok()) .collect();
let mirror_fut = async { let triggers = crate::mirror::triggers_from_config(&config); crate::mirror::trigger_all(&triggers, &repo_dids).await; };
let publish_fut = async { if !config.publish.is_empty() { crate::pds::publish_all(config.clone(), changed_repos).await; } };
tokio::join!(mirror_fut, publish_fut);}
pub async fn run(config: Arc<ValidatedConfig>, generation: &str) -> anyhow::Result<()> { let result = activate(config.clone(), generation).await?; post_activate(config, &result.changed_repos).await; Ok(())}