difftreelog
feat mkAskPass generator
in: trunk
8 files changed
Cargo.tomldiffbeforeafterboth24tracing = "0.1"24tracing = "0.1"25tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }25tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }2627[profile.dev]28panic = "abort"29[profile.release]30panic = "abort"2631crates/fleet-base/src/fleetdata.rsdiffbeforeafterboth121 })121 })122}122}123123124#[derive(Serialize, Deserialize, Clone)]124#[derive(Serialize, Deserialize, Clone, Debug)]125pub struct FleetSecretPart {125pub struct FleetSecretPart {126 pub raw: SecretData,126 pub raw: SecretData,127}127}128128129#[derive(Serialize, Deserialize, Clone)]129#[derive(Serialize, Deserialize, Clone, Debug)]130#[serde(rename_all = "camelCase")]130#[serde(rename_all = "camelCase")]131#[must_use]131#[must_use]132pub struct FleetSecretData {132pub struct FleetSecretData {144 pub generation_data: Value,144 pub generation_data: Value,145}145}146146147#[derive(Serialize, Deserialize, Clone)]147#[derive(Serialize, Deserialize, Clone, Debug)]148#[serde(rename_all = "camelCase")]148#[serde(rename_all = "camelCase")]149#[must_use]149#[must_use]150pub struct FleetSecretDistribution {150pub struct FleetSecretDistribution {crates/fleet-base/src/opts.rsdiffbeforeafterboth23use crate::{23use crate::{24 fleetdata::FleetData,24 fleetdata::FleetData,25 host::{Config, ConfigHost, FleetConfigInternals}, primops::init_primops,25 host::{Config, ConfigHost, FleetConfigInternals},26 primops::{PRIMOPS_DATA, init_primops},26};27};272828#[derive(Clone)]29#[derive(Clone)]213 std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;214 std::fs::read_to_string(&fleet_data_path).context("reading fleet state (fleet.nix)")?;214 let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));215 let data = Arc::new(Mutex::new(FleetData::from_str(&bytes)?));215216216 init_primops(data.clone());217 init_primops();217218218 let mut fetch_settings = FetchSettings::new();219 let mut fetch_settings = FetchSettings::new();219 fetch_settings.set(c"warn-dirty", c"false");220 fetch_settings.set(c"warn-dirty", c"false");265 gc_now();266 gc_now();266 }267 }267268 Ok(Config(Arc::new(FleetConfigInternals {268 let config = Config(Arc::new(FleetConfigInternals {269 directory,269 directory,270 data,270 data,271 flake_outputs: flake,271 flake_outputs: flake,275 default_pkgs,275 default_pkgs,276 nixpkgs,276 nixpkgs,277 localhost: self.localhost.to_owned(),277 localhost: self.localhost.to_owned(),278 })))278 }));279280 PRIMOPS_DATA281 .set(config.clone())282 .map_err(|_| ())283 .expect("only one fleet config may exist per process");284 Ok(config)279 }285 }280}286}281287crates/fleet-base/src/primops.rsdiffbeforeafterboth1use std::cell::OnceCell;1use std::collections::HashMap;2use std::collections::{BTreeMap, HashMap};2use std::sync::{Arc, Mutex};3use std::sync::{Arc, Mutex, OnceLock};345use anyhow::{Context, bail};6use itertools::Itertools;4use nix_eval::{NativeFn, Value};7use nix_eval::{NativeFn, Value, nix_go, nix_go_json};8use serde::Deserialize;5use tracing::info;9use tracing::{info, warn};6107use crate::fleetdata::{FleetData, FleetSecrets};11use crate::fleetdata::{FleetData, FleetSecrets};12use crate::host::Config;8139#[derive(thiserror::Error, Debug)]14#[derive(thiserror::Error, Debug)]10enum Error {}15enum Error {}232824struct FsSecretsBackend {}29struct FsSecretsBackend {}3031pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();3233#[derive(Deserialize, Debug)]34struct GeneratorPart {35 encrypted: bool,36}253726pub fn init_primops(secrets: Arc<Mutex<FleetData>>) {38pub fn init_primops() {27 info!("initializing primops");39 info!("initializing primops");28 NativeFn::new(40 NativeFn::new(29 c"__fleetEnsureHostSecret",41 c"__fleetEnsureHostSecret",30 c"Ensure secret existence for a host, regenerating it in case of some mismatch",42 c"Ensure secret existence for a host, regenerating it in case of some mismatch",31 [c"host", c"secret", c"generator"],43 [c"host", c"secret", c"generator"],32 |[host, secret, generator]| {44 |es, [host, secret, generator]| {45 info!("get host");46 let host = host.to_string()?;33 todo!("ensure secret");47 info!("get secret");48 let secret = secret.to_string()?;4950 info!("get config");51 let config = PRIMOPS_DATA52 .get()53 .expect("primops data should be set on init");5455 info!("get pkgs");56 let nixpkgs = &config.nixpkgs;57 let default_pkgs = &config.default_pkgs;58 let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);59 let generators = nix_go!(default_mk_secret_generators(Obj {60 recipients: <Vec<String>>::new(),61 }));62 let pkgs_and_generators = default_pkgs.clone().attrs_update(generators)?;6364 info!("call package");65 let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));66 let default_generator = call_package67 .call(generator.clone())68 .context("calling callPackage with generator")?69 .call(Value::new_attrs(HashMap::new()))70 .context("providing extra callPackage args")?;7172 info!("get parts");73 let mut parts: BTreeMap<String, GeneratorPart> = nix_go_json!(default_generator.parts);74 info!("got parts: {parts:?}");7576 let Some(existing) = config77 .host_secret(&host, &secret) else {78 bail!("missing secret {secret} for host {host}; secret needs regeneration")79 };8081 info!("got existing: {existing:?}");8283 let mut out = HashMap::new();8485 for (part_name, part) in &existing.secret.parts {86 let Some(definition) = parts.remove(part_name) else {87 warn!("secret {secret} part {part_name} is stored, but not defined in nixos config, it will not be passed to nix");88 continue;89 };90 if definition.encrypted != part.raw.encrypted {91 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"});92 }34 Ok(Value::new_attrs(HashMap::from_iter([(93 out.insert(part_name.as_str(), Value::new_attrs(HashMap::from_iter([("raw", Value::new_str(&part.raw.to_string()))])));35 "raw",94 }36 Value::new_str("rawData"),95 if !parts.is_empty(){37 )])))96 let defs = parts.keys().collect_vec();97 bail!("secret parts are defined, but not stored: {defs:?}, secret needs regeneration")98 }99100 Ok(Value::new_attrs(out))38 },101 },39 )102 )40 .register();103 .register();crates/nix-eval/src/lib.rsdiffbeforeafterboth12use serde::de::DeserializeOwned;12use serde::de::DeserializeOwned;131314pub use anyhow::Result;14pub use anyhow::Result;15use tracing::instrument;15use tracing::{info, instrument, warn};161617use self::logging::{ErrorInfoBuilder, nix_logging_cxx};17use self::logging::{ErrorInfoBuilder, nix_logging_cxx};18use self::nix_cxx::set_fetcher_setting;18use self::nix_cxx::set_fetcher_setting;22 GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp,22 GC_thread_is_registered, GC_unregister_my_thread, ListBuilder as c_list_builder, PrimOp,23 PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value,23 PrimOpFun, Store as c_store, StorePath as c_store_path, alloc_primop, alloc_value,24 bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free,24 bindings_builder_free, bindings_builder_insert, c_context, c_context_create, c_context_free,25 clear_err, copy_value, err_code, err_info_msg, err_msg, eval_state_build,25 clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW,26 err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build,26 eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,27 eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,27 expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,28 expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,28 flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,29 flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,36 make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,37 make_attrs, make_bindings_builder, make_list, make_list_builder, realised_string,37 realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,38 realised_string_free, realised_string_get_buffer_size, realised_string_get_buffer_start,38 realised_string_get_store_path, realised_string_get_store_path_count, register_primop,39 realised_string_get_store_path, realised_string_get_store_path_count, register_primop,39 set_err_msg, setting_set, state_free, store_copy_closure, store_get_fs_closure, store_open,40 set_err_msg, setting_set, state_free, store_open, store_parse_path, store_path_free,40 store_parse_path, store_path_free, store_path_name, string_realise, value, value_call,41 store_path_name, string_realise, value, value_call, value_decref, value_force, value_incref,41 value_decref, value_incref,42};42};434362 type nix_fetchers_settings;62 type nix_fetchers_settings;63 include!("nix-eval/src/lib.hh");63 include!("nix-eval/src/lib.hh");646465 #[allow(clippy::missing_safety_doc)]65 unsafe fn set_fetcher_setting(66 unsafe fn set_fetcher_setting(66 settings: *mut nix_fetchers_settings,67 settings: *mut nix_fetchers_settings,67 setting: *const c_char,68 setting: *const c_char,111#[derive(Debug)]112#[derive(Debug)]112#[repr(i32)]113#[repr(i32)]113pub enum NixErrorKind {114pub enum NixErrorKind {114 Unknown = 1,115 Unknown = err_NIX_ERR_UNKNOWN,115 Overflow = 2,116 Overflow = err_NIX_ERR_OVERFLOW,116 Key = 3,117 Key = err_NIX_ERR_KEY,117 Generic = 4,118 Generic = err_NIX_ERR_NIX_ERROR,118}119}119impl NixErrorKind {120impl NixErrorKind {120 fn from_int(v: c_int) -> Option<Self> {121 fn from_int(v: c_int) -> Option<Self> {121 Some(match v {122 Some(match v {122 0 => return None,123 0 => return None,123 -1 => Self::Unknown,124 nix_raw::err_NIX_ERR_UNKNOWN => Self::Unknown,124 -2 => Self::Overflow,125 nix_raw::err_NIX_ERR_OVERFLOW => Self::Overflow,125 -3 => Self::Key,126 nix_raw::err_NIX_ERR_KEY => Self::Key,126 -4 => Self::Generic,127 nix_raw::err_NIX_ERR_NIX_ERROR => Self::Generic,127 _ => {128 _ => {128 debug_assert!(false, "unexpected nix error kind: {v}");129 debug_assert!(false, "unexpected nix error kind: {v}");129 Self::Unknown130 Self::Unknown298 }299 }299}300}300301301static GLOBAL_STATE: LazyLock<GlobalState> =302static GLOBAL_STATE: LazyLock<GlobalState> = LazyLock::new(|| {303 info!("initializing nix global state");302 LazyLock::new(|| GlobalState::new().expect("global state init shouldn't fail"));304 GlobalState::new().expect("global state init shouldn't fail")305});303306304thread_local! {307thread_local! {305 static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));308 static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));424 }427 }425}428}426429430#[repr(transparent)]427struct EvalState(*mut c_eval_state);431pub struct EvalState(*mut c_eval_state);428unsafe impl Send for EvalState {}432unsafe impl Send for EvalState {}429unsafe impl Sync for EvalState {}433unsafe impl Sync for EvalState {}430434697 let builtin = Self::eval("builtins.toString")?;701 let builtin = Self::eval("builtins.toString")?;698 builtin.call(self.clone())702 builtin.call(self.clone())699 }703 }704 fn force(&mut self, s: *mut nix_raw::EvalState) -> Result<()> {705 with_default_context(|c, _| unsafe { value_force(c, s, self.0) })?;706 Ok(())707 }700 pub fn to_string(&self) -> Result<String> {708 pub fn to_string(&self) -> Result<String> {709 let ty = self.type_of();710 if !matches!(ty, NixType::String) {711 bail!("unexpected type: {ty:?}, expected string");712 }701 let mut str_out = String::new();713 let mut str_out = String::new();702 with_default_context(|c, _| unsafe {714 with_default_context(|c, _| unsafe {703 get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast())715 get_string(c, self.0, Some(copy_nix_str), (&raw mut str_out).cast())931 ret: *mut value,943 ret: *mut value,932) {944) {933 let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };945 let user_closure: &UserClosure<N> = unsafe { &*user_data.cast_const().cast() };946 let mut e = None;934 let args: [&Value; N] = array::from_fn(|i| {947 let args: [&Value; N] = array::from_fn(|i| {935 let v: &Value = unsafe { &*args.add(i).cast_const().cast() };948 let v: &mut Value = unsafe { &mut *args.add(i).cast() };949950 info!("forcing arg");951 if matches!(v.type_of(), NixType::Thunk)952 && let Err(err) = v.force(state)953 {954 e = Some(err);955 };936 v956 v as &Value937 });957 });958 info!("args forced");938 let ctx: &mut NixContext = unsafe { &mut *context.cast() };959 let ctx: &mut NixContext = unsafe { &mut *context.cast() };960961 if let Some(e) = e {962 warn!("set err = {e}");963 unsafe { init_int(context, ret, 0) };964 return ctx.set_err(965 NixErrorKind::Unknown,966 &CString::new(e.to_string()).expect("forcing argument value failed"),967 );968 }969970 let state: &EvalState = unsafe { std::mem::transmute(&state) };939971940 match user_closure(args) {972 match user_closure(state, args) {941 Ok(v) => {973 Ok(v) => {942 unsafe { copy_value(context, ret, v.0) };974 unsafe { copy_value(context, ret, v.0) };943 }975 }944 Err(e) => {976 Err(e) => {977 unsafe { init_int(context, ret, 0) };978 warn!("set err = {e:#?}");945 ctx.set_err(979 ctx.set_err(946 NixErrorKind::Unknown,980 NixErrorKind::Unknown,947 &CString::new(e.to_string()).expect("error should not contain internal nuls"),981 &CString::new(e.to_string()).expect("error should not contain internal nuls"),950 }984 }951}985}952986953type UserClosure<const N: usize> = Box<dyn Fn([&Value; N]) -> Result<Value>>;987type UserClosure<const N: usize> = Box<dyn Fn(&EvalState, [&Value; N]) -> Result<Value>>;954988955pub struct NativeFn(*mut PrimOp);989pub struct NativeFn(*mut PrimOp);956impl NativeFn {990impl NativeFn {957 pub fn new<const N: usize>(991 pub fn new<const N: usize>(958 name: &'static CStr,992 name: &'static CStr,959 doc: &'static CStr,993 doc: &'static CStr,960 args: [&'static CStr; N],994 args: [&'static CStr; N],961 f: impl Fn([&Value; N]) -> Result<Value> + 'static,995 f: impl Fn(&EvalState, [&Value; N]) -> Result<Value> + 'static,962 ) -> Self {996 ) -> Self {963 // Double-boxing to make it thin pointer, as vtable gets outside of first Box997 // Double-boxing to make it thin pointer, as vtable gets outside of first Box964 let closure: Box<UserClosure<N>> = Box::new(Box::new(f));998 let closure: Box<UserClosure<N>> = Box::new(Box::new(f));1003 c"__uppercaseSuffix2",1037 c"__uppercaseSuffix2",1004 c"make string uppercase and add suffix",1038 c"make string uppercase and add suffix",1005 [c"str", c"suffix"],1039 [c"str", c"suffix"],1006 |[str, suffix]: [&Value; 2]| {1040 |_, [str, suffix]: [&Value; 2]| {1007 let str = str.to_string()?;1041 let str = str.to_string()?;1008 let suffix = suffix.to_string()?;1042 let suffix = suffix.to_string()?;1009 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1043 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1038 c"uppercase_suffix",1072 c"uppercase_suffix",1039 c"make string uppercase and add suffix",1073 c"make string uppercase and add suffix",1040 [c"str", c"suffix"],1074 [c"str", c"suffix"],1041 |[str, suffix]: [&Value; 2]| {1075 |es, [str, suffix]: [&Value; 2]| {1042 let str = str.to_string()?;1076 let str = str.to_string()?;1043 let suffix = suffix.to_string()?;1077 let suffix = suffix.to_string()?;1044 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))1078 Ok(Value::new_str(&format!("{}{suffix}", str.to_uppercase())))crates/nix-eval/src/macros.rsdiffbeforeafterboth16 $(nix_expr_inner!(@obj($o) $($tt)*);)?16 $(nix_expr_inner!(@obj($o) $($tt)*);)?17 }};17 }};18 (@obj($o:ident)) => {{}};18 (@obj($o:ident)) => {{}};19 (Obj { }) => {{20 use $crate::{nix_expr_inner};21 let out = std::collections::hash_map::HashMap::new();22 Value::new_attrs(out)23 }};19 (Obj { $($tt:tt)* }) => {{24 (Obj { $($tt:tt)+ }) => {{20 use $crate::{nix_expr_inner};25 use $crate::{nix_expr_inner};21 let mut out = std::collections::hash_map::HashMap::new();26 let mut out = std::collections::hash_map::HashMap::new();22 nix_expr_inner!(@obj(out) $($tt)*);27 nix_expr_inner!(@obj(out) $($tt)*);lib/default.nixdiffbeforeafterboth149 }149 }150 );150 );151152 mkAskPass =153 { prompt ? "Secret value", part ? "secret" }:154 (155 {156 kdePackages,157 mkImpureSecretGenerator,158 }:159 mkImpureSecretGenerator {160 # TODO: Escape prompt?161 script = ''162 ${kdePackages.kdialog}/bin/kdialog --inputbox "${prompt}" | gh private -o $out/${part}163 '';164165 parts.${part}.encrypted = true;166 }167 );151168152 /**169 /**153 Generate a random RSA keypair170 Generate a random RSA keypair251 mkBytes268 mkBytes252 mkHexBytes269 mkHexBytes253 mkBase64Bytes270 mkBase64Bytes271 mkAskPass254 ;272 ;255273256 strings =274 strings =modules/nixos/secrets.nixdiffbeforeafterboth19 submodule19 submodule20 str20 str21 attrsOf21 attrsOf22 nullOr23 unspecified22 unspecified24 uniq23 uniq25 functionTo24 functionTo77 {76 {78 options = {77 options = {79 parts = mkOption {78 parts = mkOption {80 type = attrsOf (secretPartType secretName);79 type = uniq (attrsOf (secretPartType secretName));81 description = "Definition of secret parts";80 description = "Definition of secret parts";82 };81 };83 generator = mkOption {82 generator = mkOption {84 type = uniq (nullOr (functionTo package));83 type = uniq (functionTo package);85 description = "Derivation to evaluate for secret generation";84 description = "Derivation to evaluate for secret generation";86 default = null;87 };85 };88 mode = mkOption {86 mode = mkOption {89 type = str;87 type = str;103 };101 };104 };102 };105 config = {103 config = {104 # C api is broken in regard to thunks105 # https://github.com/NixOS/nix/issues/12800106 parts = builtins.fleetEnsureHostSecret sysConfig.networking.hostName secretName config.generator;106 parts = let 107 hostName = sysConfig.networking.hostName;108 generator = config.generator;109 in builtins.deepSeq [110 hostName111 secretName112 generator113 ] (builtins.fleetEnsureHostSecret114 hostName115 secretName116 generator);107 };117 };108 }118 }109 );119 );110 secretsData = (mapAttrs (_: s: s.definition) config.secrets);111 secretsFile = pkgs.writeTextFile {120 secretsFile = pkgs.writeTextFile {112 name = "secrets.json";121 name = "secrets.json";113 text = toJSON secretsData;122 text = toJSON config.system.secretsData;114 };123 };115 useSysusers =124 useSysusers =116 (config.systemd ? sysusers && config.systemd.sysusers.enable)125 (config.systemd ? sysusers && config.systemd.sysusers.enable)121 secrets = mkOption {130 secrets = mkOption {122 type = attrsOf secretType;131 type = attrsOf secretType;123 default = { };132 default = { };124 apply = v: (mapAttrs (_: secret: secret.parts // { definition = secret; }) v);133 apply = mapAttrs (_: secret: secret.parts // {definition = secret;});125 description = "Host-local secrets";134 description = "Host-local secrets";126 };135 };127 system.secretsData = mkOption {136 system.secretsData = mkOption {128 type = unspecified;137 type = unspecified;129 default = { };138 default = mapAttrs (_: s:139 (removeAttrs s.definition ["generator"]) // {140 parts = mapAttrs (_: part: removeAttrs part ["data"]) s.definition.parts;141 }142 ) config.secrets;130 description = "secrets.json contents";143 description = "secrets.json contents";131 };144 };132 };145 };133 config = {146 config = {134 system = { inherit secretsData; };135 environment.systemPackages = [ pkgs.fleet-install-secrets ];147 environment.systemPackages = [ pkgs.fleet-install-secrets ];136148137 systemd.services.fleet-install-secrets = mkIf useSysusers {149 systemd.services.fleet-install-secrets = mkIf useSysusers {