difftreelog
refactor c bindings
in: trunk
26 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -198,9 +198,9 @@
[[package]]
name = "anyhow"
-version = "1.0.83"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arc-swap"
@@ -222,7 +222,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -374,9 +374,9 @@
[[package]]
name = "cc"
-version = "1.0.97"
+version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
[[package]]
name = "cfg-if"
@@ -468,7 +468,7 @@
"heck",
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -587,6 +587,7 @@
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
+ "digest",
"fiat-crypto",
"platforms",
"rustc_version",
@@ -602,7 +603,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -658,14 +659,39 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core",
+ "serde",
+ "sha2",
+ "subtle",
+ "zeroize",
]
[[package]]
name = "either"
-version = "1.11.0"
+version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
+checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]]
name = "encode_unicode"
@@ -894,7 +920,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -928,6 +954,18 @@
]
[[package]]
+name = "generator-helper"
+version = "0.1.0"
+dependencies = [
+ "age",
+ "anyhow",
+ "clap",
+ "ed25519-dalek",
+ "fleet-shared",
+ "rand",
+]
+
+[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1033,7 +1071,7 @@
"serde",
"serde_derive",
"thiserror",
- "toml 0.8.12",
+ "toml 0.8.13",
"unic-langid",
]
@@ -1075,7 +1113,7 @@
"proc-macro2",
"quote",
"strsim",
- "syn 2.0.63",
+ "syn 2.0.66",
"unic-langid",
]
@@ -1089,7 +1127,7 @@
"i18n-config",
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -1151,9 +1189,9 @@
[[package]]
name = "instant"
-version = "0.1.12"
+version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
dependencies = [
"cfg-if",
]
@@ -1241,9 +1279,9 @@
[[package]]
name = "libc"
-version = "0.2.154"
+version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libm"
@@ -1253,9 +1291,9 @@
[[package]]
name = "libmimalloc-sys"
-version = "0.1.37"
+version = "0.1.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7"
+checksum = "0e7bb23d733dfcc8af652a78b7bf232f0e967710d044732185e561e47c0336b6"
dependencies = [
"cc",
"libc",
@@ -1269,9 +1307,9 @@
[[package]]
name = "linux-raw-sys"
-version = "0.4.13"
+version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
@@ -1321,9 +1359,9 @@
[[package]]
name = "mimalloc"
-version = "0.1.41"
+version = "0.1.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d"
+checksum = "e9186d86b79b52f4a77af65604b51225e8db1d6ee7e3f41aec1e40829c71a176"
dependencies = [
"libmimalloc-sys",
]
@@ -1336,9 +1374,9 @@
[[package]]
name = "miniz_oxide"
-version = "0.7.2"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
+checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae"
dependencies = [
"adler",
]
@@ -1533,9 +1571,9 @@
[[package]]
name = "parking_lot"
-version = "0.12.2"
+version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -1608,7 +1646,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -1717,9 +1755,9 @@
[[package]]
name = "proc-macro2"
-version = "1.0.82"
+version = "1.0.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
+checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
dependencies = [
"unicode-ident",
]
@@ -1911,7 +1949,7 @@
"proc-macro2",
"quote",
"rust-embed-utils",
- "syn 2.0.63",
+ "syn 2.0.66",
"walkdir",
]
@@ -2041,9 +2079,9 @@
[[package]]
name = "serde"
-version = "1.0.202"
+version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
@@ -2059,13 +2097,13 @@
[[package]]
name = "serde_derive"
-version = "1.0.202"
+version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -2081,9 +2119,9 @@
[[package]]
name = "serde_spanned"
-version = "0.6.5"
+version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
+checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
dependencies = [
"serde",
]
@@ -2245,9 +2283,9 @@
[[package]]
name = "syn"
-version = "2.0.63"
+version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
+checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
@@ -2308,22 +2346,22 @@
[[package]]
name = "thiserror"
-version = "1.0.60"
+version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.60"
+version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -2401,7 +2439,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -2438,9 +2476,9 @@
[[package]]
name = "toml"
-version = "0.8.12"
+version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
+checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
dependencies = [
"serde",
"serde_spanned",
@@ -2450,18 +2488,18 @@
[[package]]
name = "toml_datetime"
-version = "0.6.5"
+version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
+checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
-version = "0.22.12"
+version = "0.22.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef"
+checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
dependencies = [
"indexmap",
"serde",
@@ -2489,7 +2527,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
[[package]]
@@ -2708,7 +2746,7 @@
"once_cell",
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
"wasm-bindgen-shared",
]
@@ -2730,7 +2768,7 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -2964,5 +3002,5 @@
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.63",
+ "syn 2.0.66",
]
Cargo.tomldiffbeforeafterboth--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,27 @@
[workspace.dependencies]
nixlike = { path = "./crates/nixlike" }
better-command = { path = "./crates/better-command" }
-bifrostlink = "0.1.0"
-uuid = { version = "1.7.0", features = ["v4"] }
-tokio = { version = "1.36.0", features = ["fs", "rt", "macros", "sync", "time", "rt-multi-thread"] }
fleet-shared = { path = "./crates/fleet-shared" }
+tokio = { version = "1.36.0", features = [
+ "fs",
+ "rt",
+ "macros",
+ "sync",
+ "time",
+ "rt-multi-thread",
+] }
+# Using fixed version for rust on stable nixos branches.
+clap = { version = ">=4.4, <4.5", features = [
+ "derive",
+ "env",
+ "wrap_help",
+ "unicode",
+] }
+age = { version = "0.10", features = ["ssh"] }
+anyhow = "1.0"
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tempfile = "3.10"
+nix = {version = "0.27.1", features = ["user", "fs"]}
cmds/fleet/Cargo.tomldiffbeforeafterboth--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -9,27 +9,21 @@
nixlike.workspace = true
better-command.workspace = true
tokio.workspace = true
-anyhow = "1.0"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
+clap.workspace = true
+age = { workspace = true, features = ["armor"] }
+anyhow.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+tempfile.workspace = true
time = { version = "0.3", features = ["serde"] }
-tempfile = "3.10"
once_cell = "1.19"
hostname = "0.3"
age-core = "0.10"
peg = "0.8"
-age = { version = "0.10", features = ["ssh", "armor"] }
base64 = "0.22.1"
chrono = { version = "0.4", features = ["serde"] }
-# Using fixed version for rust on stable nixos branches.
-clap = { version = ">=4.4, <4.5", features = [
- "derive",
- "env",
- "wrap_help",
- "unicode",
-] }
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
tokio-util = { version = "0.7", features = ["codec"] }
async-trait = "0.1"
futures = "0.3"
@@ -40,9 +34,7 @@
"supports-color",
"supports-colors",
] }
-r2d2 = "0.8.10"
abort-on-drop = "0.2"
-unindent = "0.2"
regex = "1.10"
openssh = "0.10"
crossterm = { version = "0.27.0", features = ["use-dev-tty"] }
@@ -51,12 +43,13 @@
tracing-indicatif = { version = "0.3", optional = true }
human-repr = { version = "1.1", optional = true }
indicatif = { version = "0.17", optional = true }
+nix-eval = { version = "0.1.0", path = "../../crates/nix-eval" }
[features]
# Not quite stable
indicatif = [
- "tracing-indicatif",
+ "dep:tracing-indicatif",
"dep:indicatif",
- "human-repr",
+ "dep:human-repr",
"better-command/indicatif",
]
cmds/fleet/src/better_nix_eval.rsdiffbeforeafterboth--- a/cmds/fleet/src/better_nix_eval.rs
+++ b/cmds/fleet/src/better_nix_eval.rs
@@ -12,892 +12,14 @@
use better_command::{ClonableHandler, Handler, NixHandler, NoopHandler};
use futures::StreamExt;
use itertools::Itertools;
-use r2d2::{Pool, PooledConnection};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use tokio::process::{ChildStderr, ChildStdin, ChildStdout, Command};
use tokio::select;
use tokio::sync::{mpsc, oneshot, Mutex};
-use tokio_util::codec::{FramedRead, LinesCodec};
use tracing::{debug, error, warn, Level};
-
-const REPL_DELIMITER: &str = "\"FLEET_MAGIC_REPL_DELIMITER\"";
-
-pub struct NixSessionInner {
- full_delimiter: String,
- nix_handler: ClonableHandler<NixHandler>,
- out: OutputHandler,
- stdin: ChildStdin,
- string_wrapping: (String, String),
- number_wrapping: (String, String),
-
- executing_command: Arc<Mutex<()>>,
-
- next_id: u32,
- free_list: Vec<u32>,
-}
-const TRAIN_STRING: &str = "\"TRAIN_STRING\"";
-const TRAIN_NUMBER: &str = "13141516";
-
-#[must_use]
-struct ErrorCollector<'i, H> {
- collected: Vec<String>,
- inner: &'i mut H,
-}
-impl<'i, H> ErrorCollector<'i, H> {
- fn new(inner: &'i mut H) -> Self {
- Self {
- collected: vec![],
- inner,
- }
- }
-}
-impl<H> ErrorCollector<'_, H> {
- fn handle_line_inner(&mut self, msg: &str) -> bool {
- let Some(msg) = msg.strip_prefix("@nix ") else {
- return false;
- };
- #[derive(Deserialize)]
- struct ErrorAction {
- action: String,
- level: u32,
- msg: String,
- }
- let Ok(act) = serde_json::from_str::<ErrorAction>(msg) else {
- return false;
- };
- if act.action != "msg" || act.level != 0 {
- return false;
- }
- self.collected.push(act.msg);
- true
- }
- fn finish(self) -> Result<()> {
- // fn dedent(s: String) -> String {
- // s.split('\n').filter(|s| !s.trim().is_empty()).map(|v| v.)
- // }
- if !self.collected.is_empty() {
- bail!(
- "{}",
- self.collected
- .iter()
- .map(|v| {
- if let Some(f) = v.strip_prefix("\u{1b}[31;1merror:\u{1b}[0m ") {
- let v = unindent::unindent(f.trim_start());
- v.trim().to_owned()
- } else {
- v.to_owned()
- }
- })
- .join("\n")
- );
- }
- Ok(())
- }
- fn flush(self) {
- for line in self.collected {
- warn!("{line}");
- }
- }
-}
-impl<H: Handler> Handler for ErrorCollector<'_, H> {
- fn handle_line(&mut self, e: &str) {
- if self.handle_line_inner(e) {
- return;
- }
- self.inner.handle_line(e)
- }
-}
-
-enum OutputLine {
- Out(String),
- Err(String),
-}
-struct OutputHandler {
- rx: mpsc::Receiver<OutputLine>,
- _cancel_handle: oneshot::Receiver<()>,
-}
-impl OutputHandler {
- fn new(out: ChildStdout, err: ChildStderr) -> Self {
- let mut out = FramedRead::new(out, LinesCodec::new());
- let mut err = FramedRead::new(err, LinesCodec::new());
- let (tx, rx) = mpsc::channel(20);
- let (mut cancelled, _cancel_handle) = oneshot::channel();
- tokio::spawn(async move {
- loop {
- select! {
- // We should receive errors earlier than synchronization
- biased;
- e = err.next() => {
- let Some(Ok(e)) = e else {
- if e.is_some() {
- error!("bad repl stderr: {e:?}");
- }
- continue;
- };
- let _ = tx.send(OutputLine::Err(e)).await;
- }
- o = out.next() => {
- let Some(Ok(o)) = o else {
- if o.is_some() {
- error!("bad repl stdout: {o:?}");
- }
- continue;
- };
- let _ = tx.send(OutputLine::Out(o)).await;
- }
- // Reader doesn't care about stdout, as this is cancelled.
- // Error still might be useful, to process leftover span closures?
- _ = cancelled.closed() => {
- break;
- }
- }
- }
- });
- Self { rx, _cancel_handle }
- }
- async fn next(&mut self) -> Option<OutputLine> {
- self.rx.recv().await
- }
-}
-
-struct WarnHandler;
-impl Handler for WarnHandler {
- fn handle_line(&mut self, e: &str) {
- warn!(target: "nix", "{e}")
- }
-}
-
-impl NixSessionInner {
- async fn new(flake: &OsStr, extra_args: impl IntoIterator<Item = &OsStr>) -> Result<Self> {
- let mut cmd = Command::new("nix");
- cmd.arg("repl")
- .arg(flake)
- .arg("--log-format")
- .arg("internal-json");
- for arg in extra_args {
- cmd.arg(arg);
- }
- cmd.stdin(Stdio::piped());
- cmd.stdout(Stdio::piped());
- cmd.stderr(Stdio::piped());
- let cmd = cmd.spawn()?;
- let stdout = cmd.stdout.unwrap();
- let stderr = cmd.stderr.unwrap();
- let mut out = OutputHandler::new(stdout, stderr);
- let mut stdin = cmd.stdin.unwrap();
- // Standard repl hello doesn't work with internal-json logger
- stdin.write_all(REPL_DELIMITER.as_bytes()).await?;
- stdin.write_all(b"\n").await?;
- stdin.flush().await?;
- let nix_handler = NixHandler::default();
- let mut full_delimiter = None;
- let mut errors = vec![];
- while let Some(line) = out.next().await {
- let line = match line {
- OutputLine::Out(o) => o,
- OutputLine::Err(_e) => {
- // Handle startup errors, but skip repl hello?
- errors.push(_e);
- continue;
- }
- };
- if line.contains(REPL_DELIMITER) {
- debug!("discovered repl delimiter with added colors: {line}");
- full_delimiter = Some(line.to_owned());
- break;
- }
- }
- let Some(full_delimiter) = full_delimiter else {
- for e in errors {
- error!("{e}");
- }
- bail!("failed to discover delimiter");
- };
- let mut res = Self {
- full_delimiter,
- nix_handler: ClonableHandler::new(nix_handler),
- out,
- stdin,
- string_wrapping: Default::default(),
- number_wrapping: Default::default(),
-
- executing_command: Arc::new(Mutex::new(())),
-
- next_id: 0,
- free_list: vec![],
- };
- res.train().await?;
- Ok(res)
- }
- async fn train(&mut self) -> Result<()> {
- {
- let full_string = self
- .execute_expression_raw(TRAIN_STRING, &mut NoopHandler)
- .await?;
- let string_offset = full_string.find(TRAIN_STRING).expect("contained");
- let string_prefix = &full_string[..string_offset];
- let string_suffix = &full_string[string_offset + TRAIN_STRING.len()..];
- self.string_wrapping = (string_prefix.to_owned(), string_suffix.to_owned());
- }
- {
- let full_number = self
- .execute_expression_raw(TRAIN_NUMBER, &mut NoopHandler)
- .await?;
- let number_offset = full_number.find(TRAIN_NUMBER).expect("contained");
- let number_prefix = &full_number[..number_offset];
- let number_suffix = &full_number[number_offset + TRAIN_NUMBER.len()..];
- self.number_wrapping = (number_prefix.to_owned(), number_suffix.to_owned());
- }
- Ok(())
- }
- async fn send_command(&mut self, cmd: impl AsRef<[u8]>) -> Result<()> {
- if tracing::enabled!(Level::DEBUG) && cmd.as_ref() != REPL_DELIMITER.as_bytes() {
- let cmd_str = String::from_utf8_lossy(cmd.as_ref());
- tracing::debug!("{cmd_str}");
- };
- self.stdin.write_all(cmd.as_ref()).await?;
- self.stdin.write_all(b"\n").await?;
- Ok(())
- }
- async fn read_until_delimiter(&mut self, err_handler: &mut dyn Handler) -> Result<String> {
- let mut out = String::new();
- while let Some(line) = self.out.next().await {
- let line = match line {
- OutputLine::Out(out) => out,
- OutputLine::Err(err) => {
- err_handler.handle_line(&err);
- continue;
- }
- };
- if line == self.full_delimiter {
- return Ok(out);
- }
- if !out.is_empty() {
- out.push('\n');
- }
- out.push_str(&line);
- }
- bail!("didn't reached delimiter");
- }
- async fn execute_expression_number(&mut self, expr: impl AsRef<[u8]>) -> Result<u64> {
- let num = self.number_wrapping.clone();
- let n = self.execute_expression_wrapping(expr, &num).await?;
- Ok(n.parse::<u64>()?)
- }
- async fn execute_expression_string(&mut self, expr: impl AsRef<[u8]>) -> Result<String> {
- let num = self.string_wrapping.clone();
- let n = self.execute_expression_wrapping(expr, &num).await?;
- let str: String = serde_json::from_str(&n)?;
- Ok(str)
- }
- async fn execute_expression_to_json<V: DeserializeOwned>(
- &mut self,
- expr: impl AsRef<[u8]>,
- ) -> Result<V> {
- let mut fexpr = b"builtins.toJSON (".to_vec();
- fexpr.extend_from_slice(expr.as_ref());
- fexpr.push(b')');
- let v = self
- .execute_expression_string(fexpr)
- .await
- .context("string expression")?;
- serde_json::from_str(&v).context("json parse")
- }
- async fn execute_expression_wrapping(
- &mut self,
- expr: impl AsRef<[u8]>,
- wrapping: &(String, String),
- ) -> Result<String> {
- let mut nix_handler = self.nix_handler.clone();
- let mut collected = ErrorCollector::new(&mut nix_handler);
- let res = self.execute_expression_raw(expr, &mut collected).await?;
- if res.is_empty() {
- collected.finish()?;
- bail!("expected expression, got nothing")
- } else {
- collected.flush()
- };
- let Some(res) = res.strip_prefix(&wrapping.0) else {
- bail!("invalid type")
- };
- let Some(res) = res.strip_suffix(&wrapping.1) else {
- bail!("invalid type")
- };
- Ok(res.to_owned())
- }
- async fn execute_expression_empty(&mut self, expr: impl AsRef<[u8]>) -> Result<()> {
- let mut nix_handler = self.nix_handler.clone();
- let mut collected = ErrorCollector::new(&mut nix_handler);
- let v = self.execute_expression_raw(expr, &mut collected).await?;
- collected.finish()?;
- ensure!(v.is_empty(), "unexpected expression result");
- Ok(())
- }
- async fn execute_expression_raw(
- &mut self,
- expr: impl AsRef<[u8]>,
- err_handler: &mut dyn Handler,
- ) -> Result<String> {
- // Prevent two commands from being executed in parallel, messing with each other.
- let _lock = self.executing_command.clone();
- let _guard = _lock.lock().await;
-
- self.send_command(expr).await?;
- // It will be echoed
- self.send_command(REPL_DELIMITER).await?;
- self.read_until_delimiter(err_handler).await
- }
- async fn execute_assign(&mut self, expr: impl AsRef<str>) -> Result<u32> {
- let id = self.allocate_id();
- self.execute_expression_empty(format!("sess_field_{id} = {}", expr.as_ref()))
- .await?;
- Ok(id)
- }
-
- /// Id should be immediately used
- fn allocate_id(&mut self) -> u32 {
- if let Some(free) = self.free_list.pop() {
- free
- } else {
- let v = self.next_id;
- self.next_id += 1;
- v
- }
- }
- // Nix has no way to deallocate variable, yet GC will correct everything not reachable.
- // async fn free_id(&mut self, id: u32) -> Result<()> {
- // self.execute_expression_empty(format!("sess_field_{id} = null"))
- // .await?;
- // self.free_list.push(id);
- // Ok(())
- // }
-}
-
-#[derive(Clone)]
-pub struct NixSession(Arc<tokio::sync::Mutex<PooledConnection<NixSessionPoolInner>>>);
-
-#[derive(Clone)]
-pub struct NixExprBuilder {
- out: String,
- used_fields: Vec<Field>,
-}
-impl NixExprBuilder {
- pub fn object() -> Self {
- NixExprBuilder {
- out: "{ ".to_owned(),
- used_fields: Vec::new(),
- }
- }
- pub fn string(s: &str) -> Self {
- NixExprBuilder {
- out: nixlike::serialize(s)
- .expect("no problems with serializing_string")
- .trim_end()
- .to_owned(),
- used_fields: Vec::new(),
- }
- }
- pub fn serialized(v: impl Serialize) -> Self {
- let serialized = nixlike::serialize(v).expect("invalid value for apply");
- Self {
- out: serialized.trim_end().to_owned(),
- used_fields: Vec::new(),
- }
- }
- pub fn field(f: Field) -> Self {
- Self {
- out: format!("sess_field_{}", f.0.value.expect("no value")),
- used_fields: vec![f],
- }
- }
- pub fn end_obj(&mut self) {
- self.out.push('}');
- }
- pub fn obj_key(&mut self, name: Self, value: Self) {
- self.out.push_str(r#""${"#);
- self.extend(name);
- self.out.push_str(r#"}" = "#);
- self.extend(value);
- self.out.push_str("; ");
- }
-
- pub fn extend(&mut self, e: Self) {
- self.out.push_str(&e.out);
- self.used_fields.extend(e.used_fields);
- }
-
- #[allow(dead_code)]
- pub fn session(&self) -> NixSession {
- let mut session = None;
- for ele in &self.used_fields {
- if session.is_none() {
- session = Some(ele.0.session.clone());
- continue;
- }
- let session = &session.as_ref().expect("checked").0;
- let ele_sess = &ele.0.session.0;
- assert!(
- Arc::ptr_eq(session, ele_sess),
- "can't mix fields from different session"
- );
- }
- session.expect("expr without fields used")
- }
- #[allow(dead_code)]
- pub fn index_attr(&mut self, s: &str) {
- let escaped = nixlike::serialize(s).expect("string");
- self.out.push('.');
- self.out.push_str(escaped.trim_end());
- }
-}
-
-#[macro_export]
-macro_rules! nix_expr_inner {
- //(@munch_object FIXME: value should be arbitrary nix_expr_inner input... Time to write proc-macro?
- (@obj($o:ident) $field:ident, $($tt:tt)*) => {{
- $o.obj_key(
- NixExprBuilder::string(stringify!($field)),
- NixExprBuilder::field($field),
- );
- nix_expr_inner!(@obj($o) $($tt)*);
- }};
- (@obj($o:ident) $field:ident: $v:block, $($tt:tt)*) => {{
- $o.obj_key(
- NixExprBuilder::string(stringify!($field)),
- NixExprBuilder::serialized(&$v),
- );
- nix_expr_inner!(@obj($o) $($tt)*);
- }};
- (@obj($o:ident)) => {{}};
- (Obj { $($tt:tt)* }) => {{
- use $crate::{better_nix_eval::NixExprBuilder, nix_expr_inner};
- let mut out = NixExprBuilder::object();
- nix_expr_inner!(@obj(out) $($tt)*);
- out.end_obj();
- out
- }};
- (@field($o:ident) . $var:ident $($tt:tt)*) => {{
- $o.index_attr(stringify!($var));
- nix_expr_inner!(@field($o) $($tt)*);
- }};
- (@field($o:ident) [{ $v:expr }] $($tt:tt)*) => {{
- $o.push(Index::attr(&$v));
- nix_expr_inner!(@o($o) $($tt)*);
- }};
- (@field($o:ident) [ $($var:tt)+ ] $($tt:tt)*) => {{
- $o.push(Index::Expr($crate::nix_expr_inner!($($var)+)));
- nix_expr_inner!(@o($o) $($tt)*);
- }};
- (@field($o:ident) ($($var:tt)*) $($tt:tt)*) => {
- $o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
- nix_expr_inner!(@o($o) $($tt)*);
- };
- (@field($o:ident)) => {};
- ($field:ident $($tt:tt)*) => {{
- use $crate::{better_nix_eval::NixExprBuilder, nix_expr_inner};
- #[allow(unused_mut, reason = "might be used if indexed")]
- let mut out = NixExprBuilder::field($field.clone());
- nix_expr_inner!(@field(out) $($tt)*);
- out
- }};
- ($v:literal) => {{
- use $crate::better_nix_eval::NixExprBuilder;
- NixExprBuilder::string($v)
- }};
- ({$v:expr}) => {{
- use $crate::better_nix_eval::NixExprBuilder;
- NixExprBuilder::serialized(&$v)
- }}
-}
-#[macro_export]
-macro_rules! nix_expr {
- ($($tt:tt)+) => {{
- use $crate::{better_nix_eval::{NixExprBuilder, Field}, nix_expr_inner};
- let expr = nix_expr_inner!($($tt)+);
- Field::new(expr.session(), expr.out)
- }};
-}
-#[macro_export]
-macro_rules! nix_go {
- (@o($o:ident) . $var:ident $($tt:tt)*) => {{
- $o.push(Index::attr(stringify!($var)));
- nix_go!(@o($o) $($tt)*);
- }};
- (@o($o:ident) [{ $v:expr }] $($tt:tt)*) => {{
- $o.push(Index::attr(&$v));
- nix_go!(@o($o) $($tt)*);
- }};
- (@o($o:ident) [ $($var:tt)+ ] $($tt:tt)*) => {{
- $o.push(Index::Expr($crate::nix_expr_inner!($($var)+)));
- nix_go!(@o($o) $($tt)*);
- }};
- (@o($o:ident) ($($var:tt)*) $($tt:tt)*) => {
- $o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
- nix_go!(@o($o) $($tt)*);
- };
- (@o($o:ident) | $($var:tt)*) => {
- $o.push(Index::Pipe($crate::nix_expr_inner!($($var)+)));
- };
- (@o($o:ident)) => {};
- ($field:ident $($tt:tt)+) => {{
- use $crate::{nix_go, better_nix_eval::Index};
- let field = $field.clone();
- let mut out = vec![];
- nix_go!(@o(out) $($tt)*);
- field.select(out).await?
- }}
-}
-#[macro_export]
-macro_rules! nix_go_json {
- ($($tt:tt)*) => {{
- $crate::nix_go!($($tt)*).as_json().await?
- }};
-}
-#[derive(Clone)]
-pub enum Index {
- Var(String),
- String(String),
- #[allow(dead_code)]
- Apply(String),
- #[allow(dead_code)]
- Expr(NixExprBuilder),
- ExprApply(NixExprBuilder),
- Pipe(NixExprBuilder),
-}
-impl Index {
- pub fn var(v: impl AsRef<str>) -> Self {
- let v = v.as_ref();
- assert!(
- !(v.contains('.') | v.contains(' ')),
- "bad variable name: {v}"
- );
- Self::Var(v.to_owned())
- }
- pub fn attr(v: impl AsRef<str>) -> Self {
- Self::String(v.as_ref().to_owned())
- }
- #[allow(dead_code)]
- pub fn apply(v: impl Serialize) -> Self {
- let serialized = nixlike::serialize(v).expect("invalid value for apply");
- Self::Apply(serialized.trim_end().to_owned())
- }
-}
-impl Display for Index {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Index::Var(v) => {
- write!(f, "{v}")
- }
- Index::String(k) => {
- let v = nixlike::format_identifier(k.as_str());
- write!(f, ".{v}")
- }
- Index::Apply(o) => {
- write!(f, "<apply>({o})")
- }
- Index::Expr(e) => {
- write!(f, "[{}]", e.out)
- }
- Index::ExprApply(e) => {
- write!(f, "<apply>({})", e.out)
- }
- Index::Pipe(e) => {
- write!(f, "<map>({})", e.out)
- }
- }
- }
-}
-impl fmt::Debug for Index {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{self}")
- }
-}
-struct PathDisplay<'i>(&'i [Index]);
-impl Display for PathDisplay<'_> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- for i in self.0 {
- write!(f, "{i}")?;
- }
- Ok(())
- }
-}
-struct FieldInner {
- full_path: Option<Vec<Index>>,
- session: NixSession,
- value: Option<u32>,
-}
-fn context(op: &str, full_path: Option<&[Index]>, query: &str) -> String {
- if let Some(full_path) = &full_path {
- format!("on {op}, full path: {}", PathDisplay(full_path))
- } else {
- format!("query: {query:?}")
- }
-}
-#[derive(Clone)]
-pub struct Field(Arc<FieldInner>);
-impl Field {
- fn root(session: NixSession) -> Self {
- Self(Arc::new(FieldInner {
- full_path: Some(vec![]),
- session,
- value: None,
- }))
- }
- async fn new(session: NixSession, query: &str) -> Result<Self> {
- let vid = session
- .0
- .lock()
- .await
- .execute_assign(query)
- .await
- .with_context(|| context("new root", None, query))?;
- Ok(Self(Arc::new(FieldInner {
- full_path: None,
- session,
- value: Some(vid),
- })))
- }
- pub async fn field(session: NixSession, field: &str) -> Result<Self> {
- Self::root(session).select([Index::var(field)]).await
- }
- pub async fn select<'a>(&self, name: impl IntoIterator<Item = Index>) -> Result<Self> {
- let mut used_fields = Vec::new();
- let mut name = name.into_iter();
- let mut full_path = self.0.full_path.clone();
- let mut query = if let Some(id) = self.0.value {
- format!("sess_field_{id}")
- } else {
- let first = name.next();
- if let Some(Index::Var(i)) = first {
- if let Some(full_path) = &mut full_path {
- full_path.push(Index::Var(i.clone()));
- }
- i.clone()
- } else {
- panic!("first path item should be variable, got {first:?}")
- }
- };
- for v in name {
- if let Some(full_path) = &mut full_path {
- full_path.push(v.clone());
- }
- match v {
- Index::Var(_) => panic!("var item may only be first"),
- Index::String(s) => {
- let escaped = nixlike::serialize(s)?;
- query.push('.');
- query.push_str(escaped.trim());
- }
- Index::Apply(a) => {
- // In cases like `a {}.b` first `{}.b` will be evaluated, so `a {}` should be encased in `()`
- query = format!("({query} {a})");
- }
- Index::Expr(e) => {
- let index = Field::new(self.0.session.clone(), &e.out).await?;
- used_fields.push(index.clone());
- query.push('.');
- let index = format!("${{sess_field_{}}}", index.0.value.expect("value"));
- query.push_str(&index);
- }
- Index::ExprApply(e) => {
- let index = Field::new(self.0.session.clone(), &e.out).await?;
- used_fields.push(index.clone());
- query.push(' ');
- let index = format!("sess_field_{}", index.0.value.expect("value"));
- query.push_str(&index);
- query = format!("({query})");
- }
- Index::Pipe(v) => {
- let index = Field::new(self.0.session.clone(), &v.out).await?;
- used_fields.push(index.clone());
- let index = format!("sess_field_{}", index.0.value.expect("value"));
- query = format!("({index} {query})");
- }
- }
- }
- let vid = self
- .0
- .session
- .0
- .lock()
- .await
- .execute_assign(&query)
- .await
- .with_context(|| {
- if let Some(full_path) = &full_path {
- format!("full path: {}", PathDisplay(full_path))
- } else {
- format!("query: {query:?}")
- }
- })?;
- Ok(Self(Arc::new(FieldInner {
- full_path,
- session: self.0.session.clone(),
- value: Some(vid),
- })))
- }
- pub async fn as_json<V: DeserializeOwned>(&self) -> Result<V> {
- let id = self.0.value.expect("can't serialize root field");
- let query = format!("sess_field_{id}");
- self.0
- .session
- .0
- .lock()
- .await
- .execute_expression_to_json(&query)
- .await
- .with_context(|| context("as_json", self.0.full_path.as_deref(), &query))
- }
- #[allow(dead_code)]
- pub async fn has_field(&self, name: &str) -> Result<bool> {
- let id = self.0.value.expect("can't list root fields");
- let key = nixlike::escape_string(name);
- let query = format!("sess_field_{id} ? {key}");
- self.0
- .session
- .0
- .lock()
- .await
- .execute_expression_to_json(&query)
- .await
- .with_context(|| context("has_field", self.0.full_path.as_deref(), &query))
- }
- pub async fn list_fields(&self) -> Result<Vec<String>> {
- let id = self.0.value.expect("can't list root fields");
- let query = format!("builtins.attrNames sess_field_{id}");
- self.0
- .session
- .0
- .lock()
- .await
- .execute_expression_to_json(&query)
- .await
- .with_context(|| context("list field", self.0.full_path.as_deref(), &query))
- }
- pub async fn type_of(&self) -> Result<String> {
- let id = self.0.value.expect("can't list root fields");
- let query = format!("builtins.typeOf sess_field_{id}");
- self.0
- .session
- .0
- .lock()
- .await
- .execute_expression_to_json(&query)
- .await
- .with_context(|| context("type_of", self.0.full_path.as_deref(), &query))
- }
- #[allow(dead_code)]
- pub async fn import(&self) -> Result<Self> {
- let import = Self::new(self.0.session.clone(), "import").await?;
- Ok(nix_go!(self | import))
- }
- pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {
- let id = self.0.value.expect("can't use build on not-value");
- let query = format!(":b sess_field_{id}");
- let vid = self
- .0
- .session
- .0
- .lock()
- .await
- .execute_expression_raw(&query, &mut NixHandler::default())
- .await?;
- ensure!(
- !vid.is_empty(),
- "build failed: {}",
- context("build", self.0.full_path.as_deref(), &query),
- );
- let Some(vid) = vid.strip_prefix("This derivation produced the following outputs:\n")
- else {
- panic!("unexpected build output: {vid:?}");
- };
- let outputs = vid
- .split('\n')
- .filter(|v| !v.is_empty())
- .map(|v| v.split_once(" -> ").expect("unexpected build output"))
- .map(|(a, b)| (a.trim_start().to_owned(), PathBuf::from(b)))
- .collect();
- Ok(outputs)
- }
-}
-impl Drop for FieldInner {
- fn drop(&mut self) {
- if let Some(id) = self.value {
- if let Ok(mut lock) = self.session.0.try_lock() {
- lock.free_list.push(id)
- }
- // Leaked
- }
- }
-}
-struct NixSessionPoolInner {
- flake: OsString,
- nix_args: Vec<OsString>,
-}
-
-#[derive(Debug)]
-pub struct NixPoolError(anyhow::Error);
-impl From<anyhow::Error> for NixPoolError {
- fn from(value: anyhow::Error) -> Self {
- Self(value)
- }
-}
-impl std::error::Error for NixPoolError {}
-impl std::fmt::Display for NixPoolError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- self.0.fmt(f)
- }
-}
-impl r2d2::ManageConnection for NixSessionPoolInner {
- type Connection = NixSessionInner;
- type Error = NixPoolError;
- fn connect(&self) -> std::result::Result<Self::Connection, Self::Error> {
- let _v = TOKIO_RUNTIME
- .get()
- .expect("missed tokio runtime init!")
- .enter();
- Ok(futures::executor::block_on(NixSessionInner::new(
- self.flake.as_os_str(),
- self.nix_args.iter().map(OsString::as_os_str),
- ))?)
- }
-
- fn is_valid(&self, conn: &mut Self::Connection) -> std::result::Result<(), Self::Error> {
- let _v = TOKIO_RUNTIME
- .get()
- .expect("missed tokio runtime init!")
- .enter();
- let res = futures::executor::block_on(conn.execute_expression_number("2 + 2"))?;
- if res != 4 {
- return Err(anyhow!("sanity check failed").into());
- };
- Ok(())
- }
-
- fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
- false
- }
-}
-pub struct NixSessionPool(Pool<NixSessionPoolInner>);
-impl NixSessionPool {
- pub async fn new(flake: OsString, nix_args: Vec<OsString>) -> Result<Self> {
- let inner = tokio::task::block_in_place(|| {
- r2d2::Builder::<NixSessionPoolInner>::new()
- .min_idle(Some(0))
- .build(NixSessionPoolInner { flake, nix_args })
- })?;
- Ok(Self(inner))
- }
- pub async fn get(&self) -> Result<NixSession> {
- let v = tokio::task::block_in_place(|| self.0.get())?;
- Ok(NixSession(Arc::new(tokio::sync::Mutex::new(v))))
- }
-}
-
-pub static TOKIO_RUNTIME: OnceLock<tokio::runtime::Handle> = OnceLock::new();
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -4,10 +4,10 @@
use crate::command::MyCommand;
use crate::host::{Config, ConfigHost};
-use crate::nix_go;
use anyhow::{anyhow, Result};
use clap::{Parser, ValueEnum};
use itertools::Itertools as _;
+use nix_eval::nix_go;
use tokio::{task::LocalSet, time::sleep};
use tracing::{error, field, info, info_span, warn, Instrument};
cmds/fleet/src/cmds/info.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/info.rs
+++ b/cmds/fleet/src/cmds/info.rs
@@ -1,9 +1,9 @@
use std::collections::BTreeSet;
use crate::host::Config;
-use crate::nix_go_json;
use anyhow::{ensure, Result};
use clap::Parser;
+use nix_eval::nix_go_json;
#[derive(Parser)]
pub struct Info {
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth1use std::{2 collections::{BTreeMap, BTreeSet, HashSet},3 ffi::OsString,4 io::{self, stdin, stdout, Read, Write},5 path::PathBuf,6};78use anyhow::{anyhow, bail, ensure, Context, Result};9use chrono::{DateTime, Utc};10use clap::Parser;11use crossterm::{terminal, tty::IsTty};12use fleet_shared::SecretData;13use itertools::Itertools;14use owo_colors::OwoColorize;15use serde::Deserialize;16use tabled::{Table, Tabled};17use tokio::{fs::read, process::Command};18use tracing::{error, info, info_span, warn, Instrument};1920use crate::{21 better_nix_eval::Field,22 fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},23 host::Config,24 nix_go, nix_go_json,25};2627#[derive(Parser)]28pub enum Secret {29 /// Force load host keys for all defined hosts30 ForceKeys,31 /// Add secret, data should be provided in stdin32 AddShared {33 /// Secret name34 name: String,35 /// Secret owners36 machines: Vec<String>,37 /// Override secret if already present38 #[clap(long)]39 force: bool,40 /// Secret public part41 #[clap(long)]42 public: Option<String>,43 /// How to name public secret part44 #[clap(long, default_value = "public")]45 public_name: String,46 /// Load public part from specified file47 #[clap(long)]48 public_file: Option<PathBuf>,4950 /// Create a notification on secret expiration51 #[clap(long)]52 expires_at: Option<DateTime<Utc>>,5354 /// Secret with this name already exists, override its value while keeping the same owners.55 #[clap(long)]56 re_add: bool,5758 #[clap(default_value = "secret")]59 part_name: String,60 },61 /// Add secret, data should be provided in stdin62 Add {63 /// Secret name64 name: String,65 /// Secret owners66 machine: String,67 /// Override secret if already present68 #[clap(long)]69 force: bool,70 /// Secret public part71 #[clap(long)]72 public: Option<String>,73 /// How to name public secret part74 #[clap(long, default_value = "public")]75 public_name: String,76 /// Load public part from specified file77 #[clap(long)]78 public_file: Option<PathBuf>,7980 #[clap(default_value = "secret")]81 part_name: String,82 },83 /// Read secret from remote host, requires sudo on said host84 Read {85 name: String,86 machine: String,8788 #[clap(default_value = "secret")]89 part_name: String,90 },91 UpdateShared {92 name: String,9394 #[clap(long)]95 machines: Option<Vec<String>>,9697 #[clap(long)]98 add_machines: Vec<String>,99 #[clap(long)]100 remove_machines: Vec<String>,101102 /// Which host should we use to decrypt103 #[clap(long)]104 prefer_identities: Vec<String>,105106 #[clap(default_value = "secret")]107 part_name: String,108 },109 Regenerate {110 /// Which host should we use to decrypt, in case if reencryption is required, without111 /// regeneration112 #[clap(long)]113 prefer_identities: Vec<String>,114 },115 List {},116 Edit {117 name: String,118 machine: String,119120 #[clap(default_value = "secret")]121 part: String,122123 #[clap(long)]124 add: bool,125 },126}127128#[tracing::instrument(skip(config, secret, field, prefer_identities))]129async fn update_owner_set(130 secret_name: &str,131 config: &Config,132 mut secret: FleetSharedSecret,133 field: Field,134 updated_set: &[String],135 prefer_identities: &[String],136) -> Result<FleetSharedSecret> {137 let original_set = secret.owners.clone();138139 let set = original_set.iter().collect::<BTreeSet<_>>();140 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();141142 if set == expected_set {143 info!("no need to update owner list, it is already correct");144 return Ok(secret);145 }146147 let should_regenerate = if set.difference(&expected_set).next().is_some() {148 // TODO: Remove this warning for revokable secrets.149 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");150 nix_go_json!(field.regenerateOnOwnerRemoved)151 } else if expected_set.difference(&set).next().is_some() {152 nix_go_json!(field.regenerateOnOwnerAdded)153 } else {154 false155 };156157 if should_regenerate {158 info!("secret is owner-dependent, will regenerate");159 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;160 Ok(generated)161 } else {162 let identity_holder = if !prefer_identities.is_empty() {163 prefer_identities164 .iter()165 .find(|i| original_set.iter().any(|s| s == *i))166 } else {167 secret.owners.first()168 };169 let Some(identity_holder) = identity_holder else {170 bail!("no available holder found");171 };172173 for (part_name, part) in secret.secret.parts.iter_mut() {174 let _span = info_span!("part reencryption", part_name);175 if !part.raw.encrypted {176 continue;177 }178 let host = config.host(identity_holder).await?;179 let encrypted = host180 .reencrypt(part.raw.clone(), updated_set.to_vec())181 .await?;182 part.raw = encrypted;183 }184185 secret.owners = updated_set.to_vec();186 Ok(secret)187 }188}189190#[derive(Deserialize)]191#[serde(rename_all = "camelCase")]192enum GeneratorKind {193 Impure,194 Pure,195}196197async fn generate_pure(198 _config: &Config,199 _display_name: &str,200 _secret: Field,201 _default_generator: Field,202 _owners: &[String],203) -> Result<FleetSecret> {204 bail!("pure generators are broken for now")205}206async fn generate_impure(207 config: &Config,208 _display_name: &str,209 secret: Field,210 default_generator: Field,211 owners: &[String],212) -> Result<FleetSecret> {213 let generator = nix_go!(secret.generator);214 let on: Option<String> = nix_go_json!(default_generator.impureOn);215216 let host = if let Some(on) = &on {217 config.host(on).await?218 } else {219 config.local_host()220 };221 let on_pkgs = host.pkgs().await?;222 let call_package = nix_go!(on_pkgs.callPackage);223 let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);224225 let mut recipients = Vec::new();226 for owner in owners {227 let key = config.key(owner).await?;228 recipients.push(key);229 }230 let encrypt = nix_go!(mk_encrypt_secret(Obj {231 recipients: { recipients },232 }));233234 let generator = nix_go!(call_package(generator)(Obj {235 encrypt,236 // rustfmt_please_newline237 }));238239 let generator = generator.build().await?;240 let generator = generator241 .get("out")242 .ok_or_else(|| anyhow!("missing generateImpure out"))?;243 let generator = host.remote_derivation(generator).await?;244245 let out_parent = host.mktemp_dir().await?;246 let out = format!("{out_parent}/out");247248 let mut gen = host.cmd(generator).await?;249 gen.env("out", &out);250 if on.is_none() {251 // This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.252 let project_path: String = config253 .directory254 .clone()255 .into_os_string()256 .into_string()257 .map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;258 gen.env("FLEET_PROJECT", project_path);259 }260 gen.run().await.context("impure generator")?;261262 {263 let marker = host.read_file_text(format!("{out}/marker")).await?;264 ensure!(marker == "SUCCESS", "generation not succeeded");265 }266267 let mut parts = BTreeMap::new();268 for part in host.read_dir(&out).await? {269 if part == "created_at" || part == "expired_at" || part == "marker" {270 continue;271 }272 let contents: SecretData = host273 .read_file_text(format!("{out}/{part}"))274 .await?275 .parse()276 .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;277 parts.insert(part.to_owned(), FleetSecretPart { raw: contents });278 }279280 let created_at = host.read_file_value(format!("{out}/created_at")).await?;281 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();282283 Ok(FleetSecret {284 created_at,285 expires_at,286 parts,287 })288}289async fn generate(290 config: &Config,291 display_name: &str,292 secret: Field,293 owners: &[String],294) -> Result<FleetSecret> {295 let generator = nix_go!(secret.generator);296 // Can't properly check on nix module system level297 {298 let gen_ty = generator.type_of().await?;299 if gen_ty == "null" {300 bail!("secret has no generator defined, can't automatically generate it.");301 }302 if gen_ty != "lambda" {303 bail!("generator should be lambda, got {gen_ty}");304 }305 }306 let default_pkgs = &config.default_pkgs;307 let default_call_package = nix_go!(default_pkgs.callPackage);308 // Generators provide additional information in passthru, to access309 // passthru we should call generator, but information about where this generator is supposed to build310 // is located in passthru... Thus evaluating generator on host.311 //312 // Maybe it is also possible to do some magic with __functor?313 //314 // I don't want to make modules always responsible for additional secret data anyway,315 // so it should be in derivation, and not in the secret data itself.316 let default_generator = nix_go!(default_call_package(generator)(Obj {317 encrypt: { "exit 1" },318 // rustfmt_please_newline319 }));320321 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);322323 match kind {324 GeneratorKind::Impure => {325 generate_impure(config, display_name, secret, default_generator, owners).await326 }327 GeneratorKind::Pure => {328 generate_pure(config, display_name, secret, default_generator, owners).await329 }330 }331}332async fn generate_shared(333 config: &Config,334 display_name: &str,335 secret: Field,336 expected_owners: Vec<String>,337) -> Result<FleetSharedSecret> {338 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);339 Ok(FleetSharedSecret {340 secret: generate(config, display_name, secret, &expected_owners).await?,341 owners: expected_owners,342 })343}344345async fn parse_public(346 public: Option<String>,347 public_file: Option<PathBuf>,348) -> Result<Option<SecretData>> {349 Ok(match (public, public_file) {350 (Some(v), None) => Some(SecretData {351 data: v.into(),352 encrypted: false,353 }),354 (None, Some(v)) => Some(SecretData {355 data: read(v).await?,356 encrypted: false,357 }),358 (Some(_), Some(_)) => {359 bail!("only public or public_file should be set")360 }361 (None, None) => None,362 })363}364365async fn parse_secret() -> Result<Option<Vec<u8>>> {366 let mut input = vec![];367 io::stdin().read_to_end(&mut input)?;368 if input.is_empty() {369 Ok(None)370 } else {371 Ok(Some(input))372 }373}374375fn parse_machines(376 initial: Vec<String>,377 machines: Option<Vec<String>>,378 mut add_machines: Vec<String>,379 mut remove_machines: Vec<String>,380) -> Result<Vec<String>> {381 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {382 bail!("no operation");383 }384385 let initial_machines = initial.clone();386 let mut target_machines = initial;387 info!("Currently encrypted for {initial_machines:?}");388389 // ensure!(machines.is_some() || !add_machines.is_empty() || )390 if let Some(machines) = machines {391 ensure!(392 add_machines.is_empty() && remove_machines.is_empty(),393 "can't combine --machines and --add-machines/--remove-machines"394 );395 let target = initial_machines.iter().collect::<HashSet<_>>();396 let source = machines.iter().collect::<HashSet<_>>();397 for removed in target.difference(&source) {398 remove_machines.push((*removed).clone());399 }400 for added in source.difference(&target) {401 add_machines.push((*added).clone());402 }403 }404405 for machine in &remove_machines {406 let mut removed = false;407 while let Some(pos) = target_machines.iter().position(|m| m == machine) {408 target_machines.swap_remove(pos);409 removed = true;410 }411 if !removed {412 warn!("secret is not enabled for {machine}");413 }414 }415 for machine in &add_machines {416 if target_machines.iter().any(|m| m == machine) {417 warn!("secret is already added to {machine}");418 } else {419 target_machines.push(machine.to_owned());420 }421 }422 if !remove_machines.is_empty() {423 // TODO: maybe force secret regeneration?424 // Not that useful without revokation.425 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");426 }427 Ok(target_machines)428}429impl Secret {430 pub async fn run(self, config: &Config) -> Result<()> {431 match self {432 Secret::ForceKeys => {433 for host in config.list_hosts().await? {434 if config.should_skip(&host.name) {435 continue;436 }437 config.key(&host.name).await?;438 }439 }440 Secret::AddShared {441 mut machines,442 name,443 force,444 public,445 public_name,446 public_file,447 expires_at,448 re_add,449 part_name,450 } => {451 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).452453 let exists = config.has_shared(&name);454 if exists && !force && !re_add {455 bail!("secret already defined");456 }457 if re_add {458 // Fixme: use clap to limit this usage459 ensure!(!force, "--force and --readd are not compatible");460 ensure!(exists, "secret doesn't exists");461 ensure!(462 machines.is_empty(),463 "you can't use machines argument for --readd"464 );465 let shared = config.shared_secret(&name)?;466 machines = shared.owners;467 }468469 let recipients = config.recipients(machines.clone()).await?;470471 let mut parts = BTreeMap::new();472473 let mut input = vec![];474 io::stdin().read_to_end(&mut input)?;475476 if !input.is_empty() {477 let encrypted = encrypt_secret_data(recipients, input)478 .ok_or_else(|| anyhow!("no recipients provided"))?;479 parts.insert(part_name, FleetSecretPart { raw: encrypted });480 }481482 if let Some(public) = parse_public(public, public_file).await? {483 parts.insert(public_name, FleetSecretPart { raw: public });484 }485486 config.replace_shared(487 name,488 FleetSharedSecret {489 owners: machines,490 secret: FleetSecret {491 created_at: Utc::now(),492 expires_at,493 parts,494 },495 },496 );497 }498 Secret::Add {499 machine,500 name,501 force,502 public,503 public_name,504 public_file,505 part_name,506 } => {507 if config.has_secret(&machine, &name) && !force {508 bail!("secret already defined");509 }510511 let mut parts = BTreeMap::new();512513 if let Some(secret) = parse_secret().await? {514 let recipient = config.recipient(&machine).await?;515 let encrypted =516 encrypt_secret_data(vec![recipient], secret).expect("recipient provided");517 parts.insert(part_name, FleetSecretPart { raw: encrypted });518 }519520 if let Some(public) = parse_public(public, public_file).await? {521 parts.insert(public_name, FleetSecretPart { raw: public });522 };523524 config.insert_secret(525 &machine,526 name,527 FleetSecret {528 created_at: Utc::now(),529 expires_at: None,530 parts,531 },532 );533 }534 #[allow(clippy::await_holding_refcell_ref)]535 Secret::Read {536 name,537 machine,538 part_name,539 } => {540 let secret = config.host_secret(&machine, &name)?;541 let Some(secret) = secret.parts.get(&part_name) else {542 bail!("no part {part_name} in secret {name}");543 };544 let data = if secret.raw.encrypted {545 let host = config.host(&machine).await?;546 host.decrypt(secret.raw.clone()).await?547 } else {548 secret.raw.data.clone()549 };550551 stdout().write_all(&data)?;552 }553 Secret::UpdateShared {554 name,555 machines,556 add_machines,557 remove_machines,558 prefer_identities,559 part_name,560 } => {561 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).562563 let secret = config.shared_secret(&name)?;564 if secret.secret.parts.get(&part_name).is_none() {565 bail!("no secret");566 }567568 let initial_machines = secret.owners.clone();569 let target_machines = parse_machines(570 initial_machines.clone(),571 machines,572 add_machines,573 remove_machines,574 )?;575576 if target_machines.is_empty() {577 info!("no machines left for secret, removing it");578 config.remove_shared(&name);579 return Ok(());580 }581582 let config_field = &config.config_unchecked_field;583 let field = nix_go!(config_field.sharedSecrets[{ name }]);584585 let updated = update_owner_set(586 &name,587 config,588 secret,589 field,590 &target_machines,591 &prefer_identities,592 )593 .await?;594 config.replace_shared(name, updated);595 }596 Secret::Regenerate { prefer_identities } => {597 info!("checking for secrets to regenerate");598 {599 let _span = info_span!("shared").entered();600 let expected_shared_set = config601 .list_configured_shared()602 .await?603 .into_iter()604 .collect::<HashSet<_>>();605 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();606 for missing in expected_shared_set.difference(&shared_set) {607 let config_field = &config.config_unchecked_field;608 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);609 let expected_owners: Option<Vec<String>> =610 nix_go_json!(secret.expectedOwners);611 let Some(expected_owners) = expected_owners else {612 // TODO: Might still need to regenerate613 continue;614 };615 info!("generating secret: {missing}");616 let shared = generate_shared(config, missing, secret, expected_owners)617 .in_current_span()618 .await?;619 config.replace_shared(missing.to_string(), shared)620 }621 }622 for host in config.list_hosts().await? {623 if config.should_skip(&host.name) {624 continue;625 }626627 let _span = info_span!("host", host = host.name).entered();628 let expected_set = host629 .list_configured_secrets()630 .in_current_span()631 .await?632 .into_iter()633 .collect::<HashSet<_>>();634 let stored_set = config635 .list_secrets(&host.name)636 .into_iter()637 .collect::<HashSet<_>>();638 for missing in expected_set.difference(&stored_set) {639 info!("generating secret: {missing}");640 let secret = host.secret_field(missing).in_current_span().await?;641 let generated =642 match generate(config, missing, secret, &[host.name.clone()])643 .in_current_span()644 .await645 {646 Ok(v) => v,647 Err(e) => {648 error!("{e:?}");649 continue;650 }651 };652 config.insert_secret(&host.name, missing.to_string(), generated)653 }654 }655 let mut to_remove = Vec::new();656 for name in &config.list_shared() {657 info!("updating secret: {name}");658 let data = config.shared_secret(name)?;659 let config_field = &config.config_unchecked_field;660 let expected_owners: Vec<String> =661 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);662 if expected_owners.is_empty() {663 warn!("secret was removed from fleet config: {name}, removing from data");664 to_remove.push(name.to_string());665 continue;666 }667668 let secret = nix_go!(config_field.sharedSecrets[{ name }]);669 config.replace_shared(670 name.to_owned(),671 update_owner_set(672 name,673 config,674 data,675 secret,676 &expected_owners,677 &prefer_identities,678 )679 .await?,680 );681 }682 for k in to_remove {683 config.remove_shared(&k);684 }685 }686 Secret::List {} => {687 let _span = info_span!("loading secrets").entered();688 let configured = config.list_configured_shared().await?;689 #[derive(Tabled)]690 struct SecretDisplay {691 #[tabled(rename = "Name")]692 name: String,693 #[tabled(rename = "Owners")]694 owners: String,695 }696 let mut table = vec![];697 for name in configured.iter().cloned() {698 let config = config.clone();699 let expected_owners = config.shared_secret_expected_owners(&name).await?;700 let data = config.shared_secret(&name)?;701 let owners = data702 .owners703 .iter()704 .map(|o| {705 if expected_owners.contains(o) {706 o.green().to_string()707 } else {708 o.red().to_string()709 }710 })711 .collect::<Vec<_>>();712 table.push(SecretDisplay {713 owners: owners.join(", "),714 name,715 })716 }717 info!("loaded\n{}", Table::new(table).to_string())718 }719 Secret::Edit {720 name,721 machine,722 part,723 add,724 } => {725 let secret = config.host_secret(&machine, &name)?;726 if let Some(data) = secret.parts.get(&part) {727 let host = config.host(&machine).await?;728 let secret = host.decrypt(data.raw.clone()).await?;729 String::from_utf8(secret).context("secret is not utf8")?730 } else if add {731 String::new()732 } else {733 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");734 };735 }736 }737 Ok(())738 }739}740741async fn edit_temp_file(742 builder: tempfile::Builder<'_, '_>,743 r: Vec<u8>,744 header: &str,745 comment: &str,746) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {747 if !stdin().is_tty() {748 // TODO: Also try to open /dev/tty directly?749 bail!("stdin is not tty, can't open editor");750 }751752 use std::fmt::Write;753 let mut file = builder.tempfile()?;754755 let mut full_header = String::new();756 let mut had = false;757 for line in header.trim_end().lines() {758 had = true;759 writeln!(&mut full_header, "{comment}{line}")?;760 }761 if had {762 writeln!(&mut full_header, "{}", comment.trim_end())?;763 }764 writeln!(765 &mut full_header,766 "{comment}Do not touch this header! It will be removed automatically"767 )?;768769 file.write_all(full_header.as_bytes())?;770 file.write_all(&r)?;771772 let abs_path = file.into_temp_path();773 let editor = std::env::var_os("VISUAL")774 .or_else(|| std::env::var_os("EDITOR"))775 .unwrap_or_else(|| "vi".into());776 let editor_args = shlex::bytes::split(editor.as_encoded_bytes())777 .ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;778 let editor_args = editor_args779 .into_iter()780 .map(|v| {781 // Only ASCII subsequences are replaced782 unsafe { OsString::from_encoded_bytes_unchecked(v) }783 })784 .collect_vec();785 let Some((editor, args)) = editor_args.split_first() else {786 bail!("EDITOR env var has no command");787 };788 let mut command = Command::new(editor);789 command.args(args);790791 let path_arg = abs_path.canonicalize()?;792793 // TODO: Save full state, using tcget/_getmode/_setmode794 let was_raw = terminal::is_raw_mode_enabled()?;795 terminal::enable_raw_mode()?;796797 let status = command.arg(path_arg).status().await;798799 if !was_raw {800 terminal::disable_raw_mode()?;801 }802803 let success = match status {804 Ok(s) => s.success(),805 Err(e) if e.kind() == io::ErrorKind::NotFound => {806 bail!("editor not found")807 }808 Err(e) => bail!("editor spawn error: {e}"),809 };810811 let mut file = std::fs::read(&abs_path).context("read editor output")?;812 let Some(v) = file.strip_prefix(full_header.as_bytes()) else {813 todo!();814 };815 todo!();816817 // Ok((success, abs_path))818}1use std::{2 collections::{BTreeMap, BTreeSet, HashSet},3 ffi::OsString,4 io::{self, stdin, stdout, Read, Write},5 path::PathBuf,6};78use anyhow::{anyhow, bail, ensure, Context, Result};9use chrono::{DateTime, Utc};10use clap::Parser;11use crossterm::{terminal, tty::IsTty};12use fleet_shared::SecretData;13use itertools::Itertools;14use nix_eval::{nix_go, nix_go_json, Value};15use owo_colors::OwoColorize;16use serde::Deserialize;17use tabled::{Table, Tabled};18use tokio::{fs::read, process::Command};19use tracing::{error, info, info_span, warn, Instrument};2021use crate::{22 fleetdata::{encrypt_secret_data, FleetSecret, FleetSecretPart, FleetSharedSecret},23 host::Config,24};2526#[derive(Parser)]27pub enum Secret {28 /// Force load host keys for all defined hosts29 ForceKeys,30 /// Add secret, data should be provided in stdin31 AddShared {32 /// Secret name33 name: String,34 /// Secret owners35 machines: Vec<String>,36 /// Override secret if already present37 #[clap(long)]38 force: bool,39 /// Secret public part40 #[clap(long)]41 public: Option<String>,42 /// How to name public secret part43 #[clap(long, default_value = "public")]44 public_name: String,45 /// Load public part from specified file46 #[clap(long)]47 public_file: Option<PathBuf>,4849 /// Create a notification on secret expiration50 #[clap(long)]51 expires_at: Option<DateTime<Utc>>,5253 /// Secret with this name already exists, override its value while keeping the same owners.54 #[clap(long)]55 re_add: bool,5657 #[clap(default_value = "secret")]58 part_name: String,59 },60 /// Add secret, data should be provided in stdin61 Add {62 /// Secret name63 name: String,64 /// Secret owners65 machine: String,66 /// Override secret if already present67 #[clap(long)]68 force: bool,69 /// Secret public part70 #[clap(long)]71 public: Option<String>,72 /// How to name public secret part73 #[clap(long, default_value = "public")]74 public_name: String,75 /// Load public part from specified file76 #[clap(long)]77 public_file: Option<PathBuf>,7879 #[clap(default_value = "secret")]80 part_name: String,81 },82 /// Read secret from remote host, requires sudo on said host83 Read {84 name: String,85 machine: String,8687 #[clap(default_value = "secret")]88 part_name: String,89 },90 UpdateShared {91 name: String,9293 #[clap(long)]94 machines: Option<Vec<String>>,9596 #[clap(long)]97 add_machines: Vec<String>,98 #[clap(long)]99 remove_machines: Vec<String>,100101 /// Which host should we use to decrypt102 #[clap(long)]103 prefer_identities: Vec<String>,104105 #[clap(default_value = "secret")]106 part_name: String,107 },108 Regenerate {109 /// Which host should we use to decrypt, in case if reencryption is required, without110 /// regeneration111 #[clap(long)]112 prefer_identities: Vec<String>,113 },114 List {},115 Edit {116 name: String,117 machine: String,118119 #[clap(default_value = "secret")]120 part: String,121122 #[clap(long)]123 add: bool,124 },125}126127#[tracing::instrument(skip(config, secret, field, prefer_identities))]128async fn update_owner_set(129 secret_name: &str,130 config: &Config,131 mut secret: FleetSharedSecret,132 field: Value,133 updated_set: &[String],134 prefer_identities: &[String],135) -> Result<FleetSharedSecret> {136 let original_set = secret.owners.clone();137138 let set = original_set.iter().collect::<BTreeSet<_>>();139 let expected_set = updated_set.iter().collect::<BTreeSet<_>>();140141 if set == expected_set {142 info!("no need to update owner list, it is already correct");143 return Ok(secret);144 }145146 let should_regenerate = if set.difference(&expected_set).next().is_some() {147 // TODO: Remove this warning for revokable secrets.148 warn!("host was removed from secret owners, but until this host rebuild, the secret will still be stored on it.");149 nix_go_json!(field.regenerateOnOwnerRemoved)150 } else if expected_set.difference(&set).next().is_some() {151 nix_go_json!(field.regenerateOnOwnerAdded)152 } else {153 false154 };155156 if should_regenerate {157 info!("secret is owner-dependent, will regenerate");158 let generated = generate_shared(config, secret_name, field, updated_set.to_vec()).await?;159 Ok(generated)160 } else {161 let identity_holder = if !prefer_identities.is_empty() {162 prefer_identities163 .iter()164 .find(|i| original_set.iter().any(|s| s == *i))165 } else {166 secret.owners.first()167 };168 let Some(identity_holder) = identity_holder else {169 bail!("no available holder found");170 };171172 for (part_name, part) in secret.secret.parts.iter_mut() {173 let _span = info_span!("part reencryption", part_name);174 if !part.raw.encrypted {175 continue;176 }177 let host = config.host(identity_holder).await?;178 let encrypted = host179 .reencrypt(part.raw.clone(), updated_set.to_vec())180 .await?;181 part.raw = encrypted;182 }183184 secret.owners = updated_set.to_vec();185 Ok(secret)186 }187}188189#[derive(Deserialize)]190#[serde(rename_all = "camelCase")]191enum GeneratorKind {192 Impure,193 Pure,194}195196async fn generate_pure(197 _config: &Config,198 _display_name: &str,199 _secret: Value,200 _default_generator: Value,201 _owners: &[String],202) -> Result<FleetSecret> {203 bail!("pure generators are broken for now")204}205async fn generate_impure(206 config: &Config,207 _display_name: &str,208 secret: Value,209 default_generator: Value,210 owners: &[String],211) -> Result<FleetSecret> {212 let generator = nix_go!(secret.generator);213 let on: Option<String> = nix_go_json!(default_generator.impureOn);214215 let host = if let Some(on) = &on {216 config.host(on).await?217 } else {218 config.local_host()219 };220 let on_pkgs = host.pkgs().await?;221 let call_package = nix_go!(on_pkgs.callPackage);222 let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);223224 let mut recipients = Vec::new();225 for owner in owners {226 let key = config.key(owner).await?;227 recipients.push(key);228 }229 let encrypt = nix_go!(mk_encrypt_secret(Obj {230 recipients: { recipients },231 }));232233 let generator = nix_go!(call_package(generator)(Obj {234 encrypt,235 // rustfmt_please_newline236 }));237238 let generator = generator.build().await?;239 let generator = generator240 .get("out")241 .ok_or_else(|| anyhow!("missing generateImpure out"))?;242 let generator = host.remote_derivation(generator).await?;243244 let out_parent = host.mktemp_dir().await?;245 let out = format!("{out_parent}/out");246247 let mut gen = host.cmd(generator).await?;248 gen.env("out", &out);249 if on.is_none() {250 // This path is local, thus we can feed `OsString` directly to env var... But I don't think that's necessary to handle.251 let project_path: String = config252 .directory253 .clone()254 .into_os_string()255 .into_string()256 .map_err(|s| anyhow!("fleet project path is not utf-8: {s:?}"))?;257 gen.env("FLEET_PROJECT", project_path);258 }259 gen.run().await.context("impure generator")?;260261 {262 let marker = host.read_file_text(format!("{out}/marker")).await?;263 ensure!(marker == "SUCCESS", "generation not succeeded");264 }265266 let mut parts = BTreeMap::new();267 for part in host.read_dir(&out).await? {268 if part == "created_at" || part == "expired_at" || part == "marker" {269 continue;270 }271 let contents: SecretData = host272 .read_file_text(format!("{out}/{part}"))273 .await?274 .parse()275 .map_err(|e| anyhow!("failed to decode secret {out:?} part {part:?}: {e}"))?;276 parts.insert(part.to_owned(), FleetSecretPart { raw: contents });277 }278279 let created_at = host.read_file_value(format!("{out}/created_at")).await?;280 let expires_at = host.read_file_value(format!("{out}/expires_at")).await.ok();281282 Ok(FleetSecret {283 created_at,284 expires_at,285 parts,286 })287}288async fn generate(289 config: &Config,290 display_name: &str,291 secret: Value,292 owners: &[String],293) -> Result<FleetSecret> {294 let generator = nix_go!(secret.generator);295 // Can't properly check on nix module system level296 {297 let gen_ty = generator.type_of().await?;298 if gen_ty == "null" {299 bail!("secret has no generator defined, can't automatically generate it.");300 }301 if gen_ty != "lambda" {302 bail!("generator should be lambda, got {gen_ty}");303 }304 }305 let default_pkgs = &config.default_pkgs;306 let default_call_package = nix_go!(default_pkgs.callPackage);307 // Generators provide additional information in passthru, to access308 // passthru we should call generator, but information about where this generator is supposed to build309 // is located in passthru... Thus evaluating generator on host.310 //311 // Maybe it is also possible to do some magic with __functor?312 //313 // I don't want to make modules always responsible for additional secret data anyway,314 // so it should be in derivation, and not in the secret data itself.315 let default_generator = nix_go!(default_call_package(generator)(Obj {316 encrypt: { "exit 1" },317 // rustfmt_please_newline318 }));319320 let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);321322 match kind {323 GeneratorKind::Impure => {324 generate_impure(config, display_name, secret, default_generator, owners).await325 }326 GeneratorKind::Pure => {327 generate_pure(config, display_name, secret, default_generator, owners).await328 }329 }330}331async fn generate_shared(332 config: &Config,333 display_name: &str,334 secret: Value,335 expected_owners: Vec<String>,336) -> Result<FleetSharedSecret> {337 // let owners: Vec<String> = nix_go_json!(secret.expectedOwners);338 Ok(FleetSharedSecret {339 secret: generate(config, display_name, secret, &expected_owners).await?,340 owners: expected_owners,341 })342}343344async fn parse_public(345 public: Option<String>,346 public_file: Option<PathBuf>,347) -> Result<Option<SecretData>> {348 Ok(match (public, public_file) {349 (Some(v), None) => Some(SecretData {350 data: v.into(),351 encrypted: false,352 }),353 (None, Some(v)) => Some(SecretData {354 data: read(v).await?,355 encrypted: false,356 }),357 (Some(_), Some(_)) => {358 bail!("only public or public_file should be set")359 }360 (None, None) => None,361 })362}363364async fn parse_secret() -> Result<Option<Vec<u8>>> {365 let mut input = vec![];366 io::stdin().read_to_end(&mut input)?;367 if input.is_empty() {368 Ok(None)369 } else {370 Ok(Some(input))371 }372}373374fn parse_machines(375 initial: Vec<String>,376 machines: Option<Vec<String>>,377 mut add_machines: Vec<String>,378 mut remove_machines: Vec<String>,379) -> Result<Vec<String>> {380 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {381 bail!("no operation");382 }383384 let initial_machines = initial.clone();385 let mut target_machines = initial;386 info!("Currently encrypted for {initial_machines:?}");387388 // ensure!(machines.is_some() || !add_machines.is_empty() || )389 if let Some(machines) = machines {390 ensure!(391 add_machines.is_empty() && remove_machines.is_empty(),392 "can't combine --machines and --add-machines/--remove-machines"393 );394 let target = initial_machines.iter().collect::<HashSet<_>>();395 let source = machines.iter().collect::<HashSet<_>>();396 for removed in target.difference(&source) {397 remove_machines.push((*removed).clone());398 }399 for added in source.difference(&target) {400 add_machines.push((*added).clone());401 }402 }403404 for machine in &remove_machines {405 let mut removed = false;406 while let Some(pos) = target_machines.iter().position(|m| m == machine) {407 target_machines.swap_remove(pos);408 removed = true;409 }410 if !removed {411 warn!("secret is not enabled for {machine}");412 }413 }414 for machine in &add_machines {415 if target_machines.iter().any(|m| m == machine) {416 warn!("secret is already added to {machine}");417 } else {418 target_machines.push(machine.to_owned());419 }420 }421 if !remove_machines.is_empty() {422 // TODO: maybe force secret regeneration?423 // Not that useful without revokation.424 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");425 }426 Ok(target_machines)427}428impl Secret {429 pub async fn run(self, config: &Config) -> Result<()> {430 match self {431 Secret::ForceKeys => {432 for host in config.list_hosts().await? {433 if config.should_skip(&host.name) {434 continue;435 }436 config.key(&host.name).await?;437 }438 }439 Secret::AddShared {440 mut machines,441 name,442 force,443 public,444 public_name,445 public_file,446 expires_at,447 re_add,448 part_name,449 } => {450 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).451452 let exists = config.has_shared(&name);453 if exists && !force && !re_add {454 bail!("secret already defined");455 }456 if re_add {457 // Fixme: use clap to limit this usage458 ensure!(!force, "--force and --readd are not compatible");459 ensure!(exists, "secret doesn't exists");460 ensure!(461 machines.is_empty(),462 "you can't use machines argument for --readd"463 );464 let shared = config.shared_secret(&name)?;465 machines = shared.owners;466 }467468 let recipients = config.recipients(machines.clone()).await?;469470 let mut parts = BTreeMap::new();471472 let mut input = vec![];473 io::stdin().read_to_end(&mut input)?;474475 if !input.is_empty() {476 let encrypted = encrypt_secret_data(recipients, input)477 .ok_or_else(|| anyhow!("no recipients provided"))?;478 parts.insert(part_name, FleetSecretPart { raw: encrypted });479 }480481 if let Some(public) = parse_public(public, public_file).await? {482 parts.insert(public_name, FleetSecretPart { raw: public });483 }484485 config.replace_shared(486 name,487 FleetSharedSecret {488 owners: machines,489 secret: FleetSecret {490 created_at: Utc::now(),491 expires_at,492 parts,493 },494 },495 );496 }497 Secret::Add {498 machine,499 name,500 force,501 public,502 public_name,503 public_file,504 part_name,505 } => {506 if config.has_secret(&machine, &name) && !force {507 bail!("secret already defined");508 }509510 let mut parts = BTreeMap::new();511512 if let Some(secret) = parse_secret().await? {513 let recipient = config.recipient(&machine).await?;514 let encrypted =515 encrypt_secret_data(vec![recipient], secret).expect("recipient provided");516 parts.insert(part_name, FleetSecretPart { raw: encrypted });517 }518519 if let Some(public) = parse_public(public, public_file).await? {520 parts.insert(public_name, FleetSecretPart { raw: public });521 };522523 config.insert_secret(524 &machine,525 name,526 FleetSecret {527 created_at: Utc::now(),528 expires_at: None,529 parts,530 },531 );532 }533 #[allow(clippy::await_holding_refcell_ref)]534 Secret::Read {535 name,536 machine,537 part_name,538 } => {539 let secret = config.host_secret(&machine, &name)?;540 let Some(secret) = secret.parts.get(&part_name) else {541 bail!("no part {part_name} in secret {name}");542 };543 let data = if secret.raw.encrypted {544 let host = config.host(&machine).await?;545 host.decrypt(secret.raw.clone()).await?546 } else {547 secret.raw.data.clone()548 };549550 stdout().write_all(&data)?;551 }552 Secret::UpdateShared {553 name,554 machines,555 add_machines,556 remove_machines,557 prefer_identities,558 part_name,559 } => {560 // TODO: Forbid updating secrets with set expectedOwners (= not user-managed).561562 let secret = config.shared_secret(&name)?;563 if secret.secret.parts.get(&part_name).is_none() {564 bail!("no secret");565 }566567 let initial_machines = secret.owners.clone();568 let target_machines = parse_machines(569 initial_machines.clone(),570 machines,571 add_machines,572 remove_machines,573 )?;574575 if target_machines.is_empty() {576 info!("no machines left for secret, removing it");577 config.remove_shared(&name);578 return Ok(());579 }580581 let config_field = &config.config_unchecked_field;582 let field = nix_go!(config_field.sharedSecrets[{ name }]);583584 let updated = update_owner_set(585 &name,586 config,587 secret,588 field,589 &target_machines,590 &prefer_identities,591 )592 .await?;593 config.replace_shared(name, updated);594 }595 Secret::Regenerate { prefer_identities } => {596 info!("checking for secrets to regenerate");597 {598 let _span = info_span!("shared").entered();599 let expected_shared_set = config600 .list_configured_shared()601 .await?602 .into_iter()603 .collect::<HashSet<_>>();604 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();605 for missing in expected_shared_set.difference(&shared_set) {606 let config_field = &config.config_unchecked_field;607 let secret = nix_go!(config_field.sharedSecrets[{ missing }]);608 let expected_owners: Option<Vec<String>> =609 nix_go_json!(secret.expectedOwners);610 let Some(expected_owners) = expected_owners else {611 // TODO: Might still need to regenerate612 continue;613 };614 info!("generating secret: {missing}");615 let shared = generate_shared(config, missing, secret, expected_owners)616 .in_current_span()617 .await?;618 config.replace_shared(missing.to_string(), shared)619 }620 }621 for host in config.list_hosts().await? {622 if config.should_skip(&host.name) {623 continue;624 }625626 let _span = info_span!("host", host = host.name).entered();627 let expected_set = host628 .list_configured_secrets()629 .in_current_span()630 .await?631 .into_iter()632 .collect::<HashSet<_>>();633 let stored_set = config634 .list_secrets(&host.name)635 .into_iter()636 .collect::<HashSet<_>>();637 for missing in expected_set.difference(&stored_set) {638 info!("generating secret: {missing}");639 let secret = host.secret_field(missing).in_current_span().await?;640 let generated =641 match generate(config, missing, secret, &[host.name.clone()])642 .in_current_span()643 .await644 {645 Ok(v) => v,646 Err(e) => {647 error!("{e:?}");648 continue;649 }650 };651 config.insert_secret(&host.name, missing.to_string(), generated)652 }653 }654 let mut to_remove = Vec::new();655 for name in &config.list_shared() {656 info!("updating secret: {name}");657 let data = config.shared_secret(name)?;658 let config_field = &config.config_unchecked_field;659 let expected_owners: Vec<String> =660 nix_go_json!(config_field.sharedSecrets[{ name }].expectedOwners);661 if expected_owners.is_empty() {662 warn!("secret was removed from fleet config: {name}, removing from data");663 to_remove.push(name.to_string());664 continue;665 }666667 let secret = nix_go!(config_field.sharedSecrets[{ name }]);668 config.replace_shared(669 name.to_owned(),670 update_owner_set(671 name,672 config,673 data,674 secret,675 &expected_owners,676 &prefer_identities,677 )678 .await?,679 );680 }681 for k in to_remove {682 config.remove_shared(&k);683 }684 }685 Secret::List {} => {686 let _span = info_span!("loading secrets").entered();687 let configured = config.list_configured_shared().await?;688 #[derive(Tabled)]689 struct SecretDisplay {690 #[tabled(rename = "Name")]691 name: String,692 #[tabled(rename = "Owners")]693 owners: String,694 }695 let mut table = vec![];696 for name in configured.iter().cloned() {697 let config = config.clone();698 let expected_owners = config.shared_secret_expected_owners(&name).await?;699 let data = config.shared_secret(&name)?;700 let owners = data701 .owners702 .iter()703 .map(|o| {704 if expected_owners.contains(o) {705 o.green().to_string()706 } else {707 o.red().to_string()708 }709 })710 .collect::<Vec<_>>();711 table.push(SecretDisplay {712 owners: owners.join(", "),713 name,714 })715 }716 info!("loaded\n{}", Table::new(table).to_string())717 }718 Secret::Edit {719 name,720 machine,721 part,722 add,723 } => {724 let secret = config.host_secret(&machine, &name)?;725 if let Some(data) = secret.parts.get(&part) {726 let host = config.host(&machine).await?;727 let secret = host.decrypt(data.raw.clone()).await?;728 String::from_utf8(secret).context("secret is not utf8")?729 } else if add {730 String::new()731 } else {732 bail!("part {part} not found in secret {name}. Did you mean to `--add` it?");733 };734 }735 }736 Ok(())737 }738}739740async fn edit_temp_file(741 builder: tempfile::Builder<'_, '_>,742 r: Vec<u8>,743 header: &str,744 comment: &str,745) -> Result<(Vec<u8>, Option<String>), anyhow::Error> {746 if !stdin().is_tty() {747 // TODO: Also try to open /dev/tty directly?748 bail!("stdin is not tty, can't open editor");749 }750751 use std::fmt::Write;752 let mut file = builder.tempfile()?;753754 let mut full_header = String::new();755 let mut had = false;756 for line in header.trim_end().lines() {757 had = true;758 writeln!(&mut full_header, "{comment}{line}")?;759 }760 if had {761 writeln!(&mut full_header, "{}", comment.trim_end())?;762 }763 writeln!(764 &mut full_header,765 "{comment}Do not touch this header! It will be removed automatically"766 )?;767768 file.write_all(full_header.as_bytes())?;769 file.write_all(&r)?;770771 let abs_path = file.into_temp_path();772 let editor = std::env::var_os("VISUAL")773 .or_else(|| std::env::var_os("EDITOR"))774 .unwrap_or_else(|| "vi".into());775 let editor_args = shlex::bytes::split(editor.as_encoded_bytes())776 .ok_or_else(|| anyhow!("EDITOR env var has wrong syntax"))?;777 let editor_args = editor_args778 .into_iter()779 .map(|v| {780 // Only ASCII subsequences are replaced781 unsafe { OsString::from_encoded_bytes_unchecked(v) }782 })783 .collect_vec();784 let Some((editor, args)) = editor_args.split_first() else {785 bail!("EDITOR env var has no command");786 };787 let mut command = Command::new(editor);788 command.args(args);789790 let path_arg = abs_path.canonicalize()?;791792 // TODO: Save full state, using tcget/_getmode/_setmode793 let was_raw = terminal::is_raw_mode_enabled()?;794 terminal::enable_raw_mode()?;795796 let status = command.arg(path_arg).status().await;797798 if !was_raw {799 terminal::disable_raw_mode()?;800 }801802 let success = match status {803 Ok(s) => s.success(),804 Err(e) if e.kind() == io::ErrorKind::NotFound => {805 bail!("editor not found")806 }807 Err(e) => bail!("editor spawn error: {e}"),808 };809810 let mut file = std::fs::read(&abs_path).context("read editor output")?;811 let Some(v) = file.strip_prefix(full_header.as_bytes()) else {812 todo!();813 };814 todo!();815816 // Ok((success, abs_path))817}cmds/fleet/src/host.rsdiffbeforeafterboth--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -12,15 +12,14 @@
use anyhow::{anyhow, bail, ensure, Context, Result};
use clap::{ArgGroup, Parser};
use fleet_shared::SecretData;
+use nix_eval::{nix_go, nix_go_json, NixSessionPool, Value};
use openssh::SessionBuilder;
use serde::de::DeserializeOwned;
use tempfile::NamedTempFile;
use crate::{
- better_nix_eval::{Field, NixSessionPool},
command::MyCommand,
fleetdata::{FleetData, FleetSecret, FleetSharedSecret},
- nix_go, nix_go_json,
};
pub struct FleetConfigInternals {
@@ -30,12 +29,12 @@
pub data: Mutex<FleetData>,
pub nix_args: Vec<OsString>,
/// fleet_config.config
- pub config_field: Field,
+ pub config_field: Value,
/// fleet_config.unchecked.config
- pub config_unchecked_field: Field,
+ pub config_unchecked_field: Value,
/// import nixpkgs {system = local};
- pub default_pkgs: Field,
+ pub default_pkgs: Value,
}
#[derive(Clone)]
@@ -55,7 +54,7 @@
pub local: bool,
pub session: OnceLock<Arc<openssh::Session>>,
- pub nixos_config: Option<Field>,
+ pub nixos_config: Option<Value>,
}
impl ConfigHost {
async fn open_session(&self) -> Result<Arc<openssh::Session>> {
@@ -201,7 +200,7 @@
}
Ok(out)
}
- pub async fn secret_field(&self, name: &str) -> Result<Field> {
+ pub async fn secret_field(&self, name: &str) -> Result<Value> {
let Some(nixos) = &self.nixos_config else {
bail!("host is virtual and has no secrets");
};
@@ -209,7 +208,7 @@
}
/// Packages for this host, resolved with nixpkgs overlays
- pub async fn pkgs(&self) -> Result<Field> {
+ pub async fn pkgs(&self) -> Result<Value> {
let Some(nixos) = &self.nixos_config else {
return Ok(self.config.default_pkgs.clone());
};
@@ -261,7 +260,7 @@
}
Ok(out)
}
- pub async fn system_config(&self, host: &str) -> Result<Field> {
+ pub async fn system_config(&self, host: &str) -> Result<Value> {
let fleet_field = &self.config_unchecked_field;
Ok(nix_go!(fleet_field.hosts[{ host }].nixosSystem.config))
}
@@ -275,7 +274,7 @@
/// Shared secrets configured in fleet.nix or in flake
pub async fn list_configured_shared(&self) -> Result<Vec<String>> {
let config_field = &self.config_unchecked_field;
- nix_go!(config_field.sharedSecrets).list_fields().await
+ Ok(nix_go!(config_field.sharedSecrets).list_fields().await?)
}
/// Shared secrets configured in fleet.nix
pub fn list_shared(&self) -> Vec<String> {
@@ -389,13 +388,13 @@
let pool = NixSessionPool::new(directory.as_os_str().to_owned(), nix_args.clone()).await?;
let root_field = pool.get().await?;
- let builtins_field = Field::field(root_field.clone(), "builtins").await?;
+ let builtins_field = Value::binding(root_field.clone(), "builtins").await?;
if self.local_system == "detect" {
self.local_system = nix_go_json!(builtins_field.currentSystem);
}
let local_system = self.local_system.clone();
- let fleet_root = Field::field(root_field, "fleetConfigurations").await?;
+ let fleet_root = Value::binding(root_field, "fleetConfigurations").await?;
let fleet_field = nix_go!(fleet_root.default);
let config_field = nix_go!(fleet_field.config);
cmds/fleet/src/main.rsdiffbeforeafterboth--- a/cmds/fleet/src/main.rs
+++ b/cmds/fleet/src/main.rs
@@ -1,5 +1,5 @@
#![recursion_limit = "512"]
-#![feature(try_blocks, lint_reasons)]
+#![feature(try_blocks)]
pub(crate) mod cmds;
pub(crate) mod command;
@@ -173,6 +173,8 @@
setup_logging();
if let Err(e) = main_real().await {
// If I remove this line, the next error!() line gets eaten.
+ // This is a bug in indicatif, it needs to be fixed
+ #[cfg(feature = "indicatif")]
info!("fixme: this line gets eaten by tracing-indicatif on levels info+");
error!("{e:#}");
return ExitCode::FAILURE;
@@ -181,7 +183,7 @@
}
async fn main_real() -> Result<()> {
- let _ = better_nix_eval::TOKIO_RUNTIME.set(tokio::runtime::Handle::current());
+ nix_eval::init_tokio();
let nix_args = std::env::var_os("NIX_ARGS")
.map(|a| extra_args::parse_os(&a))
cmds/generator-helper/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/cmds/generator-helper/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "fleet-generator-helper"
+edition = "2021"
+version.workspace = true
+
+[dependencies]
+age.workspace = true
+anyhow.workspace = true
+clap.workspace = true
+ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
+fleet-shared.workspace = true
+rand = "0.8.5"
cmds/generator-helper/src/main.rsdiffbeforeafterboth--- /dev/null
+++ b/cmds/generator-helper/src/main.rs
@@ -0,0 +1,204 @@
+use std::{
+ fs,
+ io::{self, stdout, Cursor, Read, Write},
+ path::PathBuf,
+ str::FromStr,
+};
+
+use age::Recipient;
+use anyhow::{anyhow, bail, ensure, Context, Result};
+use clap::Parser;
+use ed25519_dalek::SigningKey;
+use fleet_shared::SecretData;
+use rand::{
+ distributions::{Alphanumeric, DistString, Distribution, Uniform},
+ rngs::OsRng,
+ thread_rng, Rng,
+};
+
+fn write_output(out: &str, data: impl AsRef<[u8]>, stdout_marker: &mut bool) -> Result<()> {
+ let data = data.as_ref();
+ if out == "-" {
+ let mut stdout = stdout();
+ if *stdout_marker {
+ stdout.write_all(&[b'\n'])?;
+ }
+ *stdout_marker = true;
+ stdout.write_all(data)?;
+ } else {
+ fs::write(out, data)?;
+ };
+ Ok(())
+}
+
+#[derive(Parser)]
+enum Generate {
+ /// Generate public, private keys without wrapping, in standard ed25519 schema
+ /// (64 bytes private (due to merge with private), 32 bytes public)
+ Ed25519 {
+ public: String,
+ private: String,
+ /// Private key should be just the private key (32 bytes), not standard private+public.
+ #[arg(long)]
+ no_embed_public: bool,
+ },
+ Password {
+ output: String,
+ size: usize,
+ #[arg(long, short = 'n')]
+ no_symbols: bool,
+ },
+}
+
+#[derive(Parser)]
+enum Opts {
+ /// Encode public part from stdin.
+ Public {
+ #[arg(long)]
+ allow_empty: bool,
+ },
+ /// Encrypt private part from stdin.
+ Private {
+ #[arg(long)]
+ allow_empty: bool,
+ #[arg(short = 'r')]
+ recipient: Vec<String>,
+ },
+ /// Generate keys in well-known schemas.
+ ///
+ /// Note that this command is only intended to be used in fleet secret generator,
+ /// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.
+ #[command(subcommand)]
+ Generate(Generate),
+ // Generate {
+ // kind: GenerateKind,
+ // /// Different generators generate different number of files, you need to specify number of outputs corresponding to the generator.
+ // #[arg(short = 'o')]
+ // outputs: Vec<String>,
+ // },
+}
+
+fn parse_stdin() -> Result<Option<Vec<u8>>> {
+ let mut input = vec![];
+ io::stdin().read_to_end(&mut input)?;
+ if input.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(input))
+ }
+}
+pub fn encrypt_secret_data(
+ recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,
+ data: Vec<u8>,
+) -> Option<SecretData> {
+ let mut encrypted = vec![];
+ let recipients = recipients
+ .into_iter()
+ .map(|v| Box::new(v) as Box<dyn Recipient + Send>)
+ .collect::<Vec<_>>();
+ let mut encryptor = age::Encryptor::with_recipients(recipients)?
+ .wrap_output(&mut encrypted)
+ .expect("in memory write");
+ io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");
+ encryptor.finish().expect("in memory flush");
+ Some(SecretData {
+ data: encrypted,
+ encrypted: true,
+ })
+}
+
+fn main() -> Result<()> {
+ let opts = Opts::parse();
+ // Assumed to be secure, seeded from secure OsRng+reseeded.
+ let mut rng = thread_rng();
+
+ match opts {
+ Opts::Public { allow_empty } => {
+ let stdin = parse_stdin()?;
+ if stdin.is_none() && !allow_empty {
+ bail!("empty stdin input is not allowed unless --allow-empty is set");
+ }
+ let stdin = stdin.unwrap_or_default();
+ io::stdout().write_all(
+ SecretData {
+ data: stdin,
+ encrypted: false,
+ }
+ .to_string()
+ .as_bytes(),
+ )?;
+ }
+ Opts::Private {
+ allow_empty,
+ recipient,
+ } => {
+ let stdin = parse_stdin()?;
+ if stdin.is_none() && !allow_empty {
+ bail!("empty stdin input is not allowed unless --allow-empty is set");
+ }
+ let stdin = stdin.unwrap_or_default();
+ if recipient.is_empty() {
+ bail!("recipient list is empty");
+ }
+ let out = encrypt_secret_data(
+ recipient
+ .into_iter()
+ .map(|r| age::ssh::Recipient::from_str(&r))
+ .collect::<Result<Vec<age::ssh::Recipient>, age::ssh::ParseRecipientKeyError>>()
+ .map_err(|e| anyhow!("parse recipients: {e:?}"))?,
+ stdin,
+ )
+ .expect("got recipients");
+ io::stdout().write_all(out.to_string().as_bytes())?;
+ }
+ Opts::Generate(gen) => {
+ let mut stdout_marker: bool = false;
+ match gen {
+ Generate::Ed25519 {
+ public,
+ private,
+ no_embed_public,
+ } => {
+ let key = SigningKey::generate(&mut rng).to_keypair_bytes();
+
+ write_output(&public, &key[32..], &mut stdout_marker).context("public")?;
+ write_output(
+ &private,
+ &key[..{
+ if no_embed_public {
+ 32
+ } else {
+ 64
+ }
+ }],
+ &mut stdout_marker,
+ )
+ .context("private")?;
+ }
+ Generate::Password {
+ size,
+ no_symbols,
+ output,
+ } => {
+ ensure!(
+ size >= 6,
+ "misconfiguration? password is shorter than 6 chars"
+ );
+ let out = if no_symbols {
+ Alphanumeric.sample_string(&mut rng, size)
+ } else {
+ // Alphabet of Alphanumberic + symbols
+ const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
+ let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len());
+ (0..size)
+ .map(|_| uniform.sample(&mut rng))
+ .map(|i| GEN_ASCII_SYMBOLS[i] as char)
+ .collect::<String>()
+ };
+ write_output(&output, out, &mut stdout_marker)?;
+ }
+ }
+ }
+ }
+ Ok(())
+}
cmds/install-secrets/Cargo.tomldiffbeforeafterboth--- a/cmds/install-secrets/Cargo.toml
+++ b/cmds/install-secrets/Cargo.toml
@@ -6,18 +6,13 @@
[dependencies]
-age = { version = "0.10.0", features = ["ssh"] }
-anyhow = "1.0.79"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-tracing = "0.1"
-nix = {version = "0.27.1", features = ["user", "fs"]}
-serde = { version = "1.0.196", features = ["derive"] }
-serde_json = "1.0.113"
-clap = { version = ">=4.4, <4.5", features = [
- "derive",
- "env",
- "wrap_help",
- "unicode",
-] }
-tempfile = "3.10.0"
+clap.workspace = true
fleet-shared.workspace = true
+age.workspace = true
+anyhow.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+tempfile.workspace = true
+nix.workspace = true
cmds/remowt-agent/Cargo.tomldiffbeforeafterboth--- a/cmds/remowt-agent/Cargo.toml
+++ b/cmds/remowt-agent/Cargo.toml
@@ -6,3 +6,5 @@
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+iroh-net = "0.17.0"
+tracing.workspace = true
crates/nix-eval/Cargo.tomldiffbeforeafterboth--- /dev/null
+++ b/crates/nix-eval/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "nix-eval"
+edition = "2021"
+version.workspace = true
+
+[dependencies]
+better-command.workspace = true
+futures = "0.3.30"
+itertools = "0.13.0"
+nixlike.workspace = true
+r2d2 = "0.8.10"
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
+thiserror = "1.0.61"
+tokio = { workspace = true, features = ["process", "io-util"] }
+tokio-util = { version = "0.7.11", features = ["codec"] }
+tracing.workspace = true
+unindent = "0.2.3"
crates/nix-eval/src/lib.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/nix-eval/src/lib.rs
@@ -0,0 +1,32 @@
+//! This whole library should be replaced with either binding to nix libexpr,
+//! or with tvix (once it is able to build NixOS).
+//!
+//! Current api is awful, little effort was put into this implementation.
+
+use std::sync::Arc;
+
+pub use pool::NixSessionPool;
+use pool::NixSessionPoolInner;
+use r2d2::PooledConnection;
+pub use session::{Error, Result};
+pub use value::{Index, Value};
+
+mod pool;
+mod session;
+mod value;
+// Contains macros helpers
+#[doc(hidden)]
+pub mod macros;
+
+#[derive(Clone)]
+pub struct NixSession(pub(crate) Arc<tokio::sync::Mutex<PooledConnection<NixSessionPoolInner>>>);
+
+impl NixSession {
+ fn ptr_eq(a: &Self, b: &Self) -> bool {
+ Arc::ptr_eq(&a.0, &b.0)
+ }
+}
+
+pub fn init_tokio() {
+ let _ = pool::TOKIO_RUNTIME.set(tokio::runtime::Handle::current());
+}
crates/nix-eval/src/macros.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/nix-eval/src/macros.rs
@@ -0,0 +1,183 @@
+use serde::Serialize;
+
+use crate::{NixSession, Value};
+
+#[derive(Clone)]
+pub struct NixExprBuilder {
+ pub(crate) out: String,
+ used_fields: Vec<Value>,
+}
+impl NixExprBuilder {
+ pub fn object() -> Self {
+ NixExprBuilder {
+ out: "{ ".to_owned(),
+ used_fields: Vec::new(),
+ }
+ }
+ pub fn string(s: &str) -> Self {
+ NixExprBuilder {
+ out: nixlike::serialize(s)
+ .expect("no problems with serializing_string")
+ .trim_end()
+ .to_owned(),
+ used_fields: Vec::new(),
+ }
+ }
+ pub fn serialized(v: impl Serialize) -> Self {
+ let serialized = nixlike::serialize(v).expect("invalid value for apply");
+ Self {
+ out: serialized.trim_end().to_owned(),
+ used_fields: Vec::new(),
+ }
+ }
+ pub fn value(f: Value) -> Self {
+ Self {
+ out: format!("sess_field_{}", f.session_field_id()),
+ used_fields: vec![f],
+ }
+ }
+ pub fn end_obj(&mut self) {
+ self.out.push('}');
+ }
+ pub fn obj_key(&mut self, name: Self, value: Self) {
+ self.out.push_str(r#""${"#);
+ self.extend(name);
+ self.out.push_str(r#"}" = "#);
+ self.extend(value);
+ self.out.push_str("; ");
+ }
+
+ pub fn extend(&mut self, e: Self) {
+ self.out.push_str(&e.out);
+ self.used_fields.extend(e.used_fields);
+ }
+
+ #[allow(dead_code)]
+ pub fn session(&self) -> NixSession {
+ let mut session = None;
+ for ele in &self.used_fields {
+ if session.is_none() {
+ session = Some(ele.session());
+ continue;
+ }
+ let session = session.as_ref().expect("checked");
+ let ele_sess = ele.session();
+ assert!(
+ NixSession::ptr_eq(session, &ele_sess),
+ "can't mix fields from different session"
+ );
+ }
+ session.expect("expr without fields used")
+ }
+ #[allow(dead_code)]
+ pub fn index_attr(&mut self, s: &str) {
+ let escaped = nixlike::serialize(s).expect("string");
+ self.out.push('.');
+ self.out.push_str(escaped.trim_end());
+ }
+}
+
+#[macro_export]
+macro_rules! nix_expr_inner {
+ //(@munch_object FIXME: value should be arbitrary nix_expr_inner input... Time to write proc-macro?
+ (@obj($o:ident) $field:ident, $($tt:tt)*) => {{
+ $o.obj_key(
+ NixExprBuilder::string(stringify!($field)),
+ NixExprBuilder::value($field),
+ );
+ nix_expr_inner!(@obj($o) $($tt)*);
+ }};
+ (@obj($o:ident) $field:ident: $v:block, $($tt:tt)*) => {{
+ $o.obj_key(
+ NixExprBuilder::string(stringify!($field)),
+ NixExprBuilder::serialized(&$v),
+ );
+ nix_expr_inner!(@obj($o) $($tt)*);
+ }};
+ (@obj($o:ident)) => {{}};
+ (Obj { $($tt:tt)* }) => {{
+ use $crate::{macros::NixExprBuilder, nix_expr_inner};
+ let mut out = NixExprBuilder::object();
+ nix_expr_inner!(@obj(out) $($tt)*);
+ out.end_obj();
+ out
+ }};
+ (@field($o:ident) . $var:ident $($tt:tt)*) => {{
+ $o.index_attr(stringify!($var));
+ nix_expr_inner!(@field($o) $($tt)*);
+ }};
+ (@field($o:ident) [{ $v:expr }] $($tt:tt)*) => {{
+ $o.push(Index::attr(&$v));
+ nix_expr_inner!(@o($o) $($tt)*);
+ }};
+ (@field($o:ident) [ $($var:tt)+ ] $($tt:tt)*) => {{
+ $o.push(Index::Expr($crate::nix_expr_inner!($($var)+)));
+ nix_expr_inner!(@o($o) $($tt)*);
+ }};
+ (@field($o:ident) ($($var:tt)*) $($tt:tt)*) => {
+ $o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
+ nix_expr_inner!(@o($o) $($tt)*);
+ };
+ (@field($o:ident)) => {};
+ ($field:ident $($tt:tt)*) => {{
+ use $crate::{macros::NixExprBuilder, nix_expr_inner};
+ // might be used if indexed
+ #[allow(unused_mut)]
+ let mut out = NixExprBuilder::value($field.clone());
+ nix_expr_inner!(@field(out) $($tt)*);
+ out
+ }};
+ ($v:literal) => {{
+ use $crate::macros::NixExprBuilder;
+ NixExprBuilder::string($v)
+ }};
+ ({$v:expr}) => {{
+ use $crate::macros::NixExprBuilder;
+ NixExprBuilder::serialized(&$v)
+ }}
+}
+#[macro_export]
+macro_rules! nix_expr {
+ ($($tt:tt)+) => {{
+ use $crate::{macros::{NixExprBuilder}, Value, nix_expr_inner};
+ let expr = nix_expr_inner!($($tt)+);
+ Field::new(expr.session(), expr.out)
+ }};
+}
+
+#[macro_export]
+macro_rules! nix_go {
+ (@o($o:ident) . $var:ident $($tt:tt)*) => {{
+ $o.push(Index::attr(stringify!($var)));
+ nix_go!(@o($o) $($tt)*);
+ }};
+ (@o($o:ident) [{ $v:expr }] $($tt:tt)*) => {{
+ $o.push(Index::attr(&$v));
+ nix_go!(@o($o) $($tt)*);
+ }};
+ (@o($o:ident) [ $($var:tt)+ ] $($tt:tt)*) => {{
+ $o.push(Index::Expr($crate::nix_expr_inner!($($var)+)));
+ nix_go!(@o($o) $($tt)*);
+ }};
+ (@o($o:ident) ($($var:tt)*) $($tt:tt)*) => {
+ $o.push(Index::ExprApply($crate::nix_expr_inner!($($var)+)));
+ nix_go!(@o($o) $($tt)*);
+ };
+ (@o($o:ident) | $($var:tt)*) => {
+ $o.push(Index::Pipe($crate::nix_expr_inner!($($var)+)));
+ };
+ (@o($o:ident)) => {};
+ ($field:ident $($tt:tt)+) => {{
+ use $crate::{nix_go, Index};
+ let field = $field.clone();
+ let mut out = vec![];
+ nix_go!(@o(out) $($tt)*);
+ field.select(out).await?
+ }}
+}
+#[macro_export]
+macro_rules! nix_go_json {
+ ($($tt:tt)*) => {{
+ $crate::nix_go!($($tt)*).as_json().await?
+ }};
+}
crates/nix-eval/src/pool.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/nix-eval/src/pool.rs
@@ -0,0 +1,61 @@
+use std::ffi::OsString;
+use std::sync::{Arc, OnceLock};
+
+use r2d2::Pool;
+
+use crate::session::NixSessionInner;
+use crate::{Error, NixSession, Result};
+
+pub struct NixSessionPool(Pool<NixSessionPoolInner>);
+impl NixSessionPool {
+ pub async fn new(flake: OsString, nix_args: Vec<OsString>) -> Result<Self> {
+ let inner = tokio::task::block_in_place(|| {
+ r2d2::Builder::<NixSessionPoolInner>::new()
+ .min_idle(Some(0))
+ .build(NixSessionPoolInner { flake, nix_args })
+ })?;
+ Ok(Self(inner))
+ }
+ pub async fn get(&self) -> Result<NixSession> {
+ let v = tokio::task::block_in_place(|| self.0.get())?;
+ Ok(NixSession(Arc::new(tokio::sync::Mutex::new(v))))
+ }
+}
+
+pub(crate) struct NixSessionPoolInner {
+ flake: OsString,
+ nix_args: Vec<OsString>,
+}
+
+impl r2d2::ManageConnection for NixSessionPoolInner {
+ type Connection = NixSessionInner;
+ type Error = Error;
+ fn connect(&self) -> std::result::Result<Self::Connection, Self::Error> {
+ let _v = TOKIO_RUNTIME
+ .get()
+ .expect("missed tokio runtime init!")
+ .enter();
+ Ok(futures::executor::block_on(NixSessionInner::new(
+ self.flake.as_os_str(),
+ self.nix_args.iter().map(OsString::as_os_str),
+ ))?)
+ }
+
+ fn is_valid(&self, conn: &mut Self::Connection) -> std::result::Result<(), Self::Error> {
+ let _v = TOKIO_RUNTIME
+ .get()
+ .expect("missed tokio runtime init!")
+ .enter();
+ let res = futures::executor::block_on(conn.execute_expression_number("2 + 2"))?;
+ if res != 4 {
+ // just in case, should fail much earlier
+ return Err(Error::SessionInit("misbehaving session"));
+ };
+ Ok(())
+ }
+
+ fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
+ false
+ }
+}
+pub static TOKIO_RUNTIME: OnceLock<tokio::runtime::Handle> = OnceLock::new();
crates/nix-eval/src/session.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/nix-eval/src/session.rs
@@ -0,0 +1,415 @@
+use std::{ffi::OsStr, num::ParseIntError, process::Stdio, sync::Arc};
+
+use better_command::{ClonableHandler, Handler, NixHandler, NoopHandler};
+use futures::StreamExt;
+use itertools::Itertools as _;
+use serde::{de::DeserializeOwned, Deserialize};
+use thiserror::Error;
+use tokio::{
+ io::AsyncWriteExt,
+ process::{ChildStderr, ChildStdin, ChildStdout, Command},
+ select,
+ sync::{mpsc, oneshot, Mutex},
+};
+use tokio_util::codec::{FramedRead, LinesCodec};
+use tracing::{debug, error, warn, Level};
+
+#[derive(Error, Debug)]
+pub enum Error {
+ #[error("failed to create nix repl session: {0}")]
+ SessionInit(&'static str),
+ #[error("unexpected end of output, nix crashed?")]
+ MissingDelimiter,
+
+ #[error("expression did'nt produce any output")]
+ ExpectedOutput,
+ #[error("expression produced output, which is unexpected")]
+ UnexpectedOutput,
+
+ #[error("unexpected expression output type")]
+ InvalidType,
+
+ #[error("failed to build attr {attribute}:\n{error}")]
+ BuildFailed { attribute: String, error: String },
+
+ #[error("output: {0}")]
+ Json(#[from] serde_json::Error),
+ // int outputs are too specific, and should not be used,
+ // thus error is ok to be not informative.
+ #[error("int output: {0}")]
+ Int(ParseIntError),
+ #[error("pool: {0}")]
+ Pool(#[from] r2d2::Error),
+ #[error("io: {0}")]
+ Io(#[from] std::io::Error),
+
+ // TODO: Should be done by wrapper/in different type.
+ #[error("at {0}: {1}")]
+ InContext(String, Box<Self>),
+
+ #[error("error: {0}")]
+ NixError(String),
+}
+impl Error {
+ pub(crate) fn context(self, context: String) -> Self {
+ Self::InContext(context, Box::new(self))
+ }
+}
+pub type Result<T, E = Error> = std::result::Result<T, E>;
+
+enum OutputLine {
+ Out(String),
+ Err(String),
+}
+struct OutputHandler {
+ rx: mpsc::Receiver<OutputLine>,
+ _cancel_handle: oneshot::Receiver<()>,
+}
+impl OutputHandler {
+ fn new(out: ChildStdout, err: ChildStderr) -> Self {
+ let mut out = FramedRead::new(out, LinesCodec::new());
+ let mut err = FramedRead::new(err, LinesCodec::new());
+ let (tx, rx) = mpsc::channel(20);
+ let (mut cancelled, _cancel_handle) = oneshot::channel();
+ tokio::spawn(async move {
+ loop {
+ select! {
+ // We should receive errors earlier than synchronization
+ biased;
+ e = err.next() => {
+ let Some(Ok(e)) = e else {
+ if e.is_some() {
+ error!("bad repl stderr: {e:?}");
+ }
+ continue;
+ };
+ let _ = tx.send(OutputLine::Err(e)).await;
+ }
+ o = out.next() => {
+ let Some(Ok(o)) = o else {
+ if o.is_some() {
+ error!("bad repl stdout: {o:?}");
+ }
+ continue;
+ };
+ let _ = tx.send(OutputLine::Out(o)).await;
+ }
+ // Reader doesn't care about stdout, as this is cancelled.
+ // Error still might be useful, to process leftover span closures?
+ _ = cancelled.closed() => {
+ break;
+ }
+ }
+ }
+ });
+ Self { rx, _cancel_handle }
+ }
+ async fn next(&mut self) -> Option<OutputLine> {
+ self.rx.recv().await
+ }
+}
+
+#[must_use]
+struct ErrorCollector<'i, H> {
+ collected: Vec<String>,
+ inner: &'i mut H,
+}
+impl<'i, H> ErrorCollector<'i, H> {
+ fn new(inner: &'i mut H) -> Self {
+ Self {
+ collected: vec![],
+ inner,
+ }
+ }
+}
+impl<H> ErrorCollector<'_, H> {
+ fn handle_line_inner(&mut self, msg: &str) -> bool {
+ let Some(msg) = msg.strip_prefix("@nix ") else {
+ return false;
+ };
+ #[derive(Deserialize)]
+ struct ErrorAction {
+ action: String,
+ level: u32,
+ msg: String,
+ }
+ let Ok(act) = serde_json::from_str::<ErrorAction>(msg) else {
+ return false;
+ };
+ if act.action != "msg" || act.level != 0 {
+ return false;
+ }
+ self.collected.push(act.msg);
+ true
+ }
+ fn finish(self) -> Result<()> {
+ // fn dedent(s: String) -> String {
+ // s.split('\n').filter(|s| !s.trim().is_empty()).map(|v| v.)
+ // }
+ if !self.collected.is_empty() {
+ return Err(Error::NixError(format!(
+ "{}",
+ self.collected
+ .iter()
+ .map(|v| {
+ if let Some(f) = v.strip_prefix("\u{1b}[31;1merror:\u{1b}[0m ") {
+ let v = unindent::unindent(f.trim_start());
+ v.trim().to_owned()
+ } else {
+ v.to_owned()
+ }
+ })
+ .join("\n"),
+ )));
+ }
+ Ok(())
+ }
+ fn flush(self) {
+ for line in self.collected {
+ warn!("{line}");
+ }
+ }
+}
+impl<H: Handler> Handler for ErrorCollector<'_, H> {
+ fn handle_line(&mut self, e: &str) {
+ if self.handle_line_inner(e) {
+ return;
+ }
+ self.inner.handle_line(e)
+ }
+}
+
+pub struct NixSessionInner {
+ full_delimiter: String,
+ nix_handler: ClonableHandler<NixHandler>,
+ out: OutputHandler,
+ stdin: ChildStdin,
+ string_wrapping: (String, String),
+ number_wrapping: (String, String),
+
+ executing_command: Arc<Mutex<()>>,
+
+ next_id: u32,
+ pub(crate) free_list: Vec<u32>,
+}
+
+/// Discover inter-message repl delimiter
+const REPL_DELIMITER: &str = "\"FLEET_MAGIC_REPL_DELIMITER\"";
+/// Discover formatting around strings
+const TRAIN_STRING: &str = "\"TRAIN_STRING\"";
+/// Discover formatting around numbers
+const TRAIN_NUMBER: &str = "13141516";
+// Other types of formatting are not discovered, because they are not used, JSON serialization is used instead
+// Techically, number training is also not required, because numbers can be converted to string too...
+// Eh, I'll remove it later.
+
+impl NixSessionInner {
+ pub(crate) async fn new(
+ flake: &OsStr,
+ extra_args: impl IntoIterator<Item = &OsStr>,
+ ) -> Result<Self> {
+ let mut cmd = Command::new("nix");
+ cmd.arg("repl")
+ .arg(flake)
+ .arg("--log-format")
+ .arg("internal-json");
+ for arg in extra_args {
+ cmd.arg(arg);
+ }
+ cmd.stdin(Stdio::piped());
+ cmd.stdout(Stdio::piped());
+ cmd.stderr(Stdio::piped());
+ let cmd = cmd.spawn()?;
+ let stdout = cmd.stdout.unwrap();
+ let stderr = cmd.stderr.unwrap();
+ let mut out = OutputHandler::new(stdout, stderr);
+ let mut stdin = cmd.stdin.unwrap();
+ // Standard repl hello doesn't work with internal-json logger
+ stdin.write_all(REPL_DELIMITER.as_bytes()).await?;
+ stdin.write_all(b"\n").await?;
+ stdin.flush().await?;
+ let nix_handler = NixHandler::default();
+ let mut full_delimiter = None;
+ let mut errors = vec![];
+ while let Some(line) = out.next().await {
+ let line = match line {
+ OutputLine::Out(o) => o,
+ OutputLine::Err(_e) => {
+ // Handle startup errors, but skip repl hello?
+ errors.push(_e);
+ continue;
+ }
+ };
+ if line.contains(REPL_DELIMITER) {
+ debug!("discovered repl delimiter with added colors: {line}");
+ full_delimiter = Some(line.to_owned());
+ break;
+ }
+ }
+ let Some(full_delimiter) = full_delimiter else {
+ for e in errors {
+ error!("{e}");
+ }
+ return Err(Error::SessionInit("failed to discover delimiter"));
+ };
+ let mut res = Self {
+ full_delimiter,
+ nix_handler: ClonableHandler::new(nix_handler),
+ out,
+ stdin,
+ string_wrapping: Default::default(),
+ number_wrapping: Default::default(),
+
+ executing_command: Arc::new(Mutex::new(())),
+
+ next_id: 0,
+ free_list: vec![],
+ };
+ res.train().await?;
+ Ok(res)
+ }
+ async fn train(&mut self) -> Result<()> {
+ {
+ let full_string = self
+ .execute_expression_raw(TRAIN_STRING, &mut NoopHandler)
+ .await?;
+ let string_offset = full_string.find(TRAIN_STRING).expect("contained");
+ let string_prefix = &full_string[..string_offset];
+ let string_suffix = &full_string[string_offset + TRAIN_STRING.len()..];
+ self.string_wrapping = (string_prefix.to_owned(), string_suffix.to_owned());
+ }
+ {
+ let full_number = self
+ .execute_expression_raw(TRAIN_NUMBER, &mut NoopHandler)
+ .await?;
+ let number_offset = full_number.find(TRAIN_NUMBER).expect("contained");
+ let number_prefix = &full_number[..number_offset];
+ let number_suffix = &full_number[number_offset + TRAIN_NUMBER.len()..];
+ self.number_wrapping = (number_prefix.to_owned(), number_suffix.to_owned());
+ }
+ Ok(())
+ }
+ async fn send_command(&mut self, cmd: impl AsRef<[u8]>) -> Result<()> {
+ if tracing::enabled!(Level::DEBUG) && cmd.as_ref() != REPL_DELIMITER.as_bytes() {
+ let cmd_str = String::from_utf8_lossy(cmd.as_ref());
+ tracing::debug!("{cmd_str}");
+ };
+ self.stdin.write_all(cmd.as_ref()).await?;
+ self.stdin.write_all(b"\n").await?;
+ Ok(())
+ }
+ async fn read_until_delimiter(&mut self, err_handler: &mut dyn Handler) -> Result<String> {
+ let mut out = String::new();
+ while let Some(line) = self.out.next().await {
+ let line = match line {
+ OutputLine::Out(out) => out,
+ OutputLine::Err(err) => {
+ err_handler.handle_line(&err);
+ continue;
+ }
+ };
+ if line == self.full_delimiter {
+ return Ok(out);
+ }
+ if !out.is_empty() {
+ out.push('\n');
+ }
+ out.push_str(&line);
+ }
+ return Err(Error::MissingDelimiter);
+ }
+ pub(crate) async fn execute_expression_number(
+ &mut self,
+ expr: impl AsRef<[u8]>,
+ ) -> Result<u64> {
+ let num = self.number_wrapping.clone();
+ let n = self.execute_expression_wrapping(expr, &num).await?;
+ n.parse::<u64>().map_err(Error::Int)
+ }
+ async fn execute_expression_string(&mut self, expr: impl AsRef<[u8]>) -> Result<String> {
+ let num = self.string_wrapping.clone();
+ let n = self.execute_expression_wrapping(expr, &num).await?;
+ let str: String = serde_json::from_str(&n)?;
+ Ok(str)
+ }
+ pub(crate) async fn execute_expression_to_json<V: DeserializeOwned>(
+ &mut self,
+ expr: impl AsRef<[u8]>,
+ ) -> Result<V> {
+ let mut fexpr = b"builtins.toJSON (".to_vec();
+ fexpr.extend_from_slice(expr.as_ref());
+ fexpr.push(b')');
+ let v = self.execute_expression_string(fexpr).await?;
+ Ok(serde_json::from_str(&v)?)
+ }
+ async fn execute_expression_wrapping(
+ &mut self,
+ expr: impl AsRef<[u8]>,
+ wrapping: &(String, String),
+ ) -> Result<String> {
+ let mut nix_handler = self.nix_handler.clone();
+ let mut collected = ErrorCollector::new(&mut nix_handler);
+ let res = self.execute_expression_raw(expr, &mut collected).await?;
+ if res.is_empty() {
+ collected.finish()?;
+ return Err(Error::ExpectedOutput);
+ } else {
+ collected.flush()
+ };
+ let Some(res) = res.strip_prefix(&wrapping.0) else {
+ return Err(Error::InvalidType);
+ };
+ let Some(res) = res.strip_suffix(&wrapping.1) else {
+ return Err(Error::InvalidType);
+ };
+ Ok(res.to_owned())
+ }
+ async fn execute_expression_empty(&mut self, expr: impl AsRef<[u8]>) -> Result<()> {
+ let mut nix_handler = self.nix_handler.clone();
+ let mut collected = ErrorCollector::new(&mut nix_handler);
+ let v = self.execute_expression_raw(expr, &mut collected).await?;
+ collected.finish()?;
+ if !v.is_empty() {
+ return Err(Error::UnexpectedOutput);
+ }
+ Ok(())
+ }
+ pub(crate) async fn execute_expression_raw(
+ &mut self,
+ expr: impl AsRef<[u8]>,
+ err_handler: &mut dyn Handler,
+ ) -> Result<String> {
+ // Prevent two commands from being executed in parallel, messing with each other.
+ let _lock = self.executing_command.clone();
+ let _guard = _lock.lock().await;
+
+ self.send_command(expr).await?;
+ // It will be echoed
+ self.send_command(REPL_DELIMITER).await?;
+ self.read_until_delimiter(err_handler).await
+ }
+ pub(crate) async fn execute_assign(&mut self, expr: impl AsRef<str>) -> Result<u32> {
+ let id = self.allocate_id();
+ self.execute_expression_empty(format!("sess_field_{id} = {}", expr.as_ref()))
+ .await?;
+ Ok(id)
+ }
+
+ /// Id should be immediately used
+ fn allocate_id(&mut self) -> u32 {
+ if let Some(free) = self.free_list.pop() {
+ free
+ } else {
+ let v = self.next_id;
+ self.next_id += 1;
+ v
+ }
+ }
+ // Nix has no way to deallocate variable, yet GC will correct everything not reachable.
+ // async fn free_id(&mut self, id: u32) -> Result<()> {
+ // self.execute_expression_empty(format!("sess_field_{id} = null"))
+ // .await?;
+ // self.free_list.push(id);
+ // Ok(())
+ // }
+}
crates/nix-eval/src/value.rsdiffbeforeafterboth--- /dev/null
+++ b/crates/nix-eval/src/value.rs
@@ -0,0 +1,291 @@
+use std::{collections::HashMap, fmt, path::PathBuf, sync::Arc};
+
+use better_command::NixHandler;
+use serde::{de::DeserializeOwned, Serialize};
+
+use crate::{macros::NixExprBuilder, nix_go, Error, NixSession, Result};
+
+#[derive(Clone)]
+pub enum Index {
+ Var(String),
+ String(String),
+ #[allow(dead_code)]
+ Apply(String),
+ #[allow(dead_code)]
+ Expr(NixExprBuilder),
+ ExprApply(NixExprBuilder),
+ Pipe(NixExprBuilder),
+}
+impl Index {
+ pub fn var(v: impl AsRef<str>) -> Self {
+ let v = v.as_ref();
+ assert!(
+ !(v.contains('.') | v.contains(' ')),
+ "bad variable name: {v}"
+ );
+ Self::Var(v.to_owned())
+ }
+ pub fn attr(v: impl AsRef<str>) -> Self {
+ Self::String(v.as_ref().to_owned())
+ }
+ #[allow(dead_code)]
+ pub fn apply(v: impl Serialize) -> Self {
+ let serialized = nixlike::serialize(v).expect("invalid value for apply");
+ Self::Apply(serialized.trim_end().to_owned())
+ }
+}
+impl fmt::Display for Index {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Index::Var(v) => {
+ write!(f, "{v}")
+ }
+ Index::String(k) => {
+ let v = nixlike::format_identifier(k.as_str());
+ write!(f, ".{v}")
+ }
+ Index::Apply(o) => {
+ write!(f, "<apply>({o})")
+ }
+ Index::Expr(e) => {
+ write!(f, "[{}]", e.out)
+ }
+ Index::ExprApply(e) => {
+ write!(f, "<apply>({})", e.out)
+ }
+ Index::Pipe(e) => {
+ write!(f, "<map>({})", e.out)
+ }
+ }
+ }
+}
+impl fmt::Debug for Index {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{self}")
+ }
+}
+struct PathDisplay<'i>(&'i [Index]);
+impl fmt::Display for PathDisplay<'_> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ for i in self.0 {
+ write!(f, "{i}")?;
+ }
+ Ok(())
+ }
+}
+struct ValueInner {
+ full_path: Option<Vec<Index>>,
+ session: NixSession,
+ value: Option<u32>,
+}
+#[derive(Clone)]
+pub struct Value(Arc<ValueInner>);
+impl Value {
+ fn root(session: NixSession) -> Self {
+ Self(Arc::new(ValueInner {
+ full_path: Some(vec![]),
+ session,
+ value: None,
+ }))
+ }
+ async fn new(session: NixSession, query: &str) -> Result<Self> {
+ let vid = session.0.lock().await.execute_assign(query).await?;
+ Ok(Self(Arc::new(ValueInner {
+ full_path: None,
+ session,
+ value: Some(vid),
+ })))
+ }
+ /// Get a top-level binding.
+ ///
+ /// In flake repl session, every output is exposed as top-level binding.
+ pub async fn binding(session: NixSession, field: &str) -> Result<Self> {
+ Self::root(session).select([Index::var(field)]).await
+ }
+ pub async fn select<'a>(&self, name: impl IntoIterator<Item = Index>) -> Result<Self> {
+ let mut used_fields = Vec::new();
+ let mut name = name.into_iter();
+
+ let mut full_path = self.0.full_path.clone();
+ let mut query = if let Some(id) = self.0.value {
+ format!("sess_field_{id}")
+ } else {
+ let first = name.next();
+ if let Some(Index::Var(i)) = first {
+ if let Some(full_path) = &mut full_path {
+ full_path.push(Index::Var(i.clone()));
+ }
+ i.clone()
+ } else {
+ panic!("first path item should be variable, got {first:?}")
+ }
+ };
+ for v in name {
+ if let Some(full_path) = &mut full_path {
+ full_path.push(v.clone());
+ }
+ match v {
+ Index::Var(_) => panic!("var item may only be first"),
+ Index::String(s) => {
+ let escaped =
+ nixlike::serialize(s).expect("strings are always serialized successfully");
+ query.push('.');
+ query.push_str(escaped.trim());
+ }
+ Index::Apply(a) => {
+ // In cases like `a {}.b` first `{}.b` will be evaluated, so `a {}` should be encased in `()`
+ query = format!("({query} {a})");
+ }
+ Index::Expr(e) => {
+ let index = Value::new(self.0.session.clone(), &e.out).await?;
+ used_fields.push(index.clone());
+ query.push('.');
+ let index = format!("${{sess_field_{}}}", index.0.value.expect("value"));
+ query.push_str(&index);
+ }
+ Index::ExprApply(e) => {
+ let index = Value::new(self.0.session.clone(), &e.out).await?;
+ used_fields.push(index.clone());
+ query.push(' ');
+ let index = format!("sess_field_{}", index.0.value.expect("value"));
+ query.push_str(&index);
+ query = format!("({query})");
+ }
+ Index::Pipe(v) => {
+ let index = Value::new(self.0.session.clone(), &v.out).await?;
+ used_fields.push(index.clone());
+ let index = format!("sess_field_{}", index.0.value.expect("value"));
+ query = format!("({index} {query})");
+ }
+ }
+ }
+
+ let vid = self
+ .0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_assign(&query)
+ .await
+ .map_err(|e| e.context(self.attribute()))?;
+ Ok(Self(Arc::new(ValueInner {
+ full_path,
+ session: self.0.session.clone(),
+ value: Some(vid),
+ })))
+ }
+ pub async fn as_json<V: DeserializeOwned>(&self) -> Result<V> {
+ let id = self.0.value.expect("can't serialize root field");
+ let query = format!("sess_field_{id}");
+ self.0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&query)
+ .await
+ .map_err(|e| e.context(self.attribute()))
+ }
+ #[allow(dead_code)]
+ pub async fn has_field(&self, name: &str) -> Result<bool> {
+ let id = self.0.value.expect("can't list root fields");
+ let key = nixlike::escape_string(name);
+ let query = format!("sess_field_{id} ? {key}");
+ self.0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&query)
+ .await
+ .map_err(|e| e.context(self.attribute()))
+ }
+ pub async fn list_fields(&self) -> Result<Vec<String>> {
+ let id = self.0.value.expect("can't list root fields");
+ let query = format!("builtins.attrNames sess_field_{id}");
+ self.0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&query)
+ .await
+ .map_err(|e| e.context(self.attribute()))
+ }
+ pub async fn type_of(&self) -> Result<String> {
+ let id = self.0.value.expect("can't list root fields");
+ let query = format!("builtins.typeOf sess_field_{id}");
+ self.0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_expression_to_json(&query)
+ .await
+ .map_err(|e| e.context(self.attribute()))
+ }
+ #[allow(dead_code)]
+ pub async fn import(&self) -> Result<Self> {
+ let import = Self::new(self.0.session.clone(), "import").await?;
+ Ok(nix_go!(self | import))
+ }
+ pub async fn build(&self) -> Result<HashMap<String, PathBuf>> {
+ let id = self.0.value.expect("can't use build on not-value");
+ let query = format!(":b sess_field_{id}");
+ let vid = self
+ .0
+ .session
+ .0
+ .lock()
+ .await
+ .execute_expression_raw(&query, &mut NixHandler::default())
+ .await?;
+ if vid.is_empty() {
+ return Err(Error::BuildFailed {
+ attribute: self.attribute(),
+ error: "build produced no output".to_owned(),
+ });
+ }
+ let Some(vid) = vid.strip_prefix("This derivation produced the following outputs:\n")
+ else {
+ return Err(Error::BuildFailed {
+ attribute: self.attribute(),
+ error: format!("failed to parse output: {vid}"),
+ });
+ };
+ let outputs = vid
+ .split('\n')
+ .filter(|v| !v.is_empty())
+ .map(|v| v.split_once(" -> ").expect("unexpected build output"))
+ .map(|(a, b)| (a.trim_start().to_owned(), PathBuf::from(b)))
+ .collect();
+ Ok(outputs)
+ }
+
+ fn attribute(&self) -> String {
+ if let Some(full_path) = &self.0.full_path {
+ PathDisplay(full_path).to_string()
+ } else {
+ "<root>".to_owned()
+ }
+ }
+
+ pub(crate) fn session(&self) -> NixSession {
+ self.0.session.clone()
+ }
+
+ pub(crate) fn session_field_id(&self) -> u32 {
+ self.0.value.expect("not root")
+ }
+}
+impl Drop for ValueInner {
+ fn drop(&mut self) {
+ if let Some(id) = self.value {
+ if let Ok(mut lock) = self.session.0.try_lock() {
+ lock.free_list.push(id)
+ }
+ // Leaked
+ }
+ }
+}
crates/nixlike/src/lib.rsdiffbeforeafterboth--- a/crates/nixlike/src/lib.rs
+++ b/crates/nixlike/src/lib.rs
@@ -1,5 +1,9 @@
//! Serialization/deserialization for nix subset usable for static configurations
-//! Serialized results from this library are readable by both this library and standard nix tools
+//!
+//! Serialized results from this library are readable by both this library and standard nix tools.
+//! Nix produced output should also be readable by this library, however, you can't write arbitrary nix
+//! expressions and expect it to work, only basic primitives are supported, and there is no
+//! variables/recursive records, interpolation, e.t.c.
use linked_hash_map::LinkedHashMap;
use peg::str::LineCol;
@@ -198,9 +202,15 @@
#[test]
fn parse_multiline() {
+ // First line is ignored, unless there is a significant characters.
assert_eq!(nixlike::multiline_string("''\n''").expect("parse"), "");
+ // Rest of the lines are processed normally.
assert_eq!(nixlike::multiline_string("''\n\n''").expect("parse"), "\n");
+ // Example with significant character on first line.
assert_eq!(nixlike::multiline_string("''t\n''").expect("parse"), "t\n");
+ // There might be nothing in multiline string block.
assert_eq!(nixlike::multiline_string("''''").expect("parse"), "");
+ // And there also might just be spaces, they are removed due to dedent, and output is empty because
+ // first line was also ignored due to missing significant characters.
assert_eq!(nixlike::multiline_string("'' ''").expect("parse"), "");
}
flake.lockdiffbeforeafterboth--- a/flake.lock
+++ b/flake.lock
@@ -7,11 +7,11 @@
]
},
"locked": {
- "lastModified": 1715274763,
- "narHash": "sha256-3Iv1PGHJn9sV3HO4FlOVaaztOxa9uGLfOmUWrH7v7+A=",
+ "lastModified": 1716569590,
+ "narHash": "sha256-5eDbq8TuXFGGO3mqJFzhUbt5zHVTf5zilQoyW5jnJwo=",
"owner": "ipetkov",
"repo": "crane",
- "rev": "27025ab71bdca30e7ed0a16c88fd74c5970fc7f5",
+ "rev": "109987da061a1bf452f435f1653c47511587d919",
"type": "github"
},
"original": {
@@ -40,11 +40,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1715619775,
- "narHash": "sha256-c1XVqTH9IeUukc4LcWLzHCSpMfo4Dj4K8t/kLV3c80c=",
+ "lastModified": 1716658583,
+ "narHash": "sha256-A93mYmlLvCz0YjQiQ5Tc3DpLrP6Brs+gAlK9nlnSOVg=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "0cb78770f66945bb3130f762aef05373e283f2b9",
+ "rev": "3e280884c0b0e8222ec6b05a99db01505964e1c3",
"type": "github"
},
"original": {
@@ -54,11 +54,28 @@
"type": "github"
}
},
+ "nixpkgs-stable-for-tests": {
+ "locked": {
+ "lastModified": 1716361217,
+ "narHash": "sha256-mzZDr00WUiUXVm1ujBVv6A0qRd8okaITyUp4ezYRgc4=",
+ "owner": "nixos",
+ "repo": "nixpkgs",
+ "rev": "46397778ef1f73414b03ed553a3368f0e7e33c2f",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nixos",
+ "ref": "nixos-23.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
+ "nixpkgs-stable-for-tests": "nixpkgs-stable-for-tests",
"rust-overlay": "rust-overlay"
}
},
@@ -72,11 +89,11 @@
]
},
"locked": {
- "lastModified": 1715566659,
- "narHash": "sha256-OpI0TnN+uE0vvxjPStlTzf5RTohIXVSMwrP9NEgMtaY=",
+ "lastModified": 1716603336,
+ "narHash": "sha256-81u/zd7V+XRTq88zwRLxw5GnwZyEiAvGA2BvAXUe864=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "6c465248316cd31502c82f81f1a3acf2d621b01c",
+ "rev": "4d0f1e4d5d65c23cdbb77e4b0d91940be7309bd4",
"type": "github"
},
"original": {
flake.nixdiffbeforeafterboth--- a/flake.nix
+++ b/flake.nix
@@ -3,6 +3,7 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/master";
+ nixpkgs-stable-for-tests.url = "github:nixos/nixpkgs/nixos-23.11";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs = {
@@ -21,6 +22,7 @@
rust-overlay,
flake-utils,
nixpkgs,
+ nixpkgs-stable-for-tests,
crane,
}:
with nixpkgs.lib;
@@ -37,11 +39,37 @@
rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLib = (crane.mkLib pkgs).overrideToolchain rust;
in {
- packages = import ./pkgs {
- inherit (pkgs) callPackage;
- inherit craneLib;
- };
- devShell = craneLib.devShell {
+ packages = let
+ packages = import ./pkgs {
+ inherit (pkgs) callPackage;
+ inherit craneLib;
+ };
+ in
+ packages // {default = packages.fleet;};
+
+ checks = let
+ packages = import ./pkgs {
+ inherit (pkgs) callPackage;
+ craneLib = crane.mkLib (import nixpkgs {inherit system;});
+ };
+ packages-with-nixpkgs-stable = import ./pkgs {
+ inherit (pkgs) callPackage;
+ craneLib = crane.mkLib (import nixpkgs-stable-for-tests {inherit system;});
+ };
+ prefixAttrs = prefix: attrs:
+ nixpkgs.lib.attrsets.mapAttrs' (name: value: {
+ name = "${prefix}${name}";
+ value = value.overrideAttrs (prev: {
+ pname = "${prefix}${prev.pname}";
+ });
+ })
+ attrs;
+ in
+ # `fleet` crate wants nightly rust, also little sense of supporting it on stable nixpkgs.
+ (prefixAttrs "nixpkgs-" (removeAttrs packages ["fleet"]))
+ // (prefixAttrs "nixpkgs-stable-" (removeAttrs packages-with-nixpkgs-stable ["fleet"]));
+
+ devShells.default = craneLib.devShell {
nativeBuildInputs = with pkgs; [
alejandra
lld
nixos/fleetPkgs.nixdiffbeforeafterboth--- a/nixos/fleetPkgs.nix
+++ /dev/null
@@ -1,24 +0,0 @@
-{...}: {
- nixpkgs.overlays = [
- # Not using craneLib here, because we don't want to have two different rust versions for some platforms.
- (final: prev: {
- fleet-install-secrets = prev.callPackage ({rustPlatform}:
- rustPlatform.buildRustPackage rec {
- pname = "fleet-install-secrets";
- name = "${pname}";
-
- src = ../.;
- strictDeps = true;
-
- buildAndTestSubdir = "cmds/install-secrets";
-
- cargoLock = {
- lockFile = ../Cargo.lock;
- outputHashes = {
- "alejandra-3.0.0" = "sha256-q2oTMen8E1YUbNyU4chPOj728/YR0RzdpN+bNjZX2QU=";
- };
- };
- }) {};
- })
- ];
-}
pkgs/default.nixdiffbeforeafterboth--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,9 +1,7 @@
{
callPackage,
craneLib,
-}: rec {
- default = fleet;
-
+}: {
fleet-install-secrets = callPackage ./fleet-install-secrets.nix {inherit craneLib;};
fleet = callPackage ./fleet.nix {inherit craneLib;};
}
pkgs/generator-helper.nixdiffbeforeafterboth--- /dev/null
+++ b/pkgs/generator-helper.nix
@@ -0,0 +1,14 @@
+
+{craneLib}:
+craneLib.buildPackage rec {
+ pname = "fleet-generator-helper";
+
+ src = craneLib.cleanCargoSource (craneLib.path ../.);
+ strictDeps = true;
+
+ cargoExtraArgs = "--locked -p ${pname}";
+
+ postInstall = ''
+ mv bin/${pname} bin/genhelper
+ '';
+}
rust-toolchain.tomldiffbeforeafterboth--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
[toolchain]
-channel = "nightly-2024-02-10"
+channel = "nightly-2024-05-01"
components = ["rustfmt", "clippy", "rust-analyzer", "rust-src"]