difftreelog
feat on-demand secret generation
in: trunk
24 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1149,7 +1149,6 @@
"base64 0.22.1",
"serde",
"unicode_categories",
- "z85",
]
[[package]]
@@ -2089,6 +2088,7 @@
"serde_json",
"test-log",
"thiserror 2.0.17",
+ "tokio",
"tracing",
"tracing-indicatif",
"vte 0.15.0",
@@ -4638,12 +4638,6 @@
"syn",
"synstructure",
]
-
-[[package]]
-name = "z85"
-version = "3.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b3a41ce106832b4da1c065baa4c31cf640cf965fa1483816402b7f6b96f0a64"
[[package]]
name = "zerocopy"
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -7,8 +7,9 @@
host::{Config, DeployKind, GenerationStorage},
opts::FleetOpts,
};
+use futures::{StreamExt as _, stream::FuturesUnordered};
use nix_eval::nix_go;
-use tokio::task::{LocalSet, spawn_blocking};
+use tokio::task::spawn_blocking;
use tracing::{Instrument, error, field, info, info_span, warn};
#[derive(Parser)]
@@ -47,7 +48,7 @@
"--profile",
format!(
"/nix/var/nix/profiles/{}-{hostname}",
- config.data().gc_root_prefix
+ config.data.gc_root_prefix
),
)
.arg(&out_output);
@@ -60,14 +61,14 @@
impl BuildSystems {
pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
let hosts = opts.filter_skipped(config.list_hosts()?)?;
- let set = LocalSet::new();
+ let mut tasks = FuturesUnordered::new();
let build_attr = self.build_attr.clone();
for host in hosts {
let config = config.clone();
let span = info_span!("build", host = field::display(&host.name));
let hostname = host.name;
let build_attr = build_attr.clone();
- set.spawn_local(
+ tasks.push(
(async move {
let built = match build_task(config, hostname.clone(), &build_attr).await {
Ok(path) => path,
@@ -88,7 +89,7 @@
.instrument(span),
);
}
- set.await;
+ for _task in tasks.next().await {}
Ok(())
}
}
@@ -96,7 +97,7 @@
impl Deploy {
pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
let hosts = opts.filter_skipped(config.list_hosts()?)?;
- let set = LocalSet::new();
+ let mut tasks = FuturesUnordered::new();
for host in hosts.into_iter() {
let config = config.clone();
let span = info_span!("deploy", host = field::display(&host.name));
@@ -112,7 +113,7 @@
host.set_legacy_ssh_store(legacy);
};
- set.spawn_local(
+ tasks.push(
(async move {
let built = match build_task(config.clone(), hostname.clone(), "toplevel-fleet")
.await
@@ -170,7 +171,7 @@
.instrument(span),
);
}
- set.await;
+ for _task in tasks.next().await {}
Ok(())
}
}
cmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -56,7 +56,7 @@
.collect::<HashSet<_>>();
let mut stored_locally = config
.local_host()
- .list_generations(&format!("{}-{}", config.data().gc_root_prefix, host.name))
+ .list_generations(&format!("{}-{}", config.data.gc_root_prefix, host.name))
.await
.inspect_err(|e| {
warn!("failed to list generations available locally: {e}");
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -412,7 +412,7 @@
if opts.should_skip(&host)? {
continue;
}
- config.key(&host.name).await?;
+ config.host_key(&host.name).await?;
}
}
Secret::Read {
@@ -421,6 +421,7 @@
part: part_name,
mut prefer_identities,
} => {
+ /*
let Some(secret) = config.shared_secret(&name) else {
bail!("secret doesn't exists");
};
@@ -460,6 +461,8 @@
part.raw.data.clone()
};
stdout().write_all(&data)?;
+ */
+ todo!()
}
Secret::Regenerate {
prefer_identities,
@@ -605,6 +608,7 @@
todo!()
}
Secret::List {} => {
+ /*
let _span = info_span!("loading secrets").entered();
let configured = config.list_configured_shared()?;
#[derive(Tabled)]
@@ -638,6 +642,8 @@
*/
}
// info!("loaded\n{}", Table::new(table).to_string())
+ */
+ todo!()
}
Secret::Edit {
name,
@@ -645,7 +651,7 @@
part,
add,
} => {
- let secret = config
+ /*let secret = config
.host_secret(&machine, &name)
.context("secret not found")?;
if let Some(data) = secret.secret.parts.get(&part) {
@@ -656,7 +662,8 @@
String::new()
} else {
bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");
- };
+ };*/
+ todo!()
}
}
Ok(())
cmds/fleet/src/cmds/tf.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/tf.rs
+++ b/cmds/fleet/src/cmds/tf.rs
@@ -72,9 +72,9 @@
let tf_data: TfData = serde_json::from_slice(&data.stdout)
.context("failed to parse terraform fleet output")?;
- let mut data = config.data();
debug!("synchronized done = {tf_data:?}");
- data.extra.insert(
+ let mut extra = config.data.extra.write().expect("no poisoning");
+ extra.insert(
"terraformHosts".to_owned(),
serde_json::to_value(tf_data.hosts).expect("should be valid extra"),
);
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -4,7 +4,7 @@
// pub(crate) mod command;
pub(crate) mod extra_args;
-use std::{env, ffi::OsString, process::ExitCode};
+use std::{env, ffi::OsString, process::ExitCode, sync::Arc};
use anyhow::{Result, bail};
use clap::{CommandFactory, Parser};
@@ -23,7 +23,9 @@
use human_repr::HumanCount;
#[cfg(feature = "indicatif")]
use indicatif::{ProgressState, ProgressStyle};
-use nix_eval::{gc_register_my_thread, gc_unregister_my_thread, init_libraries};
+use nix_eval::{
+ gc_register_my_thread, gc_unregister_my_thread, init_libraries, init_tokio_for_nix,
+};
use tracing::{Instrument, error, info, info_span};
#[cfg(feature = "indicatif")]
use tracing_indicatif::IndicatifLayer;
@@ -39,9 +41,9 @@
info!("nothing to prefetch: no prefetch directory");
return Ok(());
}
- let tasks = <FuturesUnordered<LocalBoxFuture<Result<()>>>>::new();
+ let tasks = FuturesUnordered::new();
for entry in std::fs::read_dir(&prefetch_dir)? {
- tasks.push(Box::pin(async {
+ tasks.push(async {
let entry = entry?;
if !entry.metadata()?.is_file() {
bail!("only files should exist in prefetch directory");
@@ -59,7 +61,7 @@
status.arg("store").arg("prefetch-file").arg(path);
status.run_nix_string().instrument(span).await?;
Ok(())
- }));
+ });
}
tasks.try_collect::<Vec<()>>().await?;
Ok(())
@@ -190,7 +192,7 @@
init_libraries();
- tokio::runtime::Builder::new_multi_thread()
+ let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.on_thread_start(|| {
gc_register_my_thread();
@@ -199,8 +201,13 @@
gc_unregister_my_thread();
})
.build()
- .expect("failed to build runtime")
- .block_on(async {
+ .expect("failed to build runtime");
+ let runtime = Arc::new(runtime);
+
+ init_tokio_for_nix(runtime.clone());
+
+ runtime.block_on(async {
+ tokio::task::spawn(async move {
if let Err(e) = main_real(opts).await {
error!("{e:#}");
ExitCode::FAILURE
@@ -208,6 +215,9 @@
ExitCode::SUCCESS
}
})
+ .await
+ .expect("primary task panicked")
+ })
// async_main(opts)
}
crates/fleet-base/src/command.rsdiffbeforeafterboth--- a/crates/fleet-base/src/command.rs
+++ b/crates/fleet-base/src/command.rs
@@ -334,7 +334,7 @@
let mut stderr = child.stderr.take().unwrap();
let stdout = child.stdout.take().unwrap();
let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
- let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));
+ let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
let mut ob = want_stdout
.then(|| out.take().unwrap())
.unwrap_or_else(|| Box::new(EmptyAsyncRead));
@@ -397,7 +397,7 @@
let mut stderr = child.stderr().take().unwrap();
let stdout = child.stdout().take().unwrap();
let mut err = FramedRead::new(&mut stderr, LinesCodec::new());
- let mut out: Option<Box<dyn AsyncRead + Unpin>> = Some(Box::new(stdout));
+ let mut out: Option<Box<dyn AsyncRead + Unpin + Send>> = Some(Box::new(stdout));
let mut ob = want_stdout
.then(|| out.take().unwrap())
.unwrap_or_else(|| Box::new(EmptyAsyncRead));
crates/fleet-base/src/deploy.rsdiffbeforeafterboth--- a/crates/fleet-base/src/deploy.rs
+++ b/crates/fleet-base/src/deploy.rs
@@ -78,7 +78,7 @@
// unit name conflict in systemd-run
// This code is tied to rollback.nix
if !disable_rollback && action.should_create_rollback_marker() {
- let _span = info_span!("preparing").entered();
+ // let _span = info_span!("preparing").entered();
info!("preparing for rollback");
let generation = get_current_generation(host).await?;
info!(
@@ -179,7 +179,7 @@
// FIXME: Connection might be disconnected after activation run
if action.should_activate() && !failed {
- let _span = info_span!("activating").entered();
+ // let _span = info_span!("activating").entered();
info!("executing activation script");
let specialised = if let Some(specialisation) = specialisation {
let mut specialised = built.join("specialisation");
crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -1,10 +1,12 @@
use std::{
+ cmp::Ordering,
collections::{
BTreeMap, BTreeSet,
btree_map::{self, Entry},
},
+ fmt,
io::{self, Cursor},
- ops::Deref,
+ sync::RwLock,
};
use age::Recipient;
@@ -77,19 +79,18 @@
pub manager_keys: Vec<ManagerKey>,
#[serde(default)]
- pub hosts: BTreeMap<String, HostData>,
+ pub hosts: RwLock<BTreeMap<String, HostData>>,
#[serde(default, alias = "shared_secrets")]
- pub secrets: FleetSecrets,
+ pub secrets: RwLock<FleetSecrets>,
// extra_name => anything
#[serde(default)]
- #[serde(skip_serializing_if = "BTreeMap::is_empty")]
- pub extra: BTreeMap<String, Value>,
+ pub extra: RwLock<BTreeMap<String, Value>>,
#[serde(default)]
- #[serde(skip_serializing_if = "BTreeMap::is_empty")]
- host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,
+ #[serde(skip_serializing)]
+ host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,
}
impl FleetData {
pub fn from_str(s: &str) -> anyhow::Result<Self> {
@@ -97,6 +98,8 @@
if !data.host_secrets.is_empty() {
info!("migrating host secrets into shared secrets structure");
data.secrets
+ .write()
+ .expect("no poisoning")
.merge_from_hosts(std::mem::take(&mut data.host_secrets));
}
Ok(data)
@@ -130,128 +133,431 @@
#[serde(rename_all = "camelCase")]
#[must_use]
pub struct FleetSecretData {
- #[serde(default = "Utc::now")]
pub created_at: DateTime<Utc>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Option::is_none", alias = "expire_at")]
+ #[serde(default, skip_serializing_if = "Option::is_none", alias = "expire_at")]
pub expires_at: Option<DateTime<Utc>>,
#[serde(flatten)]
pub parts: BTreeMap<String, FleetSecretPart>,
- #[serde(default)]
- #[serde(skip_serializing_if = "Value::is_null")]
+ #[serde(default, skip_serializing_if = "Value::is_null")]
pub generation_data: Value,
}
+fn is_false(b: &bool) -> bool {
+ *b == false
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct SecretOwner(String);
+
+impl fmt::Display for SecretOwner {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "host:{}", self.0)
+ }
+}
+
+impl SecretOwner {
+ pub fn host(s: impl AsRef<str>) -> SecretOwner {
+ SecretOwner(s.as_ref().to_owned())
+ }
+ pub fn as_host(&self) -> Option<&str> {
+ Some(&self.0)
+ }
+}
+
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
#[must_use]
pub struct FleetSecretDistribution {
#[serde(default)]
- pub owners: BTreeSet<String>,
+ owners: BTreeSet<SecretOwner>,
+ #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
+ owners_pending_prune: BTreeMap<SecretOwner, String>,
+
#[serde(flatten)]
pub secret: FleetSecretData,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pending_prune: Option<String>,
#[serde(default, skip_serializing, alias = "managed")]
- pub _deprecated_managed: bool,
+ _deprecated_managed: bool,
+}
+
+const EMPTY_PENDING_PRUNE: &BTreeMap<SecretOwner, String> = &BTreeMap::new();
+impl FleetSecretDistribution {
+ pub fn new(owners: BTreeSet<SecretOwner>, secret: FleetSecretData, now: DateTime<Utc>) -> Self {
+ assert!(
+ !owners.is_empty(),
+ "distribution should have at least one owner"
+ );
+ if let Some(expires_at) = &secret.expires_at {
+ assert!(
+ *expires_at > now,
+ "secret should not be expired on creation"
+ );
+ }
+ Self {
+ owners,
+ secret,
+ owners_pending_prune: BTreeMap::new(),
+ pending_prune: None,
+ _deprecated_managed: true,
+ }
+ }
+
+ fn owners_ex(&self, including_pruned: bool) -> impl Iterator<Item = &SecretOwner> {
+ let pending_prune = if including_pruned {
+ &self.owners_pending_prune
+ } else {
+ EMPTY_PENDING_PRUNE
+ };
+ self.owners.iter().chain(pending_prune.keys())
+ }
+ pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {
+ self.owners_ex(false)
+ }
+
+ pub fn prune(&mut self, reason: String) {
+ assert!(
+ self.pending_prune.is_none(),
+ "it shouldn't be possible to prune the same distribution twice using public api"
+ );
+ self.pending_prune = Some(reason);
+ }
+ pub fn prune_owners(&mut self, owners: &BTreeSet<SecretOwner>, reason: String) {
+ // if self.owners.iter().all(|o| owners.contains(o)) && self.owners_pending_prune.is_empty() {
+ // self.prune(format!("all owners were pruned: {reason}"));
+ // return;
+ // }
+ for owner in owners {
+ if self.owners.remove(owner) {
+ self.owners_pending_prune
+ .insert(owner.to_owned(), reason.clone());
+ }
+ }
+ // if self.owners.is_empty() {
+ // self.prune("no owners left".to_owned());
+ // }
+ }
+ pub fn unprune_owner(&mut self, owner: SecretOwner) {
+ if self.owners_pending_prune.remove(&owner).is_some() {
+ self.owners.insert(owner);
+ }
+ }
}
-#[derive(Clone)]
+#[derive(Clone, Debug, Default)]
#[must_use]
-pub struct FleetSecretDistributions(Vec<FleetSecretDistribution>);
+pub struct FleetSecretDistributions {
+ stored: Vec<FleetSecretDistribution>,
+}
-impl Deref for FleetSecretDistributions {
- type Target = [FleetSecretDistribution];
-
- fn deref(&self) -> &Self::Target {
- self.0.as_slice()
+fn compare_dists(
+ a: &FleetSecretDistribution,
+ b: &FleetSecretDistribution,
+ prefer_identities: &BTreeSet<SecretOwner>,
+ include_pruned_owners: bool,
+) -> Ordering {
+ use Ordering::*;
+ if prefer_identities.is_empty() {
+ let a_has = a
+ .owners_ex(include_pruned_owners)
+ .any(|o| prefer_identities.contains(o));
+ let b_has = b
+ .owners_ex(include_pruned_owners)
+ .any(|o| prefer_identities.contains(o));
+ match (a_has, b_has) {
+ (true, false) => return Greater,
+ (false, true) => return Less,
+ _ => {}
+ }
+ }
+ match (a.secret.expires_at, b.secret.expires_at) {
+ (None, Some(_)) => return Greater,
+ (Some(_), None) => return Less,
+ (Some(a), Some(b)) => {
+ // Later is better
+ return a.cmp(&b);
+ }
+ (None, None) => {}
}
+
+ // Which one is easier to access
+ return a.owners.len().cmp(&b.owners.len());
}
impl FleetSecretDistributions {
- pub fn owners(&self) -> impl Iterator<Item = &String> {
- self.0.iter().flat_map(|v| v.owners.iter())
+ /// Drop expired distributions
+ fn prune_expired(&mut self, now: DateTime<Utc>) {
+ for ele in self.distributions_mut() {
+ if let Some(expires_at) = ele.secret.expires_at {
+ if expires_at < now {
+ ele.prune(format!("expired during check at {now}"));
+ }
+ }
+ }
+ }
+ /// Perform all pruning relevant to shared secrets
+ /// Also see expected_owner_removed
+ pub fn prune_shared(
+ &mut self,
+ expected_owners: &BTreeSet<SecretOwner>,
+ unique: bool,
+ expected_parts: &BTreeMap<String, GeneratorPart>,
+ expected_generation_data: &Value,
+ regenerate_on_owner_removed: bool,
+ regenerate_on_owner_added: bool,
+ prefer_identities: &BTreeSet<SecretOwner>,
+ now: DateTime<Utc>,
+ ) {
+ self.prune_expired(now);
+ self.prune_generation_data(expected_generation_data, None);
+ self.prune_missing_parts(expected_parts, None);
+
+ let current_owners = self.owners().cloned().collect::<BTreeSet<SecretOwner>>();
+
+ let mut to_add = expected_owners.difference(¤t_owners);
+ if to_add.next().is_some() && unique && regenerate_on_owner_added {
+ for dist in self.distributions_mut() {
+ dist.prune(format!(
+ "owners missing, can't add new distribution, regeneration preferred"
+ ));
+ }
+ return;
+ }
+
+ for to_remove in current_owners.difference(&expected_owners) {
+ self.entry(to_remove.clone()).remove(
+ regenerate_on_owner_removed,
+ "owner was removed from expected owners list, regenerate_on_owner_removed is set"
+ .to_string(),
+ );
+ }
+ if unique {
+ self.prune_nonunique(prefer_identities);
+ }
+ }
+ pub fn prune_host(
+ &mut self,
+ owner: SecretOwner,
+ expected_parts: &BTreeMap<String, GeneratorPart>,
+ expected_generation_data: &Value,
+ now: DateTime<Utc>,
+ ) {
+ self.prune_expired(now);
+ self.prune_generation_data(expected_generation_data, Some(&owner));
+ // TODO: Owner-based pruning is warranted (e.g host no longer has secret defined)
+ self.prune_missing_parts(expected_parts, Some(&owner));
+ }
+ /// Position of best distributions as in iterator returned by distributions()
+ /// None if distributions not found
+ fn best_idx(
+ &self,
+ prefer_identities: &BTreeSet<SecretOwner>,
+ include_pruned_owners: bool,
+ ) -> Option<usize> {
+ self.distributions()
+ .enumerate()
+ .max_by(|(_, a), (_, b)| {
+ compare_dists(&a, &b, prefer_identities, include_pruned_owners)
+ })
+ .map(|(p, _)| p)
+ }
+ /// Secret wants to be the same on all hosts, leave only one unpruned version of it
+ fn prune_nonunique(&mut self, prefer_identities: &BTreeSet<SecretOwner>) {
+ if self.distributions().next().is_none() {
+ return;
+ }
+ let best = self.best_idx(prefer_identities, false).expect("not empty");
+ for (i, dist) in self.distributions_mut().enumerate() {
+ if i != best {
+ dist.prune(
+ "secret wants to be the same on all hosts, only the best one was left"
+ .to_owned(),
+ );
+ }
+ }
+ }
+
+ pub fn try_unprune(&mut self, owner: SecretOwner) -> Option<&FleetSecretDistribution> {
+ assert!(self.get(&owner).is_none(), "secret is not pruned for host");
+ if let Some(dist) = self
+ .distributions_mut()
+ .find(|v| v.owners_pending_prune.contains_key(&owner))
+ {
+ dist.unprune_owner(owner);
+ Some(dist)
+ } else {
+ None
+ }
+ }
+
+ pub fn best_distribution_for_reencryption(
+ &mut self,
+ prefer_identities: &BTreeSet<SecretOwner>,
+ ) -> Option<&mut FleetSecretDistribution> {
+ let best_idx = self.best_idx(prefer_identities, true)?;
+ self.distributions_mut().nth(best_idx)
+ }
+
+ fn prune_missing_parts(
+ &mut self,
+ expected_parts: &BTreeMap<String, GeneratorPart>,
+ filter_owner: Option<&SecretOwner>,
+ ) {
+ 'dist: for ele in self.distributions_mut() {
+ if let Some(filter_owner) = filter_owner {
+ if !ele.owners.contains(filter_owner) {
+ continue;
+ }
+ // Note: secret still can have multiple owners even if it is host-owned
+ // in this case we expect that all owners using the same generator, so we can prune distribution for all of them
+ }
+ for (name, part) in expected_parts {
+ let Some(stored_part) = ele.secret.parts.get(name) else {
+ ele.prune(format!("secret definition added new part: {name}"));
+ continue 'dist;
+ };
+ if part.encrypted != stored_part.raw.encrypted {
+ ele.prune(format!(
+ "secret definition now requires part to be {}",
+ if part.encrypted {
+ "encrypted"
+ } else {
+ "non-encrypted"
+ }
+ ));
+ continue 'dist;
+ }
+ }
+ }
+ }
+ fn prune_generation_data(
+ &mut self,
+ expected_generation_data: &Value,
+ filter_owner: Option<&SecretOwner>,
+ ) {
+ for ele in self.distributions_mut() {
+ if let Some(filter_owner) = filter_owner {
+ if !ele.owners.contains(filter_owner) {
+ continue;
+ }
+ // Note: secret still can have multiple owners even if it is host-owned
+ // in this case we expect that all owners using the same generator, so we can prune distribution for all of them
+ }
+ if ele.secret.generation_data != *expected_generation_data {
+ ele.prune(format!(
+ "expected generation data mismatch: {expected_generation_data:?}"
+ ));
+ }
+ }
}
+
+ /// Prune all distributions with no unpruned owners.
+ /// For ease of reencryption where possible, it is only called on persistence, when in memory - pruned owners are kept and
+ /// can decrypt their secrets.
+ fn prune_dead(&mut self) {
+ for ele in self.distributions_mut() {
+ if ele.owners.is_empty() {
+ ele.prune("no owners left".to_owned());
+ }
+ }
+ }
+
+ pub fn distributions(&self) -> impl Iterator<Item = &FleetSecretDistribution> {
+ self.stored.iter().filter(|v| v.pending_prune.is_none())
+ }
+ pub fn distributions_mut(&mut self) -> impl Iterator<Item = &mut FleetSecretDistribution> {
+ self.stored.iter_mut().filter(|v| v.pending_prune.is_none())
+ }
+ pub fn owners(&self) -> impl Iterator<Item = &SecretOwner> {
+ self.distributions().flat_map(|v| v.owners.iter())
+ }
#[allow(
clippy::len_without_is_empty,
reason = "should not be empty for a long time"
)]
pub fn len(&self) -> usize {
- self.0.len()
+ self.distributions().count()
}
- pub fn get(&self, owner: &str) -> Option<&FleetSecretDistribution> {
- self.0.iter().find(|d| d.owners.contains(owner))
+ pub fn get(&self, owner: &SecretOwner) -> Option<&FleetSecretDistribution> {
+ self.distributions().find(|d| d.owners.contains(owner))
}
- fn entry(&mut self, owner: String) -> DistEntry<'_> {
- let Some(idx) = self.0.iter().position(|d| d.owners.contains(&owner)) else {
+ fn entry(&mut self, owner: SecretOwner) -> DistEntry<'_> {
+ let Some((idx, dist)) = self
+ .distributions()
+ .enumerate()
+ .find(|(_, d)| d.owners.contains(&owner))
+ else {
return DistEntry::Vacant(VacantDistEntry {
distributions: self,
- owner,
+ owners: BTreeSet::from([owner]),
});
};
DistEntry::Occupied(OccupiedDistEntry {
+ owners: dist.owners.clone(),
distributions: self,
idx,
- owner,
})
}
- fn extend(&mut self, dist: FleetSecretDistribution) {
- for owner in &dist.owners {
- self.entry(owner.to_owned()).remove();
+ pub fn extend(&mut self, dist: FleetSecretDistribution, reason: String) {
+ for ele in self.distributions_mut() {
+ ele.prune_owners(&dist.owners, reason.clone());
}
- self.0.push(dist);
+ self.stored.push(dist);
}
- pub fn contains(&self, owner: &str) -> bool {
- self.0.iter().any(|d| d.owners.contains(owner))
+ pub fn contains(&self, owner: &SecretOwner) -> bool {
+ self.distributions().any(|d| d.owners.contains(owner))
}
}
struct OccupiedDistEntry<'d> {
distributions: &'d mut FleetSecretDistributions,
idx: usize,
- owner: String,
+ owners: BTreeSet<SecretOwner>,
}
impl<'d> OccupiedDistEntry<'d> {
- fn remove(self) -> VacantDistEntry<'d> {
- let dist = &mut self.distributions.0[self.idx];
- assert!(
- dist.owners.remove(&self.owner),
- "entry exists, as we have its reference"
- );
- if dist.owners.is_empty() {
- self.distributions.0.remove(self.idx);
+ fn remove(self, whole_dist: bool, reason: String) -> VacantDistEntry<'d> {
+ let dist = &mut self.distributions.stored[self.idx];
+ if whole_dist {
+ dist.prune(reason);
+ } else {
+ dist.prune_owners(&self.owners, reason);
}
VacantDistEntry {
distributions: self.distributions,
- owner: self.owner,
+ owners: self.owners,
}
}
- fn set(self, secret: FleetSecretData) -> Self {
- self.remove().set(secret)
+ fn set(self, secret: FleetSecretData, reason: String) -> Self {
+ self.remove(false, reason).set(secret)
}
}
struct VacantDistEntry<'d> {
distributions: &'d mut FleetSecretDistributions,
- owner: String,
+ owners: BTreeSet<SecretOwner>,
}
impl<'d> VacantDistEntry<'d> {
fn set(self, secret: FleetSecretData) -> OccupiedDistEntry<'d> {
let Self {
distributions,
- owner,
+ owners,
} = self;
- let idx = distributions.0.len();
- distributions.0.push(FleetSecretDistribution {
- owners: BTreeSet::from_iter([owner.clone()]),
+ let idx = distributions.stored.len();
+ distributions.stored.push(FleetSecretDistribution {
+ owners: owners.clone(),
secret,
+ owners_pending_prune: BTreeMap::new(),
+ pending_prune: None,
_deprecated_managed: true,
});
OccupiedDistEntry {
distributions,
- owner,
+ owners,
idx,
}
}
@@ -262,16 +568,16 @@
Occupied(OccupiedDistEntry<'d>),
}
impl DistEntry<'_> {
- fn remove(self) -> Self {
+ fn remove(self, whole_dist: bool, reason: String) -> Self {
match self {
DistEntry::Vacant(_) => self,
- DistEntry::Occupied(o) => Self::Vacant(o.remove()),
+ DistEntry::Occupied(o) => Self::Vacant(o.remove(whole_dist, reason)),
}
}
- fn set(self, secret: FleetSecretData) -> Self {
+ fn set(self, secret: FleetSecretData, reason: String) -> Self {
Self::Occupied(match self {
DistEntry::Vacant(e) => e.set(secret),
- DistEntry::Occupied(e) => e.set(secret),
+ DistEntry::Occupied(e) => e.set(secret, reason),
})
}
}
@@ -281,8 +587,13 @@
where
S: serde::Serializer,
{
+ let mut v = self.clone();
+ v.prune_dead();
let mut found_hosts = BTreeSet::new();
- for ele in self.0.iter() {
+ for ele in v.distributions() {
+ if ele.pending_prune.is_some() {
+ continue;
+ }
if ele.owners.is_empty() {
panic!("consistency: secret distribution has no defined owners");
}
@@ -294,10 +605,15 @@
}
}
}
- match self.0.len() {
+ match v.stored.len() {
0 => panic!("consistency: empty distributions"),
- 1 => self.0[0].serialize(serializer),
- _ => self.0.serialize(serializer),
+ 1 => v.stored[0].serialize(serializer),
+ _ => {
+ let mut sorted = v.stored.clone();
+ // Store outdated distributions last
+ sorted.sort_by_key(|v| v.pending_prune.is_some() as u32);
+ sorted.serialize(serializer)
+ }
}
}
}
@@ -313,15 +629,18 @@
Many(Vec<FleetSecretDistribution>),
}
let d = Distributions::deserialize(deserializer)?;
- let ds = match d {
+ let stored = match d {
Distributions::One(d) => vec![d],
Distributions::Many(ds) => ds,
};
- if ds.is_empty() {
+ if stored.is_empty() {
return Err(de::Error::custom("consistency: empty distributions"));
}
let mut found_hosts = BTreeSet::new();
- for ele in ds.iter() {
+ for ele in stored.iter() {
+ if ele.pending_prune.is_some() {
+ continue;
+ }
if ele.owners.is_empty() {
return Err(de::Error::custom(
"consistency: secret distribution has no defined owners",
@@ -335,73 +654,65 @@
}
}
}
- Ok(Self(ds))
+ Ok(Self { stored })
}
}
-#[derive(Serialize, Deserialize, Default)]
+#[derive(Deserialize, Default)]
pub struct FleetSecrets(BTreeMap<String, FleetSecretDistributions>);
+impl Serialize for FleetSecrets {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let data: BTreeMap<String, FleetSecretDistributions> = self
+ .0
+ .iter()
+ .filter(|(_, v)| !v.stored.is_empty())
+ .map(|(k, v)| (k.clone(), v.clone()))
+ .collect();
+
+ data.serialize(serializer)
+ }
+}
+
impl FleetSecrets {
pub fn keys(&self) -> btree_map::Keys<String, FleetSecretDistributions> {
self.0.keys()
}
- pub fn keys_for_owner(&self, owner: &str) -> impl Iterator<Item = &String> {
+ pub fn keys_for_owner(&self, owner: &SecretOwner) -> impl Iterator<Item = &String> {
self.0
.iter()
.filter(|(_, d)| d.contains(owner))
.map(|(n, _)| n)
}
- pub fn drop_owner_no_reencrypt(&mut self, secret: &str, owner: &str) -> bool {
- let Entry::Occupied(mut dists) = self.0.entry(secret.to_owned()) else {
- return false;
- };
- let DistEntry::Occupied(dist) = dists.get_mut().entry(owner.to_owned()) else {
- return false;
- };
-
- dist.remove();
-
- if dists.get().0.is_empty() {
- dists.remove();
- };
-
- true
- }
- pub fn set_single_data(&mut self, secret: String, owner: String, data: FleetSecretData) {
- let e = self
- .0
- .entry(secret.to_owned())
- .or_insert_with(|| FleetSecretDistributions(Default::default()));
- e.entry(owner.to_owned()).set(data);
- }
pub fn set_data(&mut self, secret: String, data: FleetSecretDistribution) {
match self.0.entry(secret) {
Entry::Vacant(e) => {
- e.insert(FleetSecretDistributions(vec![data]));
+ e.insert(FleetSecretDistributions { stored: vec![data] });
}
Entry::Occupied(mut e) => {
let dists = e.get_mut();
- dists.extend(data)
+ dists.extend(data, "secret data was replaced".to_owned())
}
}
- }
- pub fn get_single(&self, secret: &str, owner: &str) -> Option<&FleetSecretDistribution> {
- let secret = self.0.get(secret)?;
- secret.get(owner)
}
pub fn get(&self, secret: &str) -> Option<&FleetSecretDistributions> {
self.0.get(secret)
}
+ pub fn get_mut(&mut self, secret: &str) -> Option<&mut FleetSecretDistributions> {
+ self.0.get_mut(secret)
+ }
- pub fn contains_for_owner(&self, secret: &str, owner: &str) -> bool {
- let Some(secret) = self.0.get(secret) else {
- return false;
- };
- secret.contains(owner)
+ pub fn get_or_create(&mut self, secret: &str) -> &mut FleetSecretDistributions {
+ self.0
+ .entry(secret.to_owned())
+ .or_insert(FleetSecretDistributions::default())
}
+
pub fn contains(&self, secret: &str) -> bool {
self.0.contains_key(secret)
}
@@ -411,7 +722,7 @@
fn merge_from_hosts(
&mut self,
- host_secrets: BTreeMap<String, BTreeMap<String, FleetSecretDistribution>>,
+ host_secrets: BTreeMap<SecretOwner, BTreeMap<String, FleetSecretDistribution>>,
) {
for (host, host_secrets) in host_secrets {
for (secret_name, mut secret_data) in host_secrets {
@@ -420,11 +731,27 @@
}
}
}
+
+ pub fn prune_host(&mut self, host: &SecretOwner, expected_nonshared: BTreeSet<String>) {
+ for (name, dists) in self.0.iter_mut() {
+ if expected_nonshared.contains(name) {
+ continue;
+ }
+ for dist in dists.distributions_mut() {
+ if dist.owners.contains(host) {
+ dist.prune_owners(
+ &BTreeSet::from([host.to_owned()]),
+ "host no longer defines this secret".to_owned(),
+ );
+ }
+ }
+ }
+ }
}
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Expectations {
- pub owners: BTreeSet<String>,
+ pub owners: BTreeSet<SecretOwner>,
pub generation_data: serde_json::Value,
pub parts: BTreeMap<String, GeneratorPart>,
}
@@ -432,3 +759,26 @@
pub struct GeneratorPart {
pub encrypted: bool,
}
+
+#[derive(Debug, Clone, Copy)]
+pub struct RegenerationConstraints {
+ pub allow_different: bool,
+ pub regenerate_on_owner_added: bool,
+ pub regenerate_on_owner_removed: bool,
+}
+impl RegenerationConstraints {
+ pub fn host_personal() -> Self {
+ Self {
+ allow_different: false,
+ regenerate_on_owner_added: true,
+ regenerate_on_owner_removed: true,
+ }
+ }
+ pub fn without_preferences(self) -> Self {
+ Self {
+ allow_different: self.allow_different,
+ regenerate_on_owner_added: false,
+ regenerate_on_owner_removed: false,
+ }
+ }
+}
crates/fleet-base/src/host.rsdiffbeforeafterboth--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -1,6 +1,5 @@
use std::{
- cell::OnceCell,
- collections::BTreeSet,
+ collections::{BTreeMap, BTreeSet},
ffi::{OsStr, OsString},
fmt::Display,
io::Write,
@@ -11,6 +10,7 @@
};
use anyhow::{Context, Result, anyhow, bail, ensure};
+use chrono::{DateTime, Utc};
use fleet_shared::SecretData;
use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};
use openssh::{ControlPersist, SessionBuilder};
@@ -22,15 +22,20 @@
use crate::{
command::MyCommand,
- fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},
+ fleetdata::{
+ FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretPart, SecretOwner,
+ },
};
pub struct FleetConfigInternals {
+ pub prefer_identities: BTreeSet<SecretOwner>,
+ pub now: DateTime<Utc>,
+
/// Fleet project directory, containing fleet.nix file.
pub directory: PathBuf,
/// builtins.currentSystem
pub local_system: String,
- pub data: Arc<Mutex<FleetData>>,
+ pub data: Arc<FleetData>,
pub nix_args: Vec<OsString>,
/// fleet_config.config
pub config_field: Value,
@@ -96,16 +101,16 @@
pub struct ConfigHost {
config: Config,
pub name: String,
- groups: OnceCell<Vec<String>>,
+ groups: OnceLock<Vec<String>>,
// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it
- deploy_kind: OnceCell<DeployKind>,
- session_destination: OnceCell<String>,
- legacy_ssh_store: OnceCell<bool>,
+ deploy_kind: OnceLock<DeployKind>,
+ session_destination: OnceLock<String>,
+ legacy_ssh_store: OnceLock<bool>,
pub host_config: Option<Value>,
- pub nixos_config: OnceCell<Value>,
- pub nixos_unchecked_config: OnceCell<Value>,
+ pub nixos_config: OnceLock<Value>,
+ pub nixos_unchecked_config: OnceLock<Value>,
pub pkgs_override: Option<Value>,
// TODO: Move command helpers away with connectivity refactor
@@ -397,7 +402,38 @@
ensure!(!data.encrypted, "secret came out encrypted");
Ok(data.data)
}
- pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {
+ pub async fn reencrypt_distribution(
+ &self,
+ data: &FleetSecretDistribution,
+ targets: BTreeSet<SecretOwner>,
+ now: DateTime<Utc>,
+ ) -> Result<FleetSecretDistribution> {
+ let mut parts = BTreeMap::new();
+ for (part_name, part) in &data.secret.parts {
+ parts.insert(
+ part_name.clone(),
+ if part.raw.encrypted {
+ FleetSecretPart {
+ raw: self.reencrypt(part.raw.clone(), targets.clone()).await?,
+ }
+ } else {
+ part.clone()
+ },
+ );
+ }
+ let secret = FleetSecretData {
+ created_at: data.secret.created_at,
+ expires_at: data.secret.expires_at,
+ generation_data: data.secret.generation_data.clone(),
+ parts,
+ };
+ Ok(FleetSecretDistribution::new(targets, secret, now))
+ }
+ pub async fn reencrypt(
+ &self,
+ data: SecretData,
+ targets: BTreeSet<SecretOwner>,
+ ) -> Result<SecretData> {
ensure!(data.encrypted, "secret is not encrypted");
let mut cmd = self.cmd("fleet-install-secrets").await?;
cmd.arg("reencrypt").eqarg("--secret", data.to_string());
@@ -537,12 +573,25 @@
}
}
+#[derive(Clone)]
pub struct SharedSecretDefinition(Value);
impl SharedSecretDefinition {
- pub fn expected_owners(&self) -> Result<BTreeSet<String>> {
+ pub fn expected_owners(&self) -> Result<BTreeSet<SecretOwner>> {
let secret = &self.0;
Ok(nix_go_json!(secret.expectedOwners))
}
+ pub fn allow_different(&self) -> Result<bool> {
+ let secret = &self.0;
+ Ok(nix_go_json!(secret.allowDifferent))
+ }
+ pub fn regenerate_on_owner_added(&self) -> Result<bool> {
+ let secret = &self.0;
+ Ok(nix_go_json!(secret.regenerateOnOwnerAdded))
+ }
+ pub fn regenerate_on_owner_removed(&self) -> Result<bool> {
+ let secret = &self.0;
+ Ok(nix_go_json!(secret.regenerateOnOwnerRemoved))
+ }
pub fn generator(&self) -> Result<Value> {
let secret = &self.0;
Ok(nix_go!(secret.generator))
@@ -572,10 +621,10 @@
config: self.clone(),
name: "<virtual localhost>".to_owned(),
host_config: None,
- nixos_config: OnceCell::new(),
- nixos_unchecked_config: OnceCell::new(),
+ nixos_config: OnceLock::new(),
+ nixos_unchecked_config: OnceLock::new(),
groups: {
- let cell = OnceCell::new();
+ let cell = OnceLock::new();
let _ = cell.set(vec![]);
cell
},
@@ -583,9 +632,9 @@
local: true,
session: OnceLock::new(),
- deploy_kind: OnceCell::new(),
- session_destination: OnceCell::new(),
- legacy_ssh_store: OnceCell::new(),
+ deploy_kind: OnceLock::new(),
+ session_destination: OnceLock::new(),
+ legacy_ssh_store: OnceLock::new(),
}
}
@@ -597,17 +646,17 @@
config: self.clone(),
name: name.to_owned(),
host_config: Some(host_config),
- nixos_config: OnceCell::new(),
- nixos_unchecked_config: OnceCell::new(),
- groups: OnceCell::new(),
+ nixos_config: OnceLock::new(),
+ nixos_unchecked_config: OnceLock::new(),
+ groups: OnceLock::new(),
pkgs_override: None,
// TODO: Remove with connectivit refactor
local: self.localhost == name,
session: OnceLock::new(),
- deploy_kind: OnceCell::new(),
- session_destination: OnceCell::new(),
- legacy_ssh_store: OnceCell::new(),
+ deploy_kind: OnceLock::new(),
+ session_destination: OnceLock::new(),
+ legacy_ssh_store: OnceLock::new(),
})
}
pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
@@ -623,55 +672,6 @@
pub fn system_config(&self, host: &str) -> Result<Value> {
let fleet_field = &self.config_field;
Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))
- }
-
- /// Shared secrets configured in fleet.nix or in flake
- pub fn list_configured_shared(&self) -> Result<Vec<String>> {
- let config_field = &self.config_field;
- nix_go!(config_field.sharedSecrets).list_fields()
- }
- pub fn has_shared(&self, name: &str) -> bool {
- let data = self.data();
- data.secrets.contains(name)
- }
- pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {
- let mut data = self.data_mut();
- data.secrets.set_data(name, shared);
- }
- pub fn remove_shared(&self, secret: &str) {
- let mut data = self.data_mut();
- data.secrets.remove(secret);
- }
-
- pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {
- let data = self.data_mut();
- data.secrets.keys_for_owner(host).cloned().collect()
- }
- pub fn list_secrets(&self) -> Vec<String> {
- let data = self.data_mut();
- data.secrets.keys().cloned().collect()
- }
-
- pub fn has_secret(&self, host: &str, secret: &str) -> bool {
- let data = self.data();
- data.secrets.contains_for_owner(secret, host)
- }
- pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {
- let mut data = self.data_mut();
- data.secrets.set_single_data(secret, host, value);
- }
- pub fn remove_secret(&self, host: &str, secret: &str) {
- let mut data = self.data_mut();
- data.secrets.drop_owner_no_reencrypt(secret, host);
- }
-
- pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {
- let data = self.data();
- data.secrets.get_single(secret, host).cloned()
- }
- pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {
- let data = self.data();
- data.secrets.get(secret).cloned()
}
pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {
@@ -685,22 +685,9 @@
))))
}
- // TODO: Should this be something modifiable from other processes?
- // E.g terraform provider might want to update FleetData (e.g secrets),
- // and current implementation assumes only one process holds current fleet.nix
- // Given that it is no longer needs to be a file for nix evaluation,
- // maybe it can be a .nix file for persistence, but accessible only
- // thru some shared state controller? Might it be stored in terraform
- // state provider?
- pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {
- self.data.lock().unwrap()
- }
- pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {
- self.data.lock().unwrap()
- }
pub fn save(&self) -> Result<()> {
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.")?;
- let data = nixlike::serialize(&self.data() as &FleetData)?;
+ let data = nixlike::serialize(&*self.data)?;
tempfile.write_all(
format!(
"# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"
crates/fleet-base/src/keys.rsdiffbeforeafterboth--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -1,17 +1,17 @@
use std::str::FromStr as _;
use age::Recipient;
-use anyhow::{Result, anyhow};
+use anyhow::{Result, anyhow, bail};
use futures::{StreamExt as _, TryStreamExt as _};
use itertools::Itertools as _;
use tracing::warn;
-use crate::host::Config;
+use crate::{fleetdata::SecretOwner, host::Config};
impl Config {
- pub fn cached_key(&self, host: &str) -> Option<String> {
- let data = self.data();
- let key = data.hosts.get(host).map(|h| &h.encryption_key);
+ fn cached_host_key(&self, host: &str) -> Option<String> {
+ let hosts = self.data.hosts.read().expect("no poisoning");
+ let key = hosts.get(host).map(|h| &h.encryption_key);
if let Some(key) = key
&& key.is_empty()
{
@@ -20,13 +20,13 @@
key.cloned()
}
pub fn update_key(&self, host: &str, key: String) {
- let mut data = self.data_mut();
- let host = data.hosts.entry(host.to_string()).or_default();
+ let mut hosts = self.data.hosts.write().expect("no poisoning");
+ let host = hosts.entry(host.to_string()).or_default();
host.encryption_key = key.trim().to_string();
}
- pub async fn key(&self, host: &str) -> anyhow::Result<String> {
- if let Some(key) = self.cached_key(host) {
+ pub async fn host_key(&self, host: &str) -> anyhow::Result<String> {
+ if let Some(key) = self.cached_host_key(host) {
Ok(key)
} else {
warn!("Loading key for {}", host);
@@ -38,18 +38,24 @@
Ok(key)
}
}
+ pub async fn key(&self, owner: &SecretOwner) -> anyhow::Result<String> {
+ if let Some(host) = owner.as_host() {
+ self.host_key(host).await
+ } else {
+ bail!("only host keys supported for now")
+ }
+ }
/// Insecure, requires root
- pub async fn recipient(&self, host: &str) -> anyhow::Result<Box<dyn Recipient>> {
+ pub async fn recipient(&self, host: &SecretOwner) -> anyhow::Result<Box<dyn Recipient>> {
let key = self.key(host).await?;
age::ssh::Recipient::from_str(&key)
.map_err(|e| anyhow!("parse recipient error: {e:?}"))
.map(|v| Box::new(v) as Box<dyn Recipient>)
}
- pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<Box<dyn Recipient>>> {
- let hosts = self.expand_owner_set(hosts)?;
+ pub async fn recipients(&self, hosts: Vec<SecretOwner>) -> Result<Vec<Box<dyn Recipient>>> {
futures::stream::iter(hosts.iter())
- .then(|m| self.recipient(m.as_ref()))
+ .then(|m| self.recipient(m))
.try_collect::<Vec<_>>()
.await
}
@@ -58,9 +64,8 @@
pub async fn orphaned_data(&self) -> Result<Vec<String>> {
let mut out = Vec::new();
let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();
- for hostname in self
- .data()
- .hosts
+ let hosts = self.data.hosts.read().expect("no poisoning");
+ for hostname in hosts
.iter()
.filter(|(_, host)| !host.encryption_key.is_empty())
.map(|(n, _)| n)
crates/fleet-base/src/lib.rsdiffbeforeafterboth--- a/crates/fleet-base/src/lib.rs
+++ b/crates/fleet-base/src/lib.rs
@@ -5,5 +5,4 @@
mod keys;
pub mod opts;
pub mod primops;
-pub mod secret;
pub mod secret_storage;
crates/fleet-base/src/opts.rsdiffbeforeafterboth--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -1,5 +1,5 @@
use std::{
- collections::BTreeMap,
+ collections::{BTreeMap, BTreeSet},
env::current_dir,
ffi::OsString,
str::FromStr,
@@ -7,6 +7,7 @@
};
use anyhow::{Context, Result, bail};
+use chrono::Utc;
use nix_eval::{
FetchSettings, FlakeLockFlags, FlakeReference, FlakeReferenceParseFlags, FlakeSettings, Value,
gc_now, nix_go, util::assert_warn,
@@ -212,7 +213,7 @@
}
let bytes =
std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;
- let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));
+ let data = Arc::new(FleetData::from_str(&bytes)?);
init_primops();
@@ -265,6 +266,10 @@
gc_now();
}
let config = Config(Arc::new(FleetConfigInternals {
+ // TODO: Load from somewhere
+ prefer_identities: BTreeSet::new(),
+ now: Utc::now(),
+
directory,
data,
flake_outputs: flake,
crates/fleet-base/src/primops.rsdiffbeforeafterboth--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -4,19 +4,16 @@
use anyhow::{Context, bail, ensure};
use fleet_shared::SecretData;
use itertools::Itertools;
-use nix_eval::{NativeFn, Value, nix_go, nix_go_json};
+use nix_eval::{NativeFn, Value, await_in_nix, nix_go, nix_go_json};
use serde::Deserialize;
use tracing::{info, warn};
use crate::fleetdata::{
Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,
+ RegenerationConstraints, SecretOwner,
};
use crate::host::{Config, ConfigHost};
-use crate::secret::{RegenerationReason, secret_needs_regeneration};
use anyhow::{Result, anyhow};
-
-#[derive(thiserror::Error, Debug)]
-enum Error {}
pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
@@ -28,7 +25,6 @@
}
pub fn get_pkgs_and_generators(host_on: &ConfigHost, recipients: Vec<String>) -> Result<Value> {
- info!("get pkgs");
let pkgs = host_on.pkgs()?;
let default_mk_secret_generators = nix_go!(pkgs.mkSecretGenerators);
let generators = nix_go!(default_mk_secret_generators(Obj { recipients }));
@@ -57,6 +53,31 @@
Ok(default_generator_drv)
}
+fn secret_to_parts(
+ secret_name: &str,
+ secret: &BTreeMap<String, FleetSecretPart>,
+ expected: &BTreeMap<String, GeneratorPart>,
+) -> Value {
+ let mut out = HashMap::new();
+ for (part_name, part) in secret {
+ if !expected.contains_key(part_name) {
+ warn!(
+ "secret {secret_name} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix"
+ );
+ continue;
+ };
+ out.insert(
+ part_name.as_str(),
+ Value::new_attrs(HashMap::from_iter([(
+ "raw",
+ Value::new_str(&part.raw.to_string()),
+ )])),
+ );
+ }
+
+ Value::new_attrs(out)
+}
+
pub async fn generate(
config: &Config,
expectations: Expectations,
@@ -76,9 +97,12 @@
} else {
config.local_host()
};
- let pkgs_and_generators =
- get_pkgs_and_generators(&host_on, expectations.owners.iter().cloned().collect())
- .context("failed to get pkgs for target host")?;
+ let mut recipients = Vec::new();
+ for owner in &expectations.owners {
+ recipients.push(config.key(owner).await?);
+ }
+ let pkgs_and_generators = get_pkgs_and_generators(&host_on, recipients)
+ .context("failed to get pkgs for target host")?;
let generator = call_package(config, &pkgs_and_generators, generator)
.context("failed to evaluate generator for target host")?;
@@ -147,15 +171,8 @@
generation_data: expectations.generation_data.clone(),
};
- let new_data = FleetSecretDistribution {
- secret: new_data,
- owners: expectations.owners.clone(),
- _deprecated_managed: true,
- };
-
- if let Some(reason) = secret_needs_regeneration(&new_data, &expectations) {
- bail!("newly generated secret needs to be regenerated: {reason}")
- }
+ let new_data =
+ FleetSecretDistribution::new(expectations.owners.clone(), new_data, config.now);
Ok(new_data)
}
@@ -166,18 +183,41 @@
}
pub fn init_primops() {
- info!("initializing primops");
NativeFn::new(
+ c"__fleetEnsureHostSecrets",
+ c"Ensure no extra secrets are stored for the host, pruning unknown",
+ [c"host", c"expectedNonshared", c"expectedShared", c"rest"],
+ |_es, [host, expected_nonshared, expected_shared, rest]| {
+ let host = SecretOwner::host(host.to_string()?);
+ let expected_nonshared: BTreeSet<String> = expected_nonshared.as_json()?;
+ let expected_shared: BTreeSet<String> = expected_shared.as_json()?;
+
+ let mut expected = expected_nonshared;
+ expected.extend(expected_shared);
+
+ let config = PRIMOPS_DATA
+ .get()
+ .expect("primops data should be set on init");
+
+ config
+ .data
+ .secrets
+ .write()
+ .expect("no poisoning")
+ .prune_host(&host, expected);
+
+ Ok(rest.clone())
+ },
+ )
+ .register();
+ NativeFn::new(
c"__fleetEnsureHostSecret",
c"Ensure secret existence for a host, regenerating it in case of some mismatch",
[c"host", c"secret", c"generator"],
|es, [host, secret, generator]| {
- info!("get host");
- let host = host.to_string()?;
- info!("get secret");
+ let host = SecretOwner::host(&host.to_string()?);
let secret = secret.to_string()?;
- info!("get config");
let config = PRIMOPS_DATA
.get()
.expect("primops data should be set on init");
@@ -193,50 +233,101 @@
ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");
- (true, shared_def.generator()?, expected_owners)
+ (Some(shared_def.clone()), shared_def.generator()?, expected_owners)
} else {
if shared_def.is_some() {
bail!("hosts can only have their own generators for non-shared secrets, either set host secret generator to \"shared\", or remove shared secret generator at fleetConfiguration.secrets.{secret}.generator")
}
- (false, generator.clone(), BTreeSet::from_iter([host.clone()]))
+ (None, generator.clone(), BTreeSet::from_iter([host.clone()]))
};
- let default_generator_drv = get_default_generator_drv(config, &generator).context("failed to evaluate default generator")?;
- let expectations = Expectations {
+ let default_generator_drv = get_default_generator_drv(config, &generator)?;
+ let mut expectations = Expectations {
parts: nix_go_json!(default_generator_drv.parts),
generation_data: nix_go_json!(default_generator_drv.generationData),
- owners: expected_owners,
+ owners: expected_owners.clone(),
+ };
+ let constraints = if let Some(shared) = &shared{
+ RegenerationConstraints {
+ allow_different: nix_go_json!(default_generator_drv.allowDifferent) && shared.allow_different()?,
+ regenerate_on_owner_added: shared.regenerate_on_owner_added()?,
+ regenerate_on_owner_removed: shared.regenerate_on_owner_added()?,
+ }
+ } else {
+ RegenerationConstraints::host_personal()
};
- let reason: RegenerationReason = 'regenerate: {
- let Some(existing) = config
- .host_secret(&host, &secret) else {
- break 'regenerate RegenerationReason::Missing;
+ let mut secrets = config.data.secrets.write().expect("no poisoning");
+ let dists = secrets.get_or_create(&secret);
+
+ if shared.is_some() {
+ dists.prune_shared(&expected_owners, !constraints.allow_different, &expectations.parts, &expectations.generation_data, constraints.regenerate_on_owner_removed, constraints.regenerate_on_owner_added, &config.prefer_identities, config.now);
+ } else {
+ dists.prune_host(host.clone(), &expectations.parts, &expectations.generation_data, config.now);
+ };
+
+ if let Some(dist) = dists.get(&host) {
+ return Ok(secret_to_parts(&secret, &dist.secret.parts, &expectations.parts));
};
- if let Some(reason) = secret_needs_regeneration(&existing, &expectations) {
- break 'regenerate reason;
+
+ let mut reencrypt_targets = expectations.owners.clone();
+ for dist in dists.distributions() {
+ for own in dist.owners() {
+ reencrypt_targets.remove(own);
+ }
}
+ if !constraints.regenerate_on_owner_added {
+ if let Some(unpruned) = dists.try_unprune(host.clone()) {
+ return Ok(secret_to_parts(&secret, &unpruned.secret.parts, &expectations.parts));
+ } else if let Some(best) = dists.best_distribution_for_reencryption(&config.prefer_identities) {
+ let new_owners = reencrypt_targets.clone();
+ let mut reencrypt_targets = reencrypt_targets;
+ reencrypt_targets.extend(best.owners().cloned());
- let mut parts = expectations.parts.clone();
+ let mut preferred = best.owners().collect_vec();
+ preferred.sort_by_key(|v| !config.prefer_identities.contains(*v));
+
+ warn!("reencrypting secret {secret} as it is missing for host {host}");
- let mut out = HashMap::new();
- for (part_name, part) in &existing.secret.parts {
- let Some(definition) = parts.remove(part_name) else {
- warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");
- continue;
+ for owner in preferred {
+ if let Some(hostname) = owner.as_host() && let Ok(host) = config.host(hostname) {
+ let best = best.clone();
+ let reencrypt_targets = reencrypt_targets.clone();
+ let reencrypted = match await_in_nix(async move {
+ host.reencrypt_distribution(&best, reencrypt_targets.clone(), config.now).await
+ }) {
+ Ok(r) => r,
+ Err(e) => {
+ warn!("reencryption failed on {hostname}: {e:?}");
+ continue;
+ }
+ };
+ dists.extend(reencrypted.clone(), format!("secret was reencrypted to extend with new owners: {new_owners:?}"));
+ return Ok(secret_to_parts(&secret, &reencrypted.secret.parts, &expectations.parts));
+ };
+ }
+ warn!("failed to reencrypt using any host")
};
- assert!(definition.encrypted != part.raw.encrypted, "encryption status is checked by secret_needs_regeneration");
- out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));
- }
- assert!(parts.is_empty(), "secret part is missing, secret_needs_regeneration should check that");
+ };
- return Ok(Value::new_attrs(out))
- };
+ if constraints.allow_different {
+ for dist in dists.distributions() {
+ for own in dist.owners() {
+ expectations.owners.remove(own);
+ }
+ }
+ }
+ info!("secret {secret} is being generated for {:?}", expectations.owners);
- todo!()
+ let expectations_ = expectations.clone();
+ let generated = await_in_nix(async move {
+ generate(config, expectations_, &generator, &default_generator_drv).await
+ })?;
+ dists.extend(generated.clone(), format!("secret was generated"));
+ return Ok(secret_to_parts(&secret, &generated.secret.parts, &expectations.parts));
},
)
.register();
crates/fleet-base/src/secret.rsdiffbeforeafterboth--- a/crates/fleet-base/src/secret.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use std::collections::{BTreeMap, BTreeSet};
-
-use chrono::{DateTime, Utc};
-
-use crate::fleetdata::{Expectations, FleetSecretData, FleetSecretDistribution, GeneratorPart};
-
-#[derive(thiserror::Error, Debug)]
-pub enum RegenerationReason {
- #[error("owners added: {0:?}")]
- OwnersAdded(BTreeSet<String>),
- #[error("owners added: {0:?}")]
- OwnersRemoved(BTreeSet<String>),
- #[error("unexpected generation data, expected: {expected:?}, found: {found:?}")]
- GenerationData {
- expected: serde_json::Value,
- found: serde_json::Value,
- },
- #[error("unexpected part list, expected: {expected:?}, found: {found:?}")]
- PartList {
- expected: BTreeSet<String>,
- found: BTreeSet<String>,
- },
- #[error("part {0} is expected to be encrypted")]
- ExpectedPrivate(String),
- #[error("part {0} is not expected to be encrypted")]
- ExpectedPublic(String),
- #[error("secret is expired at {0}")]
- Expired(DateTime<Utc>),
-
- #[error("secret is not generated for this host")]
- Missing,
-}
-
-pub fn secret_needs_regeneration(
- secret: &FleetSecretDistribution,
- expectations: &Expectations,
-) -> Option<RegenerationReason> {
- let added: BTreeSet<String> = expectations
- .owners
- .difference(&secret.owners)
- .cloned()
- .collect();
- if !added.is_empty() {
- return Some(RegenerationReason::OwnersAdded(added));
- }
-
- let removed: BTreeSet<String> = secret
- .owners
- .difference(&expectations.owners)
- .cloned()
- .collect();
- if !removed.is_empty() {
- return Some(RegenerationReason::OwnersRemoved(removed));
- }
-
- if secret.secret.generation_data != expectations.generation_data {
- return Some(RegenerationReason::GenerationData {
- expected: expectations.generation_data.clone(),
- found: secret.secret.generation_data.clone(),
- });
- }
-
- let expected: BTreeSet<String> = expectations.parts.keys().cloned().collect();
- let found: BTreeSet<String> = secret.secret.parts.keys().cloned().collect();
-
- if found != expected {
- return Some(RegenerationReason::PartList { expected, found });
- }
-
- for (name, value) in secret.secret.parts.iter() {
- let expectation = expectations
- .parts
- .get(name)
- .expect("found == expected checked");
- if value.raw.encrypted {
- if !expectation.encrypted {
- return Some(RegenerationReason::ExpectedPrivate(name.clone()));
- }
- } else if expectation.encrypted {
- return Some(RegenerationReason::ExpectedPublic(name.clone()));
- }
- }
-
- if let Some(expiration) = secret.secret.expires_at {
- // TODO: Leeway?
- if expiration < Utc::now() {
- return Some(RegenerationReason::Expired(expiration));
- }
- }
-
- None
-}
crates/fleet-shared/Cargo.tomldiffbeforeafterboth--- a/crates/fleet-shared/Cargo.toml
+++ b/crates/fleet-shared/Cargo.toml
@@ -8,4 +8,3 @@
base64 = "0.22.1"
serde = "1.0.219"
unicode_categories = "0.1.1"
-z85 = "3.0.6"
crates/fleet-shared/src/encoding.rsdiffbeforeafterboth--- a/crates/fleet-shared/src/encoding.rs
+++ b/crates/fleet-shared/src/encoding.rs
@@ -1,5 +1,4 @@
use std::{
- collections::BTreeMap,
fmt::{self, Display},
str::FromStr,
};
@@ -15,7 +14,6 @@
}
const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";
-const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";
// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.
const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";
const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";
@@ -54,18 +52,12 @@
STANDARD_NO_PAD
.decode(unprefixed.replace(['\n', '\t', ' '], ""))
.map_err(|e| format!("base64-encoded failed: {e}"))?
- } else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {
- z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))
- .map_err(|e| format!("z85-encoded failed: {e}"))?
} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {
unprefixed.as_bytes().to_owned()
} else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {
unprefixed.as_bytes().to_owned()
} else {
- let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");
- return Err(format!(
- "unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"
- ));
+ return Err(format!("unknown secret encoding"));
};
Ok(Self { data, encrypted })
}
crates/nix-eval/Cargo.tomldiffbeforeafterboth--- a/crates/nix-eval/Cargo.toml
+++ b/crates/nix-eval/Cargo.toml
@@ -18,6 +18,7 @@
test-log = { version = "0.2.18", features = ["trace"] }
tracing-indicatif = { version = "0.3.13", optional = true }
vte = { version = "0.15.0", features = ["ansi"] }
+tokio.workspace = true
[build-dependencies]
bindgen = "0.72.0"
crates/nix-eval/src/lib.rsdiffbeforeafterboth1use std::borrow::Cow;2use std::cell::RefCell;3use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};4use std::ptr::{null, null_mut};5use std::sync::LazyLock;6use std::{array, fmt, slice};7use std::{collections::HashMap, path::PathBuf};89use anyhow::{Context, anyhow, bail};10use itertools::Itertools;11use serde::Serialize;12use serde::de::DeserializeOwned;1314pub use anyhow::Result;15use tracing::{info, instrument, warn};1617use self::logging::{ErrorInfoBuilder, nix_logging_cxx};18use self::nix_cxx::set_fetcher_setting;19use self::nix_raw::{20 BindingsBuilder as c_bindings_builder, EvalState as c_eval_state, GC_SUCCESS,21 GC_allow_register_threads, GC_get_stack_base, GC_register_my_thread, GC_stack_base,22 GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp,23 PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value,24 bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free,25 clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW,26 err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build,27 eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,28 expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,29 flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,30 flake_reference_and_fragment_from_string, flake_reference_parse_flags,31 flake_reference_parse_flags_free, flake_reference_parse_flags_new,32 flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free,33 flake_settings_new, gc_now as gc_now_raw, get_attr_byname, get_attr_name_byidx, get_attrs_size,34 get_list_byidx, get_list_size, get_string, get_type, has_attr_byname, init_bool, init_int,35 init_primop, init_string, libexpr_init, libstore_init, libutil_init, list_builder_free,36 list_builder_insert, locked_flake, locked_flake_free, locked_flake_get_output_attrs,37 make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,38 realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,39 realised_string_get_store_path, realised_string_get_store_path_count, register_primop,40 set_err_msg, setting_set, state_free, store_open, store_parse_path, store_path_free,41 store_path_name, string_realise, value, value_call, value_decref, value_force, value_incref,42};4344// Contains macros helpers45pub mod logging;46#[doc(hidden)]47pub mod macros;48pub mod util;4950#[allow(51 non_upper_case_globals,52 non_camel_case_types,53 non_snake_case,54 dead_code55)]56mod nix_raw {57 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));58}59#[cxx::bridge]60pub mod nix_cxx {61 unsafe extern "C++" {62 type nix_fetchers_settings;63 include!("nix-eval/src/lib.hh");6465 #[allow(clippy::missing_safety_doc)]66 unsafe fn set_fetcher_setting(67 settings: *mut nix_fetchers_settings,68 setting: *const c_char,69 value: *const c_char,70 );71 }72}7374#[derive(Debug, PartialEq, Eq)]75pub enum NixType {76 Thunk,77 Int,78 Float,79 Bool,80 String,81 Path,82 Null,83 Attrs,84 List,85 Function,86 External,87}88impl NixType {89 fn from_int(c: c_uint) -> Self {90 match c {91 0 => Self::Thunk,92 1 => Self::Int,93 2 => Self::Float,94 3 => Self::Bool,95 4 => Self::String,96 5 => Self::Path,97 6 => Self::Null,98 7 => Self::Attrs,99 8 => Self::List,100 9 => Self::Function,101 10 => Self::External,102 _ => unreachable!("unknown nix type: {c}"),103 }104 }105}106107enum FunctorKind {108 Function,109 Functor,110}111112#[derive(Debug)]113#[repr(i32)]114pub enum NixErrorKind {115 Unknown = err_NIX_ERR_UNKNOWN,116 Overflow = err_NIX_ERR_OVERFLOW,117 Key = err_NIX_ERR_KEY,118 Generic = err_NIX_ERR_NIX_ERROR,119}120impl NixErrorKind {121 fn from_int(v: c_int) -> Option<Self> {122 Some(match v {123 0 => return None,124 nix_raw::err_NIX_ERR_UNKNOWN => Self::Unknown,125 nix_raw::err_NIX_ERR_OVERFLOW => Self::Overflow,126 nix_raw::err_NIX_ERR_KEY => Self::Key,127 nix_raw::err_NIX_ERR_NIX_ERROR => Self::Generic,128 _ => {129 debug_assert!(false, "unexpected nix error kind: {v}");130 Self::Unknown131 }132 })133 }134}135136pub fn gc_now() {137 unsafe { gc_now_raw() };138}139140pub fn gc_register_my_thread() {141 assert_eq!(unsafe { GC_thread_is_registered() }, 0);142143 let mut sb = GC_stack_base {144 mem_base: null_mut(),145 };146 let r = unsafe { GC_get_stack_base(&mut sb) };147 if r as u32 != GC_SUCCESS {148 panic!("failed to get thread stack base");149 }150 unsafe { GC_register_my_thread(&sb) };151}152pub fn gc_unregister_my_thread() {153 assert_eq!(unsafe { GC_thread_is_registered() }, 1);154155 unsafe { GC_unregister_my_thread() };156}157158pub struct ThreadRegisterGuard {}159impl ThreadRegisterGuard {160 #[allow(clippy::new_without_default)]161 pub fn new() -> Self {162 gc_register_my_thread();163 Self {}164 }165}166impl Drop for ThreadRegisterGuard {167 fn drop(&mut self) {168 gc_unregister_my_thread();169 }170}171172#[repr(transparent)]173pub struct NixContext(*mut c_context);174impl NixContext {175 pub fn set_err(&mut self, err: NixErrorKind, msg: &CStr) {176 unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };177 }178 pub fn new() -> Self {179 let ctx = unsafe { c_context_create() };180 Self(ctx)181 }182 fn error_kind(&self) -> Option<NixErrorKind> {183 let code = unsafe { err_code(self.0) };184 NixErrorKind::from_int(code)185 }186 fn error<'t>(&self) -> Option<(Cow<'t, str>, Option<Box<ErrorInfoBuilder>>)> {187 if let NixErrorKind::Generic = self.error_kind()? {188 let ei = unsafe { logging::nix_logging_cxx::extract_error_info(self.0) };189 let mut err_out = String::new();190 unsafe {191 err_info_msg(192 null_mut(),193 self.0,194 Some(copy_nix_str),195 (&raw mut err_out).cast(),196 )197 };198 return Some((Cow::Owned(err_out), Some(ei)));199 };200201 // TODO: Can throw error (resulting in panic) if unable to retrieve error. Should be able to resolve by passing context as a first argument,202 // but it looks ugly203 let str = unsafe { err_msg(null_mut(), self.0, null_mut()) };204 Some((unsafe { CStr::from_ptr(str) }.to_string_lossy(), None))205 }206 fn clean_err(&mut self) {207 unsafe {208 clear_err(self.0);209 }210 }211212 fn bail_if_error(&self) -> Result<()> {213 if let Some((err, stack)) = self.error() {214 let mut e = Err(anyhow!("{err}"));215 if let Some(stack) = stack {216 for ele in stack.stack_frames {217 e = e.with_context(|| {218 if ele.pos.is_empty() {219 ele.msg220 } else {221 format!("{} at {}", ele.msg, ele.pos)222 }223 })224 }225 }226 return e.context("<nix frames>");227 };228 Ok(())229 }230231 fn run_in_context<T>(&mut self, f: impl FnOnce(*mut c_context) -> T) -> Result<T> {232 self.clean_err();233 let o = f(self.0);234 self.bail_if_error()?;235 self.clean_err();236 Ok(o)237 }238}239240impl Default for NixContext {241 fn default() -> Self {242 Self::new()243 }244}245impl Drop for NixContext {246 fn drop(&mut self) {247 unsafe {248 c_context_free(self.0);249 }250 }251}252struct GlobalState {253 // Store should be valid as long as EvalState is valid254 #[allow(dead_code)]255 store: Store,256 state: EvalState,257}258impl GlobalState {259 fn new() -> Result<Self> {260 let mut ctx = NixContext::new();261 let store = ctx262 .run_in_context(|c| unsafe { store_open(c, c"auto".as_ptr(), null_mut()) })263 .map(Store)?;264265 let builder = ctx.run_in_context(|c| unsafe { eval_state_builder_new(c, store.0) })?;266 ctx.run_in_context(|c| unsafe { eval_state_builder_load(c, builder) })?;267 ctx.run_in_context(|c| unsafe {268 eval_state_builder_set_eval_setting(269 c,270 builder,271 c"lazy-trees".as_ptr(),272 c"true".as_ptr(),273 )274 })?;275 ctx.run_in_context(|c| unsafe {276 eval_state_builder_set_eval_setting(277 c,278 builder,279 c"lazy-locks".as_ptr(),280 c"true".as_ptr(),281 )282 })?;283 let state = ctx284 .run_in_context(|c| unsafe { eval_state_build(c, builder) })285 .map(EvalState)?;286287 Ok(Self { store, state })288 }289}290291struct ThreadState {292 ctx: NixContext,293}294impl ThreadState {295 fn new() -> Result<Self> {296 let ctx = NixContext::new();297298 Ok(Self { ctx })299 }300}301302static GLOBAL_STATE: LazyLock<GlobalState> = LazyLock::new(|| {303 info!("initializing nix global state");304 GlobalState::new().expect("global state init shouldn't fail")305});306307thread_local! {308 static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));309}310fn with_default_context<T>(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result<T> {311 let global = &GLOBAL_STATE.state;312 let (ctx, state) = THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.0));313 let mut ctx = NixContext(ctx);314 let v = ctx.run_in_context(|c| f(c, state));315 // It is reused for thread316 std::mem::forget(ctx);317 v318}319320pub fn set_setting(s: &CStr, v: &CStr) -> Result<()> {321 with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ())322}323324pub struct FetchSettings(*mut fetchers_settings);325impl FetchSettings {326 pub fn new() -> Self {327 Self::try_new().expect("allocation should not fail")328 }329 fn try_new() -> Result<Self> {330 with_default_context(|c, _| unsafe { fetchers_settings_new(c) }).map(Self)331 }332 pub fn set(&mut self, setting: &CStr, value: &CStr) {333 unsafe {334 set_fetcher_setting(self.0.cast(), setting.as_ptr(), value.as_ptr());335 };336 }337}338unsafe impl Send for FetchSettings {}339unsafe impl Sync for FetchSettings {}340341impl Default for FetchSettings {342 fn default() -> Self {343 Self::new()344 }345}346347impl Drop for FetchSettings {348 fn drop(&mut self) {349 unsafe { fetchers_settings_free(self.0) };350 }351}352pub struct FlakeSettings(*mut flake_settings);353impl FlakeSettings {354 pub fn new() -> Result<Self> {355 with_default_context(|c, _| unsafe { flake_settings_new(c) }).map(Self)356 }357}358unsafe impl Send for FlakeSettings {}359unsafe impl Sync for FlakeSettings {}360impl Drop for FlakeSettings {361 fn drop(&mut self) {362 unsafe {363 flake_settings_free(self.0);364 }365 }366}367368pub struct FlakeReferenceParseFlags(*mut flake_reference_parse_flags);369impl FlakeReferenceParseFlags {370 pub fn new(settings: &FlakeSettings) -> Result<Self> {371 with_default_context(|c, _| unsafe { flake_reference_parse_flags_new(c, settings.0) })372 .map(Self)373 }374 pub fn set_base_dir(&mut self, dir: &str) -> Result<()> {375 with_default_context(|c, _| {376 unsafe {377 flake_reference_parse_flags_set_base_directory(378 c,379 self.0,380 dir.as_ptr().cast(),381 dir.len(),382 )383 };384 })385 }386}387impl Drop for FlakeReferenceParseFlags {388 fn drop(&mut self) {389 unsafe {390 flake_reference_parse_flags_free(self.0);391 }392 }393}394pub struct FlakeLockFlags(*mut flake_lock_flags);395impl FlakeLockFlags {396 pub fn new(settings: &FlakeSettings) -> Result<Self> {397 let o = with_default_context(|c, _| unsafe { flake_lock_flags_new(c, settings.0) })398 .map(Self)?;399 // with_default_context(|c, _| unsafe { flake_lock_flags_set_mode_virtual(c, o.0) })?;400401 Ok(o)402 }403}404impl Drop for FlakeLockFlags {405 fn drop(&mut self) {406 unsafe {407 flake_lock_flags_free(self.0);408 }409 }410}411412unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) {413 let s = unsafe { slice::from_raw_parts(start.cast::<u8>(), n as usize) };414 let s = std::str::from_utf8(s).expect("c string has invalid utf-8");415 unsafe { *user_data.cast::<String>() = s.to_owned() };416}417418struct Store(*mut c_store);419unsafe impl Send for Store {}420unsafe impl Sync for Store {}421422impl Store {423 fn parse_path(&self, path: &CStr) -> Result<StorePath> {424 with_default_context(|c, _| {425 StorePath(unsafe { store_parse_path(c, self.0, path.as_ptr()) })426 })427 }428}429430#[repr(transparent)]431pub struct EvalState(*mut c_eval_state);432unsafe impl Send for EvalState {}433unsafe impl Sync for EvalState {}434435impl Drop for EvalState {436 fn drop(&mut self) {437 unsafe {438 state_free(self.0);439 }440 }441}442443pub struct FlakeReference(*mut flake_reference);444impl FlakeReference {445 #[instrument(name = "new-flake-reference", skip(flake, parse, fetch))]446 pub fn new(447 s: &str,448 flake: &FlakeSettings,449 parse: &FlakeReferenceParseFlags,450 fetch: &FetchSettings,451 ) -> Result<(Self, String)> {452 let mut out = null_mut();453 let mut fragment = String::new();454 // let fetch_settings = fetcher_settings;455 with_default_context(|c, _| unsafe {456 flake_reference_and_fragment_from_string(457 c,458 fetch.0,459 flake.0,460 parse.0,461 s.as_ptr().cast(),462 s.len(),463 &mut out,464 Some(copy_nix_str),465 (&raw mut fragment).cast(),466 )467 })?;468 assert!(!out.is_null());469470 Ok((Self(out), fragment))471 }472 #[instrument(name = "lock-flake", skip(self, fetch, flake, lock))]473 pub fn lock(474 &mut self,475 fetch: &FetchSettings,476 flake: &FlakeSettings,477 lock: &FlakeLockFlags,478 ) -> Result<LockedFlake> {479 with_default_context(|c, es| unsafe { flake_lock(c, fetch.0, flake.0, es, lock.0, self.0) })480 .map(LockedFlake)481 }482}483unsafe impl Send for FlakeReference {}484unsafe impl Sync for FlakeReference {}485486pub struct LockedFlake(*mut locked_flake);487impl LockedFlake {488 pub fn get_attrs(&self, settings: &mut FlakeSettings) -> Result<Value> {489 with_default_context(|c, es| unsafe {490 locked_flake_get_output_attrs(c, settings.0, es, self.0)491 })492 .map(Value)493 }494}495unsafe impl Send for LockedFlake {}496unsafe impl Sync for LockedFlake {}497impl Drop for LockedFlake {498 fn drop(&mut self) {499 unsafe {500 locked_flake_free(self.0);501 };502 }503}504505type FieldName = [u8; 64];506fn init_field_name(v: &str) -> FieldName {507 let mut f = [0; 64];508 assert!(v.len() < 64, "max field name is 63 chars");509 assert!(510 v.bytes().all(|v| v != 0),511 "nul bytes are unsupported in field name"512 );513 f[0..v.len()].copy_from_slice(v.as_bytes());514 f515}516517pub struct RealisedString(*mut realised_string);518impl fmt::Debug for RealisedString {519 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {520 self.as_str().fmt(f)521 }522}523524impl RealisedString {525 pub fn as_str(&self) -> &str {526 let len = unsafe { realised_string_get_buffer_size(self.0) };527 let data: *const u8 = unsafe { realised_string_get_buffer_start(self.0) }.cast();528 let data = unsafe { slice::from_raw_parts(data, len) };529 std::str::from_utf8(data).expect("non-utf8 strings not supported")530 }531 pub fn path_count(&self) -> usize {532 unsafe { realised_string_get_store_path_count(self.0) }533 }534 pub fn path(&self, i: usize) -> String {535 assert!(i < self.path_count());536 let path = unsafe { realised_string_get_store_path(self.0, i) };537 let mut err_out = String::new();538 unsafe { store_path_name(path, Some(copy_nix_str), (&raw mut err_out).cast()) };539 err_out540 }541}542543unsafe impl Send for RealisedString {}544impl Drop for RealisedString {545 fn drop(&mut self) {546 unsafe { realised_string_free(self.0) }547 }548}549550#[repr(transparent)]551pub struct Value(*mut value);552553unsafe impl Send for Value {}554unsafe impl Sync for Value {}555556pub trait AsFieldName {557 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T>;558 fn to_field_name(&self) -> Result<String>;559}560impl AsFieldName for Value {561 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {562 let f = self.to_string()?;563 v(init_field_name(&f))564 }565 fn to_field_name(&self) -> Result<String> {566 self.to_string()567 }568}569impl<E> AsFieldName for E570where571 E: AsRef<str>,572{573 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {574 let f = self.as_ref();575 v(init_field_name(f))576 }577 fn to_field_name(&self) -> Result<String> {578 Ok(self.as_ref().to_owned())579 }580}581582struct AttrsBuilder(*mut c_bindings_builder);583impl AttrsBuilder {584 fn new(capacity: usize) -> Self {585 with_default_context(|c, es| unsafe { make_bindings_builder(c, es, capacity) })586 .map(Self)587 .expect("alloc should not fail")588 }589 fn insert(&mut self, k: &impl AsFieldName, v: Value) {590 k.as_field_name(|name| {591 with_default_context(|c, _| unsafe {592 bindings_builder_insert(c, self.0, name.as_ptr().cast(), v.0);593 // bindings_builder_insert doesn't do incref594 })595 })596 .expect("builder insert shouldn't fail");597 }598}599impl Drop for AttrsBuilder {600 fn drop(&mut self) {601 unsafe { bindings_builder_free(self.0) };602 }603}604605struct ListBuilder(*mut c_list_builder, c_uint);606impl ListBuilder {607 fn new(capacity: usize) -> Self {608 with_default_context(|c, es| unsafe { make_list_builder(c, es, capacity) })609 .map(|l| Self(l, 0))610 .expect("alloc should not fail")611 }612}613impl ListBuilder {614 fn push(&mut self, v: Value) {615 with_default_context(|c, _| unsafe {616 list_builder_insert(617 c,618 self.0,619 {620 let v = self.1;621 self.1 += 1;622 v623 },624 v.0,625 )626 })627 .expect("list insert shouldn't fail");628 }629}630impl Drop for ListBuilder {631 fn drop(&mut self) {632 unsafe { list_builder_free(self.0) };633 }634}635636impl Value {637 pub fn new_primop(v: NativeFn) -> Self {638 let out = Self::new_uninit();639 with_default_context(|c, _| unsafe { init_primop(c, out.0, v.0) })640 .expect("primop initialization should not fail");641 out642 }643 pub fn new_attrs(v: HashMap<&str, Value>) -> Self {644 let out = Self::new_uninit();645 let mut b = AttrsBuilder::new(v.len());646 for (k, v) in v {647 b.insert(&k, v);648 }649 with_default_context(|c, _| unsafe { make_attrs(c, out.0, b.0) })650 .expect("attrs initialization should not fail");651652 out653 }654 fn new_list<T: Into<Self>>(v: Vec<T>) -> Self {655 let out = Self::new_uninit();656 let mut b = ListBuilder::new(v.len());657 for v in v {658 b.push(v.into());659 }660 with_default_context(|c, _| unsafe { make_list(c, b.0, out.0) })661 .expect("list initialization should not fail");662663 out664 }665 fn new_uninit() -> Self {666 let out = with_default_context(|c, es| unsafe { alloc_value(c, es) })667 .expect("value allocation should not fail");668 Self(out)669 }670 pub fn new_str(v: &str) -> Self {671 let s = CString::new(v).expect("string should not contain NULs");672 let out = Self::new_uninit();673 // String is copied, `s` is free to be dropped674 with_default_context(|c, _| unsafe { init_string(c, out.0, s.as_ptr()) })675 .expect("string initialization should not fail");676 out677 }678 pub fn new_int(i: i64) -> Self {679 let out = Self::new_uninit();680 with_default_context(|c, _| unsafe { init_int(c, out.0, i) })681 .expect("int initialization should not fail");682 out683 }684 pub fn new_bool(v: bool) -> Self {685 let out = Self::new_uninit();686 with_default_context(|c, _| unsafe { init_bool(c, out.0, v) })687 .expect("bool initialization should not fail");688 out689 }690 // TODO: As far as I can see, there is no way to get Thunks from nix public C api, so this function is useless691 // fn force(&mut self, st: &mut EvalState) -> Result<()> {692 // with_default_context(|c, _| unsafe { value_force(c, st.0, self.0) })?;693 // Ok(())694 // }695 pub fn type_of(&self) -> NixType {696 let ty = with_default_context(|c, _| unsafe { get_type(c, self.0) })697 .expect("get_type should not fail");698 NixType::from_int(ty)699 }700 fn builtin_to_string(&self) -> Result<Self> {701 let builtin = Self::eval("builtins.toString")?;702 builtin.call(self.clone())703 }704 fn force(&mut self, s: *mut nix_raw::EvalState) -> Result<()> {705 with_default_context(|c, _| unsafe { value_force(c, s, self.0) })?;706 Ok(())707 }708 pub fn to_string(&self) -> Result<String> {709 let ty = self.type_of();710 if !matches!(ty, NixType::String) {711 bail!("unexpected type: {ty:?}, expected string");712 }713 let mut str_out = String::new();714 with_default_context(|c, _| unsafe {715 get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast())716 })?;717718 Ok(str_out)719 }720 pub fn to_realised_string(&self) -> Result<RealisedString> {721 with_default_context(|c, es| unsafe { string_realise(c, es, self.0, false) })722 .map(RealisedString)723724 // let store_paths = unsafe { nix_raw::realised_string_get_store_path_count(str) };725 // for i in 0..store_paths {726 // let store_path = unsafe { nix_raw::realised_string_get_store_path(str, i) };727 // nix_raw::store_path_name(store_path, callback, user_data);728 // }729 // dbg!(store_paths);730 // todo!();731 }732733 pub fn has_field(&self, field: &str) -> Result<bool> {734 if !matches!(self.type_of(), NixType::Attrs) {735 bail!("invalid type: expected attrs");736 }737738 let f = init_field_name(field);739 with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })740 }741 // pub fn derivation_path(&self) {742 // nix_raw::real743 // }744 pub fn list_fields(&self) -> Result<Vec<String>> {745 if !matches!(self.type_of(), NixType::Attrs) {746 bail!("invalid type: expected attrs");747 }748749 let len = with_default_context(|c, _| unsafe { get_attrs_size(c, self.0) })?;750 let mut out = Vec::with_capacity(len as usize);751752 for i in 0..len {753 let name =754 with_default_context(|c, es| unsafe { get_attr_name_byidx(c, self.0, es, i) })?;755 let c = unsafe { CStr::from_ptr(name) };756 out.push(c.to_str().expect("nix field names are utf-8").to_owned());757 }758 Ok(out)759 }760 pub fn get_elem(&self, v: usize) -> Result<Self> {761 if !matches!(self.type_of(), NixType::List) {762 bail!("invalid type: expected list");763 }764 let len = with_default_context(|c, _| unsafe { get_list_size(c, self.0) })? as usize;765 if v >= len {766 bail!("oob list get: {v} >= {len}");767 }768769 with_default_context(|c, es| unsafe { get_list_byidx(c, self.0, es, v as u32) }).map(Self)770 }771 pub fn attrs_update(self, other: Value /*, ignore_errors: bool*/) -> Result<Self> {772 let attrs_update_fn = Self::eval("a: b: a // b")?;773774 attrs_update_fn775 .call(self)?776 .call(other)777 .context("attrs update")778 }779 pub fn get_field(&self, name: impl AsFieldName) -> Result<Self> {780 if !matches!(self.type_of(), NixType::Attrs) {781 bail!("invalid type: expected attrs");782 }783784 name.as_field_name(|name| {785 with_default_context(|c, es| unsafe {786 get_attr_byname(c, self.0, es, name.as_ptr().cast())787 })788 .map(Self)789 })790 .with_context(|| format!("getting field {:?}", name.to_field_name()))791 }792 pub fn call(&self, v: Value) -> Result<Self> {793 let kind = self794 .functor_kind()795 .ok_or_else(|| anyhow!("can only call function or functor"))?;796797 let function = match kind {798 FunctorKind::Function => self.clone(),799 FunctorKind::Functor => {800 let f = self801 .get_field("__functor")802 .context("getting functor value")?;803 assert_eq!(804 f.type_of(),805 NixType::Function,806 "invalid functor encountered"807 );808 f809 }810 };811812 let out = Value::new_uninit();813 with_default_context(|c, es| unsafe { value_call(c, es, function.0, v.0, out.0) })?;814815 Ok(out)816 }817 pub fn eval(v: &str) -> Result<Self> {818 let s = CString::new(v).expect("expression shouldn't have internal NULs");819 let out = Self::new_uninit();820 with_default_context(|c, es| unsafe {821 expr_eval_from_string(c, es, s.as_ptr(), c"/root".as_ptr(), out.0)822 })?;823 Ok(out)824 }825 pub fn build(&self, output: &str) -> Result<PathBuf> {826 if !self.is_derivation() {827 bail!("expected derivation to build")828 }829 let output_name = self830 .get_field("outputName")831 .context("getting output name field")?832 .to_string()?;833 let v = if output_name != output {834 let out = self.get_field(output).context("getting target output")?;835 if !out.is_derivation() {836 bail!("unknown output: {output}");837 }838 out839 } else {840 self.clone()841 };842 // to_string here blocks until the path is built843 let s = v.builtin_to_string()?;844 let rs = s.to_realised_string()?;845 let drv_path = rs.as_str().to_owned();846 Ok(PathBuf::from(drv_path))847 }848 pub fn as_json<T: DeserializeOwned>(&self) -> Result<T> {849 let to_json = Self::eval("builtins.toJSON")?;850 let s = to_json.call(self.clone())?.to_string()?;851 Ok(serde_json::from_str(&s)?)852 }853 pub fn serialized<T: Serialize>(v: &T) -> Result<Self> {854 Self::eval(&nixlike::serialize(v)?)855 }856857 // Convert to string/evaluate derivations/etc858 // fn to_string_weak(&self) -> Result<String> {859 // // TODO: For now, it works exactly like to_string, see the comment for fn force()860 // self.to_string()861 // }862863 fn is_derivation(&self) -> bool {864 if !matches!(self.type_of(), NixType::Attrs) {865 return false;866 }867 let Some(ty) = self.get_field("type").ok() else {868 return false;869 };870 matches!(ty.to_string().as_deref(), Ok("derivation"))871 }872 fn functor_kind(&self) -> Option<FunctorKind> {873 match self.type_of() {874 NixType::Attrs => self875 .has_field("__functor")876 .expect("has_field shouldn't fail for attrs")877 .then_some(FunctorKind::Functor),878 NixType::Function => Some(FunctorKind::Function),879 _ => None,880 }881 }882 pub fn is_function(&self) -> bool {883 self.functor_kind().is_some()884 }885 pub fn is_null(&self) -> bool {886 matches!(self.type_of(), NixType::Null)887 }888 pub fn is_string(&self) -> bool {889 matches!(self.type_of(), NixType::String)890 }891 pub fn is_attrs(&self) -> bool {892 matches!(self.type_of(), NixType::Attrs)893 }894}895896impl From<String> for Value {897 fn from(value: String) -> Self {898 Value::new_str(&value)899 }900}901impl From<bool> for Value {902 fn from(value: bool) -> Self {903 Value::new_bool(value)904 }905}906impl From<&str> for Value {907 fn from(value: &str) -> Self {908 Value::new_str(value)909 }910}911impl<T> From<Vec<T>> for Value912where913 T: Into<Value>,914{915 fn from(value: Vec<T>) -> Self {916 Value::new_list(value)917 }918}919920impl Clone for Value {921 fn clone(&self) -> Self {922 with_default_context(|c, _| unsafe { value_incref(c, self.0) })923 .expect("value incref should not fail");924 Self(self.0)925 }926}927impl Drop for Value {928 fn drop(&mut self) {929 with_default_context(|c, _| unsafe { value_decref(c, self.0) })930 .expect("value drop should not fail");931 }932}933934pub fn init_libraries() {935 unsafe { GC_allow_register_threads() };936937 let mut ctx = NixContext::new();938 ctx.run_in_context(|c| unsafe { libutil_init(c) })939 .expect("util init should not fail");940 ctx.run_in_context(|c| unsafe { libstore_init(c) })941 .expect("store init should not fail");942 ctx.run_in_context(|c| unsafe { libexpr_init(c) })943 .expect("expr init should not fail");944945 nix_logging_cxx::apply_tracing_logger();946}947948unsafe extern "C" fn nix_primop_closure_adapter<const N: usize>(949 user_data: *mut c_void,950 context: *mut c_context,951 state: *mut nix_raw::EvalState,952 args: *mut *mut value,953 ret: *mut value,954) {955 let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };956 let mut e = None;957 let args: [&Value; N] = array::from_fn(|i| {958 let v: &mut Value = unsafe { &mut *args.add(i).cast() };959960 info!("forcing arg");961 if matches!(v.type_of(), NixType::Thunk)962 && let Err(err) = v.force(state)963 {964 e = Some(err);965 };966 v as &Value967 });968 info!("args forced");969 let ctx: &mut NixContext = unsafe { &mut *context.cast() };970971 if let Some(e) = e {972 warn!("set err = {e}");973 unsafe { init_int(context, ret, 0) };974 return ctx.set_err(975 NixErrorKind::Unknown,976 &CString::new(e.to_string()).expect("forcing argument value failed"),977 );978 }979980 let state: &EvalState = unsafe { std::mem::transmute(&state) };981982 match user_closure(state, args) {983 Ok(v) => {984 unsafe { copy_value(context, ret, v.0) };985 }986 Err(e) => {987 unsafe { init_int(context, ret, 0) };988 warn!("set err = {e:#?}");989 ctx.set_err(990 NixErrorKind::Unknown,991 &CString::new(e.to_string()).expect("error should not contain internal nuls"),992 );993 }994 }995}996997type UserClosure<const N: usize> = Box<dyn Fn(&EvalState, [&Value; N]) -> Result<Value>>;998999pub struct NativeFn(*mut PrimOp);1000impl NativeFn {1001 pub fn new<const N: usize>(1002 name: &'static CStr,1003 doc: &'static CStr,1004 args: [&'static CStr; N],1005 f: impl Fn(&EvalState, [&Value; N]) -> Result<Value> + 'static,1006 ) -> Self {1007 // Double-boxing to make it thin pointer, as vtable gets outside of first Box1008 let closure: Box<UserClosure<N>> = Box::new(Box::new(f));1009 let f: PrimOpFun = Some(nix_primop_closure_adapter::<N>);1010 let mut args = args.into_iter().map(|v| v.as_ptr()).collect_vec();1011 args.push(null());1012 let args = args.as_mut_ptr();1013 let primop = unsafe {1014 alloc_primop(1015 null_mut(),1016 f,1017 N as i32,1018 name.as_ptr(),1019 args,1020 doc.as_ptr(),1021 Box::into_raw(closure).cast(),1022 )1023 };10241025 assert!(!primop.is_null(), "primop allocation should not fail");10261027 Self(primop)1028 }1029 pub fn register(self) {1030 unsafe { register_primop(null_mut(), self.0) };1031 }1032}10331034struct StorePath(*mut c_store_path);1035impl StorePath {}10361037impl Drop for StorePath {1038 fn drop(&mut self) {1039 unsafe { store_path_free(self.0) }1040 }1041}10421043#[test_log::test]1044fn test_native() -> Result<()> {1045 init_libraries();1046 NativeFn::new(1047 c"__uppercaseSuffix2",1048 c"make string uppercase and add suffix",1049 [c"str", c"suffix"],1050 |_, [str, suffix]: [&Value; 2]| {1051 let str = str.to_string()?;1052 let suffix = suffix.to_string()?;1053 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1054 },1055 )1056 .register();10571058 let mut fetch_settings = FetchSettings::new();1059 fetch_settings.set(c"warn-dirty", c"false");10601061 let manifest = format!("git+file://{}/../../", env!("CARGO_MANIFEST_DIR"));1062 let flake = FlakeSettings::new()?;1063 let parse = FlakeReferenceParseFlags::new(&flake)?;1064 let (mut r, _) = FlakeReference::new(&manifest, &flake, &parse, &fetch_settings)?;1065 let lock = FlakeLockFlags::new(&flake)?;1066 let locked = r.lock(&fetch_settings, &flake, &lock)?;1067 let attrs = locked.get_attrs(&mut FlakeSettings::new()?)?;10681069 let builtins = Value::eval("builtins")?;1070 assert_eq!(builtins.type_of(), NixType::Attrs);10711072 assert_eq!(attrs.type_of(), NixType::Attrs);1073 let test_data = nix_go!(attrs.testData);10741075 let test_string: String = nix_go_json!(test_data.testString);1076 assert_eq!(test_string, "hello");10771078 let s = nix_go!(attrs.packages["x86_64-linux"].fleet.drvPath);1079 let s = CString::new(s.to_string()?).expect("path str is cstring");10801081 let uppercase_suffix = Value::new_primop(NativeFn::new(1082 c"uppercase_suffix",1083 c"make string uppercase and add suffix",1084 [c"str", c"suffix"],1085 |es, [str, suffix]: [&Value; 2]| {1086 let str = str.to_string()?;1087 let suffix = suffix.to_string()?;1088 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1089 },1090 ));10911092 let test_result: String = nix_go_json!(test_data.testPrimop(uppercase_suffix));1093 assert_eq!(test_result, "PREFIX_BODY_SUFFIX");1094 let test_result: String = nix_go_json!(builtins.uppercaseSuffix2("test")("suffix"));1095 assert_eq!(test_result, "TESTsuffix");10961097 let nix_ctx = NixContext::new();1098 let store = GLOBAL_STATE.store.parse_path(s.as_c_str())?;10991100 // nix_raw::store_get_fs_closure(1);11011102 Ok(())1103}11041105// pub struct GcAlloc;1106// unsafe impl GlobalAlloc for GcAlloc {1107// unsafe fn alloc(&self, l: Layout) -> *mut u8 {1108// let ptr = unsafe { GC_malloc(l.size()) };1109// ptr.cast()1110// }1111// unsafe fn dealloc(&self, ptr: *mut u8, _: Layout) {1112// // unsafe { GC_free(ptr.cast()) };1113// }1114//1115// unsafe fn realloc(&self, ptr: *mut u8, _: Layout, new_size: usize) -> *mut u8 {1116// let ptr = unsafe { GC_realloc(ptr.cast(), new_size) };1117// ptr.cast()1118// }1119// }1120//1121// #[global_allocator]1122// static GC: GcAlloc = GcAlloc;1use std::borrow::Cow;2use std::cell::RefCell;3use std::ffi::{CStr, CString, c_char, c_int, c_uint, c_void};4use std::ptr::{null, null_mut};5use std::sync::{Arc, LazyLock, OnceLock};6use std::{array, fmt, slice};7use std::{collections::HashMap, path::PathBuf};89use anyhow::{Context, anyhow, bail};10use itertools::Itertools;11use serde::Serialize;12use serde::de::DeserializeOwned;13use std::mem::transmute;1415pub use anyhow::Result;16use tracing::{Instrument, info, instrument, warn};1718use self::logging::{ErrorInfoBuilder, nix_logging_cxx};19use self::nix_cxx::set_fetcher_setting;20use self::nix_raw::{21 BindingsBuilder as c_bindings_builder, EvalState as c_eval_state, GC_SUCCESS,22 GC_allow_register_threads, GC_get_stack_base, GC_register_my_thread, GC_stack_base,23 GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp,24 PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value,25 bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free,26 clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW,27 err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build,28 eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,29 expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,30 flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,31 flake_reference_and_fragment_from_string, flake_reference_parse_flags,32 flake_reference_parse_flags_free, flake_reference_parse_flags_new,33 flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free,34 flake_settings_new, gc_now as gc_now_raw, get_attr_byname, get_attr_name_byidx, get_attrs_size,35 get_list_byidx, get_list_size, get_string, get_type, has_attr_byname, init_bool, init_int,36 init_primop, init_string, libexpr_init, libstore_init, libutil_init, list_builder_free,37 list_builder_insert, locked_flake, locked_flake_free, locked_flake_get_output_attrs,38 make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,39 realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,40 realised_string_get_store_path, realised_string_get_store_path_count, register_primop,41 set_err_msg, setting_set, state_free, store_open, store_parse_path, store_path_free,42 store_path_name, string_realise, value, value_call, value_decref, value_force, value_incref,43};4445// Contains macros helpers46pub mod logging;47#[doc(hidden)]48pub mod macros;49pub mod util;5051#[allow(52 non_upper_case_globals,53 non_camel_case_types,54 non_snake_case,55 dead_code56)]57mod nix_raw {58 include!(concat!(env!("OUT_DIR"), "/bindings.rs"));59}60#[cxx::bridge]61pub mod nix_cxx {62 unsafe extern "C++" {63 type nix_fetchers_settings;64 include!("nix-eval/src/lib.hh");6566 #[allow(clippy::missing_safety_doc)]67 unsafe fn set_fetcher_setting(68 settings: *mut nix_fetchers_settings,69 setting: *const c_char,70 value: *const c_char,71 );72 }73}7475#[derive(Debug, PartialEq, Eq)]76pub enum NixType {77 Thunk,78 Int,79 Float,80 Bool,81 String,82 Path,83 Null,84 Attrs,85 List,86 Function,87 External,88}89impl NixType {90 fn from_int(c: c_uint) -> Self {91 match c {92 0 => Self::Thunk,93 1 => Self::Int,94 2 => Self::Float,95 3 => Self::Bool,96 4 => Self::String,97 5 => Self::Path,98 6 => Self::Null,99 7 => Self::Attrs,100 8 => Self::List,101 9 => Self::Function,102 10 => Self::External,103 _ => unreachable!("unknown nix type: {c}"),104 }105 }106}107108enum FunctorKind {109 Function,110 Functor,111}112113#[derive(Debug)]114#[repr(i32)]115pub enum NixErrorKind {116 Unknown = err_NIX_ERR_UNKNOWN,117 Overflow = err_NIX_ERR_OVERFLOW,118 Key = err_NIX_ERR_KEY,119 Generic = err_NIX_ERR_NIX_ERROR,120}121impl NixErrorKind {122 fn from_int(v: c_int) -> Option<Self> {123 Some(match v {124 0 => return None,125 nix_raw::err_NIX_ERR_UNKNOWN => Self::Unknown,126 nix_raw::err_NIX_ERR_OVERFLOW => Self::Overflow,127 nix_raw::err_NIX_ERR_KEY => Self::Key,128 nix_raw::err_NIX_ERR_NIX_ERROR => Self::Generic,129 _ => {130 debug_assert!(false, "unexpected nix error kind: {v}");131 Self::Unknown132 }133 })134 }135}136137pub fn gc_now() {138 unsafe { gc_now_raw() };139}140141pub fn gc_register_my_thread() {142 assert_eq!(unsafe { GC_thread_is_registered() }, 0);143144 let mut sb = GC_stack_base {145 mem_base: null_mut(),146 };147 let r = unsafe { GC_get_stack_base(&mut sb) };148 if r as u32 != GC_SUCCESS {149 panic!("failed to get thread stack base");150 }151 unsafe { GC_register_my_thread(&sb) };152}153pub fn gc_unregister_my_thread() {154 assert_eq!(unsafe { GC_thread_is_registered() }, 1);155156 unsafe { GC_unregister_my_thread() };157}158159pub struct ThreadRegisterGuard {}160impl ThreadRegisterGuard {161 #[allow(clippy::new_without_default)]162 pub fn new() -> Self {163 gc_register_my_thread();164 Self {}165 }166}167impl Drop for ThreadRegisterGuard {168 fn drop(&mut self) {169 gc_unregister_my_thread();170 }171}172173#[repr(transparent)]174pub struct NixContext(*mut c_context);175impl NixContext {176 pub fn set_err_raw(&mut self, err: NixErrorKind, msg: &CStr) {177 unsafe { set_err_msg(self.0, err as c_int, msg.as_ptr()) };178 }179 pub fn set_err(&mut self, err: anyhow::Error) {180 let mut fmt = format!("{err:?}").replace("\0", "\\0");181 self.set_err_raw(182 NixErrorKind::Generic,183 &CString::new(fmt).expect("NUL bytes were just replaced"),184 );185 }186 pub fn new() -> Self {187 let ctx = unsafe { c_context_create() };188 Self(ctx)189 }190 fn error_kind(&self) -> Option<NixErrorKind> {191 let code = unsafe { err_code(self.0) };192 NixErrorKind::from_int(code)193 }194 fn error<'t>(&self) -> Option<(Cow<'t, str>, Option<Box<ErrorInfoBuilder>>)> {195 if let NixErrorKind::Generic = self.error_kind()? {196 let ei = unsafe { logging::nix_logging_cxx::extract_error_info(self.0) };197 let mut err_out = String::new();198 unsafe {199 err_info_msg(200 null_mut(),201 self.0,202 Some(copy_nix_str),203 (&raw mut err_out).cast(),204 )205 };206 return Some((Cow::Owned(err_out), Some(ei)));207 };208209 // TODO: Can throw error (resulting in panic) if unable to retrieve error. Should be able to resolve by passing context as a first argument,210 // but it looks ugly211 let str = unsafe { err_msg(null_mut(), self.0, null_mut()) };212 Some((unsafe { CStr::from_ptr(str) }.to_string_lossy(), None))213 }214 fn clean_err(&mut self) {215 unsafe {216 clear_err(self.0);217 }218 }219220 fn bail_if_error(&self) -> Result<()> {221 if let Some((err, stack)) = self.error() {222 let mut e = Err(anyhow!("{err}"));223 if let Some(stack) = stack {224 for ele in stack.stack_frames {225 e = e.with_context(|| {226 if ele.pos.is_empty() {227 ele.msg228 } else {229 format!("{} at {}", ele.msg, ele.pos)230 }231 })232 }233 }234 return e.context("<nix frames>");235 };236 Ok(())237 }238239 fn run_in_context<T>(&mut self, f: impl FnOnce(*mut c_context) -> T) -> Result<T> {240 self.clean_err();241 let o = f(self.0);242 self.bail_if_error()?;243 self.clean_err();244 Ok(o)245 }246}247248impl Default for NixContext {249 fn default() -> Self {250 Self::new()251 }252}253impl Drop for NixContext {254 fn drop(&mut self) {255 unsafe {256 c_context_free(self.0);257 }258 }259}260struct GlobalState {261 // Store should be valid as long as EvalState is valid262 #[allow(dead_code)]263 store: Store,264 state: EvalState,265}266impl GlobalState {267 fn new() -> Result<Self> {268 let mut ctx = NixContext::new();269 let store = ctx270 .run_in_context(|c| unsafe { store_open(c, c"auto".as_ptr(), null_mut()) })271 .map(Store)?;272273 let builder = ctx.run_in_context(|c| unsafe { eval_state_builder_new(c, store.0) })?;274 ctx.run_in_context(|c| unsafe { eval_state_builder_load(c, builder) })?;275 ctx.run_in_context(|c| unsafe {276 eval_state_builder_set_eval_setting(277 c,278 builder,279 c"lazy-trees".as_ptr(),280 c"true".as_ptr(),281 )282 })?;283 ctx.run_in_context(|c| unsafe {284 eval_state_builder_set_eval_setting(285 c,286 builder,287 c"lazy-locks".as_ptr(),288 c"true".as_ptr(),289 )290 })?;291 let state = ctx292 .run_in_context(|c| unsafe { eval_state_build(c, builder) })293 .map(EvalState)?;294295 Ok(Self { store, state })296 }297}298299struct ThreadState {300 ctx: NixContext,301}302impl ThreadState {303 fn new() -> Result<Self> {304 let ctx = NixContext::new();305306 Ok(Self { ctx })307 }308}309310static GLOBAL_STATE: LazyLock<GlobalState> = LazyLock::new(|| {311 info!("initializing nix global state");312 GlobalState::new().expect("global state init shouldn't fail")313});314315thread_local! {316 static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));317}318fn with_default_context<T>(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result<T> {319 let global = &GLOBAL_STATE.state;320 let (ctx, state) = THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.0));321 let mut ctx = NixContext(ctx);322 let v = ctx.run_in_context(|c| f(c, state));323 // It is reused for thread324 std::mem::forget(ctx);325 v326}327328pub fn set_setting(s: &CStr, v: &CStr) -> Result<()> {329 with_default_context(|c, _| unsafe { setting_set(c, s.as_ptr(), v.as_ptr()) }).map(|_| ())330}331332pub struct FetchSettings(*mut fetchers_settings);333impl FetchSettings {334 pub fn new() -> Self {335 Self::try_new().expect("allocation should not fail")336 }337 fn try_new() -> Result<Self> {338 with_default_context(|c, _| unsafe { fetchers_settings_new(c) }).map(Self)339 }340 pub fn set(&mut self, setting: &CStr, value: &CStr) {341 unsafe {342 set_fetcher_setting(self.0.cast(), setting.as_ptr(), value.as_ptr());343 };344 }345}346unsafe impl Send for FetchSettings {}347unsafe impl Sync for FetchSettings {}348349impl Default for FetchSettings {350 fn default() -> Self {351 Self::new()352 }353}354355impl Drop for FetchSettings {356 fn drop(&mut self) {357 unsafe { fetchers_settings_free(self.0) };358 }359}360pub struct FlakeSettings(*mut flake_settings);361impl FlakeSettings {362 pub fn new() -> Result<Self> {363 with_default_context(|c, _| unsafe { flake_settings_new(c) }).map(Self)364 }365}366unsafe impl Send for FlakeSettings {}367unsafe impl Sync for FlakeSettings {}368impl Drop for FlakeSettings {369 fn drop(&mut self) {370 unsafe {371 flake_settings_free(self.0);372 }373 }374}375376pub struct FlakeReferenceParseFlags(*mut flake_reference_parse_flags);377impl FlakeReferenceParseFlags {378 pub fn new(settings: &FlakeSettings) -> Result<Self> {379 with_default_context(|c, _| unsafe { flake_reference_parse_flags_new(c, settings.0) })380 .map(Self)381 }382 pub fn set_base_dir(&mut self, dir: &str) -> Result<()> {383 with_default_context(|c, _| {384 unsafe {385 flake_reference_parse_flags_set_base_directory(386 c,387 self.0,388 dir.as_ptr().cast(),389 dir.len(),390 )391 };392 })393 }394}395impl Drop for FlakeReferenceParseFlags {396 fn drop(&mut self) {397 unsafe {398 flake_reference_parse_flags_free(self.0);399 }400 }401}402pub struct FlakeLockFlags(*mut flake_lock_flags);403impl FlakeLockFlags {404 pub fn new(settings: &FlakeSettings) -> Result<Self> {405 let o = with_default_context(|c, _| unsafe { flake_lock_flags_new(c, settings.0) })406 .map(Self)?;407 // with_default_context(|c, _| unsafe { flake_lock_flags_set_mode_virtual(c, o.0) })?;408409 Ok(o)410 }411}412impl Drop for FlakeLockFlags {413 fn drop(&mut self) {414 unsafe {415 flake_lock_flags_free(self.0);416 }417 }418}419420unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) {421 let s = unsafe { slice::from_raw_parts(start.cast::<u8>(), n as usize) };422 let s = std::str::from_utf8(s).expect("c string has invalid utf-8");423 unsafe { *user_data.cast::<String>() = s.to_owned() };424}425426struct Store(*mut c_store);427unsafe impl Send for Store {}428unsafe impl Sync for Store {}429430impl Store {431 fn parse_path(&self, path: &CStr) -> Result<StorePath> {432 with_default_context(|c, _| {433 StorePath(unsafe { store_parse_path(c, self.0, path.as_ptr()) })434 })435 }436}437438#[repr(transparent)]439pub struct EvalState(*mut c_eval_state);440unsafe impl Send for EvalState {}441unsafe impl Sync for EvalState {}442443impl Drop for EvalState {444 fn drop(&mut self) {445 unsafe {446 state_free(self.0);447 }448 }449}450451pub struct FlakeReference(*mut flake_reference);452impl FlakeReference {453 #[instrument(name = "new-flake-reference", skip(flake, parse, fetch))]454 pub fn new(455 s: &str,456 flake: &FlakeSettings,457 parse: &FlakeReferenceParseFlags,458 fetch: &FetchSettings,459 ) -> Result<(Self, String)> {460 let mut out = null_mut();461 let mut fragment = String::new();462 // let fetch_settings = fetcher_settings;463 with_default_context(|c, _| unsafe {464 flake_reference_and_fragment_from_string(465 c,466 fetch.0,467 flake.0,468 parse.0,469 s.as_ptr().cast(),470 s.len(),471 &mut out,472 Some(copy_nix_str),473 (&raw mut fragment).cast(),474 )475 })?;476 assert!(!out.is_null());477478 Ok((Self(out), fragment))479 }480 #[instrument(name = "lock-flake", skip(self, fetch, flake, lock))]481 pub fn lock(482 &mut self,483 fetch: &FetchSettings,484 flake: &FlakeSettings,485 lock: &FlakeLockFlags,486 ) -> Result<LockedFlake> {487 with_default_context(|c, es| unsafe { flake_lock(c, fetch.0, flake.0, es, lock.0, self.0) })488 .map(LockedFlake)489 }490}491unsafe impl Send for FlakeReference {}492unsafe impl Sync for FlakeReference {}493494pub struct LockedFlake(*mut locked_flake);495impl LockedFlake {496 pub fn get_attrs(&self, settings: &mut FlakeSettings) -> Result<Value> {497 with_default_context(|c, es| unsafe {498 locked_flake_get_output_attrs(c, settings.0, es, self.0)499 })500 .map(Value)501 }502}503unsafe impl Send for LockedFlake {}504unsafe impl Sync for LockedFlake {}505impl Drop for LockedFlake {506 fn drop(&mut self) {507 unsafe {508 locked_flake_free(self.0);509 };510 }511}512513type FieldName = [u8; 64];514fn init_field_name(v: &str) -> FieldName {515 let mut f = [0; 64];516 assert!(v.len() < 64, "max field name is 63 chars");517 assert!(518 v.bytes().all(|v| v != 0),519 "nul bytes are unsupported in field name"520 );521 f[0..v.len()].copy_from_slice(v.as_bytes());522 f523}524525pub struct RealisedString(*mut realised_string);526impl fmt::Debug for RealisedString {527 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {528 self.as_str().fmt(f)529 }530}531532impl RealisedString {533 pub fn as_str(&self) -> &str {534 let len = unsafe { realised_string_get_buffer_size(self.0) };535 let data: *const u8 = unsafe { realised_string_get_buffer_start(self.0) }.cast();536 let data = unsafe { slice::from_raw_parts(data, len) };537 std::str::from_utf8(data).expect("non-utf8 strings not supported")538 }539 pub fn path_count(&self) -> usize {540 unsafe { realised_string_get_store_path_count(self.0) }541 }542 pub fn path(&self, i: usize) -> String {543 assert!(i < self.path_count());544 let path = unsafe { realised_string_get_store_path(self.0, i) };545 let mut err_out = String::new();546 unsafe { store_path_name(path, Some(copy_nix_str), (&raw mut err_out).cast()) };547 err_out548 }549}550551unsafe impl Send for RealisedString {}552impl Drop for RealisedString {553 fn drop(&mut self) {554 unsafe { realised_string_free(self.0) }555 }556}557558#[repr(transparent)]559pub struct Value(*mut value);560561unsafe impl Send for Value {}562unsafe impl Sync for Value {}563564pub trait AsFieldName {565 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T>;566 fn to_field_name(&self) -> Result<String>;567}568impl AsFieldName for Value {569 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {570 let f = self.to_string()?;571 v(init_field_name(&f))572 }573 fn to_field_name(&self) -> Result<String> {574 self.to_string()575 }576}577impl<E> AsFieldName for E578where579 E: AsRef<str>,580{581 fn as_field_name<T>(&self, v: impl FnOnce(FieldName) -> Result<T>) -> Result<T> {582 let f = self.as_ref();583 v(init_field_name(f))584 }585 fn to_field_name(&self) -> Result<String> {586 Ok(self.as_ref().to_owned())587 }588}589590struct AttrsBuilder(*mut c_bindings_builder);591impl AttrsBuilder {592 fn new(capacity: usize) -> Self {593 with_default_context(|c, es| unsafe { make_bindings_builder(c, es, capacity) })594 .map(Self)595 .expect("alloc should not fail")596 }597 fn insert(&mut self, k: &impl AsFieldName, v: Value) {598 k.as_field_name(|name| {599 with_default_context(|c, _| unsafe {600 bindings_builder_insert(c, self.0, name.as_ptr().cast(), v.0);601 // bindings_builder_insert doesn't do incref602 })603 })604 .expect("builder insert shouldn't fail");605 }606}607impl Drop for AttrsBuilder {608 fn drop(&mut self) {609 unsafe { bindings_builder_free(self.0) };610 }611}612613struct ListBuilder(*mut c_list_builder, c_uint);614impl ListBuilder {615 fn new(capacity: usize) -> Self {616 with_default_context(|c, es| unsafe { make_list_builder(c, es, capacity) })617 .map(|l| Self(l, 0))618 .expect("alloc should not fail")619 }620}621impl ListBuilder {622 fn push(&mut self, v: Value) {623 with_default_context(|c, _| unsafe {624 list_builder_insert(625 c,626 self.0,627 {628 let v = self.1;629 self.1 += 1;630 v631 },632 v.0,633 )634 })635 .expect("list insert shouldn't fail");636 }637}638impl Drop for ListBuilder {639 fn drop(&mut self) {640 unsafe { list_builder_free(self.0) };641 }642}643644impl Value {645 pub fn new_primop(v: NativeFn) -> Self {646 let out = Self::new_uninit();647 with_default_context(|c, _| unsafe { init_primop(c, out.0, v.0) })648 .expect("primop initialization should not fail");649 out650 }651 pub fn new_attrs(v: HashMap<&str, Value>) -> Self {652 let out = Self::new_uninit();653 let mut b = AttrsBuilder::new(v.len());654 for (k, v) in v {655 b.insert(&k, v);656 }657 with_default_context(|c, _| unsafe { make_attrs(c, out.0, b.0) })658 .expect("attrs initialization should not fail");659660 out661 }662 fn new_list<T: Into<Self>>(v: Vec<T>) -> Self {663 let out = Self::new_uninit();664 let mut b = ListBuilder::new(v.len());665 for v in v {666 b.push(v.into());667 }668 with_default_context(|c, _| unsafe { make_list(c, b.0, out.0) })669 .expect("list initialization should not fail");670671 out672 }673 fn new_uninit() -> Self {674 let out = with_default_context(|c, es| unsafe { alloc_value(c, es) })675 .expect("value allocation should not fail");676 Self(out)677 }678 pub fn new_str(v: &str) -> Self {679 let s = CString::new(v).expect("string should not contain NULs");680 let out = Self::new_uninit();681 // String is copied, `s` is free to be dropped682 with_default_context(|c, _| unsafe { init_string(c, out.0, s.as_ptr()) })683 .expect("string initialization should not fail");684 out685 }686 pub fn new_int(i: i64) -> Self {687 let out = Self::new_uninit();688 with_default_context(|c, _| unsafe { init_int(c, out.0, i) })689 .expect("int initialization should not fail");690 out691 }692 pub fn new_bool(v: bool) -> Self {693 let out = Self::new_uninit();694 with_default_context(|c, _| unsafe { init_bool(c, out.0, v) })695 .expect("bool initialization should not fail");696 out697 }698 // TODO: As far as I can see, there is no way to get Thunks from nix public C api, so this function is useless699 // fn force(&mut self, st: &mut EvalState) -> Result<()> {700 // with_default_context(|c, _| unsafe { value_force(c, st.0, self.0) })?;701 // Ok(())702 // }703 pub fn type_of(&self) -> NixType {704 let ty = with_default_context(|c, _| unsafe { get_type(c, self.0) })705 .expect("get_type should not fail");706 NixType::from_int(ty)707 }708 fn builtin_to_string(&self) -> Result<Self> {709 let builtin = Self::eval("builtins.toString")?;710 builtin.call(self.clone())711 }712 fn force(&mut self, s: *mut nix_raw::EvalState) -> Result<()> {713 with_default_context(|c, _| unsafe { value_force(c, s, self.0) })?;714 Ok(())715 }716 pub fn to_string(&self) -> Result<String> {717 let ty = self.type_of();718 if !matches!(ty, NixType::String) {719 bail!("unexpected type: {ty:?}, expected string");720 }721 let mut str_out = String::new();722 with_default_context(|c, _| unsafe {723 get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast())724 })?;725726 Ok(str_out)727 }728 pub fn to_realised_string(&self) -> Result<RealisedString> {729 with_default_context(|c, es| unsafe { string_realise(c, es, self.0, false) })730 .map(RealisedString)731732 // let store_paths = unsafe { nix_raw::realised_string_get_store_path_count(str) };733 // for i in 0..store_paths {734 // let store_path = unsafe { nix_raw::realised_string_get_store_path(str, i) };735 // nix_raw::store_path_name(store_path, callback, user_data);736 // }737 // dbg!(store_paths);738 // todo!();739 }740741 pub fn has_field(&self, field: &str) -> Result<bool> {742 if !matches!(self.type_of(), NixType::Attrs) {743 bail!("invalid type: expected attrs");744 }745746 let f = init_field_name(field);747 with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })748 }749 // pub fn derivation_path(&self) {750 // nix_raw::real751 // }752 pub fn list_fields(&self) -> Result<Vec<String>> {753 if !matches!(self.type_of(), NixType::Attrs) {754 bail!("invalid type: expected attrs");755 }756757 let len = with_default_context(|c, _| unsafe { get_attrs_size(c, self.0) })?;758 let mut out = Vec::with_capacity(len as usize);759760 for i in 0..len {761 let name =762 with_default_context(|c, es| unsafe { get_attr_name_byidx(c, self.0, es, i) })?;763 let c = unsafe { CStr::from_ptr(name) };764 out.push(c.to_str().expect("nix field names are utf-8").to_owned());765 }766 Ok(out)767 }768 pub fn get_elem(&self, v: usize) -> Result<Self> {769 if !matches!(self.type_of(), NixType::List) {770 bail!("invalid type: expected list");771 }772 let len = with_default_context(|c, _| unsafe { get_list_size(c, self.0) })? as usize;773 if v >= len {774 bail!("oob list get: {v} >= {len}");775 }776777 with_default_context(|c, es| unsafe { get_list_byidx(c, self.0, es, v as u32) }).map(Self)778 }779 pub fn attrs_update(self, other: Value /*, ignore_errors: bool*/) -> Result<Self> {780 let attrs_update_fn = Self::eval("a: b: a // b")?;781782 attrs_update_fn783 .call(self)?784 .call(other)785 .context("attrs update")786 }787 pub fn get_field(&self, name: impl AsFieldName) -> Result<Self> {788 if !matches!(self.type_of(), NixType::Attrs) {789 bail!("invalid type: expected attrs");790 }791792 name.as_field_name(|name| {793 with_default_context(|c, es| unsafe {794 get_attr_byname(c, self.0, es, name.as_ptr().cast())795 })796 .map(Self)797 })798 .with_context(|| format!("getting field {:?}", name.to_field_name()))799 }800 pub fn call(&self, v: Value) -> Result<Self> {801 let kind = self802 .functor_kind()803 .ok_or_else(|| anyhow!("can only call function or functor"))?;804805 let function = match kind {806 FunctorKind::Function => self.clone(),807 FunctorKind::Functor => {808 let f = self809 .get_field("__functor")810 .context("getting functor value")?;811 assert_eq!(812 f.type_of(),813 NixType::Function,814 "invalid functor encountered"815 );816 f817 }818 };819820 let out = Value::new_uninit();821 with_default_context(|c, es| unsafe { value_call(c, es, function.0, v.0, out.0) })?;822823 Ok(out)824 }825 pub fn eval(v: &str) -> Result<Self> {826 let s = CString::new(v).expect("expression shouldn't have internal NULs");827 let out = Self::new_uninit();828 with_default_context(|c, es| unsafe {829 expr_eval_from_string(c, es, s.as_ptr(), c"/root".as_ptr(), out.0)830 })?;831 Ok(out)832 }833 pub fn build(&self, output: &str) -> Result<PathBuf> {834 if !self.is_derivation() {835 bail!("expected derivation to build")836 }837 let output_name = self838 .get_field("outputName")839 .context("getting output name field")?840 .to_string()?;841 let v = if output_name != output {842 let out = self.get_field(output).context("getting target output")?;843 if !out.is_derivation() {844 bail!("unknown output: {output}");845 }846 out847 } else {848 self.clone()849 };850 // to_string here blocks until the path is built851 let s = v.builtin_to_string()?;852 let rs = s.to_realised_string()?;853 let drv_path = rs.as_str().to_owned();854 Ok(PathBuf::from(drv_path))855 }856 pub fn as_json<T: DeserializeOwned>(&self) -> Result<T> {857 let to_json = Self::eval("builtins.toJSON")?;858 let s = to_json.call(self.clone())?.to_string()?;859 Ok(serde_json::from_str(&s)?)860 }861 pub fn serialized<T: Serialize>(v: &T) -> Result<Self> {862 Self::eval(&nixlike::serialize(v)?)863 }864865 // Convert to string/evaluate derivations/etc866 // fn to_string_weak(&self) -> Result<String> {867 // // TODO: For now, it works exactly like to_string, see the comment for fn force()868 // self.to_string()869 // }870871 fn is_derivation(&self) -> bool {872 if !matches!(self.type_of(), NixType::Attrs) {873 return false;874 }875 let Some(ty) = self.get_field("type").ok() else {876 return false;877 };878 matches!(ty.to_string().as_deref(), Ok("derivation"))879 }880 fn functor_kind(&self) -> Option<FunctorKind> {881 match self.type_of() {882 NixType::Attrs => self883 .has_field("__functor")884 .expect("has_field shouldn't fail for attrs")885 .then_some(FunctorKind::Functor),886 NixType::Function => Some(FunctorKind::Function),887 _ => None,888 }889 }890 pub fn is_function(&self) -> bool {891 self.functor_kind().is_some()892 }893 pub fn is_null(&self) -> bool {894 matches!(self.type_of(), NixType::Null)895 }896 pub fn is_string(&self) -> bool {897 matches!(self.type_of(), NixType::String)898 }899 pub fn is_attrs(&self) -> bool {900 matches!(self.type_of(), NixType::Attrs)901 }902}903904impl From<String> for Value {905 fn from(value: String) -> Self {906 Value::new_str(&value)907 }908}909impl From<bool> for Value {910 fn from(value: bool) -> Self {911 Value::new_bool(value)912 }913}914impl From<&str> for Value {915 fn from(value: &str) -> Self {916 Value::new_str(value)917 }918}919impl<T> From<Vec<T>> for Value920where921 T: Into<Value>,922{923 fn from(value: Vec<T>) -> Self {924 Value::new_list(value)925 }926}927928impl Clone for Value {929 fn clone(&self) -> Self {930 with_default_context(|c, _| unsafe { value_incref(c, self.0) })931 .expect("value incref should not fail");932 Self(self.0)933 }934}935impl Drop for Value {936 fn drop(&mut self) {937 with_default_context(|c, _| unsafe { value_decref(c, self.0) })938 .expect("value drop should not fail");939 }940}941942static TOKIO_FOR_NIX: OnceLock<Arc<tokio::runtime::Runtime>> = OnceLock::new();943944pub fn init_libraries() {945 unsafe { GC_allow_register_threads() };946947 let mut ctx = NixContext::new();948 ctx.run_in_context(|c| unsafe { libutil_init(c) })949 .expect("util init should not fail");950 ctx.run_in_context(|c| unsafe { libstore_init(c) })951 .expect("store init should not fail");952 ctx.run_in_context(|c| unsafe { libexpr_init(c) })953 .expect("expr init should not fail");954955 nix_logging_cxx::apply_tracing_logger();956}957958pub fn init_tokio_for_nix(tokio: Arc<tokio::runtime::Runtime>) {959 TOKIO_FOR_NIX960 .set(tokio)961 .expect("tokio for nix should only be initialized once");962}963964pub fn await_in_nix<F: Send + 'static>(f: impl Future<Output = F> + Send + 'static) -> F {965 // It should be possible to do Handle::current(), but some of the planned features don't work well with that966 let runtime = TOKIO_FOR_NIX967 .get()968 .expect("init_tokio_for_nix was not called");969 std::thread::spawn(move || runtime.block_on(f)).join().expect("await_in_nix inner thread panicked")970}971972unsafe extern "C" fn nix_primop_closure_adapter<const N: usize>(973 user_data: *mut c_void,974 mut context: *mut c_context,975 state: *mut nix_raw::EvalState,976 args: *mut *mut value,977 ret: *mut value,978) {979 let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };980 let args: [&Value; N] = array::from_fn(|i| {981 let v: &mut Value = unsafe { &mut *args.add(i).cast() };982 v as &Value983 });984 let ctx: &mut NixContext = unsafe { transmute(&mut context) };985986 let state: &EvalState = unsafe { std::mem::transmute(&state) };987988 match user_closure(state, args) {989 Ok(v) => {990 unsafe { copy_value(context, ret, v.0) };991 }992 Err(e) => {993 ctx.set_err(e);994 }995 }996}997998type UserClosure<const N: usize> = Box<dyn Fn(&EvalState, [&Value; N]) -> Result<Value>>;9991000pub struct NativeFn(*mut PrimOp);1001impl NativeFn {1002 pub fn new<const N: usize>(1003 name: &'static CStr,1004 doc: &'static CStr,1005 args: [&'static CStr; N],1006 f: impl Fn(&EvalState, [&Value; N]) -> Result<Value> + 'static,1007 ) -> Self {1008 // Double-boxing to make it thin pointer, as vtable gets outside of first Box1009 let closure: Box<UserClosure<N>> = Box::new(Box::new(f));1010 let f: PrimOpFun = Some(nix_primop_closure_adapter::<N>);1011 let mut args = args.into_iter().map(|v| v.as_ptr()).collect_vec();1012 args.push(null());1013 let args = args.as_mut_ptr();1014 let primop = unsafe {1015 alloc_primop(1016 null_mut(),1017 f,1018 N as i32,1019 name.as_ptr(),1020 args,1021 doc.as_ptr(),1022 Box::into_raw(closure).cast(),1023 )1024 };10251026 assert!(!primop.is_null(), "primop allocation should not fail");10271028 Self(primop)1029 }1030 pub fn register(self) {1031 unsafe { register_primop(null_mut(), self.0) };1032 }1033}10341035struct StorePath(*mut c_store_path);1036impl StorePath {}10371038impl Drop for StorePath {1039 fn drop(&mut self) {1040 unsafe { store_path_free(self.0) }1041 }1042}10431044#[test_log::test]1045fn test_native() -> Result<()> {1046 init_libraries();1047 NativeFn::new(1048 c"__uppercaseSuffix2",1049 c"make string uppercase and add suffix",1050 [c"str", c"suffix"],1051 |_, [str, suffix]: [&Value; 2]| {1052 let str = str.to_string()?;1053 let suffix = suffix.to_string()?;1054 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1055 },1056 )1057 .register();10581059 let mut fetch_settings = FetchSettings::new();1060 fetch_settings.set(c"warn-dirty", c"false");10611062 let manifest = format!("git+file://{}/../../", env!("CARGO_MANIFEST_DIR"));1063 let flake = FlakeSettings::new()?;1064 let parse = FlakeReferenceParseFlags::new(&flake)?;1065 let (mut r, _) = FlakeReference::new(&manifest, &flake, &parse, &fetch_settings)?;1066 let lock = FlakeLockFlags::new(&flake)?;1067 let locked = r.lock(&fetch_settings, &flake, &lock)?;1068 let attrs = locked.get_attrs(&mut FlakeSettings::new()?)?;10691070 let builtins = Value::eval("builtins")?;1071 assert_eq!(builtins.type_of(), NixType::Attrs);10721073 assert_eq!(attrs.type_of(), NixType::Attrs);1074 let test_data = nix_go!(attrs.testData);10751076 let test_string: String = nix_go_json!(test_data.testString);1077 assert_eq!(test_string, "hello");10781079 let s = nix_go!(attrs.packages["x86_64-linux"].fleet.drvPath);1080 let s = CString::new(s.to_string()?).expect("path str is cstring");10811082 let uppercase_suffix = Value::new_primop(NativeFn::new(1083 c"uppercase_suffix",1084 c"make string uppercase and add suffix",1085 [c"str", c"suffix"],1086 |es, [str, suffix]: [&Value; 2]| {1087 let str = str.to_string()?;1088 let suffix = suffix.to_string()?;1089 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1090 },1091 ));10921093 let test_result: String = nix_go_json!(test_data.testPrimop(uppercase_suffix));1094 assert_eq!(test_result, "PREFIX_BODY_SUFFIX");1095 let test_result: String = nix_go_json!(builtins.uppercaseSuffix2("test")("suffix"));1096 assert_eq!(test_result, "TESTsuffix");10971098 let nix_ctx = NixContext::new();1099 let store = GLOBAL_STATE.store.parse_path(s.as_c_str())?;11001101 // nix_raw::store_get_fs_closure(1);11021103 Ok(())1104}11051106// pub struct GcAlloc;1107// unsafe impl GlobalAlloc for GcAlloc {1108// unsafe fn alloc(&self, l: Layout) -> *mut u8 {1109// let ptr = unsafe { GC_malloc(l.size()) };1110// ptr.cast()1111// }1112// unsafe fn dealloc(&self, ptr: *mut u8, _: Layout) {1113// // unsafe { GC_free(ptr.cast()) };1114// }1115//1116// unsafe fn realloc(&self, ptr: *mut u8, _: Layout, new_size: usize) -> *mut u8 {1117// let ptr = unsafe { GC_realloc(ptr.cast(), new_size) };1118// ptr.cast()1119// }1120// }1121//1122// #[global_allocator]1123// static GC: GcAlloc = GcAlloc;crates/nix-eval/src/macros.rsdiffbeforeafterboth--- a/crates/nix-eval/src/macros.rs
+++ b/crates/nix-eval/src/macros.rs
@@ -68,13 +68,13 @@
#[macro_export]
macro_rules! nix_go {
(@o($o:expr, $path:expr) . $var:ident $($tt:tt)*) => {{
- nix_go!(@o($o.get_field(stringify!($var)).context(concat!("getting nested ", $path))?, $path) $($tt)*)
+ nix_go!(@o(tokio::task::block_in_place(|| $o.get_field(stringify!($var))).context(concat!("getting nested ", $path))?, $path) $($tt)*)
}};
(@o($o:expr, $path:expr) [ $v:expr ] $($tt:tt)*) => {{
- nix_go!(@o($o.get_field($v).context(concat!("getting nested ", $path))?, $path) $($tt)*)
+ nix_go!(@o(tokio::task::block_in_place(|| $o.get_field($v)).context(concat!("getting nested ", $path))?, $path) $($tt)*)
}};
(@o($o:expr, $path:expr) ($($var:tt)*) $($tt:tt)*) => {
- nix_go!(@o($o.call($crate::nix_expr_inner!($($var)+)).context(concat!("getting nested ", $path))?, $path) $($tt)*)
+ nix_go!(@o(tokio::task::block_in_place(|| $o.call($crate::nix_expr_inner!($($var)+))).context(concat!("getting nested ", $path))?, $path) $($tt)*)
};
(@o($o:expr, $path:expr)) => {$o};
($field:ident $($tt:tt)+) => {{
@@ -87,6 +87,6 @@
#[macro_export]
macro_rules! nix_go_json {
($($tt:tt)*) => {{
- $crate::nix_go!($($tt)*).as_json()?
+ tokio::task::block_in_place(|| $crate::nix_go!($($tt)*).as_json())?
}};
}
flake.lockdiffbeforeafterboth--- a/flake.lock
+++ b/flake.lock
@@ -2,10 +2,10 @@
"nodes": {
"crane": {
"locked": {
- "lastModified": 1767461147,
+ "lastModified": 1768700043,
"owner": "ipetkov",
"repo": "crane",
- "rev": "7d59256814085fd9666a2ae3e774dc5ee216b630",
+ "rev": "935de8bd6838d940988bb065be2a2034259327b9",
"type": "github"
},
"original": {
@@ -37,10 +37,10 @@
]
},
"locked": {
- "lastModified": 1767609335,
+ "lastModified": 1768135262,
"owner": "hercules-ci",
"repo": "flake-parts",
- "rev": "250481aafeb741edfe23d29195671c19b36b6dca",
+ "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
"type": "github"
},
"original": {
@@ -111,10 +111,10 @@
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
- "lastModified": 1767670640,
+ "lastModified": 1768702010,
"owner": "deltarocks",
"repo": "nix",
- "rev": "2181cd07134c9049bd77b7f48c3b1ea8647267de",
+ "rev": "b05b52670b9c7affff5b9be3edb539a1603c39e6",
"type": "github"
},
"original": {
@@ -126,10 +126,10 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1767657734,
+ "lastModified": 1768697925,
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "d4ccebf51ee4dbeb9df364dce1fe9848635c1258",
+ "rev": "665062f7df2c7db8fdbbec4f1b730091143828a3",
"type": "github"
},
"original": {
@@ -190,10 +190,10 @@
]
},
"locked": {
- "lastModified": 1767667566,
+ "lastModified": 1768617670,
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "056ce5b125ab32ffe78c7d3e394d9da44733c95e",
+ "rev": "56d0fbdd732f3686e8414b857cf885038fc17d57",
"type": "github"
},
"original": {
@@ -223,10 +223,10 @@
]
},
"locked": {
- "lastModified": 1767468822,
+ "lastModified": 1768158989,
"owner": "numtide",
"repo": "treefmt-nix",
- "rev": "d56486eb9493ad9c4777c65932618e9c2d0468fc",
+ "rev": "e96d59dff5c0d7fddb9d113ba108f03c3ef99eca",
"type": "github"
},
"original": {
modules/nixos.nixdiffbeforeafterboth--- a/modules/nixos.nix
+++ b/modules/nixos.nix
@@ -10,7 +10,7 @@
let
inherit (lib.attrsets) mapAttrs;
inherit (lib.options) mkOption;
- inherit (lib.types) deferredModule unspecified;
+ inherit (lib.types) deferredModule unspecified uniq str;
inherit (lib.strings) escapeNixIdentifier;
inherit (fleetLib.options) mkHostsOption;
@@ -24,9 +24,18 @@
'';
type = deferredModule;
};
- hosts = mkHostsOption (hostArgs: {
+ hosts = mkHostsOption (hostArgs: let
+ hostName = hostArgs.config._module.args.name;
+ in {
inherit _file;
options = {
+ name = mkOption {
+ description = ''
+ Host name (alias)
+ '';
+ type = uniq str;
+ default = hostName;
+ };
nixos = mkOption {
description = ''
Nixos configuration for the current host.
@@ -42,7 +51,7 @@
prefix = [
"fleetConfiguration"
"hosts"
- hostArgs.config._module.args.name
+ hostName
"nixos"
];
modules = (import "${modulesPath}/module-list.nix") ++ [
modules/nixos/secrets.nixdiffbeforeafterboth--- a/modules/nixos/secrets.nix
+++ b/modules/nixos/secrets.nix
@@ -15,7 +15,12 @@
inherit (lib.stringsWithDeps) stringAfter;
inherit (lib.options) mkOption literalExpression;
inherit (lib.lists) optional elem;
- inherit (lib.attrsets) mapAttrs mapAttrsToList;
+ inherit (lib.attrsets)
+ mapAttrs
+ mapAttrsToList
+ filterAttrs
+ attrNames
+ ;
inherit (lib.modules) mkIf;
inherit (lib.types)
submodule
@@ -25,9 +30,9 @@
uniq
functionTo
package
- bool
enum
either
+ listOf
;
inherit (fleetLib.strings) decodeRawSecret;
@@ -132,10 +137,33 @@
in
{
options = {
+ _providedSharedSecrets = mkOption {
+ description = ''
+ List of shared secrets, for which the current host was specified as `expectedOwners`
+ '';
+ type = listOf str;
+ default = [];
+ internal = true;
+ };
secrets = mkOption {
type = attrsOf secretType;
default = { };
- apply = mapAttrs (_: secret: secret.parts // { definition = secret; });
+ apply =
+ secrets:
+ mapAttrs (_: secret: secret.parts // { definition = secret; })
+
+ (
+ let
+ hostName = host.name;
+ expectedNonshared = attrNames (filterAttrs (_: def: def.generator != "shared") secrets);
+ expectedShared = config._providedSharedSecrets;
+ in
+ builtins.deepSeq [
+ hostName
+ expectedNonshared
+ expectedShared
+ ] (builtins.fleetEnsureHostSecrets hostName expectedNonshared expectedShared secrets)
+ );
description = "Host-local secrets";
};
system.secretsData = mkOption {
@@ -163,7 +191,7 @@
(secret.definition.generator == "shared") == hasSharedDefinition
&& (
hasSharedDefinition
- -> (elem host._module.args.name fleetConfiguration.secrets.${name}.expectedOwners)
+ -> (elem host.name fleetConfiguration.secrets.${name}.expectedOwners)
);
message =
if hasSharedDefinition then
modules/secrets.nixdiffbeforeafterboth--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -1,9 +1,10 @@
{
lib,
+ config,
...
}:
let
- inherit (lib.options) mkOption literalExpression;
+ inherit (lib.options) mkOption;
inherit (lib.types)
nullOr
listOf
@@ -16,6 +17,8 @@
uniq
;
inherit (lib.strings) concatStringsSep;
+ inherit (lib.lists) elem filter;
+ inherit (lib.attrsets) attrNames;
sharedSecret =
{ config, ... }:
@@ -30,28 +33,33 @@
regenerateOnOwnerAdded = mkOption {
type = bool;
description = ''
- Controls whether the secret must be regenerated when new owners are added.
+ Whether the secret prefers to be rotated when new owners are added.
- Set to true when the secret contains owner-specific references (e.g., X.509 Subject Alternative Names).
- When true, adding a new owner will trigger secret regeneration instead of simple re-encryption.
+ Note that this is only a security measure, if the secret needs to be regenerated due to e.g X.509 SANs
+ changes - then you most likely want to use generationData for that instead.
'';
+ default = false;
};
regenerateOnOwnerRemoved = mkOption {
- default = config.regenerateOnOwnerAdded;
- defaultText = literalExpression "regenerateOnOwnerAdded";
type = bool;
description = ''
- Determines secret behavior when owners are removed from the configuration.
-
- Typically mirrors regenerateOnOwnerAdded. Override cautiously.
- Set to false if host permissions are revoked through alternative mechanisms like firewall rules.
+ Whether the secret prefers to be rotated when the owners are removed, so the encrypted data
+ stored in fleet state can't be decrypted by those. Note that the secrets are still present in encrypted
+ form on those hosts until gc happens.
'';
+ default = false;
};
allowDifferent = mkOption {
type = bool;
description = ''
- When adding owner, do not update secret value for other owners, instead creating a new distribution
+ When adding owner, do not update secret value for other owners, instead creating a new distribution.
+
+ Defaults to true, since all secrets might differ on hosts on some point of deployment process.
+
+ Secret generator might also have opinion on this, like it makes little sense for askPass/synchronizing
+ generators to keep old data.
'';
+ default = true;
};
generator = mkOption {
type = uniq (nullOr (functionTo package));
@@ -75,6 +83,9 @@
};
};
config = {
+ nixos = {host, ...}: {
+ _providedSharedSecrets = filter (name: elem host.name config.secrets.${name}.expectedOwners) (attrNames config.secrets);
+ };
nixpkgs.overlays = [
(final: prev: {
mkSecretGenerators =
@@ -90,6 +101,7 @@
# (Some secrets-encryption-in-git/managed PKI solution is expected)
impureOn ? null,
generationData ? null,
+ allowDifferent ? true,
parts,
}:
(prev.writeShellScript "impureGenerator.sh" ''
@@ -118,7 +130,12 @@
'').overrideAttrs
(old: {
passthru = {
- inherit impureOn parts generationData;
+ inherit
+ impureOn
+ parts
+ generationData
+ allowDifferent
+ ;
generatorKind = "impure";
};
});