difftreelog
fix do not require wildcard with callPackage
in: trunk
6 files changed
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -265,13 +265,14 @@
let generator = nix_go!(secret.generator);
let on: Option<String> = nix_go_json!(default_generator.impureOn);
+ let nixpkgs = &config.nixpkgs;
+
let host = if let Some(on) = &on {
config.host(on).await?
} else {
config.local_host()
};
let on_pkgs = host.pkgs().await?;
- let call_package = nix_go!(on_pkgs.callPackage);
let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);
let mut recipients = Vec::new();
@@ -280,8 +281,11 @@
recipients.push(key);
}
let generators = nix_go!(mk_secret_generators(Obj { recipients }));
+ let pkgs_and_generators = nix_go!(on_pkgs + generators);
+
+ let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
- let generator = nix_go!(call_package(generator)(generators));
+ let generator = nix_go!(call_package(generator)(Obj {}));
let generator = generator.build_maybe_batch(batch).await?;
let generator = generator
@@ -353,8 +357,8 @@
bail!("generator should be lambda, got {gen_ty}");
}
}
+ let nixpkgs = &config.nixpkgs;
let default_pkgs = &config.default_pkgs;
- let default_call_package = nix_go!(default_pkgs.callPackage);
let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);
// Generators provide additional information in passthru, to access
// passthru we should call generator, but information about where this generator is supposed to build
@@ -367,7 +371,10 @@
let generators = nix_go!(default_mk_secret_generators(Obj {
recipients: <Vec<String>>::new(),
}));
- let default_generator = nix_go!(default_call_package(generator)(generators));
+ let pkgs_and_generators = nix_go!(default_pkgs + generators);
+
+ let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
+ let default_generator = nix_go!(call_package(generator)(Obj {}));
let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);
crates/fleet-base/src/host.rsdiffbeforeafterboth1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{anyhow, bail, ensure, Context, Result};14use fleet_shared::SecretData;15use nix_eval::{nix_go, nix_go_json, util::assert_warn, NixSession, Value};16use openssh::SessionBuilder;17use serde::de::DeserializeOwned;18use tempfile::NamedTempFile;1920use crate::{21 command::MyCommand,22 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},23};2425pub struct FleetConfigInternals {26 pub local_system: String,27 pub directory: PathBuf,28 pub data: Mutex<FleetData>,29 pub nix_args: Vec<OsString>,30 /// fleet_config.config31 pub config_field: Value,32 // TODO: Remove with connectivity refactor33 pub localhost: String,3435 /// import nixpkgs {system = local};36 pub default_pkgs: Value,3738 pub nix_session: NixSession,39}4041// TODO: Make field not pub42#[derive(Clone)]43pub struct Config(pub Arc<FleetConfigInternals>);4445impl Deref for Config {46 type Target = FleetConfigInternals;4748 fn deref(&self) -> &Self::Target {49 &self.050 }51}5253#[derive(Clone, Copy, Debug)]54pub enum EscalationStrategy {55 Sudo,56 Run0,57 Su,58}5960pub struct ConfigHost {61 config: Config,62 pub name: String,63 groups: OnceCell<Vec<String>>,6465 pub host_config: Option<Value>,66 pub nixos_config: OnceCell<Value>,67 pub pkgs_override: Option<Value>,6869 // TODO: Move command helpers away with connectivity refactor70 pub local: bool,71 pub session: OnceLock<Arc<openssh::Session>>,72}73// TODO: Move command helpers away with connectivity refactor74impl ConfigHost {75 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {76 // Prefer sudo, as run0 has some gotchas with polkit77 // and too many repeating prompts.78 if (self.find_in_path("sudo").await).is_ok() {79 return Ok(EscalationStrategy::Sudo);80 }81 if (self.find_in_path("run0").await).is_ok() {82 return Ok(EscalationStrategy::Run0);83 }84 Ok(EscalationStrategy::Su)85 }86 async fn open_session(&self) -> Result<Arc<openssh::Session>> {87 assert!(!self.local, "do not open ssh connection to local session");88 // FIXME: TOCTOU89 if let Some(session) = &self.session.get() {90 return Ok((*session).clone());91 };92 let session = SessionBuilder::default();93 let session = session94 .connect(&self.name)95 .await96 .map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;97 let session = Arc::new(session);98 self.session.set(session.clone()).expect("TOCTOU happened");99 Ok(session)100 }101 pub async fn mktemp_dir(&self) -> Result<String> {102 let mut cmd = self.cmd("mktemp").await?;103 cmd.arg("-d");104 let path = cmd.run_string().await?;105 Ok(path.trim_end().to_owned())106 }107 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {108 let mut cmd = self.cmd("cat").await?;109 cmd.arg(path);110 cmd.run_bytes().await111 }112 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {113 let mut cmd = self.cmd("cat").await?;114 cmd.arg(path);115 cmd.run_string().await116 }117 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {118 let mut cmd = self.cmd("ls").await?;119 cmd.arg(path);120 let out = cmd.run_string().await?;121 let mut lines = out.split('\n');122 if let Some(last) = lines.next_back() {123 ensure!(last.is_empty(), "output of ls should end with newline");124 }125 Ok(lines.map(ToOwned::to_owned).collect())126 }127 #[allow(dead_code)]128 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {129 let text = self.read_file_text(path).await?;130 Ok(serde_json::from_str(&text)?)131 }132 pub async fn read_env(&self, env: &str) -> Result<String> {133 let mut cmd = self.cmd("printenv").await?;134 cmd.arg(env);135 cmd.run_string().await136 }137 pub async fn find_in_path(&self, command: &str) -> Result<String> {138 // // `which` is not a part of coreutils, and it might not exist on machine.139 // let path = self.read_env("PATH").await?;140 // // Assuming delimiter is :, we don't work with windows host, this check will be much141 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)142 // for ele in path.split(':') {143 // let test_path = format!("{ele}/{cmd}");144 // test -x etc145 // }146 // let mut cmd = self.cmd("printenv").await?;147 // cmd.arg(env);148 // Ok(cmd.run_string().await?)149 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.150 let mut cmd = self151 .cmd_escalation(152 // Not used153 EscalationStrategy::Su,154 "which",155 )156 .await?;157 cmd.arg(command);158 cmd.run_string().await159 }160 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>161 where162 <D as FromStr>::Err: Display,163 {164 let text = self.read_file_text(path).await?;165 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))166 }167 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {168 self.cmd_escalation(self.escalation_strategy().await?, cmd)169 .await170 }171 pub async fn cmd_escalation(172 &self,173 escalation: EscalationStrategy,174 cmd: impl AsRef<OsStr>,175 ) -> Result<MyCommand> {176 if self.local {177 Ok(MyCommand::new(escalation, cmd))178 } else {179 let session = self.open_session().await?;180 Ok(MyCommand::new_on(escalation, cmd, session))181 }182 }183184 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {185 ensure!(data.encrypted, "secret is not encrypted");186 let mut cmd = self.cmd("fleet-install-secrets").await?;187 cmd.arg("decrypt").eqarg("--secret", data.to_string());188 let encoded = cmd189 .sudo()190 .run_string()191 .await192 .context("failed to call remote host for decrypt")?;193 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;194 ensure!(!data.encrypted, "secret came out encrypted");195 Ok(data.data)196 }197 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {198 ensure!(data.encrypted, "secret is not encrypted");199 let mut cmd = self.cmd("fleet-install-secrets").await?;200 cmd.arg("reencrypt").eqarg("--secret", data.to_string());201 for target in targets {202 let key = self.config.key(&target).await?;203 cmd.eqarg("--targets", key);204 }205 let encoded = cmd206 .sudo()207 .run_string()208 .await209 .context("failed to call remote host for decrypt")?;210 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;211 ensure!(data.encrypted, "secret came out not encrypted");212 Ok(data)213 }214 /// Returns path for futureproofing, as path might change i.e on conversion to CA215 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {216 if self.local {217 // Path is located locally, thus already trusted.218 return Ok(path.to_owned());219 }220 let mut nix = MyCommand::new(221 // Not used222 EscalationStrategy::Su,223 "nix",224 );225 nix.arg("copy")226 .arg("--substitute-on-destination")227 .comparg("--to", format!("ssh-ng://{}", self.name))228 .arg(path);229 nix.run_nix().await.context("nix copy")?;230 Ok(path.to_owned())231 }232 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {233 let mut cmd = self.cmd("systemctl").await?;234 cmd.arg("stop").arg(name);235 cmd.sudo().run().await236 }237 pub async fn systemctl_start(&self, name: &str) -> Result<()> {238 let mut cmd = self.cmd("systemctl").await?;239 cmd.arg("start").arg(name);240 cmd.sudo().run().await241 }242243 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {244 let mut cmd = self.cmd("rm").await?;245 cmd.arg("-f").arg(path);246 if sudo {247 cmd = cmd.sudo()248 }249 cmd.run().await250 }251}252impl ConfigHost {253 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,254 // assuming getting tags always returns the same value.255 pub async fn tags(&self) -> Result<Vec<String>> {256 if let Some(v) = self.groups.get() {257 return Ok(v.clone());258 }259 let Some(host_config) = &self.host_config else {260 return Ok(vec![]);261 };262 let tags: Vec<String> = nix_go_json!(host_config.tags);263264 let _ = self.groups.set(tags.clone());265266 Ok(tags)267 }268 pub async fn nixos_config(&self) -> Result<Value> {269 if let Some(v) = self.nixos_config.get() {270 return Ok(v.clone());271 }272 let Some(host_config) = &self.host_config else {273 bail!("local host has no nixos_config");274 };275 let nixos_config = nix_go!(host_config.nixos.config);276 assert_warn("nixos config evaluation", &nixos_config).await?;277278 let _ = self.nixos_config.set(nixos_config.clone());279280 Ok(nixos_config)281 }282283 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {284 let nixos = self.nixos_config().await?;285 let secrets = nix_go!(nixos.secrets);286 let mut out = Vec::new();287 for name in secrets.list_fields().await? {288 let secret = nix_go!(secrets[{ name }]);289 let is_shared: bool = nix_go_json!(secret.shared);290 if is_shared {291 continue;292 }293 out.push(name);294 }295 Ok(out)296 }297 pub async fn secret_field(&self, name: &str) -> Result<Value> {298 let nixos = self.nixos_config().await?;299 Ok(nix_go!(nixos.secrets[{ name }]))300 }301302 /// Packages for this host, resolved with nixpkgs overlays303 pub async fn pkgs(&self) -> Result<Value> {304 if let Some(value) = &self.pkgs_override {305 return Ok(value.clone());306 }307 let Some(host_config) = &self.host_config else {308 bail!("local host has no host_config");309 };310 // TODO: Should nixos.options be cached?311 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))312 }313}314315impl Config {316 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {317 let config = &self.config_field;318 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);319 Ok(tagged)320 }321 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {322 let mut out = BTreeSet::new();323 for owner in owners {324 if let Some(tag) = owner.strip_prefix('@') {325 let hosts = self.tagged_hostnames(tag).await?;326 out.extend(hosts);327 } else {328 out.insert(owner);329 }330 }331 Ok(out)332 }333 pub fn local_host(&self) -> ConfigHost {334 ConfigHost {335 config: self.clone(),336 name: "<virtual localhost>".to_owned(),337 host_config: None,338 nixos_config: OnceCell::new(),339 groups: {340 let cell = OnceCell::new();341 let _ = cell.set(vec![]);342 cell343 },344 pkgs_override: Some(self.default_pkgs.clone()),345346 local: true,347 session: OnceLock::new(),348 }349 }350351 pub async fn host(&self, name: &str) -> Result<ConfigHost> {352 let config = &self.config_field;353 let host_config = nix_go!(config.hosts[{ name }]);354355 Ok(ConfigHost {356 config: self.clone(),357 name: name.to_owned(),358 host_config: Some(host_config),359 nixos_config: OnceCell::new(),360 groups: OnceCell::new(),361 pkgs_override: None,362363 // TODO: Remove with connectivit refactor364 local: self.localhost == name,365 session: OnceLock::new(),366 })367 }368 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {369 let config = &self.config_field;370 let names = nix_go!(config.hosts).list_fields().await?;371 let mut out = vec![];372 for name in names {373 out.push(self.host(&name).await?);374 }375 Ok(out)376 }377 // TODO: Replace usages with .host().nixos_config378 pub async fn system_config(&self, host: &str) -> Result<Value> {379 let fleet_field = &self.config_field;380 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))381 }382383 /// Shared secrets configured in fleet.nix or in flake384 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {385 let config_field = &self.config_field;386 Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)387 }388 /// Shared secrets configured in fleet.nix389 pub fn list_shared(&self) -> Vec<String> {390 let data = self.data();391 data.shared_secrets.keys().cloned().collect()392 }393 pub fn has_shared(&self, name: &str) -> bool {394 let data = self.data();395 data.shared_secrets.contains_key(name)396 }397 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {398 let mut data = self.data_mut();399 data.shared_secrets.insert(name.to_owned(), shared);400 }401 pub fn remove_shared(&self, secret: &str) {402 let mut data = self.data_mut();403 data.shared_secrets.remove(secret);404 }405406 pub fn list_secrets(&self, host: &str) -> Vec<String> {407 let data = self.data();408 let Some(secrets) = data.host_secrets.get(host) else {409 return Vec::new();410 };411 secrets.keys().cloned().collect()412 }413414 pub fn has_secret(&self, host: &str, secret: &str) -> bool {415 let data = self.data();416 let Some(host_secrets) = data.host_secrets.get(host) else {417 return false;418 };419 host_secrets.contains_key(secret)420 }421 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {422 let mut data = self.data_mut();423 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();424 host_secrets.insert(secret, value);425 }426427 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {428 let data = self.data();429 let Some(host_secrets) = data.host_secrets.get(host) else {430 bail!("no secrets for machine {host}");431 };432 let Some(secret) = host_secrets.get(secret) else {433 bail!("machine {host} has no secret {secret}");434 };435 Ok(secret.clone())436 }437 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {438 let data = self.data();439 let Some(secret) = data.shared_secrets.get(secret) else {440 bail!("no shared secret {secret}");441 };442 Ok(secret.clone())443 }444 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {445 let config_field = &self.config_field;446 Ok(nix_go_json!(447 config_field.sharedSecrets[{ secret }].expectedOwners448 ))449 }450451 // TODO: Should this be something modifiable from other processes?452 // E.g terraform provider might want to update FleetData (e.g secrets),453 // and current implementation assumes only one process holds current fleet.nix454 // Given that it is no longer needs to be a file for nix evaluation,455 // maybe it can be a .nix file for persistence, but accessible only456 // thru some shared state controller? Might it be stored in terraform457 // state provider?458 pub fn data(&self) -> MutexGuard<FleetData> {459 self.data.lock().unwrap()460 }461 pub fn data_mut(&self) -> MutexGuard<FleetData> {462 self.data.lock().unwrap()463 }464 pub fn save(&self) -> Result<()> {465 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.")?;466 let data = nixlike::serialize(&self.data() as &FleetData)?;467 tempfile.write_all(468 format!(469 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",470 data471 )472 .as_bytes(),473 )?;474 let mut fleet_data_path = self.directory.clone();475 fleet_data_path.push("fleet.nix");476 tempfile.persist(fleet_data_path)?;477 Ok(())478 }479}1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{anyhow, bail, ensure, Context, Result};14use fleet_shared::SecretData;15use nix_eval::{nix_go, nix_go_json, util::assert_warn, NixSession, Value};16use openssh::SessionBuilder;17use serde::de::DeserializeOwned;18use tempfile::NamedTempFile;1920use crate::{21 command::MyCommand,22 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},23};2425pub struct FleetConfigInternals {26 pub local_system: String,27 pub directory: PathBuf,28 pub data: Mutex<FleetData>,29 pub nix_args: Vec<OsString>,30 /// fleet_config.config31 pub config_field: Value,32 // TODO: Remove with connectivity refactor33 pub localhost: String,3435 /// import nixpkgs {system = local};36 pub default_pkgs: Value,37 pub nixpkgs: Value,3839 pub nix_session: NixSession,40}4142// TODO: Make field not pub43#[derive(Clone)]44pub struct Config(pub Arc<FleetConfigInternals>);4546impl Deref for Config {47 type Target = FleetConfigInternals;4849 fn deref(&self) -> &Self::Target {50 &self.051 }52}5354#[derive(Clone, Copy, Debug)]55pub enum EscalationStrategy {56 Sudo,57 Run0,58 Su,59}6061pub struct ConfigHost {62 config: Config,63 pub name: String,64 groups: OnceCell<Vec<String>>,6566 pub host_config: Option<Value>,67 pub nixos_config: OnceCell<Value>,68 pub pkgs_override: Option<Value>,6970 // TODO: Move command helpers away with connectivity refactor71 pub local: bool,72 pub session: OnceLock<Arc<openssh::Session>>,73}74// TODO: Move command helpers away with connectivity refactor75impl ConfigHost {76 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {77 // Prefer sudo, as run0 has some gotchas with polkit78 // and too many repeating prompts.79 if (self.find_in_path("sudo").await).is_ok() {80 return Ok(EscalationStrategy::Sudo);81 }82 if (self.find_in_path("run0").await).is_ok() {83 return Ok(EscalationStrategy::Run0);84 }85 Ok(EscalationStrategy::Su)86 }87 async fn open_session(&self) -> Result<Arc<openssh::Session>> {88 assert!(!self.local, "do not open ssh connection to local session");89 // FIXME: TOCTOU90 if let Some(session) = &self.session.get() {91 return Ok((*session).clone());92 };93 let session = SessionBuilder::default();94 let session = session95 .connect(&self.name)96 .await97 .map_err(|e| anyhow!("ssh error while connecting to {}: {e}", self.name))?;98 let session = Arc::new(session);99 self.session.set(session.clone()).expect("TOCTOU happened");100 Ok(session)101 }102 pub async fn mktemp_dir(&self) -> Result<String> {103 let mut cmd = self.cmd("mktemp").await?;104 cmd.arg("-d");105 let path = cmd.run_string().await?;106 Ok(path.trim_end().to_owned())107 }108 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {109 let mut cmd = self.cmd("cat").await?;110 cmd.arg(path);111 cmd.run_bytes().await112 }113 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {114 let mut cmd = self.cmd("cat").await?;115 cmd.arg(path);116 cmd.run_string().await117 }118 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {119 let mut cmd = self.cmd("ls").await?;120 cmd.arg(path);121 let out = cmd.run_string().await?;122 let mut lines = out.split('\n');123 if let Some(last) = lines.next_back() {124 ensure!(last.is_empty(), "output of ls should end with newline");125 }126 Ok(lines.map(ToOwned::to_owned).collect())127 }128 #[allow(dead_code)]129 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {130 let text = self.read_file_text(path).await?;131 Ok(serde_json::from_str(&text)?)132 }133 pub async fn read_env(&self, env: &str) -> Result<String> {134 let mut cmd = self.cmd("printenv").await?;135 cmd.arg(env);136 cmd.run_string().await137 }138 pub async fn find_in_path(&self, command: &str) -> Result<String> {139 // // `which` is not a part of coreutils, and it might not exist on machine.140 // let path = self.read_env("PATH").await?;141 // // Assuming delimiter is :, we don't work with windows host, this check will be much142 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)143 // for ele in path.split(':') {144 // let test_path = format!("{ele}/{cmd}");145 // test -x etc146 // }147 // let mut cmd = self.cmd("printenv").await?;148 // cmd.arg(env);149 // Ok(cmd.run_string().await?)150 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.151 let mut cmd = self152 .cmd_escalation(153 // Not used154 EscalationStrategy::Su,155 "which",156 )157 .await?;158 cmd.arg(command);159 cmd.run_string().await160 }161 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>162 where163 <D as FromStr>::Err: Display,164 {165 let text = self.read_file_text(path).await?;166 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))167 }168 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {169 self.cmd_escalation(self.escalation_strategy().await?, cmd)170 .await171 }172 pub async fn cmd_escalation(173 &self,174 escalation: EscalationStrategy,175 cmd: impl AsRef<OsStr>,176 ) -> Result<MyCommand> {177 if self.local {178 Ok(MyCommand::new(escalation, cmd))179 } else {180 let session = self.open_session().await?;181 Ok(MyCommand::new_on(escalation, cmd, session))182 }183 }184185 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {186 ensure!(data.encrypted, "secret is not encrypted");187 let mut cmd = self.cmd("fleet-install-secrets").await?;188 cmd.arg("decrypt").eqarg("--secret", data.to_string());189 let encoded = cmd190 .sudo()191 .run_string()192 .await193 .context("failed to call remote host for decrypt")?;194 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;195 ensure!(!data.encrypted, "secret came out encrypted");196 Ok(data.data)197 }198 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {199 ensure!(data.encrypted, "secret is not encrypted");200 let mut cmd = self.cmd("fleet-install-secrets").await?;201 cmd.arg("reencrypt").eqarg("--secret", data.to_string());202 for target in targets {203 let key = self.config.key(&target).await?;204 cmd.eqarg("--targets", key);205 }206 let encoded = cmd207 .sudo()208 .run_string()209 .await210 .context("failed to call remote host for decrypt")?;211 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;212 ensure!(data.encrypted, "secret came out not encrypted");213 Ok(data)214 }215 /// Returns path for futureproofing, as path might change i.e on conversion to CA216 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {217 if self.local {218 // Path is located locally, thus already trusted.219 return Ok(path.to_owned());220 }221 let mut nix = MyCommand::new(222 // Not used223 EscalationStrategy::Su,224 "nix",225 );226 nix.arg("copy")227 .arg("--substitute-on-destination")228 .comparg("--to", format!("ssh-ng://{}", self.name))229 .arg(path);230 nix.run_nix().await.context("nix copy")?;231 Ok(path.to_owned())232 }233 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {234 let mut cmd = self.cmd("systemctl").await?;235 cmd.arg("stop").arg(name);236 cmd.sudo().run().await237 }238 pub async fn systemctl_start(&self, name: &str) -> Result<()> {239 let mut cmd = self.cmd("systemctl").await?;240 cmd.arg("start").arg(name);241 cmd.sudo().run().await242 }243244 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {245 let mut cmd = self.cmd("rm").await?;246 cmd.arg("-f").arg(path);247 if sudo {248 cmd = cmd.sudo()249 }250 cmd.run().await251 }252}253impl ConfigHost {254 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,255 // assuming getting tags always returns the same value.256 pub async fn tags(&self) -> Result<Vec<String>> {257 if let Some(v) = self.groups.get() {258 return Ok(v.clone());259 }260 let Some(host_config) = &self.host_config else {261 return Ok(vec![]);262 };263 let tags: Vec<String> = nix_go_json!(host_config.tags);264265 let _ = self.groups.set(tags.clone());266267 Ok(tags)268 }269 pub async fn nixos_config(&self) -> Result<Value> {270 if let Some(v) = self.nixos_config.get() {271 return Ok(v.clone());272 }273 let Some(host_config) = &self.host_config else {274 bail!("local host has no nixos_config");275 };276 let nixos_config = nix_go!(host_config.nixos.config);277 assert_warn("nixos config evaluation", &nixos_config).await?;278279 let _ = self.nixos_config.set(nixos_config.clone());280281 Ok(nixos_config)282 }283284 pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {285 let nixos = self.nixos_config().await?;286 let secrets = nix_go!(nixos.secrets);287 let mut out = Vec::new();288 for name in secrets.list_fields().await? {289 let secret = nix_go!(secrets[{ name }]);290 let is_shared: bool = nix_go_json!(secret.shared);291 if is_shared {292 continue;293 }294 out.push(name);295 }296 Ok(out)297 }298 pub async fn secret_field(&self, name: &str) -> Result<Value> {299 let nixos = self.nixos_config().await?;300 Ok(nix_go!(nixos.secrets[{ name }]))301 }302303 /// Packages for this host, resolved with nixpkgs overlays304 pub async fn pkgs(&self) -> Result<Value> {305 if let Some(value) = &self.pkgs_override {306 return Ok(value.clone());307 }308 let Some(host_config) = &self.host_config else {309 bail!("local host has no host_config");310 };311 // TODO: Should nixos.options be cached?312 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))313 }314}315316impl Config {317 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {318 let config = &self.config_field;319 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);320 Ok(tagged)321 }322 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {323 let mut out = BTreeSet::new();324 for owner in owners {325 if let Some(tag) = owner.strip_prefix('@') {326 let hosts = self.tagged_hostnames(tag).await?;327 out.extend(hosts);328 } else {329 out.insert(owner);330 }331 }332 Ok(out)333 }334 pub fn local_host(&self) -> ConfigHost {335 ConfigHost {336 config: self.clone(),337 name: "<virtual localhost>".to_owned(),338 host_config: None,339 nixos_config: OnceCell::new(),340 groups: {341 let cell = OnceCell::new();342 let _ = cell.set(vec![]);343 cell344 },345 pkgs_override: Some(self.default_pkgs.clone()),346347 local: true,348 session: OnceLock::new(),349 }350 }351352 pub async fn host(&self, name: &str) -> Result<ConfigHost> {353 let config = &self.config_field;354 let host_config = nix_go!(config.hosts[{ name }]);355356 Ok(ConfigHost {357 config: self.clone(),358 name: name.to_owned(),359 host_config: Some(host_config),360 nixos_config: OnceCell::new(),361 groups: OnceCell::new(),362 pkgs_override: None,363364 // TODO: Remove with connectivit refactor365 local: self.localhost == name,366 session: OnceLock::new(),367 })368 }369 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {370 let config = &self.config_field;371 let names = nix_go!(config.hosts).list_fields().await?;372 let mut out = vec![];373 for name in names {374 out.push(self.host(&name).await?);375 }376 Ok(out)377 }378 // TODO: Replace usages with .host().nixos_config379 pub async fn system_config(&self, host: &str) -> Result<Value> {380 let fleet_field = &self.config_field;381 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))382 }383384 /// Shared secrets configured in fleet.nix or in flake385 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {386 let config_field = &self.config_field;387 Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)388 }389 /// Shared secrets configured in fleet.nix390 pub fn list_shared(&self) -> Vec<String> {391 let data = self.data();392 data.shared_secrets.keys().cloned().collect()393 }394 pub fn has_shared(&self, name: &str) -> bool {395 let data = self.data();396 data.shared_secrets.contains_key(name)397 }398 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {399 let mut data = self.data_mut();400 data.shared_secrets.insert(name.to_owned(), shared);401 }402 pub fn remove_shared(&self, secret: &str) {403 let mut data = self.data_mut();404 data.shared_secrets.remove(secret);405 }406407 pub fn list_secrets(&self, host: &str) -> Vec<String> {408 let data = self.data();409 let Some(secrets) = data.host_secrets.get(host) else {410 return Vec::new();411 };412 secrets.keys().cloned().collect()413 }414415 pub fn has_secret(&self, host: &str, secret: &str) -> bool {416 let data = self.data();417 let Some(host_secrets) = data.host_secrets.get(host) else {418 return false;419 };420 host_secrets.contains_key(secret)421 }422 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {423 let mut data = self.data_mut();424 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();425 host_secrets.insert(secret, value);426 }427428 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {429 let data = self.data();430 let Some(host_secrets) = data.host_secrets.get(host) else {431 bail!("no secrets for machine {host}");432 };433 let Some(secret) = host_secrets.get(secret) else {434 bail!("machine {host} has no secret {secret}");435 };436 Ok(secret.clone())437 }438 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {439 let data = self.data();440 let Some(secret) = data.shared_secrets.get(secret) else {441 bail!("no shared secret {secret}");442 };443 Ok(secret.clone())444 }445 pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {446 let config_field = &self.config_field;447 Ok(nix_go_json!(448 config_field.sharedSecrets[{ secret }].expectedOwners449 ))450 }451452 // TODO: Should this be something modifiable from other processes?453 // E.g terraform provider might want to update FleetData (e.g secrets),454 // and current implementation assumes only one process holds current fleet.nix455 // Given that it is no longer needs to be a file for nix evaluation,456 // maybe it can be a .nix file for persistence, but accessible only457 // thru some shared state controller? Might it be stored in terraform458 // state provider?459 pub fn data(&self) -> MutexGuard<FleetData> {460 self.data.lock().unwrap()461 }462 pub fn data_mut(&self) -> MutexGuard<FleetData> {463 self.data.lock().unwrap()464 }465 pub fn save(&self) -> Result<()> {466 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.")?;467 let data = nixlike::serialize(&self.data() as &FleetData)?;468 tempfile.write_all(469 format!(470 "# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",471 data472 )473 .as_bytes(),474 )?;475 let mut fleet_data_path = self.directory.clone();476 fleet_data_path.push("fleet.nix");477 tempfile.persist(fleet_data_path)?;478 Ok(())479 }480}crates/fleet-base/src/opts.rsdiffbeforeafterboth--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -225,6 +225,7 @@
nix_args,
config_field,
default_pkgs,
+ nixpkgs,
localhost: self.localhost.to_owned(),
})))
}
crates/nix-eval/src/macros.rsdiffbeforeafterboth--- a/crates/nix-eval/src/macros.rs
+++ b/crates/nix-eval/src/macros.rs
@@ -231,6 +231,9 @@
(@o($o:ident) | $($var:tt)*) => {
$o.push(Index::Pipe($crate::nix_expr_inner!($($var)+)));
};
+ (@o($o:ident) + $($var:tt)*) => {
+ $o.push(Index::Merge($crate::nix_expr_inner!($($var)+)));
+ };
(@o($o:ident)) => {};
($field:ident $($tt:tt)+) => {{
use $crate::{nix_go, Index};
crates/nix-eval/src/value.rsdiffbeforeafterboth--- a/crates/nix-eval/src/value.rs
+++ b/crates/nix-eval/src/value.rs
@@ -15,6 +15,7 @@
Expr(NixExprBuilder),
ExprApply(NixExprBuilder),
Pipe(NixExprBuilder),
+ Merge(NixExprBuilder),
}
impl Index {
pub fn var(v: impl AsRef<str>) -> Self {
@@ -56,6 +57,9 @@
Index::Pipe(e) => {
write!(f, "<map>({})", e.out)
}
+ Index::Merge(e) => {
+ write!(f, "//({})", e.out)
+ }
}
}
}
@@ -157,6 +161,12 @@
let index = format!("sess_field_{}", index.0.value.expect("value"));
query = format!("({index} {query})");
}
+ Index::Merge(v) => {
+ let index = Value::new(self.0.session.clone(), &v.out).await?;
+ used_fields.push(index.clone());
+ let index = format!("sess_field_{}", index.0.value.expect("value"));
+ query = format!("({query} // {index})");
+ }
}
}
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -46,7 +46,6 @@
mkPassword = {size ? 32}: {
coreutils,
mkSecretGenerator,
- ...
}:
mkSecretGenerator {
script = ''
@@ -58,7 +57,7 @@
mkEd25519 = {
noEmbedPublic ? false,
encoding ? null,
- }: {mkSecretGenerator, ...}:
+ }: {mkSecretGenerator}:
mkSecretGenerator {
script = ''
mkdir $out
@@ -68,7 +67,7 @@
'';
};
- mkX25519 = {encoding ? null}: {mkSecretGenerator, ...}:
+ mkX25519 = {encoding ? null}: {mkSecretGenerator}:
mkSecretGenerator {
script = ''
mkdir $out
@@ -80,7 +79,6 @@
mkRsa = {size ? 4096}: {
openssl,
mkSecretGenerator,
- ...
}:
mkSecretGenerator {
script = ''
@@ -98,7 +96,7 @@
count ? 32,
encoding,
noNuls ? false,
- }: {mkSecretGenerator, ...}:
+ }: {mkSecretGenerator}:
mkSecretGenerator {
script = ''
mkdir $out