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