--- a/Cargo.lock +++ b/Cargo.lock @@ -1438,6 +1438,7 @@ "itertools", "nixlike", "r2d2", + "regex", "serde", "serde_json", "thiserror", @@ -1866,9 +1867,9 @@ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", --- a/cmds/fleet/src/cmds/mod.rs +++ b/cmds/fleet/src/cmds/mod.rs @@ -2,3 +2,4 @@ pub mod complete; pub mod info; pub mod secrets; +pub mod tf; --- /dev/null +++ b/cmds/fleet/src/cmds/tf.rs @@ -0,0 +1,23 @@ +use anyhow::Result; +use clap::Parser; +use nix_eval::nix_go_json; +use serde_json::Value; +use tokio::fs::write; +use tracing::info; + +use crate::host::Config; + +#[derive(Parser)] +pub struct Tf; +impl Tf { + pub async fn run(&self, config: &Config) -> Result<()> { + let system = &config.local_system; + let config = &config.config_field; + let data: Value = nix_go_json!(config.tf({ system }).config); + let str = serde_json::to_string_pretty(&data)?; + + write("fleet.tf.json", str.as_bytes()).await?; + + Ok(()) + } +} --- a/cmds/fleet/src/main.rs +++ b/cmds/fleet/src/main.rs @@ -19,6 +19,7 @@ complete::Complete, info::Info, secrets::Secret, + tf::Tf, }; use futures::{future::LocalBoxFuture, stream::FuturesUnordered, TryStreamExt}; use host::{Config, FleetOpts}; @@ -86,6 +87,8 @@ /// Command completions #[clap(hide(true))] Complete(Complete), + /// Compile and evaluate terranix configuration + Tf(Tf), } #[derive(Parser)] @@ -104,6 +107,7 @@ Opts::Secret(s) => s.run(config).await?, Opts::Info(i) => i.run(config).await?, Opts::Prefetch(p) => p.run(config).await?, + Opts::Tf(t) => t.run(config).await?, // TODO: actually parse commands before starting the async runtime Opts::Complete(c) => { tokio::task::spawn_blocking(move || c.run(RootOpts::command())).await? --- a/crates/nix-eval/Cargo.toml +++ b/crates/nix-eval/Cargo.toml @@ -11,6 +11,7 @@ itertools = "0.13.0" nixlike.workspace = true r2d2 = "0.8.10" +regex = "1.10.6" serde = { workspace = true, features = ["derive"] } serde_json.workspace = true thiserror = "1.0.61" --- a/crates/nix-eval/src/session.rs +++ b/crates/nix-eval/src/session.rs @@ -12,7 +12,7 @@ sync::{mpsc, oneshot, Mutex}, }; use tokio_util::codec::{FramedRead, LinesCodec}; -use tracing::{debug, error, warn, Level}; +use tracing::{debug, error, info, warn, Level}; #[derive(Error, Debug)] pub enum Error { @@ -327,8 +327,16 @@ n.parse::().map_err(Error::Int) } async fn execute_expression_string(&mut self, expr: impl AsRef<[u8]>) -> Result { + // builtins.toJSON escapes some thing in incorrect way, e.g escaped "$" in "\${" is being outputed as "\$", + // while this escape should be removed as it is intended for nix itself, not for json output. + // + // This regex only allows \$ in the beginning of the string, it is easier to implement correctly. + // TODO: Add peg parser for nix-produced JSON?.. + let regex = regex::Regex::new(r#"(?[: {,\[]\\")\\\$"#).expect("fixup json"); + let num = self.string_wrapping.clone(); let n = self.execute_expression_wrapping(expr, &num).await?; + let n = regex.replace_all(&n, "$prefix$$"); let str: String = serde_json::from_str(&n)?; Ok(str) } @@ -339,6 +347,7 @@ let mut fexpr = b"builtins.toJSON (".to_vec(); fexpr.extend_from_slice(expr.as_ref()); fexpr.push(b')'); + let s = String::from_utf8_lossy(expr.as_ref()); let v = self.execute_expression_string(fexpr).await?; Ok(serde_json::from_str(&v)?) } --- a/flake.nix +++ b/flake.nix @@ -37,6 +37,8 @@ }; flakeModule = flakeModules.default; + fleetModules.tf = ./modules/extras/tf.nix; + # To be used with https://github.com/NixOS/nix/pull/8892 schemas = let inherit (inputs.nixpkgs.lib) mapAttrs; --- a/lib/flakePart.nix +++ b/lib/flakePart.nix @@ -2,6 +2,7 @@ fleetLib, lib, config, + inputs ? {}, ... }: let inherit (lib.options) mkOption; @@ -58,8 +59,11 @@ }; } ]; - specialArgs.fleetLib = import ../lib { - inherit (bootstrapNixpkgs) lib; + specialArgs = { + fleetLib = import ../lib { + inherit (bootstrapNixpkgs) lib; + }; + inputs = inputs; }; }; in --- /dev/null +++ b/modules/extras/tf.nix @@ -0,0 +1,26 @@ +{ + config, + lib, + inputs, + ... +}: let + inherit (lib) mkOption; + inherit (lib.types) deferredModule; +in { + options.tf = mkOption { + type = deferredModule; + apply = module: system: + inputs.terranix.lib.terranixConfigurationAst { + inherit system; + pkgs = config.nixpkgs.buildUsing.legacyPackages.${system}; + modules = [module]; + }; + }; + config.tf.output.fleet = { + value = { + managed = true; + }; + # Just to avoid printing this attribute on every apply. + sensitive = true; + }; +}