use anyhow::{bail, Context, Result};
use std::collections::HashMap;
use std::path::PathBuf;
use super::context::{active_endpoint, endpoint_to_server};
use super::output::{render_value, OutputFormat};
use tatara_engine::nix_eval::evaluator::NixEvaluator;
pub async fn run(
flake_ref: &str,
set_values: &[(String, String)],
name: Option<&str>,
dry_run: bool,
endpoint: Option<&str>,
output: OutputFormat,
) -> Result<()> {
let server = endpoint_to_server(&active_endpoint(endpoint));
println!("Resolving {}...", flake_ref);
let mut spec = resolve_flake_ref(flake_ref).await?;
if !set_values.is_empty() {
apply_overrides(&mut spec, set_values)?;
}
if let Some(n) = name {
spec["id"] = serde_json::Value::String(n.to_string());
}
if dry_run {
println!("--- Dry Run ---");
let job_id = spec["id"].as_str().unwrap_or("unknown");
let client = reqwest::Client::new();
let url = format!("http://{}/api/v1/jobs/{}", server, job_id);
let existing = client.get(&url).send().await;
if let Ok(resp) = existing {
if resp.status().is_success() {
let current: serde_json::Value = resp.json().await?;
println!("Existing job found. Diff:");
let current_str = serde_json::to_string_pretty(¤t["job"])?;
let new_str = serde_json::to_string_pretty(&spec)?;
for diff in diff::lines(¤t_str, &new_str) {
match diff {
diff::Result::Left(l) => println!("-{}", l),
diff::Result::Right(r) => println!("+{}", r),
diff::Result::Both(b, _) => println!(" {}", b),
}
}
} else {
println!("New job (no existing job with id '{}'):", job_id);
println!("{}", serde_json::to_string_pretty(&spec)?);
}
}
println!("\nResources required:");
if let Some(groups) = spec["groups"].as_array() {
for group in groups {
let count = group["count"].as_u64().unwrap_or(1);
let cpu = group["resources"]["cpu_mhz"].as_u64().unwrap_or(0);
let mem = group["resources"]["memory_mb"].as_u64().unwrap_or(0);
println!(
" Group '{}': {} instances x {}MHz CPU, {}MB memory",
group["name"].as_str().unwrap_or("?"),
count,
cpu,
mem
);
}
}
return Ok(());
}
let client = reqwest::Client::new();
let url = format!("http://{}/api/v1/jobs", server);
let resp = client
.post(&url)
.json(&spec)
.send()
.await
.context("Failed to connect to tatara server")?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
bail!("Server returned {}: {}", status, body);
}
let job: serde_json::Value = resp.json().await?;
let job_id = job["id"].as_str().unwrap_or("unknown");
let release_body = serde_json::json!({
"name": name.unwrap_or(job_id),
"flake_ref": flake_ref,
"job_id": job_id,
});
let release_url = format!("http://{}/api/v1/releases", server);
let _ = client.post(&release_url).json(&release_body).send().await;
match output {
OutputFormat::Json | OutputFormat::Yaml => {
println!("{}", render_value(&job, output)?);
}
_ => {
println!("Deployed: {} (version {})", job_id, job["version"]);
println!("Flake: {}", flake_ref);
println!("Status: {}", job["status"]);
}
}
println!("\nWaiting for allocations...");
let mut settled = false;
for _ in 0..15 {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let alloc_url = format!("http://{}/api/v1/jobs/{}", server, job_id);
if let Ok(resp) = client.get(&alloc_url).send().await {
if resp.status().is_success() {
let detail: serde_json::Value = resp.json().await?;
if let Some(allocs) = detail["allocations"].as_array() {
let running = allocs
.iter()
.filter(|a| a["state"].as_str() == Some("running"))
.count();
let total = allocs.len();
print!("\r Allocations: {}/{} running", running, total);
if running == total && total > 0 {
println!(" - deployment complete!");
settled = true;
break;
}
}
}
}
}
if !settled {
println!(
"\n Deployment still in progress. Use 'tatara job get {}' to check.",
job_id
);
}
Ok(())
}
async fn resolve_flake_ref(flake_ref: &str) -> Result<serde_json::Value> {
let path = PathBuf::from(flake_ref);
if path.exists() {
if flake_ref.ends_with(".nix") {
let spec = NixEvaluator::eval_file(&path).await?;
return Ok(serde_json::to_value(spec)?);
}
if flake_ref.ends_with(".json") {
let content = tokio::fs::read_to_string(&path).await?;
return Ok(serde_json::from_str(&content)?);
}
if flake_ref.ends_with(".yaml") || flake_ref.ends_with(".yml") {
let content = tokio::fs::read_to_string(&path).await?;
let value: serde_json::Value = serde_yaml::from_str(&content)?;
return Ok(value);
}
}
let output = tokio::process::Command::new("nix")
.args(["eval", "--json", flake_ref])
.output()
.await
.context("Failed to run nix eval")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("nix eval failed for '{}': {}", flake_ref, stderr);
}
let json = String::from_utf8(output.stdout)?;
let spec: serde_json::Value = serde_json::from_str(&json)?;
Ok(spec)
}
fn apply_overrides(spec: &mut serde_json::Value, overrides: &[(String, String)]) -> Result<()> {
for (key, value) in overrides {
let parts: Vec<&str> = key.split('.').collect();
let mut target = &mut *spec;
for (i, part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
let json_value = serde_json::from_str(value)
.unwrap_or_else(|_| serde_json::Value::String(value.clone()));
target[part] = json_value;
} else if let Ok(idx) = part.parse::<usize>() {
target = &mut target[idx];
} else {
target = &mut target[*part];
}
}
}
Ok(())
}