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

difftreelog

feat use builtin for getting secret

lyunptusYaroslav Bolyukin2026-01-22parent: #c810e3a.patch.diff
in: trunk

12 files changed

modifiedCargo.lockdiffbeforeafterboth
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -723,15 +723,6 @@
 checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
 
 [[package]]
-name = "convert_case"
-version = "0.7.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7"
-dependencies = [
- "unicode-segmentation",
-]
-
-[[package]]
 name = "cookie-factory"
 version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -765,34 +756,6 @@
 ]
 
 [[package]]
-name = "crossterm"
-version = "0.29.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
-dependencies = [
- "bitflags",
- "crossterm_winapi",
- "derive_more",
- "document-features",
- "filedescriptor",
- "mio",
- "parking_lot",
- "rustix 1.1.2",
- "signal-hook",
- "signal-hook-mio",
- "winapi",
-]
-
-[[package]]
-name = "crossterm_winapi"
-version = "0.9.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
-dependencies = [
- "winapi",
-]
-
-[[package]]
 name = "crypto-common"
 version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -932,27 +895,6 @@
 dependencies = [
  "powerfmt",
  "serde_core",
-]
-
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "convert_case",
- "proc-macro2",
- "quote",
- "syn",
 ]
 
 [[package]]
@@ -976,15 +918,6 @@
  "proc-macro2",
  "quote",
  "syn",
-]
-
-[[package]]
-name = "document-features"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d"
-dependencies = [
- "litrs",
 ]
 
 [[package]]
@@ -1073,17 +1006,6 @@
 checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
 
 [[package]]
-name = "filedescriptor"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
-dependencies = [
- "libc",
- "thiserror 1.0.69",
- "winapi",
-]
-
-[[package]]
 name = "find-crate"
 version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1128,7 +1050,6 @@
  "chrono",
  "clap",
  "clap_complete",
- "crossterm",
  "fleet-base",
  "fleet-shared",
  "futures",
@@ -1142,7 +1063,6 @@
  "openssh",
  "opentelemetry",
  "opentelemetry_sdk",
- "owo-colors",
  "peg",
  "regex",
  "serde",
@@ -1503,12 +1423,6 @@
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
 [[package]]
-name = "hermit-abi"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
-
-[[package]]
 name = "hex"
 version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1961,24 +1875,7 @@
 dependencies = [
  "memchr",
  "serde",
-]
-
-[[package]]
-name = "is-terminal"
-version = "0.4.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
-dependencies = [
- "hermit-abi",
- "libc",
- "windows-sys 0.59.0",
 ]
-
-[[package]]
-name = "is_ci"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
 
 [[package]]
 name = "is_terminal_polyfill"
@@ -2083,12 +1980,6 @@
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
-
-[[package]]
-name = "litrs"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed"
 
 [[package]]
 name = "lock_api"
@@ -2161,7 +2052,6 @@
 checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
 dependencies = [
  "libc",
- "log",
  "wasi 0.11.1+wasi-snapshot-preview1",
  "windows-sys 0.59.0",
 ]
@@ -2428,16 +2318,6 @@
 ]
 
 [[package]]
-name = "owo-colors"
-version = "4.2.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
-dependencies = [
- "supports-color 2.1.0",
- "supports-color 3.0.2",
-]
-
-[[package]]
 name = "papergrid"
 version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3335,27 +3215,6 @@
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
-
-[[package]]
-name = "signal-hook"
-version = "0.3.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
-dependencies = [
- "libc",
- "signal-hook-registry",
-]
-
-[[package]]
-name = "signal-hook-mio"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
-dependencies = [
- "libc",
- "mio",
- "signal-hook",
-]
 
 [[package]]
 name = "signal-hook-registry"
@@ -3449,25 +3308,6 @@
 checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
 [[package]]
-name = "supports-color"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
-dependencies = [
- "is-terminal",
- "is_ci",
-]
-
-[[package]]
-name = "supports-color"
-version = "3.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6"
-dependencies = [
- "is_ci",
-]
-
-[[package]]
 name = "syn"
 version = "2.0.106"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4170,12 +4010,6 @@
 checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
 
 [[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
-
-[[package]]
 name = "unicode-width"
 version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4448,25 +4282,9 @@
  "home",
  "once_cell",
  "rustix 0.38.44",
-]
-
-[[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
 ]
 
 [[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
 name = "winapi-util"
 version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4474,12 +4292,6 @@
 dependencies = [
  "windows-sys 0.61.2",
 ]
-
-[[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "windows-core"
modifiedREADME.adocdiffbeforeafterboth
--- a/README.adoc
+++ b/README.adoc
@@ -211,7 +211,7 @@
   ];
   # And finally, I have secrets, which are shared between machines.
   # Note that this example is somewhat wrong, as this goes not into the machine configuration, but to fleet configuration.
-  sharedSecrets = {
+  secrets = {
     "ca.pem" = {
       # This is just the public key, no need to regenerate it to change owner list
       regenerateOnOwnerAdded = false;
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -28,12 +28,10 @@
 async-trait = "0.1"
 base64 = "0.22.1"
 chrono = { version = "0.4", features = ["serde"] }
-crossterm = { version = "0.29.0", features = ["use-dev-tty"] }
 futures = "0.3"
 hostname = "0.4.1"
 itertools = "0.14"
 openssh = "0.11"
-owo-colors = { version = "4.2", features = ["supports-color", "supports-colors"] }
 peg = "0.8"
 regex = "1.11"
 shlex = "1.3"
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -11,11 +11,10 @@
 	fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},
 	host::Config,
 	opts::FleetOpts,
-	secret::{Expectations, RegenerationReason, SharedSecretDefinition, secret_needs_regeneration},
+	secret::{Expectations, RegenerationReason, secret_needs_regeneration},
 };
 use fleet_shared::SecretData;
 use nix_eval::{NixType, Value, nix_go, nix_go_json};
-use owo_colors::OwoColorize;
 use serde::Deserialize;
 use tabled::{Table, Tabled};
 use tokio::{fs::read, task::spawn_blocking};
@@ -69,6 +68,7 @@
 	},
 }
 
+/*
 #[allow(clippy::too_many_arguments)]
 #[tracing::instrument(skip(config, secret, definition, prefer_identities))]
 async fn maybe_regenerate_shared_secret(
@@ -143,6 +143,7 @@
 		Ok(secret)
 	}
 }
+*/
 
 #[derive(Deserialize)]
 #[serde(rename_all = "camelCase")]
@@ -314,6 +315,7 @@
 		}
 	}
 }
+/*
 async fn generate_shared(
 	config: &Config,
 	display_name: &str,
@@ -332,7 +334,7 @@
 		.await?,
 		owners: expectations.owners.clone(),
 	})
-}
+}*/
 
 async fn parse_public(
 	public: Option<String>,
@@ -625,10 +627,11 @@
 					#[tabled(rename = "Owners")]
 					owners: String,
 				}
-				let mut table = vec![];
+				// let mut table = vec![];
 				for name in configured.iter().cloned() {
 					let config = config.clone();
 					let data = config.shared_secret(&name).expect("exists");
+					/*
 					let definition = config.shared_secret_definition(&name)?;
 					let expectations = definition.expectations()?;
 					let owners = data
@@ -645,8 +648,9 @@
 						owners: owners.join(", "),
 						name,
 					})
+*/
 				}
-				info!("loaded\n{}", Table::new(table).to_string())
+				// info!("loaded\n{}", Table::new(table).to_string())
 			}
 			Secret::Edit {
 				name,
modifiedcrates/fleet-base/src/host.rsdiffbeforeafterboth
before · crates/fleet-base/src/host.rs
1use std::{2	cell::OnceCell,3	collections::BTreeSet,4	ffi::{OsStr, OsString},5	fmt::Display,6	io::Write,7	ops::Deref,8	path::PathBuf,9	str::FromStr,10	sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};16use openssh::{ControlPersist, SessionBuilder};17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24	command::MyCommand,25	fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},26	secret::{HostSecretDefinition, SharedSecretDefinition},27};2829pub struct FleetConfigInternals {30	/// Fleet project directory, containing fleet.nix file.31	pub directory: PathBuf,32	/// builtins.currentSystem33	pub local_system: String,34	pub data: Mutex<FleetData>,35	pub nix_args: Vec<OsString>,36	/// fleet_config.config37	pub config_field: Value,38	/// flake.output39	pub flake_outputs: Value,40	// TODO: Remove with connectivity refactor41	pub localhost: String,4243	/// import nixpkgs {system = local};44	pub default_pkgs: Value,45	/// inputs.nixpkgs46	pub nixpkgs: Value,47}4849// TODO: Make field not pub50#[derive(Clone)]51pub struct Config(pub Arc<FleetConfigInternals>);5253impl Deref for Config {54	type Target = FleetConfigInternals;5556	fn deref(&self) -> &Self::Target {57		&self.058	}59}6061#[derive(Clone, Copy, Debug)]62pub enum EscalationStrategy {63	Sudo,64	Run0,65	Su,66}6768#[derive(Clone, PartialEq, Copy, Debug)]69pub enum DeployKind {70	/// NixOS => NixOS managed by fleet71	UpgradeToFleet,72	/// NixOS managed by fleet => NixOS managed by fleet73	Fleet,74	/// Remote host has /mnt, /mnt/boot mounted,75	/// generated config is added to fleet configuration.76	NixosInstall,77	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),78	/// generated config is added to fleet configuration,79	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.80	NixosLustrate,81}8283impl FromStr for DeployKind {84	type Err = anyhow::Error;85	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {86		match s {87			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),88			"fleet" => Ok(Self::Fleet),89			"nixos-install" => Ok(Self::NixosInstall),90			"nixos-lustrate" => Ok(Self::NixosLustrate),91			v => bail!(92				"unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""93			),94		}95	}96}97pub struct ConfigHost {98	config: Config,99	pub name: String,100	groups: OnceCell<Vec<String>>,101102	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it103	deploy_kind: OnceCell<DeployKind>,104	session_destination: OnceCell<String>,105	legacy_ssh_store: OnceCell<bool>,106107	pub host_config: Option<Value>,108	pub nixos_config: OnceCell<Value>,109	pub nixos_unchecked_config: OnceCell<Value>,110	pub pkgs_override: Option<Value>,111112	// TODO: Move command helpers away with connectivity refactor113	pub local: bool,114	pub session: OnceLock<Arc<openssh::Session>>,115}116117#[derive(Debug, Clone, Copy)]118pub enum GenerationStorage {119	Deployer,120	Machine,121	Pusher,122}123impl GenerationStorage {124	fn prefix(&self) -> &'static str {125		match self {126			GenerationStorage::Deployer => "deployer.",127			GenerationStorage::Machine => "",128			GenerationStorage::Pusher => "pusher.",129		}130	}131}132133#[derive(Tabled, Debug)]134pub struct Generation {135	#[tabled(rename = "ID", format("{}", self.rollback_id()))]136	pub id: u32,137	#[tabled(rename = "Current")]138	pub current: bool,139	#[tabled(rename = "Created at")]140	pub datetime: UtcDateTime,141	#[tabled(format = "{:?}")]142	pub store_path: PathBuf,143	#[tabled(skip)]144	pub location: GenerationStorage,145}146impl Generation {147	pub fn rollback_id(&self) -> String {148		format!("{}{}", self.location.prefix(), self.id)149	}150}151152fn parse_generation_line(g: &str) -> Option<Generation> {153	let mut parts = g.split_whitespace();154	let id = parts.next()?;155	let id: u32 = id.parse().ok()?;156	let date = parts.next()?;157	let time = parts.next()?;158	let current = if let Some(current) = parts.next() {159		if current == "(current)" {160			Some(true)161		} else {162			None163		}164	} else {165		Some(false)166	};167	let current = current?;168	if parts.next().is_some() {169		warn!("unexpected text after generation: {g}");170	}171172	let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")173		.expect("valid format");174	let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;175176	Some(Generation {177		id,178		current,179		datetime,180		store_path: PathBuf::new(),181		location: GenerationStorage::Machine,182	})183}184// TODO: Move command helpers away with connectivity refactor185impl ConfigHost {186	pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {187		let mut cmd = self.cmd("nix-env").await?;188		cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))189			.arg("--list-generations")190			.env("TZ", "UTC");191		// Sudo is required because --list-generations tries to acquire profile lock192		let data = cmd.sudo().run_string().await?;193		let mut generations = data194			.split('\n')195			.map(|e| e.trim())196			.filter(|&l| !l.is_empty())197			.filter_map(|g| {198				let generation = parse_generation_line(g);199				if generation.is_none() {200					warn!("bad generation: {g}");201				};202				generation203			})204			.collect::<Vec<_>>();205		for ele in generations.iter_mut() {206			let mut cmd = self.cmd("readlink").await?;207			cmd.arg("--")208				.arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));209			let path = cmd.run_string().await?;210			ele.store_path = PathBuf::from(path.trim_end_matches("\n"));211		}212213		Ok(generations)214	}215216	pub fn set_session_destination(&self, dest: String) {217		self.session_destination218			.set(dest)219			.expect("session destination is already set")220	}221	pub fn set_deploy_kind(&self, kind: DeployKind) {222		self.deploy_kind223			.set(kind)224			.expect("deploy kind is already set");225	}226	pub fn set_legacy_ssh_store(&self, legacy: bool) {227		self.legacy_ssh_store228			.set(legacy)229			.expect("legacy ssh store is already set")230	}231	pub async fn deploy_kind(&self) -> Result<DeployKind> {232		if let Some(kind) = self.deploy_kind.get() {233			return Ok(*kind);234		}235		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {236			Ok(v) => v,237			Err(e) => {238				bail!("failed to query remote system kind: {e}");239			}240		};241		if !is_fleet_managed {242			bail!(243				"{}",244				indoc::indoc! {"245				host is not marked as managed by fleet246				if you're not trying to lustrate/install system from scratch,247				you should either248					1. manually create /etc/FLEET_HOST file on the target host,249					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet250					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos251			"}252			);253		}254		// TOCTOU is possible255		let _ = self.deploy_kind.set(DeployKind::Fleet);256		Ok(*self.deploy_kind.get().expect("deploy kind is just set"))257	}258	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {259		// Prefer sudo, as run0 has some gotchas with polkit260		// and too many repeating prompts.261		if (self.find_in_path("sudo").await).is_ok() {262			return Ok(EscalationStrategy::Sudo);263		}264		if (self.find_in_path("run0").await).is_ok() {265			return Ok(EscalationStrategy::Run0);266		}267		Ok(EscalationStrategy::Su)268	}269	async fn open_session(&self) -> Result<Arc<openssh::Session>> {270		assert!(!self.local, "do not open ssh connection to local session");271		// FIXME: TOCTOU272		if let Some(session) = &self.session.get() {273			return Ok((*session).clone());274		};275		let mut session = SessionBuilder::default();276		session.control_persist(ControlPersist::ClosedAfterInitialConnection);277278		let dest = self.session_destination.get().unwrap_or(&self.name);279		let session = session280			.connect(&dest)281			.await282			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;283		let session = Arc::new(session);284		self.session.set(session.clone()).expect("TOCTOU happened");285		Ok(session)286	}287	pub async fn mktemp_dir(&self) -> Result<String> {288		let mut cmd = self.cmd("mktemp").await?;289		cmd.arg("-d");290		let path = cmd.run_string().await?;291		Ok(path.trim_end().to_owned())292	}293	pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {294		let mut cmd = self.cmd("sh").await?;295		cmd.arg("-c")296			.arg("test -e \"$1\" && echo true || echo false")297			.arg("_")298			.arg(path);299		cmd.run_value().await300	}301	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {302		let mut cmd = self.cmd("cat").await?;303		cmd.arg(path);304		cmd.run_bytes().await305	}306	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {307		let mut cmd = self.cmd("cat").await?;308		cmd.arg(path);309		cmd.run_string().await310	}311	pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {312		let mut cmd = self.cmd("ls").await?;313		cmd.arg(path);314		let out = cmd.run_string().await?;315		let mut lines = out.split('\n');316		if let Some(last) = lines.next_back() {317			ensure!(last.is_empty(), "output of ls should end with newline");318		}319		Ok(lines.map(ToOwned::to_owned).collect())320	}321	#[allow(dead_code)]322	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {323		let text = self.read_file_text(path).await?;324		Ok(serde_json::from_str(&text)?)325	}326	pub async fn read_env(&self, env: &str) -> Result<String> {327		let mut cmd = self.cmd("printenv").await?;328		cmd.arg(env);329		cmd.run_string().await330	}331	pub async fn find_in_path(&self, command: &str) -> Result<String> {332		// // `which` is not a part of coreutils, and it might not exist on machine.333		// let path = self.read_env("PATH").await?;334		// // Assuming delimiter is :, we don't work with windows host, this check will be much335		// // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)336		// for ele in path.split(':') {337		// 	let test_path = format!("{ele}/{cmd}");338		// 	test -x etc339		// }340		// let mut cmd = self.cmd("printenv").await?;341		// cmd.arg(env);342		// Ok(cmd.run_string().await?)343		// Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.344		let mut cmd = self345			.cmd_escalation(346				// Not used347				EscalationStrategy::Su,348				"which",349			)350			.await?;351		cmd.arg(command);352		cmd.run_string().await353	}354	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>355	where356		<D as FromStr>::Err: Display,357	{358		let text = self.read_file_text(path).await?;359		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))360	}361	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {362		self.cmd_escalation(self.escalation_strategy().await?, cmd)363			.await364	}365	pub async fn cmd_escalation(366		&self,367		escalation: EscalationStrategy,368		cmd: impl AsRef<OsStr>,369	) -> Result<MyCommand> {370		if self.local {371			Ok(MyCommand::new(escalation, cmd))372		} else {373			let session = self.open_session().await?;374			Ok(MyCommand::new_on(escalation, cmd, session))375		}376	}377	pub async fn nix_cmd(&self) -> Result<MyCommand> {378		let mut nix = self.cmd("nix").await?;379		nix.args([380			"--extra-experimental-features",381			"nix-command",382			"--extra-experimental-features",383			"flakes",384		]);385		Ok(nix)386	}387388	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {389		ensure!(data.encrypted, "secret is not encrypted");390		let mut cmd = self.cmd("fleet-install-secrets").await?;391		cmd.arg("decrypt").eqarg("--secret", data.to_string());392		let encoded = cmd393			.sudo()394			.run_string()395			.await396			.context("failed to call remote host for decrypt")?;397		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;398		ensure!(!data.encrypted, "secret came out encrypted");399		Ok(data.data)400	}401	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {402		ensure!(data.encrypted, "secret is not encrypted");403		let mut cmd = self.cmd("fleet-install-secrets").await?;404		cmd.arg("reencrypt").eqarg("--secret", data.to_string());405		for target in targets {406			let key = self.config.key(&target).await?;407			cmd.eqarg("--targets", key);408		}409		let encoded = cmd410			.sudo()411			.run_string()412			.await413			.context("failed to call remote host for decrypt")?;414		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;415		ensure!(data.encrypted, "secret came out not encrypted");416		Ok(data)417	}418	/// Returns path for futureproofing, as path might change i.e on conversion to CA419	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {420		if self.local {421			// Path is located locally, thus already trusted.422			return Ok(path.to_owned());423		}424		let mut nix = MyCommand::new(425			// Not used426			EscalationStrategy::Su,427			"nix",428		);429		nix.arg("copy").arg("--substitute-on-destination");430431		let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {432			"ssh"433		} else {434			"ssh-ng"435		};436437		match self.deploy_kind().await? {438			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {439				nix.comparg("--to", format!("{proto}://{}", self.name));440			}441			DeployKind::NixosInstall => {442				nix443					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon444					.arg("--no-check-sigs")445					.comparg(446						"--to",447						format!("{proto}://root@{}?remote-store=/mnt", self.name),448					);449			}450		}451		nix.arg(path);452		nix.run_nix().await.context("nix copy")?;453		Ok(path.to_owned())454	}455	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {456		let mut cmd = self.cmd("systemctl").await?;457		cmd.arg("stop").arg(name);458		cmd.sudo().run().await459	}460	pub async fn systemctl_start(&self, name: &str) -> Result<()> {461		let mut cmd = self.cmd("systemctl").await?;462		cmd.arg("start").arg(name);463		cmd.sudo().run().await464	}465466	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {467		let mut cmd = self.cmd("rm").await?;468		cmd.arg("-f").arg(path);469		if sudo {470			cmd = cmd.sudo()471		}472		cmd.run().await473	}474}475impl ConfigHost {476	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,477	// assuming getting tags always returns the same value.478	pub async fn tags(&self) -> Result<Vec<String>> {479		if let Some(v) = self.groups.get() {480			return Ok(v.clone());481		}482		let Some(host_config) = &self.host_config else {483			return Ok(vec![]);484		};485		let tags: Vec<String> = nix_go_json!(host_config.tags);486487		let _ = self.groups.set(tags.clone());488489		Ok(tags)490	}491	pub async fn nixos_config(&self) -> Result<Value> {492		if let Some(v) = self.nixos_config.get() {493			return Ok(v.clone());494		}495		let Some(host_config) = &self.host_config else {496			bail!("local host has no nixos_config");497		};498		let nixos_config = nix_go!(host_config.nixos.config);499		assert_warn("nixos config evaluation", &nixos_config).await?;500501		let _ = self.nixos_config.set(nixos_config.clone());502503		Ok(nixos_config)504	}505	pub fn nixos_unchecked_config(&self) -> Result<Value> {506		if let Some(v) = self.nixos_unchecked_config.get() {507			return Ok(v.clone());508		}509		let Some(host_config) = &self.host_config else {510			bail!("local host has no nixos_config");511		};512		let nixos_config = nix_go!(host_config.nixos_unchecked.config);513514		let _ = self.nixos_unchecked_config.set(nixos_config.clone());515516		Ok(nixos_config)517	}518519	pub fn list_defined_secrets(&self) -> Result<Vec<String>> {520		let nixos = self.nixos_unchecked_config()?;521		let secrets = nix_go!(nixos.secrets);522		secrets.list_fields()523	}524	pub fn secret_definition(&self, name: &str) -> Result<HostSecretDefinition> {525		let nixos = self.nixos_unchecked_config()?;526		Ok(HostSecretDefinition(527			self.name.clone(),528			nix_go!(nixos.secrets[{ name }]),529		))530	}531532	/// Packages for this host, resolved with nixpkgs overlays533	pub async fn pkgs(&self) -> Result<Value> {534		if let Some(value) = &self.pkgs_override {535			return Ok(value.clone());536		}537		let Some(host_config) = &self.host_config else {538			bail!("local host has no host_config");539		};540		// TODO: Should nixos.options be cached?541		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))542	}543}544545impl Config {546	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {547		let config = &self.config_field;548		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);549		Ok(tagged)550	}551	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {552		let mut out = BTreeSet::new();553		for owner in owners {554			if let Some(tag) = owner.strip_prefix('@') {555				let hosts = self.tagged_hostnames(tag).await?;556				out.extend(hosts);557			} else {558				out.insert(owner);559			}560		}561		Ok(out)562	}563	pub fn local_host(&self) -> ConfigHost {564		ConfigHost {565			config: self.clone(),566			name: "<virtual localhost>".to_owned(),567			host_config: None,568			nixos_config: OnceCell::new(),569			nixos_unchecked_config: OnceCell::new(),570			groups: {571				let cell = OnceCell::new();572				let _ = cell.set(vec![]);573				cell574			},575			pkgs_override: Some(self.default_pkgs.clone()),576577			local: true,578			session: OnceLock::new(),579			deploy_kind: OnceCell::new(),580			session_destination: OnceCell::new(),581			legacy_ssh_store: OnceCell::new(),582		}583	}584585	pub async fn host(&self, name: &str) -> Result<ConfigHost> {586		let config = &self.config_field;587		let host_config = nix_go!(config.hosts[{ name }]);588589		Ok(ConfigHost {590			config: self.clone(),591			name: name.to_owned(),592			host_config: Some(host_config),593			nixos_config: OnceCell::new(),594			nixos_unchecked_config: OnceCell::new(),595			groups: OnceCell::new(),596			pkgs_override: None,597598			// TODO: Remove with connectivit refactor599			local: self.localhost == name,600			session: OnceLock::new(),601			deploy_kind: OnceCell::new(),602			session_destination: OnceCell::new(),603			legacy_ssh_store: OnceCell::new(),604		})605	}606	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {607		let config = &self.config_field;608		let names = nix_go!(config.hosts).list_fields()?;609		let mut out = vec![];610		for name in names {611			out.push(self.host(&name).await?);612		}613		Ok(out)614	}615	// TODO: Replace usages with .host().nixos_config616	pub async fn system_config(&self, host: &str) -> Result<Value> {617		let fleet_field = &self.config_field;618		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))619	}620621	/// Shared secrets configured in fleet.nix or in flake622	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {623		let config_field = &self.config_field;624		nix_go!(config_field.sharedSecrets).list_fields()625	}626	pub fn has_shared(&self, name: &str) -> bool {627		let data = self.data();628		data.secrets.contains(name)629	}630	pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {631		let mut data = self.data_mut();632		data.secrets.set_data(name, shared);633	}634	pub fn remove_shared(&self, secret: &str) {635		let mut data = self.data_mut();636		data.secrets.remove(secret);637	}638639	pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {640		let data = self.data_mut();641		data.secrets.keys_for_owner(host).cloned().collect()642	}643	pub fn list_secrets(&self) -> Vec<String> {644		let data = self.data_mut();645		data.secrets.keys().cloned().collect()646	}647648	pub fn has_secret(&self, host: &str, secret: &str) -> bool {649		let data = self.data();650		data.secrets.contains_for_owner(secret, host)651	}652	pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {653		let mut data = self.data_mut();654		data.secrets.set_single_data(secret, host, value);655	}656	pub fn remove_secret(&self, host: &str, secret: &str) {657		let mut data = self.data_mut();658		data.secrets.drop_owner_no_reencrypt(secret, host);659	}660661	pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {662		let data = self.data();663		data.secrets.get_single(secret, host).cloned()664	}665	pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {666		let data = self.data();667		data.secrets.get(secret).cloned()668	}669	pub fn shared_secret_definition(&self, secret: &str) -> Result<SharedSecretDefinition> {670		let config_field = &self.config_field;671		Ok(SharedSecretDefinition(nix_go!(672			config_field.sharedSecrets[{ secret }]673		)))674	}675676	// TODO: Should this be something modifiable from other processes?677	// E.g terraform provider might want to update FleetData (e.g secrets),678	// and current implementation assumes only one process holds current fleet.nix679	// Given that it is no longer needs to be a file for nix evaluation,680	// maybe it can be a .nix file for persistence, but accessible only681	// thru some shared state controller? Might it be stored in terraform682	// state provider?683	pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {684		self.data.lock().unwrap()685	}686	pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {687		self.data.lock().unwrap()688	}689	pub fn save(&self) -> Result<()> {690		let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;691		let data = nixlike::serialize(&self.data() as &FleetData)?;692		tempfile.write_all(693			format!(694				"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"695			)696			.as_bytes(),697		)?;698		let mut fleet_data_path = self.directory.clone();699		fleet_data_path.push("fleet.nix");700		tempfile.persist(fleet_data_path)?;701		Ok(())702	}703}
after · crates/fleet-base/src/host.rs
1use std::{2	cell::OnceCell,3	collections::BTreeSet,4	ffi::{OsStr, OsString},5	fmt::Display,6	io::Write,7	ops::Deref,8	path::PathBuf,9	str::FromStr,10	sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};16use openssh::{ControlPersist, SessionBuilder};17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24	command::MyCommand,25	fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},26};2728pub struct FleetConfigInternals {29	/// Fleet project directory, containing fleet.nix file.30	pub directory: PathBuf,31	/// builtins.currentSystem32	pub local_system: String,33	pub data: Arc<Mutex<FleetData>>,34	pub nix_args: Vec<OsString>,35	/// fleet_config.config36	pub config_field: Value,37	/// flake.output38	pub flake_outputs: Value,39	// TODO: Remove with connectivity refactor40	pub localhost: String,4142	/// import nixpkgs {system = local};43	pub default_pkgs: Value,44	/// inputs.nixpkgs45	pub nixpkgs: Value,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53	type Target = FleetConfigInternals;5455	fn deref(&self) -> &Self::Target {56		&self.057	}58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62	Sudo,63	Run0,64	Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69	/// NixOS => NixOS managed by fleet70	UpgradeToFleet,71	/// NixOS managed by fleet => NixOS managed by fleet72	Fleet,73	/// Remote host has /mnt, /mnt/boot mounted,74	/// generated config is added to fleet configuration.75	NixosInstall,76	/// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77	/// generated config is added to fleet configuration,78	/// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79	NixosLustrate,80}8182impl FromStr for DeployKind {83	type Err = anyhow::Error;84	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85		match s {86			"upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87			"fleet" => Ok(Self::Fleet),88			"nixos-install" => Ok(Self::NixosInstall),89			"nixos-lustrate" => Ok(Self::NixosLustrate),90			v => bail!(91				"unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""92			),93		}94	}95}96pub struct ConfigHost {97	config: Config,98	pub name: String,99	groups: OnceCell<Vec<String>>,100101	// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it102	deploy_kind: OnceCell<DeployKind>,103	session_destination: OnceCell<String>,104	legacy_ssh_store: OnceCell<bool>,105106	pub host_config: Option<Value>,107	pub nixos_config: OnceCell<Value>,108	pub nixos_unchecked_config: OnceCell<Value>,109	pub pkgs_override: Option<Value>,110111	// TODO: Move command helpers away with connectivity refactor112	pub local: bool,113	pub session: OnceLock<Arc<openssh::Session>>,114}115116#[derive(Debug, Clone, Copy)]117pub enum GenerationStorage {118	Deployer,119	Machine,120	Pusher,121}122impl GenerationStorage {123	fn prefix(&self) -> &'static str {124		match self {125			GenerationStorage::Deployer => "deployer.",126			GenerationStorage::Machine => "",127			GenerationStorage::Pusher => "pusher.",128		}129	}130}131132#[derive(Tabled, Debug)]133pub struct Generation {134	#[tabled(rename = "ID", format("{}", self.rollback_id()))]135	pub id: u32,136	#[tabled(rename = "Current")]137	pub current: bool,138	#[tabled(rename = "Created at")]139	pub datetime: UtcDateTime,140	#[tabled(format = "{:?}")]141	pub store_path: PathBuf,142	#[tabled(skip)]143	pub location: GenerationStorage,144}145impl Generation {146	pub fn rollback_id(&self) -> String {147		format!("{}{}", self.location.prefix(), self.id)148	}149}150151fn parse_generation_line(g: &str) -> Option<Generation> {152	let mut parts = g.split_whitespace();153	let id = parts.next()?;154	let id: u32 = id.parse().ok()?;155	let date = parts.next()?;156	let time = parts.next()?;157	let current = if let Some(current) = parts.next() {158		if current == "(current)" {159			Some(true)160		} else {161			None162		}163	} else {164		Some(false)165	};166	let current = current?;167	if parts.next().is_some() {168		warn!("unexpected text after generation: {g}");169	}170171	let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")172		.expect("valid format");173	let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;174175	Some(Generation {176		id,177		current,178		datetime,179		store_path: PathBuf::new(),180		location: GenerationStorage::Machine,181	})182}183// TODO: Move command helpers away with connectivity refactor184impl ConfigHost {185	pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {186		let mut cmd = self.cmd("nix-env").await?;187		cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))188			.arg("--list-generations")189			.env("TZ", "UTC");190		// Sudo is required because --list-generations tries to acquire profile lock191		let data = cmd.sudo().run_string().await?;192		let mut generations = data193			.split('\n')194			.map(|e| e.trim())195			.filter(|&l| !l.is_empty())196			.filter_map(|g| {197				let generation = parse_generation_line(g);198				if generation.is_none() {199					warn!("bad generation: {g}");200				};201				generation202			})203			.collect::<Vec<_>>();204		for ele in generations.iter_mut() {205			let mut cmd = self.cmd("readlink").await?;206			cmd.arg("--")207				.arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));208			let path = cmd.run_string().await?;209			ele.store_path = PathBuf::from(path.trim_end_matches("\n"));210		}211212		Ok(generations)213	}214215	pub fn set_session_destination(&self, dest: String) {216		self.session_destination217			.set(dest)218			.expect("session destination is already set")219	}220	pub fn set_deploy_kind(&self, kind: DeployKind) {221		self.deploy_kind222			.set(kind)223			.expect("deploy kind is already set");224	}225	pub fn set_legacy_ssh_store(&self, legacy: bool) {226		self.legacy_ssh_store227			.set(legacy)228			.expect("legacy ssh store is already set")229	}230	pub async fn deploy_kind(&self) -> Result<DeployKind> {231		if let Some(kind) = self.deploy_kind.get() {232			return Ok(*kind);233		}234		let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {235			Ok(v) => v,236			Err(e) => {237				bail!("failed to query remote system kind: {e}");238			}239		};240		if !is_fleet_managed {241			bail!(242				"{}",243				indoc::indoc! {"244				host is not marked as managed by fleet245				if you're not trying to lustrate/install system from scratch,246				you should either247					1. manually create /etc/FLEET_HOST file on the target host,248					2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet249					3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos250			"}251			);252		}253		// TOCTOU is possible254		let _ = self.deploy_kind.set(DeployKind::Fleet);255		Ok(*self.deploy_kind.get().expect("deploy kind is just set"))256	}257	pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {258		// Prefer sudo, as run0 has some gotchas with polkit259		// and too many repeating prompts.260		if (self.find_in_path("sudo").await).is_ok() {261			return Ok(EscalationStrategy::Sudo);262		}263		if (self.find_in_path("run0").await).is_ok() {264			return Ok(EscalationStrategy::Run0);265		}266		Ok(EscalationStrategy::Su)267	}268	async fn open_session(&self) -> Result<Arc<openssh::Session>> {269		assert!(!self.local, "do not open ssh connection to local session");270		// FIXME: TOCTOU271		if let Some(session) = &self.session.get() {272			return Ok((*session).clone());273		};274		let mut session = SessionBuilder::default();275		session.control_persist(ControlPersist::ClosedAfterInitialConnection);276277		let dest = self.session_destination.get().unwrap_or(&self.name);278		let session = session279			.connect(&dest)280			.await281			.map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;282		let session = Arc::new(session);283		self.session.set(session.clone()).expect("TOCTOU happened");284		Ok(session)285	}286	pub async fn mktemp_dir(&self) -> Result<String> {287		let mut cmd = self.cmd("mktemp").await?;288		cmd.arg("-d");289		let path = cmd.run_string().await?;290		Ok(path.trim_end().to_owned())291	}292	pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {293		let mut cmd = self.cmd("sh").await?;294		cmd.arg("-c")295			.arg("test -e \"$1\" && echo true || echo false")296			.arg("_")297			.arg(path);298		cmd.run_value().await299	}300	pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {301		let mut cmd = self.cmd("cat").await?;302		cmd.arg(path);303		cmd.run_bytes().await304	}305	pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {306		let mut cmd = self.cmd("cat").await?;307		cmd.arg(path);308		cmd.run_string().await309	}310	pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {311		let mut cmd = self.cmd("ls").await?;312		cmd.arg(path);313		let out = cmd.run_string().await?;314		let mut lines = out.split('\n');315		if let Some(last) = lines.next_back() {316			ensure!(last.is_empty(), "output of ls should end with newline");317		}318		Ok(lines.map(ToOwned::to_owned).collect())319	}320	#[allow(dead_code)]321	pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {322		let text = self.read_file_text(path).await?;323		Ok(serde_json::from_str(&text)?)324	}325	pub async fn read_env(&self, env: &str) -> Result<String> {326		let mut cmd = self.cmd("printenv").await?;327		cmd.arg(env);328		cmd.run_string().await329	}330	pub async fn find_in_path(&self, command: &str) -> Result<String> {331		// // `which` is not a part of coreutils, and it might not exist on machine.332		// let path = self.read_env("PATH").await?;333		// // Assuming delimiter is :, we don't work with windows host, this check will be much334		// // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)335		// for ele in path.split(':') {336		// 	let test_path = format!("{ele}/{cmd}");337		// 	test -x etc338		// }339		// let mut cmd = self.cmd("printenv").await?;340		// cmd.arg(env);341		// Ok(cmd.run_string().await?)342		// Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.343		let mut cmd = self344			.cmd_escalation(345				// Not used346				EscalationStrategy::Su,347				"which",348			)349			.await?;350		cmd.arg(command);351		cmd.run_string().await352	}353	pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>354	where355		<D as FromStr>::Err: Display,356	{357		let text = self.read_file_text(path).await?;358		D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))359	}360	pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {361		self.cmd_escalation(self.escalation_strategy().await?, cmd)362			.await363	}364	pub async fn cmd_escalation(365		&self,366		escalation: EscalationStrategy,367		cmd: impl AsRef<OsStr>,368	) -> Result<MyCommand> {369		if self.local {370			Ok(MyCommand::new(escalation, cmd))371		} else {372			let session = self.open_session().await?;373			Ok(MyCommand::new_on(escalation, cmd, session))374		}375	}376	pub async fn nix_cmd(&self) -> Result<MyCommand> {377		let mut nix = self.cmd("nix").await?;378		nix.args([379			"--extra-experimental-features",380			"nix-command",381			"--extra-experimental-features",382			"flakes",383		]);384		Ok(nix)385	}386387	pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {388		ensure!(data.encrypted, "secret is not encrypted");389		let mut cmd = self.cmd("fleet-install-secrets").await?;390		cmd.arg("decrypt").eqarg("--secret", data.to_string());391		let encoded = cmd392			.sudo()393			.run_string()394			.await395			.context("failed to call remote host for decrypt")?;396		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;397		ensure!(!data.encrypted, "secret came out encrypted");398		Ok(data.data)399	}400	pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {401		ensure!(data.encrypted, "secret is not encrypted");402		let mut cmd = self.cmd("fleet-install-secrets").await?;403		cmd.arg("reencrypt").eqarg("--secret", data.to_string());404		for target in targets {405			let key = self.config.key(&target).await?;406			cmd.eqarg("--targets", key);407		}408		let encoded = cmd409			.sudo()410			.run_string()411			.await412			.context("failed to call remote host for decrypt")?;413		let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;414		ensure!(data.encrypted, "secret came out not encrypted");415		Ok(data)416	}417	/// Returns path for futureproofing, as path might change i.e on conversion to CA418	pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {419		if self.local {420			// Path is located locally, thus already trusted.421			return Ok(path.to_owned());422		}423		let mut nix = MyCommand::new(424			// Not used425			EscalationStrategy::Su,426			"nix",427		);428		nix.arg("copy").arg("--substitute-on-destination");429430		let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {431			"ssh"432		} else {433			"ssh-ng"434		};435436		match self.deploy_kind().await? {437			DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {438				nix.comparg("--to", format!("{proto}://{}", self.name));439			}440			DeployKind::NixosInstall => {441				nix442					// Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon443					.arg("--no-check-sigs")444					.comparg(445						"--to",446						format!("{proto}://root@{}?remote-store=/mnt", self.name),447					);448			}449		}450		nix.arg(path);451		nix.run_nix().await.context("nix copy")?;452		Ok(path.to_owned())453	}454	pub async fn systemctl_stop(&self, name: &str) -> Result<()> {455		let mut cmd = self.cmd("systemctl").await?;456		cmd.arg("stop").arg(name);457		cmd.sudo().run().await458	}459	pub async fn systemctl_start(&self, name: &str) -> Result<()> {460		let mut cmd = self.cmd("systemctl").await?;461		cmd.arg("start").arg(name);462		cmd.sudo().run().await463	}464465	pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {466		let mut cmd = self.cmd("rm").await?;467		cmd.arg("-f").arg(path);468		if sudo {469			cmd = cmd.sudo()470		}471		cmd.run().await472	}473}474impl ConfigHost {475	// TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,476	// assuming getting tags always returns the same value.477	pub async fn tags(&self) -> Result<Vec<String>> {478		if let Some(v) = self.groups.get() {479			return Ok(v.clone());480		}481		let Some(host_config) = &self.host_config else {482			return Ok(vec![]);483		};484		let tags: Vec<String> = nix_go_json!(host_config.tags);485486		let _ = self.groups.set(tags.clone());487488		Ok(tags)489	}490	pub async fn nixos_config(&self) -> Result<Value> {491		if let Some(v) = self.nixos_config.get() {492			return Ok(v.clone());493		}494		let Some(host_config) = &self.host_config else {495			bail!("local host has no nixos_config");496		};497		let nixos_config = nix_go!(host_config.nixos.config);498		assert_warn("nixos config evaluation", &nixos_config).await?;499500		let _ = self.nixos_config.set(nixos_config.clone());501502		Ok(nixos_config)503	}504	pub fn nixos_unchecked_config(&self) -> Result<Value> {505		if let Some(v) = self.nixos_unchecked_config.get() {506			return Ok(v.clone());507		}508		let Some(host_config) = &self.host_config else {509			bail!("local host has no nixos_config");510		};511		let nixos_config = nix_go!(host_config.nixos_unchecked.config);512513		let _ = self.nixos_unchecked_config.set(nixos_config.clone());514515		Ok(nixos_config)516	}517518	pub fn list_defined_secrets(&self) -> Result<Vec<String>> {519		let nixos = self.nixos_unchecked_config()?;520		let secrets = nix_go!(nixos.secrets);521		secrets.list_fields()522	}523524	/// Packages for this host, resolved with nixpkgs overlays525	pub async fn pkgs(&self) -> Result<Value> {526		if let Some(value) = &self.pkgs_override {527			return Ok(value.clone());528		}529		let Some(host_config) = &self.host_config else {530			bail!("local host has no host_config");531		};532		// TODO: Should nixos.options be cached?533		Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))534	}535}536537impl Config {538	pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {539		let config = &self.config_field;540		let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);541		Ok(tagged)542	}543	pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {544		let mut out = BTreeSet::new();545		for owner in owners {546			if let Some(tag) = owner.strip_prefix('@') {547				let hosts = self.tagged_hostnames(tag).await?;548				out.extend(hosts);549			} else {550				out.insert(owner);551			}552		}553		Ok(out)554	}555	pub fn local_host(&self) -> ConfigHost {556		ConfigHost {557			config: self.clone(),558			name: "<virtual localhost>".to_owned(),559			host_config: None,560			nixos_config: OnceCell::new(),561			nixos_unchecked_config: OnceCell::new(),562			groups: {563				let cell = OnceCell::new();564				let _ = cell.set(vec![]);565				cell566			},567			pkgs_override: Some(self.default_pkgs.clone()),568569			local: true,570			session: OnceLock::new(),571			deploy_kind: OnceCell::new(),572			session_destination: OnceCell::new(),573			legacy_ssh_store: OnceCell::new(),574		}575	}576577	pub async fn host(&self, name: &str) -> Result<ConfigHost> {578		let config = &self.config_field;579		let host_config = nix_go!(config.hosts[{ name }]);580581		Ok(ConfigHost {582			config: self.clone(),583			name: name.to_owned(),584			host_config: Some(host_config),585			nixos_config: OnceCell::new(),586			nixos_unchecked_config: OnceCell::new(),587			groups: OnceCell::new(),588			pkgs_override: None,589590			// TODO: Remove with connectivit refactor591			local: self.localhost == name,592			session: OnceLock::new(),593			deploy_kind: OnceCell::new(),594			session_destination: OnceCell::new(),595			legacy_ssh_store: OnceCell::new(),596		})597	}598	pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {599		let config = &self.config_field;600		let names = nix_go!(config.hosts).list_fields()?;601		let mut out = vec![];602		for name in names {603			out.push(self.host(&name).await?);604		}605		Ok(out)606	}607	// TODO: Replace usages with .host().nixos_config608	pub async fn system_config(&self, host: &str) -> Result<Value> {609		let fleet_field = &self.config_field;610		Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))611	}612613	/// Shared secrets configured in fleet.nix or in flake614	pub async fn list_configured_shared(&self) -> Result<Vec<String>> {615		let config_field = &self.config_field;616		nix_go!(config_field.sharedSecrets).list_fields()617	}618	pub fn has_shared(&self, name: &str) -> bool {619		let data = self.data();620		data.secrets.contains(name)621	}622	pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {623		let mut data = self.data_mut();624		data.secrets.set_data(name, shared);625	}626	pub fn remove_shared(&self, secret: &str) {627		let mut data = self.data_mut();628		data.secrets.remove(secret);629	}630631	pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {632		let data = self.data_mut();633		data.secrets.keys_for_owner(host).cloned().collect()634	}635	pub fn list_secrets(&self) -> Vec<String> {636		let data = self.data_mut();637		data.secrets.keys().cloned().collect()638	}639640	pub fn has_secret(&self, host: &str, secret: &str) -> bool {641		let data = self.data();642		data.secrets.contains_for_owner(secret, host)643	}644	pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {645		let mut data = self.data_mut();646		data.secrets.set_single_data(secret, host, value);647	}648	pub fn remove_secret(&self, host: &str, secret: &str) {649		let mut data = self.data_mut();650		data.secrets.drop_owner_no_reencrypt(secret, host);651	}652653	pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {654		let data = self.data();655		data.secrets.get_single(secret, host).cloned()656	}657	pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {658		let data = self.data();659		data.secrets.get(secret).cloned()660	}661662	// TODO: Should this be something modifiable from other processes?663	// E.g terraform provider might want to update FleetData (e.g secrets),664	// and current implementation assumes only one process holds current fleet.nix665	// Given that it is no longer needs to be a file for nix evaluation,666	// maybe it can be a .nix file for persistence, but accessible only667	// thru some shared state controller? Might it be stored in terraform668	// state provider?669	pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {670		self.data.lock().unwrap()671	}672	pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {673		self.data.lock().unwrap()674	}675	pub fn save(&self) -> Result<()> {676		let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;677		let data = nixlike::serialize(&self.data() as &FleetData)?;678		tempfile.write_all(679			format!(680				"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"681			)682			.as_bytes(),683		)?;684		let mut fleet_data_path = self.directory.clone();685		fleet_data_path.push("fleet.nix");686		tempfile.persist(fleet_data_path)?;687		Ok(())688	}689}
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -211,7 +211,7 @@
 		}
 		let bytes =
 			std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
-		let data = Mutex::new(FleetData::from_str(&bytes)?);
+		let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));
 
 		let mut fetch_settings = FetchSettings::new();
 		fetch_settings.set(c"warn-dirty", c"false");
@@ -239,8 +239,7 @@
 		let builtins_field = Value::eval("builtins")?;
 
 		let fleet_root = flake.get_field("fleetConfigurations")?;
-		let data_val = Value::serialized(&data)?;
-		let fleet_field = nix_go!(fleet_root.default(data_val));
+		let fleet_field = nix_go!(fleet_root.default(Obj {}));
 
 		let config_field = nix_go!(fleet_field.config);
 
modifiedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -1,4 +1,9 @@
-use nix_eval::NativeFn;
+use std::collections::HashMap;
+use std::sync::{Arc, Mutex};
+
+use nix_eval::{NativeFn, Value};
+
+use crate::fleetdata::{FleetData, FleetSecrets};
 
 #[derive(thiserror::Error, Debug)]
 enum Error {}
@@ -15,30 +20,32 @@
 	fn host_parts(&self, host: &str, name: &str) -> Parts;
 }
 
-struct FsSecretsBackend {
+struct FsSecretsBackend {}
 
-}
-
-pub fn init_primops() {
+pub fn init_primops(secrets: Arc<Mutex<FleetData>>) {
 	NativeFn::new(
-		c"fleet_ensure_secret",
+		c"fleet_ensure_host_secret",
 		c"Ensure secret existence for a host, regenerating it in case of some mismatch",
-		[
-			c"host",
-			c"secret",
-			c"expected_parts",
-			c"expected_encrypted_parts",
-			c"generator",
-		],
-		|[
-			host,
-			secret,
-			expected_parts,
-			expected_encrypted_parts,
-			generator,
-		]| { 
-
-			todo!()
+		[c"host", c"secret", c"generator"],
+		|[host, secret, generator]| {
+			todo!("ensure secret");
+			Ok(Value::new_attrs(HashMap::from_iter([(
+				"raw",
+				Value::new_str("rawData"),
+			)])))
+		},
+	)
+	.register();
+	NativeFn::new(
+		c"fleet_ensure_host_secret",
+		c"Ensure secret existence for a host, regenerating it in case of some mismatch",
+		[c"host", c"secret", c"generator"],
+		|[host, secret, generator]| {
+			todo!("ensure secret");
+			Ok(Value::new_attrs(HashMap::from_iter([(
+				"raw",
+				Value::new_str("rawData"),
+			)])))
 		},
 	)
 	.register();
modifiedcrates/fleet-base/src/secret.rsdiffbeforeafterboth
--- a/crates/fleet-base/src/secret.rs
+++ b/crates/fleet-base/src/secret.rs
@@ -1,8 +1,6 @@
 use std::collections::BTreeSet;
 
-use anyhow::Result;
 use chrono::{DateTime, Utc};
-use nix_eval::{Value, nix_go, nix_go_json};
 
 use crate::fleetdata::FleetSecretData;
 
@@ -12,63 +10,6 @@
 	pub generation_data: serde_json::Value,
 	pub public_parts: BTreeSet<String>,
 	pub private_parts: BTreeSet<String>,
-}
-
-pub struct HostSecretDefinition(pub(crate) String, pub(crate) Value);
-impl HostSecretDefinition {
-	pub fn is_managed(&self) -> Result<bool> {
-		let def = self.definition_value()?;
-		Ok(!nix_go!(def.generator).is_null())
-	}
-	pub fn is_shared(&self) -> Result<bool> {
-		let def = self.definition_value()?;
-		Ok(nix_go_json!(def.shared))
-	}
-	pub fn expectations(&self) -> Result<Expectations> {
-		let def = self.definition_value()?;
-		let parts = nix_go!(def.parts);
-
-		let mut public_parts = BTreeSet::new();
-		let mut private_parts = BTreeSet::new();
-		for part in parts.list_fields()? {
-			if nix_go_json!(parts[&part].encrypted) {
-				private_parts.insert(part.clone());
-			} else {
-				public_parts.insert(part.clone());
-			}
-		}
-
-		Ok(Expectations {
-			owners: BTreeSet::from([self.0.clone()]),
-			generation_data: nix_go_json!(def.expectedGenerationData),
-			public_parts,
-			private_parts,
-		})
-	}
-	pub fn definition_value(&self) -> Result<Value> {
-		let value = &self.1;
-		Ok(nix_go!(value.definition))
-	}
-}
-
-pub struct SharedSecretDefinition(pub(crate) Value);
-impl SharedSecretDefinition {
-	pub fn is_managed(&self) -> Result<bool> {
-		let value = &self.0;
-		Ok(!nix_go!(value.generator).is_null())
-	}
-	pub fn expectations(&self) -> Result<Expectations> {
-		let value = &self.0;
-		Ok(Expectations {
-			owners: nix_go_json!(value.expectedOwners),
-			generation_data: nix_go_json!(value.expectedGenerationData),
-			public_parts: nix_go_json!(value.expectedPublicParts),
-			private_parts: nix_go_json!(value.expectedPrivateParts),
-		})
-	}
-	pub fn definition_value(&self) -> Value {
-		self.0.clone()
-	}
 }
 
 #[derive(thiserror::Error, Debug)]
modifiedmodules/module-list.nixdiffbeforeafterboth
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -6,5 +6,4 @@
   ./nixos.nix
   ./nixpkgs.nix
   ./secrets.nix
-  ./secrets-data.nix
 ]
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -8,50 +8,26 @@
 let
   inherit (builtins)
     hashString
-    elemAt
-    length
     toJSON
-    filter
     ;
   inherit (lib.stringsWithDeps) stringAfter;
   inherit (lib.options) mkOption literalExpression;
   inherit (lib.lists) optional;
-  inherit (lib.attrsets) mapAttrs mapAttrsToList;
-  inherit (lib.modules) mkIf mkMerge;
+  inherit (lib.attrsets) mapAttrs;
+  inherit (lib.modules) mkIf;
   inherit (lib.types)
     submodule
     str
     attrsOf
     nullOr
     unspecified
-    lazyAttrsOf
     uniq
     functionTo
     package
-    listOf
-    bool
     ;
   inherit (fleetLib.strings) decodeRawSecret;
 
   sysConfig = config;
-  secretPartDataType = submodule {
-    options = {
-      raw = mkOption {
-        type = str;
-        internal = true;
-        description = "Encoded & Encrypted secret part data, passed from fleet.nix";
-      };
-    };
-  };
-  secretDataType = submodule {
-    freeformType = lazyAttrsOf secretPartDataType;
-    options = {
-      shared = mkOption {
-        description = "Is this secret owned by this machine, or propagated from shared secrets";
-        default = false;
-      };
-    };
-  };
   secretPartType =
     secretName:
     submodule (
@@ -61,11 +37,6 @@
       in
       {
         options = {
-          encrypted = mkOption {
-            type = bool;
-            description = "Is this secret part supposed to be encrypted?";
-          };
-
           hash = mkOption {
             type = str;
             description = "Hash of secret in encoded format";
@@ -82,17 +53,17 @@
             type = str;
             description = "Secret public data (only available for plaintext)";
           };
+          raw = mkOption {
+            type = str;
+            description = "Raw (encoded/encrypted secret part data)";
+          };
+        };
+        config = {
+          hash = hashString "sha1" config.raw;
+          data = decodeRawSecret config.raw;
+          path = "/run/secrets/${secretName}/${config.hash}-${partName}";
+          stablePath = "/run/secrets/${secretName}/${partName}";
         };
-        config =
-          let
-            raw = sysConfig.data.secrets.${secretName}.${partName}.raw;
-          in
-          {
-            hash = hashString "sha1" raw;
-            data = decodeRawSecret raw;
-            path = "/run/secrets/${secretName}/${config.hash}-${partName}";
-            stablePath = "/run/secrets/${secretName}/${partName}";
-          };
       }
     );
   secretType = submodule (
@@ -105,14 +76,9 @@
     in
     {
       options = {
-        shared = mkOption {
-          type = bool;
-          description = "Was this secret propagated from a shared secret?";
-        };
         parts = mkOption {
-          type = lazyAttrsOf (secretPartType secretName);
+          type = attrsOf (secretPartType secretName);
           description = "Definition of secret parts";
-          default = { };
         };
         generator = mkOption {
           type = uniq (nullOr (functionTo package));
@@ -134,47 +100,14 @@
           description = "Group of the secret";
           default = sysConfig.users.users.${config.owner}.group;
           defaultText = literalExpression "config.users.users.$${owner}.group";
-        };
-        expectedGenerationData = mkOption {
-          type = unspecified;
-          description = "Data that gets embedded into secret part";
-          default = null;
         };
       };
       config = {
-        shared = (sysConfig.data.secrets.${secretName} or { shared = false; }).shared;
-        parts = mkMerge [
-          (mkIf (config.generator != null)
-            (
-              # Get fake derivation body, in future it should be implemented the same way as in Rust.
-              lib.callPackageWith (
-                pkgs
-                // {
-                  mkSecretGenerator = pkgs.stdenv.mkDerivation;
-                  mkImpureSecretGenerator = pkgs.stdenv.mkDerivation;
-                }
-              ) config.generator { }
-            ).parts
-          )
-          (mapAttrs (_: _: { }) (
-            removeAttrs (sysConfig.data.secrets.${secretName} or { }) [
-              "shared"
-              "managed"
-            ]
-          ))
-        ];
+        parts = builtins.fleet_ensure_host_secret sysConfig.networking.hostName secretName config.generator;
       };
     }
   );
-  processPart = secretName: partName: part: {
-    inherit (part) path stablePath;
-    raw = config.data.secrets.${secretName}.${partName}.raw;
-  };
-  processSecret = secretName: secret: {
-    inherit (secret.definition) group mode owner;
-    parts = (mapAttrs (processPart secretName) (secret.definition.parts));
-  };
-  secretsData = (mapAttrs (processSecret) config.secrets);
+  secretsData = (mapAttrs (_: s: s.definition) config.secrets);
   secretsFile = pkgs.writeTextFile {
     name = "secrets.json";
     text = toJSON secretsData;
@@ -185,11 +118,6 @@
 in
 {
   options = {
-    data.secrets = mkOption {
-      type = attrsOf secretDataType;
-      default = { };
-      description = "Host-local secret data";
-    };
     secrets = mkOption {
       type = attrsOf secretType;
       default = { };
deletedmodules/secrets-data.nixdiffbeforeafterboth
--- a/modules/secrets-data.nix
+++ /dev/null
@@ -1,95 +0,0 @@
-{
-  lib,
-  fleetLib,
-  ...
-}:
-let
-  inherit (fleetLib.options) mkDataOption;
-  inherit (lib.options) mkOption;
-  inherit (lib.types)
-    nullOr
-    listOf
-    str
-    attrsOf
-    submodule
-    bool
-    unspecified
-    ;
-
-  secretDataValue = {
-    options = {
-      raw = mkOption {
-        type = nullOr str;
-        description = "Raw secret data in unspecified encoded and optionally encrypted format.";
-        default = null;
-      };
-    };
-  };
-
-  sharedSecretData = {
-    freeformType = attrsOf (submodule secretDataValue);
-    options = {
-      managed = mkOption {
-        type = nullOr bool;
-        description = "Is current fleet data value is generated by generator";
-        default = null;
-      };
-
-      createdAt = mkOption {
-        type = str;
-        description = "Timestamp of secret generation/last rotation.";
-        default = null;
-      };
-      expiresAt = mkOption {
-        type = nullOr str;
-        description = "Expiration timestamp triggering mandatory secret rotation.";
-        default = null;
-      };
-
-      owners = mkOption {
-        type = listOf str;
-        description = ''
-          List of hosts currently authorized to decrypt this shared secret.
-
-          If owners differ from expected owners, the secret is considered outdated
-          and requires regeneration or re-encryption.
-        '';
-        default = [ ];
-      };
-      generationData = mkOption {
-        type = unspecified;
-        description = "Contextual metadata associated with secret part.";
-        default = null;
-      };
-    };
-  };
-
-  managerKey = {
-    options = {
-      name = mkOption {
-        type = str;
-        description = "Who does this manager key belongs to.";
-      };
-      key = mkOption {
-        type = str;
-        description = "Age-compatible key";
-      };
-    };
-    config = { };
-  };
-in
-{
-  options.data = mkDataOption ({ config, ... }:
-    {
-      options = {
-        managerKeys = mkOption {
-          type = listOf (submodule managerKey);
-        };
-        secrets = mkOption {
-          type = attrsOf (listOf submodule sharedSecretData);
-          default = { };
-          description = "Shared secret data.";
-        };
-      };
-    });
-}
modifiedmodules/secrets.nixdiffbeforeafterboth
--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -5,7 +5,6 @@
 let
   inherit (lib.options) mkOption literalExpression;
   inherit (lib.types)
-    unspecified
     nullOr
     listOf
     str
@@ -66,22 +65,7 @@
             An input to this function - `pkgs` of a generator host with implementation-defined representation of extra encryption data,
             use `mkSecretGenerator` helpers to implement own generators.
           '';
-          default = null;
-        };
-        expectedGenerationData = mkOption {
-          type = unspecified;
-          description = "Contextual metadata embedded within the secret part value";
           default = null;
-        };
-        expectedPrivateParts = mkOption {
-          type = listOf str;
-          default = [ ];
-          description = "List of parts that are expected to be encrypted";
-        };
-        expectedPublicParts = mkOption {
-          type = listOf str;
-          default = [ ];
-          description = "List of parts that are expected to be public";
         };
       };
     };