From b00d46da7979cafd5f2038d4f2f7fa9c605bcf49 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Mon, 08 Dec 2025 00:57:05 +0000 Subject: [PATCH] secret management --- --- a/Cargo.lock +++ b/Cargo.lock @@ -1092,6 +1092,7 @@ "time", "tokio", "tokio-util", + "toml_edit", "tracing", ] @@ -2744,6 +2745,13 @@ checksum = "c3160422bbd54dd5ecfdca71e5fd59b7b8fe2b1697ab2baf64f6d05dcc66d298" [[package]] +name = "repl-plugin-unstable" +version = "0.1.0" +dependencies = [ + "fleet-base", +] + +[[package]] name = "reqwest" version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3618,6 +3626,43 @@ ] [[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.11.4", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] name = "tonic" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4469,6 +4514,15 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + +[[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" package.version = "0.1.0" package.edition = "2024" -package.rust-version = "1.86.0" +package.rust-version = "1.89.0" [workspace.dependencies] better-command = { path = "./crates/better-command" } --- /dev/null +++ b/cmds/repl-plugin-unstable/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "repl-plugin-unstable" +version.workspace = true +edition.workspace = true +rust-version.workspace = true + +[lib] +crate-type = ["cdylib"] + +[dependencies] +fleet-base = { version = "0.1.0", path = "../../crates/fleet-base" } --- /dev/null +++ b/cmds/repl-plugin-unstable/src/lib.rs @@ -0,0 +1,6 @@ +use fleet_base::primops::init_primops; + +#[unsafe(no_mangle)] +fn nix_plugin_entry() { + init_primops(); +} --- a/crates/fleet-base/Cargo.toml +++ b/crates/fleet-base/Cargo.toml @@ -8,7 +8,7 @@ age.workspace = true anyhow.workspace = true better-command.workspace = true -chrono = "0.4.41" +chrono = { version = "0.4.41", features = ["serde"] } clap = { workspace = true, features = ["derive"] } fleet-shared.workspace = true futures = "0.3.31" @@ -27,5 +27,6 @@ thiserror.workspace = true time = { version = "0.3.41", features = ["parsing"] } tokio.workspace = true -tokio-util = "0.7.15" +tokio-util = { version = "0.7.15", features = ["codec"] } +toml_edit = "0.23.7" tracing.workspace = true --- a/crates/fleet-base/src/lib.rs +++ b/crates/fleet-base/src/lib.rs @@ -4,4 +4,6 @@ pub mod host; mod keys; pub mod opts; +pub mod primops; pub mod secret; +pub mod secret_storage; --- /dev/null +++ b/crates/fleet-base/src/primops.rs @@ -0,0 +1,45 @@ +use nix_eval::NativeFn; + +#[derive(thiserror::Error, Debug)] +enum Error {} + +struct Parts { + encrypted: Vec, + public: Vec, +} + +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; +} + +struct FsSecretsBackend { + +} + +pub fn init_primops() { + NativeFn::new( + c"fleet_ensure_secret", + c"Ensure secret existence for a host, regenerating it in case of some mismatch", + [ + c"host", + c"secret", + c"expected_parts", + c"expected_encrypted_parts", + c"generator", + ], + |[ + host, + secret, + expected_parts, + expected_encrypted_parts, + generator, + ]| { + + todo!() + }, + ) + .register(); +} --- /dev/null +++ b/crates/fleet-base/src/secret_storage.rs @@ -0,0 +1,278 @@ +use anyhow::{Result, bail, ensure}; +use itertools::Itertools; +use std::fs::{File, metadata}; +use std::io::{self, ErrorKind, Read, Write}; +use std::path::PathBuf; +use std::str::FromStr; +use std::{env, fs}; + +use tempfile::{TempPath, tempfile_in}; +use toml_edit::{Document, DocumentMut, Formatted, Item, Value}; + +struct Name(String); + +fn encode_name(name: &str) -> Name { + assert!( + !name.starts_with(['_', '.']), + "groups should not start with _ or ." + ); + assert!( + !name.chars().any(|c| c == '/'), + "group name should not contain internal slash" + ); + Name(name.to_owned()) +} + +enum RewriteError { + ConcurrentCreate, + ConcurrentDelete, + ConcurrentModify, + ConcurrentWrite, + Io(io::Error), + Persist(tempfile::PersistError), +} + +fn safe_rewrite( + path: &PathBuf, + old_content: Option>, + new_content: Option>, +) -> Result<(), RewriteError> { + let mut f = match (old_content.is_some(), new_content.is_some()) { + (false, true) => match File::create_new(path) { + Ok(v) => v, + Err(e) if e.kind() == ErrorKind::AlreadyExists => { + return Err(RewriteError::ConcurrentCreate); + } + Err(e) => return Err(RewriteError::Io(e)), + }, + (true, _) => match File::open(&path) { + Ok(v) => v, + Err(_e) => return Err(RewriteError::ConcurrentDelete), + }, + (false, false) => match metadata(&path) { + Err(e) if e.kind() == ErrorKind::NotFound => { + return Ok(()); + } + Ok(_) => return Err(RewriteError::ConcurrentCreate), + Err(e) => return Err(RewriteError::Io(e)), + }, + }; + f.lock().map_err(RewriteError::Io)?; + let mut check_content = vec![]; + f.read_to_end(&mut check_content) + .map_err(RewriteError::Io)?; + match &old_content { + Some(old) => { + if old != &check_content { + return Err(RewriteError::ConcurrentModify); + } + } + None => { + if !check_content.is_empty() { + return Err(RewriteError::ConcurrentDelete); + } + } + } + if let Some(new_content) = new_content { + if Some(&new_content) == old_content.as_ref() { + return Ok(()); + } + let dir = path.parent().expect("file is in directory, thus not root"); + let mut tempfile = tempfile::Builder::new() + .prefix(".rewrite-") + .tempfile_in(dir) + .map_err(RewriteError::Io)?; + tempfile.write_all(&new_content).map_err(RewriteError::Io)?; + tempfile.flush().map_err(RewriteError::Io)?; + tempfile.persist(path).map_err(RewriteError::Persist)?; + } else { + fs::remove_file(path).map_err(RewriteError::Io)?; + } + let _ = f.unlock(); + Ok(()) +} +fn update_string(path: PathBuf, modify: impl Fn(&mut Option) -> Result<()>) -> Result<()> { + loop { + let orig = match fs::read_to_string(&path) { + Ok(v) => Some(v), + Err(e) if e.kind() == ErrorKind::NotFound => None, + Err(e) => return Err(e.into()), + }; + let mut edit = orig.clone(); + modify(&mut edit); + + match safe_rewrite(&path, orig.map(String::into), edit.map(String::into)) { + Ok(()) => return Ok(()), + Err( + RewriteError::ConcurrentCreate + | RewriteError::ConcurrentModify + | RewriteError::ConcurrentWrite + | RewriteError::ConcurrentDelete, + ) => { + continue; + } + Err(RewriteError::Io(io)) => return Err(io.into()), + Err(RewriteError::Persist(io)) => return Err(io.into()), + } + } +} +fn update_toml(path: PathBuf, modify: impl Fn(&mut DocumentMut) -> Result<()>) -> Result<()> { + update_string(path, |str| { + let mut doc = match str { + None => DocumentMut::new(), + Some(v) => DocumentMut::from_str(v)?, + }; + modify(&mut doc)?; + if doc.is_empty() { + *str = None + } else { + *str = Some(doc.to_string()) + } + Ok(()) + }) +} +fn update_lines(path: PathBuf, modify: impl Fn(&mut Vec) -> Result<()>) -> Result<()> { + update_string(path, |str| { + let mut list = if let Some(str) = str { + str.split('\n').map(|s| s.to_owned()).collect_vec() + } else { + vec![] + }; + let had_end_newline = if list.last().map(|v| v.as_str()) == Some("") { + list.pop(); + true + } else { + false + }; + modify(&mut list)?; + if list.is_empty() { + *str = None + } else { + if had_end_newline { + list.push("".to_owned()) + } + *str = Some(list.join("\n")); + } + Ok(()) + }) +} +fn update_section( + data: &mut Vec, + start: &str, + end: &str, + modify: impl Fn(&mut Vec) -> Result<()>, +) -> Result<()> { + let first = data + .iter() + .enumerate() + .filter(|(_, v)| *v == start) + .at_most_one() + .map_err(|_| anyhow::anyhow!("there should be at most one section start"))? + .map(|(v, _)| v); + let last = data + .iter() + .enumerate() + .filter(|(_, v)| *v == end) + .at_most_one() + .map_err(|_| anyhow::anyhow!("there should be at most one section end"))? + .map(|(v, _)| v); + + match (first, last) { + (None, None) => { + let mut out = Vec::new(); + modify(&mut out)?; + if out.is_empty() { + return Ok(()); + } + data.push(start.to_owned()); + data.extend(out); + data.push(end.to_owned()); + Ok(()) + } + (None, Some(_)) | (Some(_), None) => { + bail!("mismatched section start/end") + } + (Some(first), Some(last)) => { + ensure!(first < last, "section end should come after start"); + let mut out = data[first + 1..last] + .iter() + .map(|v| v.to_owned()) + .collect_vec(); + modify(&mut out)?; + if out.is_empty() { + data.drain(first..=last); + } else { + data.splice(first + 1..last, out); + } + Ok(()) + } + } +} + +struct Group { + path: PathBuf, +} +impl Group { + fn new(path: PathBuf) -> Self { + Self { path } + } + fn manage(&self, manager: &str) {} + fn ensure_managing(&self, manager: &str) { + if !self.has_stored() { + return; + } + let managed = match fs::read_to_string(self.path.join(".managed_by")) { + Ok(found_manager) => found_manager.lines().any(|line| line == manager), + Err(e) if e.kind() == ErrorKind::NotFound => true, + Err(e) => panic!("{e}"), + }; + assert!(managed); + } + fn has_stored(&self) -> bool { + match fs::metadata(&self.path) { + Ok(d) => d.is_dir(), + Err(e) if e.kind() == ErrorKind::NotFound => false, + Err(e) => panic!("{e}"), + } + } +} + +struct Root { + path: PathBuf, +} +impl Root { + fn new(path: PathBuf) -> Self { + Self { path } + } + fn subgroup(&self, name: &str) -> Group { + Group::new(self.path.join(name)) + } +} + +#[test] +fn test() { + let mut data = vec![ + "a".to_owned(), + "b".to_owned(), + "start".to_owned(), + "c".to_owned(), + "d".to_owned(), + "end".to_owned(), + "e".to_owned(), + "f".to_owned(), + ]; + update_section(&mut data, "start", "end", |a| { + a.push("vv".to_owned()); + Ok(()) + }) + .unwrap(); + dbg!(&data); + // for v in 0..1000 { + // update_toml(PathBuf::from("./test.toml"), |e| { + // e.as_table_mut() + // .insert("hello", Item::Value(Value::Integer(Formatted::new(v)))); + // }) + // .expect("update") + // } + // v.subgroup(name) +} --- /dev/null +++ b/crates/fleet-base/test.toml @@ -0,0 +1 @@ +hello = 999 --- a/crates/nix-eval/src/lib.rs +++ b/crates/nix-eval/src/lib.rs @@ -952,7 +952,7 @@ type UserClosure = Box Result>; -struct NativeFn(*mut PrimOp); +pub struct NativeFn(*mut PrimOp); impl NativeFn { pub fn new( name: &'static CStr, --- a/flake.nix +++ b/flake.nix @@ -185,6 +185,7 @@ inputs'.nix.packages.nix-flake-c inputs'.nix.packages.nix-fetchers-c inputs'.nix.packages.nix-store-c + inputs'.nix.packages.nix (rage.overrideAttrs { cargoFeatures = [ "plugin" ]; }) ]; -- gitstuff