git.delta.rocks / jrsonnet / refs/commits / 7e2e5c591e04

difftreelog

refactor more repl abstractions

Yaroslav Bolyukin2023-12-27parent: #624fe7e.patch.diff
in: trunk

11 files changed

modifiedcmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth
--- a/cmds/fleet/src/better_nix_eval.rs
+++ b/cmds/fleet/src/better_nix_eval.rs
@@ -365,47 +365,176 @@
 #[derive(Clone)]
 pub struct NixSession(Arc<tokio::sync::Mutex<PooledConnection<NixSessionPoolInner>>>);
 
+#[derive(Clone)]
+pub struct NixExprBuilder {
+	out: String,
+	used_fields: Vec<Field>,
+}
+impl NixExprBuilder {
+	pub fn object() -> Self {
+		NixExprBuilder {
+			out: "{ ".to_owned(),
+			used_fields: Vec::new(),
+		}
+	}
+	pub fn string(s: &str) -> Self {
+		NixExprBuilder {
+			out: nixlike::serialize(s)
+				.expect("no problems with serializing_string")
+				.trim_end()
+				.to_owned(),
+			used_fields: Vec::new(),
+		}
+	}
+	pub fn serialized(v: impl Serialize) -> Self {
+		let serialized = nixlike::serialize(v).expect("invalid value for apply");
+		Self {
+			out: serialized.trim_end().to_owned(),
+			used_fields: Vec::new(),
+		}
+	}
+	pub fn field(f: Field) -> Self {
+		Self {
+			out: format!("sess_field_{}", f.0.value.expect("no value")),
+			used_fields: vec![f],
+		}
+	}
+	pub fn end_obj(&mut self) {
+		self.out.push('}');
+	}
+	pub fn obj_key(&mut self, name: Self, value: Self) {
+		self.out.push_str(r#""${"#);
+		self.extend(name);
+		self.out.push_str(r#"}" = "#);
+		self.extend(value);
+		self.out.push_str("; ");
+	}
+
+	pub fn extend(&mut self, e: Self) {
+		self.out.push_str(&e.out);
+		self.used_fields.extend(e.used_fields);
+	}
+
+	pub fn session(&self) -> NixSession {
+		let mut session = None;
+		for ele in &self.used_fields {
+			if session.is_none() {
+				session = Some(ele.0.session.clone());
+				continue;
+			}
+			let session = &session.as_ref().expect("checked").0;
+			let ele_sess = &ele.0.session.0;
+			assert!(
+				Arc::ptr_eq(session, ele_sess),
+				"can't mix fields from different session"
+			);
+		}
+		session.expect("expr without fields used")
+	}
+	pub fn index_attr(&mut self, s: &str) {
+		let escaped = nixlike::serialize(s).expect("string");
+		self.out.push('.');
+		self.out.push_str(escaped.trim_end());
+	}
+}
+
+#[macro_export]
+macro_rules! nix_expr_inner {
+	(Obj { $($ident:ident: $($val:tt)+),* $(,)? }) => {{
+		use $crate::better_nix_eval::NixExprBuilder;
+		let mut out = NixExprBuilder::object();
+		$(
+			out.obj_key(
+				NixExprBuilder::string(stringify!($ident)),
+				$crate::nix_expr_inner!($($val)+),
+			);
+		)*
+		out.end_obj();
+		out
+	}};
+	(@field($o:ident) . $var:ident $($tt:tt)*) => {{
+		$o.index_attr(stringify!($var));
+		nix_expr_inner!(@field($o) $($tt)*);
+	}};
+	(@field($o:ident) [{ $v:expr }] $($tt:tt)*) => {{
+		$o.push(Index::attr(&$v));
+		nix_expr_inner!(@o($o) $($tt)*);
+	}};
+	(@field($o:ident) [ $($var:tt)+ ] $($tt:tt)*) => {{
+		$o.push(Index::Expr($crate::nix_expr_inner!($($var)+)));
+		nix_expr_inner!(@o($o) $($tt)*);
+	}};
+	(@field($o:ident) ($($var:tt)*) $($tt:tt)*) => {
+		$o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
+		nix_expr_inner!(@o($o) $($tt)*);
+	};
+	(@field($o:ident)) => {};
+	($field:ident $($tt:tt)*) => {{
+		use $crate::{better_nix_eval::NixExprBuilder, nix_expr_inner};
+		#[allow(unused_mut, reason = "might be used if indexed")]
+		let mut out = NixExprBuilder::field($field);
+		nix_expr_inner!(@field(out) $($tt)*);
+		out
+	}};
+	($v:literal) => {{
+		use $crate::better_nix_eval::NixExprBuilder;
+		NixExprBuilder::string($v)
+	}};
+	({$v:expr}) => {{
+		use $crate::better_nix_eval::NixExprBuilder;
+		NixExprBuilder::serialized(&$v)
+	}}
+}
 #[macro_export]
-macro_rules! nix_path {
-	(@o($o:ident) $var:ident $($tt:tt)*) => {{
-		$o.push(Index::var(stringify!($var)));
-		nix_path!(@o($o) $($tt)*);
+macro_rules! nix_expr {
+	($($tt:tt)+) => {{
+		use $crate::{better_nix_eval::{NixExprBuilder, Field}, nix_expr_inner};
+		let expr = nix_expr_inner!($($tt)+);
+		Field::new(expr.session(), expr.out)
 	}};
+}
+
+#[macro_export]
+macro_rules! nix_go {
 	(@o($o:ident) . $var:ident $($tt:tt)*) => {{
 		$o.push(Index::attr(stringify!($var)));
-		nix_path!(@o($o) $($tt)*);
+		nix_go!(@o($o) $($tt)*);
 	}};
-	(@o($o:ident) . $var:literal $($tt:tt)*) => {{
-		$o.push(Index::attr($var));
-		nix_path!(@o($o) $($tt)*);
+	(@o($o:ident) [{ $v:expr }] $($tt:tt)*) => {{
+		$o.push(Index::attr(&$v));
+		nix_go!(@o($o) $($tt)*);
 	}};
-	(@o($o:ident) . { $var:expr } $($tt:tt)*) => {{
-		$o.push(Index::attr($var));
-		nix_path!(@o($o) $($tt)*);
+	(@o($o:ident) [ $($var:tt)+ ] $($tt:tt)*) => {{
+		$o.push(Index::Expr($crate::nix_expr_inner!($($var)+)));
+		nix_go!(@o($o) $($tt)*);
 	}};
-	(@o($o:ident) [ $var:literal ] $($tt:tt)*) => {{
-		$o.push(Index::idx($var));
-		nix_path!(@o($o) $($tt)*);
-	}};
-	(@o($o:ident) ($e:expr) $($tt:tt)*) => {
-		$o.push(Index::apply($e));
-		nix_path!(@o($o) $($tt)*);
+	(@o($o:ident) ($($var:tt)*) $($tt:tt)*) => {
+		$o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
+		nix_go!(@o($o) $($tt)*);
 	};
 	(@o($o:ident)) => {};
-	($($tt:tt)+) => {{
-		use $crate::{nix_path, better_nix_eval::Index};
+	($field:ident $($tt:tt)+) => {{
+		use $crate::{nix_go, better_nix_eval::Index};
+		let field = $field.clone();
 		let mut out = vec![];
-		nix_path!(@o(out) $($tt)*);
-		out
+		nix_go!(@o(out) $($tt)*);
+		field.select(out).await?
 	}}
 }
+#[macro_export]
+macro_rules! nix_go_json {
+	($($tt:tt)*) => {{
+		$crate::nix_go!($($tt)*).as_json().await?
+	}};
+}
 
 #[derive(Clone)]
 pub enum Index {
 	Var(String),
 	String(String),
 	Apply(String),
-	Idx(u32),
+	Expr(NixExprBuilder),
+	ExprApply(NixExprBuilder),
 }
 impl Index {
 	pub fn var(v: impl AsRef<str>) -> Self {
@@ -419,9 +548,6 @@
 	pub fn attr(v: impl AsRef<str>) -> Self {
 		Self::String(v.as_ref().to_owned())
 	}
-	pub fn idx(v: u32) -> Self {
-		Self::Idx(v)
-	}
 	pub fn apply(v: impl Serialize) -> Self {
 		let serialized = nixlike::serialize(v).expect("invalid value for apply");
 		Self::Apply(serialized.trim_end().to_owned())
@@ -440,9 +566,12 @@
 			Index::Apply(o) => {
 				write!(f, "<apply>({o})")
 			}
-			Index::Idx(i) => {
-				write!(f, "[{i}]")
+			Index::Expr(e) => {
+				write!(f, "[{}]", e.out)
 			}
+			Index::ExprApply(e) => {
+				write!(f, "<apply>({})", e.out)
+			}
 		}
 	}
 }
@@ -460,24 +589,45 @@
 		Ok(())
 	}
 }
-pub struct Field {
-	full_path: Vec<Index>,
+struct FieldInner {
+	full_path: Option<Vec<Index>>,
 	session: NixSession,
 	value: Option<u32>,
 }
+fn context(full_path: Option<&[Index]>, query: &str) -> String {
+	if let Some(full_path) = &full_path {
+		format!("full path: {}", PathDisplay(full_path))
+	} else {
+		format!("query: {query:?}")
+	}
+}
+#[derive(Clone)]
+pub struct Field(Arc<FieldInner>);
 impl Field {
 	fn root(session: NixSession) -> Self {
-		Self {
-			full_path: vec![],
+		Self(Arc::new(FieldInner {
+			full_path: Some(vec![]),
 			session,
 			value: None,
-		}
+		}))
 	}
-	pub async fn field(session: NixSession, field: &str) -> Result<Self> {
-		Self::root(session)
-			.select([Index::var(field)])
+	async fn new(session: NixSession, query: &str) -> Result<Self> {
+		let vid = session
+			.0
+			.lock()
 			.await
+			.execute_assign(query)
+			.await
+			.with_context(|| context(None, query))?;
+		Ok(Self(Arc::new(FieldInner {
+			full_path: None,
+			session,
+			value: Some(vid),
+		})))
 	}
+	pub async fn field(session: NixSession, field: &str) -> Result<Self> {
+		Self::root(session).select([Index::var(field)]).await
+	}
 	pub async fn get_json_deep<'a, V: DeserializeOwned>(
 		&self,
 		name: impl IntoIterator<Item = Index>,
@@ -486,22 +636,27 @@
 		field.as_json().await
 	}
 	pub async fn select<'a>(&self, name: impl IntoIterator<Item = Index>) -> Result<Self> {
+		let mut used_fields = Vec::new();
 		let mut name = name.into_iter();
 
-		let mut full_path = self.full_path.clone();
-		let mut query = if let Some(id) = self.value {
+		let mut full_path = self.0.full_path.clone();
+		let mut query = if let Some(id) = self.0.value {
 			format!("sess_field_{id}")
 		} else {
 			let first = name.next();
 			if let Some(Index::Var(i)) = first {
-				full_path.push(Index::Var(i.clone()));
+				if let Some(full_path) = &mut full_path {
+					full_path.push(Index::Var(i.clone()));
+				}
 				i.clone()
 			} else {
 				panic!("first path item should be variable, got {first:?}")
 			}
 		};
 		for v in name {
-			full_path.push(v.clone());
+			if let Some(full_path) = &mut full_path {
+				full_path.push(v.clone());
+			}
 			match v {
 				Index::Var(_) => panic!("var item may only be first"),
 				Index::String(s) => {
@@ -513,56 +668,85 @@
 					// In cases like `a {}.b` first `{}.b` will be evaluated, so `a {}` should be encased in `()`
 					query = format!("({query} {a})");
 				}
-				Index::Idx(idx) => {
-					query = format!("builtins.elemAt ({query}) {idx}");
+				Index::Expr(e) => {
+					let index = Field::new(self.0.session.clone(), &e.out).await?;
+					used_fields.push(index.clone());
+					query.push('.');
+					let index = format!("${{sess_field_{}}}", index.0.value.expect("value"));
+					query.push_str(&index);
+				}
+				Index::ExprApply(e) => {
+					let index = Field::new(self.0.session.clone(), &e.out).await?;
+					used_fields.push(index.clone());
+					query.push(' ');
+					let index = format!("sess_field_{}", index.0.value.expect("value"));
+					query.push_str(&index);
+					query = format!("({query})");
 				}
 			}
 		}
 
 		let vid = self
+			.0
 			.session
 			.0
 			.lock()
 			.await
 			.execute_assign(&query)
 			.await
-			.with_context(|| format!("full path: {}", PathDisplay(&full_path)))?;
-		Ok(Self {
+			.with_context(|| {
+				if let Some(full_path) = &full_path {
+					format!("full path: {}", PathDisplay(full_path))
+				} else {
+					format!("query: {query:?}")
+				}
+			})?;
+		Ok(Self(Arc::new(FieldInner {
 			full_path,
-			session: self.session.clone(),
+			session: self.0.session.clone(),
 			value: Some(vid),
-		})
+		})))
 	}
 	pub async fn as_json<V: DeserializeOwned>(&self) -> Result<V> {
-		let id = self.value.expect("can't serialize root field");
-		self.session
+		let id = self.0.value.expect("can't serialize root field");
+		let query = format!("sess_field_{id}");
+		self.0
+			.session
 			.0
 			.lock()
 			.await
-			.execute_expression_to_json(&format!("sess_field_{id}"))
+			.execute_expression_to_json(&query)
 			.await
-			.with_context(|| format!("full path: {}", PathDisplay(&self.full_path)))
+			.with_context(|| context(self.0.full_path.as_deref(), &query))
 	}
 	pub async fn list_fields(&self) -> Result<Vec<String>> {
-		let id = self.value.expect("can't list root fields");
-		self.session
+		let id = self.0.value.expect("can't list root fields");
+		let query = format!("builtins.attrNames sess_field_{id}");
+		self.0
+			.session
 			.0
 			.lock()
 			.await
-			.execute_expression_to_json(&format!("builtins.attrNames sess_field_{id}"))
+			.execute_expression_to_json(&query)
 			.await
-			.with_context(|| format!("full path: {}", PathDisplay(&self.full_path)))
+			.with_context(|| context(self.0.full_path.as_deref(), &query))
 	}
 	pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {
-		let id = self.value.expect("can't use build on not-value");
+		let id = self.0.value.expect("can't use build on not-value");
+		let query = format!(":b sess_field_{id}");
 		let vid = self
+			.0
 			.session
 			.0
 			.lock()
 			.await
-			.execute_expression_raw(&format!(":b sess_field_{id}"), &mut NixHandler::default())
+			.execute_expression_raw(&query, &mut NixHandler::default())
 			.await?;
-		ensure!(!vid.is_empty(), "build failed: {}", PathDisplay(&self.full_path));
+		ensure!(
+			!vid.is_empty(),
+			"build failed: {}",
+			context(self.0.full_path.as_deref(), &query),
+		);
 		let Some(vid) = vid.strip_prefix("This derivation produced the following outputs:\n")
 		else {
 			panic!("unexpected build output: {vid:?}");
@@ -576,7 +760,7 @@
 		Ok(outputs)
 	}
 }
-impl Drop for Field {
+impl Drop for FieldInner {
 	fn drop(&mut self) {
 		if let Some(id) = self.value {
 			if let Ok(mut lock) = self.session.0.try_lock() {
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -4,8 +4,8 @@
 
 use crate::command::MyCommand;
 use crate::host::Config;
-use crate::nix_path;
-use anyhow::{anyhow, Result, Context};
+use crate::nix_go;
+use anyhow::{anyhow, Result};
 use clap::Parser;
 use itertools::Itertools;
 use tokio::{task::LocalSet, time::sleep};
@@ -290,12 +290,10 @@
 	async fn build_task(self, config: Config, host: String) -> Result<()> {
 		info!("building");
 		let action = Action::from(self.subcommand.clone());
-		let drv = config
-			.fleet_field
-			.select(nix_path!(.buildSystems((serde_json::json!({
-				"localSystem": config.local_system.clone(),
-			}))).{action.build_attr()}.{&host}))
-			.await.context("system attribute")?;
+		let fleet_field = &config.fleet_field;
+		let drv = nix_go!(fleet_field.buildSystems(Obj {
+			localSystem: { config.local_system.clone() }
+		}));
 		let outputs = drv.build().await.map_err(|e| {
 			if action.build_attr() == "sdImage" {
 				info!("sd-image build failed");
modifiedcmds/fleet/src/cmds/info.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -1,7 +1,7 @@
 use std::collections::BTreeSet;
 
 use crate::host::Config;
-use crate::nix_path;
+use crate::nix_go_json;
 use anyhow::{ensure, Result};
 use clap::Parser;
 
@@ -37,12 +37,9 @@
 			InfoCmd::ListHosts { ref tagged } => {
 				'host: for host in config.list_hosts().await? {
 					if !tagged.is_empty() {
-						let tags: Vec<String> = config
-							.fleet_field
-							.select(nix_path!(.configuredSystems.{&host.name}.config.tags))
-							.await?
-							.as_json()
-							.await?;
+						let fleet_field = &config.fleet_field;
+						let tags: Vec<String> =
+							nix_go_json!(fleet_field.configuredSystems[{ host.name }].config.tags);
 						for tag in tagged {
 							if !tags.contains(tag) {
 								continue 'host;
@@ -64,20 +61,12 @@
 				let mut out = <BTreeSet<String>>::new();
 				let host = config.system_config(&host).await?;
 				if external {
-					out.extend(
-						host.select(nix_path!(.network.externalIps))
-							.await?
-							.as_json::<Vec<String>>()
-							.await?,
-					);
+					let data: Vec<String> = nix_go_json!(host.network.externalIps);
+					out.extend(data);
 				}
 				if internal {
-					out.extend(
-						host.select(nix_path!(.network.internalIps))
-							.await?
-							.as_json::<Vec<String>>()
-							.await?,
-					);
+					let data: Vec<String> = nix_go_json!(host.network.internalIps);
+					out.extend(data);
 				}
 				for ip in out {
 					data.push(ip);
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,9 +1,10 @@
 use crate::{
 	fleetdata::{FleetSecret, FleetSharedSecret},
-	host::Config, nix_path,
+	host::Config,
+	nix_go, nix_go_json,
 };
-use anyhow::{bail, ensure, Context, Result};
-use chrono::Utc;
+use anyhow::{anyhow, bail, ensure, Context, Result};
+use chrono::{DateTime, Utc};
 use clap::Parser;
 use futures::{StreamExt, TryStreamExt};
 use owo_colors::OwoColorize;
@@ -17,8 +18,8 @@
 use tracing::{error, info, info_span, warn};
 
 #[derive(Parser)]
-pub enum Secrets {
-	/// Force load keys for all defined hosts
+pub enum Secret {
+	/// Force load host keys for all defined hosts
 	ForceKeys,
 	/// Add secret, data should be provided in stdin
 	AddShared {
@@ -29,14 +30,20 @@
 		/// Override secret if already present
 		#[clap(long)]
 		force: bool,
+		/// Secret public part
 		#[clap(long)]
 		public: Option<String>,
+		/// Load public part from specified file
 		#[clap(long)]
 		public_file: Option<PathBuf>,
 
+		/// Create a notification on secret expiration
+		#[clap(long)]
+		expires_at: Option<DateTime<Utc>>,
+
 		/// Secret with this name already exists, override its value while keeping the same owners.
 		#[clap(long)]
-		readd: bool,
+		re_add: bool,
 	},
 	/// Add secret, data should be provided in stdin
 	Add {
@@ -81,12 +88,33 @@
 		prefer_identities: Vec<String>,
 	},
 	List {},
+	InvokeGenerator,
 }
 
-impl Secrets {
+impl Secret {
 	pub async fn run(self, config: &Config) -> Result<()> {
 		match self {
-			Secrets::ForceKeys => {
+			Secret::InvokeGenerator => {
+				let config_field = &config.config_unchecked_field;
+
+				let generate_impure =
+					nix_go!(config_field.sharedSecrets["kube-apiserver.pem"].generateImpure);
+				let on = nix_go!(generate_impure.on);
+				let call_package = nix_go!(
+					config_field.buildableSystems(Obj {
+						localSystem: { config.local_system.clone() }
+					})[on]
+						.config
+						.nixpkgs
+						.pkgs
+						.callPackage
+				);
+				let generator = nix_go!(call_package(generate_impure.generator));
+				let built = generator.build().await?;
+				// .as_json().await?;
+				dbg!(&built);
+			}
+			Secret::ForceKeys => {
 				for host in config.list_hosts().await? {
 					if config.should_skip(&host.name) {
 						continue;
@@ -94,19 +122,20 @@
 					config.key(&host.name).await?;
 				}
 			}
-			Secrets::AddShared {
+			Secret::AddShared {
 				mut machines,
 				name,
 				force,
 				public,
 				public_file,
-				readd,
+				expires_at,
+				re_add,
 			} => {
 				let exists = config.has_shared(&name);
-				if exists && !force && !readd {
+				if exists && !force && !re_add {
 					bail!("secret already defined");
 				}
-				if readd {
+				if re_add {
 					// Fixme: use clap to limit this usage
 					ensure!(!force, "--force and --readd are not compatible");
 					ensure!(exists, "secret doesn't exists");
@@ -137,7 +166,7 @@
 							.map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)
 							.collect();
 						let mut encryptor = age::Encryptor::with_recipients(recipients)
-							.expect("recipients provided")
+							.ok_or_else(|| anyhow!("no recipients provided"))?
 							.wrap_output(&mut encrypted)?;
 						io::copy(&mut Cursor::new(input), &mut encryptor)?;
 						encryptor.finish()?;
@@ -150,7 +179,7 @@
 						owners: machines,
 						secret: FleetSecret {
 							created_at: Utc::now(),
-							expires_at: None,
+							expires_at,
 							secret,
 							public: match (public, public_file) {
 								(Some(v), None) => Some(v),
@@ -164,7 +193,7 @@
 					},
 				);
 			}
-			Secrets::Add {
+			Secret::Add {
 				machine,
 				name,
 				force,
@@ -211,7 +240,7 @@
 			}
 			// TODO: Instead of using sudo, decode secret on remote machine
 			#[allow(clippy::await_holding_refcell_ref)]
-			Secrets::Read {
+			Secret::Read {
 				name,
 				machine,
 				plaintext,
@@ -228,7 +257,7 @@
 					println!("{}", z85::encode(&data));
 				}
 			}
-			Secrets::UpdateShared {
+			Secret::UpdateShared {
 				name,
 				machines,
 				mut add_machines,
@@ -321,7 +350,7 @@
 				secret.secret.secret = encrypted;
 				config.replace_shared(name, secret);
 			}
-			Secrets::Regenerate { prefer_identities } => {
+			Secret::Regenerate { prefer_identities } => {
 				{
 					let expected_shared_set = config
 						.list_configured_shared()
@@ -337,10 +366,9 @@
 				for name in &config.list_shared() {
 					info!("updating secret: {name}");
 					let mut data = config.shared_secret(name)?;
-					let expected_owners: Vec<String> = config
-						.config_field
-						.get_json_deep(nix_path!(sharedSecrets.{name}.expectedOwners))
-						.await?;
+					let config_field = &config.config_field;
+					let expected_owners: Vec<String> =
+						nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);
 					if expected_owners.is_empty() {
 						warn!("secret was removed from fleet config: {name}, removing from data");
 						to_remove.push(name.to_string());
@@ -350,10 +378,8 @@
 					let expected_set = expected_owners.iter().collect::<HashSet<_>>();
 					let should_remove = set.difference(&expected_set).next().is_some();
 					if set != expected_set {
-						let owner_dependent: bool = config
-							.config_field
-							.get_json_deep(nix_path!(.sharedSecrets.{name}.ownerDependent))
-							.await?;
+						let owner_dependent: bool =
+							nix_go_json!(config_field.sharedSecrets[{ name }].ownerDependent);
 						if !owner_dependent {
 							warn!("reencrypting secret '{name}' for new owner set");
 							// TODO: force regeneration
@@ -401,7 +427,7 @@
 					config.remove_shared(&k);
 				}
 			}
-			Secrets::List {} => {
+			Secret::List {} => {
 				let _span = info_span!("loading secrets").entered();
 				let configured = config.list_configured_shared().await?;
 				#[derive(Tabled)]
modifiedcmds/fleet/src/command.rsdiffbeforeafterboth
before · cmds/fleet/src/command.rs
1use std::{2	collections::HashMap,3	ffi::OsStr,4	process::Stdio,5	sync::{Arc, Mutex},6	task::Poll,7};89use anyhow::{anyhow, Result};10use futures::StreamExt;11use itertools::Either;12use once_cell::sync::Lazy;13use openssh::{OverSsh, Session};14use regex::Regex;15use serde::{de::Visitor, Deserialize};16use tokio::{io::AsyncRead, process::Command, select};17use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};18use tracing::{info, info_span, warn, Span};19use tracing_indicatif::span_ext::IndicatifSpanExt;2021fn escape_bash(input: &str, out: &mut String) {22	const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}";23	if input.chars().all(|c| !TO_ESCAPE.contains(c)) {24		out.push_str(input);25		return;26	}27	out.push('\'');28	for (i, v) in input.split('\'').enumerate() {29		if i != 0 {30			out.push_str("'\"'\"'");31		}32		out.push_str(v);33	}34	out.push('\'');35}36fn ostoutf8(os: impl AsRef<OsStr>) -> String {37	os.as_ref().to_str().expect("non-utf8 data").to_owned()38}39#[derive(Clone)]40pub struct MyCommand {41	command: String,42	args: Vec<String>,43	env: Vec<(String, String)>,44	ssh_session: Option<Arc<Session>>,45}46impl MyCommand {47	pub fn new(cmd: impl AsRef<OsStr>) -> Self {48		assert!(!cmd.as_ref().is_empty());49		Self {50			command: ostoutf8(cmd),51			args: vec![],52			env: vec![],53			ssh_session: None,54		}55	}56	fn into_args(self) -> Vec<String> {57		let mut out = Vec::new();58		if !self.env.is_empty() {59			out.push("env".to_owned());60			for (k, v) in self.env {61				assert!(!k.contains('='));62				out.push(format!("{k}={v}"));63			}64		}65		out.push(self.command);66		out.extend(self.args);67		out68	}69	fn into_string(self) -> String {70		let mut out = String::new();71		if !self.env.is_empty() {72			out.push_str("env");73			for (k, v) in self.env {74				out.push(' ');75				assert!(!k.contains('='));76				escape_bash(&k, &mut out);77				out.push('=');78				escape_bash(&v, &mut out);79			}80		}81		if !out.is_empty() {82			out.push(' ');83		}84		escape_bash(&self.command, &mut out);85		for arg in self.args {86			out.push(' ');87			escape_bash(&arg, &mut out);88		}89		out90	}91	fn into_command(self) -> Command {92		let mut out = Command::new(self.command);93		out.args(self.args);94		for (k, v) in self.env {95			out.env(k, v);96		}97		out98	}99	fn into_command_new(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {100		Ok(if let Some(session) = self.ssh_session.clone() {101			let cmd = self.into_command();102			Either::Right(103				cmd.over_ssh(session)104					.map_err(|e| anyhow!("ssh error: {e}"))?,105			)106		} else {107			let cmd = self.into_command();108			Either::Left(cmd)109		})110	}111	pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {112		let arg = arg.as_ref();113		self.args.push(ostoutf8(arg));114		self115	}116	pub fn eqarg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {117		let arg = arg.as_ref();118		let value = value.as_ref();119		let arg = ostoutf8(arg);120		let value = ostoutf8(value);121		self.arg(format!("{arg}={value}"));122		self123	}124	pub fn comparg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {125		self.arg(arg);126		self.arg(value);127		self128	}129	pub fn args<V: AsRef<OsStr>>(&mut self, args: impl IntoIterator<Item = V>) -> &mut Self {130		for arg in args.into_iter() {131			let arg = arg.as_ref();132			self.args.push(ostoutf8(arg));133		}134		self135	}136	pub fn sudo(self) -> Self {137		if std::env::var_os("NO_SUDO").is_some() {138			let mut out = Self::new("su");139			out.arg("-c").arg(self.into_string());140			out141		} else {142			let mut out = Self::new("sudo");143			out.args(self.into_args());144			out145		}146	}147	pub fn ssh(self, on: impl AsRef<OsStr>) -> Self {148		let mut out = Self::new("ssh");149		out.arg(on).arg("--");150		out.arg(self.into_string());151		out152	}153	pub fn over_ssh(mut self, session: Arc<Session>) -> Self {154		self.ssh_session = Some(session);155		self156	}157158	pub async fn run(self) -> Result<()> {159		let str = self.clone().into_string();160		let cmd = self.into_command();161		run_nix_inner(str, cmd, &mut PlainHandler).await?;162		Ok(())163	}164	pub async fn run_string(self) -> Result<String> {165		let str = self.clone().into_string();166		let cmd = self.into_command();167		let v = run_nix_inner_stdout(str, cmd, &mut PlainHandler).await?;168		Ok(v)169	}170171	pub async fn run_nix_string(self) -> Result<String> {172		let str = self.clone().into_string();173		let mut cmd = self.into_command();174		cmd.arg("--log-format").arg("internal-json");175		run_nix_inner_stdout(str, cmd, &mut NixHandler::default()).await176	}177	pub async fn run_nix(self) -> Result<()> {178		let str = self.clone().into_string();179		let mut cmd = self.into_command();180		cmd.arg("--log-format").arg("internal-json");181		cmd.stdout(Stdio::inherit());182		run_nix_inner(str, cmd, &mut NixHandler::default()).await183	}184}185186struct EmptyAsyncRead;187impl AsyncRead for EmptyAsyncRead {188	fn poll_read(189		self: std::pin::Pin<&mut Self>,190		_cx: &mut std::task::Context<'_>,191		_buf: &mut tokio::io::ReadBuf<'_>,192	) -> Poll<std::io::Result<()>> {193		Poll::Pending194	}195}196197async fn run_nix_inner_stdout(198	str: String,199	cmd: Command,200	handler: &mut dyn Handler,201) -> Result<String> {202	Ok(run_nix_inner_raw(str, cmd, true, handler, None)203		.await?204		.expect("has out"))205}206async fn run_nix_inner(str: String, cmd: Command, handler: &mut dyn Handler) -> Result<()> {207	let v = run_nix_inner_raw(str, cmd, false, handler, None).await?;208	assert!(v.is_none());209	Ok(())210}211212pub trait Handler: Send {213	fn handle_line(&mut self, e: &str);214}215216pub struct ClonableHandler<H>(Arc<Mutex<H>>);217impl<H> Clone for ClonableHandler<H> {218	fn clone(&self) -> Self {219		Self(self.0.clone())220	}221}222impl<H> ClonableHandler<H> {223	pub fn new(inner: H) -> Self {224		Self(Arc::new(Mutex::new(inner)))225	}226}227impl<H: Handler> Handler for ClonableHandler<H> {228	fn handle_line(&mut self, e: &str) {229		self.0.lock().unwrap().handle_line(e)230	}231}232233struct PlainHandler;234impl Handler for PlainHandler {235	fn handle_line(&mut self, e: &str) {236		info!(target: "log", "{e}");237	}238}239240pub struct NoopHandler;241impl Handler for NoopHandler {242	fn handle_line(&mut self, _e: &str) {}243}244245#[derive(Default)]246pub struct NixHandler {247	spans: HashMap<u64, Span>,248}249fn process_message(m: &str) -> String {250	static OSC_CLEANER: Lazy<Regex> =251		Lazy::new(|| Regex::new(r"\x1B\]([^\x07\x1C]*[\x07\x1C])?|\r").unwrap());252	static DETABBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"\t").unwrap());253	let m = OSC_CLEANER.replace_all(m, "");254	// Indicatif can't format tabs. This is not the correct tab formatting, as correct one should be aligned,255	// and not just be replaced with the constant number of spaces, but it's ok for now, as statuses are single-line.256	DETABBER.replace_all(m.as_ref(), "  ").to_string()257}258impl Handler for NixHandler {259	fn handle_line(&mut self, e: &str) {260		if let Some(e) = e.strip_prefix("@nix ") {261			let log: NixLog = match serde_json::from_str(e) {262				Ok(l) => l,263				Err(err) => {264					warn!("failed to parse nix log line {:?}: {}", e, err);265					return;266				}267			};268			match log {269				NixLog::Msg { msg, raw_msg, .. } => {270					#[allow(clippy::nonminimal_bool)]271					if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '") && msg.ends_with("' is dirty"))272					&& !msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake")273					&& 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" {274						if let Some(raw_msg) = raw_msg {275							if !msg.is_empty() {276								info!(target: "nix", "{}\n{}", raw_msg.trim_end(), msg.trim_end())277							} else {278								info!(target: "nix", "{}", raw_msg.trim_end())279							}280						} else {281							info!(target: "nix", "{}", msg.trim_end())282						}283					}284				}285				NixLog::Start {286					ref fields,287					typ,288					id,289					..290				} if typ == 105 && !fields.is_empty() => {291					if let [LogField::String(drv), ..] = &fields[..] {292						let mut drv = drv.as_str();293						if let Some(pkg) = drv.strip_prefix("/nix/store/") {294							let mut it = pkg.splitn(2, '-');295							it.next();296							if let Some(pkg) = it.next() {297								drv = pkg;298							}299						}300						info!(target: "nix","building {}", drv);301						let span = info_span!("build", drv);302						span.pb_start();303						self.spans.insert(id, span);304					} else {305						warn!("bad build log: {:?}", log)306					}307				}308				NixLog::Start {309					ref fields,310					typ,311					id,312					..313				} if typ == 100 && fields.len() >= 3 => {314					if let [LogField::String(drv), LogField::String(from), LogField::String(to), ..] =315						&fields[..]316					{317						let mut drv = drv.as_str();318319						if let Some(pkg) = drv.strip_prefix("/nix/store/") {320							let mut it = pkg.splitn(2, '-');321							it.next();322							if let Some(pkg) = it.next() {323								drv = pkg;324							}325						}326						// info!(target: "nix","copying {} {} -> {}", drv, from, to);327						let span = info_span!("copy", from, to, drv);328						span.pb_start();329						self.spans.insert(id, span);330					} else {331						warn!("bad copy log: {:?}", log)332					}333				}334				NixLog::Start { text, typ, id, .. }335					if typ == 0 || typ == 102 || typ == 103 || typ == 104 =>336				{337					if !text.is_empty()338						&& text != "querying info about missing paths"339						&& text != "copying 0 paths"340					{341						let span = info_span!("job");342						span.pb_start();343						span.pb_set_message(&process_message(text.trim()));344						self.spans.insert(id, span);345						info!(target: "nix", "{}", text);346					}347				}348				NixLog::Start {349					text,350					level: 0,351					typ: 108,352					..353				} if text.is_empty() => {354					// Cache lookup? Coupled with copy log355				}356				NixLog::Start {357					text,358					level: 4,359					typ: 109,360					..361				} if text.starts_with("querying info about ") => {362					// Cache lookup363				}364				NixLog::Start {365					text,366					level: 4,367					typ: 101,368					..369				} if text.starts_with("downloading ") => {370					// NAR downloading, coupled with copy log371				}372				NixLog::Start {373					text,374					level: 1,375					typ: 111,376					..377				} if text.starts_with("waiting for a machine to build ") => {378					// Useless repeating notification about build379				}380				NixLog::Start {381					text,382					level: 3,383					typ: 111,384					..385				} if text.starts_with("resolved derivation: ") => {386					// CA resolved387				}388				NixLog::Start {389					text,390					level: 1,391					typ: 111,392					id,393					..394				} if text.starts_with("waiting for lock on ") => {395					let mut drv = text.strip_prefix("waiting for lock on ").unwrap();396					if let Some(txt) = drv.strip_prefix("\u{1b}[35;1m'") {397						drv = txt;398					}399					if let Some(txt) = drv.strip_suffix("'\u{1b}[0m") {400						drv = txt;401					}402					if let Some(txt) = drv.split("', '").next() {403						drv = txt;404					}405					if let Some(pkg) = drv.strip_prefix("/nix/store/") {406						let mut it = pkg.splitn(2, '-');407						it.next();408						if let Some(pkg) = it.next() {409							drv = pkg;410						}411					}412					let span = info_span!("waiting on drv", drv);413					span.pb_start();414					self.spans.insert(id, span);415					// Concurrent build of the same message416				}417				NixLog::Stop { id, .. } => {418					self.spans.remove(&id);419				}420				NixLog::Result { fields, id, typ } if typ == 101 && !fields.is_empty() => {421					if let Some(span) = self.spans.get(&id) {422						if let LogField::String(s) = &fields[0] {423							span.pb_set_message(&process_message(s.trim()));424						} else {425							warn!("bad fields: {fields:?}");426						}427					} else {428						warn!("unknown result id: {id} {typ} {fields:?}");429					}430					// dbg!(fields, id, typ);431				}432				NixLog::Result { fields, id, typ } if typ == 105 && fields.len() >= 4 => {433					if let Some(span) = self.spans.get(&id) {434						if let [LogField::Num(done), LogField::Num(expected), LogField::Num(_running), LogField::Num(_failed)] =435							&fields[..4]436						{437							span.pb_set_length(*expected);438							span.pb_set_position(*done);439						} else {440							warn!("bad fields: {fields:?}");441						}442					} else {443						// warn!("unknown result id: {id} {typ} {fields:?}");444						// Unaccounted progress.445					}446					// dbg!(fields, id, typ);447				}448				NixLog::Result { typ, .. } if typ == 104 || typ == 106 => {449					// Set phase, expected450				}451				_ => warn!("unknown log: {:?}", log),452			};453		} else {454			let e = e.trim();455			if e.starts_with("Failed tcsetattr(TCSADRAIN): ") {456				return;457			}458			info!("{e}")459		}460	}461}462463async fn run_nix_inner_raw(464	str: String,465	mut cmd: Command,466	want_stdout: bool,467	err_handler: &mut dyn Handler,468	mut out_handler: Option<&mut dyn Handler>,469) -> Result<Option<String>> {470	cmd.stderr(Stdio::piped());471	cmd.stdout(Stdio::piped());472	let mut child = cmd.spawn()?;473	let mut stderr = child.stderr.take().unwrap();474	let stdout = child.stdout.take().unwrap();475	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());476	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));477	let mut ob = want_stdout478		.then(|| out.take().unwrap())479		.unwrap_or_else(|| Box::new(EmptyAsyncRead));480	let mut ol = (!want_stdout)481		.then(|| out.take().unwrap())482		.unwrap_or_else(|| Box::new(EmptyAsyncRead));483	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());484	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());485486	// while let Some(line) = read.next().await? {}487488	let mut out_buf = if want_stdout { Some(vec![]) } else { None };489	loop {490		select! {491			e = err.next() => {492				if let Some(e) = e {493					let e = e?;494					err_handler.handle_line(&e);495				}496			},497			o = ob.next() => {498				if let Some(o) = o {499					out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);500				}501			},502			o = ol.next() => {503				if let Some(o) = o {504					let o = o?;505					if let Some(out) = out_handler.as_mut() {506						out.handle_line(&o)507					} else {508						err_handler.handle_line(&o)509					}510					// out_handler.handle_info(&o);511				}512			},513			code = child.wait() => {514				let code = code?;515				if !code.success() {516					anyhow::bail!("command '{str}' failed with status {}", code);517				}518				break;519			}520		}521	}522523	Ok(out_buf.map(String::from_utf8).transpose()?)524}525526pub trait ErrorRecorder: Send {527	/// Return true to discard message from logging528	fn push_message(&mut self, msg: &str) -> bool;529}530531#[derive(Debug)]532enum LogField {533	String(String),534	Num(u64),535}536537impl<'de> Deserialize<'de> for LogField {538	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>539	where540		D: serde::Deserializer<'de>,541	{542		struct StringOrNum;543		impl<'de> Visitor<'de> for StringOrNum {544			type Value = LogField;545546			fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {547				write!(f, "string or unsigned")548			}549550			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>551			where552				E: serde::de::Error,553			{554				Ok(LogField::String(v.to_owned()))555			}556557			fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>558			where559				E: serde::de::Error,560			{561				Ok(LogField::Num(v))562			}563		}564565		deserializer.deserialize_any(StringOrNum)566	}567}568569#[derive(Deserialize, Debug)]570#[serde(rename_all = "camelCase", tag = "action")]571#[allow(dead_code)]572enum NixLog {573	Msg {574		level: u32,575		msg: String,576		raw_msg: Option<String>,577	},578	Start {579		id: u64,580		level: u32,581		#[serde(default)]582		fields: Vec<LogField>,583		text: String,584		#[serde(rename = "type")]585		typ: u32,586	},587	Stop {588		id: u64,589	},590	Result {591		id: u64,592		#[serde(rename = "type")]593		typ: u32,594		#[serde(default)]595		fields: Vec<LogField>,596	},597}
after · cmds/fleet/src/command.rs
1use std::{2	collections::HashMap,3	ffi::OsStr,4	process::Stdio,5	sync::{Arc, Mutex},6	task::Poll,7};89use anyhow::{anyhow, Result};10use futures::StreamExt;11use itertools::Either;12use once_cell::sync::Lazy;13use openssh::{OverSsh, Session};14use regex::Regex;15use serde::{de::Visitor, Deserialize};16use tokio::{io::AsyncRead, process::Command, select};17use tokio_util::codec::{BytesCodec, FramedRead, LinesCodec};18use tracing::{info, info_span, warn, Span};19use tracing_indicatif::span_ext::IndicatifSpanExt;2021fn escape_bash(input: &str, out: &mut String) {22	const TO_ESCAPE: &str = "$ !\"#&'()*,;<>?[\\]^`{|}";23	if input.chars().all(|c| !TO_ESCAPE.contains(c)) {24		out.push_str(input);25		return;26	}27	out.push('\'');28	for (i, v) in input.split('\'').enumerate() {29		if i != 0 {30			out.push_str("'\"'\"'");31		}32		out.push_str(v);33	}34	out.push('\'');35}36fn ostoutf8(os: impl AsRef<OsStr>) -> String {37	os.as_ref().to_str().expect("non-utf8 data").to_owned()38}39#[derive(Clone)]40pub struct MyCommand {41	command: String,42	args: Vec<String>,43	env: Vec<(String, String)>,44	ssh_session: Option<Arc<Session>>,45}46impl MyCommand {47	pub fn new(cmd: impl AsRef<OsStr>) -> Self {48		assert!(!cmd.as_ref().is_empty());49		Self {50			command: ostoutf8(cmd),51			args: vec![],52			env: vec![],53			ssh_session: None,54		}55	}56	fn into_args(self) -> Vec<String> {57		let mut out = Vec::new();58		if !self.env.is_empty() {59			out.push("env".to_owned());60			for (k, v) in self.env {61				assert!(!k.contains('='));62				out.push(format!("{k}={v}"));63			}64		}65		out.push(self.command);66		out.extend(self.args);67		out68	}69	fn into_string(self) -> String {70		let mut out = String::new();71		if !self.env.is_empty() {72			out.push_str("env");73			for (k, v) in self.env {74				out.push(' ');75				assert!(!k.contains('='));76				escape_bash(&k, &mut out);77				out.push('=');78				escape_bash(&v, &mut out);79			}80		}81		if !out.is_empty() {82			out.push(' ');83		}84		escape_bash(&self.command, &mut out);85		for arg in self.args {86			out.push(' ');87			escape_bash(&arg, &mut out);88		}89		out90	}91	fn into_command(self) -> Command {92		let mut out = Command::new(self.command);93		out.args(self.args);94		for (k, v) in self.env {95			out.env(k, v);96		}97		out98	}99	fn into_command_new(self) -> Result<Either<Command, openssh::OwningCommand<Arc<Session>>>> {100		Ok(if let Some(session) = self.ssh_session.clone() {101			let cmd = self.into_command();102			Either::Right(103				cmd.over_ssh(session)104					.map_err(|e| anyhow!("ssh error: {e}"))?,105			)106		} else {107			let cmd = self.into_command();108			Either::Left(cmd)109		})110	}111	pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut Self {112		let arg = arg.as_ref();113		self.args.push(ostoutf8(arg));114		self115	}116	pub fn eqarg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {117		let arg = arg.as_ref();118		let value = value.as_ref();119		let arg = ostoutf8(arg);120		let value = ostoutf8(value);121		self.arg(format!("{arg}={value}"));122		self123	}124	pub fn comparg(&mut self, arg: impl AsRef<OsStr>, value: impl AsRef<OsStr>) -> &mut Self {125		self.arg(arg);126		self.arg(value);127		self128	}129	pub fn args<V: AsRef<OsStr>>(&mut self, args: impl IntoIterator<Item = V>) -> &mut Self {130		for arg in args.into_iter() {131			let arg = arg.as_ref();132			self.args.push(ostoutf8(arg));133		}134		self135	}136	pub fn sudo(self) -> Self {137		if std::env::var_os("NO_SUDO").is_some() {138			let mut out = Self::new("su");139			out.arg("-c").arg(self.into_string());140			out141		} else {142			let mut out = Self::new("sudo");143			out.args(self.into_args());144			out145		}146	}147	pub fn ssh(self, on: impl AsRef<OsStr>) -> Self {148		let mut out = Self::new("ssh");149		out.arg(on).arg("--");150		out.arg(self.into_string());151		out152	}153	pub fn over_ssh(mut self, session: Arc<Session>) -> Self {154		self.ssh_session = Some(session);155		self156	}157158	pub async fn run(self) -> Result<()> {159		let str = self.clone().into_string();160		let cmd = self.into_command();161		run_nix_inner(str, cmd, &mut PlainHandler).await?;162		Ok(())163	}164	pub async fn run_string(self) -> Result<String> {165		let str = self.clone().into_string();166		let cmd = self.into_command();167		let v = run_nix_inner_stdout(str, cmd, &mut PlainHandler).await?;168		Ok(v)169	}170171	pub async fn run_nix_string(self) -> Result<String> {172		let str = self.clone().into_string();173		let mut cmd = self.into_command();174		cmd.arg("--log-format").arg("internal-json");175		run_nix_inner_stdout(str, cmd, &mut NixHandler::default()).await176	}177	pub async fn run_nix(self) -> Result<()> {178		let str = self.clone().into_string();179		let mut cmd = self.into_command();180		cmd.arg("--log-format").arg("internal-json");181		cmd.stdout(Stdio::inherit());182		run_nix_inner(str, cmd, &mut NixHandler::default()).await183	}184}185186struct EmptyAsyncRead;187impl AsyncRead for EmptyAsyncRead {188	fn poll_read(189		self: std::pin::Pin<&mut Self>,190		_cx: &mut std::task::Context<'_>,191		_buf: &mut tokio::io::ReadBuf<'_>,192	) -> Poll<std::io::Result<()>> {193		Poll::Pending194	}195}196197async fn run_nix_inner_stdout(198	str: String,199	cmd: Command,200	handler: &mut dyn Handler,201) -> Result<String> {202	Ok(run_nix_inner_raw(str, cmd, true, handler, None)203		.await?204		.expect("has out"))205}206async fn run_nix_inner(str: String, cmd: Command, handler: &mut dyn Handler) -> Result<()> {207	let v = run_nix_inner_raw(str, cmd, false, handler, None).await?;208	assert!(v.is_none());209	Ok(())210}211212pub trait Handler: Send {213	fn handle_line(&mut self, e: &str);214}215216pub struct ClonableHandler<H>(Arc<Mutex<H>>);217impl<H> Clone for ClonableHandler<H> {218	fn clone(&self) -> Self {219		Self(self.0.clone())220	}221}222impl<H> ClonableHandler<H> {223	pub fn new(inner: H) -> Self {224		Self(Arc::new(Mutex::new(inner)))225	}226}227impl<H: Handler> Handler for ClonableHandler<H> {228	fn handle_line(&mut self, e: &str) {229		self.0.lock().unwrap().handle_line(e)230	}231}232233struct PlainHandler;234impl Handler for PlainHandler {235	fn handle_line(&mut self, e: &str) {236		info!(target: "log", "{e}");237	}238}239240pub struct NoopHandler;241impl Handler for NoopHandler {242	fn handle_line(&mut self, _e: &str) {}243}244245#[derive(Default)]246pub struct NixHandler {247	spans: HashMap<u64, Span>,248}249fn process_message(m: &str) -> String {250	static OSC_CLEANER: Lazy<Regex> =251		Lazy::new(|| Regex::new(r"\x1B\]([^\x07\x1C]*[\x07\x1C])?|\r").unwrap());252	static DETABBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"\t").unwrap());253	let m = OSC_CLEANER.replace_all(m, "");254	// Indicatif can't format tabs. This is not the correct tab formatting, as correct one should be aligned,255	// and not just be replaced with the constant number of spaces, but it's ok for now, as statuses are single-line.256	DETABBER.replace_all(m.as_ref(), "  ").to_string()257}258impl Handler for NixHandler {259	fn handle_line(&mut self, e: &str) {260		if let Some(e) = e.strip_prefix("@nix ") {261			let log: NixLog = match serde_json::from_str(e) {262				Ok(l) => l,263				Err(err) => {264					warn!("failed to parse nix log line {:?}: {}", e, err);265					return;266				}267			};268			match log {269				NixLog::Msg { msg, raw_msg, .. } => {270					#[allow(clippy::nonminimal_bool)]271					if !(msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m Git tree '") && msg.ends_with("' is dirty"))272					&& !msg.starts_with("\u{1b}[35;1mwarning:\u{1b}[0m not writing modified lock file of flake")273					&& 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" {274						if let Some(raw_msg) = raw_msg {275							if !msg.is_empty() {276								info!(target: "nix", "{}\n{}", raw_msg.trim_end(), msg.trim_end())277							} else {278								info!(target: "nix", "{}", raw_msg.trim_end())279							}280						} else {281							info!(target: "nix", "{}", msg.trim_end())282						}283					}284				}285				NixLog::Start {286					ref fields,287					typ,288					id,289					..290				} if typ == 105 && !fields.is_empty() => {291					if let [LogField::String(drv), ..] = &fields[..] {292						let mut drv = drv.as_str();293						if let Some(pkg) = drv.strip_prefix("/nix/store/") {294							let mut it = pkg.splitn(2, '-');295							it.next();296							if let Some(pkg) = it.next() {297								drv = pkg;298							}299						}300						info!(target: "nix","building {}", drv);301						let span = info_span!("build", drv);302						span.pb_start();303						self.spans.insert(id, span);304					} else {305						warn!("bad build log: {:?}", log)306					}307				}308				NixLog::Start {309					ref fields,310					typ,311					id,312					..313				} if typ == 100 && fields.len() >= 3 => {314					if let [LogField::String(drv), LogField::String(from), LogField::String(to), ..] =315						&fields[..]316					{317						let mut drv = drv.as_str();318319						if let Some(pkg) = drv.strip_prefix("/nix/store/") {320							let mut it = pkg.splitn(2, '-');321							it.next();322							if let Some(pkg) = it.next() {323								drv = pkg;324							}325						}326						// info!(target: "nix","copying {} {} -> {}", drv, from, to);327						let span = info_span!("copy", from, to, drv);328						span.pb_start();329						self.spans.insert(id, span);330					} else {331						warn!("bad copy log: {:?}", log)332					}333				}334				NixLog::Start { text, typ, id, .. }335					if typ == 0 || typ == 102 || typ == 103 || typ == 104 =>336				{337					if !text.is_empty()338						&& text != "querying info about missing paths"339						&& text != "copying 0 paths"340						// Too much spam on lazy-trees branch341						&& !(text.starts_with("copying '") && text.ends_with("' to the store"))342					{343						let span = info_span!("job");344						span.pb_start();345						span.pb_set_message(&process_message(text.trim()));346						self.spans.insert(id, span);347						info!(target: "nix", "{}", text);348					}349				}350				NixLog::Start {351					text,352					level: 0,353					typ: 108,354					..355				} if text.is_empty() => {356					// Cache lookup? Coupled with copy log357				}358				NixLog::Start {359					text,360					level: 4,361					typ: 109,362					..363				} if text.starts_with("querying info about ") => {364					// Cache lookup365				}366				NixLog::Start {367					text,368					level: 4,369					typ: 101,370					..371				} if text.starts_with("downloading ") => {372					// NAR downloading, coupled with copy log373				}374				NixLog::Start {375					text,376					level: 1,377					typ: 111,378					..379				} if text.starts_with("waiting for a machine to build ") => {380					// Useless repeating notification about build381				}382				NixLog::Start {383					text,384					level: 3,385					typ: 111,386					..387				} if text.starts_with("resolved derivation: ") => {388					// CA resolved389				}390				NixLog::Start {391					text,392					level: 1,393					typ: 111,394					id,395					..396				} if text.starts_with("waiting for lock on ") => {397					let mut drv = text.strip_prefix("waiting for lock on ").unwrap();398					if let Some(txt) = drv.strip_prefix("\u{1b}[35;1m'") {399						drv = txt;400					}401					if let Some(txt) = drv.strip_suffix("'\u{1b}[0m") {402						drv = txt;403					}404					if let Some(txt) = drv.split("', '").next() {405						drv = txt;406					}407					if let Some(pkg) = drv.strip_prefix("/nix/store/") {408						let mut it = pkg.splitn(2, '-');409						it.next();410						if let Some(pkg) = it.next() {411							drv = pkg;412						}413					}414					let span = info_span!("waiting on drv", drv);415					span.pb_start();416					self.spans.insert(id, span);417					// Concurrent build of the same message418				}419				NixLog::Stop { id, .. } => {420					self.spans.remove(&id);421				}422				NixLog::Result { fields, id, typ } if typ == 101 && !fields.is_empty() => {423					if let Some(span) = self.spans.get(&id) {424						if let LogField::String(s) = &fields[0] {425							span.pb_set_message(&process_message(s.trim()));426						} else {427							warn!("bad fields: {fields:?}");428						}429					} else {430						warn!("unknown result id: {id} {typ} {fields:?}");431					}432					// dbg!(fields, id, typ);433				}434				NixLog::Result { fields, id, typ } if typ == 105 && fields.len() >= 4 => {435					if let Some(span) = self.spans.get(&id) {436						if let [LogField::Num(done), LogField::Num(expected), LogField::Num(_running), LogField::Num(_failed)] =437							&fields[..4]438						{439							span.pb_set_length(*expected);440							span.pb_set_position(*done);441						} else {442							warn!("bad fields: {fields:?}");443						}444					} else {445						// warn!("unknown result id: {id} {typ} {fields:?}");446						// Unaccounted progress.447					}448					// dbg!(fields, id, typ);449				}450				NixLog::Result { typ, .. } if typ == 104 || typ == 106 => {451					// Set phase, expected452				}453				_ => warn!("unknown log: {:?}", log),454			};455		} else {456			let e = e.trim();457			if e.starts_with("Failed tcsetattr(TCSADRAIN): ") {458				return;459			}460			info!("{e}")461		}462	}463}464465async fn run_nix_inner_raw(466	str: String,467	mut cmd: Command,468	want_stdout: bool,469	err_handler: &mut dyn Handler,470	mut out_handler: Option<&mut dyn Handler>,471) -> Result<Option<String>> {472	cmd.stderr(Stdio::piped());473	cmd.stdout(Stdio::piped());474	let mut child = cmd.spawn()?;475	let mut stderr = child.stderr.take().unwrap();476	let stdout = child.stdout.take().unwrap();477	let mut err = FramedRead::new(&mut stderr, LinesCodec::new());478	let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));479	let mut ob = want_stdout480		.then(|| out.take().unwrap())481		.unwrap_or_else(|| Box::new(EmptyAsyncRead));482	let mut ol = (!want_stdout)483		.then(|| out.take().unwrap())484		.unwrap_or_else(|| Box::new(EmptyAsyncRead));485	let mut ob = FramedRead::new(&mut ob, BytesCodec::new());486	let mut ol = FramedRead::new(&mut ol, LinesCodec::new());487488	// while let Some(line) = read.next().await? {}489490	let mut out_buf = if want_stdout { Some(vec![]) } else { None };491	loop {492		select! {493			e = err.next() => {494				if let Some(e) = e {495					let e = e?;496					err_handler.handle_line(&e);497				}498			},499			o = ob.next() => {500				if let Some(o) = o {501					out_buf.as_mut().expect("stdout == wants_stdout").extend_from_slice(&o?);502				}503			},504			o = ol.next() => {505				if let Some(o) = o {506					let o = o?;507					if let Some(out) = out_handler.as_mut() {508						out.handle_line(&o)509					} else {510						err_handler.handle_line(&o)511					}512					// out_handler.handle_info(&o);513				}514			},515			code = child.wait() => {516				let code = code?;517				if !code.success() {518					anyhow::bail!("command '{str}' failed with status {}", code);519				}520				break;521			}522		}523	}524525	Ok(out_buf.map(String::from_utf8).transpose()?)526}527528pub trait ErrorRecorder: Send {529	/// Return true to discard message from logging530	fn push_message(&mut self, msg: &str) -> bool;531}532533#[derive(Debug)]534enum LogField {535	String(String),536	Num(u64),537}538539impl<'de> Deserialize<'de> for LogField {540	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>541	where542		D: serde::Deserializer<'de>,543	{544		struct StringOrNum;545		impl<'de> Visitor<'de> for StringOrNum {546			type Value = LogField;547548			fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {549				write!(f, "string or unsigned")550			}551552			fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>553			where554				E: serde::de::Error,555			{556				Ok(LogField::String(v.to_owned()))557			}558559			fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>560			where561				E: serde::de::Error,562			{563				Ok(LogField::Num(v))564			}565		}566567		deserializer.deserialize_any(StringOrNum)568	}569}570571#[derive(Deserialize, Debug)]572#[serde(rename_all = "camelCase", tag = "action")]573#[allow(dead_code)]574enum NixLog {575	Msg {576		level: u32,577		msg: String,578		raw_msg: Option<String>,579	},580	Start {581		id: u64,582		level: u32,583		#[serde(default)]584		fields: Vec<LogField>,585		text: String,586		#[serde(rename = "type")]587		typ: u32,588	},589	Stop {590		id: u64,591	},592	Result {593		id: u64,594		#[serde(rename = "type")]595		typ: u32,596		#[serde(default)]597		fields: Vec<LogField>,598	},599}
modifiedcmds/fleet/src/host.rsdiffbeforeafterboth
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -16,7 +16,7 @@
 	better_nix_eval::{Field, NixSessionPool},
 	command::MyCommand,
 	fleetdata::{FleetData, FleetSecret, FleetSharedSecret},
-	nix_path,
+	nix_go, nix_go_json,
 };
 
 pub struct FleetConfigInternals {
@@ -29,6 +29,8 @@
 	pub fleet_field: Field,
 	/// fleet_config.configUnchecked
 	pub config_field: Field,
+	/// fleet_config.unchecked
+	pub config_unchecked_field: Field,
 }
 
 #[derive(Clone)]
@@ -95,12 +97,8 @@
 	}
 
 	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
-		let names = self
-			.fleet_field
-			.select(nix_path!(.configuredHosts))
-			.await?
-			.list_fields()
-			.await?;
+		let fleet_field = &self.fleet_field;
+		let names = nix_go!(fleet_field.configuredHosts).list_fields().await?;
 		let mut out = vec![];
 		for name in names {
 			out.push(ConfigHost { name })
@@ -108,9 +106,8 @@
 		Ok(out)
 	}
 	pub async fn system_config(&self, host: &str) -> Result<Field> {
-		self.fleet_field
-			.select(nix_path!(.configuredSystems.{host}.config))
-			.await
+		let fleet_field = &self.fleet_field;
+		Ok(nix_go!(fleet_field.configuredSystems[{ host }].config))
 	}
 
 	pub(super) fn data(&self) -> MutexGuard<FleetData> {
@@ -121,11 +118,8 @@
 	}
 	/// Shared secrets configured in fleet.nix or in flake
 	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
-		self.config_field
-			.select(nix_path!(.sharedSecrets))
-			.await?
-			.list_fields()
-			.await
+		let config_field = &self.config_field;
+		nix_go!(config_field.sharedSecrets).list_fields().await
 	}
 	/// Shared secrets configured in fleet.nix
 	pub fn list_shared(&self) -> Vec<String> {
@@ -211,11 +205,10 @@
 		Ok(secret.clone())
 	}
 	pub async fn shared_secret_expected_owners(&self, secret: &str) -> Result<Vec<String>> {
-		self.config_field
-			.select(nix_path!(.sharedSecrets.{secret}.expectedOwners))
-			.await?
-			.as_json()
-			.await
+		let config_field = &self.config_field;
+		Ok(nix_go_json!(
+			config_field.sharedSecrets[{ secret }].expectedOwners
+		))
 	}
 
 	pub fn save(&self) -> Result<()> {
@@ -269,21 +262,15 @@
 
 		if self.local_system == "detect" {
 			let builtins_field = Field::field(root_field.clone(), "builtins").await?;
-			let system = builtins_field
-				.select(nix_path!(.currentSystem))
-				.await?;
-			self.local_system = system.as_json().await?;
+			self.local_system = nix_go_json!(builtins_field.currentSystem);
 		}
 		let local_system = self.local_system.clone();
 
 		let fleet_root = Field::field(root_field, "fleetConfigurations").await?;
 
-		let fleet_field = fleet_root
-			.select(nix_path!(.default))
-			.await?;
-		let config_field = fleet_field
-			.select(nix_path!(.configUnchecked))
-			.await?;
+		let fleet_field = nix_go!(fleet_root.default);
+		let config_field = nix_go!(fleet_field.configUnchecked);
+		let config_unchecked_field = nix_go!(fleet_field.unchecked);
 
 		let mut fleet_data_path = directory.clone();
 		fleet_data_path.push("fleet.nix");
@@ -298,6 +285,7 @@
 			nix_args,
 			fleet_field,
 			config_field,
+			config_unchecked_field,
 		})))
 	}
 }
modifiedcmds/fleet/src/main.rsdiffbeforeafterboth
--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -1,5 +1,5 @@
 #![recursion_limit = "512"]
-#![feature(try_blocks)]
+#![feature(try_blocks, lint_reasons)]
 
 pub(crate) mod cmds;
 pub(crate) mod command;
@@ -17,7 +17,7 @@
 use anyhow::{bail, Result};
 use clap::Parser;
 
-use cmds::{build_systems::BuildSystems, info::Info, secrets::Secrets};
+use cmds::{build_systems::BuildSystems, info::Info, secrets::Secret};
 use futures::future::LocalBoxFuture;
 use futures::stream::FuturesUnordered;
 use futures::TryStreamExt;
@@ -73,7 +73,7 @@
 	BuildSystems(BuildSystems),
 	/// Secret management
 	#[clap(subcommand)]
-	Secrets(Secrets),
+	Secret(Secret),
 	/// Upload prefetch directory to the nix store
 	Prefetch(Prefetch),
 	/// Config parsing
@@ -92,7 +92,7 @@
 async fn run_command(config: &Config, command: Opts) -> Result<()> {
 	match command {
 		Opts::BuildSystems(c) => c.run(config).await?,
-		Opts::Secrets(s) => s.run(config).await?,
+		Opts::Secret(s) => s.run(config).await?,
 		Opts::Info(i) => i.run(config).await?,
 		Opts::Prefetch(p) => p.run(config).await?,
 	};
modifiedflake.lockdiffbeforeafterboth
--- a/flake.lock
+++ b/flake.lock
@@ -5,11 +5,11 @@
         "systems": "systems"
       },
       "locked": {
-        "lastModified": 1694529238,
-        "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
+        "lastModified": 1701680307,
+        "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
         "owner": "numtide",
         "repo": "flake-utils",
-        "rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
+        "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
         "type": "github"
       },
       "original": {
@@ -38,11 +38,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1698350982,
-        "narHash": "sha256-zoEV8Ad3bOAejp0ys/mOpaHSWrzK+GupZwGGYfuWuEY=",
+        "lastModified": 1703705939,
+        "narHash": "sha256-9s2Ep3NyRDj9HUgfv2TQUwQEanRUAmeXkvKIr/o1XbY=",
         "owner": "nixos",
         "repo": "nixpkgs",
-        "rev": "dd83f9de26ff7c0326468b659ea4729fa5cf6262",
+        "rev": "1ada32da4ba24d7310653c9ac54888bee463f455",
         "type": "github"
       },
       "original": {
@@ -67,11 +67,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1698199907,
-        "narHash": "sha256-n8RtHBIb0rLuYs4RDehW6mj6r6Yam/ODY1af/VCcurw=",
+        "lastModified": 1703643208,
+        "narHash": "sha256-UL4KO8JxnD5rOycwHqBAf84lExF1/VnYMDC7b/wpPDU=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "22b8d29fd22cfaa2c311e0d6fd8a0ed9c2a1152b",
+        "rev": "ce117f3e0de8262be8cd324ee6357775228687cf",
         "type": "github"
       },
       "original": {
modifiedflake.nixdiffbeforeafterboth
--- a/flake.nix
+++ b/flake.nix
@@ -3,35 +3,52 @@
 
   inputs = {
     nixpkgs.url = "github:nixos/nixpkgs/master";
-    rust-overlay = { url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; };
-    flake-utils = { url = "github:numtide/flake-utils"; };
+    rust-overlay = {
+      url = "github:oxalica/rust-overlay";
+      inputs.nixpkgs.follows = "nixpkgs";
+    };
+    flake-utils = {url = "github:numtide/flake-utils";};
   };
-  outputs = { self, rust-overlay, flake-utils, nixpkgs }: with nixpkgs.lib; rec {
-    lib = import ./lib { inherit flake-utils; };
-  } // flake-utils.lib.eachDefaultSystem (system:
-    let
-      pkgs = import nixpkgs
-        {
-          inherit system; overlays = [ (import rust-overlay) ];
-        };
-      llvmPkgs = pkgs.buildPackages.llvmPackages_11;
-      rust = (pkgs.rustChannelOf { date = "2023-10-20"; channel = "nightly"; }).default.override { extensions = [ "rust-src" "rust-analyzer" ]; };
-      rustPlatform = pkgs.makeRustPlatform { cargo = rust; rustc = rust; };
-    in
-    {
-		packages = (import ./pkgs) pkgs pkgs;
-      devShell = (pkgs.mkShell.override { stdenv = llvmPkgs.stdenv; }) {
-        nativeBuildInputs = with pkgs; [
-          rust
-          lld
-          cargo-edit
-          cargo-udeps
-          cargo-fuzz
+  outputs = {
+    self,
+    rust-overlay,
+    flake-utils,
+    nixpkgs,
+  }:
+    with nixpkgs.lib;
+      {
+        lib = import ./lib {inherit flake-utils;};
+      }
+      // flake-utils.lib.eachDefaultSystem (system: let
+        pkgs =
+          import nixpkgs
+          {
+            inherit system;
+            overlays = [(import rust-overlay)];
+          };
+        llvmPkgs = pkgs.buildPackages.llvmPackages_11;
+        rust =
+          (pkgs.rustChannelOf {
+            date = "2023-12-26";
+            channel = "nightly";
+          })
+          .default
+          .override {extensions = ["rust-src" "rust-analyzer"];};
+      in {
+        packages = (import ./pkgs) pkgs pkgs;
+        devShell = (pkgs.mkShell.override {stdenv = llvmPkgs.stdenv;}) {
+          nativeBuildInputs = with pkgs; [
+            rust
+            lld
+            cargo-edit
+            cargo-udeps
+            cargo-fuzz
+            cargo-watch
 
-          pkg-config
-          openssl
-          bacon
-        ];
-      };
-    });
+            pkg-config
+            openssl
+            bacon
+          ];
+        };
+      });
 }
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -10,80 +10,99 @@
     fleetLib = import ./fleetLib.nix {
       inherit nixpkgs hostNames;
     };
-  in
-    let
-      withData = data: rec {
-        root = nixpkgs.lib.evalModules {
-          modules = (import ../modules/fleet/_modules.nix) ++ [config data];
-          specialArgs = {
-            inherit nixpkgs fleetLib;
-          };
-        };
-        failedAssertions = map (x: x.message) (nixpkgs.lib.filter (x: !x.assertion) root.config.assertions);
-        rootAssertWarn =
-          if failedAssertions != []
-          then throw "Failed assertions:\n${nixpkgs.lib.concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
-          else nixpkgs.lib.showWarnings root.config.warnings root;
-        configuredHosts = rootAssertWarn.config.hosts;
-        configuredSecrets = rootAssertWarn.config.secrets;
-        configuredSystems = configuredSystemsWithExtraModules [];
-        configuredSystemsWithExtraModules = extraModules:
-          nixpkgs.lib.listToAttrs (
-            map
-            (
-              name: {
-                inherit name;
-                value = nixpkgs.lib.nixosSystem {
-                  system = configuredHosts.${name}.system;
-                  modules = configuredHosts.${name}.modules ++ extraModules;
-                  specialArgs = {
-                    inherit fleetLib;
-                    fleet = fleetLib.hostsToAttrs (host: configuredSystems.${host}.config);
-                  };
+  in let
+    root = nixpkgs.lib.evalModules {
+      modules = (import ../modules/fleet/_modules.nix) ++ [config data];
+      specialArgs = {
+        inherit nixpkgs fleetLib;
+      };
+    };
+    failedAssertions = map (x: x.message) (nixpkgs.lib.filter (x: !x.assertion) root.config.assertions);
+    checkedRoot =
+      if failedAssertions != []
+      then throw "Fleet failed assertions:\n${nixpkgs.lib.concatStringsSep "\n" (map (x: "- ${x}") failedAssertions)}"
+      else nixpkgs.lib.showWarnings root.config.warnings root;
+    withData = {
+      root,
+      data,
+    }: rec {
+      configuredHosts = root.config.hosts;
+      configuredUncheckedHosts = root.config.hosts;
+      configuredSystems = configuredSystemsWithExtraModules [];
+      configuredSystemsWithExtraModules = extraModules:
+        nixpkgs.lib.listToAttrs (
+          map
+          (
+            name: {
+              inherit name;
+              value = nixpkgs.lib.nixosSystem {
+                system = configuredHosts.${name}.system;
+                modules = configuredHosts.${name}.modules ++ extraModules;
+                specialArgs = {
+                  inherit fleetLib;
+                  fleet = fleetLib.hostsToAttrs (host: configuredSystems.${host}.config);
                 };
-              }
-            )
-            (builtins.attrNames rootAssertWarn.config.hosts)
-          );
-        buildSystems = {localSystem}: let
-          buildConfigurationModule = {config, ...}: {
-            # Equivalent to nixpkgs.localSystem
-            # nixpkgs.system = localSystem;
-            nixpkgs.buildPlatform.system = localSystem;
-          };
-        in {
-          toplevel = builtins.mapAttrs (_name: value: value.config.system.build.toplevel) (configuredSystemsWithExtraModules [
-            buildConfigurationModule
-            ({...}: {
-              buildTarget = "toplevel";
-            })
-          ]);
-          sdImage = builtins.mapAttrs (_name: value: value.config.system.build.sdImage) (configuredSystemsWithExtraModules [
-            buildConfigurationModule
-            #(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix")
-            ({...}: {
-              buildTarget = "sd-image";
-            })
-          ]);
-          installationCd = builtins.mapAttrs (_name: value: value.config.system.build.isoImage) (configuredSystemsWithExtraModules [
-            buildConfigurationModule
-            (nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix")
-            ({lib, ...}: {
-              buildTarget = "installation-cd";
-              # Needed for https://github.com/NixOS/nixpkgs/issues/58959
-              boot.supportedFilesystems = lib.mkForce ["btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs"];
-            })
-          ]);
+              };
+            }
+          )
+          (builtins.attrNames root.config.hosts)
+        );
+      buildableSystems = {localSystem}: let
+        buildConfigurationModule = {config, ...}: {
+          # Equivalent to nixpkgs.localSystem
+          # nixpkgs.system = localSystem;
+          nixpkgs.buildPlatform.system = localSystem;
+        };
+      in
+        configuredSystemsWithExtraModules [
+          buildConfigurationModule
+        ];
+      buildSystems = {localSystem}: let
+        buildConfigurationModule = {config, ...}: {
+          # Equivalent to nixpkgs.localSystem
+          # nixpkgs.system = localSystem;
+          nixpkgs.buildPlatform.system = localSystem;
         };
-        configUnchecked = root.config;
-      };
-      defaultData = withData data;
-    in rec {
-      inherit (defaultData) configuredHosts configuredSecrets configuredSystems buildSystems configUnchecked;
-      injectData = data: let
-        injectedData = withData data;
       in {
-        inherit (injectedData) configuredHosts configuredSecrets configuredSystems buildSystems configUnchecked;
+        toplevel = builtins.mapAttrs (_name: value: value.config.system.build.toplevel) (configuredSystemsWithExtraModules [
+          buildConfigurationModule
+          ({...}: {
+            buildTarget = "toplevel";
+          })
+        ]);
+        sdImage = builtins.mapAttrs (_name: value: value.config.system.build.sdImage) (configuredSystemsWithExtraModules [
+          buildConfigurationModule
+          #(nixpkgs + "/nixos/modules/installer/sd-card/sd-image-aarch64-installer.nix")
+          ({...}: {
+            buildTarget = "sd-image";
+          })
+        ]);
+        installationCd = builtins.mapAttrs (_name: value: value.config.system.build.isoImage) (configuredSystemsWithExtraModules [
+          buildConfigurationModule
+          (nixpkgs + "/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix")
+          ({lib, ...}: {
+            buildTarget = "installation-cd";
+            # Needed for https://github.com/NixOS/nixpkgs/issues/58959
+            boot.supportedFilesystems = lib.mkForce ["btrfs" "reiserfs" "vfat" "f2fs" "xfs" "ntfs" "cifs"];
+          })
+        ]);
       };
+      configUnchecked = root.config;
+    };
+    defaultData = withData {
+      inherit data;
+      root = checkedRoot;
+    };
+    uncheckedData = withData {inherit data root;};
+  in rec {
+    inherit (defaultData) configuredHosts configuredSystems buildSystems configUnchecked buildableSystems;
+    unchecked = {
+      inherit (uncheckedData) configuredHosts configuredSystems buildSystems configUnchecked buildableSystems;
+    };
+    injectData = data: let
+      injectedData = withData data;
+    in {
+      inherit (injectedData) configuredHosts configuredSystems buildSystems configUnchecked;
     };
+  };
 }
modifiedmodules/fleet/secrets.nixdiffbeforeafterboth
--- a/modules/fleet/secrets.nix
+++ b/modules/fleet/secrets.nix
@@ -15,6 +15,9 @@
         type = bool;
         description = "Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted";
       };
+      generateImpure = mkOption {
+        type = unspecified;
+      };
       generator = mkOption {
         type = nullOr (submodule {
           packages = mkOption {