difftreelog
refactor drop unnecessary async/await
in: trunk
15 files changed
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -30,9 +30,9 @@
async fn build_task(config: Config, hostname: String, build_attr: &str) -> Result<PathBuf> {
info!("building");
- let host = config.host(&hostname).await?;
+ let host = config.host(&hostname)?;
// let action = Action::from(self.subcommand.clone());
- let nixos = host.nixos_config().await?;
+ let nixos = host.nixos_config()?;
let drv = nix_go!(nixos.system.build[{ build_attr }]);
let out_output = spawn_blocking(move || drv.build("out"))
.await
@@ -59,7 +59,7 @@
impl BuildSystems {
pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
- let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
+ let hosts = opts.filter_skipped(config.list_hosts()?)?;
let set = LocalSet::new();
let build_attr = self.build_attr.clone();
for host in hosts {
@@ -95,20 +95,20 @@
impl Deploy {
pub async fn run(self, config: &Config, opts: &FleetOpts) -> Result<()> {
- let hosts = opts.filter_skipped(config.list_hosts().await?).await?;
+ let hosts = opts.filter_skipped(config.list_hosts()?)?;
let set = LocalSet::new();
for host in hosts.into_iter() {
let config = config.clone();
let span = info_span!("deploy", host = field::display(&host.name));
let hostname = host.name.clone();
let opts = opts.clone();
- if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind").await? {
+ if let Some(deploy_kind) = opts.action_attr::<DeployKind>(&host, "deploy_kind")? {
host.set_deploy_kind(deploy_kind);
};
- if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {
+ if let Some(destination) = opts.action_attr::<String>(&host, "dest")? {
host.set_session_destination(destination);
};
- if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {
+ if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store")? {
host.set_legacy_ssh_store(legacy);
};
@@ -153,7 +153,7 @@
self.action,
&host,
remote_path,
- match opts.action_attr(&host, "specialisation").await {
+ match opts.action_attr(&host, "specialisation") {
Ok(v) => v,
_ => {
error!("unreachable? failed to get specialization");
cmds/fleet/src/cmds/info.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -35,7 +35,7 @@
let mut data = Vec::new();
match self.cmd {
InfoCmd::ListHosts { ref tagged } => {
- 'host: for host in config.list_hosts().await? {
+ 'host: for host in config.list_hosts()? {
if !tagged.is_empty() {
let config = &config.config_field;
let host_name = &host.name;
@@ -59,7 +59,7 @@
"at leas one of --external or --internal must be set"
);
let mut out = <BTreeSet<String>>::new();
- let host = config.system_config(&host).await?;
+ let host = config.system_config(&host)?;
if external {
let data: Vec<String> = nix_go_json!(host.network.externalIps);
out.extend(data);
cmds/fleet/src/cmds/rollback.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/rollback.rs
+++ b/cmds/fleet/src/cmds/rollback.rs
@@ -75,7 +75,7 @@
impl RollbackSingle {
pub(crate) async fn run(&self, config: &Config, _opts: &FleetOpts) -> Result<()> {
- let host = config.host(&self.machine).await?;
+ let host = config.host(&self.machine)?;
match &self.action {
RollbackAction::ListTargets => {
let generations = list_all_generations(&host, config).await;
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -1,24 +1,16 @@
use std::{
- collections::{BTreeMap, BTreeSet, HashSet},
- io::{self, Read, Write, stdin, stdout},
+ collections::{BTreeSet, HashSet},
+ io::{Read, Write, stdin, stdout},
path::PathBuf,
};
-use anyhow::{Context, Result, anyhow, bail, ensure};
-use chrono::{DateTime, Utc};
+use anyhow::{Context, Result, bail, ensure};
use clap::Parser;
-use fleet_base::{
- fleetdata::{FleetSecretData, FleetSecretDistribution, FleetSecretPart, encrypt_secret_data},
- host::Config,
- opts::FleetOpts,
- secret::{Expectations, RegenerationReason, secret_needs_regeneration},
-};
+use fleet_base::{host::Config, opts::FleetOpts};
use fleet_shared::SecretData;
-use nix_eval::{NixType, Value, nix_go, nix_go_json};
-use serde::Deserialize;
-use tabled::{Table, Tabled};
-use tokio::{fs::read, task::spawn_blocking};
-use tracing::{Instrument, error, info, info_span, warn};
+use tabled::Tabled;
+use tokio::fs::read;
+use tracing::{info, info_span, warn};
#[derive(Parser)]
pub enum Secret {
@@ -145,13 +137,7 @@
}
*/
-#[derive(Deserialize)]
-#[serde(rename_all = "camelCase")]
-enum GeneratorKind {
- Impure,
- Pure,
-}
-
+/*
async fn generate_pure(
_config: &Config,
_display_name: &str,
@@ -315,6 +301,7 @@
}
}
}
+*/
/*
async fn generate_shared(
config: &Config,
@@ -421,8 +408,8 @@
todo!("part of fleet-pusher")
}
Secret::ForceKeys => {
- for host in config.list_hosts().await? {
- if opts.should_skip(&host).await? {
+ for host in config.list_hosts()? {
+ if opts.should_skip(&host)? {
continue;
}
config.key(&host.name).await?;
@@ -467,7 +454,7 @@
let Some(identity_holder) = identity_holder else {
bail!("no available holder found");
};
- let host = config.host(identity_holder).await?;
+ let host = config.host(identity_holder)?;
host.decrypt(part.raw.clone()).await?
} else {
part.raw.data.clone()
@@ -619,7 +606,7 @@
}
Secret::List {} => {
let _span = info_span!("loading secrets").entered();
- let configured = config.list_configured_shared().await?;
+ let configured = config.list_configured_shared()?;
#[derive(Tabled)]
struct SecretDisplay {
#[tabled(rename = "Name")]
@@ -662,7 +649,7 @@
.host_secret(&machine, &name)
.context("secret not found")?;
if let Some(data) = secret.secret.parts.get(&part) {
- let host = config.host(&machine).await?;
+ let host = config.host(&machine)?;
let secret = host.decrypt(data.raw.clone()).await?;
String::from_utf8(secret).context("secret is not utf8")?
} else if add {
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -216,13 +216,10 @@
.map(|a| extra_args::parse_os(&a))
.transpose()?
.unwrap_or_default();
- let config = opts
- .fleet_opts
- .build(
- nix_args,
- matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),
- )
- .await?;
+ let config = opts.fleet_opts.build(
+ nix_args,
+ matches!(opts.command, Opts::Deploy(_) | Opts::BuildSystems(_)),
+ )?;
match run_command(&config, opts.fleet_opts, opts.command).await {
Ok(()) => {
crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth--- a/crates/fleet-base/src/fleetdata.rs
+++ b/crates/fleet-base/src/fleetdata.rs
@@ -421,3 +421,14 @@
}
}
}
+
+#[derive(Debug)]
+pub struct Expectations {
+ pub owners: BTreeSet<String>,
+ pub generation_data: serde_json::Value,
+ pub parts: BTreeMap<String, GeneratorPart>,
+}
+#[derive(Deserialize, Debug, Clone)]
+pub struct GeneratorPart {
+ pub encrypted: bool,
+}
crates/fleet-base/src/host.rsdiffbeforeafterboth1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};16use openssh::{ControlPersist, SessionBuilder};17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24 command::MyCommand,25 fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},26};2728pub struct FleetConfigInternals {29 /// Fleet project directory, containing fleet.nix file.30 pub directory: PathBuf,31 /// builtins.currentSystem32 pub local_system: String,33 pub data: Arc<Mutex<FleetData>>,34 pub nix_args: Vec<OsString>,35 /// fleet_config.config36 pub config_field: Value,37 /// flake.output38 pub flake_outputs: Value,39 // TODO: Remove with connectivity refactor40 pub localhost: String,4142 /// import nixpkgs {system = local};43 pub default_pkgs: Value,44 /// inputs.nixpkgs45 pub nixpkgs: Value,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53 type Target = FleetConfigInternals;5455 fn deref(&self) -> &Self::Target {56 &self.057 }58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62 Sudo,63 Run0,64 Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69 /// NixOS => NixOS managed by fleet70 UpgradeToFleet,71 /// NixOS managed by fleet => NixOS managed by fleet72 Fleet,73 /// Remote host has /mnt, /mnt/boot mounted,74 /// generated config is added to fleet configuration.75 NixosInstall,76 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77 /// generated config is added to fleet configuration,78 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79 NixosLustrate,80}8182impl FromStr for DeployKind {83 type Err = anyhow::Error;84 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85 match s {86 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87 "fleet" => Ok(Self::Fleet),88 "nixos-install" => Ok(Self::NixosInstall),89 "nixos-lustrate" => Ok(Self::NixosLustrate),90 v => bail!(91 "unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""92 ),93 }94 }95}96pub struct ConfigHost {97 config: Config,98 pub name: String,99 groups: OnceCell<Vec<String>>,100101 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it102 deploy_kind: OnceCell<DeployKind>,103 session_destination: OnceCell<String>,104 legacy_ssh_store: OnceCell<bool>,105106 pub host_config: Option<Value>,107 pub nixos_config: OnceCell<Value>,108 pub nixos_unchecked_config: OnceCell<Value>,109 pub pkgs_override: Option<Value>,110111 // TODO: Move command helpers away with connectivity refactor112 pub local: bool,113 pub session: OnceLock<Arc<openssh::Session>>,114}115116#[derive(Debug, Clone, Copy)]117pub enum GenerationStorage {118 Deployer,119 Machine,120 Pusher,121}122impl GenerationStorage {123 fn prefix(&self) -> &'static str {124 match self {125 GenerationStorage::Deployer => "deployer.",126 GenerationStorage::Machine => "",127 GenerationStorage::Pusher => "pusher.",128 }129 }130}131132#[derive(Tabled, Debug)]133pub struct Generation {134 #[tabled(rename = "ID", format("{}", self.rollback_id()))]135 pub id: u32,136 #[tabled(rename = "Current")]137 pub current: bool,138 #[tabled(rename = "Created at")]139 pub datetime: UtcDateTime,140 #[tabled(format = "{:?}")]141 pub store_path: PathBuf,142 #[tabled(skip)]143 pub location: GenerationStorage,144}145impl Generation {146 pub fn rollback_id(&self) -> String {147 format!("{}{}", self.location.prefix(), self.id)148 }149}150151fn parse_generation_line(g: &str) -> Option<Generation> {152 let mut parts = g.split_whitespace();153 let id = parts.next()?;154 let id: u32 = id.parse().ok()?;155 let date = parts.next()?;156 let time = parts.next()?;157 let current = if let Some(current) = parts.next() {158 if current == "(current)" {159 Some(true)160 } else {161 None162 }163 } else {164 Some(false)165 };166 let current = current?;167 if parts.next().is_some() {168 warn!("unexpected text after generation: {g}");169 }170171 let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")172 .expect("valid format");173 let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;174175 Some(Generation {176 id,177 current,178 datetime,179 store_path: PathBuf::new(),180 location: GenerationStorage::Machine,181 })182}183// TODO: Move command helpers away with connectivity refactor184impl ConfigHost {185 pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {186 let mut cmd = self.cmd("nix-env").await?;187 cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))188 .arg("--list-generations")189 .env("TZ", "UTC");190 // Sudo is required because --list-generations tries to acquire profile lock191 let data = cmd.sudo().run_string().await?;192 let mut generations = data193 .split('\n')194 .map(|e| e.trim())195 .filter(|&l| !l.is_empty())196 .filter_map(|g| {197 let generation = parse_generation_line(g);198 if generation.is_none() {199 warn!("bad generation: {g}");200 };201 generation202 })203 .collect::<Vec<_>>();204 for ele in generations.iter_mut() {205 let mut cmd = self.cmd("readlink").await?;206 cmd.arg("--")207 .arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));208 let path = cmd.run_string().await?;209 ele.store_path = PathBuf::from(path.trim_end_matches("\n"));210 }211212 Ok(generations)213 }214215 pub fn set_session_destination(&self, dest: String) {216 self.session_destination217 .set(dest)218 .expect("session destination is already set")219 }220 pub fn set_deploy_kind(&self, kind: DeployKind) {221 self.deploy_kind222 .set(kind)223 .expect("deploy kind is already set");224 }225 pub fn set_legacy_ssh_store(&self, legacy: bool) {226 self.legacy_ssh_store227 .set(legacy)228 .expect("legacy ssh store is already set")229 }230 pub async fn deploy_kind(&self) -> Result<DeployKind> {231 if let Some(kind) = self.deploy_kind.get() {232 return Ok(*kind);233 }234 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {235 Ok(v) => v,236 Err(e) => {237 bail!("failed to query remote system kind: {e}");238 }239 };240 if !is_fleet_managed {241 bail!(242 "{}",243 indoc::indoc! {"244 host is not marked as managed by fleet245 if you're not trying to lustrate/install system from scratch,246 you should either247 1. manually create /etc/FLEET_HOST file on the target host,248 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet249 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos250 "}251 );252 }253 // TOCTOU is possible254 let _ = self.deploy_kind.set(DeployKind::Fleet);255 Ok(*self.deploy_kind.get().expect("deploy kind is just set"))256 }257 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {258 // Prefer sudo, as run0 has some gotchas with polkit259 // and too many repeating prompts.260 if (self.find_in_path("sudo").await).is_ok() {261 return Ok(EscalationStrategy::Sudo);262 }263 if (self.find_in_path("run0").await).is_ok() {264 return Ok(EscalationStrategy::Run0);265 }266 Ok(EscalationStrategy::Su)267 }268 async fn open_session(&self) -> Result<Arc<openssh::Session>> {269 assert!(!self.local, "do not open ssh connection to local session");270 // FIXME: TOCTOU271 if let Some(session) = &self.session.get() {272 return Ok((*session).clone());273 };274 let mut session = SessionBuilder::default();275 session.control_persist(ControlPersist::ClosedAfterInitialConnection);276277 let dest = self.session_destination.get().unwrap_or(&self.name);278 let session = session279 .connect(&dest)280 .await281 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;282 let session = Arc::new(session);283 self.session.set(session.clone()).expect("TOCTOU happened");284 Ok(session)285 }286 pub async fn mktemp_dir(&self) -> Result<String> {287 let mut cmd = self.cmd("mktemp").await?;288 cmd.arg("-d");289 let path = cmd.run_string().await?;290 Ok(path.trim_end().to_owned())291 }292 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {293 let mut cmd = self.cmd("sh").await?;294 cmd.arg("-c")295 .arg("test -e \"$1\" && echo true || echo false")296 .arg("_")297 .arg(path);298 cmd.run_value().await299 }300 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {301 let mut cmd = self.cmd("cat").await?;302 cmd.arg(path);303 cmd.run_bytes().await304 }305 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {306 let mut cmd = self.cmd("cat").await?;307 cmd.arg(path);308 cmd.run_string().await309 }310 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {311 let mut cmd = self.cmd("ls").await?;312 cmd.arg(path);313 let out = cmd.run_string().await?;314 let mut lines = out.split('\n');315 if let Some(last) = lines.next_back() {316 ensure!(last.is_empty(), "output of ls should end with newline");317 }318 Ok(lines.map(ToOwned::to_owned).collect())319 }320 #[allow(dead_code)]321 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {322 let text = self.read_file_text(path).await?;323 Ok(serde_json::from_str(&text)?)324 }325 pub async fn read_env(&self, env: &str) -> Result<String> {326 let mut cmd = self.cmd("printenv").await?;327 cmd.arg(env);328 cmd.run_string().await329 }330 pub async fn find_in_path(&self, command: &str) -> Result<String> {331 // // `which` is not a part of coreutils, and it might not exist on machine.332 // let path = self.read_env("PATH").await?;333 // // Assuming delimiter is :, we don't work with windows host, this check will be much334 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)335 // for ele in path.split(':') {336 // let test_path = format!("{ele}/{cmd}");337 // test -x etc338 // }339 // let mut cmd = self.cmd("printenv").await?;340 // cmd.arg(env);341 // Ok(cmd.run_string().await?)342 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.343 let mut cmd = self344 .cmd_escalation(345 // Not used346 EscalationStrategy::Su,347 "which",348 )349 .await?;350 cmd.arg(command);351 cmd.run_string().await352 }353 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>354 where355 <D as FromStr>::Err: Display,356 {357 let text = self.read_file_text(path).await?;358 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))359 }360 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {361 self.cmd_escalation(self.escalation_strategy().await?, cmd)362 .await363 }364 pub async fn cmd_escalation(365 &self,366 escalation: EscalationStrategy,367 cmd: impl AsRef<OsStr>,368 ) -> Result<MyCommand> {369 if self.local {370 Ok(MyCommand::new(escalation, cmd))371 } else {372 let session = self.open_session().await?;373 Ok(MyCommand::new_on(escalation, cmd, session))374 }375 }376 pub async fn nix_cmd(&self) -> Result<MyCommand> {377 let mut nix = self.cmd("nix").await?;378 nix.args([379 "--extra-experimental-features",380 "nix-command",381 "--extra-experimental-features",382 "flakes",383 ]);384 Ok(nix)385 }386387 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {388 ensure!(data.encrypted, "secret is not encrypted");389 let mut cmd = self.cmd("fleet-install-secrets").await?;390 cmd.arg("decrypt").eqarg("--secret", data.to_string());391 let encoded = cmd392 .sudo()393 .run_string()394 .await395 .context("failed to call remote host for decrypt")?;396 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;397 ensure!(!data.encrypted, "secret came out encrypted");398 Ok(data.data)399 }400 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {401 ensure!(data.encrypted, "secret is not encrypted");402 let mut cmd = self.cmd("fleet-install-secrets").await?;403 cmd.arg("reencrypt").eqarg("--secret", data.to_string());404 for target in targets {405 let key = self.config.key(&target).await?;406 cmd.eqarg("--targets", key);407 }408 let encoded = cmd409 .sudo()410 .run_string()411 .await412 .context("failed to call remote host for decrypt")?;413 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;414 ensure!(data.encrypted, "secret came out not encrypted");415 Ok(data)416 }417 /// Returns path for futureproofing, as path might change i.e on conversion to CA418 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {419 if self.local {420 // Path is located locally, thus already trusted.421 return Ok(path.to_owned());422 }423 let mut nix = MyCommand::new(424 // Not used425 EscalationStrategy::Su,426 "nix",427 );428 nix.arg("copy").arg("--substitute-on-destination");429430 let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {431 "ssh"432 } else {433 "ssh-ng"434 };435436 match self.deploy_kind().await? {437 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {438 nix.comparg("--to", format!("{proto}://{}", self.name));439 }440 DeployKind::NixosInstall => {441 nix442 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon443 .arg("--no-check-sigs")444 .comparg(445 "--to",446 format!("{proto}://root@{}?remote-store=/mnt", self.name),447 );448 }449 }450 nix.arg(path);451 nix.run_nix().await.context("nix copy")?;452 Ok(path.to_owned())453 }454 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {455 let mut cmd = self.cmd("systemctl").await?;456 cmd.arg("stop").arg(name);457 cmd.sudo().run().await458 }459 pub async fn systemctl_start(&self, name: &str) -> Result<()> {460 let mut cmd = self.cmd("systemctl").await?;461 cmd.arg("start").arg(name);462 cmd.sudo().run().await463 }464465 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {466 let mut cmd = self.cmd("rm").await?;467 cmd.arg("-f").arg(path);468 if sudo {469 cmd = cmd.sudo()470 }471 cmd.run().await472 }473}474impl ConfigHost {475 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,476 // assuming getting tags always returns the same value.477 pub async fn tags(&self) -> Result<Vec<String>> {478 if let Some(v) = self.groups.get() {479 return Ok(v.clone());480 }481 let Some(host_config) = &self.host_config else {482 return Ok(vec![]);483 };484 let tags: Vec<String> = nix_go_json!(host_config.tags);485486 let _ = self.groups.set(tags.clone());487488 Ok(tags)489 }490 pub async fn nixos_config(&self) -> Result<Value> {491 if let Some(v) = self.nixos_config.get() {492 return Ok(v.clone());493 }494 let Some(host_config) = &self.host_config else {495 bail!("local host has no nixos_config");496 };497 let nixos_config = nix_go!(host_config.nixos.config);498 assert_warn("nixos config evaluation", &nixos_config).await?;499500 let _ = self.nixos_config.set(nixos_config.clone());501502 Ok(nixos_config)503 }504 pub fn nixos_unchecked_config(&self) -> Result<Value> {505 if let Some(v) = self.nixos_unchecked_config.get() {506 return Ok(v.clone());507 }508 let Some(host_config) = &self.host_config else {509 bail!("local host has no nixos_config");510 };511 let nixos_config = nix_go!(host_config.nixos_unchecked.config);512513 let _ = self.nixos_unchecked_config.set(nixos_config.clone());514515 Ok(nixos_config)516 }517518 pub fn list_defined_secrets(&self) -> Result<Vec<String>> {519 let nixos = self.nixos_unchecked_config()?;520 let secrets = nix_go!(nixos.secrets);521 secrets.list_fields()522 }523524 /// Packages for this host, resolved with nixpkgs overlays525 pub async fn pkgs(&self) -> Result<Value> {526 if let Some(value) = &self.pkgs_override {527 return Ok(value.clone());528 }529 let Some(host_config) = &self.host_config else {530 bail!("local host has no host_config");531 };532 // TODO: Should nixos.options be cached?533 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))534 }535}536537impl Config {538 pub async fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {539 let config = &self.config_field;540 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);541 Ok(tagged)542 }543 pub async fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {544 let mut out = BTreeSet::new();545 for owner in owners {546 if let Some(tag) = owner.strip_prefix('@') {547 let hosts = self.tagged_hostnames(tag).await?;548 out.extend(hosts);549 } else {550 out.insert(owner);551 }552 }553 Ok(out)554 }555 pub fn local_host(&self) -> ConfigHost {556 ConfigHost {557 config: self.clone(),558 name: "<virtual localhost>".to_owned(),559 host_config: None,560 nixos_config: OnceCell::new(),561 nixos_unchecked_config: OnceCell::new(),562 groups: {563 let cell = OnceCell::new();564 let _ = cell.set(vec![]);565 cell566 },567 pkgs_override: Some(self.default_pkgs.clone()),568569 local: true,570 session: OnceLock::new(),571 deploy_kind: OnceCell::new(),572 session_destination: OnceCell::new(),573 legacy_ssh_store: OnceCell::new(),574 }575 }576577 pub async fn host(&self, name: &str) -> Result<ConfigHost> {578 let config = &self.config_field;579 let host_config = nix_go!(config.hosts[{ name }]);580581 Ok(ConfigHost {582 config: self.clone(),583 name: name.to_owned(),584 host_config: Some(host_config),585 nixos_config: OnceCell::new(),586 nixos_unchecked_config: OnceCell::new(),587 groups: OnceCell::new(),588 pkgs_override: None,589590 // TODO: Remove with connectivit refactor591 local: self.localhost == name,592 session: OnceLock::new(),593 deploy_kind: OnceCell::new(),594 session_destination: OnceCell::new(),595 legacy_ssh_store: OnceCell::new(),596 })597 }598 pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {599 let config = &self.config_field;600 let names = nix_go!(config.hosts).list_fields()?;601 let mut out = vec![];602 for name in names {603 out.push(self.host(&name).await?);604 }605 Ok(out)606 }607 // TODO: Replace usages with .host().nixos_config608 pub async fn system_config(&self, host: &str) -> Result<Value> {609 let fleet_field = &self.config_field;610 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))611 }612613 /// Shared secrets configured in fleet.nix or in flake614 pub async fn list_configured_shared(&self) -> Result<Vec<String>> {615 let config_field = &self.config_field;616 nix_go!(config_field.sharedSecrets).list_fields()617 }618 pub fn has_shared(&self, name: &str) -> bool {619 let data = self.data();620 data.secrets.contains(name)621 }622 pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {623 let mut data = self.data_mut();624 data.secrets.set_data(name, shared);625 }626 pub fn remove_shared(&self, secret: &str) {627 let mut data = self.data_mut();628 data.secrets.remove(secret);629 }630631 pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {632 let data = self.data_mut();633 data.secrets.keys_for_owner(host).cloned().collect()634 }635 pub fn list_secrets(&self) -> Vec<String> {636 let data = self.data_mut();637 data.secrets.keys().cloned().collect()638 }639640 pub fn has_secret(&self, host: &str, secret: &str) -> bool {641 let data = self.data();642 data.secrets.contains_for_owner(secret, host)643 }644 pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {645 let mut data = self.data_mut();646 data.secrets.set_single_data(secret, host, value);647 }648 pub fn remove_secret(&self, host: &str, secret: &str) {649 let mut data = self.data_mut();650 data.secrets.drop_owner_no_reencrypt(secret, host);651 }652653 pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {654 let data = self.data();655 data.secrets.get_single(secret, host).cloned()656 }657 pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {658 let data = self.data();659 data.secrets.get(secret).cloned()660 }661662 // TODO: Should this be something modifiable from other processes?663 // E.g terraform provider might want to update FleetData (e.g secrets),664 // and current implementation assumes only one process holds current fleet.nix665 // Given that it is no longer needs to be a file for nix evaluation,666 // maybe it can be a .nix file for persistence, but accessible only667 // thru some shared state controller? Might it be stored in terraform668 // state provider?669 pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {670 self.data.lock().unwrap()671 }672 pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {673 self.data.lock().unwrap()674 }675 pub fn save(&self) -> Result<()> {676 let mut tempfile = NamedTempFile::new_in(self.directory.clone()).context("failed to create updated version of fleet.nix in the same directory as original.\nDo you have write access to it? Access only to the fleet.nix won't be enough, the directory is used for atomic overwrite operation.\nIt is not recommended to use fleet by root anyway, move fleet project to your home directory.")?;677 let data = nixlike::serialize(&self.data() as &FleetData)?;678 tempfile.write_all(679 format!(680 "# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"681 )682 .as_bytes(),683 )?;684 let mut fleet_data_path = self.directory.clone();685 fleet_data_path.push("fleet.nix");686 tempfile.persist(fleet_data_path)?;687 Ok(())688 }689}1use std::{2 cell::OnceCell,3 collections::BTreeSet,4 ffi::{OsStr, OsString},5 fmt::Display,6 io::Write,7 ops::Deref,8 path::PathBuf,9 str::FromStr,10 sync::{Arc, Mutex, MutexGuard, OnceLock},11};1213use anyhow::{Context, Result, anyhow, bail, ensure};14use fleet_shared::SecretData;15use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};16use openssh::{ControlPersist, SessionBuilder};17use serde::de::DeserializeOwned;18use tabled::Tabled;19use tempfile::NamedTempFile;20use time::{UtcDateTime, format_description};21use tracing::warn;2223use crate::{24 command::MyCommand,25 fleetdata::{FleetData, FleetSecretData, FleetSecretDistribution, FleetSecretDistributions},26};2728pub struct FleetConfigInternals {29 /// Fleet project directory, containing fleet.nix file.30 pub directory: PathBuf,31 /// builtins.currentSystem32 pub local_system: String,33 pub data: Arc<Mutex<FleetData>>,34 pub nix_args: Vec<OsString>,35 /// fleet_config.config36 pub config_field: Value,37 /// flake.output38 pub flake_outputs: Value,39 // TODO: Remove with connectivity refactor40 pub localhost: String,4142 /// import nixpkgs {system = local};43 pub default_pkgs: Value,44 /// inputs.nixpkgs45 pub nixpkgs: Value,46}4748// TODO: Make field not pub49#[derive(Clone)]50pub struct Config(pub Arc<FleetConfigInternals>);5152impl Deref for Config {53 type Target = FleetConfigInternals;5455 fn deref(&self) -> &Self::Target {56 &self.057 }58}5960#[derive(Clone, Copy, Debug)]61pub enum EscalationStrategy {62 Sudo,63 Run0,64 Su,65}6667#[derive(Clone, PartialEq, Copy, Debug)]68pub enum DeployKind {69 /// NixOS => NixOS managed by fleet70 UpgradeToFleet,71 /// NixOS managed by fleet => NixOS managed by fleet72 Fleet,73 /// Remote host has /mnt, /mnt/boot mounted,74 /// generated config is added to fleet configuration.75 NixosInstall,76 /// Remote host has some system and nix installed in multi-user mode (/nix is owned by root),77 /// generated config is added to fleet configuration,78 /// and /etc/NIXOS_LUSTRATE exists, fleet will perform the rest.79 NixosLustrate,80}8182impl FromStr for DeployKind {83 type Err = anyhow::Error;84 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {85 match s {86 "upgrade-to-fleet" => Ok(Self::UpgradeToFleet),87 "fleet" => Ok(Self::Fleet),88 "nixos-install" => Ok(Self::NixosInstall),89 "nixos-lustrate" => Ok(Self::NixosLustrate),90 v => bail!(91 "unknown deploy_kind: {v}; expected on of \"upgrade-to-fleet\", \"fleet\", \"nixos-install\", \"nixos-lustrate\""92 ),93 }94 }95}96pub struct ConfigHost {97 config: Config,98 pub name: String,99 groups: OnceCell<Vec<String>>,100101 // TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it102 deploy_kind: OnceCell<DeployKind>,103 session_destination: OnceCell<String>,104 legacy_ssh_store: OnceCell<bool>,105106 pub host_config: Option<Value>,107 pub nixos_config: OnceCell<Value>,108 pub nixos_unchecked_config: OnceCell<Value>,109 pub pkgs_override: Option<Value>,110111 // TODO: Move command helpers away with connectivity refactor112 pub local: bool,113 pub session: OnceLock<Arc<openssh::Session>>,114}115116#[derive(Debug, Clone, Copy)]117pub enum GenerationStorage {118 Deployer,119 Machine,120 Pusher,121}122impl GenerationStorage {123 fn prefix(&self) -> &'static str {124 match self {125 GenerationStorage::Deployer => "deployer.",126 GenerationStorage::Machine => "",127 GenerationStorage::Pusher => "pusher.",128 }129 }130}131132#[derive(Tabled, Debug)]133pub struct Generation {134 #[tabled(rename = "ID", format("{}", self.rollback_id()))]135 pub id: u32,136 #[tabled(rename = "Current")]137 pub current: bool,138 #[tabled(rename = "Created at")]139 pub datetime: UtcDateTime,140 #[tabled(format = "{:?}")]141 pub store_path: PathBuf,142 #[tabled(skip)]143 pub location: GenerationStorage,144}145impl Generation {146 pub fn rollback_id(&self) -> String {147 format!("{}{}", self.location.prefix(), self.id)148 }149}150151fn parse_generation_line(g: &str) -> Option<Generation> {152 let mut parts = g.split_whitespace();153 let id = parts.next()?;154 let id: u32 = id.parse().ok()?;155 let date = parts.next()?;156 let time = parts.next()?;157 let current = if let Some(current) = parts.next() {158 if current == "(current)" {159 Some(true)160 } else {161 None162 }163 } else {164 Some(false)165 };166 let current = current?;167 if parts.next().is_some() {168 warn!("unexpected text after generation: {g}");169 }170171 let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")172 .expect("valid format");173 let datetime = UtcDateTime::parse(&format!("{date} {time}"), &format).ok()?;174175 Some(Generation {176 id,177 current,178 datetime,179 store_path: PathBuf::new(),180 location: GenerationStorage::Machine,181 })182}183// TODO: Move command helpers away with connectivity refactor184impl ConfigHost {185 pub async fn list_generations(&self, profile: &str) -> Result<Vec<Generation>> {186 let mut cmd = self.cmd("nix-env").await?;187 cmd.comparg("--profile", format!("/nix/var/nix/profiles/{profile}"))188 .arg("--list-generations")189 .env("TZ", "UTC");190 // Sudo is required because --list-generations tries to acquire profile lock191 let data = cmd.sudo().run_string().await?;192 let mut generations = data193 .split('\n')194 .map(|e| e.trim())195 .filter(|&l| !l.is_empty())196 .filter_map(|g| {197 let generation = parse_generation_line(g);198 if generation.is_none() {199 warn!("bad generation: {g}");200 };201 generation202 })203 .collect::<Vec<_>>();204 for ele in generations.iter_mut() {205 let mut cmd = self.cmd("readlink").await?;206 cmd.arg("--")207 .arg(format!("/nix/var/nix/profiles/{profile}-{}-link", ele.id));208 let path = cmd.run_string().await?;209 ele.store_path = PathBuf::from(path.trim_end_matches("\n"));210 }211212 Ok(generations)213 }214215 pub fn set_session_destination(&self, dest: String) {216 self.session_destination217 .set(dest)218 .expect("session destination is already set")219 }220 pub fn set_deploy_kind(&self, kind: DeployKind) {221 self.deploy_kind222 .set(kind)223 .expect("deploy kind is already set");224 }225 pub fn set_legacy_ssh_store(&self, legacy: bool) {226 self.legacy_ssh_store227 .set(legacy)228 .expect("legacy ssh store is already set")229 }230 pub async fn deploy_kind(&self) -> Result<DeployKind> {231 if let Some(kind) = self.deploy_kind.get() {232 return Ok(*kind);233 }234 let is_fleet_managed = match self.file_exists("/etc/FLEET_HOST").await {235 Ok(v) => v,236 Err(e) => {237 bail!("failed to query remote system kind: {e}");238 }239 };240 if !is_fleet_managed {241 bail!(242 "{}",243 indoc::indoc! {"244 host is not marked as managed by fleet245 if you're not trying to lustrate/install system from scratch,246 you should either247 1. manually create /etc/FLEET_HOST file on the target host,248 2. use ?deploy_kind=fleet host argument if you're upgrading from older version of fleet249 3. use ?deploy_kind=upgrade_to_fleet if you're upgrading from plain nixos to fleet-managed nixos250 "}251 );252 }253 // TOCTOU is possible254 let _ = self.deploy_kind.set(DeployKind::Fleet);255 Ok(*self.deploy_kind.get().expect("deploy kind is just set"))256 }257 pub async fn escalation_strategy(&self) -> Result<EscalationStrategy> {258 // Prefer sudo, as run0 has some gotchas with polkit259 // and too many repeating prompts.260 if (self.find_in_path("sudo").await).is_ok() {261 return Ok(EscalationStrategy::Sudo);262 }263 if (self.find_in_path("run0").await).is_ok() {264 return Ok(EscalationStrategy::Run0);265 }266 Ok(EscalationStrategy::Su)267 }268 async fn open_session(&self) -> Result<Arc<openssh::Session>> {269 assert!(!self.local, "do not open ssh connection to local session");270 // FIXME: TOCTOU271 if let Some(session) = &self.session.get() {272 return Ok((*session).clone());273 };274 let mut session = SessionBuilder::default();275 session.control_persist(ControlPersist::ClosedAfterInitialConnection);276277 let dest = self.session_destination.get().unwrap_or(&self.name);278 let session = session279 .connect(&dest)280 .await281 .map_err(|e| anyhow!("ssh error while connecting to {}: {e:#?}", self.name))?;282 let session = Arc::new(session);283 self.session.set(session.clone()).expect("TOCTOU happened");284 Ok(session)285 }286 pub async fn mktemp_dir(&self) -> Result<String> {287 let mut cmd = self.cmd("mktemp").await?;288 cmd.arg("-d");289 let path = cmd.run_string().await?;290 Ok(path.trim_end().to_owned())291 }292 pub async fn file_exists(&self, path: impl AsRef<OsStr>) -> Result<bool> {293 let mut cmd = self.cmd("sh").await?;294 cmd.arg("-c")295 .arg("test -e \"$1\" && echo true || echo false")296 .arg("_")297 .arg(path);298 cmd.run_value().await299 }300 pub async fn read_file_bin(&self, path: impl AsRef<OsStr>) -> Result<Vec<u8>> {301 let mut cmd = self.cmd("cat").await?;302 cmd.arg(path);303 cmd.run_bytes().await304 }305 pub async fn read_file_text(&self, path: impl AsRef<OsStr>) -> Result<String> {306 let mut cmd = self.cmd("cat").await?;307 cmd.arg(path);308 cmd.run_string().await309 }310 pub async fn read_dir(&self, path: impl AsRef<OsStr>) -> Result<Vec<String>> {311 let mut cmd = self.cmd("ls").await?;312 cmd.arg(path);313 let out = cmd.run_string().await?;314 let mut lines = out.split('\n');315 if let Some(last) = lines.next_back() {316 ensure!(last.is_empty(), "output of ls should end with newline");317 }318 Ok(lines.map(ToOwned::to_owned).collect())319 }320 #[allow(dead_code)]321 pub async fn read_file_json<D: DeserializeOwned>(&self, path: impl AsRef<OsStr>) -> Result<D> {322 let text = self.read_file_text(path).await?;323 Ok(serde_json::from_str(&text)?)324 }325 pub async fn read_env(&self, env: &str) -> Result<String> {326 let mut cmd = self.cmd("printenv").await?;327 cmd.arg(env);328 cmd.run_string().await329 }330 pub async fn find_in_path(&self, command: &str) -> Result<String> {331 // // `which` is not a part of coreutils, and it might not exist on machine.332 // let path = self.read_env("PATH").await?;333 // // Assuming delimiter is :, we don't work with windows host, this check will be much334 // // more sophisticated in remowt backend (and quicker, since actual PATH search will be done on remote machine)335 // for ele in path.split(':') {336 // let test_path = format!("{ele}/{cmd}");337 // test -x etc338 // }339 // let mut cmd = self.cmd("printenv").await?;340 // cmd.arg(env);341 // Ok(cmd.run_string().await?)342 // Assuming this is an environment issue if which doesn't exist, will be fixed with remowt.343 let mut cmd = self344 .cmd_escalation(345 // Not used346 EscalationStrategy::Su,347 "which",348 )349 .await?;350 cmd.arg(command);351 cmd.run_string().await352 }353 pub async fn read_file_value<D: FromStr>(&self, path: impl AsRef<OsStr>) -> Result<D>354 where355 <D as FromStr>::Err: Display,356 {357 let text = self.read_file_text(path).await?;358 D::from_str(&text).map_err(|e| anyhow!("failed to parse value: {e}"))359 }360 pub async fn cmd(&self, cmd: impl AsRef<OsStr>) -> Result<MyCommand> {361 self.cmd_escalation(self.escalation_strategy().await?, cmd)362 .await363 }364 pub async fn cmd_escalation(365 &self,366 escalation: EscalationStrategy,367 cmd: impl AsRef<OsStr>,368 ) -> Result<MyCommand> {369 if self.local {370 Ok(MyCommand::new(escalation, cmd))371 } else {372 let session = self.open_session().await?;373 Ok(MyCommand::new_on(escalation, cmd, session))374 }375 }376 pub async fn nix_cmd(&self) -> Result<MyCommand> {377 let mut nix = self.cmd("nix").await?;378 nix.args([379 "--extra-experimental-features",380 "nix-command",381 "--extra-experimental-features",382 "flakes",383 ]);384 Ok(nix)385 }386387 pub async fn decrypt(&self, data: SecretData) -> Result<Vec<u8>> {388 ensure!(data.encrypted, "secret is not encrypted");389 let mut cmd = self.cmd("fleet-install-secrets").await?;390 cmd.arg("decrypt").eqarg("--secret", data.to_string());391 let encoded = cmd392 .sudo()393 .run_string()394 .await395 .context("failed to call remote host for decrypt")?;396 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;397 ensure!(!data.encrypted, "secret came out encrypted");398 Ok(data.data)399 }400 pub async fn reencrypt(&self, data: SecretData, targets: Vec<String>) -> Result<SecretData> {401 ensure!(data.encrypted, "secret is not encrypted");402 let mut cmd = self.cmd("fleet-install-secrets").await?;403 cmd.arg("reencrypt").eqarg("--secret", data.to_string());404 for target in targets {405 let key = self.config.key(&target).await?;406 cmd.eqarg("--targets", key);407 }408 let encoded = cmd409 .sudo()410 .run_string()411 .await412 .context("failed to call remote host for decrypt")?;413 let data: SecretData = encoded.parse().map_err(|e| anyhow!("{e}"))?;414 ensure!(data.encrypted, "secret came out not encrypted");415 Ok(data)416 }417 /// Returns path for futureproofing, as path might change i.e on conversion to CA418 pub async fn remote_derivation(&self, path: &PathBuf) -> Result<PathBuf> {419 if self.local {420 // Path is located locally, thus already trusted.421 return Ok(path.to_owned());422 }423 let mut nix = MyCommand::new(424 // Not used425 EscalationStrategy::Su,426 "nix",427 );428 nix.arg("copy").arg("--substitute-on-destination");429430 let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {431 "ssh"432 } else {433 "ssh-ng"434 };435436 match self.deploy_kind().await? {437 DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {438 nix.comparg("--to", format!("{proto}://{}", self.name));439 }440 DeployKind::NixosInstall => {441 nix442 // Signature checking makes no sense with remote-store store argument set, as we're not even interacting with remote nix daemon443 .arg("--no-check-sigs")444 .comparg(445 "--to",446 format!("{proto}://root@{}?remote-store=/mnt", self.name),447 );448 }449 }450 nix.arg(path);451 nix.run_nix().await.context("nix copy")?;452 Ok(path.to_owned())453 }454 pub async fn systemctl_stop(&self, name: &str) -> Result<()> {455 let mut cmd = self.cmd("systemctl").await?;456 cmd.arg("stop").arg(name);457 cmd.sudo().run().await458 }459 pub async fn systemctl_start(&self, name: &str) -> Result<()> {460 let mut cmd = self.cmd("systemctl").await?;461 cmd.arg("start").arg(name);462 cmd.sudo().run().await463 }464465 pub async fn rm_file(&self, path: impl AsRef<OsStr>, sudo: bool) -> Result<()> {466 let mut cmd = self.cmd("rm").await?;467 cmd.arg("-f").arg(path);468 if sudo {469 cmd = cmd.sudo()470 }471 cmd.run().await472 }473}474475struct HostSecretDefinition(Value);476477impl ConfigHost {478 // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway,479 // assuming getting tags always returns the same value.480 pub fn tags(&self) -> Result<Vec<String>> {481 if let Some(v) = self.groups.get() {482 return Ok(v.clone());483 }484 let Some(host_config) = &self.host_config else {485 return Ok(vec![]);486 };487 let tags: Vec<String> = nix_go_json!(host_config.tags);488489 let _ = self.groups.set(tags.clone());490491 Ok(tags)492 }493 pub fn nixos_config(&self) -> Result<Value> {494 if let Some(v) = self.nixos_config.get() {495 return Ok(v.clone());496 }497 let Some(host_config) = &self.host_config else {498 bail!("local host has no nixos_config");499 };500 let nixos_config = nix_go!(host_config.nixos.config);501 assert_warn("nixos config evaluation", &nixos_config)?;502503 let _ = self.nixos_config.set(nixos_config.clone());504505 Ok(nixos_config)506 }507 pub fn nixos_unchecked_config(&self) -> Result<Value> {508 if let Some(v) = self.nixos_unchecked_config.get() {509 return Ok(v.clone());510 }511 let Some(host_config) = &self.host_config else {512 bail!("local host has no nixos_config");513 };514 let nixos_config = nix_go!(host_config.nixos_unchecked.config);515516 let _ = self.nixos_unchecked_config.set(nixos_config.clone());517518 Ok(nixos_config)519 }520521 pub fn list_defined_secrets(&self) -> Result<Vec<String>> {522 let nixos = self.nixos_unchecked_config()?;523 let secrets = nix_go!(nixos.secrets);524 secrets.list_fields()525 }526527 /// Packages for this host, resolved with nixpkgs overlays528 pub fn pkgs(&self) -> Result<Value> {529 if let Some(value) = &self.pkgs_override {530 return Ok(value.clone());531 }532 let Some(host_config) = &self.host_config else {533 bail!("local host has no host_config");534 };535 // TODO: Should nixos.options be cached?536 Ok(nix_go!(host_config.nixos.options._module.args.value.pkgs))537 }538}539540pub struct SharedSecretDefinition(Value);541impl SharedSecretDefinition {542 pub fn expected_owners(&self) -> Result<BTreeSet<String>> {543 let secret = &self.0;544 Ok(nix_go_json!(secret.expectedOwners))545 }546 pub fn generator(&self) -> Result<Value> {547 let secret = &self.0;548 Ok(nix_go!(secret.generator))549 }550}551552impl Config {553 pub fn tagged_hostnames(&self, tag: &str) -> Result<Vec<String>> {554 let config = &self.config_field;555 let tagged: Vec<String> = nix_go_json!(config.taggedWith[{ tag }]);556 Ok(tagged)557 }558 pub fn expand_owner_set(&self, owners: Vec<String>) -> Result<BTreeSet<String>> {559 let mut out = BTreeSet::new();560 for owner in owners {561 if let Some(tag) = owner.strip_prefix('@') {562 let hosts = self.tagged_hostnames(tag)?;563 out.extend(hosts);564 } else {565 out.insert(owner);566 }567 }568 Ok(out)569 }570 pub fn local_host(&self) -> ConfigHost {571 ConfigHost {572 config: self.clone(),573 name: "<virtual localhost>".to_owned(),574 host_config: None,575 nixos_config: OnceCell::new(),576 nixos_unchecked_config: OnceCell::new(),577 groups: {578 let cell = OnceCell::new();579 let _ = cell.set(vec![]);580 cell581 },582 pkgs_override: Some(self.default_pkgs.clone()),583584 local: true,585 session: OnceLock::new(),586 deploy_kind: OnceCell::new(),587 session_destination: OnceCell::new(),588 legacy_ssh_store: OnceCell::new(),589 }590 }591592 pub fn host(&self, name: &str) -> Result<ConfigHost> {593 let config = &self.config_field;594 let host_config = nix_go!(config.hosts[{ name }]);595596 Ok(ConfigHost {597 config: self.clone(),598 name: name.to_owned(),599 host_config: Some(host_config),600 nixos_config: OnceCell::new(),601 nixos_unchecked_config: OnceCell::new(),602 groups: OnceCell::new(),603 pkgs_override: None,604605 // TODO: Remove with connectivit refactor606 local: self.localhost == name,607 session: OnceLock::new(),608 deploy_kind: OnceCell::new(),609 session_destination: OnceCell::new(),610 legacy_ssh_store: OnceCell::new(),611 })612 }613 pub fn list_hosts(&self) -> Result<Vec<ConfigHost>> {614 let config = &self.config_field;615 let names = nix_go!(config.hosts).list_fields()?;616 let mut out = vec![];617 for name in names {618 out.push(self.host(&name)?);619 }620 Ok(out)621 }622 // TODO: Replace usages with .host().nixos_config623 pub fn system_config(&self, host: &str) -> Result<Value> {624 let fleet_field = &self.config_field;625 Ok(nix_go!(fleet_field.hosts[{ host }].nixos.config))626 }627628 /// Shared secrets configured in fleet.nix or in flake629 pub fn list_configured_shared(&self) -> Result<Vec<String>> {630 let config_field = &self.config_field;631 nix_go!(config_field.sharedSecrets).list_fields()632 }633 pub fn has_shared(&self, name: &str) -> bool {634 let data = self.data();635 data.secrets.contains(name)636 }637 pub fn replace_shared(&self, name: String, shared: FleetSecretDistribution) {638 let mut data = self.data_mut();639 data.secrets.set_data(name, shared);640 }641 pub fn remove_shared(&self, secret: &str) {642 let mut data = self.data_mut();643 data.secrets.remove(secret);644 }645646 pub fn list_secrets_for_owner(&self, host: &str) -> Vec<String> {647 let data = self.data_mut();648 data.secrets.keys_for_owner(host).cloned().collect()649 }650 pub fn list_secrets(&self) -> Vec<String> {651 let data = self.data_mut();652 data.secrets.keys().cloned().collect()653 }654655 pub fn has_secret(&self, host: &str, secret: &str) -> bool {656 let data = self.data();657 data.secrets.contains_for_owner(secret, host)658 }659 pub fn insert_secret(&self, host: String, secret: String, value: FleetSecretData) {660 let mut data = self.data_mut();661 data.secrets.set_single_data(secret, host, value);662 }663 pub fn remove_secret(&self, host: &str, secret: &str) {664 let mut data = self.data_mut();665 data.secrets.drop_owner_no_reencrypt(secret, host);666 }667668 pub fn host_secret(&self, host: &str, secret: &str) -> Option<FleetSecretDistribution> {669 let data = self.data();670 data.secrets.get_single(secret, host).cloned()671 }672 pub fn shared_secret(&self, secret: &str) -> Option<FleetSecretDistributions> {673 let data = self.data();674 data.secrets.get(secret).cloned()675 }676677 pub fn secret_definition(&self, secret: &str) -> Result<Option<SharedSecretDefinition>> {678 let config = &self.config_field;679 let shared_secrets = nix_go!(config.secrets);680 if !shared_secrets.has_field(secret)? {681 return Ok(None);682 }683 Ok(Some(SharedSecretDefinition(nix_go!(684 shared_secrets[secret]685 ))))686 }687688 // TODO: Should this be something modifiable from other processes?689 // E.g terraform provider might want to update FleetData (e.g secrets),690 // and current implementation assumes only one process holds current fleet.nix691 // Given that it is no longer needs to be a file for nix evaluation,692 // maybe it can be a .nix file for persistence, but accessible only693 // thru some shared state controller? Might it be stored in terraform694 // state provider?695 pub fn data(&'_ self) -> MutexGuard<'_, FleetData> {696 self.data.lock().unwrap()697 }698 pub fn data_mut(&'_ self) -> MutexGuard<'_, FleetData> {699 self.data.lock().unwrap()700 }701 pub fn save(&self) -> Result<()> {702 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.")?;703 let data = nixlike::serialize(&self.data() as &FleetData)?;704 tempfile.write_all(705 format!(706 "# This file contains fleet state and shouldn't be edited by hand\n\n{data}\n\n# vim: ts=2 et nowrap\n"707 )708 .as_bytes(),709 )?;710 let mut fleet_data_path = self.directory.clone();711 fleet_data_path.push("fleet.nix");712 tempfile.persist(fleet_data_path)?;713 Ok(())714 }715}crates/fleet-base/src/keys.rsdiffbeforeafterboth--- a/crates/fleet-base/src/keys.rs
+++ b/crates/fleet-base/src/keys.rs
@@ -12,10 +12,10 @@
pub fn cached_key(&self, host: &str) -> Option<String> {
let data = self.data();
let key = data.hosts.get(host).map(|h| &h.encryption_key);
- if let Some(key) = key {
- if key.is_empty() {
- return None;
- }
+ if let Some(key) = key
+ && key.is_empty()
+ {
+ return None;
}
key.cloned()
}
@@ -30,7 +30,7 @@
Ok(key)
} else {
warn!("Loading key for {}", host);
- let host = self.host(host).await?;
+ let host = self.host(host)?;
let mut cmd = host.cmd("cat").await?;
cmd.arg("/etc/ssh/ssh_host_ed25519_key.pub");
let key = cmd.run_string().await?;
@@ -47,7 +47,7 @@
}
pub async fn recipients(&self, hosts: Vec<String>) -> Result<Vec<Box<dyn Recipient>>> {
- let hosts = self.expand_owner_set(hosts).await?;
+ let hosts = self.expand_owner_set(hosts)?;
futures::stream::iter(hosts.iter())
.then(|m| self.recipient(m.as_ref()))
.try_collect::<Vec<_>>()
@@ -57,12 +57,7 @@
#[allow(dead_code)]
pub async fn orphaned_data(&self) -> Result<Vec<String>> {
let mut out = Vec::new();
- let host_names = self
- .list_hosts()
- .await?
- .into_iter()
- .map(|h| h.name)
- .collect_vec();
+ let host_names = self.list_hosts()?.into_iter().map(|h| h.name).collect_vec();
for hostname in self
.data()
.hosts
crates/fleet-base/src/opts.rsdiffbeforeafterboth--- a/crates/fleet-base/src/opts.rs
+++ b/crates/fleet-base/src/opts.rs
@@ -104,20 +104,20 @@
}
impl FleetOpts {
- pub async fn filter_skipped(
+ pub fn filter_skipped(
&self,
hosts: impl IntoIterator<Item = ConfigHost>,
) -> Result<Vec<ConfigHost>> {
let mut out = Vec::new();
for host in hosts {
- if self.should_skip(&host).await? {
+ if self.should_skip(&host)? {
continue;
}
out.push(host);
}
Ok(out)
}
- pub async fn should_skip(&self, host: &ConfigHost) -> Result<bool> {
+ pub fn should_skip(&self, host: &ConfigHost) -> Result<bool> {
if self.skip.iter().any(|h| h as &str == host.name) {
return Ok(true);
}
@@ -137,7 +137,7 @@
}
}
if have_group_matches {
- let host_tags = host.tags().await?;
+ let host_tags = host.tags()?;
for item in self.only.iter() {
match item {
HostItem::Tag { name, .. } if host_tags.contains(name) => {
@@ -149,15 +149,15 @@
}
Ok(true)
}
- pub async fn action_attr<T: FromStr>(&self, host: &ConfigHost, attr: &str) -> Result<Option<T>>
+ pub fn action_attr<T: FromStr>(&self, host: &ConfigHost, attr: &str) -> Result<Option<T>>
where
T::Err: Sync,
anyhow::Error: From<T::Err>,
{
- let str = self.action_attr_str(host, attr).await?;
+ let str = self.action_attr_str(host, attr)?;
Ok(str.map(|v| T::from_str(&v)).transpose()?)
}
- pub async fn action_attr_str(&self, host: &ConfigHost, attr: &str) -> Result<Option<String>> {
+ pub fn action_attr_str(&self, host: &ConfigHost, attr: &str) -> Result<Option<String>> {
if self.only.is_empty() {
return Ok(None);
}
@@ -176,7 +176,7 @@
}
}
if have_group_matches {
- let host_tags = host.tags().await?;
+ let host_tags = host.tags()?;
for item in self.only.iter() {
match item {
HostItem::Tag { name, attrs }
@@ -195,7 +195,7 @@
}
// TODO: Config should be detached from opts.
- pub async fn build(&self, nix_args: Vec<OsString>, assert: bool) -> Result<Config> {
+ pub fn build(&self, nix_args: Vec<OsString>, assert: bool) -> Result<Config> {
let cwd = current_dir()?;
let mut directory = cwd.clone();
let mut fleet_data_path = directory.join("fleet.nix");
@@ -248,7 +248,6 @@
if assert {
assert_warn("fleet config evaluation", &config_field)
- .await
.context("failed to verify assertions")?;
}
crates/fleet-base/src/primops.rsdiffbeforeafterboth--- a/crates/fleet-base/src/primops.rs
+++ b/crates/fleet-base/src/primops.rs
@@ -1,38 +1,168 @@
-use std::cell::OnceCell;
-use std::collections::{BTreeMap, HashMap};
-use std::sync::{Arc, Mutex, OnceLock};
+use std::collections::{BTreeMap, BTreeSet, HashMap};
+use std::sync::OnceLock;
-use anyhow::{Context, bail};
+use anyhow::{Context, bail, ensure};
+use fleet_shared::SecretData;
use itertools::Itertools;
use nix_eval::{NativeFn, Value, nix_go, nix_go_json};
use serde::Deserialize;
use tracing::{info, warn};
-use crate::fleetdata::{FleetData, FleetSecrets};
-use crate::host::Config;
+use crate::fleetdata::{
+ Expectations, FleetSecretData, FleetSecretDistribution, FleetSecretPart, GeneratorPart,
+};
+use crate::host::{Config, ConfigHost};
+use crate::secret::{RegenerationReason, secret_needs_regeneration};
+use anyhow::{Result, anyhow};
#[derive(thiserror::Error, Debug)]
enum Error {}
-struct Parts {
- encrypted: Vec<String>,
- public: Vec<String>,
+pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+enum GeneratorKind {
+ Impure,
+ Pure,
}
-trait SecretsBackend {
- fn has_shared(&self, name: &str);
- fn has_host(&self, host: &str, name: &str);
- fn shared_parts(&self, name: &str) -> Parts;
- fn host_parts(&self, host: &str, name: &str) -> Parts;
+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 }));
+ Ok(pkgs.clone().attrs_update(generators)?)
+}
+pub fn get_default_pkgs_and_generators(config: &Config) -> Result<Value> {
+ let host_on = config.local_host();
+ get_pkgs_and_generators(&host_on, vec![])
}
+pub fn call_package(config: &Config, pkgs: &Value, package: &Value) -> Result<Value> {
+ ensure!(
+ package.is_function(),
+ "package should be a function to be called with callPackage"
+ );
+ // No need to use nixpkgs.buildUsing, as only nixpkgs-lib is used.
+ let nixpkgs = &config.nixpkgs;
+ let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs));
+ Ok(nix_go!(call_package(package)(Obj {})))
+}
+
+pub fn get_default_generator_drv(config: &Config, generator: &Value) -> Result<Value> {
+ let default_pkgs_and_generators = get_default_pkgs_and_generators(config)?;
+ let default_generator_drv = call_package(config, &default_pkgs_and_generators, generator)
+ .context("failed to initialize generator to get metadata")?;
+
+ Ok(default_generator_drv)
+}
+
+pub async fn generate(
+ config: &Config,
+ expectations: Expectations,
+ generator: &Value,
+ default_generator_drv: &Value,
+) -> Result<FleetSecretDistribution> {
+ let kind: GeneratorKind = nix_go_json!(default_generator_drv.generatorKind);
+
+ match kind {
+ GeneratorKind::Impure => {
+ let impure_on: Option<String> = nix_go_json!(default_generator_drv.impureOn);
-struct FsSecretsBackend {}
+ let host_on = if let Some(on) = &impure_on {
+ config
+ .host(on)
+ .context("failed to get secret generation target host")?
+ } 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 generator = call_package(config, &pkgs_and_generators, generator)
+ .context("failed to evaluate generator for target host")?;
-pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
+ let generator = generator
+ .build("out")
+ .context("failed to build generator for target host")?;
-#[derive(Deserialize, Debug)]
-struct GeneratorPart {
- encrypted: bool,
+ let generator = host_on
+ .remote_derivation(&generator)
+ .await
+ .context("failed to copy generator to target host")?;
+
+ // TODO: Remove destdir after everything is done
+ let out_parent = host_on
+ .mktemp_dir()
+ .await
+ .context("failed to prepare generator output dir on target host")?;
+ let out = format!("{out_parent}/out");
+ let mut generator_cmd = host_on.cmd(generator).await?;
+ generator_cmd.env("out", &out);
+ if impure_on.is_none() {
+ let project_path: String = config
+ .directory
+ .clone()
+ .into_os_string()
+ .into_string()
+ .map_err(|e| anyhow!("fleet project path is not utf-8: {e:?}"))?;
+ generator_cmd.env("FLEET_PROJECT", project_path);
+ };
+ generator_cmd
+ .run()
+ .await
+ .context("failed to run impure generator")?;
+
+ {
+ let marker = host_on.read_file_text(format!("{out}/marker")).await?;
+ ensure!(
+ marker == "SUCCESS",
+ "impure generator ended prematurely, secret generation failed"
+ );
+ }
+
+ let mut parts = BTreeMap::new();
+ for part in host_on.read_dir(&out).await? {
+ if part == "created_at" || part == "expires_at" || part == "marker" {
+ continue;
+ }
+ let contents: SecretData = host_on
+ .read_file_text(format!("{out}/{part}"))
+ .await?
+ .parse()
+ .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;
+ parts.insert(part.to_owned(), FleetSecretPart { raw: contents });
+ }
+
+ let created_at = host_on.read_file_value(format!("{out}/created_at")).await?;
+ let expires_at = host_on
+ .read_file_value(format!("{out}/expires_at"))
+ .await
+ .ok();
+
+ let new_data = FleetSecretData {
+ created_at,
+ expires_at,
+ parts,
+ 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}")
+ }
+
+ Ok(new_data)
+ }
+ GeneratorKind::Pure => {
+ bail!("pure generators are disabled for now")
+ }
+ }
}
pub fn init_primops() {
@@ -52,52 +182,61 @@
.get()
.expect("primops data should be set on init");
- info!("get pkgs");
- let nixpkgs = &config.nixpkgs;
- let default_pkgs = &config.default_pkgs;
- let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);
- let generators = nix_go!(default_mk_secret_generators(Obj {
- recipients: <Vec<String>>::new(),
- }));
- let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;
+ let shared_def = config.secret_definition(&secret).context("failed to get shared secret definition")?;
- info!("call package");
- let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
- let default_generator = call_package
- .call(generator.clone())
- .context("calling callPackage with generator")?
- .call(Value::new_attrs(HashMap::new()))
- .context("providing extra callPackage args")?;
+ let (shared, generator, expected_owners) = if generator.is_string() {
+ assert_eq!(generator.to_string()?, "shared", "asserted by nixos type system");
+ let Some(shared_def) = shared_def else {
+ bail!("secret {secret} is defined on host {host} as shared, but there is no shared secret with same name defined at fleetConfiguration.secrets.{secret}.generator")
+ };
+ let expected_owners = shared_def.expected_owners()?;
+
+ ensure!(expected_owners.contains(&host), "secret {secret} does not define {host} as expected owner");
- info!("get parts");
- let mut parts: BTreeMap<String, GeneratorPart> = nix_go_json!(default_generator.parts);
- info!("got parts: {parts:?}");
+ (true, 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")
+ }
- let Some(existing) = config
- .host_secret(&host, &secret) else {
- bail!("missing secret {secret} for host {host}; secret needs regeneration")
+ (false, generator.clone(), BTreeSet::from_iter([host.clone()]))
};
- info!("got existing: {existing:?}");
+ let default_generator_drv = get_default_generator_drv(config, &generator).context("failed to evaluate default generator")?;
+ let expectations = Expectations {
+ parts: nix_go_json!(default_generator_drv.parts),
+ generation_data: nix_go_json!(default_generator_drv.generationData),
+ owners: expected_owners,
+ };
+
+ let reason: RegenerationReason = 'regenerate: {
+ let Some(existing) = config
+ .host_secret(&host, &secret) else {
+ break 'regenerate RegenerationReason::Missing;
+ };
+ if let Some(reason) = secret_needs_regeneration(&existing, &expectations) {
+ break 'regenerate reason;
+ }
- let mut out = HashMap::new();
+ let mut parts = expectations.parts.clone();
- 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;
- };
- if definition.encrypted != part.raw.encrypted {
- bail!("secret {secret} part {part_name} is supposed to be {}, but it is {}; secret needs regeneration", if definition.encrypted {"encrypted"} else {"unencrypted"}, if part.raw.encrypted {"encrypted"} else {"unencrypted"});
+ 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;
+ };
+ 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()))])));
}
- out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));
- }
- if !parts.is_empty(){
- let defs = parts.keys().collect_vec();
- bail!("secret parts are defined, but not stored: {defs:?}, secret needs regeneration")
- }
+ assert!(parts.is_empty(), "secret part is missing, secret_needs_regeneration should check that");
- Ok(Value::new_attrs(out))
+ return Ok(Value::new_attrs(out))
+ };
+
+ todo!()
+
+
},
)
.register();
crates/fleet-base/src/secret.rsdiffbeforeafterboth--- a/crates/fleet-base/src/secret.rs
+++ b/crates/fleet-base/src/secret.rs
@@ -1,16 +1,8 @@
-use std::collections::BTreeSet;
+use std::collections::{BTreeMap, BTreeSet};
use chrono::{DateTime, Utc};
-use crate::fleetdata::FleetSecretData;
-
-#[derive(Debug)]
-pub struct Expectations {
- pub owners: BTreeSet<String>,
- pub generation_data: serde_json::Value,
- pub public_parts: BTreeSet<String>,
- pub private_parts: BTreeSet<String>,
-}
+use crate::fleetdata::{Expectations, FleetSecretData, FleetSecretDistribution, GeneratorPart};
#[derive(thiserror::Error, Debug)]
pub enum RegenerationReason {
@@ -34,56 +26,62 @@
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: &FleetSecretData,
- owners: &BTreeSet<String>,
+ secret: &FleetSecretDistribution,
expectations: &Expectations,
) -> Option<RegenerationReason> {
- if !owners.is_empty() {
- let added: BTreeSet<String> = expectations.owners.difference(owners).cloned().collect();
- if !added.is_empty() {
- return Some(RegenerationReason::OwnersAdded(added));
- }
+ let added: BTreeSet<String> = expectations
+ .owners
+ .difference(&secret.owners)
+ .cloned()
+ .collect();
+ if !added.is_empty() {
+ return Some(RegenerationReason::OwnersAdded(added));
+ }
- let removed: BTreeSet<String> = owners.difference(&expectations.owners).cloned().collect();
- if !removed.is_empty() {
- return Some(RegenerationReason::OwnersRemoved(removed));
- }
+ let removed: BTreeSet<String> = secret
+ .owners
+ .difference(&expectations.owners)
+ .cloned()
+ .collect();
+ if !removed.is_empty() {
+ return Some(RegenerationReason::OwnersRemoved(removed));
}
- if secret.generation_data != expectations.generation_data {
+ if secret.secret.generation_data != expectations.generation_data {
return Some(RegenerationReason::GenerationData {
expected: expectations.generation_data.clone(),
- found: secret.generation_data.clone(),
+ found: secret.secret.generation_data.clone(),
});
}
- if !expectations.public_parts.is_empty() || !expectations.private_parts.is_empty() {
- let expected: BTreeSet<String> = expectations
- .public_parts
- .union(&expectations.private_parts)
- .cloned()
- .collect();
- let found: BTreeSet<String> = secret.parts.keys().cloned().collect();
+ 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 });
- }
+ if found != expected {
+ return Some(RegenerationReason::PartList { expected, found });
+ }
- for (name, value) in secret.parts.iter() {
- if value.raw.encrypted {
- if !expectations.private_parts.contains(name) {
- return Some(RegenerationReason::ExpectedPrivate(name.clone()));
- }
- } else if !expectations.public_parts.contains(name) {
- return Some(RegenerationReason::ExpectedPublic(name.clone()));
+ 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.expires_at {
+ if let Some(expiration) = secret.secret.expires_at {
// TODO: Leeway?
if expiration < Utc::now() {
return Some(RegenerationReason::Expired(expiration));
crates/nix-eval/src/lib.rsdiffbeforeafterboth--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -731,6 +731,10 @@
}
pub fn has_field(&self, field: &str) -> Result<bool> {
+ if !matches!(self.type_of(), NixType::Attrs) {
+ bail!("invalid type: expected attrs");
+ }
+
let f = init_field_name(field);
with_default_context(|c, es| unsafe { has_attr_byname(c, self.0, es, f.as_ptr().cast()) })
}
@@ -881,6 +885,12 @@
pub fn is_null(&self) -> bool {
matches!(self.type_of(), NixType::Null)
}
+ pub fn is_string(&self) -> bool {
+ matches!(self.type_of(), NixType::String)
+ }
+ pub fn is_attrs(&self) -> bool {
+ matches!(self.type_of(), NixType::Attrs)
+ }
}
impl From<String> for Value {
crates/nix-eval/src/util.rsdiffbeforeafterboth--- a/crates/nix-eval/src/util.rs
+++ b/crates/nix-eval/src/util.rs
@@ -1,23 +1,15 @@
use std::time::Instant;
use anyhow::bail;
-use serde::Deserialize;
use tracing::{debug, warn};
use crate::{Value, nix_go_json};
-
-#[derive(Deserialize, Debug)]
-struct Assertion {
- assertion: bool,
- message: String,
-}
#[tracing::instrument(level = "info", skip(val))]
-pub async fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
+pub fn assert_warn(action: &str, val: &Value) -> anyhow::Result<()> {
let before_errors = Instant::now();
let errors: Vec<String> = nix_go_json!(val.errors);
- // let assertions: Vec<Assertion> = nix_go_json!(val.assertions);
- debug!("errors evaluation took {:?} {errors:?} ", before_errors.elapsed());
+ debug!("errors evaluation took {:?}", before_errors.elapsed());
if !errors.is_empty() {
bail!(
"failed with error{}{}",
lib/default.nixdiffbeforeafterboth--- a/lib/default.nix
+++ b/lib/default.nix
@@ -160,7 +160,7 @@
mkImpureSecretGenerator,
}:
mkImpureSecretGenerator {
- # TODO: Escape prompt?
+ # TODO: Escape prompt/part (preferrably just use env) to prevent shell injection
script = ''
${kdePackages.kdialog}/bin/kdialog --inputbox "${prompt}" | gh private -o $out/${part}
'';
modules/secrets.nixdiffbeforeafterboth--- a/modules/secrets.nix
+++ b/modules/secrets.nix
@@ -89,6 +89,7 @@
# If set - script will be run on remote machine, otherwise it will be run with fleet project in CWD
# (Some secrets-encryption-in-git/managed PKI solution is expected)
impureOn ? null,
+ generationData ? null,
parts,
}:
(prev.writeShellScript "impureGenerator.sh" ''
@@ -117,7 +118,7 @@
'').overrideAttrs
(old: {
passthru = {
- inherit impureOn parts;
+ inherit impureOn parts generationData;
generatorKind = "impure";
};
});