Monorepo for Tangled tangled.org
5

Configure Feed

Select the types of activity you want to include in your feed.

1use crate::command::{self, Spec}; 2use crate::protocol::v1; 3use anyhow::{Context, Result}; 4use serde::{Deserialize, Serialize}; 5use std::collections::HashSet; 6use std::fmt::Write as _; 7use std::fs; 8use std::io::Write as _; 9use std::os::unix::fs::PermissionsExt; 10use std::path::{Path, PathBuf}; 11use std::time::Duration; 12use tempfile::Builder; 13use tracing::{info, warn}; 14 15pub const SPINDLE_RUN_DIR: &str = "/run/spindle"; 16pub const SPINDLE_NIX_CONFIG: &str = "/run/spindle/nix.conf"; 17pub const SPINDLE_CACHE_CONFIG: &str = "/run/spindle/cache.json"; 18pub const SYSTEMCTL_EXECUTABLE: &str = "/run/current-system/sw/bin/systemctl"; 19 20// nix lives in different places depending on the guest OS (NixOS system 21// profile vs. plain /usr/local on e.g. alpine) 22pub fn nix_executable() -> &'static str { 23 static NIX: once_cell::sync::Lazy<&'static str> = once_cell::sync::Lazy::new(|| { 24 let paths = [ 25 "/run/current-system/sw/bin/nix", 26 "/usr/local/bin/nix", 27 "/usr/bin/nix", 28 ]; 29 for candidate in paths { 30 if Path::new(candidate).exists() { 31 return candidate; 32 } 33 } 34 "/run/current-system/sw/bin/nix" 35 }); 36 &NIX 37} 38 39#[derive(Clone, Debug, Default, Deserialize, Serialize)] 40pub struct RuntimeCacheConfig { 41 pub read_urls: Vec<String>, 42 pub trusted_public_keys: Vec<String>, 43} 44 45// configures nix daemon with the configuration passed from host 46pub async fn configure(init: &v1::Init, read_proxy_url: &str) -> Result<RuntimeCacheConfig> { 47 let read_urls = vec![read_proxy_url.to_owned()]; 48 let cfg = RuntimeCacheConfig { 49 read_urls, 50 trusted_public_keys: clean_strings(&init.cache_trusted_public_keys), 51 }; 52 53 if cfg.read_urls.is_empty() && cfg.trusted_public_keys.is_empty() { 54 remove_if_exists(SPINDLE_NIX_CONFIG)?; 55 remove_if_exists(SPINDLE_CACHE_CONFIG)?; 56 return Ok(cfg); 57 } 58 59 fs::create_dir_all(SPINDLE_RUN_DIR).with_context(|| format!("create {SPINDLE_RUN_DIR}"))?; 60 61 let cache_json = serde_json::to_vec_pretty(&cfg)?; 62 write_file_atomic(SPINDLE_CACHE_CONFIG, &cache_json, 0o600)?; 63 64 let mut nix_conf = String::new(); 65 if !cfg.read_urls.is_empty() { 66 writeln!( 67 &mut nix_conf, 68 "extra-substituters = {}", 69 cfg.read_urls.join(" ") 70 ) 71 .unwrap(); 72 } 73 if !cfg.trusted_public_keys.is_empty() { 74 writeln!( 75 &mut nix_conf, 76 "extra-trusted-public-keys = {}", 77 cfg.trusted_public_keys.join(" ") 78 ) 79 .unwrap(); 80 } 81 82 if nix_conf.is_empty() { 83 remove_if_exists(SPINDLE_NIX_CONFIG)?; 84 return Ok(cfg); 85 } 86 87 write_file_atomic(SPINDLE_NIX_CONFIG, nix_conf.as_bytes(), 0o644)?; 88 restart_nix_daemon().await; 89 info!( 90 read_urls = ?cfg.read_urls, 91 trusted_public_keys = cfg.trusted_public_keys.len(), 92 "configured nix cache" 93 ); 94 95 Ok(cfg) 96} 97 98pub fn clean_strings(values: &[String]) -> Vec<String> { 99 let mut seen = HashSet::new(); 100 let mut out = Vec::with_capacity(values.len()); 101 102 for value in values { 103 let value = value.trim(); 104 if value.is_empty() || !seen.insert(value.to_owned()) { 105 continue; 106 } 107 out.push(value.to_owned()); 108 } 109 110 out 111} 112 113pub fn clean_store_paths(values: &[String]) -> Vec<String> { 114 clean_strings(values) 115 .into_iter() 116 .filter(|value| value.starts_with("/nix/store/")) 117 .collect() 118} 119 120pub async fn nix_version() -> String { 121 let spec = Spec::new(nix_executable()) 122 .arg("--version") 123 .timeout(Duration::from_secs(1)); 124 125 let Ok(output) = command::run_capture(spec).await else { 126 return String::new(); 127 }; 128 if !output.success() { 129 return String::new(); 130 } 131 132 String::from_utf8_lossy(&output.stdout).trim().to_owned() 133} 134 135fn write_file_atomic(path: impl AsRef<Path>, data: &[u8], mode: u32) -> Result<()> { 136 let path = path.as_ref(); 137 let dir = path.parent().unwrap_or_else(|| Path::new(".")); 138 let prefix = path 139 .file_name() 140 .and_then(|name| name.to_str()) 141 .map(|name| format!(".{name}.tmp-")) 142 .unwrap_or_else(|| ".tmp-".to_owned()); 143 144 let mut tmp = Builder::new() 145 .prefix(&prefix) 146 .permissions(fs::Permissions::from_mode(mode)) 147 .tempfile_in(dir) 148 .with_context(|| format!("create temp file for {}", path.display()))?; 149 150 // no separate sync here because we don't need to be crash-safe (this is an 151 // ephemeral vm) only atomicity is needed 152 tmp.write_all(data) 153 .with_context(|| format!("write temp file for {}", path.display()))?; 154 tmp.persist(path) 155 .map(|_| ()) 156 .map_err(|err| err.error) 157 .with_context(|| format!("install {}", path.display())) 158} 159 160const NIX_DAEMON_SOCKET: &str = "/nix/var/nix/daemon-socket/socket"; 161 162async fn restart_nix_daemon() { 163 let systemd = Path::new(SYSTEMCTL_EXECUTABLE).exists(); 164 let spec = if systemd { 165 Spec::new(SYSTEMCTL_EXECUTABLE) 166 .args(["try-restart", "nix-daemon.service"]) 167 .timeout(Duration::from_secs(5)) 168 } else { 169 // on non-systemd we can just kill the daemon and it should restart 170 Spec::new("pkill") 171 .args(["-f", "nix-daemon"]) 172 .timeout(Duration::from_secs(5)) 173 }; 174 175 match command::run_capture(spec).await { 176 Ok(output) if output.success() => { 177 if !systemd { 178 // init has to respawn the daemon before any step needs it 179 wait_for_nix_daemon_socket(Duration::from_secs(5)).await; 180 } 181 } 182 // pkill exits 1 when nothing matched, ie. no daemon to restart 183 Ok(output) if !systemd && output.exit.exit_code == 1 => { 184 info!("no nix-daemon running, skipping restart") 185 } 186 Ok(output) => warn!( 187 exit_code = output.exit.exit_code, 188 error = ?output.exit.error, 189 output = %output.combined_lossy(), 190 "nix-daemon restart failed" 191 ), 192 Err(error) => warn!(%error, "nix-daemon restart failed"), 193 } 194} 195 196async fn wait_for_nix_daemon_socket(timeout: Duration) { 197 let deadline = tokio::time::Instant::now() + timeout; 198 loop { 199 if tokio::net::UnixStream::connect(NIX_DAEMON_SOCKET) 200 .await 201 .is_ok() 202 { 203 return; 204 } 205 if tokio::time::Instant::now() >= deadline { 206 warn!( 207 socket = NIX_DAEMON_SOCKET, 208 "nix-daemon did not come back after restart" 209 ); 210 return; 211 } 212 tokio::time::sleep(Duration::from_millis(100)).await; 213 } 214} 215 216fn remove_if_exists(path: impl AsRef<Path>) -> Result<()> { 217 let path: PathBuf = path.as_ref().to_owned(); 218 match fs::remove_file(&path) { 219 Ok(()) => Ok(()), 220 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), 221 Err(error) => Err(error).with_context(|| format!("remove {}", path.display())), 222 } 223}