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

difftreelog

refactor replace eval with repl where possible

Yaroslav Bolyukin2023-10-29parent: #353ae3b.patch.diff
in: trunk

22 files changed

modifiedCargo.lockdiffbeforeafterboth
after · Cargo.lock
309 packageslockfile v3
modifiedREADME.adocdiffbeforeafterboth
--- a/README.adoc
+++ b/README.adoc
@@ -6,3 +6,4 @@
 
 - Modules can configure multiple hosts at once (I.e for wireguard/kubernetes installation)
 - Secrets can be securely stored in Git (No one except target hosts can decrypt them), automatically regenerated, reencrypted, etc.
+- Automatic rollback on deployment failure, which will work, as long as system is passing initrd stage (So still be carefull with root filesystem mount)
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -9,29 +9,35 @@
 anyhow = "1.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
-time = { version = "0.3.2", features = ["serde"] }
-tempfile = "3.2"
-once_cell = "1.5"
+time = { version = "0.3.30", features = ["serde"] }
+tempfile = "3.8"
+once_cell = "1.18"
 hostname = "0.3.1"
 age-core = "0.9.0"
-peg = "0.8.0"
+peg = "0.8.2"
 nixlike = { path = "../../crates/nixlike" }
-age = { version = "0.9.0", features = ["ssh", "armor"] }
-base64 = "0.21.0"
-chrono = { version = "0.4.19", features = ["serde"] }
-z85 = "3.0.3"
-clap = { version = "4.0.29", features = [
+age = { version = "0.9.2", features = ["ssh", "armor"] }
+base64 = "0.21.5"
+chrono = { version = "0.4.31", features = ["serde"] }
+z85 = "3.0.5"
+clap = { version = "4.4.7", features = [
 	"derive",
 	"env",
 	"wrap_help",
 	"unicode",
 ] }
-tokio = { version = "1.14.0", features = ["full"] }
+tokio = { version = "1.33.0", features = ["full"] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
-tokio-util = { version = "0.7.0", features = ["codec"] }
-async-trait = "0.1.52"
-futures = "0.3.17"
+tokio-util = { version = "0.7.10", features = ["codec"] }
+async-trait = "0.1.74"
+futures = "0.3.29"
 tracing-indicatif = "0.3.5"
 indicatif = "0.17.7"
 itertools = "0.11.0"
+shlex = "1.2.0"
+tabled = { version = "0.14.0", features = ["color"] }
+owo-colors = { version = "3.5.0", features = ["supports-color", "supports-colors"] }
+r2d2 = "0.8.10"
+abort-on-drop = "0.2.2"
+unindent = "0.2.3"
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -121,11 +121,11 @@
 	cmd.comparg("--profile", "/nix/var/nix/profiles/system")
 		.arg("--list-generations");
 	// Sudo is required due to --list-generations acquiring lock on the profile.
-	let data = config.run_string_on(&host, cmd, true).await?;
+	let data = config.run_string_on(host, cmd, true).await?;
 	let generations = data
 		.split('\n')
 		.map(|e| e.trim())
-		.filter(|&l| l != "")
+		.filter(|&l| !l.is_empty())
 		.filter_map(|g| {
 			let gen: Option<Generation> = try {
 				let mut parts = g.split_whitespace();
@@ -170,13 +170,13 @@
 async fn systemctl_stop(config: &Config, host: &str, unit: &str) -> Result<()> {
 	let mut cmd = MyCommand::new("systemctl");
 	cmd.arg("stop").arg(unit);
-	config.run_on(&host, cmd, true).await
+	config.run_on(host, cmd, true).await
 }
 
 async fn systemctl_start(config: &Config, host: &str, unit: &str) -> Result<()> {
 	let mut cmd = MyCommand::new("systemctl");
 	cmd.arg("start").arg(unit);
-	config.run_on(&host, cmd, true).await
+	config.run_on(host, cmd, true).await
 }
 
 async fn execute_upload(
@@ -195,7 +195,7 @@
 	if !build.disable_rollback {
 		let _span = info_span!("preparing").entered();
 		info!("preparing for rollback");
-		let generation = get_current_generation(&config, &host).await?;
+		let generation = get_current_generation(config, host).await?;
 		info!(
 			"rollback target would be {} {}",
 			generation.id, generation.datetime
@@ -203,7 +203,7 @@
 		{
 			let mut cmd = MyCommand::new("sh");
 			cmd.arg("-c").arg(format!("mark=$(mktemp -p /etc -t fleet_rollback_marker.XXXXX) && echo -n {} > $mark && mv --no-clobber $mark /etc/fleet_rollback_marker", generation.id));
-			if let Err(e) = config.run_on(&host, cmd, true).await {
+			if let Err(e) = config.run_on(host, cmd, true).await {
 				error!("failed to set rollback marker: {e}");
 				failed = true;
 			}
@@ -225,7 +225,7 @@
 				.arg("systemctl")
 				.arg("start")
 				.arg("rollback-watchdog.service");
-			if let Err(e) = config.run_on(&host, cmd, true).await {
+			if let Err(e) = config.run_on(host, cmd, true).await {
 				error!("failed to schedule rollback run: {e}");
 				failed = true;
 			}
@@ -236,7 +236,7 @@
 		let mut cmd = MyCommand::new("nix-env");
 		cmd.comparg("--profile", "/nix/var/nix/profiles/system")
 			.comparg("--set", &built);
-		if let Err(e) = config.run_on(&host, cmd, true).await {
+		if let Err(e) = config.run_on(host, cmd, true).await {
 			error!("failed to switch generation: {e}");
 			failed = true;
 		}
@@ -249,7 +249,7 @@
 		switch_script.push("switch-to-configuration");
 		let mut cmd = MyCommand::new(switch_script);
 		cmd.arg(action.name());
-		if let Err(e) = config.run_on(&host, cmd, true).in_current_span().await {
+		if let Err(e) = config.run_on(host, cmd, true).in_current_span().await {
 			error!("failed to activate: {e}");
 			failed = true;
 		}
@@ -257,7 +257,7 @@
 	if !build.disable_rollback {
 		if failed {
 			info!("executing rollback");
-			if let Err(e) = systemctl_start(&config, &host, "rollback-watchdog.service")
+			if let Err(e) = systemctl_start(config, host, "rollback-watchdog.service")
 				.instrument(info_span!("rollback"))
 				.await
 			{
@@ -267,23 +267,23 @@
 			info!("trying to mark upgrade as successful");
 			let mut cmd = MyCommand::new("rm");
 			cmd.arg("-f").arg("/etc/fleet_rollback_marker");
-			if let Err(e) = config.run_on(&host, cmd, true).in_current_span().await {
+			if let Err(e) = config.run_on(host, cmd, true).in_current_span().await {
 				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");
-		if let Err(_e) = systemctl_stop(&config, &host, "rollback-watchdog.timer").await {
+		if let Err(_e) = systemctl_stop(config, host, "rollback-watchdog.timer").await {
 			// It is ok, if there was no reboot - then timer might not be running.
 		}
 		if action.should_schedule_rollback_run() {
-			if let Err(e) = systemctl_stop(&config, &host, "rollback-watchdog-run.timer").await {
+			if let Err(e) = systemctl_stop(config, host, "rollback-watchdog-run.timer").await {
 				error!("failed to disarm rollback run: {e}");
 			}
 		}
 	} else {
 		let mut cmd = MyCommand::new("rm");
 		cmd.arg("-f").arg("/etc/fleet_rollback_marker");
-		if let Err(_e) = config.run_on(&host, cmd, true).in_current_span().await {
+		if let Err(_e) = config.run_on(host, cmd, true).in_current_span().await {
 			// Marker might not exist, yet better try to remove it.
 		}
 	}
@@ -341,7 +341,7 @@
 						sign.arg("nix")
 							.arg("store")
 							.arg("sign")
-							.comparg("-k", "/etc/nix/private-key")
+							.comparg("--key-file", "/etc/nix/private-key")
 							.arg("-r")
 							.arg(&built);
 						if let Err(e) = sign.run_nix().await {
@@ -353,7 +353,7 @@
 						let mut nix = MyCommand::new("nix");
 						nix.arg("copy")
 							.arg("--substitute-on-destination")
-							.comparg("--to", format!("ssh-ng://root@{host}"))
+							.comparg("--to", format!("ssh-ng://{host}"))
 							.arg(&built);
 						match nix.run_nix().await {
 							Ok(()) => break,
@@ -423,17 +423,17 @@
 		let hosts = config.list_hosts().await?;
 		let set = LocalSet::new();
 		let this = &self;
-		for host in hosts.iter() {
-			if config.should_skip(host) {
+		for host in hosts.into_iter() {
+			if config.should_skip(&host.name) {
 				continue;
 			}
 			let config = config.clone();
-			let host = host.clone();
 			let this = this.clone();
-			let span = info_span!("deployment", host = field::display(&host));
+			let span = info_span!("deployment", host = field::display(&host.name));
+			let hostname = host.name;
 			set.spawn_local(
 				(async move {
-					match this.build_task(config, host).await {
+					match this.build_task(config, hostname).await {
 						Ok(_) => {}
 						Err(e) => {
 							error!("failed to deploy host: {}", e)
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -36,14 +36,19 @@
 			InfoCmd::ListHosts { ref tagged } => {
 				'host: for host in config.list_hosts().await? {
 					if !tagged.is_empty() {
-						let tags: Vec<String> = config.config_attr(&host, "tags").await?;
+						let tags: Vec<String> = config
+							.fleet_field
+							.get_field_deep(["configuredSystems", &host.name, "config", "tags"])
+							.await?
+							.as_json()
+							.await?;
 						for tag in tagged {
 							if !tags.contains(tag) {
 								continue 'host;
 							}
 						}
 					}
-					data.push(host);
+					data.push(host.name);
 				}
 			}
 			InfoCmd::HostIps {
@@ -56,17 +61,20 @@
 					"at leas one of --external or --internal must be set"
 				);
 				let mut out = <BTreeSet<String>>::new();
+				let host = config.system_config(&host).await?;
 				if external {
 					out.extend(
-						config
-							.config_attr::<Vec<String>>(&host, "network.externalIps")
+						host.get_field_deep(["network", "externalIps"])
+							.await?
+							.as_json::<Vec<String>>()
 							.await?,
 					);
 				}
 				if internal {
 					out.extend(
-						config
-							.config_attr::<Vec<String>>(&host, "network.internalIps")
+						host.get_field_deep(["network", "internalIps"])
+							.await?
+							.as_json::<Vec<String>>()
 							.await?,
 					);
 				}
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -3,15 +3,18 @@
 	host::Config,
 };
 use anyhow::{bail, ensure, Context, Result};
+use chrono::Utc;
 use clap::Parser;
 use futures::{StreamExt, TryStreamExt};
+use owo_colors::OwoColorize;
 use std::{
 	collections::HashSet,
 	io::{self, Cursor, Read},
 	path::PathBuf,
 };
+use tabled::{Table, Tabled};
 use tokio::fs::read_to_string;
-use tracing::{error, info, warn};
+use tracing::{error, info, info_span, warn};
 
 #[derive(Parser)]
 pub enum Secrets {
@@ -73,6 +76,7 @@
 		#[clap(long)]
 		prefer_identities: Vec<String>,
 	},
+	List {},
 }
 
 impl Secrets {
@@ -80,10 +84,10 @@
 		match self {
 			Secrets::ForceKeys => {
 				for host in config.list_hosts().await? {
-					if config.should_skip(&host) {
+					if config.should_skip(&host.name) {
 						continue;
 					}
-					config.key(&host).await?;
+					config.key(&host.name).await?;
 				}
 			}
 			Secrets::AddShared {
@@ -128,7 +132,8 @@
 					FleetSharedSecret {
 						owners: machines,
 						secret: FleetSecret {
-							expire_at: None,
+							created_at: Utc::now(),
+							expires_at: None,
 							secret,
 							public: match (public, public_file) {
 								(Some(v), None) => Some(v),
@@ -175,7 +180,8 @@
 					&machine,
 					name,
 					FleetSecret {
-						expire_at: None,
+						created_at: Utc::now(),
+						expires_at: None,
 						secret,
 						public: match (public, public_file) {
 							(Some(v), None) => Some(v),
@@ -291,7 +297,7 @@
 					target_recipients.into_iter().collect::<Result<Vec<_>>>()?;
 
 				let encrypted = config
-					.reencrypt_on_host(&identity_holder, secret.secret.secret, target_recipients)
+					.reencrypt_on_host(identity_holder, secret.secret.secret, target_recipients)
 					.await?;
 
 				secret.owners = target_machines;
@@ -300,13 +306,14 @@
 			}
 			Secrets::Regenerate { prefer_identities } => {
 				{
-					let expected_shared_set =
-						config.shared_config_attr_names("sharedSecrets").await?;
-					let expected_shared_set = expected_shared_set.iter().collect::<HashSet<_>>();
-					let shared_set = config.list_shared();
-					let shared_set = shared_set.iter().collect::<HashSet<_>>();
+					let expected_shared_set = config
+						.list_configured_shared()
+						.await?
+						.into_iter()
+						.collect::<HashSet<_>>();
+					let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();
 					for removed in expected_shared_set.difference(&shared_set) {
-						warn!("secret needs to be generated: {removed}")
+						error!("secret needs to be generated: {removed}")
 					}
 				}
 				let mut to_remove = Vec::new();
@@ -314,7 +321,8 @@
 					info!("updating secret: {name}");
 					let mut data = config.shared_secret(name)?;
 					let expected_owners: Vec<String> = config
-						.shared_config_attr(&format!("sharedSecrets.\"{name}\".expectedOwners"))
+						.config_field
+						.get_json_deep(["sharedSecrets", name, "expectedOwners"])
 						.await?;
 					if expected_owners.is_empty() {
 						warn!("secret was removed from fleet config: {name}, removing from data");
@@ -326,7 +334,8 @@
 					let should_remove = set.difference(&expected_set).next().is_some();
 					if set != expected_set {
 						let owner_dependent: bool = config
-							.shared_config_attr(&format!("sharedSecrets.\"{name}\".ownerDependent"))
+							.config_field
+							.get_json_deep(["sharedSecrets", name, "ownerDependent"])
 							.await?;
 						if !owner_dependent {
 							warn!("reencrypting secret '{name}' for new owner set");
@@ -355,7 +364,7 @@
 
 							let encrypted = config
 								.reencrypt_on_host(
-									&identity_holder,
+									identity_holder,
 									data.secret.secret,
 									target_recipients,
 								)
@@ -364,13 +373,6 @@
 							data.secret.secret = encrypted;
 							data.owners = expected_owners;
 							config.replace_shared(name.to_owned(), data);
-						} else if let Some(generator) = config
-							.shared_config_attr::<Option<String>>(&format!(
-								"sharedSecrets.\"{name}\".generator"
-							))
-							.await?
-						{
-							todo!("regenerate secret {name} with {generator}");
 						} else {
 							error!("secret '{name}' should be regenerated manually");
 						}
@@ -382,6 +384,39 @@
 					config.remove_shared(&k);
 				}
 			}
+			Secrets::List {} => {
+				let _span = info_span!("loading secrets").entered();
+				let configured = config.list_configured_shared().await?;
+				#[derive(Tabled)]
+				struct SecretDisplay {
+					#[tabled(rename = "Name")]
+					name: String,
+					#[tabled(rename = "Owners")]
+					owners: String,
+				}
+				let mut table = vec![];
+				for name in configured.iter().cloned() {
+					let config = config.clone();
+					let expected_owners = config.shared_secret_expected_owners(&name).await?;
+					let data = config.shared_secret(&name)?;
+					let owners = data
+						.owners
+						.iter()
+						.map(|o| {
+							if expected_owners.contains(o) {
+								o.green().to_string()
+							} else {
+								o.red().to_string()
+							}
+						})
+						.collect::<Vec<_>>();
+					table.push(SecretDisplay {
+						owners: owners.join(", "),
+						name,
+					})
+				}
+				info!("loaded\n{}", Table::new(table).to_string())
+			}
 		}
 		Ok(())
 	}
modifiedcmds/fleet/src/command.rsdiffbeforeafterboth
--- a/cmds/fleet/src/command.rs
+++ b/cmds/fleet/src/command.rs
@@ -1,11 +1,14 @@
-use std::{collections::HashMap, ffi::OsStr, process::Stdio, task::Poll};
+use std::{
+	collections::HashMap,
+	ffi::OsStr,
+	process::Stdio,
+	sync::{Arc, Mutex},
+	task::Poll,
+};
 
-use anyhow::{Context, Result};
+use anyhow::Result;
 use futures::StreamExt;
-use serde::{
-	de::{DeserializeOwned, Visitor},
-	Deserialize,
-};
+use serde::{de::Visitor, Deserialize};
 use tokio::{io::AsyncRead, process::Command, select};
 use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};
 use tracing::{info, info_span, warn, Span};
@@ -49,12 +52,12 @@
 		if !self.env.is_empty() {
 			out.push("env".to_owned());
 			for (k, v) in self.env {
-				assert!(!k.contains("="));
+				assert!(!k.contains('='));
 				out.push(format!("{k}={v}"));
 			}
 		}
 		out.push(self.command);
-		out.extend(self.args.into_iter());
+		out.extend(self.args);
 		out
 	}
 	fn into_string(self) -> String {
@@ -63,7 +66,7 @@
 			out.push_str("env");
 			for (k, v) in self.env {
 				out.push(' ');
-				assert!(!k.contains("="));
+				assert!(!k.contains('='));
 				escape_bash(&k, &mut out);
 				out.push('=');
 				escape_bash(&v, &mut out);
@@ -135,10 +138,6 @@
 		let cmd = self.into_command();
 		let v = run_nix_inner_stdout(str, cmd, &mut PlainHandler).await?;
 		Ok(v)
-	}
-	pub async fn run_nix_json<T: DeserializeOwned>(self) -> Result<T> {
-		let str = self.run_nix_string().await?;
-		serde_json::from_str(&str).with_context(|| format!("{:?}", str))
 	}
 
 	pub async fn run_nix_string(self) -> Result<String> {
@@ -172,38 +171,55 @@
 	cmd: Command,
 	handler: &mut dyn Handler,
 ) -> Result<String> {
-	Ok(run_nix_inner_raw(str, cmd, true, handler)
+	Ok(run_nix_inner_raw(str, cmd, true, handler, None)
 		.await?
 		.expect("has out"))
 }
 async fn run_nix_inner(str: String, cmd: Command, handler: &mut dyn Handler) -> Result<()> {
-	let v = run_nix_inner_raw(str, cmd, false, handler).await?;
+	let v = run_nix_inner_raw(str, cmd, false, handler, None).await?;
 	assert!(v.is_none());
 	Ok(())
 }
 
-trait Handler {
-	fn handle_err(&mut self, e: &str);
-	fn handle_info(&mut self, e: &str);
+pub trait Handler: Send {
+	fn handle_line(&mut self, e: &str);
+}
+
+pub struct ClonableHandler<H>(Arc<Mutex<H>>);
+impl<H> Clone for ClonableHandler<H> {
+	fn clone(&self) -> Self {
+		Self(self.0.clone())
+	}
+}
+impl<H> ClonableHandler<H> {
+	pub fn new(inner: H) -> Self {
+		Self(Arc::new(Mutex::new(inner)))
+	}
+}
+impl<H: Handler> Handler for ClonableHandler<H> {
+	fn handle_line(&mut self, e: &str) {
+		self.0.lock().unwrap().handle_line(e)
+	}
 }
 
 struct PlainHandler;
 impl Handler for PlainHandler {
-	fn handle_err(&mut self, e: &str) {
+	fn handle_line(&mut self, e: &str) {
 		info!(target: "log", "{e}");
 	}
+}
 
-	fn handle_info(&mut self, e: &str) {
-		info!(target: "log", "{e}");
-	}
+pub struct NoopHandler;
+impl Handler for NoopHandler {
+	fn handle_line(&mut self, _e: &str) {}
 }
 
 #[derive(Default)]
-struct NixHandler {
+pub struct NixHandler {
 	spans: HashMap<u64, Span>,
 }
 impl Handler for NixHandler {
-	fn handle_err(&mut self, e: &str) {
+	fn handle_line(&mut self, e: &str) {
 		if let Some(e) = e.strip_prefix("@nix ") {
 			let log: NixLog = match serde_json::from_str(e) {
 				Ok(l) => l,
@@ -214,6 +230,7 @@
 			};
 			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" {
@@ -397,11 +414,12 @@
 				_ => warn!("unknown log: {:?}", log),
 			};
 		} else {
-			warn!(target = "nix", "unknown: {}", e.trim())
+			let e = e.trim();
+			if e.starts_with("Failed tcsetattr(TCSADRAIN): ") {
+				return;
+			}
+			info!("{e}")
 		}
-	}
-	fn handle_info(&mut self, o: &str) {
-		self.handle_err(o)
 	}
 }
 
@@ -409,9 +427,9 @@
 	str: String,
 	mut cmd: Command,
 	want_stdout: bool,
-	handler: &mut dyn Handler,
+	err_handler: &mut dyn Handler,
+	mut out_handler: Option<&mut dyn Handler>,
 ) -> Result<Option<String>> {
-	info!("running {str}");
 	cmd.stderr(Stdio::piped());
 	cmd.stdout(Stdio::piped());
 	let mut child = cmd.spawn()?;
@@ -436,7 +454,7 @@
 			e = err.next() => {
 				if let Some(e) = e {
 					let e = e?;
-					handler.handle_err(&e);
+					err_handler.handle_line(&e);
 				}
 			},
 			o = ob.next() => {
@@ -447,7 +465,12 @@
 			o = ol.next() => {
 				if let Some(o) = o {
 					let o = o?;
-					handler.handle_info(&o);
+					if let Some(out) = out_handler.as_mut() {
+						out.handle_line(&o)
+					} else {
+						err_handler.handle_line(&o)
+					}
+					// out_handler.handle_info(&o);
 				}
 			},
 			code = child.wait() => {
@@ -463,6 +486,11 @@
 	Ok(out_buf.map(String::from_utf8).transpose()?)
 }
 
+pub trait ErrorRecorder: Send {
+	/// Return true to discard message from logging
+	fn push_message(&mut self, msg: &str) -> bool;
+}
+
 #[derive(Debug)]
 enum LogField {
 	String(String),
modifiedcmds/fleet/src/extra_args.rsdiffbeforeafterboth
--- a/cmds/fleet/src/extra_args.rs
+++ b/cmds/fleet/src/extra_args.rs
@@ -12,7 +12,7 @@
 		})
 		.collect())
 }
-pub fn parse(s: &str) -> Result<Vec<OsString>> {
-	let osstr = OsString::try_from(s)?;
-	parse_os(&osstr)
-}
+// pub fn parse(s: &str) -> Result<Vec<OsString>> {
+// 	let osstr = OsString::try_from(s)?;
+// 	parse_os(&osstr)
+// }
modifiedcmds/fleet/src/fleetdata.rsdiffbeforeafterboth
--- a/cmds/fleet/src/fleetdata.rs
+++ b/cmds/fleet/src/fleetdata.rs
@@ -44,9 +44,11 @@
 #[serde(rename_all = "camelCase")]
 #[must_use]
 pub struct FleetSecret {
+	#[serde(default = "Utc::now")]
+	pub created_at: DateTime<Utc>,
 	#[serde(default)]
-	#[serde(skip_serializing_if = "Option::is_none")]
-	pub expire_at: Option<DateTime<Utc>>,
+	#[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]
+	pub expires_at: Option<DateTime<Utc>>,
 	#[serde(skip_serializing_if = "Option::is_none")]
 	pub public: Option<String>,
 	#[serde(
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -1,19 +1,18 @@
 use std::{
-	cell::{Ref, RefCell, RefMut},
 	env::current_dir,
 	ffi::OsString,
 	io::Write,
 	ops::Deref,
 	path::PathBuf,
-	sync::Arc,
+	sync::{Arc, Mutex, MutexGuard},
 };
 
 use anyhow::{bail, Context, Result};
 use clap::{ArgGroup, Parser};
-use serde::de::DeserializeOwned;
 use tempfile::NamedTempFile;
 
 use crate::{
+	better_nix_eval::{Field, NixSessionPool},
 	command::MyCommand,
 	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},
 };
@@ -22,8 +21,12 @@
 	pub local_system: String,
 	pub directory: PathBuf,
 	pub opts: FleetOpts,
-	pub data: RefCell<FleetData>,
+	pub data: Mutex<FleetData>,
 	pub nix_args: Vec<OsString>,
+	// fleetConfigurations.<name>
+	pub fleet_field: Field,
+	// fleet_config.configUnchecked
+	pub config_field: Field,
 }
 
 #[derive(Clone)]
@@ -37,6 +40,10 @@
 	}
 }
 
+pub struct ConfigHost {
+	pub name: String,
+}
+
 impl Config {
 	pub fn should_skip(&self, host: &str) -> bool {
 		if !self.opts.skip.is_empty() {
@@ -60,7 +67,6 @@
 		}
 		command.run().await
 	}
-	#[must_use]
 	pub async fn run_string_on(
 		&self,
 		host: &str,
@@ -86,52 +92,39 @@
 		str
 	}
 
-	pub async fn list_hosts(&self) -> Result<Vec<String>> {
-		let mut cmd = MyCommand::new("nix");
-		cmd.arg("eval")
-			.arg(self.configuration_attr_name("configuredHosts"))
-			.args(["--apply", "builtins.attrNames", "--json", "--show-trace"])
-			.args(&self.nix_args);
-		cmd.run_nix_json().await
-	}
-	pub async fn shared_config_attr<T: DeserializeOwned>(&self, attr: &str) -> Result<T> {
-		let mut cmd = MyCommand::new("nix");
-		cmd.arg("eval")
-			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))
-			.args(["--json", "--show-trace"])
-			.args(&self.nix_args);
-		cmd.run_nix_json().await
-	}
-	pub async fn shared_config_attr_names(&self, attr: &str) -> Result<Vec<String>> {
-		let mut cmd = MyCommand::new("nix");
-		cmd.arg("eval")
-			.arg(self.configuration_attr_name(&format!("configUnchecked.{}", attr)))
-			.args(["--apply", "builtins.attrNames"])
-			.args(["--json", "--show-trace"])
-			.args(&self.nix_args);
-		cmd.run_nix_json().await
+	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
+		let names = self.fleet_field
+			.get_field_deep(["configuredHosts"])
+			.await?
+			.list_fields()
+			.await?;
+		 let mut out = vec![];
+		 for name in names {
+			out.push(ConfigHost {
+				name,
+			})
+		 }
+		 Ok(out)
 	}
-	pub async fn config_attr<T: DeserializeOwned>(&self, host: &str, attr: &str) -> Result<T> {
-		let mut cmd = MyCommand::new("nix");
-		cmd.arg("eval")
-			.arg(
-				self.configuration_attr_name(&format!(
-					"configuredSystems.{}.config.{}",
-					host, attr
-				)),
-			)
-			.args(["--json", "--show-trace"])
-			.args(&self.nix_args);
-		cmd.run_nix_json().await
+	pub async fn system_config(&self, host: &str) -> Result<Field> {
+		self.fleet_field.get_field_deep(["configuredSystems", host, "config"]).await
 	}
 
-	pub(super) fn data(&self) -> Ref<FleetData> {
-		self.data.borrow()
+	pub(super) fn data(&self) -> MutexGuard<FleetData> {
+		self.data.lock().unwrap()
+	}
+	pub(super) fn data_mut(&self) -> MutexGuard<FleetData> {
+		self.data.lock().unwrap()
 	}
-	pub(super) fn data_mut(&self) -> RefMut<FleetData> {
-		self.data.borrow_mut()
+	/// Shared secrets configured in fleet.nix or in flake
+	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
+		self.config_field
+			.get_field("sharedSecrets")
+			.await?
+			.list_fields()
+			.await
 	}
-
+	/// Shared secrets configured in fleet.nix
 	pub fn list_shared(&self) -> Vec<String> {
 		let data = self.data();
 		data.shared_secrets.keys().cloned().collect()
@@ -149,13 +142,6 @@
 		data.shared_secrets.remove(secret);
 	}
 
-	pub fn list_secrets(&self, host: &str) -> Vec<String> {
-		let data = self.data();
-		let Some(host_secrets) = data.host_secrets.get(host) else {
-			return Vec::new();
-		};
-		host_secrets.keys().cloned().collect()
-	}
 	pub fn has_secret(&self, host: &str, secret: &str) -> bool {
 		let data = self.data();
 		let Some(host_secrets) = data.host_secrets.get(host) else {
@@ -180,7 +166,7 @@
 			.context("failed to call remote host for decrypt")?
 			.trim()
 			.to_owned();
-		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)
+		z85::decode(encoded).context("bad encoded data? outdated host?")
 	}
 	pub async fn reencrypt_on_host(
 		&self,
@@ -201,10 +187,9 @@
 			.context("failed to call remote host for decrypt")?
 			.trim()
 			.to_owned();
-		Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)
+		z85::decode(encoded).context("bad encoded data? outdated host?")
 	}
 
-	#[must_use]
 	pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {
 		let data = self.data();
 		let Some(host_secrets) = data.host_secrets.get(host) else {
@@ -215,7 +200,6 @@
 		};
 		Ok(secret.clone())
 	}
-	#[must_use]
 	pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {
 		let data = self.data();
 		let Some(secret) = data.shared_secrets.get(secret) else {
@@ -223,13 +207,20 @@
 		};
 		Ok(secret.clone())
 	}
+	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {
+		self.config_field
+			.get_field_deep(["sharedSecrets", secret, "expectedOwners"])
+			.await?
+			.as_json()
+			.await
+	}
 
 	pub fn save(&self) -> Result<()> {
 		let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;
 		let data = nixlike::serialize(&self.data() as &FleetData)?;
 		tempfile.write_all(
 			format!(
-				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n",
+				"# This file contains fleet state and shouldn't be edited by hand\n\n{}\n\n# vim: ts=2 et nowrap\n",
 				data
 			)
 			.as_bytes(),
@@ -259,19 +250,35 @@
 	// TODO: unhardcode x86_64-linux
 	/// Override detected system for host, to perform builds via
 	/// binfmt-declared qemu instead of trying to crosscompile
-	#[clap(long, default_value = "x86_64-linux")]
+	#[clap(long, default_value = "detect")]
 	pub local_system: String,
 }
 
 impl FleetOpts {
 	pub async fn build(mut self, nix_args: Vec<OsString>) -> Result<Config> {
-		let local_system = self.local_system.clone();
 		if self.localhost.is_none() {
 			self.localhost
 				.replace(hostname::get().unwrap().to_str().unwrap().to_owned());
 		}
 		let directory = current_dir()?;
 
+		let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;
+		let root_field = pool.get().await?;
+
+		if self.local_system == "detect" {
+			let builtins_field = Field::field(root_field.clone(), "builtins").await?;
+			let system = builtins_field.get_field("currentSystem").await?;
+			self.local_system = system.as_json().await?;
+		}
+		let local_system = self.local_system.clone();
+
+		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;
+
+		let fleet_field = fleet_root
+			.get_field_deep(["default", &local_system])
+			.await?;
+		let config_field = fleet_field.get_field("configUnchecked").await?;
+
 		let mut fleet_data_path = directory.clone();
 		fleet_data_path.push("fleet.nix");
 		let bytes = std::fs::read_to_string(fleet_data_path)?;
@@ -283,6 +290,8 @@
 			data,
 			local_system,
 			nix_args,
+			fleet_field,
+			config_field,
 		})))
 	}
 }
modifiedcmds/fleet/src/keys.rsdiffbeforeafterboth
--- a/cmds/fleet/src/keys.rs
+++ b/cmds/fleet/src/keys.rs
@@ -3,6 +3,7 @@
 use crate::command::MyCommand;
 use crate::host::Config;
 use anyhow::{anyhow, Result};
+use itertools::Itertools;
 use tracing::warn;
 
 impl Config {
@@ -40,9 +41,15 @@
 		age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))
 	}
 
+	#[allow(dead_code)]
 	pub async fn orphaned_data(&self) -> Result<Vec<String>> {
 		let mut out = Vec::new();
-		let host_names = self.list_hosts().await?;
+		let host_names = self
+			.list_hosts()
+			.await?
+			.into_iter()
+			.map(|h| h.name)
+			.collect_vec();
 		for hostname in self
 			.data()
 			.hosts
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -1,9 +1,12 @@
 #![feature(try_blocks)]
 
-pub mod cmds;
-pub mod command;
-pub mod host;
-pub mod keys;
+pub(crate) mod cmds;
+pub(crate) mod command;
+pub(crate) mod host;
+pub(crate) mod keys;
+
+pub(crate) mod extra_args;
+pub(crate) mod better_nix_eval;
 
 mod fleetdata;
 
@@ -14,13 +17,18 @@
 use clap::Parser;
 
 use cmds::{build_systems::BuildSystems, info::Info, secrets::Secrets};
+use futures::future::LocalBoxFuture;
+use futures::stream::FuturesUnordered;
+use futures::TryStreamExt;
 use host::{Config, FleetOpts};
 use indicatif::{ProgressState, ProgressStyle};
-use tokio::process::Command;
 use tracing::{info, metadata::LevelFilter};
+use tracing::{info_span, Instrument};
 use tracing_indicatif::IndicatifLayer;
 use tracing_subscriber::{prelude::*, EnvFilter};
 
+use crate::command::MyCommand;
+
 #[derive(Parser)]
 struct Prefetch {}
 impl Prefetch {
@@ -31,20 +39,28 @@
 			info!("nothing to prefetch: no prefetch directory");
 			return Ok(());
 		}
+		let tasks = <FuturesUnordered<LocalBoxFuture<Result<()>>>>::new();
 		for entry in std::fs::read_dir(&prefetch_dir)? {
-			let entry = entry?;
-			if !entry.metadata()?.is_file() {
-				bail!("only files should exist in prefetch directory");
-			}
-			info!("prefetching {:?}", entry.file_name());
-			let mut path = OsString::new();
-			path.push("file://");
-			path.push(entry.path());
-			let status = Command::new("nix-prefetch-url").arg(path).status().await?;
-			if !status.success() {
-				bail!("failed with {status}");
-			}
+			tasks.push(Box::pin(async {
+				let entry = entry?;
+				if !entry.metadata()?.is_file() {
+					bail!("only files should exist in prefetch directory");
+				}
+				let span = info_span!(
+					"prefetching",
+					name = entry.file_name().to_string_lossy().as_ref()
+				);
+				let mut path = OsString::new();
+				path.push("file://");
+				path.push(entry.path());
+
+				let mut status = MyCommand::new("nix");
+				status.arg("store").arg("prefetch-file").arg(path);
+				status.run_nix_string().instrument(span).await?;
+				Ok(())
+			}));
 		}
+		tasks.try_collect::<Vec<()>>().await?;
 		Ok(())
 	}
 }
@@ -81,8 +97,28 @@
 	Ok(())
 }
 
-#[tokio::main]
-async fn main() -> Result<()> {
+// fn main() -> Result<()> {
+// 	let pool = r2d2::Builder::<NixSessionPool>::new()
+// 		.min_idle(Some(1))
+// 		.max_lifetime(Some(Duration::from_secs(10)))
+// 		.build(NixSessionPool {
+// 			flake: ".".to_owned(),
+// 			nix_args: vec![],
+// 		})?;
+// 	let conn = pool.get()?;
+// 	let field = Field::root(conn);
+// 	// let builtins = field.get_field("builtins")?;
+// 	let cur_sys: String = field.get_field("builtins")?.as_json()?;
+// 	eprintln!("current system = {cur_sys}");
+// 	let v = field.get_field("fleetConfigurations")?;
+// 	eprintln!("configs = {:?}", v.list_fields()?);
+// 	let d = v.get_field("default")?;
+// 	dbg!(d.list_fields());
+// 	Ok(())
+// }
+//
+
+fn setup_logging() {
 	let indicatif_layer = IndicatifLayer::new().with_progress_style(
 		ProgressStyle::with_template(
 			"{color_start}{span_child_prefix} {span_name}{{{span_fields}}}{color_end} {wide_msg} {color_start}{pos:>7}/{len:7}{elapsed}{color_end}",
@@ -124,10 +160,19 @@
 		)
 		.with(indicatif_layer)
 		.init();
-	info!("Starting");
-	let mut os_args = std::env::args_os();
-	let opts = RootOpts::parse_from((&mut os_args).take_while(|v| v != "--"));
-	let config = opts.fleet_opts.build(os_args.collect()).await?;
+}
+
+#[tokio::main]
+async fn main() -> Result<()> {
+	setup_logging();
+	let _ = better_nix_eval::TOKIO_RUNTIME.set(tokio::runtime::Handle::current());
+
+	let nix_args = std::env::var_os("NIX_ARGS")
+		.map(|a| extra_args::parse_os(&a))
+		.transpose()?
+		.unwrap_or_default();
+	let opts = RootOpts::parse();
+	let config = opts.fleet_opts.build(nix_args).await?;
 
 	match run_command(&config, opts.command).await {
 		Ok(()) => {
deletedcmds/fleet/src/nix_eval.rsdiffbeforeafterboth
--- a/cmds/fleet/src/nix_eval.rs
+++ /dev/null
@@ -1,256 +0,0 @@
-//! Calling nix eval for everything is slow, it is not easy to link nix evaluator itself,
-//! and tvix-nix doesn't have proper flake support. Fleets solution: automating nix repl calls.
-//!
-//! Api is synchronous, yet it is good enough with pooling, and in environment without IFDs for using
-//! those blocking calls from async code.
-
-use std::borrow::Cow;
-use std::sync::{Arc, Mutex};
-use std::time::Instant;
-
-use anyhow::{anyhow, bail, ensure, Context, Result};
-use itertools::Itertools;
-use r2d2::PooledConnection;
-use rexpect::session::{PtyReplSession, PtySession};
-use serde::de::DeserializeOwned;
-use std::ffi::OsString;
-use tracing::info_span;
-
-fn parse_error(res: &str) -> Option<String> {
-	let res = if let Some(v) = res.strip_prefix("error: ") {
-		if let Some((first_line, next)) = v.split_once('\n') {
-			format!("{first_line}\n{}", unindent::unindent(next))
-		} else {
-			v.trim_start().to_owned()
-		}
-	} else if let Some(v) = res.strip_prefix("error:\n") {
-		let mut v = v.to_owned();
-		v.insert(0, '\n');
-		unindent::unindent(&v).trim_start().to_owned()
-	} else {
-		return None;
-	};
-	let res = res.trim_end();
-	Some(
-		res.replace('Â', "")
-			.split('\n')
-			.map(|l| l.strip_prefix("â\u{80}¦ ").unwrap_or(l))
-			.join("\n"),
-	)
-}
-pub struct NixSessionPool {
-	pub flake: OsString,
-	pub nix_args: Vec<OsString>,
-}
-
-#[derive(Debug)]
-pub struct NixPoolError(anyhow::Error);
-impl From<anyhow::Error> for NixPoolError {
-	fn from(value: anyhow::Error) -> Self {
-		Self(value)
-	}
-}
-impl std::error::Error for NixPoolError {}
-impl std::fmt::Display for NixPoolError {
-	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-		self.0.fmt(f)
-	}
-}
-
-impl r2d2::ManageConnection for NixSessionPool {
-	type Connection = NixSession;
-	type Error = NixPoolError;
-
-	fn connect(&self) -> std::result::Result<Self::Connection, Self::Error> {
-		Ok(NixSession::new(&self.flake, &self.nix_args, None)?)
-	}
-
-	fn is_valid(&self, conn: &mut Self::Connection) -> std::result::Result<(), Self::Error> {
-		let res = conn.expression_result("2 + 2")?;
-		if res != "4" {
-			return Err(anyhow!("basic expression failed").into());
-		}
-		Ok(())
-	}
-
-	fn has_broken(&self, conn: &mut Self::Connection) -> bool {
-		conn.finished
-	}
-}
-
-pub struct NixSession {
-	session: PtyReplSession,
-	next_id: u32,
-	free_list: Vec<u32>,
-	finished: bool,
-}
-impl NixSession {
-	fn new(flake: &OsString, args: &[OsString], timeout: Option<u64>) -> Result<Self> {
-		let mut cmd = std::process::Command::new("nix");
-		cmd.arg("repl");
-		cmd.arg(flake);
-		for arg in args {
-			cmd.arg(arg);
-		}
-		cmd.env("TERM", "dumb");
-		cmd.env("NO_COLOR", "1");
-		let pty_session = rexpect::session::spawn_command(cmd, timeout)?;
-		let mut repl = PtyReplSession {
-			prompt: "nix-repl> ".to_string(),
-			pty_session,
-			quit_command: Some(":q".to_string()),
-			echo_on: true,
-		};
-		repl.wait_for_prompt()?;
-		Ok(Self {
-			session: repl,
-			next_id: 0,
-			free_list: vec![],
-			finished: false,
-		})
-	}
-	fn expression_result(&mut self, cmd: &str) -> Result<String> {
-		dbg!(cmd);
-		self.session.send_line(cmd)?;
-		dbg!("waiting");
-		let result = self.session.wait_for_prompt()?;
-		let result = strip_ansi_escapes::strip_str(&result);
-		let result = result.trim();
-		dbg!(result);
-		Ok(result.to_owned())
-	}
-	fn json_result<V: DeserializeOwned>(&mut self, cmd: &str) -> Result<V> {
-		let v = match self.expression_result(&format!("builtins.toJSON ({cmd})")) {
-			Ok(v) => {
-				if let Some(e) = parse_error(&v) {
-					bail!("{e}")
-				}
-				v
-			}
-			Err(e) => {
-				self.finished = true;
-				bail!("{e}")
-			}
-		};
-		// Remove outer quoting
-		let v: String = serde_json::from_str(&v)?;
-		Ok(serde_json::from_str(&v)?)
-	}
-	/// Id should be immediately used
-	fn allocate_id(&mut self) -> u32 {
-		if let Some(free) = self.free_list.pop() {
-			free
-		} else {
-			let v = self.next_id;
-			self.next_id += 1;
-			v
-		}
-	}
-	fn allocate_result(&mut self, cmd: &str) -> Result<u32> {
-		let id = self.allocate_id();
-		match self.expression_result(&format!("sess_field_{id} = ({cmd})")) {
-			Ok(v) => {
-				if let Some(e) = parse_error(&v) {
-					self.free_list.push(id);
-					bail!("{e}")
-				}
-			}
-			Err(e) => {
-				self.finished = true;
-			}
-		}
-
-		Ok(id)
-	}
-	/// Nix has no way to deallocate variable, yet GC will correct everything not reachable.
-	fn free_id(&mut self, id: u32) {
-		if let Err(e) = self.expression_result(&format!("sess_field_{id} = null")) {
-			self.finished = true;
-		} else {
-			self.free_list.push(id)
-		}
-	}
-}
-
-#[derive(Clone, Debug)]
-enum Index {
-	String(String),
-	Idx(u32),
-}
-
-pub struct Field {
-	full_path: Vec<Index>,
-	session: Arc<Mutex<PooledConnection<NixSessionPool>>>,
-	value: Option<u32>,
-}
-impl Field {
-	pub fn root(conn: PooledConnection<NixSessionPool>) -> Self {
-		Self {
-			full_path: vec![],
-			session: Arc::new(Mutex::new(conn)),
-			value: None,
-		}
-	}
-	pub fn get_field_deep<'a>(&self, name: impl IntoIterator<Item = &'a str>) -> Result<Self> {
-		let mut iter = name.into_iter();
-
-		let mut full_path = self.full_path.clone();
-		let mut query = if let Some(id) = self.value {
-			format!("sess_field_{id}")
-		} else {
-			let first = iter.next().expect("name not empty");
-			ensure!(
-				!(first.contains('.') | first.contains(' ')),
-				"bad name for root query: {first}"
-			);
-			full_path.push(Index::String(first.to_string()));
-			first.to_string()
-		};
-		for v in iter {
-			full_path.push(Index::String(v.to_string()));
-			// Escape
-			let escaped = nixlike::serialize(v)?;
-			let escaped = escaped.trim();
-			query.push('.');
-			query.push_str(escaped);
-		}
-
-		let vid = self
-			.session
-			.lock()
-			.unwrap()
-			.allocate_result(&query)
-			.with_context(|| format!("full path: {:?}", full_path))?;
-		Ok(Self {
-			full_path,
-			session: self.session.clone(),
-			value: Some(vid),
-		})
-	}
-	pub fn get_field<'a>(&self, name: &str) -> Result<Self> {
-		self.get_field_deep([name])
-	}
-	pub fn as_json<V: DeserializeOwned>(&self) -> Result<V> {
-		let id = self.value.expect("can't serialize root field");
-		self.session
-			.lock()
-			.unwrap()
-			.json_result(&format!("sess_field_{id}"))
-			.with_context(|| format!("full path: {:?}", self.full_path))
-	}
-	pub fn list_fields(&self) -> Result<Vec<String>> {
-		let id = self.value.expect("can't list root fields");
-		self.session
-			.lock()
-			.unwrap()
-			.json_result(&format!("builtins.attrNames sess_field_{id}"))
-			.with_context(|| format!("full path: {:?}", self.full_path))
-	}
-}
-impl Drop for Field {
-	fn drop(&mut self) {
-		if let Some(id) = self.value {
-			self.session.lock().unwrap().free_id(id)
-		}
-	}
-}
modifiedcmds/install-secrets/Cargo.tomldiffbeforeafterboth
--- a/cmds/install-secrets/Cargo.toml
+++ b/cmds/install-secrets/Cargo.toml
@@ -4,18 +4,18 @@
 edition = "2021"
 
 [dependencies]
-age = { version = "0.9.0", features = ["ssh"] }
-anyhow = "1.0.44"
+age = { version = "0.9.2", features = ["ssh"] }
+anyhow = "1.0.75"
 env_logger = "0.10.0"
-log = "0.4.14"
-nix = "0.26.1"
-serde = { version = "1.0.130", features = ["derive"] }
-serde_json = "1.0.89"
-clap = { version = "4.0.29", features = [
+log = "0.4.20"
+nix = {version = "0.27.1", features = ["user", "fs"]}
+serde = { version = "1.0.190", features = ["derive"] }
+serde_json = "1.0.107"
+clap = { version = "4.4.7", features = [
 	"derive",
 	"env",
 	"wrap_help",
 	"unicode",
 ] }
-tempfile = "3.2.0"
-z85 = "3.0.3"
+tempfile = "3.8.1"
+z85 = "3.0.5"
modifiedcmds/install-secrets/src/main.rsdiffbeforeafterboth
--- a/cmds/install-secrets/src/main.rs
+++ b/cmds/install-secrets/src/main.rs
@@ -4,7 +4,7 @@
 use clap::Parser;
 use log::{error, info, warn};
 use nix::sys::stat::Mode;
-use nix::unistd::{chown, Group, User};
+use nix::unistd::{User, Group, chown};
 use serde::{Deserialize, Deserializer};
 use std::fmt::{self, Display};
 use std::fs::{self, File};
@@ -161,7 +161,7 @@
 	let mut hashed = File::create(&value.secret_path)?;
 
 	// File is owned by root, and only root can modify it
-	let decrypted = decrypt(&secret, identity)?;
+	let decrypted = decrypt(secret, identity)?;
 	if decrypted.is_empty() {
 		warn!("secret is decoded as empty, something is broken?");
 	}
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -5,11 +5,10 @@
 
 [dependencies]
 alejandra = {git = "https://github.com/kamadorueda/alejandra"}
-rnix = "0.10.2"
-linked-hash-map = "0.5.4"
-peg = "0.8.0"
-serde = "1.0.130"
-thiserror = "1.0.29"
-serde_json = "1.0.91"
-ron = "0.8.0"
+linked-hash-map = "0.5.6"
+peg = "0.8.2"
+serde = "1.0.190"
+thiserror = "1.0.50"
+serde_json = "1.0.107"
+ron = "0.8.1"
 serde-transcode = "1.1.1"
modifiedcrates/nixlike/src/lib.rsdiffbeforeafterboth
--- a/crates/nixlike/src/lib.rs
+++ b/crates/nixlike/src/lib.rs
@@ -119,6 +119,12 @@
 	Ok(serialize_value_pretty(value))
 }
 
+pub fn format_identifier(i: &str) -> String {
+	let mut out = String::new();
+	to_string::write_identifier(i, &mut out);
+	out
+}
+
 #[test]
 fn test() {
 	assert_eq!(serialize("Hello\nworld").unwrap(), "\"Hello\\nworld\"\n");
modifiedcrates/nixlike/src/se_impl.rsdiffbeforeafterboth
--- a/crates/nixlike/src/se_impl.rs
+++ b/crates/nixlike/src/se_impl.rs
@@ -212,7 +212,7 @@
 	}
 
 	fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
-		Ok(Value::Number(v as i64))
+		Ok(Value::Number(v))
 	}
 
 	fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
modifiedcrates/nixlike/src/to_string.rsdiffbeforeafterboth
--- a/crates/nixlike/src/to_string.rs
+++ b/crates/nixlike/src/to_string.rs
@@ -1,24 +1,26 @@
 use crate::Value;
 
-fn write_nix_obj_key_buf(k: &str, v: &Value, out: &mut String) {
-	if k.contains('.') {
-		out.push_str("\"");
-		out.push_str(k);
-		out.push_str("\"");
+pub fn write_identifier(k: &str, out: &mut String) {
+	if k.contains(['.', '\'', '\"', '\\', '\n', '\t', '\r', '$']) {
+		write_nix_str(k, out);
 	} else {
 		out.push_str(k);
 	}
+}
+
+fn write_nix_obj_key_buf(k: &str, v: &Value, out: &mut String) {
+	write_identifier(k, out);
 	match v {
 		Value::Object(o) if o.len() == 1 => {
 			let (k, v) = o.iter().next().unwrap();
 
-			out.push_str(".");
+			out.push('.');
 			write_nix_obj_key_buf(k, v, out);
 		}
 		v => {
 			out.push_str(" = ");
 			write_nix_buf(v, out);
-			out.push_str(";");
+			out.push(';');
 		}
 	}
 }
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -38,11 +38,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1696884899,
-        "narHash": "sha256-SZILkoh8KZxjvFHO3yzOUw7n1Mf9WqMdUqoxf8eKPM4=",
+        "lastModified": 1698350982,
+        "narHash": "sha256-zoEV8Ad3bOAejp0ys/mOpaHSWrzK+GupZwGGYfuWuEY=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "ba10489eae3b2b2f665947b516e7043594a235c8",
+        "rev": "dd83f9de26ff7c0326468b659ea4729fa5cf6262",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1696817516,
-        "narHash": "sha256-Xt9OY4Wnk9/vuUfA0OHFtmSlaen5GyiS9msgwOz3okI=",
+        "lastModified": 1698199907,
+        "narHash": "sha256-n8RtHBIb0rLuYs4RDehW6mj6r6Yam/ODY1af/VCcurw=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "c0df7f2a856b5ff27a3ce314f6d7aacf5fda546f",
+        "rev": "22b8d29fd22cfaa2c311e0d6fd8a0ed9c2a1152b",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -15,7 +15,7 @@
           inherit system; overlays = [ (import rust-overlay) ];
         };
       llvmPkgs = pkgs.buildPackages.llvmPackages_11;
-      rust = (pkgs.rustChannelOf { date = "2023-10-05"; channel = "nightly"; }).default.override { extensions = [ "rust-src" "rust-analyzer" ]; };
+      rust = (pkgs.rustChannelOf { date = "2023-10-20"; channel = "nightly"; }).default.override { extensions = [ "rust-src" "rust-analyzer" ]; };
       rustPlatform = pkgs.makeRustPlatform { cargo = rust; rustc = rust; };
     in
     {
@@ -29,6 +29,7 @@
 
           pkg-config
           openssl
+          bacon
         ];
       };
     });
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -55,6 +55,14 @@
         description = "Time in hours, in which this secret should be regenerated";
         default = null;
       };
+      createdAt = mkOption {
+        type = nullOr str;
+        default = null;
+      };
+      expiresAt = mkOption {
+        type = nullOr str;
+        default = null;
+      };
 
       owners = mkOption {
         type = listOf str;
@@ -82,23 +90,24 @@
   };
   hostSecret = with types; {
     options = {
-      generator = mkOption {
-        type = package;
-        description = "Derivation to execute for secret generation";
+      createdAt = mkOption {
+        type = nullOr str;
+        default = null;
       };
-      expireIn = mkOption {
-        type = nullOr int;
-        description = "Time in hours, in which this secret should be regenerated";
+      expiresAt = mkOption {
+        type = nullOr str;
         default = null;
       };
       public = mkOption {
         type = nullOr str;
-        description = "Secret public data";
+        description = "Secret public data. Imported from fleet.nix";
         default = null;
       };
       secret = mkOption {
-        type = str;
-        description = "Encrypted secret data";
+        type = nullOr str;
+        description = "Encrypted secret data. Imported from fleet.nix";
+        default = null;
+        internal = true;
       };
     };
   };
@@ -113,7 +122,8 @@
     hostSecrets = mkOption {
       type = attrsOf (attrsOf (submodule hostSecret));
       default = { };
-      description = "Host secrets";
+      description = "Host secrets. Imported from fleet.nix";
+      internal = true;
     };
   };
   config = {