use crate::config::CliConfig;
use crate::workspace_config::WorkspaceConfig;
use comfy_table::{presets::NOTHING, Table, TableComponent};
use std::path::PathBuf;
use std::time::Instant;
fn load_workspace() -> eyre::Result<(WorkspaceConfig, PathBuf)> {
let (config, root) = WorkspaceConfig::find_and_load()?;
config.validate_paths(&root)?;
Ok((config, root))
}
fn make_table(headers: Vec<&str>) -> Table {
let mut table = Table::new();
table.load_preset(NOTHING);
table.set_style(TableComponent::HeaderLines, '-');
table.set_style(TableComponent::MiddleHeaderIntersections, ' ');
table.set_header(headers);
table
}
pub async fn deploy(skip: &[String], env: Option<&str>) -> eyre::Result<()> {
let (ws, root) = load_workspace()?;
println!("Workspace: {}", ws.workspace.name);
if let Some(name) = env {
println!("Environment: {}", name);
}
let deploy_services = ws.deploy_list(env);
let deploy_services: Vec<_> = deploy_services
.into_iter()
.filter(|s| !skip.contains(&s.name))
.collect();
if deploy_services.is_empty() {
println!("Nothing to deploy.");
return Ok(());
}
if !skip.is_empty() {
println!("Skipping: {}", skip.join(", "));
}
let original_dir = std::env::current_dir()?;
let mut results: Vec<(String, Result<(), String>)> = Vec::new();
for svc in &deploy_services {
let service_dir = root.join(&svc.path);
println!("\nDeploying '{}'...", svc.name);
std::env::set_current_dir(&service_dir)?;
match super::deploy::run(false, None, env).await {
Ok(_) => results.push((svc.name.clone(), Ok(()))),
Err(e) => {
let msg = format!("{}", e);
eprintln!(" Failed: {}", msg);
results.push((svc.name.clone(), Err(msg)));
}
}
}
std::env::set_current_dir(&original_dir)?;
println!();
let mut table = make_table(vec!["SERVICE", "STATUS"]);
let mut failures = 0;
let deployed: Vec<String> = results
.iter()
.filter(|(_, r)| r.is_ok())
.map(|(n, _)| n.clone())
.collect();
for (name, result) in &results {
let status = match result {
Ok(_) => "deployed",
Err(_) => {
failures += 1;
"FAILED"
}
};
table.add_row(vec![name.as_str(), status]);
}
println!("{table}");
let seed_services = ws.seed_list(env);
let seed_services: Vec<_> = seed_services
.into_iter()
.filter(|s| deployed.contains(&s.name))
.collect();
if !seed_services.is_empty() {
println!();
run_seed_phase(&root, &seed_services, env).await;
}
if failures > 0 {
eyre::bail!("{} service(s) failed to deploy", failures);
}
Ok(())
}
async fn run_seed_phase(
root: &std::path::Path,
seed_services: &[&crate::workspace_config::WorkspaceService],
env: Option<&str>,
) {
println!("Seeding...");
let config = match CliConfig::load_with_env(env) {
Ok(c) => c,
Err(e) => {
eprintln!("Failed to load config for seeding: {}", e);
return;
}
};
for svc in seed_services {
let seed_path = match &svc.seed {
Some(p) => root.join(p),
None => continue,
};
let service_id = match find_service_id(&config, &svc.name).await {
Ok(id) => id,
Err(e) => {
eprintln!(" {} — failed to resolve: {}", svc.name, e);
continue;
}
};
match run_seed(&config, &service_id, &seed_path).await {
Ok(rows) => println!(" {} — {} rows seeded", svc.name, rows),
Err(e) => eprintln!(" {} — seed failed: {}", svc.name, e),
}
}
}
async fn find_service_id(config: &CliConfig, service_name: &str) -> eyre::Result<String> {
let client = config.http_client();
let resp = config
.auth_request(
&client,
reqwest::Method::GET,
&format!("{}/api/services", config.api_url),
)
.send()
.await?;
let body: serde_json::Value = resp.json().await?;
body["services"]
.as_array()
.and_then(|arr| {
arr.iter()
.find(|s| s["name"].as_str() == Some(service_name))
.and_then(|s| s["id"].as_str())
})
.map(String::from)
.ok_or_else(|| eyre::eyre!("Service '{}' not found", service_name))
}
async fn run_seed(
config: &CliConfig,
service_id: &str,
seed_path: &std::path::Path,
) -> eyre::Result<u64> {
let data = std::fs::read_to_string(seed_path)?;
let mut payload: serde_json::Value = serde_json::from_str(&data)?;
if payload.get("tables").is_none() {
payload = serde_json::json!({ "tables": payload });
}
let client = config.http_client();
let resp = config
.auth_request(
&client,
reqwest::Method::POST,
&format!("{}/api/services/{}/seed", config.api_url, service_id),
)
.json(&payload)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
eyre::bail!("Seed failed ({}): {}", status, body);
}
let body: serde_json::Value = resp.json().await?;
Ok(body["total_rows"].as_u64().unwrap_or(0))
}
pub async fn test(skip: &[String], run_all: bool) -> eyre::Result<()> {
let (ws, root) = load_workspace()?;
println!("Workspace: {}", ws.workspace.name);
println!();
let services: Vec<_> = ws
.services
.iter()
.filter(|s| !skip.contains(&s.name))
.collect();
if services.is_empty() {
println!("No services to test.");
return Ok(());
}
let original_dir = std::env::current_dir()?;
let mut results: Vec<(String, bool, String)> = Vec::new();
for svc in &services {
let service_dir = root.join(&svc.path);
if !service_dir.join("Cargo.toml").exists() {
results.push((svc.name.clone(), true, "skipped".to_string()));
continue;
}
std::env::set_current_dir(&service_dir)?;
print!("Testing {}... ", svc.name);
let start = Instant::now();
let mut args = vec!["test"];
if !run_all {
args.push("--quiet");
}
let output = std::process::Command::new("cargo").args(&args).output();
let elapsed = start.elapsed();
let time_str = format!("{:.1}s", elapsed.as_secs_f64());
match output {
Ok(o) if o.status.success() => {
println!("passed ({})", time_str);
results.push((svc.name.clone(), true, time_str));
}
Ok(o) => {
println!("FAILED ({})", time_str);
if run_all {
let stderr = String::from_utf8_lossy(&o.stderr);
let stdout = String::from_utf8_lossy(&o.stdout);
if !stdout.is_empty() {
eprintln!("{}", stdout);
}
if !stderr.is_empty() {
eprintln!("{}", stderr);
}
}
results.push((svc.name.clone(), false, time_str));
}
Err(e) => {
println!("ERROR");
results.push((svc.name.clone(), false, format!("error: {}", e)));
}
}
}
std::env::set_current_dir(&original_dir)?;
println!();
let mut table = make_table(vec!["SERVICE", "RESULT", "TIME"]);
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
for (name, success, time) in &results {
if time == "skipped" {
skipped += 1;
table.add_row(vec![name.as_str(), "skipped", "-"]);
} else if *success {
passed += 1;
table.add_row(vec![name.as_str(), "passed", time.as_str()]);
} else {
failed += 1;
table.add_row(vec![name.as_str(), "FAILED", time.as_str()]);
}
}
println!("{table}");
println!();
let mut summary = format!("{} passed", passed);
if failed > 0 {
summary.push_str(&format!(", {} failed", failed));
}
if skipped > 0 {
summary.push_str(&format!(", {} skipped", skipped));
}
println!("{}", summary);
if failed > 0 {
eyre::bail!("{} service(s) failed tests", failed);
}
Ok(())
}
pub async fn status(env: Option<&str>) -> eyre::Result<()> {
let (ws, _root) = load_workspace()?;
println!("Workspace: {}", ws.workspace.name);
let config = CliConfig::load_with_env(env)?;
if let Some(ref name) = config.env_name {
println!("Environment: {}", name);
}
println!();
let client = config.http_client();
let resp = config
.auth_request(
&client,
reqwest::Method::GET,
&format!("{}/api/services", config.api_url),
)
.send()
.await?;
let body: serde_json::Value = resp.json().await?;
let api_services = body["services"].as_array();
let mut table = make_table(vec!["SERVICE", "VERSION", "STATUS", "MODE"]);
for svc in &ws.services {
let api_svc =
api_services.and_then(|arr| arr.iter().find(|s| s["name"].as_str() == Some(&svc.name)));
match api_svc {
Some(s) => {
let version = format!("v{}", s["current_version"]);
let status = s["status"].as_str().unwrap_or("?");
let mode = s["mode"].as_str().unwrap_or("?");
table.add_row(vec![svc.name.as_str(), &version, status, mode]);
}
None => {
table.add_row(vec![svc.name.as_str(), "-", "not deployed", "-"]);
}
}
}
println!("{table}");
println!("\n{} service(s)", ws.services.len());
Ok(())
}
pub async fn seed(env: Option<&str>) -> eyre::Result<()> {
let (ws, root) = load_workspace()?;
println!("Workspace: {}", ws.workspace.name);
let seed_services = ws.seed_list(env);
if seed_services.is_empty() {
println!("No services configured for seeding in this environment.");
return Ok(());
}
run_seed_phase(&root, &seed_services, env).await;
Ok(())
}