git.delta.rocks / jrsonnet / refs/commits / e89ca39c2450

difftreelog

feat mkAskPass generator

tzqksnqzYaroslav Bolyukin2026-01-22parent: #d706686.patch.diff
in: trunk

8 files changed

modifiedCargo.tomldiffbeforeafterboth
24tracing = "0.1"24tracing = "0.1"
25tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }25tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
26
27[profile.dev]
28panic = "abort"
29[profile.release]
30panic = "abort"
2631
modifiedcrates/fleet-base/src/fleetdata.rsdiffbeforeafterboth
121 })121 })
122}122}
123123
124#[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}
128128
129#[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}
146146
147#[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 {
modifiedcrates/fleet-base/src/opts.rsdiffbeforeafterboth
23use 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};
2728
28#[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)?));
215216
216 init_primops(data.clone());217 init_primops();
217218
218 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 }
267
268 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 }));
279
280 PRIMOPS_DATA
281 .set(config.clone())
282 .map_err(|_| ())
283 .expect("only one fleet config may exist per process");
284 Ok(config)
279 }285 }
280}286}
281287
modifiedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
1use std::cell::OnceCell;
1use std::collections::HashMap;2use std::collections::{BTreeMap, HashMap};
2use std::sync::{Arc, Mutex};3use std::sync::{Arc, Mutex, OnceLock};
34
5use 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};
610
7use crate::fleetdata::{FleetData, FleetSecrets};11use crate::fleetdata::{FleetData, FleetSecrets};
12use crate::host::Config;
813
9#[derive(thiserror::Error, Debug)]14#[derive(thiserror::Error, Debug)]
10enum Error {}15enum Error {}
2328
24struct FsSecretsBackend {}29struct FsSecretsBackend {}
30
31pub static PRIMOPS_DATA: OnceLock<Config> = OnceLock::new();
32
33#[derive(Deserialize, Debug)]
34struct GeneratorPart {
35 encrypted: bool,
36}
2537
26pub 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()?;
49
50 info!("get config");
51 let config = PRIMOPS_DATA
52 .get()
53 .expect("primops data should be set on init");
54
55 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)?;
63
64 info!("call package");
65 let call_package = nix_go!(nixpkgs.lib.callPackageWith(pkgs_and_generators));
66 let default_generator = call_package
67 .call(generator.clone())
68 .context("calling callPackage with generator")?
69 .call(Value::new_attrs(HashMap::new()))
70 .context("providing extra callPackage args")?;
71
72 info!("get parts");
73 let mut parts: BTreeMap<String, GeneratorPart> = nix_go_json!(default_generator.parts);
74 info!("got parts: {parts:?}");
75
76 let Some(existing) = config
77 .host_secret(&host, &secret) else {
78 bail!("missing secret {secret} for host {host}; secret needs regeneration")
79 };
80
81 info!("got existing: {existing:?}");
82
83 let mut out = HashMap::new();
84
85 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 }
99
100 Ok(Value::new_attrs(out))
38 },101 },
39 )102 )
40 .register();103 .register();
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
12use serde::de::DeserializeOwned;12use serde::de::DeserializeOwned;
1313
14pub use anyhow::Result;14pub use anyhow::Result;
15use tracing::instrument;15use tracing::{info, instrument, warn};
1616
17use 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};
4343
62 type nix_fetchers_settings;62 type nix_fetchers_settings;
63 include!("nix-eval/src/lib.hh");63 include!("nix-eval/src/lib.hh");
6464
65 #[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::Unknown
298 }299 }
299}300}
300301
301static 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});
303306
304thread_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}
426429
430#[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 {}
430434
697 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() };
949
950 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 &Value
937 });957 });
958 info!("args forced");
938 let ctx: &mut NixContext = unsafe { &mut *context.cast() };959 let ctx: &mut NixContext = unsafe { &mut *context.cast() };
960
961 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 }
969
970 let state: &EvalState = unsafe { std::mem::transmute(&state) };
939971
940 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}
952986
953type UserClosure<const N: usize> = Box<dyn Fn([&Value; N]) -> Result<Value>>;987type UserClosure<const N: usize> = Box<dyn Fn(&EvalState, [&Value; N]) -> Result<Value>>;
954988
955pub 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 Box
964 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())))
modifiedcrates/nix-eval/src/macros.rsdiffbeforeafterboth
16 $(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)*);
modifiedlib/default.nixdiffbeforeafterboth
149 }149 }
150 );150 );
151
152 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 '';
164
165 parts.${part}.encrypted = true;
166 }
167 );
151168
152 /**169 /**
153 Generate a random RSA keypair170 Generate a random RSA keypair
251 mkBytes268 mkBytes
252 mkHexBytes269 mkHexBytes
253 mkBase64Bytes270 mkBase64Bytes
271 mkAskPass
254 ;272 ;
255273
256 strings =274 strings =
modifiedmodules/nixos/secrets.nixdiffbeforeafterboth
19 submodule19 submodule
20 str20 str
21 attrsOf21 attrsOf
22 nullOr
23 unspecified22 unspecified
24 uniq23 uniq
25 functionTo24 functionTo
77 {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 thunks
105 # https://github.com/NixOS/nix/issues/12800
106 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 hostName
111 secretName
112 generator
113 ] (builtins.fleetEnsureHostSecret
114 hostName
115 secretName
116 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 ];
136148
137 systemd.services.fleet-install-secrets = mkIf useSysusers {149 systemd.services.fleet-install-secrets = mkIf useSysusers {