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    match target.deploy_type.as_str() {
17        "docker-compose" => deploy_docker_compose(service, target),
18        _ => deploy_systemd(cfg, service, target),
19    }
20}
21
22fn deploy_docker_compose(service: &str, target: &crate::config::DeployTarget) -> anyhow::Result<()> {
23    let compose_file = target.compose_file.as_deref()
24        .ok_or_else(|| anyhow::anyhow!("docker-compose deploy requires compose_file path"))?;
25
26    eprintln!("=== Deploying {} (docker-compose) ===", service);
27
28    // 1. Pull latest images
29    eprintln!("[1/3] Pulling images...");
30    run_cmd(
31        &format!("docker compose -f {} pull {}", compose_file, target.service),
32        "Docker pull",
33    )?;
34
35    // 2. Recreate containers
36    eprintln!("[2/3] Restarting containers...");
37    run_cmd(
38        &format!("docker compose -f {} up -d {}", compose_file, target.service),
39        "Docker up",
40    )?;
41
42    // 3. Smoke test
43    if let Some(ref smoke) = target.smoke {
44        eprintln!("[3/3] Smoke test...");
45        std::thread::sleep(std::time::Duration::from_secs(5));
46        match run_cmd(smoke, "Smoke test") {
47            Ok(_) => eprintln!("  Smoke test passed."),
48            Err(e) => eprintln!("  WARNING: Smoke test failed: {}", e),
49        }
50    } else {
51        eprintln!("[3/3] No smoke test configured, skipping.");
52    }
53
54    eprintln!("=== {} deployed (docker-compose) ===", service);
55    Ok(())
56}
57
58fn deploy_systemd(cfg: &Config, service: &str, target: &crate::config::DeployTarget) -> anyhow::Result<()> {
59    eprintln!("=== Deploying {} ===", service);
60
61    // 0. Disk check
62    eprintln!("[0/4] Checking disk space...");
63    let df = cmd_output("df -h / | tail -1")?;
64    let use_pct = parse_disk_usage(&df);
65    if use_pct >= 90 {
66        anyhow::bail!(
67            "Disk usage at {}%. Aborting deploy — clean target dirs first.\n  {}",
68            use_pct,
69            df.trim()
70        );
71    }
72    eprintln!("  Disk: {}% used", use_pct);
73
74    // 1. Backup current binary
75    let binary_name = &target.service;
76    let backup_path = format!("/tmp/dstack-rollback-{}", binary_name);
77    let binary_search = cmd_output(&format!(
78        "which {} 2>/dev/null || find {}/{} -name {} -path '*/release/*' 2>/dev/null | head -1",
79        binary_name, cfg.repos.root, service, binary_name
80    ))?;
81    let binary_path = binary_search.trim().lines().next().unwrap_or("");
82    if !binary_path.is_empty() && std::path::Path::new(binary_path).exists() {
83        eprintln!("[1/4] Backing up {} → {}", binary_path, backup_path);
84        let _ = std::fs::copy(binary_path, &backup_path);
85    } else {
86        eprintln!("[1/4] No existing binary found, skipping backup");
87    }
88
89    // 2. Build
90    if !target.build.is_empty() {
91        eprintln!("[2/4] Building...");
92        run_cmd(&target.build, "Build")?;
93    } else {
94        eprintln!("[2/4] No build command, skipping.");
95    }
96
97    // 3. Restart service
98    eprintln!("[3/4] Restarting {}...", target.service);
99    run_cmd(
100        &format!("sudo systemctl restart {}", target.service),
101        "Restart",
102    )?;
103
104    // 4. Smoke test
105    if let Some(ref smoke) = target.smoke {
106        eprintln!("[4/4] Smoke test...");
107        std::thread::sleep(std::time::Duration::from_secs(2));
108        match run_cmd(smoke, "Smoke test") {
109            Ok(_) => eprintln!("  Smoke test passed."),
110            Err(e) => {
111                eprintln!("  WARNING: Smoke test failed: {}", e);
112                if std::path::Path::new(&backup_path).exists() {
113                    eprintln!(
114                        "  Rollback available: dstack deploy {} --rollback",
115                        service
116                    );
117                }
118            }
119        }
120    } else {
121        eprintln!("[4/4] No smoke test configured, skipping.");
122    }
123
124    eprintln!("=== {} deployed ===", service);
125    Ok(())
126}
127
128pub fn rollback(cfg: &Config, service: &str) -> anyhow::Result<()> {
129    let target = cfg
130        .deploy
131        .get(service)
132        .ok_or_else(|| anyhow::anyhow!("No deploy target '{}'", service))?;
133
134    let backup_path = format!("/tmp/dstack-rollback-{}", target.service);
135    if !std::path::Path::new(&backup_path).exists() {
136        anyhow::bail!("No rollback binary found at {}", backup_path);
137    }
138
139    // Find where the current binary lives
140    let binary_search = cmd_output(&format!(
141        "find {}/{} -name {} -path '*/release/*' 2>/dev/null | head -1",
142        cfg.repos.root, service, target.service
143    ))?;
144    let binary_path = binary_search.trim();
145    if binary_path.is_empty() {
146        anyhow::bail!("Cannot find current binary path for {}", service);
147    }
148
149    eprintln!("=== Rolling back {} ===", service);
150    eprintln!("[1/2] Restoring {} → {}", backup_path, binary_path);
151    std::fs::copy(&backup_path, binary_path)?;
152
153    eprintln!("[2/2] Restarting {}...", target.service);
154    run_cmd(
155        &format!("sudo systemctl restart {}", target.service),
156        "Restart",
157    )?;
158
159    // Smoke test the rollback
160    if let Some(ref smoke) = target.smoke {
161        std::thread::sleep(std::time::Duration::from_secs(2));
162        match run_cmd(smoke, "Smoke test") {
163            Ok(_) => eprintln!("  Rollback smoke test passed."),
164            Err(e) => eprintln!("  WARNING: Rollback smoke test failed: {}", e),
165        }
166    }
167
168    eprintln!("=== {} rolled back ===", service);
169    Ok(())
170}
171
172pub fn deploy_all(cfg: &Config) -> anyhow::Result<()> {
173    if cfg.deploy.is_empty() {
174        anyhow::bail!("No deploy targets configured in config.toml");
175    }
176    for name in cfg.deploy.keys() {
177        deploy(cfg, name)?;
178    }
179    Ok(())
180}
181
182fn run_cmd(cmd: &str, label: &str) -> anyhow::Result<()> {
183    let status = Command::new("bash").arg("-c").arg(cmd).status()?;
184    if !status.success() {
185        anyhow::bail!("{} failed (exit {})", label, status.code().unwrap_or(-1));
186    }
187    Ok(())
188}
189
190fn cmd_output(cmd: &str) -> anyhow::Result<String> {
191    let output = Command::new("bash").arg("-c").arg(cmd).output()?;
192    Ok(String::from_utf8_lossy(&output.stdout).to_string())
193}
194
195fn parse_disk_usage(df_line: &str) -> u32 {
196    // Parse "Use%" column from df output (e.g., "45%")
197    df_line
198        .split_whitespace()
199        .find(|s| s.ends_with('%'))
200        .and_then(|s| s.trim_end_matches('%').parse().ok())
201        .unwrap_or(0)
202}