git.delta.rocks / jrsonnet / refs/commits / 3a7032e3bf89

difftreelog

fix legacy ssh store support

lwkltrupYaroslav Bolyukin2025-09-15parent: #79b689b.patch.diff
in: trunk

4 files changed

modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -106,6 +106,9 @@
 			if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {
 				host.set_session_destination(destination);
 			};
+			if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {
+				host.set_legacy_ssh_store(legacy);
+			};
 
 			set.spawn_local(
 				(async move {
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::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{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,44}4546// TODO: Make field not pub47#[derive(Clone)]48pub struct Config(pub Arc<FleetConfigInternals>);4950impl Deref for Config {51	type Target = FleetConfigInternals;5253	fn deref(&self) -> &Self::Target {54		&self.055	}56}5758#[derive(Clone, Copy, Debug)]59pub enum EscalationStrategy {60	Sudo,61	Run0,62	Su,63}6465#[derive(Clone, PartialEq, Copy, Debug)]66pub enum DeployKind {67	/// NixOS => NixOS managed by fleet68	UpgradeToFleet,69	/// NixOS managed by fleet => NixOS managed by fleet70	Fleet,71	/// Remote host has /mnt, /mnt/boot mounted,72	/// generated config is added to fleet configuration.73	NixosInstall,74	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),75	/// generated config is added to fleet configuration,76	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.77	NixosLustrate,78}7980impl FromStr for DeployKind {81	type Err = anyhow::Error;82	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {83		match s {84			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),85			"fleet" => Ok(Self::Fleet),86			"nixos-install" => Ok(Self::NixosInstall),87			"nixos-lustrate" => Ok(Self::NixosLustrate),88			v => bail!(89				"unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""90			),91		}92	}93}94pub struct ConfigHost {95	config: Config,96	pub name: String,97	groups: OnceCell<Vec<String>>,9899	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it100	deploy_kind: OnceCell<DeployKind>,101	session_destination: OnceCell<String>,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_session_destination(&self, dest: String) {213		self.session_destination214			.set(dest)215			.expect("session destination is already set")216	}217	pub fn set_deploy_kind(&self, kind: DeployKind) {218		self.deploy_kind219			.set(kind)220			.expect("deploy kind is already set");221	}222	pub async fn deploy_kind(&self) -> Result<DeployKind> {223		if let Some(kind) = self.deploy_kind.get() {224			return Ok(*kind);225		}226		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {227			Ok(v) => v,228			Err(e) => {229				bail!("failed to query remote system kind: {}", e);230			}231		};232		if !is_fleet_managed {233			bail!(234				"{}",235				indoc::indoc! {"236				host is not marked as managed by fleet237				if you're not trying to lustrate/install system from scratch,238				you should either239					1. manually create /etc/FLEET_HOST file on the target host,240					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet241					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos242			"}243			);244		}245		// TOCTOU is possible246		let _ = self.deploy_kind.set(DeployKind::Fleet);247		Ok(*self.deploy_kind.get().expect("deploy kind is just set"))248	}249	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {250		// Prefer sudo, as run0 has some gotchas with polkit251		// and too many repeating prompts.252		if (self.find_in_path("sudo").await).is_ok() {253			return Ok(EscalationStrategy::Sudo);254		}255		if (self.find_in_path("run0").await).is_ok() {256			return Ok(EscalationStrategy::Run0);257		}258		Ok(EscalationStrategy::Su)259	}260	async fn open_session(&self) -> Result<Arc<openssh::Session>> {261		assert!(!self.local, "do not open ssh connection to local session");262		// FIXME: TOCTOU263		if let Some(session) = &self.session.get() {264			return Ok((*session).clone());265		};266		let session = SessionBuilder::default();267268		let dest = self.session_destination.get().unwrap_or(&self.name);269		let session = session270			.connect(&dest)271			.await272			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;273		let session = Arc::new(session);274		self.session.set(session.clone()).expect("TOCTOU happened");275		Ok(session)276	}277	pub async fn mktemp_dir(&self) -> Result<String> {278		let mut cmd = self.cmd("mktemp").await?;279		cmd.arg("-d");280		let path = cmd.run_string().await?;281		Ok(path.trim_end().to_owned())282	}283	pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {284		let mut cmd = self.cmd("sh").await?;285		cmd.arg("-c")286			.arg("test -e \"$1\" && echo true || echo false")287			.arg("_")288			.arg(path);289		cmd.run_value().await290	}291	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {292		let mut cmd = self.cmd("cat").await?;293		cmd.arg(path);294		cmd.run_bytes().await295	}296	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {297		let mut cmd = self.cmd("cat").await?;298		cmd.arg(path);299		cmd.run_string().await300	}301	pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {302		let mut cmd = self.cmd("ls").await?;303		cmd.arg(path);304		let out = cmd.run_string().await?;305		let mut lines = out.split('\n');306		if let Some(last) = lines.next_back() {307			ensure!(last.is_empty(), "output of ls should end with newline");308		}309		Ok(lines.map(ToOwned::to_owned).collect())310	}311	#[allow(dead_code)]312	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {313		let text = self.read_file_text(path).await?;314		Ok(serde_json::from_str(&text)?)315	}316	pub async fn read_env(&self, env: &str) -> Result<String> {317		let mut cmd = self.cmd("printenv").await?;318		cmd.arg(env);319		cmd.run_string().await320	}321	pub async fn find_in_path(&self, command: &str) -> Result<String> {322		// // `which` is not a part of coreutils, and it might not exist on machine.323		// let path = self.read_env("PATH").await?;324		// // Assuming delimiter is :, we don't work with windows host, this check will be much325		// // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)326		// for ele in path.split(':') {327		// 	let test_path = format!("{ele}/{cmd}");328		// 	test -x etc329		// }330		// let mut cmd = self.cmd("printenv").await?;331		// cmd.arg(env);332		// Ok(cmd.run_string().await?)333		// Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.334		let mut cmd = self335			.cmd_escalation(336				// Not used337				EscalationStrategy::Su,338				"which",339			)340			.await?;341		cmd.arg(command);342		cmd.run_string().await343	}344	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>345	where346		<D as FromStr>::Err: Display,347	{348		let text = self.read_file_text(path).await?;349		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))350	}351	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {352		self.cmd_escalation(self.escalation_strategy().await?, cmd)353			.await354	}355	pub async fn cmd_escalation(356		&self,357		escalation: EscalationStrategy,358		cmd: impl AsRef<OsStr>,359	) -> Result<MyCommand> {360		if self.local {361			Ok(MyCommand::new(escalation, cmd))362		} else {363			let session = self.open_session().await?;364			Ok(MyCommand::new_on(escalation, cmd, session))365		}366	}367	pub async fn nix_cmd(&self) -> Result<MyCommand> {368		let mut nix = self.cmd("nix").await?;369		nix.args([370			"--extra-experimental-features",371			"nix-command",372			"--extra-experimental-features",373			"flakes",374		]);375		Ok(nix)376	}377378	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {379		ensure!(data.encrypted, "secret is not encrypted");380		let mut cmd = self.cmd("fleet-install-secrets").await?;381		cmd.arg("decrypt").eqarg("--secret", data.to_string());382		let encoded = cmd383			.sudo()384			.run_string()385			.await386			.context("failed to call remote host for decrypt")?;387		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;388		ensure!(!data.encrypted, "secret came out encrypted");389		Ok(data.data)390	}391	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {392		ensure!(data.encrypted, "secret is not encrypted");393		let mut cmd = self.cmd("fleet-install-secrets").await?;394		cmd.arg("reencrypt").eqarg("--secret", data.to_string());395		for target in targets {396			let key = self.config.key(&target).await?;397			cmd.eqarg("--targets", key);398		}399		let encoded = cmd400			.sudo()401			.run_string()402			.await403			.context("failed to call remote host for decrypt")?;404		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;405		ensure!(data.encrypted, "secret came out not encrypted");406		Ok(data)407	}408	/// Returns path for futureproofing, as path might change i.e on conversion to CA409	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {410		if self.local {411			// Path is located locally, thus already trusted.412			return Ok(path.to_owned());413		}414		let mut nix = MyCommand::new(415			// Not used416			EscalationStrategy::Su,417			"nix",418		);419		nix.arg("copy").arg("--substitute-on-destination");420421		match self.deploy_kind().await? {422			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {423				nix.comparg("--to", format!("ssh-ng://{}", self.name));424			}425			DeployKind::NixosInstall => {426				nix427					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon428					.arg("--no-check-sigs")429					.comparg(430						"--to",431						format!("ssh-ng://root@{}?remote-store=/mnt", self.name),432					);433			}434		}435		nix.arg(path);436		nix.run_nix().await.context("nix copy")?;437		Ok(path.to_owned())438	}439	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {440		let mut cmd = self.cmd("systemctl").await?;441		cmd.arg("stop").arg(name);442		cmd.sudo().run().await443	}444	pub async fn systemctl_start(&self, name: &str) -> Result<()> {445		let mut cmd = self.cmd("systemctl").await?;446		cmd.arg("start").arg(name);447		cmd.sudo().run().await448	}449450	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {451		let mut cmd = self.cmd("rm").await?;452		cmd.arg("-f").arg(path);453		if sudo {454			cmd = cmd.sudo()455		}456		cmd.run().await457	}458}459impl ConfigHost {460	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,461	// assuming getting tags always returns the same value.462	pub async fn tags(&self) -> Result<Vec<String>> {463		if let Some(v) = self.groups.get() {464			return Ok(v.clone());465		}466		let Some(host_config) = &self.host_config else {467			return Ok(vec![]);468		};469		let tags: Vec<String> = nix_go_json!(host_config.tags);470471		let _ = self.groups.set(tags.clone());472473		Ok(tags)474	}475	pub async fn nixos_config(&self) -> Result<Value> {476		if let Some(v) = self.nixos_config.get() {477			return Ok(v.clone());478		}479		let Some(host_config) = &self.host_config else {480			bail!("local host has no nixos_config");481		};482		let nixos_config = nix_go!(host_config.nixos.config);483		assert_warn("nixos config evaluation", &nixos_config).await?;484485		let _ = self.nixos_config.set(nixos_config.clone());486487		Ok(nixos_config)488	}489	pub async fn nixos_unchecked_config(&self) -> Result<Value> {490		if let Some(v) = self.nixos_unchecked_config.get() {491			return Ok(v.clone());492		}493		let Some(host_config) = &self.host_config else {494			bail!("local host has no nixos_config");495		};496		let nixos_config = nix_go!(host_config.nixos_unchecked.config);497498		let _ = self.nixos_unchecked_config.set(nixos_config.clone());499500		Ok(nixos_config)501	}502503	pub async fn list_configured_secrets(&self) -> Result<Vec<String>> {504		let nixos = self.nixos_unchecked_config().await?;505		let secrets = nix_go!(nixos.secrets);506		let mut out = Vec::new();507		for name in secrets.list_fields()? {508			let secret = secrets.get_field(&name)?;509			let is_shared: bool = nix_go_json!(secret.shared);510			if is_shared {511				continue;512			}513			out.push(name);514		}515		Ok(out)516	}517	pub async fn secret_field(&self, name: &str) -> Result<Value> {518		let nixos = self.nixos_unchecked_config().await?;519		Ok(nix_go!(nixos.secrets[{ name }]))520	}521522	/// Packages for this host, resolved with nixpkgs overlays523	pub async fn pkgs(&self) -> Result<Value> {524		if let Some(value) = &self.pkgs_override {525			return Ok(value.clone());526		}527		let Some(host_config) = &self.host_config else {528			bail!("local host has no host_config");529		};530		// TODO: Should nixos.options be cached?531		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))532	}533}534535impl Config {536	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {537		let config = &self.config_field;538		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);539		Ok(tagged)540	}541	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {542		let mut out = BTreeSet::new();543		for owner in owners {544			if let Some(tag) = owner.strip_prefix('@') {545				let hosts = self.tagged_hostnames(tag).await?;546				out.extend(hosts);547			} else {548				out.insert(owner);549			}550		}551		Ok(out)552	}553	pub fn local_host(&self) -> ConfigHost {554		ConfigHost {555			config: self.clone(),556			name: "<virtual localhost>".to_owned(),557			host_config: None,558			nixos_config: OnceCell::new(),559			nixos_unchecked_config: OnceCell::new(),560			groups: {561				let cell = OnceCell::new();562				let _ = cell.set(vec![]);563				cell564			},565			pkgs_override: Some(self.default_pkgs.clone()),566567			local: true,568			session: OnceLock::new(),569			deploy_kind: OnceCell::new(),570			session_destination: OnceCell::new(),571		}572	}573574	pub async fn host(&self, name: &str) -> Result<ConfigHost> {575		let config = &self.config_field;576		let host_config = nix_go!(config.hosts[{ name }]);577578		Ok(ConfigHost {579			config: self.clone(),580			name: name.to_owned(),581			host_config: Some(host_config),582			nixos_config: OnceCell::new(),583			nixos_unchecked_config: OnceCell::new(),584			groups: OnceCell::new(),585			pkgs_override: None,586587			// TODO: Remove with connectivit refactor588			local: self.localhost == name,589			session: OnceLock::new(),590			deploy_kind: OnceCell::new(),591			session_destination: OnceCell::new(),592		})593	}594	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {595		let config = &self.config_field;596		let names = nix_go!(config.hosts).list_fields()?;597		let mut out = vec![];598		for name in names {599			out.push(self.host(&name).await?);600		}601		Ok(out)602	}603	// TODO: Replace usages with .host().nixos_config604	pub async fn system_config(&self, host: &str) -> Result<Value> {605		let fleet_field = &self.config_field;606		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))607	}608609	/// Shared secrets configured in fleet.nix or in flake610	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {611		let config_field = &self.config_field;612		nix_go!(config_field.sharedSecrets).list_fields()613	}614	/// Shared secrets configured in fleet.nix615	pub fn list_shared(&self) -> Vec<String> {616		let data = self.data();617		data.shared_secrets.keys().cloned().collect()618	}619	pub fn has_shared(&self, name: &str) -> bool {620		let data = self.data();621		data.shared_secrets.contains_key(name)622	}623	pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {624		let mut data = self.data_mut();625		data.shared_secrets.insert(name.to_owned(), shared);626	}627	pub fn remove_shared(&self, secret: &str) {628		let mut data = self.data_mut();629		data.shared_secrets.remove(secret);630	}631632	pub fn list_secrets(&self, host: &str) -> Vec<String> {633		let data = self.data();634		let Some(secrets) = data.host_secrets.get(host) else {635			return Vec::new();636		};637		secrets.keys().cloned().collect()638	}639640	pub fn has_secret(&self, host: &str, secret: &str) -> bool {641		let data = self.data();642		let Some(host_secrets) = data.host_secrets.get(host) else {643			return false;644		};645		host_secrets.contains_key(secret)646	}647	pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {648		let mut data = self.data_mut();649		let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();650		host_secrets.insert(secret, value);651	}652653	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {654		let data = self.data();655		let Some(host_secrets) = data.host_secrets.get(host) else {656			bail!("no secrets for machine {host}");657		};658		let Some(secret) = host_secrets.get(secret) else {659			bail!("machine {host} has no secret {secret}");660		};661		Ok(secret.clone())662	}663	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {664		let data = self.data();665		let Some(secret) = data.shared_secrets.get(secret) else {666			bail!("no shared secret {secret}");667		};668		Ok(secret.clone())669	}670	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {671		let config_field = &self.config_field;672		Ok(nix_go_json!(673			config_field.sharedSecrets[{ secret }].expectedOwners674		))675	}676677	// TODO: Should this be something modifiable from other processes?678	// E.g terraform provider might want to update FleetData (e.g secrets),679	// and current implementation assumes only one process holds current fleet.nix680	// Given that it is no longer needs to be a file for nix evaluation,681	// maybe it can be a .nix file for persistence, but accessible only682	// thru some shared state controller? Might it be stored in terraform683	// state provider?684	pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {685		self.data.lock().unwrap()686	}687	pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {688		self.data.lock().unwrap()689	}690	pub fn save(&self) -> Result<()> {691		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.")?;692		let data = nixlike::serialize(&self.data() as &FleetData)?;693		tempfile.write_all(694			format!(695				"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"696			)697			.as_bytes(),698		)?;699		let mut fleet_data_path = self.directory.clone();700		fleet_data_path.push("fleet.nix");701		tempfile.persist(fleet_data_path)?;702		Ok(())703	}704}
modifiedcrates/fleet-shared/src/encoding.rsdiffbeforeafterboth
--- a/crates/fleet-shared/src/encoding.rs
+++ b/crates/fleet-shared/src/encoding.rs
@@ -1,6 +1,5 @@
 use std::{
-	fmt::{self, Display},
-	str::FromStr,
+	collections::BTreeMap, fmt::{self, Display}, str::FromStr
 };
 
 use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -168,12 +168,9 @@
                 cargo-fuzz
                 cargo-watch
                 cargo-outdated
-                gdb
 
                 pkg-config
                 openssl
-                bacon
-                nil
                 rustPlatform.bindgenHook
                 inputs'.nix.packages.nix-expr-c
                 inputs'.nix.packages.nix-flake-c