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

difftreelog

secret management

znqnrmwwYaroslav Bolyukin2026-01-22parent: #81ddc19.patch.diff
in: trunk

11 files changed

modifiedCargo.lockdiffbeforeafterboth
--- 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"
modifiedCargo.tomldiffbeforeafterboth
--- 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" }
addedcmds/repl-plugin-unstable/Cargo.tomldiffbeforeafterboth
--- /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" }
addedcmds/repl-plugin-unstable/src/lib.rsdiffbeforeafterboth
--- /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();
+}
modifiedcrates/fleet-base/Cargo.tomldiffbeforeafterboth
--- 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
modifiedcrates/fleet-base/src/lib.rsdiffbeforeafterboth
4pub mod host;4pub mod host;
5mod keys;5mod keys;
6pub mod opts;6pub mod opts;
7pub mod primops;
7pub mod secret;8pub mod secret;
9pub mod secret_storage;
810
addedcrates/fleet-base/src/primops.rsdiffbeforeafterboth
--- /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<String>,
+	public: Vec<String>,
+}
+
+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();
+}
addedcrates/fleet-base/src/secret_storage.rsdiffbeforeafterboth
--- /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<Vec<u8>>,
+	new_content: Option<Vec<u8>>,
+) -> 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<String>) -> 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<String>) -> 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<String>,
+	start: &str,
+	end: &str,
+	modify: impl Fn(&mut Vec<String>) -> 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)
+}
addedcrates/fleet-base/test.tomldiffbeforeafterboth
--- /dev/null
+++ b/crates/fleet-base/test.toml
@@ -0,0 +1 @@
+hello = 999
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth
--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -952,7 +952,7 @@
 
 type UserClosure<const N: usize> = Box<dyn Fn([&Value; N]) -> Result<Value>>;
 
-struct NativeFn(*mut PrimOp);
+pub struct NativeFn(*mut PrimOp);
 impl NativeFn {
 	pub fn new<const N: usize>(
 		name: &'static CStr,
modifiedflake.nixdiffbeforeafterboth
--- 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" ]; })
               ];