Skip to main content

consortium_nix/
eval.rs

1//! Nix evaluation — resolve toplevel store paths and detect changes.
2
3use std::collections::HashMap;
4use std::process::Command;
5
6use crate::config::{DeployAction, DeploymentPlan, DeploymentTarget, FleetConfig};
7use crate::error::{NixError, Result};
8
9/// Evaluate which hosts need deployment by comparing desired vs current state.
10pub fn evaluate(
11    config: &FleetConfig,
12    target_nodes: &[String],
13    action: DeployAction,
14    max_parallel: usize,
15) -> Result<DeploymentPlan> {
16    let mut plan = DeploymentPlan::new(action, max_parallel);
17
18    for name in target_nodes {
19        let node = config
20            .nodes
21            .get(name)
22            .ok_or_else(|| NixError::General(format!("unknown node: {}", name)))?;
23
24        // Get the expected toplevel path via nix eval
25        let toplevel_path = eval_toplevel(&config.flake_uri, name)?;
26
27        plan.targets.push(DeploymentTarget {
28            node: node.clone(),
29            toplevel_path,
30            current_system: None, // filled in during deploy if needed
31            needs_build: true,
32            needs_copy: true,
33        });
34    }
35
36    Ok(plan)
37}
38
39/// Evaluate the toplevel store path for a single host via `nix eval`.
40pub fn eval_toplevel(flake_uri: &str, hostname: &str) -> Result<String> {
41    let attr = format!(
42        "{}#nixosConfigurations.{}.config.system.build.toplevel.outPath",
43        flake_uri, hostname
44    );
45
46    let output = Command::new("nix")
47        .args(["eval", "--raw", &attr])
48        .output()
49        .map_err(|e| NixError::EvalFailed {
50            host: hostname.to_string(),
51            message: format!("failed to run nix eval: {}", e),
52        })?;
53
54    if !output.status.success() {
55        let stderr = String::from_utf8_lossy(&output.stderr);
56        return Err(NixError::EvalFailed {
57            host: hostname.to_string(),
58            message: stderr.to_string(),
59        });
60    }
61
62    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
63    if path.is_empty() {
64        return Err(NixError::EvalFailed {
65            host: hostname.to_string(),
66            message: "nix eval returned empty path".to_string(),
67        });
68    }
69
70    Ok(path)
71}
72
73/// Query the current system generation on a remote host.
74pub fn query_current_system(host: &str, user: &str) -> Result<Option<String>> {
75    let output = Command::new("ssh")
76        .args([
77            "-oStrictHostKeyChecking=no",
78            "-oPasswordAuthentication=no",
79            "-oConnectTimeout=10",
80            "-l",
81            user,
82            host,
83            "readlink",
84            "/run/current-system",
85        ])
86        .output()
87        .map_err(|e| NixError::SshFailed {
88            host: host.to_string(),
89            message: format!("failed to query current system: {}", e),
90        })?;
91
92    if !output.status.success() {
93        return Ok(None);
94    }
95
96    let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
97    if path.is_empty() {
98        Ok(None)
99    } else {
100        Ok(Some(path))
101    }
102}
103
104/// Evaluate all hosts and return a map of hostname -> toplevel path.
105pub fn eval_all(flake_uri: &str, hostnames: &[String]) -> Result<HashMap<String, String>> {
106    let mut results = HashMap::new();
107    // TODO: parallelize with consortium's Task/Worker infrastructure
108    for hostname in hostnames {
109        let path = eval_toplevel(flake_uri, hostname)?;
110        results.insert(hostname.clone(), path);
111    }
112    Ok(results)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_eval_toplevel_attr_format() {
121        // Just verify the attribute path format is correct
122        let flake = ".";
123        let host = "contra";
124        let attr = format!(
125            "{}#nixosConfigurations.{}.config.system.build.toplevel.outPath",
126            flake, host
127        );
128        assert_eq!(
129            attr,
130            ".#nixosConfigurations.contra.config.system.build.toplevel.outPath"
131        );
132    }
133}