Directly serialize Nix expressions as quoted strings

This commit is contained in:
Zhaofeng Li 2022-08-16 20:15:43 -06:00
parent 271d9ae576
commit 092e5848ab
3 changed files with 80 additions and 57 deletions

73
src/nix/expression.rs Normal file
View file

@ -0,0 +1,73 @@
//! Nix expression serializer.
use serde::Serialize;
/// A Nix expression.
pub trait NixExpression: Send + Sync {
/// Returns the full Nix expression to be evaluated.
fn expression(&self) -> String;
/// Returns whether this expression requires the use of flakes.
fn requires_flakes(&self) -> bool {
false
}
}
/// A serialized Nix expression.
pub struct SerializedNixExpression(String);
impl NixExpression for String {
fn expression(&self) -> String {
self.clone()
}
}
impl SerializedNixExpression {
pub fn new<T>(data: T) -> Self
where
T: Serialize,
{
let json = serde_json::to_string(&data).expect("Could not serialize data");
let quoted = nix_quote(&json);
Self(quoted)
}
}
impl NixExpression for SerializedNixExpression {
fn expression(&self) -> String {
format!("(builtins.fromJSON {})", &self.0)
}
}
/// Turns a string into a quoted Nix string expression.
fn nix_quote(s: &str) -> String {
let inner = s
.replace('\\', r#"\\"#)
.replace('"', r#"\""#)
.replace("${", r#"\${"#);
format!("\"{}\"", inner)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_nix_quote() {
let cases = [
(r#"["a", "b"]"#, r#""[\"a\", \"b\"]""#),
(
r#"["\"a\"", "\"b\""]"#,
r#""[\"\\\"a\\\"\", \"\\\"b\\\"\"]""#,
),
(r#"${dontExpandMe}"#, r#""\${dontExpandMe}""#),
(r#"\${dontExpandMe}"#, r#""\\\${dontExpandMe}""#),
];
for (orig, quoted) in cases {
assert_eq!(quoted, nix_quote(orig));
}
}
}

View file

@ -5,11 +5,8 @@ mod tests;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::AsRef; use std::convert::AsRef;
use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use serde::Serialize;
use tempfile::{NamedTempFile, TempPath};
use tokio::process::Command; use tokio::process::Command;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
use validator::Validate; use validator::Validate;
@ -17,7 +14,7 @@ use validator::Validate;
use super::deployment::TargetNode; use super::deployment::TargetNode;
use super::{ use super::{
Flake, MetaConfig, NixExpression, NixOptions, NodeConfig, NodeFilter, NodeName, Flake, MetaConfig, NixExpression, NixOptions, NodeConfig, NodeFilter, NodeName,
ProfileDerivation, StorePath, ProfileDerivation, SerializedNixExpression, StorePath,
}; };
use crate::error::ColmenaResult; use crate::error::ColmenaResult;
use crate::job::JobHandle; use crate::job::JobHandle;
@ -60,15 +57,6 @@ struct NixInstantiate<'hive> {
expression: String, expression: String,
} }
/// A serialized Nix expression.
///
/// Very hacky so should be avoided as much as possible. But I suppose it's
/// more robust than attempting to generate Nix expressions directly or
/// escaping a JSON string to strip off Nix interpolation.
struct SerializedNixExpression {
json_file: TempPath,
}
/// An expression to evaluate the system profiles of selected nodes. /// An expression to evaluate the system profiles of selected nodes.
struct EvalSelectedExpression<'hive> { struct EvalSelectedExpression<'hive> {
hive: &'hive Hive, hive: &'hive Hive,
@ -290,7 +278,7 @@ impl Hive {
&self, &self,
nodes: &[NodeName], nodes: &[NodeName],
) -> ColmenaResult<HashMap<NodeName, NodeConfig>> { ) -> ColmenaResult<HashMap<NodeName, NodeConfig>> {
let nodes_expr = SerializedNixExpression::new(nodes)?; let nodes_expr = SerializedNixExpression::new(nodes);
let configs: HashMap<NodeName, NodeConfig> = self let configs: HashMap<NodeName, NodeConfig> = self
.nix_instantiate(&format!( .nix_instantiate(&format!(
@ -322,7 +310,7 @@ impl Hive {
nodes: &[NodeName], nodes: &[NodeName],
job: Option<JobHandle>, job: Option<JobHandle>,
) -> ColmenaResult<HashMap<NodeName, ProfileDerivation>> { ) -> ColmenaResult<HashMap<NodeName, ProfileDerivation>> {
let nodes_expr = SerializedNixExpression::new(nodes)?; let nodes_expr = SerializedNixExpression::new(nodes);
let expr = format!("hive.evalSelectedDrvPaths {}", nodes_expr.expression()); let expr = format!("hive.evalSelectedDrvPaths {}", nodes_expr.expression());
@ -344,7 +332,7 @@ impl Hive {
/// Returns the expression to evaluate selected nodes. /// Returns the expression to evaluate selected nodes.
pub fn eval_selected_expr(&self, nodes: &[NodeName]) -> ColmenaResult<impl NixExpression + '_> { pub fn eval_selected_expr(&self, nodes: &[NodeName]) -> ColmenaResult<impl NixExpression + '_> {
let nodes_expr = SerializedNixExpression::new(nodes)?; let nodes_expr = SerializedNixExpression::new(nodes);
Ok(EvalSelectedExpression { Ok(EvalSelectedExpression {
hive: self, hive: self,
@ -446,30 +434,6 @@ impl<'hive> NixInstantiate<'hive> {
} }
} }
impl SerializedNixExpression {
pub fn new<T>(data: T) -> ColmenaResult<Self>
where
T: Serialize,
{
let mut tmp = NamedTempFile::new()?;
let json = serde_json::to_vec(&data).expect("Could not serialize data");
tmp.write_all(&json)?;
Ok(Self {
json_file: tmp.into_temp_path(),
})
}
}
impl NixExpression for SerializedNixExpression {
fn expression(&self) -> String {
format!(
"(builtins.fromJSON (builtins.readFile {}))",
self.json_file.to_str().unwrap()
)
}
}
impl<'hive> NixExpression for EvalSelectedExpression<'hive> { impl<'hive> NixExpression for EvalSelectedExpression<'hive> {
fn expression(&self) -> String { fn expression(&self) -> String {
format!( format!(

View file

@ -39,6 +39,9 @@ pub use node_filter::NodeFilter;
pub mod evaluator; pub mod evaluator;
pub mod expression;
pub use expression::{NixExpression, SerializedNixExpression};
/// Path to the main system profile. /// Path to the main system profile.
pub const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system"; pub const SYSTEM_PROFILE: &str = "/nix/var/nix/profiles/system";
@ -104,17 +107,6 @@ pub struct NixOptions {
builders: Option<String>, builders: Option<String>,
} }
/// A Nix expression.
pub trait NixExpression: Send + Sync {
/// Returns the full Nix expression to be evaluated.
fn expression(&self) -> String;
/// Returns whether this expression requires the use of flakes.
fn requires_flakes(&self) -> bool {
false
}
}
impl NodeName { impl NodeName {
/// Returns the string. /// Returns the string.
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
@ -218,12 +210,6 @@ impl NixOptions {
} }
} }
impl NixExpression for String {
fn expression(&self) -> String {
self.clone()
}
}
fn validate_keys(keys: &HashMap<String, Key>) -> Result<(), ValidationErrorType> { fn validate_keys(keys: &HashMap<String, Key>) -> Result<(), ValidationErrorType> {
// Bad secret names: // Bad secret names:
// - /etc/passwd // - /etc/passwd