stakpak_shared/
container.rs1use std::collections::HashMap;
2use std::net::TcpListener;
3use std::process::Command;
4
5pub fn stakpak_agent_image() -> String {
9 format!("ghcr.io/stakpak/agent:v{}", env!("CARGO_PKG_VERSION"))
10}
11
12pub fn stakpak_agent_default_mounts() -> Vec<String> {
18 vec![
19 "~/.stakpak/config.toml:/home/agent/.stakpak/config.toml:ro".to_string(),
21 "~/.stakpak/auth.toml:/home/agent/.stakpak/auth.toml:ro".to_string(),
22 "~/.stakpak/data/local.db:/home/agent/.stakpak/data/local.db".to_string(),
23 "~/.agent-board/data.db:/home/agent/.agent-board/data.db".to_string(),
24 "./:/agent:ro".to_string(),
26 "./.stakpak:/agent/.stakpak".to_string(),
27 "~/.aws/config:/home/agent/.aws/config:ro".to_string(),
29 "~/.aws/credentials:/home/agent/.aws/credentials:ro".to_string(),
30 "~/.aws/sso:/home/agent/.aws/sso".to_string(),
31 "~/.aws/cli:/home/agent/.aws/cli".to_string(),
32 "~/.config/gcloud/active_config:/home/agent/.config/gcloud/active_config:ro".to_string(),
34 "~/.config/gcloud/configurations:/home/agent/.config/gcloud/configurations:ro".to_string(),
35 "~/.config/gcloud/application_default_credentials.json:/home/agent/.config/gcloud/application_default_credentials.json:ro".to_string(),
36 "~/.config/gcloud/credentials.db:/home/agent/.config/gcloud/credentials.db:ro".to_string(),
37 "~/.config/gcloud/access_tokens.db:/home/agent/.config/gcloud/access_tokens.db:ro".to_string(),
38 "~/.config/gcloud/logs:/home/agent/.config/gcloud/logs".to_string(),
39 "~/.config/gcloud/cache:/home/agent/.config/gcloud/cache".to_string(),
40 "~/.azure/config:/home/agent/.azure/config:ro".to_string(),
42 "~/.azure/clouds.config:/home/agent/.azure/clouds.config:ro".to_string(),
43 "~/.azure/azureProfile.json:/home/agent/.azure/azureProfile.json:ro".to_string(),
44 "~/.azure/msal_token_cache.json:/home/agent/.azure/msal_token_cache.json".to_string(),
45 "~/.azure/msal_http_cache.bin:/home/agent/.azure/msal_http_cache.bin".to_string(),
46 "~/.azure/logs:/home/agent/.azure/logs".to_string(),
47 "~/.digitalocean:/home/agent/.digitalocean:ro".to_string(),
49 "~/.kube:/home/agent/.kube:ro".to_string(),
50 "~/.ssh:/home/agent/.ssh:ro".to_string(),
52 "stakpak-aqua-cache:/home/agent/.local/share/aquaproj-aqua".to_string(),
54 ]
55}
56
57pub fn expand_volume_path(volume: &str) -> String {
59 if (volume.starts_with("~/") || volume.starts_with("~:"))
60 && let Ok(home_dir) = std::env::var("HOME")
61 {
62 return volume.replacen("~", &home_dir, 1);
63 }
64 volume.to_string()
65}
66
67pub fn is_named_volume(host_part: &str) -> bool {
72 !host_part.starts_with('/')
73 && !host_part.starts_with('.')
74 && !host_part.starts_with('~')
75 && !host_part.contains('/')
76}
77
78pub fn ensure_named_volumes_exist() {
83 for vol in stakpak_agent_default_mounts() {
84 let host_part = vol.split(':').next().unwrap_or(&vol);
85 if is_named_volume(host_part) {
86 let _ = Command::new("docker")
87 .args(["volume", "create", host_part])
88 .stdout(std::process::Stdio::null())
89 .stderr(std::process::Stdio::null())
90 .status();
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
96pub struct ContainerConfig {
97 pub image: String,
98 pub env_vars: HashMap<String, String>,
99 pub ports: Vec<String>, pub extra_hosts: Vec<String>, pub volumes: Vec<String>, }
103
104pub fn find_available_port() -> Option<u16> {
105 match TcpListener::bind("0.0.0.0:0") {
106 Ok(listener) => listener.local_addr().ok().map(|addr| addr.port()),
107 Err(_) => None,
108 }
109}
110
111pub fn is_docker_available() -> bool {
113 Command::new("docker")
114 .arg("--version")
115 .output()
116 .map(|output| output.status.success())
117 .unwrap_or(false)
118}
119
120pub fn image_exists_locally(image: &str) -> Result<bool, String> {
122 let output = Command::new("docker")
123 .args(["images", "-q", image])
124 .output()
125 .map_err(|e| format!("Failed to execute docker images command: {}", e))?;
126
127 if output.status.success() {
128 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
129 Ok(!stdout.is_empty())
130 } else {
131 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
132 Err(format!("Docker images command failed: {}", stderr))
133 }
134}
135
136pub fn run_container_detached(config: ContainerConfig) -> Result<String, String> {
137 let mut cmd = Command::new("docker");
138
139 cmd.arg("run").arg("-d").arg("--rm");
140
141 for port_mapping in &config.ports {
143 cmd.arg("-p").arg(port_mapping);
144 }
145
146 for (key, value) in &config.env_vars {
148 cmd.arg("-e").arg(format!("{}={}", key, value));
149 }
150
151 for host_mapping in &config.extra_hosts {
153 cmd.arg("--add-host").arg(host_mapping);
154 }
155
156 for volume_mapping in &config.volumes {
158 cmd.arg("-v").arg(volume_mapping);
159 }
160
161 cmd.arg(&config.image);
163
164 let output = cmd
165 .output()
166 .map_err(|e| format!("Failed to execute docker command: {}", e))?;
167
168 if output.status.success() {
169 let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
170 Ok(container_id)
171 } else {
172 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
173 Err(format!("Docker command failed: {}", stderr))
174 }
175}
176
177pub fn stop_container(container_id: &str) -> Result<(), String> {
178 let output = Command::new("docker")
179 .arg("stop")
180 .arg(container_id)
181 .output()
182 .map_err(|e| format!("Failed to execute docker stop: {}", e))?;
183
184 if output.status.success() {
185 Ok(())
186 } else {
187 let stderr = String::from_utf8_lossy(&output.stderr);
188 if stderr.contains("No such container") {
189 Ok(())
190 } else {
191 Err(format!("Failed to stop container: {}", stderr))
192 }
193 }
194}
195
196pub fn remove_container(
197 container_id: &str,
198 force: bool,
199 remove_volumes: bool,
200) -> Result<(), String> {
201 let mut cmd = Command::new("docker");
202
203 cmd.arg("rm");
204
205 if force {
206 cmd.arg("-f");
207 }
208
209 if remove_volumes {
210 cmd.arg("-v");
211 }
212
213 cmd.arg(container_id);
214
215 let output = cmd
216 .output()
217 .map_err(|e| format!("Failed to execute docker rm: {}", e))?;
218
219 if output.status.success() {
220 Ok(())
221 } else {
222 let stderr = String::from_utf8_lossy(&output.stderr);
223 if stderr.contains("No such container") {
224 Ok(())
225 } else {
226 Err(format!("Failed to remove container: {}", stderr))
227 }
228 }
229}
230
231pub fn get_container_host_port(container_id: &str, container_port: u16) -> Result<u16, String> {
232 let output = Command::new("docker")
233 .arg("port")
234 .arg(container_id)
235 .arg(container_port.to_string())
236 .output()
237 .map_err(|e| format!("Failed to get container port: {}", e))?;
238
239 if output.status.success() {
240 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
241 let port = stdout.split(':').next_back().unwrap_or("");
242 Ok(port.parse().unwrap())
243 } else {
244 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
245 Err(format!("Failed to get container port: {}", stderr))
246 }
247}