--- 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 { 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::(&host, "deploy_kind").await? { + if let Some(deploy_kind) = opts.action_attr::(&host, "deploy_kind")? { host.set_deploy_kind(deploy_kind); }; - if let Some(destination) = opts.action_attr::(&host, "dest").await? { + if let Some(destination) = opts.action_attr::(&host, "dest")? { host.set_session_destination(destination); }; - if let Some(legacy) = opts.action_attr::(&host, "legacy_ssh_store").await? { + if let Some(legacy) = opts.action_attr::(&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"); --- 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 = >::new(); - let host = config.system_config(&host).await?; + let host = config.system_config(&host)?; if external { let data: Vec = nix_go_json!(host.network.externalIps); out.extend(data); --- 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; --- 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 { --- 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(()) => { --- 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, + pub generation_data: serde_json::Value, + pub parts: BTreeMap, +} +#[derive(Deserialize, Debug, Clone)] +pub struct GeneratorPart { + pub encrypted: bool, +} --- a/crates/fleet-base/src/host.rs +++ b/crates/fleet-base/src/host.rs @@ -471,10 +471,13 @@ cmd.run().await } } + +struct HostSecretDefinition(Value); + impl ConfigHost { // TOCTOU is possible here in case if config is changed, but this case is not handled anywhere anyway, // assuming getting tags always returns the same value. - pub async fn tags(&self) -> Result> { + pub fn tags(&self) -> Result> { if let Some(v) = self.groups.get() { return Ok(v.clone()); } @@ -487,7 +490,7 @@ Ok(tags) } - pub async fn nixos_config(&self) -> Result { + pub fn nixos_config(&self) -> Result { if let Some(v) = self.nixos_config.get() { return Ok(v.clone()); } @@ -495,7 +498,7 @@ bail!("local host has no nixos_config"); }; let nixos_config = nix_go!(host_config.nixos.config); - assert_warn("nixos config evaluation", &nixos_config).await?; + assert_warn("nixos config evaluation", &nixos_config)?; let _ = self.nixos_config.set(nixos_config.clone()); @@ -522,7 +525,7 @@ } /// Packages for this host, resolved with nixpkgs overlays - pub async fn pkgs(&self) -> Result { + pub fn pkgs(&self) -> Result { if let Some(value) = &self.pkgs_override { return Ok(value.clone()); } @@ -534,17 +537,29 @@ } } +pub struct SharedSecretDefinition(Value); +impl SharedSecretDefinition { + pub fn expected_owners(&self) -> Result> { + let secret = &self.0; + Ok(nix_go_json!(secret.expectedOwners)) + } + pub fn generator(&self) -> Result { + let secret = &self.0; + Ok(nix_go!(secret.generator)) + } +} + impl Config { - pub async fn tagged_hostnames(&self, tag: &str) -> Result> { + pub fn tagged_hostnames(&self, tag: &str) -> Result> { let config = &self.config_field; let tagged: Vec = nix_go_json!(config.taggedWith[{ tag }]); Ok(tagged) } - pub async fn expand_owner_set(&self, owners: Vec) -> Result> { + pub fn expand_owner_set(&self, owners: Vec) -> Result> { let mut out = BTreeSet::new(); for owner in owners { if let Some(tag) = owner.strip_prefix('@') { - let hosts = self.tagged_hostnames(tag).await?; + let hosts = self.tagged_hostnames(tag)?; out.extend(hosts); } else { out.insert(owner); @@ -574,7 +589,7 @@ } } - pub async fn host(&self, name: &str) -> Result { + pub fn host(&self, name: &str) -> Result { let config = &self.config_field; let host_config = nix_go!(config.hosts[{ name }]); @@ -595,23 +610,23 @@ legacy_ssh_store: OnceCell::new(), }) } - pub async fn list_hosts(&self) -> Result> { + pub fn list_hosts(&self) -> Result> { let config = &self.config_field; let names = nix_go!(config.hosts).list_fields()?; let mut out = vec![]; for name in names { - out.push(self.host(&name).await?); + out.push(self.host(&name)?); } Ok(out) } // TODO: Replace usages with .host().nixos_config - pub async fn system_config(&self, host: &str) -> Result { + pub fn system_config(&self, host: &str) -> Result { 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 async fn list_configured_shared(&self) -> Result> { + pub fn list_configured_shared(&self) -> Result> { let config_field = &self.config_field; nix_go!(config_field.sharedSecrets).list_fields() } @@ -659,6 +674,17 @@ data.secrets.get(secret).cloned() } + pub fn secret_definition(&self, secret: &str) -> Result> { + let config = &self.config_field; + let shared_secrets = nix_go!(config.secrets); + if !shared_secrets.has_field(secret)? { + return Ok(None); + } + Ok(Some(SharedSecretDefinition(nix_go!( + shared_secrets[secret] + )))) + } + // 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 --- 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 { 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) -> Result>> { - 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::>() @@ -57,12 +57,7 @@ #[allow(dead_code)] pub async fn orphaned_data(&self) -> Result> { 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 --- 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, ) -> Result> { 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 { + pub fn should_skip(&self, host: &ConfigHost) -> Result { 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(&self, host: &ConfigHost, attr: &str) -> Result> + pub fn action_attr(&self, host: &ConfigHost, attr: &str) -> Result> where T::Err: Sync, anyhow::Error: From, { - 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> { + pub fn action_attr_str(&self, host: &ConfigHost, attr: &str) -> Result> { 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, assert: bool) -> Result { + pub fn build(&self, nix_args: Vec, assert: bool) -> Result { 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")?; } --- 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, - public: Vec, +pub static PRIMOPS_DATA: OnceLock = 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) -> Result { + 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 { + let host_on = config.local_host(); + get_pkgs_and_generators(&host_on, vec![]) } +pub fn call_package(config: &Config, pkgs: &Value, package: &Value) -> Result { + 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 { + 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 { + let kind: GeneratorKind = nix_go_json!(default_generator_drv.generatorKind); + + match kind { + GeneratorKind::Impure => { + let impure_on: Option = 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 = 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: >::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 = 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(); --- 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, - pub generation_data: serde_json::Value, - pub public_parts: BTreeSet, - pub private_parts: BTreeSet, -} +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), + + #[error("secret is not generated for this host")] + Missing, } pub fn secret_needs_regeneration( - secret: &FleetSecretData, - owners: &BTreeSet, + secret: &FleetSecretDistribution, expectations: &Expectations, ) -> Option { - if !owners.is_empty() { - let added: BTreeSet = expectations.owners.difference(owners).cloned().collect(); - if !added.is_empty() { - return Some(RegenerationReason::OwnersAdded(added)); - } + let added: BTreeSet = expectations + .owners + .difference(&secret.owners) + .cloned() + .collect(); + if !added.is_empty() { + return Some(RegenerationReason::OwnersAdded(added)); + } - let removed: BTreeSet = owners.difference(&expectations.owners).cloned().collect(); - if !removed.is_empty() { - return Some(RegenerationReason::OwnersRemoved(removed)); - } + let removed: BTreeSet = 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 = expectations - .public_parts - .union(&expectations.private_parts) - .cloned() - .collect(); - let found: BTreeSet = secret.parts.keys().cloned().collect(); + let expected: BTreeSet = expectations.parts.keys().cloned().collect(); + let found: BTreeSet = 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)); --- 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 { + 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 for Value { --- 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 = nix_go_json!(val.errors); - // let assertions: Vec = 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{}{}", --- 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} ''; --- 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"; }; });