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 eprintln!("[1/3] Pulling images...");
30 run_cmd(
31 &format!("docker compose -f {} pull {}", compose_file, target.service),
32 "Docker pull",
33 )?;
34
35 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 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 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 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 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 eprintln!("[3/4] Restarting {}...", target.service);
99 run_cmd(
100 &format!("sudo systemctl restart {}", target.service),
101 "Restart",
102 )?;
103
104 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 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 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 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}