difftreelog
fix secret generation
in: trunk
3 files changed
crates/fleet-base/src/host.rsdiffbeforeafterboth1use std::{2 cell::OnceCell,3 ffi::{OsStr, OsString},4 fmt::Display,5 io::Write,6 ops::Deref,7 path::PathBuf,8 str::FromStr,9 sync::{Arc, Mutex, MutexGuard, OnceLock},10};1112use anyhow::{anyhow, bail, ensure, Context, Result};13use fleet_shared::SecretData;14use nix_eval::{nix_go, nix_go_json, util::assert_warn, Value};15use openssh::SessionBuilder;16use serde::de::DeserializeOwned;17use tempfile::NamedTempFile;1819use crate::{20 command::MyCommand,21 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},22};2324pub struct FleetConfigInternals {25 pub local_system: String,26 pub directory: PathBuf,27 pub data: Mutex<FleetData>,28 pub nix_args: Vec<OsString>,29 /// fleet_config.config30 pub config_field: Value,31 // TODO: Remove with connectivity refactor32 pub localhost: String,3334 /// import nixpkgs {system = local};35 pub default_pkgs: Value,36}3738// TODO: Make field not pub39#[derive(Clone)]40pub struct Config(pub Arc<FleetConfigInternals>);4142impl Deref for Config {43 type Target = FleetConfigInternals;4445 fn deref(&self) -> &Self::Target {46 &self.047 }48}4950#[derive(Clone, Copy, Debug)]51pub enum EscalationStrategy {52 Sudo,53 Run0,54 Su,55}5657pub struct ConfigHost {58 config: Config,59 pub name: String,60 groups: OnceCell<Vec<String>>,6162 pub host_config: Option<Value>,63 pub nixos_config: OnceCell<Value>,6465 // TODO: Move command helpers away with connectivity refactor66 pub local: bool,67 pub session: OnceLock<Arc<openssh::Session>>,68}69// TODO: Move command helpers away with connectivity refactor70impl ConfigHost {71 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {72 // Prefer sudo, as run0 has some gotchas with polkit73 // and too many repeating prompts.74 if (self.find_in_path("sudo").await).is_ok() {75 return Ok(EscalationStrategy::Sudo);76 }77 if (self.find_in_path("run0").await).is_ok() {78 return Ok(EscalationStrategy::Run0);79 }80 Ok(EscalationStrategy::Su)81 }82 async fn open_session(&self) -> Result<Arc<openssh::Session>> {83 assert!(!self.local, "do not open ssh connection to local session");84 // FIXME: TOCTOU85 if let Some(session) = &self.session.get() {86 return Ok((*session).clone());87 };88 let session = SessionBuilder::default();89 let session = session90 .connect(&self.name)91 .await92 .map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;93 let session = Arc::new(session);94 self.session.set(session.clone()).expect("TOCTOU happened");95 Ok(session)96 }97 pub async fn mktemp_dir(&self) -> Result<String> {98 let mut cmd = self.cmd("mktemp").await?;99 cmd.arg("-d");100 let path = cmd.run_string().await?;101 Ok(path.trim_end().to_owned())102 }103 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {104 let mut cmd = self.cmd("cat").await?;105 cmd.arg(path);106 cmd.run_bytes().await107 }108 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {109 let mut cmd = self.cmd("cat").await?;110 cmd.arg(path);111 cmd.run_string().await112 }113 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {114 let mut cmd = self.cmd("ls").await?;115 cmd.arg(path);116 let out = cmd.run_string().await?;117 let mut lines = out.split('\n');118 if let Some(last) = lines.next_back() {119 ensure!(last.is_empty(), "output of ls should end with newline");120 }121 Ok(lines.map(ToOwned::to_owned).collect())122 }123 #[allow(dead_code)]124 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {125 let text = self.read_file_text(path).await?;126 Ok(serde_json::from_str(&text)?)127 }128 pub async fn read_env(&self, env: &str) -> Result<String> {129 let mut cmd = self.cmd("printenv").await?;130 cmd.arg(env);131 cmd.run_string().await132 }133 pub async fn find_in_path(&self, command: &str) -> Result<String> {134 // // `which` is not a part of coreutils, and it might not exist on machine.135 // let path = self.read_env("PATH").await?;136 // // Assuming delimiter is :, we don't work with windows host, this check will be much137 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)138 // for ele in path.split(':') {139 // let test_path = format!("{ele}/{cmd}");140 // test -x etc141 // }142 // let mut cmd = self.cmd("printenv").await?;143 // cmd.arg(env);144 // Ok(cmd.run_string().await?)145 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.146 let mut cmd = self147 .cmd_escalation(148 // Not used149 EscalationStrategy::Su,150 "which",151 )152 .await?;153 cmd.arg(command);154 cmd.run_string().await155 }156 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>157 where158 <D as FromStr>::Err: Display,159 {160 let text = self.read_file_text(path).await?;161 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))162 }163 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {164 self.cmd_escalation(self.escalation_strategy().await?, cmd)165 .await166 }167 pub async fn cmd_escalation(168 &self,169 escalation: EscalationStrategy,170 cmd: impl AsRef<OsStr>,171 ) -> Result<MyCommand> {172 if self.local {173 Ok(MyCommand::new(escalation, cmd))174 } else {175 let session = self.open_session().await?;176 Ok(MyCommand::new_on(escalation, cmd, session))177 }178 }179180 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {181 ensure!(data.encrypted, "secret is not encrypted");182 let mut cmd = self.cmd("fleet-install-secrets").await?;183 cmd.arg("decrypt").eqarg("--secret", data.to_string());184 let encoded = cmd185 .sudo()186 .run_string()187 .await188 .context("failed to call remote host for decrypt")?;189 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;190 ensure!(!data.encrypted, "secret came out encrypted");191 Ok(data.data)192 }193 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {194 ensure!(data.encrypted, "secret is not encrypted");195 let mut cmd = self.cmd("fleet-install-secrets").await?;196 cmd.arg("reencrypt").eqarg("--secret", data.to_string());197 for target in targets {198 let key = self.config.key(&target).await?;199 cmd.eqarg("--targets", key);200 }201 let encoded = cmd202 .sudo()203 .run_string()204 .await205 .context("failed to call remote host for decrypt")?;206 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;207 ensure!(data.encrypted, "secret came out not encrypted");208 Ok(data)209 }210 /// Returns path for futureproofing, as path might change i.e on conversion to CA211 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {212 if self.local {213 // Path is located locally, thus already trusted.214 return Ok(path.to_owned());215 }216 let mut nix = MyCommand::new(217 // Not used218 EscalationStrategy::Su,219 "nix",220 );221 nix.arg("copy")222 .arg("--substitute-on-destination")223 .comparg("--to", format!("ssh-ng://{}", self.name))224 .arg(path);225 nix.run_nix().await.context("nix copy")?;226 Ok(path.to_owned())227 }228 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {229 let mut cmd = self.cmd("systemctl").await?;230 cmd.arg("stop").arg(name);231 cmd.sudo().run().await232 }233 pub async fn systemctl_start(&self, name: &str) -> Result<()> {234 let mut cmd = self.cmd("systemctl").await?;235 cmd.arg("start").arg(name);236 cmd.sudo().run().await237 }238239 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {240 let mut cmd = self.cmd("rm").await?;241 cmd.arg("-f").arg(path);242 if sudo {243 cmd = cmd.sudo()244 }245 cmd.run().await246 }247}248impl ConfigHost {249 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,250 // assuming getting tags always returns the same value.251 pub async fn tags(&self) -> Result<Vec<String>> {252 if let Some(v) = self.groups.get() {253 return Ok(v.clone());254 }255 let Some(host_config) = &self.host_config else {256 return Ok(vec![]);257 };258 let tags: Vec<String> = nix_go_json!(host_config.tags);259260 let _ = self.groups.set(tags.clone());261262 Ok(tags)263 }264 pub async fn nixos_config(&self) -> Result<Value> {265 if let Some(v) = self.nixos_config.get() {266 return Ok(v.clone());267 }268 let Some(host_config) = &self.host_config else {269 bail!("local host has no nixos_config");270 };271 let nixos_config = nix_go!(host_config.nixos.config);272 assert_warn("nixos config evaluation", &nixos_config).await?;273274 let _ = self.nixos_config.set(nixos_config.clone());275276 Ok(nixos_config)277 }278279 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {280 let nixos = self.nixos_config().await?;281 let secrets = nix_go!(nixos.secrets);282 let mut out = Vec::new();283 for name in secrets.list_fields().await? {284 let secret = nix_go!(secrets[{ name }]);285 let is_shared: bool = nix_go_json!(secret.shared);286 if is_shared {287 continue;288 }289 out.push(name);290 }291 Ok(out)292 }293 pub async fn secret_field(&self, name: &str) -> Result<Value> {294 let nixos = self.nixos_config().await?;295 Ok(nix_go!(nixos.secrets[{ name }]))296 }297298 /// Packages for this host, resolved with nixpkgs overlays299 pub async fn pkgs(&self) -> Result<Value> {300 let Some(host_config) = &self.host_config else {301 bail!("local host has no host_config");302 };303 // TODO: Should nixos.options be cached?304 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))305 }306}307308impl Config {309 pub fn local_host(&self) -> ConfigHost {310 ConfigHost {311 config: self.clone(),312 name: "<virtual localhost>".to_owned(),313 local: true,314 session: OnceLock::new(),315 host_config: None,316 nixos_config: OnceCell::new(),317 groups: {318 let cell = OnceCell::new();319 let _ = cell.set(vec![]);320 cell321 },322 }323 }324325 pub async fn host(&self, name: &str) -> Result<ConfigHost> {326 let config = &self.config_field;327 let host_config = nix_go!(config.hosts[{ name }]);328329 Ok(ConfigHost {330 config: self.clone(),331 name: name.to_owned(),332 host_config: Some(host_config),333 nixos_config: OnceCell::new(),334 groups: OnceCell::new(),335 336 // TODO: Remove with connectivit refactor337 local: self.localhost == name,338 session: OnceLock::new(),339 })340 }341 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {342 let config = &self.config_field;343 let names = nix_go!(config.hosts).list_fields().await?;344 let mut out = vec![];345 for name in names {346 out.push(self.host(&name).await?);347 }348 Ok(out)349 }350 // TODO: Replace usages with .host().nixos_config351 pub async fn system_config(&self, host: &str) -> Result<Value> {352 let fleet_field = &self.config_field;353 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))354 }355356 /// Shared secrets configured in fleet.nix or in flake357 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {358 let config_field = &self.config_field;359 Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)360 }361 /// Shared secrets configured in fleet.nix362 pub fn list_shared(&self) -> Vec<String> {363 let data = self.data();364 data.shared_secrets.keys().cloned().collect()365 }366 pub fn has_shared(&self, name: &str) -> bool {367 let data = self.data();368 data.shared_secrets.contains_key(name)369 }370 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {371 let mut data = self.data_mut();372 data.shared_secrets.insert(name.to_owned(), shared);373 }374 pub fn remove_shared(&self, secret: &str) {375 let mut data = self.data_mut();376 data.shared_secrets.remove(secret);377 }378379 pub fn list_secrets(&self, host: &str) -> Vec<String> {380 let data = self.data();381 let Some(secrets) = data.host_secrets.get(host) else {382 return Vec::new();383 };384 secrets.keys().cloned().collect()385 }386387 pub fn has_secret(&self, host: &str, secret: &str) -> bool {388 let data = self.data();389 let Some(host_secrets) = data.host_secrets.get(host) else {390 return false;391 };392 host_secrets.contains_key(secret)393 }394 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {395 let mut data = self.data_mut();396 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();397 host_secrets.insert(secret, value);398 }399400 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {401 let data = self.data();402 let Some(host_secrets) = data.host_secrets.get(host) else {403 bail!("no secrets for machine {host}");404 };405 let Some(secret) = host_secrets.get(secret) else {406 bail!("machine {host} has no secret {secret}");407 };408 Ok(secret.clone())409 }410 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {411 let data = self.data();412 let Some(secret) = data.shared_secrets.get(secret) else {413 bail!("no shared secret {secret}");414 };415 Ok(secret.clone())416 }417 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {418 let config_field = &self.config_field;419 Ok(nix_go_json!(420 config_field.sharedSecrets[{ secret }].expectedOwners421 ))422 }423424 // TODO: Should this be something modifiable from other processes?425 // E.g terraform provider might want to update FleetData (e.g secrets),426 // and current implementation assumes only one process holds current fleet.nix427 // Given that it is no longer needs to be a file for nix evaluation,428 // maybe it can be a .nix file for persistence, but accessible only429 // thru some shared state controller? Might it be stored in terraform430 // state provider?431 pub fn data(&self) -> MutexGuard<FleetData> {432 self.data.lock().unwrap()433 }434 pub fn data_mut(&self) -> MutexGuard<FleetData> {435 self.data.lock().unwrap()436 }437 pub fn save(&self) -> Result<()> {438 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;439 let data = nixlike::serialize(&self.data() as &FleetData)?;440 tempfile.write_all(441 format!(442 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",443 data444 )445 .as_bytes(),446 )?;447 let mut fleet_data_path = self.directory.clone();448 fleet_data_path.push("fleet.nix");449 tempfile.persist(fleet_data_path)?;450 Ok(())451 }452}1use std::{2 cell::OnceCell,3 ffi::{OsStr, OsString},4 fmt::Display,5 io::Write,6 ops::Deref,7 path::PathBuf,8 str::FromStr,9 sync::{Arc, Mutex, MutexGuard, OnceLock},10};1112use anyhow::{anyhow, bail, ensure, Context, Result};13use fleet_shared::SecretData;14use nix_eval::{nix_go, nix_go_json, util::assert_warn, Value};15use openssh::SessionBuilder;16use serde::de::DeserializeOwned;17use tempfile::NamedTempFile;1819use crate::{20 command::MyCommand,21 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},22};2324pub struct FleetConfigInternals {25 pub local_system: String,26 pub directory: PathBuf,27 pub data: Mutex<FleetData>,28 pub nix_args: Vec<OsString>,29 /// fleet_config.config30 pub config_field: Value,31 // TODO: Remove with connectivity refactor32 pub localhost: String,3334 /// import nixpkgs {system = local};35 pub default_pkgs: Value,36}3738// TODO: Make field not pub39#[derive(Clone)]40pub struct Config(pub Arc<FleetConfigInternals>);4142impl Deref for Config {43 type Target = FleetConfigInternals;4445 fn deref(&self) -> &Self::Target {46 &self.047 }48}4950#[derive(Clone, Copy, Debug)]51pub enum EscalationStrategy {52 Sudo,53 Run0,54 Su,55}5657pub struct ConfigHost {58 config: Config,59 pub name: String,60 groups: OnceCell<Vec<String>>,6162 pub host_config: Option<Value>,63 pub nixos_config: OnceCell<Value>,64 pub pkgs_override: Option<Value>,6566 // TODO: Move command helpers away with connectivity refactor67 pub local: bool,68 pub session: OnceLock<Arc<openssh::Session>>,69}70// TODO: Move command helpers away with connectivity refactor71impl ConfigHost {72 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {73 // Prefer sudo, as run0 has some gotchas with polkit74 // and too many repeating prompts.75 if (self.find_in_path("sudo").await).is_ok() {76 return Ok(EscalationStrategy::Sudo);77 }78 if (self.find_in_path("run0").await).is_ok() {79 return Ok(EscalationStrategy::Run0);80 }81 Ok(EscalationStrategy::Su)82 }83 async fn open_session(&self) -> Result<Arc<openssh::Session>> {84 assert!(!self.local, "do not open ssh connection to local session");85 // FIXME: TOCTOU86 if let Some(session) = &self.session.get() {87 return Ok((*session).clone());88 };89 let session = SessionBuilder::default();90 let session = session91 .connect(&self.name)92 .await93 .map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;94 let session = Arc::new(session);95 self.session.set(session.clone()).expect("TOCTOU happened");96 Ok(session)97 }98 pub async fn mktemp_dir(&self) -> Result<String> {99 let mut cmd = self.cmd("mktemp").await?;100 cmd.arg("-d");101 let path = cmd.run_string().await?;102 Ok(path.trim_end().to_owned())103 }104 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {105 let mut cmd = self.cmd("cat").await?;106 cmd.arg(path);107 cmd.run_bytes().await108 }109 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {110 let mut cmd = self.cmd("cat").await?;111 cmd.arg(path);112 cmd.run_string().await113 }114 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {115 let mut cmd = self.cmd("ls").await?;116 cmd.arg(path);117 let out = cmd.run_string().await?;118 let mut lines = out.split('\n');119 if let Some(last) = lines.next_back() {120 ensure!(last.is_empty(), "output of ls should end with newline");121 }122 Ok(lines.map(ToOwned::to_owned).collect())123 }124 #[allow(dead_code)]125 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {126 let text = self.read_file_text(path).await?;127 Ok(serde_json::from_str(&text)?)128 }129 pub async fn read_env(&self, env: &str) -> Result<String> {130 let mut cmd = self.cmd("printenv").await?;131 cmd.arg(env);132 cmd.run_string().await133 }134 pub async fn find_in_path(&self, command: &str) -> Result<String> {135 // // `which` is not a part of coreutils, and it might not exist on machine.136 // let path = self.read_env("PATH").await?;137 // // Assuming delimiter is :, we don't work with windows host, this check will be much138 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)139 // for ele in path.split(':') {140 // let test_path = format!("{ele}/{cmd}");141 // test -x etc142 // }143 // let mut cmd = self.cmd("printenv").await?;144 // cmd.arg(env);145 // Ok(cmd.run_string().await?)146 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.147 let mut cmd = self148 .cmd_escalation(149 // Not used150 EscalationStrategy::Su,151 "which",152 )153 .await?;154 cmd.arg(command);155 cmd.run_string().await156 }157 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>158 where159 <D as FromStr>::Err: Display,160 {161 let text = self.read_file_text(path).await?;162 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))163 }164 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {165 self.cmd_escalation(self.escalation_strategy().await?, cmd)166 .await167 }168 pub async fn cmd_escalation(169 &self,170 escalation: EscalationStrategy,171 cmd: impl AsRef<OsStr>,172 ) -> Result<MyCommand> {173 if self.local {174 Ok(MyCommand::new(escalation, cmd))175 } else {176 let session = self.open_session().await?;177 Ok(MyCommand::new_on(escalation, cmd, session))178 }179 }180181 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {182 ensure!(data.encrypted, "secret is not encrypted");183 let mut cmd = self.cmd("fleet-install-secrets").await?;184 cmd.arg("decrypt").eqarg("--secret", data.to_string());185 let encoded = cmd186 .sudo()187 .run_string()188 .await189 .context("failed to call remote host for decrypt")?;190 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;191 ensure!(!data.encrypted, "secret came out encrypted");192 Ok(data.data)193 }194 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {195 ensure!(data.encrypted, "secret is not encrypted");196 let mut cmd = self.cmd("fleet-install-secrets").await?;197 cmd.arg("reencrypt").eqarg("--secret", data.to_string());198 for target in targets {199 let key = self.config.key(&target).await?;200 cmd.eqarg("--targets", key);201 }202 let encoded = cmd203 .sudo()204 .run_string()205 .await206 .context("failed to call remote host for decrypt")?;207 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;208 ensure!(data.encrypted, "secret came out not encrypted");209 Ok(data)210 }211 /// Returns path for futureproofing, as path might change i.e on conversion to CA212 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {213 if self.local {214 // Path is located locally, thus already trusted.215 return Ok(path.to_owned());216 }217 let mut nix = MyCommand::new(218 // Not used219 EscalationStrategy::Su,220 "nix",221 );222 nix.arg("copy")223 .arg("--substitute-on-destination")224 .comparg("--to", format!("ssh-ng://{}", self.name))225 .arg(path);226 nix.run_nix().await.context("nix copy")?;227 Ok(path.to_owned())228 }229 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {230 let mut cmd = self.cmd("systemctl").await?;231 cmd.arg("stop").arg(name);232 cmd.sudo().run().await233 }234 pub async fn systemctl_start(&self, name: &str) -> Result<()> {235 let mut cmd = self.cmd("systemctl").await?;236 cmd.arg("start").arg(name);237 cmd.sudo().run().await238 }239240 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {241 let mut cmd = self.cmd("rm").await?;242 cmd.arg("-f").arg(path);243 if sudo {244 cmd = cmd.sudo()245 }246 cmd.run().await247 }248}249impl ConfigHost {250 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,251 // assuming getting tags always returns the same value.252 pub async fn tags(&self) -> Result<Vec<String>> {253 if let Some(v) = self.groups.get() {254 return Ok(v.clone());255 }256 let Some(host_config) = &self.host_config else {257 return Ok(vec![]);258 };259 let tags: Vec<String> = nix_go_json!(host_config.tags);260261 let _ = self.groups.set(tags.clone());262263 Ok(tags)264 }265 pub async fn nixos_config(&self) -> Result<Value> {266 if let Some(v) = self.nixos_config.get() {267 return Ok(v.clone());268 }269 let Some(host_config) = &self.host_config else {270 bail!("local host has no nixos_config");271 };272 let nixos_config = nix_go!(host_config.nixos.config);273 assert_warn("nixos config evaluation", &nixos_config).await?;274275 let _ = self.nixos_config.set(nixos_config.clone());276277 Ok(nixos_config)278 }279280 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {281 let nixos = self.nixos_config().await?;282 let secrets = nix_go!(nixos.secrets);283 let mut out = Vec::new();284 for name in secrets.list_fields().await? {285 let secret = nix_go!(secrets[{ name }]);286 let is_shared: bool = nix_go_json!(secret.shared);287 if is_shared {288 continue;289 }290 out.push(name);291 }292 Ok(out)293 }294 pub async fn secret_field(&self, name: &str) -> Result<Value> {295 let nixos = self.nixos_config().await?;296 Ok(nix_go!(nixos.secrets[{ name }]))297 }298299 /// Packages for this host, resolved with nixpkgs overlays300 pub async fn pkgs(&self) -> Result<Value> {301 if let Some(value) = &self.pkgs_override {302 return Ok(value.clone());303 }304 let Some(host_config) = &self.host_config else {305 bail!("local host has no host_config");306 };307 // TODO: Should nixos.options be cached?308 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))309 }310}311312impl Config {313 pub fn local_host(&self) -> ConfigHost {314 ConfigHost {315 config: self.clone(),316 name: "<virtual localhost>".to_owned(),317 host_config: None,318 nixos_config: OnceCell::new(),319 groups: {320 let cell = OnceCell::new();321 let _ = cell.set(vec![]);322 cell323 },324 pkgs_override: Some(self.default_pkgs.clone()),325326 local: true,327 session: OnceLock::new(),328 }329 }330331 pub async fn host(&self, name: &str) -> Result<ConfigHost> {332 let config = &self.config_field;333 let host_config = nix_go!(config.hosts[{ name }]);334335 Ok(ConfigHost {336 config: self.clone(),337 name: name.to_owned(),338 host_config: Some(host_config),339 nixos_config: OnceCell::new(),340 groups: OnceCell::new(),341 pkgs_override: None,342343 // TODO: Remove with connectivit refactor344 local: self.localhost == name,345 session: OnceLock::new(),346 })347 }348 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {349 let config = &self.config_field;350 let names = nix_go!(config.hosts).list_fields().await?;351 let mut out = vec![];352 for name in names {353 out.push(self.host(&name).await?);354 }355 Ok(out)356 }357 // TODO: Replace usages with .host().nixos_config358 pub async fn system_config(&self, host: &str) -> Result<Value> {359 let fleet_field = &self.config_field;360 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))361 }362363 /// Shared secrets configured in fleet.nix or in flake364 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {365 let config_field = &self.config_field;366 Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)367 }368 /// Shared secrets configured in fleet.nix369 pub fn list_shared(&self) -> Vec<String> {370 let data = self.data();371 data.shared_secrets.keys().cloned().collect()372 }373 pub fn has_shared(&self, name: &str) -> bool {374 let data = self.data();375 data.shared_secrets.contains_key(name)376 }377 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {378 let mut data = self.data_mut();379 data.shared_secrets.insert(name.to_owned(), shared);380 }381 pub fn remove_shared(&self, secret: &str) {382 let mut data = self.data_mut();383 data.shared_secrets.remove(secret);384 }385386 pub fn list_secrets(&self, host: &str) -> Vec<String> {387 let data = self.data();388 let Some(secrets) = data.host_secrets.get(host) else {389 return Vec::new();390 };391 secrets.keys().cloned().collect()392 }393394 pub fn has_secret(&self, host: &str, secret: &str) -> bool {395 let data = self.data();396 let Some(host_secrets) = data.host_secrets.get(host) else {397 return false;398 };399 host_secrets.contains_key(secret)400 }401 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {402 let mut data = self.data_mut();403 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();404 host_secrets.insert(secret, value);405 }406407 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {408 let data = self.data();409 let Some(host_secrets) = data.host_secrets.get(host) else {410 bail!("no secrets for machine {host}");411 };412 let Some(secret) = host_secrets.get(secret) else {413 bail!("machine {host} has no secret {secret}");414 };415 Ok(secret.clone())416 }417 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {418 let data = self.data();419 let Some(secret) = data.shared_secrets.get(secret) else {420 bail!("no shared secret {secret}");421 };422 Ok(secret.clone())423 }424 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {425 let config_field = &self.config_field;426 Ok(nix_go_json!(427 config_field.sharedSecrets[{ secret }].expectedOwners428 ))429 }430431 // TODO: Should this be something modifiable from other processes?432 // E.g terraform provider might want to update FleetData (e.g secrets),433 // and current implementation assumes only one process holds current fleet.nix434 // Given that it is no longer needs to be a file for nix evaluation,435 // maybe it can be a .nix file for persistence, but accessible only436 // thru some shared state controller? Might it be stored in terraform437 // state provider?438 pub fn data(&self) -> MutexGuard<FleetData> {439 self.data.lock().unwrap()440 }441 pub fn data_mut(&self) -> MutexGuard<FleetData> {442 self.data.lock().unwrap()443 }444 pub fn save(&self) -> Result<()> {445 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;446 let data = nixlike::serialize(&self.data() as &FleetData)?;447 tempfile.write_all(448 format!(449 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",450 data451 )452 .as_bytes(),453 )?;454 let mut fleet_data_path = self.directory.clone();455 fleet_data_path.push("fleet.nix");456 tempfile.persist(fleet_data_path)?;457 Ok(())458 }459}crates/fleet-base/src/opts.rsdiffbeforeafterboth--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -196,11 +196,11 @@
let import = nix_go!(builtins_field.import);
let overlays = nix_go!(config_field.nixpkgs.overlays);
- let nixpkgs = nix_go!(fleet_field.nixpkgs.buildUsing | import);
+ let nixpkgs = nix_go!(config_field.nixpkgs.buildUsing | import);
let default_pkgs = nix_go!(nixpkgs(Obj {
overlays,
- system: { self.local_system.clone() },
+ system: { local_system.clone() },
}));
Ok(Config(Arc::new(FleetConfigInternals {
lib/flakePart.nixdiffbeforeafterboth--- a/lib/flakePart.nix
+++ b/lib/flakePart.nix
@@ -9,7 +9,6 @@
inherit (lib.attrsets) mapAttrs;
inherit (lib.types) lazyAttrsOf deferredModule unspecified;
inherit (lib.strings) isPath;
- inherit (fleetLib.options) mkHostsOption;
in {
options.fleetModules = mkOption {
type = lazyAttrsOf unspecified;
@@ -42,20 +41,18 @@
++ [
module
{
- options.hosts = mkHostsOption {
- nixos.nixpkgs.overlays = [
+ config = {
+ data =
+ if isPath data
+ then import data
+ else data;
+ nixpkgs.overlays = [
(final: prev:
import ../pkgs {
inherit (prev) callPackage;
craneLib = crane.mkLib prev;
})
];
- };
- config = {
- data =
- if isPath data
- then import data
- else data;
};
}
];