git.delta.rocks / jrsonnet / refs/commits / 33f3601e24db

difftreelog

fix shared generator condition

ywyowvrkYaroslav Bolyukin2026-01-22parent: #45c49ea.patch.diff
in: trunk

10 files changed

modifiedcmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -114,7 +114,8 @@
 
 			set.spawn_local(
 				(async move {
-					let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet").await
+					let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet")
+						.await
 					{
 						Ok(path) => path,
 						Err(e) => {
modifiedcmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -632,23 +632,23 @@
 					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
-						.owners()
-						.map(|o| {
-							if expectations.owners.contains(o) {
-								o.green().to_string()
-							} else {
-								o.red().to_string()
-							}
-						})
-						.collect::<Vec<_>>();
-					table.push(SecretDisplay {
-						owners: owners.join(", "),
-						name,
-					})
-*/
+										let definition = config.shared_secret_definition(&name)?;
+										let expectations = definition.expectations()?;
+										let owners = data
+											.owners()
+											.map(|o| {
+												if expectations.owners.contains(o) {
+													o.green().to_string()
+												} else {
+													o.red().to_string()
+												}
+											})
+											.collect::<Vec<_>>();
+										table.push(SecretDisplay {
+											owners: owners.join(", "),
+											name,
+										})
+					*/
 				}
 				// info!("loaded\n{}", Table::new(table).to_string())
 			}
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
before · crates/fleet-base/src/fleetdata.rs
1use std::{2	collections::{3		BTreeMap, BTreeSet,4		btree_map::{self, Entry},5	},6	io::{self, Cursor},7	ops::Deref,8};910use age::Recipient;11use chrono::{DateTime, Utc};12use fleet_shared::SecretData;13use rand::{14	distr::{Alphanumeric, SampleString as _},15	rng,16};17use serde::{18	Deserialize, Serialize,19	de::{self, Error},20};21use serde_json::Value;22use tracing::info;2324#[derive(Serialize, Deserialize, Default)]25#[serde(rename_all = "camelCase")]26pub struct HostData {27	#[serde(default)]28	#[serde(skip_serializing_if = "String::is_empty")]29	pub encryption_key: String,30}3132const VERSION: &str = "0.1.0";33pub struct FleetDataVersion;34impl Serialize for FleetDataVersion {35	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>36	where37		S: serde::Serializer,38	{39		VERSION.serialize(serializer)40	}41}42impl<'de> Deserialize<'de> for FleetDataVersion {43	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>44	where45		D: serde::Deserializer<'de>,46	{47		let version = String::deserialize(deserializer)?;48		if version != VERSION {49			return Err(D::Error::custom(format!(50				"fleet.nix data version mismatch, expected {VERSION}, got {version}.\nFollow the docs for migration instruction"51			)));52		}53		Ok(Self)54	}55}5657fn generate_gc_prefix() -> String {58	let id = Alphanumeric.sample_string(&mut rng(), 8);59	format!("fleet-gc-{id}")60}6162#[derive(Serialize, Deserialize)]63#[serde(rename_all = "camelCase")]64pub struct ManagerKey {65	pub name: String,66	pub key: String,67}6869#[derive(Serialize, Deserialize)]70#[serde(rename_all = "camelCase")]71pub struct FleetData {72	pub version: FleetDataVersion,73	#[serde(default = "generate_gc_prefix")]74	pub gc_root_prefix: String,7576	#[serde(default, skip_serializing_if = "Vec::is_empty")]77	pub manager_keys: Vec<ManagerKey>,7879	#[serde(default)]80	pub hosts: BTreeMap<String, HostData>,8182	#[serde(default, alias = "shared_secrets")]83	pub secrets: FleetSecrets,8485	// extra_name => anything86	#[serde(default)]87	#[serde(skip_serializing_if = "BTreeMap::is_empty")]88	pub extra: BTreeMap<String, Value>,8990	#[serde(default)]91	#[serde(skip_serializing_if = "BTreeMap::is_empty")]92	host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,93}94impl FleetData {95	pub fn from_str(s: &str) -> anyhow::Result<Self> {96		let mut data: Self = nixlike::parse_str(s)?;97		if !data.host_secrets.is_empty() {98			info!("migrating host secrets into shared secrets structure");99			data.secrets100				.merge_from_hosts(std::mem::take(&mut data.host_secrets));101		}102		Ok(data)103	}104}105106/// Returns None if recipients.is_empty()107pub fn encrypt_secret_data<'r>(108	recipients: impl IntoIterator<Item = &'r Box<dyn Recipient>>,109	data: Vec<u8>,110) -> Option<SecretData> {111	let mut encrypted = vec![];112	let mut encryptor = age::Encryptor::with_recipients(recipients.into_iter().map(|v| &**v))113		.ok()?114		.wrap_output(&mut encrypted)115		.expect("in memory write");116	io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");117	encryptor.finish().expect("in memory flush");118	Some(SecretData {119		data: encrypted,120		encrypted: true,121	})122}123124#[derive(Serialize, Deserialize, Clone, Debug)]125pub struct FleetSecretPart {126	pub raw: SecretData,127}128129#[derive(Serialize, Deserialize, Clone, Debug)]130#[serde(rename_all = "camelCase")]131#[must_use]132pub struct FleetSecretData {133	#[serde(default = "Utc::now")]134	pub created_at: DateTime<Utc>,135	#[serde(default)]136	#[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]137	pub expires_at: Option<DateTime<Utc>>,138139	#[serde(flatten)]140	pub parts: BTreeMap<String, FleetSecretPart>,141142	#[serde(default)]143	#[serde(skip_serializing_if = "Value::is_null")]144	pub generation_data: Value,145}146147#[derive(Serialize, Deserialize, Clone, Debug)]148#[serde(rename_all = "camelCase")]149#[must_use]150pub struct FleetSecretDistribution {151	#[serde(default)]152	pub owners: BTreeSet<String>,153	#[serde(flatten)]154	pub secret: FleetSecretData,155156	#[serde(default, skip_serializing, alias="managed")]157	pub _deprecated_managed: bool,158}159160#[derive(Clone)]161#[must_use]162pub struct FleetSecretDistributions(Vec<FleetSecretDistribution>);163164impl Deref for FleetSecretDistributions {165	type Target = [FleetSecretDistribution];166167	fn deref(&self) -> &Self::Target {168		self.0.as_slice()169	}170}171172impl FleetSecretDistributions {173	pub fn owners(&self) -> impl Iterator<Item = &String> {174		self.0.iter().flat_map(|v| v.owners.iter())175	}176	#[allow(177		clippy::len_without_is_empty,178		reason = "should not be empty for a long time"179	)]180	pub fn len(&self) -> usize {181		self.0.len()182	}183184	pub fn get(&self, owner: &str) -> Option<&FleetSecretDistribution> {185		self.0.iter().find(|d| d.owners.contains(owner))186	}187	fn entry(&mut self, owner: String) -> DistEntry<'_> {188		let Some(idx) = self.0.iter().position(|d| d.owners.contains(&owner)) else {189			return DistEntry::Vacant(VacantDistEntry {190				distributions: self,191				owner,192			});193		};194		DistEntry::Occupied(OccupiedDistEntry {195			distributions: self,196			idx,197			owner,198		})199	}200	fn extend(&mut self, dist: FleetSecretDistribution) {201		for owner in &dist.owners {202			self.entry(owner.to_owned()).remove();203		}204		self.0.push(dist);205	}206	pub fn contains(&self, owner: &str) -> bool {207		self.0.iter().any(|d| d.owners.contains(owner))208	}209}210211struct OccupiedDistEntry<'d> {212	distributions: &'d mut FleetSecretDistributions,213	idx: usize,214	owner: String,215}216impl<'d> OccupiedDistEntry<'d> {217	fn remove(self) -> VacantDistEntry<'d> {218		let dist = &mut self.distributions.0[self.idx];219		assert!(220			dist.owners.remove(&self.owner),221			"entry exists, as we have its reference"222		);223		if dist.owners.is_empty() {224			self.distributions.0.remove(self.idx);225		}226		VacantDistEntry {227			distributions: self.distributions,228			owner: self.owner,229		}230	}231	fn set(self, secret: FleetSecretData) -> Self {232		self.remove().set(secret)233	}234}235struct VacantDistEntry<'d> {236	distributions: &'d mut FleetSecretDistributions,237	owner: String,238}239impl<'d> VacantDistEntry<'d> {240	fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {241		let Self {242			distributions,243			owner,244		} = self;245		let idx = distributions.0.len();246		distributions.0.push(FleetSecretDistribution {247			owners: BTreeSet::from_iter([owner.clone()]),248			secret,249250			_deprecated_managed: true,251		});252		OccupiedDistEntry {253			distributions,254			owner,255			idx,256		}257	}258}259260enum DistEntry<'d> {261	Vacant(VacantDistEntry<'d>),262	Occupied(OccupiedDistEntry<'d>),263}264impl DistEntry<'_> {265	fn remove(self) -> Self {266		match self {267			DistEntry::Vacant(_) => self,268			DistEntry::Occupied(o) => Self::Vacant(o.remove()),269		}270	}271	fn set(self, secret: FleetSecretData) -> Self {272		Self::Occupied(match self {273			DistEntry::Vacant(e) => e.set(secret),274			DistEntry::Occupied(e) => e.set(secret),275		})276	}277}278279impl Serialize for FleetSecretDistributions {280	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>281	where282		S: serde::Serializer,283	{284		let mut found_hosts = BTreeSet::new();285		for ele in self.0.iter() {286			if ele.owners.is_empty() {287				panic!("consistency: secret distribution has no defined owners");288			}289			for ele in ele.owners.iter() {290				if !found_hosts.insert(ele) {291					panic!(292						"consistency: secret distribution contains duplicate entry for the same host",293					);294				}295			}296		}297		match self.0.len() {298			0 => panic!("consistency: empty distributions"),299			1 => self.0[0].serialize(serializer),300			_ => self.0.serialize(serializer),301		}302	}303}304impl<'de> Deserialize<'de> for FleetSecretDistributions {305	fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>306	where307		D: serde::Deserializer<'de>,308	{309		#[derive(Deserialize)]310		#[serde(untagged)]311		enum Distributions {312			One(FleetSecretDistribution),313			Many(Vec<FleetSecretDistribution>),314		}315		let d = Distributions::deserialize(deserializer)?;316		let ds = match d {317			Distributions::One(d) => vec![d],318			Distributions::Many(ds) => ds,319		};320		if ds.is_empty() {321			return Err(de::Error::custom("consistency: empty distributions"));322		}323		let mut found_hosts = BTreeSet::new();324		for ele in ds.iter() {325			if ele.owners.is_empty() {326				return Err(de::Error::custom(327					"consistency: secret distribution has no defined owners",328				));329			}330			for ele in ele.owners.iter() {331				if !found_hosts.insert(ele) {332					return Err(de::Error::custom(333						"consistency: secret distribution contains duplicate entry for the same host",334					));335				}336			}337		}338		Ok(Self(ds))339	}340}341342#[derive(Serialize, Deserialize, Default)]343pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);344345impl FleetSecrets {346	pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {347		self.0.keys()348	}349350	pub fn keys_for_owner(&self, owner: &str) -> impl Iterator<Item = &String> {351		self.0352			.iter()353			.filter(|(_, d)| d.contains(owner))354			.map(|(n, _)| n)355	}356357	pub fn drop_owner_no_reencrypt(&mut self, secret: &str, owner: &str) -> bool {358		let Entry::Occupied(mut dists) = self.0.entry(secret.to_owned()) else {359			return false;360		};361		let DistEntry::Occupied(dist) = dists.get_mut().entry(owner.to_owned()) else {362			return false;363		};364365		dist.remove();366367		if dists.get().0.is_empty() {368			dists.remove();369		};370371		true372	}373	pub fn set_single_data(&mut self, secret: String, owner: String, data: FleetSecretData) {374		let e = self375			.0376			.entry(secret.to_owned())377			.or_insert_with(|| FleetSecretDistributions(Default::default()));378		e.entry(owner.to_owned()).set(data);379	}380	pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {381		match self.0.entry(secret) {382			Entry::Vacant(e) => {383				e.insert(FleetSecretDistributions(vec![data]));384			}385			Entry::Occupied(mut e) => {386				let dists = e.get_mut();387				dists.extend(data)388			}389		}390	}391	pub fn get_single(&self, secret: &str, owner: &str) -> Option<&FleetSecretDistribution> {392		let secret = self.0.get(secret)?;393		secret.get(owner)394	}395	pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {396		self.0.get(secret)397	}398399	pub fn contains_for_owner(&self, secret: &str, owner: &str) -> bool {400		let Some(secret) = self.0.get(secret) else {401			return false;402		};403		secret.contains(owner)404	}405	pub fn contains(&self, secret: &str) -> bool {406		self.0.contains_key(secret)407	}408	pub fn remove(&mut self, secret: &str) {409		self.0.remove(secret);410	}411412	fn merge_from_hosts(413		&mut self,414		host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,415	) {416		for (host, host_secrets) in host_secrets {417			for (secret_name, mut secret_data) in host_secrets {418				secret_data.owners.insert(host.clone());419				self.set_data(secret_name, secret_data);420			}421		}422	}423}
modifiedcrates/nix-eval/src/util.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -1,15 +1,23 @@
 use std::time::Instant;
 
 use anyhow::bail;
+use serde::Deserialize;
 use tracing::{debug, warn};
 
 use crate::{Value, nix_go_json};
 
+#[derive(Deserialize, Debug)]
+struct Assertion {
+	assertion: bool,
+	message: String,
+}
+
 #[tracing::instrument(level = "info", skip(val))]
 pub async fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
 	let before_errors = Instant::now();
 	let errors: Vec<String> = nix_go_json!(val.errors);
-	debug!("errors evaluation took {:?}", before_errors.elapsed());
+	// let assertions: Vec<Assertion> = nix_go_json!(val.assertions);
+	debug!("errors evaluation took {:?} {errors:?} ", before_errors.elapsed());
 	if !errors.is_empty() {
 		bail!(
 			"failed with error{}{}",
modifiedcrates/nixlike/Cargo.tomldiffbeforeafterboth
--- a/crates/nixlike/Cargo.toml
+++ b/crates/nixlike/Cargo.toml
@@ -7,10 +7,10 @@
 [dependencies]
 thiserror.workspace = true
 
+itertools = "0.14.0"
 linked-hash-map = "0.5.6"
 peg = "0.8.5"
 ron = "0.11.0"
 serde = { version = "1.0.219", features = ["derive"] }
 serde-transcode = "1.1.1"
 serde_json = "1.0.140"
-itertools = "0.14.0"
modifiedlib/default.nixdiffbeforeafterboth
--- a/lib/default.nix
+++ b/lib/default.nix
@@ -150,7 +150,10 @@
       );
 
     mkAskPass =
-      { prompt ? "Secret value", part ? "secret" }:
+      {
+        prompt ? "Secret value",
+        part ? "secret",
+      }:
       (
         {
           kdePackages,
modifiedlib/flakePart.nixdiffbeforeafterboth
--- a/lib/flakePart.nix
+++ b/lib/flakePart.nix
@@ -34,7 +34,7 @@
           # to do that, evaluate all the modules with only needed option declared.
           bootstrapEval = lib.evalModules {
             class = "fleet";
-            prefix = ["fleetConfiguration"];
+            prefix = [ "fleetConfiguration" ];
             modules = [
               module
               {
@@ -53,7 +53,7 @@
           bootstrapNixpkgs = bootstrapEval.config.nixpkgs.buildUsing;
           normalEval = bootstrapNixpkgs.lib.evalModules {
             class = "fleet";
-            prefix = ["fleetConfiguration"];
+            prefix = [ "fleetConfiguration" ];
             modules = (import ../modules/module-list.nix) ++ [
               module
               (
modifiedmodules/nixos.nixdiffbeforeafterboth
--- a/modules/nixos.nix
+++ b/modules/nixos.nix
@@ -39,13 +39,23 @@
             in
             config.nixpkgs.buildUsing.lib.evalModules {
               class = "nixos";
-              prefix = ["fleetConfiguration" "hosts" hostArgs.config._module.args.name "nixos"];
+              prefix = [
+                "fleetConfiguration"
+                "hosts"
+                hostArgs.config._module.args.name
+                "nixos"
+              ];
               modules = (import "${modulesPath}/module-list.nix") ++ [
                 (module // { key = "attr<host.nixos>"; })
                 (config.nixos // { key = "attr<fleet.nixos>"; })
               ];
               specialArgs = {
-                inherit fleetLib inputs self modulesPath;
+                inherit
+                  fleetLib
+                  inputs
+                  self
+                  modulesPath
+                  ;
               };
             };
         };
@@ -54,32 +64,34 @@
         };
       };
       config = {
-        nixos = let 
-          inherit (hostArgs.config) system;
-        in {
-          _module.args = {
-            nixosHosts = mapAttrs (_: value: value.nixos_unchecked.config) config.hosts;
-            hosts = config.hosts;
-            host = hostArgs.config;
-            fleetConfiguration = config;
+        nixos =
+          let
+            inherit (hostArgs.config) system;
+          in
+          {
+            _module.args = {
+              nixosHosts = mapAttrs (_: value: value.nixos_unchecked.config) config.hosts;
+              hosts = config.hosts;
+              host = hostArgs.config;
+              fleetConfiguration = config;
 
-            inputs' = mapAttrs (
-              inputName: input:
-              builtins.addErrorContext
-                "while retrieving system-dependent attributes for input ${escapeNixIdentifier inputName}"
-                (
-                  if input._type or null == "flake" then
-                    _fleetFlakeRootConfig.perInput system input
-                  else
-                    "input is not a flake, perhaps flake = false was added to te input declaration?"
-                )
-            ) inputs;
-            self' = builtins.addErrorContext "while retrieving system-dependent attributes for a flake's own outputs" (
-              _fleetFlakeRootConfig.perInput system self
-            );
+              inputs' = mapAttrs (
+                inputName: input:
+                builtins.addErrorContext
+                  "while retrieving system-dependent attributes for input ${escapeNixIdentifier inputName}"
+                  (
+                    if input._type or null == "flake" then
+                      _fleetFlakeRootConfig.perInput system input
+                    else
+                      "input is not a flake, perhaps flake = false was added to te input declaration?"
+                  )
+              ) inputs;
+              self' = builtins.addErrorContext "while retrieving system-dependent attributes for a flake's own outputs" (
+                _fleetFlakeRootConfig.perInput system self
+              );
+            };
+            nixpkgs.hostPlatform = system;
           };
-          nixpkgs.hostPlatform = system;
-        };
         nixos_unchecked = hostArgs.config.nixos.extendModules {
           modules = [
             {
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -77,7 +77,7 @@
     }:
     let
       secretName = config._module.args.name;
-      literal = l: enum [l];
+      literal = l: enum [ l ];
     in
     {
       options = {
@@ -109,17 +109,16 @@
       config = {
         # C api is broken in regard to thunks
         # https://github.com/NixOS/nix/issues/12800
-        parts = let 
-          hostName = host._module.args.name;
-          generator = config.generator;
-        in builtins.deepSeq [
-          hostName
-          secretName
-          generator
-        ] (builtins.fleetEnsureHostSecret
-          hostName
-          secretName
-          generator);
+        parts =
+          let
+            hostName = host._module.args.name;
+            generator = config.generator;
+          in
+          builtins.deepSeq [
+            hostName
+            secretName
+            generator
+          ] (builtins.fleetEnsureHostSecret hostName secretName generator);
       };
     }
   );
@@ -136,14 +135,16 @@
     secrets = mkOption {
       type = attrsOf secretType;
       default = { };
-      apply = mapAttrs (_: secret: secret.parts // {definition = secret;});
+      apply = mapAttrs (_: secret: secret.parts // { definition = secret; });
       description = "Host-local secrets";
     };
     system.secretsData = mkOption {
       type = unspecified;
-      default = mapAttrs (_: s:
-        (removeAttrs s.definition ["generator"]) // {
-          parts = mapAttrs (_: part: removeAttrs part ["data"]) s.definition.parts;
+      default = mapAttrs (
+        _: s:
+        (removeAttrs s.definition [ "generator" ])
+        // {
+          parts = mapAttrs (_: part: removeAttrs part [ "data" ]) s.definition.parts;
         }
       ) config.secrets;
       description = "secrets.json contents";
@@ -152,13 +153,25 @@
   config = {
     environment.systemPackages = [ pkgs.fleet-install-secrets ];
 
-    assertions = mapAttrsToList (name: secret: let
-      hasSharedDefinition = fleetConfiguration.secrets ? name;
-    in {
-      assertion = (secret.definition.generator == "shared") == hasSharedDefinition && hasSharedDefinition -> (elem host._module.args.name fleetConfiguration.secrets.${name}.expectedOwners);
-      message = if hasSharedDefinition then"secret ${name} has host-specific secret generator, secrets with host-specific generators can not have shared generator in fleet configuration"
-      else "secret ${name} is declared as shared, for shared secret fleet configuration should include shared secret generator, and expectedOwners should contain this host";
-    }) config.secrets;
+    assertions = mapAttrsToList (
+      name: secret:
+      let
+        hasSharedDefinition = fleetConfiguration.secrets ? ${name};
+      in
+      {
+        assertion =
+          (secret.definition.generator == "shared") == hasSharedDefinition
+          && (
+            hasSharedDefinition
+            -> (elem host._module.args.name fleetConfiguration.secrets.${name}.expectedOwners)
+          );
+        message =
+          if hasSharedDefinition then
+            "secret ${name} has host-specific secret generator, secrets with host-specific generators can not have shared generator in fleet configuration"
+          else
+            "secret ${name} is declared as shared, for shared secret fleet configuration should include shared secret generator, and expectedOwners should contain this host";
+      }
+    ) config.secrets;
 
     systemd.services.fleet-install-secrets = mkIf useSysusers {
       wantedBy = [ "sysinit.target" ];
modifiedmodules/nixos/top-level.nixdiffbeforeafterboth
--- a/modules/nixos/top-level.nix
+++ b/modules/nixos/top-level.nix
@@ -2,6 +2,7 @@
   pkgs,
   config,
   lib,
+  ...
 }:
 let
   inherit (lib.strings) optionalString;