git.delta.rocks / jrsonnet / refs/commits / cc4299b11a7b

difftreelog

refactor nix-eval is now fully sync, parallelism should be explicit at the callsite

zorryxklYaroslav Bolyukin2025-09-18parent: #cf89cc0.patch.diff
in: trunk

6 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2070,8 +2070,6 @@
  "serde_json",
  "test-log",
  "thiserror 2.0.16",
- "tokio",
- "tokio-util",
  "tracing",
  "tracing-indicatif",
  "vte 0.15.0",
modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -8,7 +8,7 @@
 	opts::FleetOpts,
 };
 use nix_eval::nix_go;
-use tokio::task::LocalSet;
+use tokio::task::{LocalSet, spawn_blocking};
 use tracing::{Instrument, error, field, info, info_span, warn};
 
 #[derive(Parser)]
@@ -34,7 +34,9 @@
 	// let action = Action::from(self.subcommand.clone());
 	let nixos = host.nixos_config().await?;
 	let drv = nix_go!(nixos.system.build[{ build_attr }]);
-	let out_output = drv.build("out").await?;
+	let out_output = spawn_blocking(move || drv.build("out"))
+		.await
+		.expect("system derivation build should not panic")?;
 
 	// We already have system profiles for backups.
 	if !host.local {
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
after · cmds/fleet/src/cmds/secrets/mod.rs
1use std::{2	collections::{BTreeMap, BTreeSet, HashSet},3	io::{self, Read, Write, stdin, stdout},4	path::PathBuf,5	slice,6};78use age::Recipient;9use anyhow::{Context, Result, anyhow, bail, ensure};10use chrono::{DateTime, Utc};11use clap::Parser;12use fleet_base::{13	fleetdata::{FleetSecret, FleetSecretPart, FleetSharedSecret, encrypt_secret_data},14	host::Config,15	opts::FleetOpts,16};17use fleet_shared::SecretData;18use nix_eval::{NixType, Value, nix_go, nix_go_json};19use owo_colors::OwoColorize;20use serde::Deserialize;21use tabled::{Table, Tabled};22use tokio::{fs::read, task::spawn_blocking};23use tracing::{Instrument, error, info, info_span, warn};2425#[derive(Parser)]26pub enum Secret {27	AddManager,28	/// Force load host keys for all defined hosts29	ForceKeys,30	/// Add secret, data should be provided in stdin31	AddShared {32		/// Secret name33		name: String,34		/// Secret owners35		#[clap(long, short)]36		machines: Vec<String>,37		/// Override secret if already present38		#[clap(long)]39		force: bool,40		/// Secret public part41		#[clap(long)]42		public: Option<String>,43		/// Load public part from specified file44		#[clap(long)]45		public_file: Option<PathBuf>,4647		/// Create a notification on secret expiration48		#[clap(long)]49		expires_at: Option<DateTime<Utc>>,5051		/// Secret with this name already exists, override its value while keeping the same owners.52		#[clap(long)]53		re_add: bool,5455		/// How to name public secret part56		#[clap(long, short = 'p', default_value = "public")]57		public_part: String,58		/// How to name private secret part59		#[clap(short = 's', long, default_value = "secret")]60		part: String,61	},62	/// Add secret, data should be provided in stdin63	Add {64		/// Secret name65		name: String,66		/// Secret owner67		#[clap(short = 'm', long)]68		machine: String,69		/// Replace secret if already present70		#[clap(long)]71		replace: bool,72		/// Add new parts to existing secret73		#[clap(long)]74		merge: bool,75		/// Secret public part76		#[clap(long)]77		public: Option<String>,78		/// Load public part from specified file79		#[clap(long)]80		public_file: Option<PathBuf>,8182		/// How to name public secret part83		#[clap(short = 'p', long, default_value = "public")]84		public_part: String,85		/// How to name private secret part86		#[clap(short = 's', long, default_value = "secret")]87		part: String,88	},89	/// Read secret from remote host, requires sudo on said host90	Read {91		name: String,92		#[clap(short = 'm', long)]93		machine: String,9495		/// Which private secret part to read96		#[clap(short = 'p', long, default_value = "secret")]97		part: String,98	},99	/// Read secret from remote host, requires sudo on said host100	ReadShared {101		name: String,102		/// Which private secret part to read103		#[clap(short = 'p', long, default_value = "secret")]104		part: String,105		/// Which host should we use to decrypt, in case if reencryption is required, without106		/// regeneration107		#[clap(long)]108		prefer_identities: Vec<String>,109	},110	UpdateShared {111		name: String,112113		#[clap(short = 'm', long)]114		machine: Option<Vec<String>>,115116		#[clap(long)]117		add_machine: Vec<String>,118		#[clap(long)]119		remove_machine: Vec<String>,120121		/// Which host should we use to decrypt122		#[clap(long)]123		prefer_identities: Vec<String>,124	},125	Regenerate {126		/// Which host should we use to decrypt, in case if reencryption is required, without127		/// regeneration128		#[clap(long)]129		prefer_identities: Vec<String>,130		/// Only regenerate shared secrets131		#[clap(long)]132		skip_hosts: bool,133	},134	List {},135	Edit {136		name: String,137		#[clap(short = 'm', long)]138		machine: String,139140		#[clap(long)]141		add: bool,142143		/// Which private secret part to read144		#[clap(short = 'p', long, default_value = "secret")]145		part: String,146	},147}148149fn secret_needs_regeneration(150	secret: &FleetSecret,151	expected_generation_data: &serde_json::Value,152) -> bool {153	let data_is_expected = secret.generation_data == *expected_generation_data;154	// TODO: Leeway?155	let expired = secret156		.expires_at157		.map(|expiration| expiration < Utc::now())158		.unwrap_or(false);159	expired || !data_is_expected160}161162#[allow(clippy::too_many_arguments)]163#[tracing::instrument(skip(config, secret, field, prefer_identities))]164async fn maybe_regenerate_shared_secret(165	secret_name: &str,166	config: &Config,167	mut secret: FleetSharedSecret,168	field: Value,169	expected_owners: &[String],170	expected_generation_data: serde_json::Value,171	prefer_identities: &[String],172) -> Result<FleetSharedSecret> {173	let original_set = secret.owners.clone();174175	let set = original_set.iter().collect::<BTreeSet<_>>();176	let expected_set = expected_owners.iter().collect::<BTreeSet<_>>();177178	let regeneration_required =179		secret_needs_regeneration(&secret.secret, &expected_generation_data);180181	if set == expected_set && !regeneration_required {182		info!("no need to update owner list, it is already correct");183		return Ok(secret);184	}185186	let should_regenerate = if regeneration_required {187		info!("secret has its generation data changed, regeneration is required");188		true189	} else if set.difference(&expected_set).next().is_some() {190		// TODO: Remove this warning for revokable secrets.191		warn!(192			"host was removed from secret owners, but until this host rebuild, the secret will still be stored on it."193		);194		nix_go_json!(field.regenerateOnOwnerRemoved)195	} else if expected_set.difference(&set).next().is_some() {196		nix_go_json!(field.regenerateOnOwnerAdded)197	} else {198		false199	};200201	if should_regenerate {202		info!("secret needs to be regenerated");203		let generated = generate_shared(204			config,205			secret_name,206			field,207			expected_owners.to_vec(),208			expected_generation_data,209		)210		.await?;211		Ok(generated)212	} else {213		let identity_holder = if !prefer_identities.is_empty() {214			prefer_identities215				.iter()216				.find(|i| original_set.iter().any(|s| s == *i))217		} else {218			secret.owners.first()219		};220		let Some(identity_holder) = identity_holder else {221			bail!("no available holder found");222		};223224		for (part_name, part) in secret.secret.parts.iter_mut() {225			let _span = info_span!("part reencryption", part_name);226			if !part.raw.encrypted {227				continue;228			}229			let host = config.host(identity_holder).await?;230			let encrypted = host231				.reencrypt(part.raw.clone(), expected_owners.to_vec())232				.await?;233			part.raw = encrypted;234		}235236		secret.owners = expected_owners.to_vec();237		Ok(secret)238	}239}240241#[derive(Deserialize)]242#[serde(rename_all = "camelCase")]243enum GeneratorKind {244	Impure,245	Pure,246}247248async fn generate_pure(249	_config: &Config,250	_display_name: &str,251	_secret: Value,252	_default_generator: Value,253	_owners: &[String],254) -> Result<FleetSecret> {255	bail!("pure generators are broken for now")256}257async fn generate_impure(258	config: &Config,259	_display_name: &str,260	secret: Value,261	default_generator: Value,262	expected_owners: &[String],263	expected_generation_data: serde_json::Value,264) -> Result<FleetSecret> {265	let generator = nix_go!(secret.generator);266	let on: Option<String> = nix_go_json!(default_generator.impureOn);267268	let nixpkgs = &config.nixpkgs;269270	let host = if let Some(on) = &on {271		config.host(on).await?272	} else {273		config.local_host()274	};275	let on_pkgs = host.pkgs().await?;276	let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);277278	let mut recipients = Vec::new();279	for owner in expected_owners {280		let key = config.key(owner).await?;281		recipients.push(key);282	}283	let generators = nix_go!(mk_secret_generators(Obj { recipients }));284	// FIXME: Apparently, // operator is slow in nix285	let pkgs_and_generators = on_pkgs.attrs_update(generators)?;286287	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));288289	let generator = nix_go!(call_package(generator)(Obj {}));290291	let generator = spawn_blocking(move || generator.build("out"))292		.await293		.expect("nix build shouldn't fail")?;294	let generator = host.remote_derivation(&generator).await?;295296	let out_parent = host.mktemp_dir().await?;297	let out = format!("{out_parent}/out");298299	let mut r#gen = host.cmd(generator).await?;300	r#gen.env("out", &out);301	if on.is_none() {302		// This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.303		let project_path: String = config304			.directory305			.clone()306			.into_os_string()307			.into_string()308			.map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;309		r#gen.env("FLEET_PROJECT", project_path);310	}311	r#gen.run().await.context("impure generator")?;312313	{314		let marker = host.read_file_text(format!("{out}/marker")).await?;315		ensure!(marker == "SUCCESS", "generation not succeeded");316	}317318	let mut parts = BTreeMap::new();319	for part in host.read_dir(&out).await? {320		if part == "created_at" || part == "expires_at" || part == "marker" {321			continue;322		}323		let contents: SecretData = host324			.read_file_text(format!("{out}/{part}"))325			.await?326			.parse()327			.map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;328		parts.insert(part.to_owned(), FleetSecretPart { raw: contents });329	}330331	let created_at = host.read_file_value(format!("{out}/created_at")).await?;332	let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();333334	Ok(FleetSecret {335		created_at,336		expires_at,337		parts,338		generation_data: expected_generation_data,339	})340}341async fn generate(342	config: &Config,343	display_name: &str,344	secret: Value,345	expected_owners: &[String],346	expected_generation_data: serde_json::Value,347) -> Result<FleetSecret> {348	let generator = nix_go!(secret.generator);349	// Can't properly check on nix module system level350	{351		let gen_ty = generator.type_of();352		if matches!(gen_ty, NixType::Null) {353			bail!("secret has no generator defined, can't automatically generate it.");354		}355		if matches!(gen_ty, NixType::Attrs) {356			if !generator.has_field("__functor")? {357				bail!("generator should be functor, got {gen_ty:?}");358			}359		} else if matches!(gen_ty, NixType::Function) {360			bail!("generator should be functor, got {gen_ty:?}");361		}362	}363	let nixpkgs = &config.nixpkgs;364	let default_pkgs = &config.default_pkgs;365	let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);366	// Generators provide additional information in passthru, to access367	// passthru we should call generator, but information about where this generator is supposed to build368	// is located in passthru... Thus evaluating generator on host.369	//370	// Maybe it is also possible to do some magic with __functor?371	//372	// I don't want to make modules always responsible for additional secret data anyway,373	// so it should be in derivation, and not in the secret data itself.374	let generators = nix_go!(default_mk_secret_generators(Obj {375		recipients: <Vec<String>>::new(),376	}));377	let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;378379	let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));380	let default_generator = nix_go!(call_package(generator)(Obj {}));381382	let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);383384	match kind {385		GeneratorKind::Impure => {386			generate_impure(387				config,388				display_name,389				secret,390				default_generator,391				expected_owners,392				expected_generation_data,393			)394			.await395		}396		GeneratorKind::Pure => {397			generate_pure(398				config,399				display_name,400				secret,401				default_generator,402				expected_owners,403			)404			.await405		}406	}407}408async fn generate_shared(409	config: &Config,410	display_name: &str,411	secret: Value,412	expected_owners: Vec<String>,413	expected_generation_data: serde_json::Value,414) -> Result<FleetSharedSecret> {415	// let owners: Vec<String> = nix_go_json!(secret.expectedOwners);416	Ok(FleetSharedSecret {417		secret: generate(418			config,419			display_name,420			secret,421			&expected_owners,422			expected_generation_data,423		)424		.await?,425		owners: expected_owners,426	})427}428429async fn parse_public(430	public: Option<String>,431	public_file: Option<PathBuf>,432) -> Result<Option<SecretData>> {433	Ok(match (public, public_file) {434		(Some(v), None) => Some(SecretData {435			data: v.into(),436			encrypted: false,437		}),438		(None, Some(v)) => Some(SecretData {439			data: read(v).await?,440			encrypted: false,441		}),442		(Some(_), Some(_)) => {443			bail!("only public or public_file should be set")444		}445		(None, None) => None,446	})447}448449async fn parse_secret() -> Result<Option<Vec<u8>>> {450	let mut input = vec![];451	stdin().read_to_end(&mut input)?;452	if input.is_empty() {453		Ok(None)454	} else {455		Ok(Some(input))456	}457}458459fn parse_machines(460	initial: Vec<String>,461	machines: Option<Vec<String>>,462	mut add_machines: Vec<String>,463	mut remove_machines: Vec<String>,464) -> Result<Vec<String>> {465	if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {466		bail!("no operation");467	}468469	let initial_machines = initial.clone();470	let mut target_machines = initial;471	info!("Currently encrypted for {initial_machines:?}");472473	// ensure!(machines.is_some() || !add_machines.is_empty() || )474	if let Some(machines) = machines {475		ensure!(476			add_machines.is_empty() && remove_machines.is_empty(),477			"can't combine --machines and --add-machines/--remove-machines"478		);479		let target = initial_machines.iter().collect::<HashSet<_>>();480		let source = machines.iter().collect::<HashSet<_>>();481		for removed in target.difference(&source) {482			remove_machines.push((*removed).clone());483		}484		for added in source.difference(&target) {485			add_machines.push((*added).clone());486		}487	}488489	for machine in &remove_machines {490		let mut removed = false;491		while let Some(pos) = target_machines.iter().position(|m| m == machine) {492			target_machines.swap_remove(pos);493			removed = true;494		}495		if !removed {496			warn!("secret is not enabled for {machine}");497		}498	}499	for machine in &add_machines {500		if target_machines.iter().any(|m| m == machine) {501			warn!("secret is already added to {machine}");502		} else {503			target_machines.push(machine.to_owned());504		}505	}506	if !remove_machines.is_empty() {507		// TODO: maybe force secret regeneration?508		// Not that useful without revokation.509		warn!(510			"secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret"511		);512	}513	Ok(target_machines)514}515impl Secret {516	pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {517		match self {518			Secret::AddManager => {519				todo!("part of fleet-pusher")520			}521			Secret::ForceKeys => {522				for host in config.list_hosts().await? {523					if opts.should_skip(&host).await? {524						continue;525					}526					config.key(&host.name).await?;527				}528			}529			Secret::AddShared {530				mut machines,531				name,532				force,533				public,534				public_part: public_name,535				public_file,536				expires_at,537				re_add,538				part: part_name,539			} => {540				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).541542				let exists = config.has_shared(&name);543				if exists && !force && !re_add {544					bail!("secret already defined");545				}546				if re_add {547					// Fixme: use clap to limit this usage548					ensure!(!force, "--force and --readd are not compatible");549					ensure!(exists, "secret doesn't exists");550					ensure!(551						machines.is_empty(),552						"you can't use machines argument for --readd"553					);554					let shared = config.shared_secret(&name)?;555					machines = shared.owners;556				}557558				let recipients = config.recipients(machines.clone()).await?;559560				let mut parts = BTreeMap::new();561562				let mut input = vec![];563				io::stdin().read_to_end(&mut input)?;564565				if !input.is_empty() {566					let encrypted =567						encrypt_secret_data(recipients.iter().map(|r| r as &dyn Recipient), input)568							.ok_or_else(|| anyhow!("no recipients provided"))?;569					parts.insert(part_name, FleetSecretPart { raw: encrypted });570				}571572				if let Some(public) = parse_public(public, public_file).await? {573					parts.insert(public_name, FleetSecretPart { raw: public });574				}575576				config.replace_shared(577					name,578					FleetSharedSecret {579						owners: machines,580						secret: FleetSecret {581							created_at: Utc::now(),582							expires_at,583							parts,584							generation_data: serde_json::Value::Null,585						},586					},587				);588			}589			Secret::Add {590				machine,591				name,592				replace,593				merge,594				public,595				public_part: public_name,596				public_file,597				part: part_name,598			} => {599				if config.has_secret(&machine, &name) && !replace && !merge {600					bail!(601						"secret already defined.\nUse --replace to override, or --merge to add new parts to existing secret"602					);603				}604605				let mut out = if merge && !replace {606					config607						.host_secret(&machine, &name)608						.context("failed to read existing secret for --merge")?609				} else {610					FleetSecret {611						created_at: Utc::now(),612						expires_at: None,613						parts: BTreeMap::new(),614						generation_data: serde_json::Value::Null,615					}616				};617618				if let Some(secret) = parse_secret().await? {619					let recipient = config.recipient(&machine).await?;620					let encrypted = encrypt_secret_data([&recipient as &dyn Recipient], secret)621						.expect("recipient provided");622					if out623						.parts624						.insert(part_name.clone(), FleetSecretPart { raw: encrypted })625						.is_some() && !replace626					{627						bail!("part {part_name:?} is already defined");628					}629				}630631				if let Some(public) = parse_public(public, public_file).await? {632					if out633						.parts634						.insert(public_name.clone(), FleetSecretPart { raw: public })635						.is_some() && !replace636					{637						bail!("part {public_name:?} is already defined");638					}639				};640641				config.insert_secret(&machine, name, out);642			}643			#[allow(clippy::await_holding_refcell_ref)]644			Secret::Read {645				name,646				machine,647				part: part_name,648			} => {649				let secret = config.host_secret(&machine, &name)?;650				let Some(secret) = secret.parts.get(&part_name) else {651					bail!("no part {part_name} in secret {name}");652				};653				let data = if secret.raw.encrypted {654					let host = config.host(&machine).await?;655					host.decrypt(secret.raw.clone()).await?656				} else {657					secret.raw.data.clone()658				};659660				stdout().write_all(&data)?;661			}662			Secret::ReadShared {663				name,664				part: part_name,665				prefer_identities,666			} => {667				let secret = config.shared_secret(&name)?;668				let Some(part) = secret.secret.parts.get(&part_name) else {669					bail!("no part {part_name} in secret {name}");670				};671				let data = if part.raw.encrypted {672					let identity_holder = if !prefer_identities.is_empty() {673						prefer_identities674							.iter()675							.find(|i| secret.owners.iter().any(|s| s == *i))676					} else {677						secret.owners.first()678					};679					let Some(identity_holder) = identity_holder else {680						bail!("no available holder found");681					};682					let host = config.host(identity_holder).await?;683					host.decrypt(part.raw.clone()).await?684				} else {685					part.raw.data.clone()686				};687				stdout().write_all(&data)?;688			}689			Secret::UpdateShared {690				name,691				machine,692				add_machine,693				remove_machine,694				prefer_identities,695			} => {696				// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).697698				let secret = config.shared_secret(&name)?;699				if secret.secret.parts.values().all(|v| !v.raw.encrypted) {700					bail!("no secret");701				}702703				let initial_machines = secret.owners.clone();704				let target_machines = parse_machines(705					initial_machines.clone(),706					machine,707					add_machine,708					remove_machine,709				)?;710711				if target_machines.is_empty() {712					info!("no machines left for secret, removing it");713					config.remove_shared(&name);714					return Ok(());715				}716717				let config_field = &config.config_field;718				let name_clone = name.clone();719				let field = nix_go!(config_field.sharedSecrets[name_clone]);720				let expected_generation_data = nix_go_json!(field.expectedGenerationData);721722				let updated = maybe_regenerate_shared_secret(723					&name,724					config,725					secret,726					field,727					&target_machines,728					expected_generation_data,729					&prefer_identities,730					// None,731				)732				.await?;733				config.replace_shared(name, updated);734			}735			Secret::Regenerate {736				prefer_identities,737				skip_hosts,738			} => {739				info!("checking for secrets to regenerate");740				let stored_shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();741				{742					// Generate missing shared743					let _span = info_span!("shared").entered();744					let expected_shared_set = config745						.list_configured_shared()746						.await?747						.into_iter()748						.collect::<HashSet<_>>();749					for missing in expected_shared_set.difference(&stored_shared_set) {750						let config_field = &config.config_field;751						let secret = nix_go!(config_field.sharedSecrets[{ missing }]);752						let expected_generation_data: serde_json::Value =753							nix_go_json!(secret.expectedGenerationData);754						let expected_owners: Option<Vec<String>> =755							nix_go_json!(secret.expectedOwners);756						let Some(expected_owners) = expected_owners else {757							// Can't generate this missing secret, as it has no defined owners.758							continue;759						};760						info!("generating secret: {missing}");761						let shared = generate_shared(762							config,763							missing,764							secret,765							expected_owners,766							expected_generation_data,767						)768						.in_current_span()769						.await?;770						config.replace_shared(missing.to_string(), shared)771					}772				}773				if !skip_hosts {774					for host in config.list_hosts().await? {775						if opts.should_skip(&host).await? {776							continue;777						}778779						let _span = info_span!("host", host = host.name).entered();780						let expected_set = host781							.list_configured_secrets()782							.in_current_span()783							.await?784							.into_iter()785							.collect::<HashSet<_>>();786						let stored_set = config787							.list_secrets(&host.name)788							.into_iter()789							.collect::<HashSet<_>>();790						for missing in expected_set.difference(&stored_set) {791							info!("generating secret: {missing}");792							let secret = host.secret_field(missing).in_current_span().await?;793							let expected_generation_data =794								nix_go_json!(secret.expectedGenerationData);795							let generated = match generate(796								config,797								missing,798								secret,799								slice::from_ref(&host.name),800								expected_generation_data,801							)802							.in_current_span()803							.await804							{805								Ok(v) => v,806								Err(e) => {807									error!("{e:?}");808									continue;809								}810							};811							config.insert_secret(&host.name, missing.to_string(), generated)812						}813						for name in stored_set {814							info!("updating secret: {name}");815							let data = config.host_secret(&host.name, &name)?;816							let secret = host.secret_field(&name).in_current_span().await?;817							let expected_generation_data =818								nix_go_json!(secret.expectedGenerationData);819							if secret_needs_regeneration(&data, &expected_generation_data) {820								let generated = match generate(821									config,822									&name,823									secret,824									slice::from_ref(&host.name),825									expected_generation_data,826								)827								.in_current_span()828								.await829								{830									Ok(v) => v,831									Err(e) => {832										error!("{e:?}");833										continue;834									}835								};836								config.insert_secret(&host.name, name.to_string(), generated)837							}838						}839					}840				}841				let mut to_remove = Vec::new();842				for name in &stored_shared_set {843					info!("updating secret: {name}");844					let data = config.shared_secret(name)?;845					let config_field = &config.config_field;846					let expected_owners: Option<Vec<String>> =847						nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);848					let Some(expected_owners) = expected_owners else {849						warn!("secret was removed from fleet config: {name}, removing from data");850						to_remove.push(name.to_string());851						continue;852					};853854					let secret = nix_go!(config_field.sharedSecrets[{ name }]);855					let expected_generation_data = nix_go_json!(secret.expectedGenerationData);856					config.replace_shared(857						name.to_owned(),858						maybe_regenerate_shared_secret(859							name,860							config,861							data,862							secret,863							&expected_owners,864							expected_generation_data,865							&prefer_identities,866							// None,867						)868						.await?,869					);870				}871				for k in to_remove {872					config.remove_shared(&k);873				}874			}875			Secret::List {} => {876				let _span = info_span!("loading secrets").entered();877				let configured = config.list_configured_shared().await?;878				#[derive(Tabled)]879				struct SecretDisplay {880					#[tabled(rename = "Name")]881					name: String,882					#[tabled(rename = "Owners")]883					owners: String,884				}885				let mut table = vec![];886				for name in configured.iter().cloned() {887					let config = config.clone();888					let expected_owners = config.shared_secret_expected_owners(&name).await?;889					let data = config.shared_secret(&name)?;890					let owners = data891						.owners892						.iter()893						.map(|o| {894							if expected_owners.contains(o) {895								o.green().to_string()896							} else {897								o.red().to_string()898							}899						})900						.collect::<Vec<_>>();901					table.push(SecretDisplay {902						owners: owners.join(", "),903						name,904					})905				}906				info!("loaded\n{}", Table::new(table).to_string())907			}908			Secret::Edit {909				name,910				machine,911				part,912				add,913			} => {914				let secret = config.host_secret(&machine, &name)?;915				if let Some(data) = secret.parts.get(&part) {916					let host = config.host(&machine).await?;917					let secret = host.decrypt(data.raw.clone()).await?;918					String::from_utf8(secret).context("secret is not utf8")?919				} else if add {920					String::new()921				} else {922					bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");923				};924			}925		}926		Ok(())927	}928}929930/*931async fn edit_temp_file(932	builder: tempfile::Builder<'_, '_>,933	r: Vec<u8>,934	header: &str,935	comment: &str,936) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {937	if !stdin().is_tty() {938		// TODO: Also try to open /dev/tty directly?939		bail!("stdin is not tty, can't open editor");940	}941942	use std::fmt::Write;943	let mut file = builder.tempfile()?;944945	let mut full_header = String::new();946	let mut had = false;947	for line in header.trim_end().lines() {948		had = true;949		writeln!(&mut full_header, "{comment}{line}")?;950	}951	if had {952		writeln!(&mut full_header, "{}", comment.trim_end())?;953	}954	writeln!(955		&mut full_header,956		"{comment}Do not touch this header! It will be removed automatically"957	)?;958959	file.write_all(full_header.as_bytes())?;960	file.write_all(&r)?;961962	let abs_path = file.into_temp_path();963	let editor = std::env::var_os("VISUAL")964		.or_else(|| std::env::var_os("EDITOR"))965		.unwrap_or_else(|| "vi".into());966	let editor_args = shlex::bytes::split(editor.as_encoded_bytes())967		.ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;968	let editor_args = editor_args969		.into_iter()970		.map(|v| {971			// Only ASCII subsequences are replaced972			unsafe { OsString::from_encoded_bytes_unchecked(v) }973		})974		.collect_vec();975	let Some((editor, args)) = editor_args.split_first() else {976		bail!("EDITOR env var has no command");977	};978	let mut command = Command::new(editor);979	command.args(args);980981	let path_arg = abs_path.canonicalize()?;982983	// TODO: Save full state, using tcget/_getmode/_setmode984	let was_raw = terminal::is_raw_mode_enabled()?;985	terminal::enable_raw_mode()?;986987	let status = command.arg(path_arg).status().await;988989	if !was_raw {990		terminal::disable_raw_mode()?;991	}992993	let success = match status {994		Ok(s) => s.success(),995		Err(e) if e.kind() == io::ErrorKind::NotFound => {996			bail!("editor not found")997		}998		Err(e) => bail!("editor spawn error: {e}"),999	};10001001	let mut file = std::fs::read(&abs_path).context("read editor output")?;1002	let Some(v) = file.strip_prefix(full_header.as_bytes()) else {1003		todo!();1004	};1005	todo!();10061007	// Ok((success, abs_path))1008}1009*/
modifiedcmds/fleet/src/cmds/tf.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/tf.rs
+++ b/cmds/fleet/src/cmds/tf.rs
@@ -10,6 +10,7 @@
 use tokio::{
 	fs::{self, create_dir_all},
 	process::Command,
+	task::spawn_blocking,
 };
 use tracing::debug;
 
@@ -38,7 +39,10 @@
 			debug!("generating terraform configs");
 			let system = &config.local_system;
 			let config = &config.config_field;
-			let data: PathBuf = nix_go!(config.tf({ system })).build("out").await?;
+			let data = nix_go!(config.tf({ system }));
+			let data: PathBuf = spawn_blocking(move || data.build("out"))
+				.await
+				.expect("tf.json derivation should not fail")?;
 			let data = fs::read(&data).await?;
 
 			create_dir_all(&dir).await?;
modifiedcrates/nix-eval/Cargo.tomldiffbeforeafterboth
--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -11,8 +11,6 @@
 serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
 thiserror.workspace = true
-tokio = { workspace = true }
-tokio-util.workspace = true
 tracing.workspace = true
 
 cxx = "1.0.168"
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -750,7 +750,7 @@
 		})?;
 		Ok(out)
 	}
-	pub async fn build(&self, output: &str) -> Result<PathBuf> {
+	pub fn build(&self, output: &str) -> Result<PathBuf> {
 		if !self.is_derivation() {
 			bail!("expected derivation to build")
 		}