opencode_cloud_core/docker/
exec.rs1use bollard::exec::{CreateExecOptions, StartExecOptions, StartExecResults};
8use futures_util::StreamExt;
9use tokio::io::AsyncWriteExt;
10
11use super::profile::remap_container_name;
12use super::{DockerClient, DockerError};
13
14pub async fn exec_command(
29 client: &DockerClient,
30 container: &str,
31 cmd: Vec<&str>,
32) -> Result<String, DockerError> {
33 let container = remap_container_name(container);
34 let exec_config = CreateExecOptions {
35 attach_stdout: Some(true),
36 attach_stderr: Some(true),
37 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
38 user: Some("root".to_string()),
39 ..Default::default()
40 };
41
42 let exec = client
43 .inner()
44 .create_exec(&container, exec_config)
45 .await
46 .map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
47
48 let start_config = StartExecOptions {
49 detach: false,
50 ..Default::default()
51 };
52
53 let mut output = String::new();
54
55 match client
56 .inner()
57 .start_exec(&exec.id, Some(start_config))
58 .await
59 .map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
60 {
61 StartExecResults::Attached {
62 output: mut stream, ..
63 } => {
64 while let Some(result) = stream.next().await {
65 match result {
66 Ok(log_output) => {
67 output.push_str(&log_output.to_string());
68 }
69 Err(e) => {
70 return Err(DockerError::Container(format!(
71 "Error reading exec output: {e}"
72 )));
73 }
74 }
75 }
76 }
77 StartExecResults::Detached => {
78 return Err(DockerError::Container(
79 "Exec unexpectedly detached".to_string(),
80 ));
81 }
82 }
83
84 Ok(output)
85}
86
87pub async fn exec_command_with_status(
91 client: &DockerClient,
92 container: &str,
93 cmd: Vec<&str>,
94) -> Result<(String, i64), DockerError> {
95 let container = remap_container_name(container);
96 let exec_config = CreateExecOptions {
97 attach_stdout: Some(true),
98 attach_stderr: Some(true),
99 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
100 user: Some("root".to_string()),
101 ..Default::default()
102 };
103
104 let exec = client
105 .inner()
106 .create_exec(&container, exec_config)
107 .await
108 .map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
109
110 let exec_id = exec.id.clone();
111 let start_config = StartExecOptions {
112 detach: false,
113 ..Default::default()
114 };
115
116 let mut output = String::new();
117
118 match client
119 .inner()
120 .start_exec(&exec.id, Some(start_config))
121 .await
122 .map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
123 {
124 StartExecResults::Attached {
125 output: mut stream, ..
126 } => {
127 while let Some(result) = stream.next().await {
128 match result {
129 Ok(log_output) => {
130 output.push_str(&log_output.to_string());
131 }
132 Err(e) => {
133 return Err(DockerError::Container(format!(
134 "Error reading exec output: {e}"
135 )));
136 }
137 }
138 }
139 }
140 StartExecResults::Detached => {
141 return Err(DockerError::Container(
142 "Exec unexpectedly detached".to_string(),
143 ));
144 }
145 }
146
147 let inspect = client
148 .inner()
149 .inspect_exec(&exec_id)
150 .await
151 .map_err(|e| DockerError::Container(format!("Failed to inspect exec: {e}")))?;
152
153 let exit_code = inspect.exit_code.unwrap_or(-1);
154
155 Ok((output, exit_code))
156}
157
158pub async fn exec_command_with_stdin(
186 client: &DockerClient,
187 container: &str,
188 cmd: Vec<&str>,
189 stdin_data: &str,
190) -> Result<String, DockerError> {
191 let container = remap_container_name(container);
192 let exec_config = CreateExecOptions {
193 attach_stdin: Some(true),
194 attach_stdout: Some(true),
195 attach_stderr: Some(true),
196 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
197 user: Some("root".to_string()),
198 ..Default::default()
199 };
200
201 let exec = client
202 .inner()
203 .create_exec(&container, exec_config)
204 .await
205 .map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
206
207 let start_config = StartExecOptions {
208 detach: false,
209 ..Default::default()
210 };
211
212 let mut output = String::new();
213
214 match client
215 .inner()
216 .start_exec(&exec.id, Some(start_config))
217 .await
218 .map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
219 {
220 StartExecResults::Attached {
221 output: mut stream,
222 input: mut input_sink,
223 } => {
224 input_sink
226 .write_all(stdin_data.as_bytes())
227 .await
228 .map_err(|e| DockerError::Container(format!("Failed to write to stdin: {e}")))?;
229
230 input_sink
232 .shutdown()
233 .await
234 .map_err(|e| DockerError::Container(format!("Failed to close stdin: {e}")))?;
235
236 while let Some(result) = stream.next().await {
238 match result {
239 Ok(log_output) => {
240 output.push_str(&log_output.to_string());
241 }
242 Err(e) => {
243 return Err(DockerError::Container(format!(
244 "Error reading exec output: {e}"
245 )));
246 }
247 }
248 }
249 }
250 StartExecResults::Detached => {
251 return Err(DockerError::Container(
252 "Exec unexpectedly detached".to_string(),
253 ));
254 }
255 }
256
257 Ok(output)
258}
259
260pub async fn exec_command_exit_code(
277 client: &DockerClient,
278 container: &str,
279 cmd: Vec<&str>,
280) -> Result<i64, DockerError> {
281 let container = remap_container_name(container);
282 let exec_config = CreateExecOptions {
283 attach_stdout: Some(true),
284 attach_stderr: Some(true),
285 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
286 user: Some("root".to_string()),
287 ..Default::default()
288 };
289
290 let exec = client
291 .inner()
292 .create_exec(&container, exec_config)
293 .await
294 .map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
295
296 let exec_id = exec.id.clone();
297
298 let start_config = StartExecOptions {
299 detach: false,
300 ..Default::default()
301 };
302
303 match client
305 .inner()
306 .start_exec(&exec.id, Some(start_config))
307 .await
308 .map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
309 {
310 StartExecResults::Attached { mut output, .. } => {
311 while output.next().await.is_some() {}
313 }
314 StartExecResults::Detached => {
315 return Err(DockerError::Container(
316 "Exec unexpectedly detached".to_string(),
317 ));
318 }
319 }
320
321 let inspect = client
323 .inner()
324 .inspect_exec(&exec_id)
325 .await
326 .map_err(|e| DockerError::Container(format!("Failed to inspect exec: {e}")))?;
327
328 let exit_code = inspect.exit_code.unwrap_or(-1);
330
331 Ok(exit_code)
332}
333
334#[cfg(test)]
335mod tests {
336 #[test]
341 fn test_command_patterns() {
342 let useradd_cmd = ["useradd", "-m", "-s", "/bin/bash", "testuser"];
344 assert_eq!(useradd_cmd.len(), 5);
345 assert_eq!(useradd_cmd[0], "useradd");
346
347 let id_cmd = ["id", "-u", "testuser"];
348 assert_eq!(id_cmd.len(), 3);
349
350 let chpasswd_cmd = ["chpasswd"];
351 assert_eq!(chpasswd_cmd.len(), 1);
352 }
353}