Skip to main content

dstack/
cmd_deploy.rs

1use crate::config::Config;
2use std::process::Command;
3
4pub fn deploy(cfg: &Config, service: &str) -> anyhow::Result<()> {
5    let target = cfg
6        .deploy
7        .get(service)
8        .ok_or_else(|| {
9            anyhow::anyhow!(
10                "No deploy target '{}'. Available: {:?}",
11                service,
12                cfg.deploy.keys().collect::<Vec<_>>()
13            )
14        })?;
15
16    eprintln!("=== Deploying {} ===", service);
17
18    // 0. Disk check
19    eprintln!("[0/4] Checking disk space...");
20    let df = cmd_output("df -h /opt | tail -1")?;
21    let use_pct = parse_disk_usage(&df);
22    if use_pct >= 90 {
23        anyhow::bail!(
24            "Disk usage at {}%. Aborting deploy — clean target dirs first.\n  {}",
25            use_pct,
26            df.trim()
27        );
28    }
29    eprintln!("  Disk: {}% used", use_pct);
30
31    // 1. Backup current binary
32    let binary_name = &target.service;
33    let backup_path = format!("/tmp/dstack-rollback-{}", binary_name);
34    let binary_search = cmd_output(&format!(
35        "which {} 2>/dev/null || find {}/{} -name {} -path '*/release/*' 2>/dev/null | head -1",
36        binary_name, cfg.repos.root, service, binary_name
37    ))?;
38    let binary_path = binary_search.trim().lines().next().unwrap_or("");
39    if !binary_path.is_empty() && std::path::Path::new(binary_path).exists() {
40        eprintln!("[1/4] Backing up {} → {}", binary_path, backup_path);
41        let _ = std::fs::copy(binary_path, &backup_path);
42    } else {
43        eprintln!("[1/4] No existing binary found, skipping backup");
44    }
45
46    // 2. Build
47    eprintln!("[2/4] Building...");
48    run_cmd(&target.build, "Build")?;
49
50    // 3. Restart service
51    eprintln!("[3/4] Restarting {}...", target.service);
52    run_cmd(
53        &format!("sudo systemctl restart {}", target.service),
54        "Restart",
55    )?;
56
57    // 4. Smoke test
58    if let Some(ref smoke) = target.smoke {
59        eprintln!("[4/4] Smoke test...");
60        std::thread::sleep(std::time::Duration::from_secs(2));
61        match run_cmd(smoke, "Smoke test") {
62            Ok(_) => eprintln!("  Smoke test passed."),
63            Err(e) => {
64                eprintln!("  WARNING: Smoke test failed: {}", e);
65                if std::path::Path::new(&backup_path).exists() {
66                    eprintln!(
67                        "  Rollback available: dstack deploy {} --rollback",
68                        service
69                    );
70                }
71            }
72        }
73    } else {
74        eprintln!("[4/4] No smoke test configured, skipping.");
75    }
76
77    eprintln!("=== {} deployed ===", service);
78    Ok(())
79}
80
81pub fn rollback(cfg: &Config, service: &str) -> anyhow::Result<()> {
82    let target = cfg
83        .deploy
84        .get(service)
85        .ok_or_else(|| anyhow::anyhow!("No deploy target '{}'", service))?;
86
87    let backup_path = format!("/tmp/dstack-rollback-{}", target.service);
88    if !std::path::Path::new(&backup_path).exists() {
89        anyhow::bail!("No rollback binary found at {}", backup_path);
90    }
91
92    // Find where the current binary lives
93    let binary_search = cmd_output(&format!(
94        "find {}/{} -name {} -path '*/release/*' 2>/dev/null | head -1",
95        cfg.repos.root, service, target.service
96    ))?;
97    let binary_path = binary_search.trim();
98    if binary_path.is_empty() {
99        anyhow::bail!("Cannot find current binary path for {}", service);
100    }
101
102    eprintln!("=== Rolling back {} ===", service);
103    eprintln!("[1/2] Restoring {} → {}", backup_path, binary_path);
104    std::fs::copy(&backup_path, binary_path)?;
105
106    eprintln!("[2/2] Restarting {}...", target.service);
107    run_cmd(
108        &format!("sudo systemctl restart {}", target.service),
109        "Restart",
110    )?;
111
112    // Smoke test the rollback
113    if let Some(ref smoke) = target.smoke {
114        std::thread::sleep(std::time::Duration::from_secs(2));
115        match run_cmd(smoke, "Smoke test") {
116            Ok(_) => eprintln!("  Rollback smoke test passed."),
117            Err(e) => eprintln!("  WARNING: Rollback smoke test failed: {}", e),
118        }
119    }
120
121    eprintln!("=== {} rolled back ===", service);
122    Ok(())
123}
124
125pub fn deploy_all(cfg: &Config) -> anyhow::Result<()> {
126    if cfg.deploy.is_empty() {
127        anyhow::bail!("No deploy targets configured in config.toml");
128    }
129    for name in cfg.deploy.keys() {
130        deploy(cfg, name)?;
131    }
132    Ok(())
133}
134
135fn run_cmd(cmd: &str, label: &str) -> anyhow::Result<()> {
136    let status = Command::new("bash").arg("-c").arg(cmd).status()?;
137    if !status.success() {
138        anyhow::bail!("{} failed (exit {})", label, status.code().unwrap_or(-1));
139    }
140    Ok(())
141}
142
143fn cmd_output(cmd: &str) -> anyhow::Result<String> {
144    let output = Command::new("bash").arg("-c").arg(cmd).output()?;
145    Ok(String::from_utf8_lossy(&output.stdout).to_string())
146}
147
148fn parse_disk_usage(df_line: &str) -> u32 {
149    // Parse "Use%" column from df output (e.g., "45%")
150    df_line
151        .split_whitespace()
152        .find(|s| s.ends_with('%'))
153        .and_then(|s| s.trim_end_matches('%').parse().ok())
154        .unwrap_or(0)
155}