git.delta.rocks / jrsonnet / refs/commits / 94ece5cae749

difftreelog

build update to nixos release-25.05

luvuxwnmYaroslav Bolyukin2025-06-28parent: #1470de8.patch.diff
in: trunk

34 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "abort-on-drop"
@@ -377,29 +377,6 @@
 ]
 
 [[package]]
-name = "bindgen"
-version = "0.69.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
-dependencies = [
- "bitflags",
- "cexpr",
- "clang-sys",
- "itertools 0.12.1",
- "lazy_static",
- "lazycell",
- "log",
- "prettyplease",
- "proc-macro2",
- "quote",
- "regex",
- "rustc-hash",
- "shlex",
- "syn",
- "which",
-]
-
-[[package]]
 name = "bitflags"
 version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -488,15 +465,6 @@
 ]
 
 [[package]]
-name = "cexpr"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
-dependencies = [
- "nom 7.1.3",
-]
-
-[[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -556,17 +524,6 @@
  "crypto-common",
  "inout",
  "zeroize",
-]
-
-[[package]]
-name = "clang-sys"
-version = "1.8.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
-dependencies = [
- "glob",
- "libc",
- "libloading",
 ]
 
 [[package]]
@@ -1242,12 +1199,6 @@
 version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
-
-[[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "h2"
@@ -1332,15 +1283,6 @@
 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
 dependencies = [
  "digest",
-]
-
-[[package]]
-name = "home"
-version = "0.5.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
-dependencies = [
- "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -1648,15 +1590,6 @@
 version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
-
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
 
 [[package]]
 name = "itertools"
@@ -1701,28 +1634,12 @@
 ]
 
 [[package]]
-name = "lazycell"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
-
-[[package]]
 name = "libc"
 version = "0.2.174"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
 
 [[package]]
-name = "libloading"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
-dependencies = [
- "cfg-if",
- "windows-targets",
-]
-
-[[package]]
 name = "libm"
 version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1887,14 +1804,6 @@
  "tokio-util",
  "tracing",
  "unindent",
-]
-
-[[package]]
-name = "nix-native-eval"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "nixrs",
 ]
 
 [[package]]
@@ -1912,25 +1821,6 @@
 ]
 
 [[package]]
-name = "nixrs"
-version = "0.1.0"
-source = "git+https://github.com/Anillc/nixrs#740fcf4048cc5b6de8c54d18254f12d53909a867"
-dependencies = [
- "libc",
- "nixrs-sys",
- "thiserror 1.0.69",
-]
-
-[[package]]
-name = "nixrs-sys"
-version = "0.1.0"
-source = "git+https://github.com/Anillc/nixrs#740fcf4048cc5b6de8c54d18254f12d53909a867"
-dependencies = [
- "bindgen",
- "pkg-config",
-]
-
-[[package]]
 name = "nom"
 version = "7.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2224,12 +2114,6 @@
  "der",
  "spki",
 ]
-
-[[package]]
-name = "pkg-config"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
 
 [[package]]
 name = "poly1305"
@@ -3752,18 +3636,6 @@
 dependencies = [
  "js-sys",
  "wasm-bindgen",
-]
-
-[[package]]
-name = "which"
-version = "4.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
-dependencies = [
- "either",
- "home",
- "once_cell",
- "rustix 0.38.40",
 ]
 
 [[package]]
modifiedCargo.tomldiffbeforeafterboth
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,8 +2,8 @@
 members = ["crates/*", "cmds/*"]
 resolver = "2"
 package.version = "0.1.0"
-package.edition = "2021"
-package.rust-version = "1.82.0"
+package.edition = "2024"
+package.rust-version = "1.86.0"
 
 [workspace.dependencies]
 better-command = { path = "./crates/better-command" }
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -1,15 +1,15 @@
 use std::{env::current_dir, os::unix::fs::symlink, path::PathBuf};
 
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 use clap::Parser;
 use fleet_base::{
-	deploy::{deploy_task, upload_task, DeployAction},
+	deploy::{DeployAction, deploy_task, upload_task},
 	host::{Config, DeployKind, GenerationStorage},
 	opts::FleetOpts,
 };
-use nix_eval::{nix_go, NixBuildBatch};
+use nix_eval::{NixBuildBatch, nix_go};
 use tokio::task::LocalSet;
-use tracing::{error, field, info, info_span, warn, Instrument};
+use tracing::{Instrument, error, field, info, info_span, warn};
 
 #[derive(Parser)]
 pub struct Deploy {
@@ -167,11 +167,12 @@
 						self.action,
 						&host,
 						remote_path,
-						if let Ok(v) = opts.action_attr(&host, "specialisation").await {
-							v
-						} else {
-							error!("unreachable? failed to get specialization");
-							return;
+						match opts.action_attr(&host, "specialisation").await {
+							Ok(v) => v,
+							_ => {
+								error!("unreachable? failed to get specialization");
+								return;
+							}
 						},
 						disable_rollback,
 					)
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -1,6 +1,6 @@
 use std::collections::BTreeSet;
 
-use anyhow::{ensure, Result};
+use anyhow::{Result, ensure};
 use clap::Parser;
 use fleet_base::host::Config;
 use nix_eval::nix_go_json;
modifiedcmds/fleet/src/cmds/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/mod.rs
+++ b/cmds/fleet/src/cmds/mod.rs
@@ -1,6 +1,6 @@
 pub mod build_systems;
 pub mod complete;
 pub mod info;
+pub mod rollback;
 pub mod secrets;
 pub mod tf;
-pub mod rollback;
\ No newline at end of file
modifiedcmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -1,9 +1,9 @@
 use std::collections::HashSet;
 
-use anyhow::{bail, Result};
+use anyhow::{Result, bail};
 use clap::Parser;
 use fleet_base::{
-	deploy::{deploy_task, upload_task, DeployAction},
+	deploy::{DeployAction, deploy_task, upload_task},
 	host::{Config, ConfigHost, Generation, GenerationStorage},
 	opts::FleetOpts,
 };
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,25 +1,25 @@
 use std::{
 	collections::{BTreeMap, BTreeSet, HashSet},
-	io::{self, stdin, stdout, Read, Write},
+	io::{self, Read, Write, stdin, stdout},
 	path::PathBuf,
 };
 
 use age::Recipient;
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
 use chrono::{DateTime, Utc};
 use clap::Parser;
 use fleet_base::{
-	fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},
+	fleetdata::{FleetSecret, FleetSecretPart, FleetSharedSecret, encrypt_secret_data},
 	host::Config,
 	opts::FleetOpts,
 };
 use fleet_shared::SecretData;
-use nix_eval::{nix_go, nix_go_json, NixBuildBatch, Value};
+use nix_eval::{NixBuildBatch, Value, nix_go, nix_go_json};
 use owo_colors::OwoColorize;
 use serde::Deserialize;
 use tabled::{Table, Tabled};
 use tokio::fs::read;
-use tracing::{error, info, info_span, warn, Instrument};
+use tracing::{Instrument, error, info, info_span, warn};
 
 #[derive(Parser)]
 pub enum Secret {
@@ -187,7 +187,9 @@
 		true
 	} else if set.difference(&expected_set).next().is_some() {
 		// TODO: Remove this warning for revokable secrets.
-		warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");
+		warn!(
+			"host was removed from secret owners, but until this host rebuild, the secret will still be stored on it."
+		);
 		nix_go_json!(field.regenerateOnOwnerRemoved)
 	} else if expected_set.difference(&set).next().is_some() {
 		nix_go_json!(field.regenerateOnOwnerAdded)
@@ -296,8 +298,8 @@
 	let out_parent = host.mktemp_dir().await?;
 	let out = format!("{out_parent}/out");
 
-	let mut gen = host.cmd(generator).await?;
-	gen.env("out", &out);
+	let mut r#gen = host.cmd(generator).await?;
+	r#gen.env("out", &out);
 	if on.is_none() {
 		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.
 		let project_path: String = config
@@ -306,9 +308,9 @@
 			.into_os_string()
 			.into_string()
 			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;
-		gen.env("FLEET_PROJECT", project_path);
+		r#gen.env("FLEET_PROJECT", project_path);
 	}
-	gen.run().await.context("impure generator")?;
+	r#gen.run().await.context("impure generator")?;
 
 	{
 		let marker = host.read_file_text(format!("{out}/marker")).await?;
@@ -510,7 +512,9 @@
 	if !remove_machines.is_empty() {
 		// TODO: maybe force secret regeneration?
 		// Not that useful without revokation.
-		warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");
+		warn!(
+			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"
+		);
 	}
 	Ok(target_machines)
 }
@@ -596,7 +600,9 @@
 				part: part_name,
 			} => {
 				if config.has_secret(&machine, &name) && !replace && !merge {
-					bail!("secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret");
+					bail!(
+						"secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret"
+					);
 				}
 
 				let mut out = if merge && !replace {
modifiedcmds/fleet/src/extra_args.rsdiffbeforeafterboth
--- a/cmds/fleet/src/extra_args.rs
+++ b/cmds/fleet/src/extra_args.rs
@@ -1,6 +1,6 @@
 use std::ffi::{OsStr, OsString};
 
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 
 pub fn parse_os(os: &OsStr) -> Result<Vec<OsString>> {
 	Ok(shlex::bytes::split(os.as_encoded_bytes())
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -6,27 +6,27 @@
 
 use std::{ffi::OsString, process::ExitCode};
 
-use anyhow::{bail, Result};
+use anyhow::{Result, bail};
 use clap::{CommandFactory, Parser};
 use cmds::{
 	build_systems::{BuildSystems, Deploy},
-	rollback::RollbackSingle,
 	complete::Complete,
 	info::Info,
+	rollback::RollbackSingle,
 	secrets::Secret,
 	tf::Tf,
 };
 use fleet_base::{host::Config, opts::FleetOpts};
-use futures::{future::LocalBoxFuture, stream::FuturesUnordered, TryStreamExt};
+use futures::{TryStreamExt, future::LocalBoxFuture, stream::FuturesUnordered};
 // use host::Config;
 #[cfg(feature = "indicatif")]
 use human_repr::HumanCount;
 #[cfg(feature = "indicatif")]
 use indicatif::{ProgressState, ProgressStyle};
-use tracing::{error, info, info_span, Instrument};
+use tracing::{Instrument, error, info, info_span};
 #[cfg(feature = "indicatif")]
 use tracing_indicatif::IndicatifLayer;
-use tracing_subscriber::{prelude::*, EnvFilter};
+use tracing_subscriber::{EnvFilter, prelude::*};
 
 #[derive(Parser)]
 struct Prefetch {}
modifiedcmds/generator-helper/src/main.rsdiffbeforeafterboth
--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -1,21 +1,22 @@
 use std::{
 	env,
 	fs::{File, OpenOptions},
-	io::{self, copy, stdin, stdout, Read, Write},
+	io::{self, Read, Write, copy, stdin, stdout},
 	str::FromStr,
 };
 
 use age::{
+	Encryptor, Recipient,
 	ssh::{ParseRecipientKeyError, Recipient as SshRecipient},
-	Encryptor, Recipient,
 };
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
 use clap::{Parser, ValueEnum};
 use ed25519_dalek::SecretKey;
 use fleet_shared::SecretData;
 use rand::{
+	RngCore,
 	distr::{Alphanumeric, Distribution, SampleString, Uniform},
-	rng, RngCore,
+	rng,
 };
 
 fn write_output_file(out: &str) -> Result<File> {
@@ -78,7 +79,9 @@
 	let list = match list {
 		Ok(v) => v,
 		Err(env::VarError::NotPresent) => {
-			bail!("gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities");
+			bail!(
+				"gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities"
+			);
 		}
 		Err(e) => bail!("somehow, identities list is not utf-8: {e}"),
 	};
@@ -254,13 +257,7 @@
 					write_private(
 						&recipients,
 						&private,
-						&key[..{
-							if no_embed_public {
-								32
-							} else {
-								64
-							}
-						}],
+						&key[..{ if no_embed_public { 32 } else { 64 } }],
 						encoding,
 					)?;
 				}
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -5,20 +5,20 @@
 	iter,
 	os::unix::prelude::PermissionsExt,
 	path::{Path, PathBuf},
-	str::{from_utf8, FromStr},
+	str::{FromStr, from_utf8},
 };
 
 use age::{
-	ssh::{Identity as SshIdentity, Recipient as SshRecipient},
 	Decryptor, Encryptor, Identity, Recipient,
+	ssh::{Identity as SshIdentity, Recipient as SshRecipient},
 };
-use anyhow::{anyhow, bail, ensure, Context, Result};
+use anyhow::{Context, Result, anyhow, bail, ensure};
 use clap::Parser;
 use fleet_shared::SecretData;
-use nix::unistd::{chown, Group, User};
+use nix::unistd::{Group, User, chown};
 use serde::Deserialize;
 use tracing::{error, info, info_span};
-use tracing_subscriber::{filter::LevelFilter, EnvFilter};
+use tracing_subscriber::{EnvFilter, filter::LevelFilter};
 
 #[derive(Parser)]
 #[clap(author)]
modifiedcmds/terraform-provider-fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/terraform-provider-fleet/src/main.rs
+++ b/cmds/terraform-provider-fleet/src/main.rs
@@ -1,3 +1,5 @@
 fn main() {
-	panic!("this is a stub, real provider is in development, I just don't want to keep it in different branch.")
+	panic!(
+		"this is a stub, real provider is in development, I just don't want to keep it in different branch."
+	)
 }
modifiedcrates/better-command/src/handler.rsdiffbeforeafterboth
--- a/crates/better-command/src/handler.rs
+++ b/crates/better-command/src/handler.rs
@@ -7,7 +7,7 @@
 
 use regex::Regex;
 use serde::Deserialize;
-use tracing::{info, info_span, warn, Span};
+use tracing::{Span, info, info_span, warn};
 #[cfg(feature = "indicatif")]
 use tracing_indicatif::span_ext::IndicatifSpanExt as _;
 
@@ -112,9 +112,13 @@
 			match log {
 				NixLog::Msg { msg, raw_msg, .. } => {
 					#[allow(clippy::nonminimal_bool)]
-					if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '") && msg.ends_with("' is dirty"))
-					&& !msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake")
-					&& msg != "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy" {
+					if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '")
+						&& msg.ends_with("' is dirty"))
+						&& !msg.starts_with(
+							"\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake",
+						) && msg
+						!= "\u{1b}[35;1mwarning:\u{1b}[0m \u{1b}[31;1merror:\u{1b}[0m SQLite database '\u{1b}[35;1m/nix/var/nix/db/db.sqlite\u{1b}[0m' is busy"
+					{
 						if let Some(raw_msg) = raw_msg {
 							if !msg.is_empty() {
 								info!(target: "nix", "{}\n{}", raw_msg.trim_end(), msg.trim_end())
@@ -156,8 +160,12 @@
 					id,
 					..
 				} if typ == 100 && fields.len() >= 3 => {
-					if let [LogField::String(drv), LogField::String(from), LogField::String(to), ..] =
-						&fields[..]
+					if let [
+						LogField::String(drv),
+						LogField::String(from),
+						LogField::String(to),
+						..,
+					] = &fields[..]
 					{
 						let mut drv = drv.as_str();
 
@@ -289,8 +297,12 @@
 				}
 				NixLog::Result { fields, id, typ } if typ == 105 && fields.len() >= 4 => {
 					if let Some(span) = self.spans.get(&id) {
-						if let [LogField::Num(done), LogField::Num(expected), LogField::Num(_running), LogField::Num(_failed)] =
-							&fields[..4]
+						if let [
+							LogField::Num(done),
+							LogField::Num(expected),
+							LogField::Num(_running),
+							LogField::Num(_failed),
+						] = &fields[..4]
 						{
 							#[cfg(feature = "indicatif")]
 							{
modifiedcrates/fleet-base/src/command.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/command.rs
+++ b/crates/fleet-base/src/command.rs
@@ -1,6 +1,6 @@
 use std::{ffi::OsStr, pin, process::Stdio, sync::Arc, task::Poll};
 
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 use better_command::{Handler, NixHandler, PlainHandler};
 use futures::StreamExt;
 use itertools::Either;
@@ -68,10 +68,9 @@
 		}
 	}
 	fn new_here(&self, cmd: impl AsRef<OsStr>) -> Self {
-		if let Some(ssh_session) = self.ssh_session.clone() {
-			Self::new_on(self.escalation, cmd, ssh_session)
-		} else {
-			Self::new(self.escalation, cmd)
+		match self.ssh_session.clone() {
+			Some(ssh_session) => Self::new_on(self.escalation, cmd, ssh_session),
+			_ => Self::new(self.escalation, cmd),
 		}
 	}
 
@@ -139,15 +138,18 @@
 		out
 	}
 	fn into_command(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {
-		Ok(if let Some(session) = self.ssh_session.clone() {
-			let cmd = self.translate_env_into_env().into_command_unchecked_local();
-			Either::Right(
-				cmd.over_ssh(session)
-					.map_err(|e| anyhow!("ssh error: {e}"))?,
-			)
-		} else {
-			let cmd = self.into_command_unchecked_local();
-			Either::Left(cmd)
+		Ok(match self.ssh_session.clone() {
+			Some(session) => {
+				let cmd = self.translate_env_into_env().into_command_unchecked_local();
+				Either::Right(
+					cmd.over_ssh(session)
+						.map_err(|e| anyhow!("ssh error: {e}"))?,
+				)
+			}
+			_ => {
+				let cmd = self.into_command_unchecked_local();
+				Either::Left(cmd)
+			}
 		})
 	}
 	pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {
modifiedcrates/fleet-base/src/deploy.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/deploy.rs
+++ b/crates/fleet-base/src/deploy.rs
@@ -1,10 +1,10 @@
 use std::{path::PathBuf, time::Duration};
 
-use anyhow::{anyhow, bail, Context as _, Result};
+use anyhow::{Context as _, Result, anyhow, bail};
 use clap::ValueEnum;
 use itertools::Itertools;
 use tokio::time::sleep;
-use tracing::{error, info, info_span, warn, Instrument as _};
+use tracing::{Instrument as _, error, info, info_span, warn};
 
 use crate::host::{Config, ConfigHost, DeployKind, Generation, GenerationStorage};
 
@@ -221,7 +221,9 @@
 					.in_current_span()
 					.await
 				{
-					error!("failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}")
+					error!(
+						"failed to remove rollback marker. This is bad, as the system will be rolled back by watchdog: {e}"
+					)
 				}
 			}
 			info!("disarming watchdog, just in case");
@@ -233,12 +235,17 @@
 					error!("failed to disarm rollback run: {e}");
 				}
 			}
-		} else if let Err(_e) = host
-			.rm_file("/etc/fleet_rollback_marker", true)
-			.in_current_span()
-			.await
-		{
-			// Marker might not exist, yet better try to remove it.
+		} else {
+			match host
+				.rm_file("/etc/fleet_rollback_marker", true)
+				.in_current_span()
+				.await
+			{
+				Err(_e) => {
+					// Marker might not exist, yet better try to remove it.
+				}
+				_ => {}
+			}
 		}
 	}
 	Ok(())
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -10,7 +10,7 @@
 	distr::{Alphanumeric, SampleString as _},
 	rng,
 };
-use serde::{de::Error, Deserialize, Serialize};
+use serde::{Deserialize, Serialize, de::Error};
 use serde_json::Value;
 
 #[derive(Serialize, Deserialize, Default)]
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
before · crates/fleet-base/src/host.rs
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 tabled::Tabled;19use tempfile::NamedTempFile;20use time::{format_description, UtcDateTime};21use tracing::warn;2223use crate::{24	command::MyCommand,25	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},26};2728pub struct FleetConfigInternals {29	/// Fleet project directory, containing fleet.nix file.30	pub directory: PathBuf,31	/// builtins.currentSystem32	pub local_system: String,33	pub data: Mutex<FleetData>,34	pub nix_args: Vec<OsString>,35	/// fleet_config.config36	pub config_field: Value,37	// TODO: Remove with connectivity refactor38	pub localhost: String,3940	/// import nixpkgs {system = local};41	pub default_pkgs: Value,42	/// inputs.nixpkgs43	pub nixpkgs: Value,4445	pub nix_session: NixSession,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53	type Target = FleetConfigInternals;5455	fn deref(&self) -> &Self::Target {56		&self.057	}58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62	Sudo,63	Run0,64	Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69	/// NixOS => NixOS managed by fleet70	UpgradeToFleet,71	/// NixOS managed by fleet => NixOS managed by fleet72	Fleet,73	/// Remote host has /mnt, /mnt/boot mounted,74	/// generated config is added to fleet configuration.75	NixosInstall,76	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77	/// generated config is added to fleet configuration,78	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79	NixosLustrate,80}8182impl FromStr for DeployKind {83	type Err = anyhow::Error;84	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85		match s {86			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87			"fleet" => Ok(Self::Fleet),88			"nixos-install" => Ok(Self::NixosInstall),89			"nixos-lustrate" => Ok(Self::NixosLustrate),90			v => bail!("unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""),91		}92	}93}94pub struct ConfigHost {95	config: Config,96	pub name: String,97	groups: OnceCell<Vec<String>>,9899	deploy_kind: OnceCell<DeployKind>,100101	pub host_config: Option<Value>,102	pub nixos_config: OnceCell<Value>,103	pub nixos_unchecked_config: OnceCell<Value>,104	pub pkgs_override: Option<Value>,105106	// TODO: Move command helpers away with connectivity refactor107	pub local: bool,108	pub session: OnceLock<Arc<openssh::Session>>,109}110111#[derive(Debug, Clone, Copy)]112pub enum GenerationStorage {113	Deployer,114	Machine,115	Pusher,116}117impl GenerationStorage {118	fn prefix(&self) -> &'static str {119		match self {120			GenerationStorage::Deployer => "deployer.",121			GenerationStorage::Machine => "",122			GenerationStorage::Pusher => "pusher.",123		}124	}125}126127#[derive(Tabled, Debug)]128pub struct Generation {129	#[tabled(rename = "ID", format("{}", self.rollback_id()))]130	pub id: u32,131	#[tabled(rename = "Current")]132	pub current: bool,133	#[tabled(rename = "Created at")]134	pub datetime: UtcDateTime,135	#[tabled(format = "{:?}")]136	pub store_path: PathBuf,137	#[tabled(skip)]138	pub location: GenerationStorage,139}140impl Generation {141	pub fn rollback_id(&self) -> String {142		format!("{}{}", self.location.prefix(), self.id)143	}144}145146fn parse_generation_line(g: &str) -> Option<Generation> {147	let mut parts = g.split_whitespace();148	let id = parts.next()?;149	let id: u32 = id.parse().ok()?;150	let date = parts.next()?;151	let time = parts.next()?;152	let current = if let Some(current) = parts.next() {153		if current == "(current)" {154			Some(true)155		} else {156			None157		}158	} else {159		Some(false)160	};161	let current = current?;162	if parts.next().is_some() {163		warn!("unexpected text after generation: {g}");164	}165166	let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")167		.expect("valid format");168	let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;169170	Some(Generation {171		id,172		current,173		datetime,174		store_path: PathBuf::new(),175		location: GenerationStorage::Machine,176	})177}178// TODO: Move command helpers away with connectivity refactor179impl ConfigHost {180	pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {181		let mut cmd = self.cmd("nix-env").await?;182		cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))183			.arg("--list-generations")184			.env("TZ", "UTC");185		// Sudo is required because --list-generations tries to acquire profile lock186		let data = cmd.sudo().run_string().await?;187		let mut generations = data188			.split('\n')189			.map(|e| e.trim())190			.filter(|&l| !l.is_empty())191			.filter_map(|g| {192				let gen = parse_generation_line(g);193				if gen.is_none() {194					warn!("bad generation: {g}");195				};196				gen197			})198			.collect::<Vec<_>>();199		for ele in generations.iter_mut() {200			let mut cmd = self.cmd("readlink").await?;201			cmd.arg("--")202				.arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));203			let path = cmd.run_string().await?;204			ele.store_path = PathBuf::from(path.trim_end_matches("\n"));205		}206207		Ok(generations)208	}209210	pub fn set_deploy_kind(&self, kind: DeployKind) {211		self.deploy_kind212			.set(kind)213			.ok()214			.expect("deploy kind is already set");215	}216	pub async fn deploy_kind(&self) -> Result<DeployKind> {217		if let Some(kind) = self.deploy_kind.get() {218			return Ok(kind.clone());219		}220		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {221			Ok(v) => v,222			Err(e) => {223				bail!("failed to query remote system kind: {}", e);224			}225		};226		if !is_fleet_managed {227			bail!(indoc::indoc! {"228				host is not marked as managed by fleet229				if you're not trying to lustrate/install system from scratch,230				you should either231					1. manually create /etc/FLEET_HOST file on the target host,232					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet233					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos234			"});235		}236		// TOCTOU is possible237		let _ = self.deploy_kind.set(DeployKind::Fleet);238		Ok(self239			.deploy_kind240			.get()241			.expect("deploy kind is just set")242			.clone())243	}244	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {245		// Prefer sudo, as run0 has some gotchas with polkit246		// and too many repeating prompts.247		if (self.find_in_path("sudo").await).is_ok() {248			return Ok(EscalationStrategy::Sudo);249		}250		if (self.find_in_path("run0").await).is_ok() {251			return Ok(EscalationStrategy::Run0);252		}253		Ok(EscalationStrategy::Su)254	}255	async fn open_session(&self) -> Result<Arc<openssh::Session>> {256		assert!(!self.local, "do not open ssh connection to local session");257		// FIXME: TOCTOU258		if let Some(session) = &self.session.get() {259			return Ok((*session).clone());260		};261		let session = SessionBuilder::default();262		let session = session263			.connect(&self.name)264			.await265			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;266		let session = Arc::new(session);267		self.session.set(session.clone()).expect("TOCTOU happened");268		Ok(session)269	}270	pub async fn mktemp_dir(&self) -> Result<String> {271		let mut cmd = self.cmd("mktemp").await?;272		cmd.arg("-d");273		let path = cmd.run_string().await?;274		Ok(path.trim_end().to_owned())275	}276	pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {277		let mut cmd = self.cmd("sh").await?;278		cmd.arg("-c")279			.arg("test -e \"$1\" && echo true || echo false")280			.arg("_")281			.arg(path);282		Ok(cmd.run_value().await?)283	}284	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {285		let mut cmd = self.cmd("cat").await?;286		cmd.arg(path);287		cmd.run_bytes().await288	}289	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {290		let mut cmd = self.cmd("cat").await?;291		cmd.arg(path);292		cmd.run_string().await293	}294	pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {295		let mut cmd = self.cmd("ls").await?;296		cmd.arg(path);297		let out = cmd.run_string().await?;298		let mut lines = out.split('\n');299		if let Some(last) = lines.next_back() {300			ensure!(last.is_empty(), "output of ls should end with newline");301		}302		Ok(lines.map(ToOwned::to_owned).collect())303	}304	#[allow(dead_code)]305	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {306		let text = self.read_file_text(path).await?;307		Ok(serde_json::from_str(&text)?)308	}309	pub async fn read_env(&self, env: &str) -> Result<String> {310		let mut cmd = self.cmd("printenv").await?;311		cmd.arg(env);312		cmd.run_string().await313	}314	pub async fn find_in_path(&self, command: &str) -> Result<String> {315		// // `which` is not a part of coreutils, and it might not exist on machine.316		// let path = self.read_env("PATH").await?;317		// // Assuming delimiter is :, we don't work with windows host, this check will be much318		// // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)319		// for ele in path.split(':') {320		// 	let test_path = format!("{ele}/{cmd}");321		// 	test -x etc322		// }323		// let mut cmd = self.cmd("printenv").await?;324		// cmd.arg(env);325		// Ok(cmd.run_string().await?)326		// Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.327		let mut cmd = self328			.cmd_escalation(329				// Not used330				EscalationStrategy::Su,331				"which",332			)333			.await?;334		cmd.arg(command);335		cmd.run_string().await336	}337	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>338	where339		<D as FromStr>::Err: Display,340	{341		let text = self.read_file_text(path).await?;342		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))343	}344	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {345		self.cmd_escalation(self.escalation_strategy().await?, cmd)346			.await347	}348	pub async fn cmd_escalation(349		&self,350		escalation: EscalationStrategy,351		cmd: impl AsRef<OsStr>,352	) -> Result<MyCommand> {353		if self.local {354			Ok(MyCommand::new(escalation, cmd))355		} else {356			let session = self.open_session().await?;357			Ok(MyCommand::new_on(escalation, cmd, session))358		}359	}360	pub async fn nix_cmd(&self) -> Result<MyCommand> {361		let mut nix = self.cmd("nix").await?;362		nix.args([363			"--extra-experimental-features",364			"nix-command",365			"--extra-experimental-features",366			"flakes",367		]);368		Ok(nix)369	}370371	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {372		ensure!(data.encrypted, "secret is not encrypted");373		let mut cmd = self.cmd("fleet-install-secrets").await?;374		cmd.arg("decrypt").eqarg("--secret", data.to_string());375		let encoded = cmd376			.sudo()377			.run_string()378			.await379			.context("failed to call remote host for decrypt")?;380		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;381		ensure!(!data.encrypted, "secret came out encrypted");382		Ok(data.data)383	}384	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {385		ensure!(data.encrypted, "secret is not encrypted");386		let mut cmd = self.cmd("fleet-install-secrets").await?;387		cmd.arg("reencrypt").eqarg("--secret", data.to_string());388		for target in targets {389			let key = self.config.key(&target).await?;390			cmd.eqarg("--targets", key);391		}392		let encoded = cmd393			.sudo()394			.run_string()395			.await396			.context("failed to call remote host for decrypt")?;397		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;398		ensure!(data.encrypted, "secret came out not encrypted");399		Ok(data)400	}401	/// Returns path for futureproofing, as path might change i.e on conversion to CA402	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {403		if self.local {404			// Path is located locally, thus already trusted.405			return Ok(path.to_owned());406		}407		let mut nix = MyCommand::new(408			// Not used409			EscalationStrategy::Su,410			"nix",411		);412		nix.arg("copy").arg("--substitute-on-destination");413414		match self.deploy_kind().await? {415			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {416				nix.comparg("--to", format!("ssh-ng://{}", self.name));417			}418			DeployKind::NixosInstall => {419				nix420					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon421					.arg("--no-check-sigs")422					.comparg(423						"--to",424						format!("ssh-ng://root@{}?remote-store=/mnt", self.name),425					);426			}427		}428		nix.arg(path);429		nix.run_nix().await.context("nix copy")?;430		Ok(path.to_owned())431	}432	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {433		let mut cmd = self.cmd("systemctl").await?;434		cmd.arg("stop").arg(name);435		cmd.sudo().run().await436	}437	pub async fn systemctl_start(&self, name: &str) -> Result<()> {438		let mut cmd = self.cmd("systemctl").await?;439		cmd.arg("start").arg(name);440		cmd.sudo().run().await441	}442443	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {444		let mut cmd = self.cmd("rm").await?;445		cmd.arg("-f").arg(path);446		if sudo {447			cmd = cmd.sudo()448		}449		cmd.run().await450	}451}452impl ConfigHost {453	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,454	// assuming getting tags always returns the same value.455	pub async fn tags(&self) -> Result<Vec<String>> {456		if let Some(v) = self.groups.get() {457			return Ok(v.clone());458		}459		let Some(host_config) = &self.host_config else {460			return Ok(vec![]);461		};462		let tags: Vec<String> = nix_go_json!(host_config.tags);463464		let _ = self.groups.set(tags.clone());465466		Ok(tags)467	}468	pub async fn nixos_config(&self) -> Result<Value> {469		if let Some(v) = self.nixos_config.get() {470			return Ok(v.clone());471		}472		let Some(host_config) = &self.host_config else {473			bail!("local host has no nixos_config");474		};475		let nixos_config = nix_go!(host_config.nixos.config);476		assert_warn("nixos config evaluation", &nixos_config).await?;477478		let _ = self.nixos_config.set(nixos_config.clone());479480		Ok(nixos_config)481	}482	pub async fn nixos_unchecked_config(&self) -> Result<Value> {483		if let Some(v) = self.nixos_unchecked_config.get() {484			return Ok(v.clone());485		}486		let Some(host_config) = &self.host_config else {487			bail!("local host has no nixos_config");488		};489		let nixos_config = nix_go!(host_config.nixos_unchecked.config);490491		let _ = self.nixos_unchecked_config.set(nixos_config.clone());492493		Ok(nixos_config)494	}495496	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {497		let nixos = self.nixos_unchecked_config().await?;498		let secrets = nix_go!(nixos.secrets);499		let mut out = Vec::new();500		for name in secrets.list_fields().await? {501			let secret = nix_go!(secrets[{ name }]);502			let is_shared: bool = nix_go_json!(secret.shared);503			if is_shared {504				continue;505			}506			out.push(name);507		}508		Ok(out)509	}510	pub async fn secret_field(&self, name: &str) -> Result<Value> {511		let nixos = self.nixos_unchecked_config().await?;512		Ok(nix_go!(nixos.secrets[{ name }]))513	}514515	/// Packages for this host, resolved with nixpkgs overlays516	pub async fn pkgs(&self) -> Result<Value> {517		if let Some(value) = &self.pkgs_override {518			return Ok(value.clone());519		}520		let Some(host_config) = &self.host_config else {521			bail!("local host has no host_config");522		};523		// TODO: Should nixos.options be cached?524		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))525	}526}527528impl Config {529	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {530		let config = &self.config_field;531		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);532		Ok(tagged)533	}534	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {535		let mut out = BTreeSet::new();536		for owner in owners {537			if let Some(tag) = owner.strip_prefix('@') {538				let hosts = self.tagged_hostnames(tag).await?;539				out.extend(hosts);540			} else {541				out.insert(owner);542			}543		}544		Ok(out)545	}546	pub fn local_host(&self) -> ConfigHost {547		ConfigHost {548			config: self.clone(),549			name: "<virtual localhost>".to_owned(),550			host_config: None,551			nixos_config: OnceCell::new(),552			nixos_unchecked_config: OnceCell::new(),553			groups: {554				let cell = OnceCell::new();555				let _ = cell.set(vec![]);556				cell557			},558			pkgs_override: Some(self.default_pkgs.clone()),559560			local: true,561			session: OnceLock::new(),562			deploy_kind: OnceCell::new(),563		}564	}565566	pub async fn host(&self, name: &str) -> Result<ConfigHost> {567		let config = &self.config_field;568		let host_config = nix_go!(config.hosts[{ name }]);569570		Ok(ConfigHost {571			config: self.clone(),572			name: name.to_owned(),573			host_config: Some(host_config),574			nixos_config: OnceCell::new(),575			nixos_unchecked_config: OnceCell::new(),576			groups: OnceCell::new(),577			pkgs_override: None,578579			// TODO: Remove with connectivit refactor580			local: self.localhost == name,581			session: OnceLock::new(),582			deploy_kind: OnceCell::new(),583		})584	}585	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {586		let config = &self.config_field;587		let names = nix_go!(config.hosts).list_fields().await?;588		let mut out = vec![];589		for name in names {590			out.push(self.host(&name).await?);591		}592		Ok(out)593	}594	// TODO: Replace usages with .host().nixos_config595	pub async fn system_config(&self, host: &str) -> Result<Value> {596		let fleet_field = &self.config_field;597		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))598	}599600	/// Shared secrets configured in fleet.nix or in flake601	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {602		let config_field = &self.config_field;603		Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)604	}605	/// Shared secrets configured in fleet.nix606	pub fn list_shared(&self) -> Vec<String> {607		let data = self.data();608		data.shared_secrets.keys().cloned().collect()609	}610	pub fn has_shared(&self, name: &str) -> bool {611		let data = self.data();612		data.shared_secrets.contains_key(name)613	}614	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {615		let mut data = self.data_mut();616		data.shared_secrets.insert(name.to_owned(), shared);617	}618	pub fn remove_shared(&self, secret: &str) {619		let mut data = self.data_mut();620		data.shared_secrets.remove(secret);621	}622623	pub fn list_secrets(&self, host: &str) -> Vec<String> {624		let data = self.data();625		let Some(secrets) = data.host_secrets.get(host) else {626			return Vec::new();627		};628		secrets.keys().cloned().collect()629	}630631	pub fn has_secret(&self, host: &str, secret: &str) -> bool {632		let data = self.data();633		let Some(host_secrets) = data.host_secrets.get(host) else {634			return false;635		};636		host_secrets.contains_key(secret)637	}638	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {639		let mut data = self.data_mut();640		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();641		host_secrets.insert(secret, value);642	}643644	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {645		let data = self.data();646		let Some(host_secrets) = data.host_secrets.get(host) else {647			bail!("no secrets for machine {host}");648		};649		let Some(secret) = host_secrets.get(secret) else {650			bail!("machine {host} has no secret {secret}");651		};652		Ok(secret.clone())653	}654	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {655		let data = self.data();656		let Some(secret) = data.shared_secrets.get(secret) else {657			bail!("no shared secret {secret}");658		};659		Ok(secret.clone())660	}661	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {662		let config_field = &self.config_field;663		Ok(nix_go_json!(664			config_field.sharedSecrets[{ secret }].expectedOwners665		))666	}667668	// TODO: Should this be something modifiable from other processes?669	// E.g terraform provider might want to update FleetData (e.g secrets),670	// and current implementation assumes only one process holds current fleet.nix671	// Given that it is no longer needs to be a file for nix evaluation,672	// maybe it can be a .nix file for persistence, but accessible only673	// thru some shared state controller? Might it be stored in terraform674	// state provider?675	pub fn data(&self) -> MutexGuard<FleetData> {676		self.data.lock().unwrap()677	}678	pub fn data_mut(&self) -> MutexGuard<FleetData> {679		self.data.lock().unwrap()680	}681	pub fn save(&self) -> Result<()> {682		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.")?;683		let data = nixlike::serialize(&self.data() as &FleetData)?;684		tempfile.write_all(685			format!(686				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",687				data688			)689			.as_bytes(),690		)?;691		let mut fleet_data_path = self.directory.clone();692		fleet_data_path.push("fleet.nix");693		tempfile.persist(fleet_data_path)?;694		Ok(())695	}696}
after · crates/fleet-base/src/host.rs
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::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{NixSession, Value, nix_go, nix_go_json, util::assert_warn};16use openssh::SessionBuilder;17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24	command::MyCommand,25	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},26};2728pub struct FleetConfigInternals {29	/// Fleet project directory, containing fleet.nix file.30	pub directory: PathBuf,31	/// builtins.currentSystem32	pub local_system: String,33	pub data: Mutex<FleetData>,34	pub nix_args: Vec<OsString>,35	/// fleet_config.config36	pub config_field: Value,37	// TODO: Remove with connectivity refactor38	pub localhost: String,3940	/// import nixpkgs {system = local};41	pub default_pkgs: Value,42	/// inputs.nixpkgs43	pub nixpkgs: Value,4445	pub nix_session: NixSession,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53	type Target = FleetConfigInternals;5455	fn deref(&self) -> &Self::Target {56		&self.057	}58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62	Sudo,63	Run0,64	Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69	/// NixOS => NixOS managed by fleet70	UpgradeToFleet,71	/// NixOS managed by fleet => NixOS managed by fleet72	Fleet,73	/// Remote host has /mnt, /mnt/boot mounted,74	/// generated config is added to fleet configuration.75	NixosInstall,76	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77	/// generated config is added to fleet configuration,78	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79	NixosLustrate,80}8182impl FromStr for DeployKind {83	type Err = anyhow::Error;84	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85		match s {86			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87			"fleet" => Ok(Self::Fleet),88			"nixos-install" => Ok(Self::NixosInstall),89			"nixos-lustrate" => Ok(Self::NixosLustrate),90			v => bail!(91				"unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""92			),93		}94	}95}96pub struct ConfigHost {97	config: Config,98	pub name: String,99	groups: OnceCell<Vec<String>>,100101	deploy_kind: OnceCell<DeployKind>,102103	pub host_config: Option<Value>,104	pub nixos_config: OnceCell<Value>,105	pub nixos_unchecked_config: OnceCell<Value>,106	pub pkgs_override: Option<Value>,107108	// TODO: Move command helpers away with connectivity refactor109	pub local: bool,110	pub session: OnceLock<Arc<openssh::Session>>,111}112113#[derive(Debug, Clone, Copy)]114pub enum GenerationStorage {115	Deployer,116	Machine,117	Pusher,118}119impl GenerationStorage {120	fn prefix(&self) -> &'static str {121		match self {122			GenerationStorage::Deployer => "deployer.",123			GenerationStorage::Machine => "",124			GenerationStorage::Pusher => "pusher.",125		}126	}127}128129#[derive(Tabled, Debug)]130pub struct Generation {131	#[tabled(rename = "ID", format("{}", self.rollback_id()))]132	pub id: u32,133	#[tabled(rename = "Current")]134	pub current: bool,135	#[tabled(rename = "Created at")]136	pub datetime: UtcDateTime,137	#[tabled(format = "{:?}")]138	pub store_path: PathBuf,139	#[tabled(skip)]140	pub location: GenerationStorage,141}142impl Generation {143	pub fn rollback_id(&self) -> String {144		format!("{}{}", self.location.prefix(), self.id)145	}146}147148fn parse_generation_line(g: &str) -> Option<Generation> {149	let mut parts = g.split_whitespace();150	let id = parts.next()?;151	let id: u32 = id.parse().ok()?;152	let date = parts.next()?;153	let time = parts.next()?;154	let current = if let Some(current) = parts.next() {155		if current == "(current)" {156			Some(true)157		} else {158			None159		}160	} else {161		Some(false)162	};163	let current = current?;164	if parts.next().is_some() {165		warn!("unexpected text after generation: {g}");166	}167168	let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")169		.expect("valid format");170	let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;171172	Some(Generation {173		id,174		current,175		datetime,176		store_path: PathBuf::new(),177		location: GenerationStorage::Machine,178	})179}180// TODO: Move command helpers away with connectivity refactor181impl ConfigHost {182	pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {183		let mut cmd = self.cmd("nix-env").await?;184		cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))185			.arg("--list-generations")186			.env("TZ", "UTC");187		// Sudo is required because --list-generations tries to acquire profile lock188		let data = cmd.sudo().run_string().await?;189		let mut generations = data190			.split('\n')191			.map(|e| e.trim())192			.filter(|&l| !l.is_empty())193			.filter_map(|g| {194				let generation = parse_generation_line(g);195				if generation.is_none() {196					warn!("bad generation: {g}");197				};198				generation199			})200			.collect::<Vec<_>>();201		for ele in generations.iter_mut() {202			let mut cmd = self.cmd("readlink").await?;203			cmd.arg("--")204				.arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));205			let path = cmd.run_string().await?;206			ele.store_path = PathBuf::from(path.trim_end_matches("\n"));207		}208209		Ok(generations)210	}211212	pub fn set_deploy_kind(&self, kind: DeployKind) {213		self.deploy_kind214			.set(kind)215			.ok()216			.expect("deploy kind is already set");217	}218	pub async fn deploy_kind(&self) -> Result<DeployKind> {219		if let Some(kind) = self.deploy_kind.get() {220			return Ok(kind.clone());221		}222		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {223			Ok(v) => v,224			Err(e) => {225				bail!("failed to query remote system kind: {}", e);226			}227		};228		if !is_fleet_managed {229			bail!(indoc::indoc! {"230				host is not marked as managed by fleet231				if you're not trying to lustrate/install system from scratch,232				you should either233					1. manually create /etc/FLEET_HOST file on the target host,234					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet235					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos236			"});237		}238		// TOCTOU is possible239		let _ = self.deploy_kind.set(DeployKind::Fleet);240		Ok(self241			.deploy_kind242			.get()243			.expect("deploy kind is just set")244			.clone())245	}246	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {247		// Prefer sudo, as run0 has some gotchas with polkit248		// and too many repeating prompts.249		if (self.find_in_path("sudo").await).is_ok() {250			return Ok(EscalationStrategy::Sudo);251		}252		if (self.find_in_path("run0").await).is_ok() {253			return Ok(EscalationStrategy::Run0);254		}255		Ok(EscalationStrategy::Su)256	}257	async fn open_session(&self) -> Result<Arc<openssh::Session>> {258		assert!(!self.local, "do not open ssh connection to local session");259		// FIXME: TOCTOU260		if let Some(session) = &self.session.get() {261			return Ok((*session).clone());262		};263		let session = SessionBuilder::default();264		let session = session265			.connect(&self.name)266			.await267			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;268		let session = Arc::new(session);269		self.session.set(session.clone()).expect("TOCTOU happened");270		Ok(session)271	}272	pub async fn mktemp_dir(&self) -> Result<String> {273		let mut cmd = self.cmd("mktemp").await?;274		cmd.arg("-d");275		let path = cmd.run_string().await?;276		Ok(path.trim_end().to_owned())277	}278	pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {279		let mut cmd = self.cmd("sh").await?;280		cmd.arg("-c")281			.arg("test -e \"$1\" && echo true || echo false")282			.arg("_")283			.arg(path);284		Ok(cmd.run_value().await?)285	}286	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {287		let mut cmd = self.cmd("cat").await?;288		cmd.arg(path);289		cmd.run_bytes().await290	}291	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {292		let mut cmd = self.cmd("cat").await?;293		cmd.arg(path);294		cmd.run_string().await295	}296	pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {297		let mut cmd = self.cmd("ls").await?;298		cmd.arg(path);299		let out = cmd.run_string().await?;300		let mut lines = out.split('\n');301		if let Some(last) = lines.next_back() {302			ensure!(last.is_empty(), "output of ls should end with newline");303		}304		Ok(lines.map(ToOwned::to_owned).collect())305	}306	#[allow(dead_code)]307	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {308		let text = self.read_file_text(path).await?;309		Ok(serde_json::from_str(&text)?)310	}311	pub async fn read_env(&self, env: &str) -> Result<String> {312		let mut cmd = self.cmd("printenv").await?;313		cmd.arg(env);314		cmd.run_string().await315	}316	pub async fn find_in_path(&self, command: &str) -> Result<String> {317		// // `which` is not a part of coreutils, and it might not exist on machine.318		// let path = self.read_env("PATH").await?;319		// // Assuming delimiter is :, we don't work with windows host, this check will be much320		// // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)321		// for ele in path.split(':') {322		// 	let test_path = format!("{ele}/{cmd}");323		// 	test -x etc324		// }325		// let mut cmd = self.cmd("printenv").await?;326		// cmd.arg(env);327		// Ok(cmd.run_string().await?)328		// Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.329		let mut cmd = self330			.cmd_escalation(331				// Not used332				EscalationStrategy::Su,333				"which",334			)335			.await?;336		cmd.arg(command);337		cmd.run_string().await338	}339	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>340	where341		<D as FromStr>::Err: Display,342	{343		let text = self.read_file_text(path).await?;344		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))345	}346	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {347		self.cmd_escalation(self.escalation_strategy().await?, cmd)348			.await349	}350	pub async fn cmd_escalation(351		&self,352		escalation: EscalationStrategy,353		cmd: impl AsRef<OsStr>,354	) -> Result<MyCommand> {355		if self.local {356			Ok(MyCommand::new(escalation, cmd))357		} else {358			let session = self.open_session().await?;359			Ok(MyCommand::new_on(escalation, cmd, session))360		}361	}362	pub async fn nix_cmd(&self) -> Result<MyCommand> {363		let mut nix = self.cmd("nix").await?;364		nix.args([365			"--extra-experimental-features",366			"nix-command",367			"--extra-experimental-features",368			"flakes",369		]);370		Ok(nix)371	}372373	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {374		ensure!(data.encrypted, "secret is not encrypted");375		let mut cmd = self.cmd("fleet-install-secrets").await?;376		cmd.arg("decrypt").eqarg("--secret", data.to_string());377		let encoded = cmd378			.sudo()379			.run_string()380			.await381			.context("failed to call remote host for decrypt")?;382		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;383		ensure!(!data.encrypted, "secret came out encrypted");384		Ok(data.data)385	}386	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {387		ensure!(data.encrypted, "secret is not encrypted");388		let mut cmd = self.cmd("fleet-install-secrets").await?;389		cmd.arg("reencrypt").eqarg("--secret", data.to_string());390		for target in targets {391			let key = self.config.key(&target).await?;392			cmd.eqarg("--targets", key);393		}394		let encoded = cmd395			.sudo()396			.run_string()397			.await398			.context("failed to call remote host for decrypt")?;399		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;400		ensure!(data.encrypted, "secret came out not encrypted");401		Ok(data)402	}403	/// Returns path for futureproofing, as path might change i.e on conversion to CA404	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {405		if self.local {406			// Path is located locally, thus already trusted.407			return Ok(path.to_owned());408		}409		let mut nix = MyCommand::new(410			// Not used411			EscalationStrategy::Su,412			"nix",413		);414		nix.arg("copy").arg("--substitute-on-destination");415416		match self.deploy_kind().await? {417			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {418				nix.comparg("--to", format!("ssh-ng://{}", self.name));419			}420			DeployKind::NixosInstall => {421				nix422					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon423					.arg("--no-check-sigs")424					.comparg(425						"--to",426						format!("ssh-ng://root@{}?remote-store=/mnt", self.name),427					);428			}429		}430		nix.arg(path);431		nix.run_nix().await.context("nix copy")?;432		Ok(path.to_owned())433	}434	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {435		let mut cmd = self.cmd("systemctl").await?;436		cmd.arg("stop").arg(name);437		cmd.sudo().run().await438	}439	pub async fn systemctl_start(&self, name: &str) -> Result<()> {440		let mut cmd = self.cmd("systemctl").await?;441		cmd.arg("start").arg(name);442		cmd.sudo().run().await443	}444445	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {446		let mut cmd = self.cmd("rm").await?;447		cmd.arg("-f").arg(path);448		if sudo {449			cmd = cmd.sudo()450		}451		cmd.run().await452	}453}454impl ConfigHost {455	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,456	// assuming getting tags always returns the same value.457	pub async fn tags(&self) -> Result<Vec<String>> {458		if let Some(v) = self.groups.get() {459			return Ok(v.clone());460		}461		let Some(host_config) = &self.host_config else {462			return Ok(vec![]);463		};464		let tags: Vec<String> = nix_go_json!(host_config.tags);465466		let _ = self.groups.set(tags.clone());467468		Ok(tags)469	}470	pub async fn nixos_config(&self) -> Result<Value> {471		if let Some(v) = self.nixos_config.get() {472			return Ok(v.clone());473		}474		let Some(host_config) = &self.host_config else {475			bail!("local host has no nixos_config");476		};477		let nixos_config = nix_go!(host_config.nixos.config);478		assert_warn("nixos config evaluation", &nixos_config).await?;479480		let _ = self.nixos_config.set(nixos_config.clone());481482		Ok(nixos_config)483	}484	pub async fn nixos_unchecked_config(&self) -> Result<Value> {485		if let Some(v) = self.nixos_unchecked_config.get() {486			return Ok(v.clone());487		}488		let Some(host_config) = &self.host_config else {489			bail!("local host has no nixos_config");490		};491		let nixos_config = nix_go!(host_config.nixos_unchecked.config);492493		let _ = self.nixos_unchecked_config.set(nixos_config.clone());494495		Ok(nixos_config)496	}497498	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {499		let nixos = self.nixos_unchecked_config().await?;500		let secrets = nix_go!(nixos.secrets);501		let mut out = Vec::new();502		for name in secrets.list_fields().await? {503			let secret = nix_go!(secrets[{ name }]);504			let is_shared: bool = nix_go_json!(secret.shared);505			if is_shared {506				continue;507			}508			out.push(name);509		}510		Ok(out)511	}512	pub async fn secret_field(&self, name: &str) -> Result<Value> {513		let nixos = self.nixos_unchecked_config().await?;514		Ok(nix_go!(nixos.secrets[{ name }]))515	}516517	/// Packages for this host, resolved with nixpkgs overlays518	pub async fn pkgs(&self) -> Result<Value> {519		if let Some(value) = &self.pkgs_override {520			return Ok(value.clone());521		}522		let Some(host_config) = &self.host_config else {523			bail!("local host has no host_config");524		};525		// TODO: Should nixos.options be cached?526		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))527	}528}529530impl Config {531	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {532		let config = &self.config_field;533		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);534		Ok(tagged)535	}536	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {537		let mut out = BTreeSet::new();538		for owner in owners {539			if let Some(tag) = owner.strip_prefix('@') {540				let hosts = self.tagged_hostnames(tag).await?;541				out.extend(hosts);542			} else {543				out.insert(owner);544			}545		}546		Ok(out)547	}548	pub fn local_host(&self) -> ConfigHost {549		ConfigHost {550			config: self.clone(),551			name: "<virtual localhost>".to_owned(),552			host_config: None,553			nixos_config: OnceCell::new(),554			nixos_unchecked_config: OnceCell::new(),555			groups: {556				let cell = OnceCell::new();557				let _ = cell.set(vec![]);558				cell559			},560			pkgs_override: Some(self.default_pkgs.clone()),561562			local: true,563			session: OnceLock::new(),564			deploy_kind: OnceCell::new(),565		}566	}567568	pub async fn host(&self, name: &str) -> Result<ConfigHost> {569		let config = &self.config_field;570		let host_config = nix_go!(config.hosts[{ name }]);571572		Ok(ConfigHost {573			config: self.clone(),574			name: name.to_owned(),575			host_config: Some(host_config),576			nixos_config: OnceCell::new(),577			nixos_unchecked_config: OnceCell::new(),578			groups: OnceCell::new(),579			pkgs_override: None,580581			// TODO: Remove with connectivit refactor582			local: self.localhost == name,583			session: OnceLock::new(),584			deploy_kind: OnceCell::new(),585		})586	}587	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {588		let config = &self.config_field;589		let names = nix_go!(config.hosts).list_fields().await?;590		let mut out = vec![];591		for name in names {592			out.push(self.host(&name).await?);593		}594		Ok(out)595	}596	// TODO: Replace usages with .host().nixos_config597	pub async fn system_config(&self, host: &str) -> Result<Value> {598		let fleet_field = &self.config_field;599		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))600	}601602	/// Shared secrets configured in fleet.nix or in flake603	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {604		let config_field = &self.config_field;605		Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)606	}607	/// Shared secrets configured in fleet.nix608	pub fn list_shared(&self) -> Vec<String> {609		let data = self.data();610		data.shared_secrets.keys().cloned().collect()611	}612	pub fn has_shared(&self, name: &str) -> bool {613		let data = self.data();614		data.shared_secrets.contains_key(name)615	}616	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {617		let mut data = self.data_mut();618		data.shared_secrets.insert(name.to_owned(), shared);619	}620	pub fn remove_shared(&self, secret: &str) {621		let mut data = self.data_mut();622		data.shared_secrets.remove(secret);623	}624625	pub fn list_secrets(&self, host: &str) -> Vec<String> {626		let data = self.data();627		let Some(secrets) = data.host_secrets.get(host) else {628			return Vec::new();629		};630		secrets.keys().cloned().collect()631	}632633	pub fn has_secret(&self, host: &str, secret: &str) -> bool {634		let data = self.data();635		let Some(host_secrets) = data.host_secrets.get(host) else {636			return false;637		};638		host_secrets.contains_key(secret)639	}640	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {641		let mut data = self.data_mut();642		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();643		host_secrets.insert(secret, value);644	}645646	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {647		let data = self.data();648		let Some(host_secrets) = data.host_secrets.get(host) else {649			bail!("no secrets for machine {host}");650		};651		let Some(secret) = host_secrets.get(secret) else {652			bail!("machine {host} has no secret {secret}");653		};654		Ok(secret.clone())655	}656	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {657		let data = self.data();658		let Some(secret) = data.shared_secrets.get(secret) else {659			bail!("no shared secret {secret}");660		};661		Ok(secret.clone())662	}663	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {664		let config_field = &self.config_field;665		Ok(nix_go_json!(666			config_field.sharedSecrets[{ secret }].expectedOwners667		))668	}669670	// TODO: Should this be something modifiable from other processes?671	// E.g terraform provider might want to update FleetData (e.g secrets),672	// and current implementation assumes only one process holds current fleet.nix673	// Given that it is no longer needs to be a file for nix evaluation,674	// maybe it can be a .nix file for persistence, but accessible only675	// thru some shared state controller? Might it be stored in terraform676	// state provider?677	pub fn data(&self) -> MutexGuard<FleetData> {678		self.data.lock().unwrap()679	}680	pub fn data_mut(&self) -> MutexGuard<FleetData> {681		self.data.lock().unwrap()682	}683	pub fn save(&self) -> Result<()> {684		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.")?;685		let data = nixlike::serialize(&self.data() as &FleetData)?;686		tempfile.write_all(687			format!(688				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",689				data690			)691			.as_bytes(),692		)?;693		let mut fleet_data_path = self.directory.clone();694		fleet_data_path.push("fleet.nix");695		tempfile.persist(fleet_data_path)?;696		Ok(())697	}698}
modifiedcrates/fleet-base/src/keys.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -1,7 +1,7 @@
 use std::str::FromStr as _;
 
 use age::Recipient;
-use anyhow::{anyhow, Result};
+use anyhow::{Result, anyhow};
 use futures::{StreamExt as _, TryStreamExt as _};
 use itertools::Itertools as _;
 use tracing::warn;
@@ -39,12 +39,12 @@
 		}
 	}
 	/// Insecure, requires root
-	pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient> {
+	pub async fn recipient(&self, host: &str) -> anyhow::Result<impl Recipient + use<>> {
 		let key = self.key(host).await?;
 		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
 	}
 
-	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient>> {
+	pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<impl Recipient + use<>>> {
 		let hosts = self.expand_owner_set(hosts).await?;
 		futures::stream::iter(hosts.iter())
 			.then(|m| self.recipient(m.as_ref()))
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -1,6 +1,6 @@
 pub mod command;
+pub mod deploy;
 pub mod fleetdata;
 pub mod host;
 mod keys;
 pub mod opts;
-pub mod deploy;
\ No newline at end of file
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -6,15 +6,15 @@
 	sync::{Arc, Mutex},
 };
 
-use anyhow::{bail, Context, Result};
-use nix_eval::{nix_go, util::assert_warn, NixSessionPool, Value};
+use anyhow::{Context, Result, bail};
+use nix_eval::{NixSessionPool, Value, nix_go, util::assert_warn};
 use nom::{
+	Parser,
 	bytes::complete::take_while1,
 	character::complete::char,
 	combinator::{map, opt},
 	multi::separated_list1,
 	sequence::{preceded, separated_pair},
-	Parser,
 };
 
 use crate::{
@@ -44,7 +44,8 @@
 	let (input, name) = map(
 		take_while1(|v| v != ',' && v != '?' && v != '@'),
 		str::to_owned,
-	).parse_complete(input)
+	)
+	.parse_complete(input)
 	.map_err(err_to_string)?;
 
 	let kw_item = separated_pair(
modifiedcrates/fleet-shared/src/encoding.rsdiffbeforeafterboth
--- a/crates/fleet-shared/src/encoding.rs
+++ b/crates/fleet-shared/src/encoding.rs
@@ -3,8 +3,8 @@
 	str::FromStr,
 };
 
-use base64::engine::{general_purpose::STANDARD_NO_PAD, Engine};
-use serde::{de::Error, Deserialize, Deserializer, Serialize};
+use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};
+use serde::{Deserialize, Deserializer, Serialize, de::Error};
 use unicode_categories::UnicodeCategories;
 
 #[derive(Debug, PartialEq, Clone)]
modifiedcrates/nix-eval/src/pool.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/pool.rs
+++ b/crates/nix-eval/src/pool.rs
@@ -5,7 +5,7 @@
 
 use r2d2::Pool;
 
-use crate::{session::NixSessionInner, Error, NixSession, Result};
+use crate::{Error, NixSession, Result, session::NixSessionInner};
 
 pub struct NixSessionPool(Pool<NixSessionPoolInner>);
 impl NixSessionPool {
modifiedcrates/nix-eval/src/session.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/session.rs
+++ b/crates/nix-eval/src/session.rs
@@ -3,16 +3,16 @@
 use better_command::{ClonableHandler, Handler, NixHandler, NoopHandler};
 use futures::StreamExt;
 use itertools::Itertools as _;
-use serde::{de::DeserializeOwned, Deserialize};
+use serde::{Deserialize, de::DeserializeOwned};
 use thiserror::Error;
 use tokio::{
 	io::AsyncWriteExt,
 	process::{ChildStderr, ChildStdin, ChildStdout, Command},
 	select,
-	sync::{mpsc, oneshot, Mutex},
+	sync::{Mutex, mpsc, oneshot},
 };
 use tokio_util::codec::{FramedRead, LinesCodec};
-use tracing::{debug, error, warn, Level};
+use tracing::{Level, debug, error, warn};
 
 #[derive(Error, Debug, Clone)]
 pub enum Error {
modifiedcrates/nix-eval/src/util.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -3,7 +3,7 @@
 use anyhow::bail;
 use tracing::{debug, warn};
 
-use crate::{nix_go_json, Value};
+use crate::{Value, nix_go_json};
 
 #[tracing::instrument(level = "info", skip(val))]
 pub async fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
modifiedcrates/nix-eval/src/value.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/value.rs
+++ b/crates/nix-eval/src/value.rs
@@ -1,9 +1,9 @@
 use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc};
 
 use better_command::NixHandler;
-use serde::{de::DeserializeOwned, Serialize};
+use serde::{Serialize, de::DeserializeOwned};
 
-use crate::{macros::NixExprBuilder, nix_go, Error, NixBuildBatch, NixSession, Result};
+use crate::{Error, NixBuildBatch, NixSession, Result, macros::NixExprBuilder, nix_go};
 
 #[derive(Clone)]
 pub enum Index {
deletedcrates/nix-native-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-native-eval/Cargo.toml
+++ /dev/null
@@ -1,10 +0,0 @@
-[package]
-name = "nix-native-eval"
-version.workspace = true
-edition.workspace = true
-rust-version.workspace = true
-
-[dependencies]
-anyhow.workspace = true
-
-nixrs = { git = "https://github.com/Anillc/nixrs", version = "0.1.0" }
deletedcrates/nix-native-eval/src/lib.rsdiffbeforeafterboth
--- a/crates/nix-native-eval/src/lib.rs
+++ /dev/null
@@ -1,11 +0,0 @@
-use anyhow::Result;
-use nixrs::{State, Store};
-
-pub fn init() -> Result<()> {
-	nixrs::init()?;
-	let store = Store::new("daemon")?;
-	let state = State::new(store)?;
-	let _ = state;
-
-	Ok(())
-}
modifiedcrates/nixlike/fuzz/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/fuzz/Cargo.toml
+++ b/crates/nixlike/fuzz/Cargo.toml
@@ -4,7 +4,7 @@
 version = "0.0.0"
 authors = ["Automatically generated"]
 publish = false
-edition = "2021"
+edition = "2024"
 
 [package.metadata]
 cargo-fuzz = true
modifiedcrates/nixlike/src/de_impl.rsdiffbeforeafterboth
--- a/crates/nixlike/src/de_impl.rs
+++ b/crates/nixlike/src/de_impl.rs
@@ -2,8 +2,8 @@
 
 use linked_hash_map::LinkedHashMap;
 use serde::{
+	Deserializer,
 	de::{self, MapAccess, SeqAccess},
-	Deserializer,
 };
 
 use crate::{Error, Value};
@@ -28,11 +28,12 @@
 	where
 		K: de::DeserializeSeed<'de>,
 	{
-		if let Some((k, v)) = self.iter.next() {
-			let _ = self.value.insert(v);
-			Ok(Some(seed.deserialize(Value::String(k))?))
-		} else {
-			Ok(None)
+		match self.iter.next() {
+			Some((k, v)) => {
+				let _ = self.value.insert(v);
+				Ok(Some(seed.deserialize(Value::String(k))?))
+			}
+			_ => Ok(None),
 		}
 	}
 
@@ -62,10 +63,9 @@
 	where
 		T: de::DeserializeSeed<'de>,
 	{
-		if let Some(v) = self.iter.next() {
-			Ok(Some(seed.deserialize(v)?))
-		} else {
-			Ok(None)
+		match self.iter.next() {
+			Some(v) => Ok(Some(seed.deserialize(v)?)),
+			_ => Ok(None),
 		}
 	}
 }
modifiedcrates/nixlike/src/se_impl.rsdiffbeforeafterboth
--- a/crates/nixlike/src/se_impl.rs
+++ b/crates/nixlike/src/se_impl.rs
@@ -2,11 +2,11 @@
 
 use linked_hash_map::LinkedHashMap;
 use serde::{
+	Serializer,
 	ser::{
 		self, SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple,
 		SerializeTupleStruct, SerializeTupleVariant,
 	},
-	Serializer,
 };
 
 use crate::{Error, Value};
@@ -90,9 +90,7 @@
 
 	fn end(self) -> Result<Self::Ok, Self::Error> {
 		Ok(Value::Object(
-			vec![(self.0, Value::Array(self.1 .0))]
-				.into_iter()
-				.collect(),
+			vec![(self.0, Value::Array(self.1.0))].into_iter().collect(),
 		))
 	}
 }
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "crane": {
       "locked": {
-        "lastModified": 1739936662,
-        "narHash": "sha256-x4syUjNUuRblR07nDPeLDP7DpphaBVbUaSoeZkFbGSk=",
+        "lastModified": 1750266157,
+        "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "19de14aaeb869287647d9461cbd389187d8ecdb7",
+        "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48",
         "type": "github"
       },
       "original": {
@@ -22,11 +22,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1738453229,
-        "narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=",
+        "lastModified": 1749398372,
+        "narHash": "sha256-tYBdgS56eXYaWVW3fsnPQ/nFlgWi/Z2Ymhyu21zVM98=",
         "owner": "hercules-ci",
         "repo": "flake-parts",
-        "rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd",
+        "rev": "9305fe4e5c2a6fcf5ba6a3ff155720fbe4076569",
         "type": "github"
       },
       "original": {
@@ -37,16 +37,16 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1740339700,
-        "narHash": "sha256-cbrw7EgQhcdFnu6iS3vane53bEagZQy/xyIkDWpCgVE=",
+        "lastModified": 1750895632,
+        "narHash": "sha256-EPZWiRmaSTxoBArK5dQyRlSNVLXiBt2hmsYIPgMf3zk=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "04ef94c4c1582fd485bbfdb8c4a8ba250e359195",
+        "rev": "6ac57ce7fee0d80226095a57ccb7519855ad7c5e",
         "type": "github"
       },
       "original": {
         "owner": "nixos",
-        "ref": "release-24.11",
+        "ref": "release-25.05",
         "repo": "nixpkgs",
         "type": "github"
       }
@@ -68,11 +68,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1740277845,
-        "narHash": "sha256-NNU0CdiaSbAeZ8tpDG4aFi9qtcdlItRvk8Xns9oBrVU=",
+        "lastModified": 1750819193,
+        "narHash": "sha256-XvkupGPZqD54HuKhN/2WhbKjAHeTl1UEnWspzUzRFfA=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "f933070c29f9c1c5457447a51903f27f76ebb519",
+        "rev": "1ba3b9c59b68a4b00156827ad46393127b51b808",
         "type": "github"
       },
       "original": {
@@ -103,11 +103,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1744961264,
-        "narHash": "sha256-aRmUh0AMwcbdjJHnytg1e5h5ECcaWtIFQa6d9gI85AI=",
+        "lastModified": 1749194973,
+        "narHash": "sha256-eEy8cuS0mZ2j/r/FE0/LYBSBcIs/MKOIVakwHVuqTfk=",
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "8d404a69efe76146368885110f29a2ca3700bee6",
+        "rev": "a05be418a1af1198ca0f63facb13c985db4cb3c5",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -2,7 +2,7 @@
   description = "NixOS cluster configuration management";
 
   inputs = {
-    nixpkgs.url = "github:nixos/nixpkgs/release-24.11";
+    nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
     rust-overlay = {
       url = "github:oxalica/rust-overlay";
       inputs.nixpkgs.follows = "nixpkgs";
@@ -156,7 +156,7 @@
                 bacon
                 nil
                 rustPlatform.bindgenHook
-                nixVersions.nix_2_22
+                # nixVersions.nix_2_22
               ];
               environment.PROTOC = "${pkgs.protobuf}/bin/protoc";
             };
modifiedrust-toolchain.tomldiffbeforeafterboth
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
 [toolchain]
-channel = "1.85.0"
+channel = "1.86.0"
 components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]
modifiedrustfmt.tomldiffbeforeafterboth
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1,3 @@
 hard_tabs = true
-imports_granularity = "Crate"
-group_imports = "StdExternalCrate"
+# imports_granularity = "Crate"
+# group_imports = "StdExternalCrate"