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;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";
};
});