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 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 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 eprintln!("[2/4] Building...");
48 run_cmd(&target.build, "Build")?;
49
50 eprintln!("[3/4] Restarting {}...", target.service);
52 run_cmd(
53 &format!("sudo systemctl restart {}", target.service),
54 "Restart",
55 )?;
56
57 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 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 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 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}