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_stdin(
113 client: &DockerClient,
114 container: &str,
115 cmd: Vec<&str>,
116 stdin_data: &str,
117) -> Result<String, DockerError> {
118 let exec_config = CreateExecOptions {
119 attach_stdin: Some(true),
120 attach_stdout: Some(true),
121 attach_stderr: Some(true),
122 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
123 user: Some("root".to_string()),
124 ..Default::default()
125 };
126
127 let exec = client
128 .inner()
129 .create_exec(container, exec_config)
130 .await
131 .map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
132
133 let start_config = StartExecOptions {
134 detach: false,
135 ..Default::default()
136 };
137
138 let mut output = String::new();
139
140 match client
141 .inner()
142 .start_exec(&exec.id, Some(start_config))
143 .await
144 .map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
145 {
146 StartExecResults::Attached {
147 output: mut stream,
148 input: mut input_sink,
149 } => {
150 input_sink
152 .write_all(stdin_data.as_bytes())
153 .await
154 .map_err(|e| DockerError::Container(format!("Failed to write to stdin: {e}")))?;
155
156 input_sink
158 .shutdown()
159 .await
160 .map_err(|e| DockerError::Container(format!("Failed to close stdin: {e}")))?;
161
162 while let Some(result) = stream.next().await {
164 match result {
165 Ok(log_output) => {
166 output.push_str(&log_output.to_string());
167 }
168 Err(e) => {
169 return Err(DockerError::Container(format!(
170 "Error reading exec output: {e}"
171 )));
172 }
173 }
174 }
175 }
176 StartExecResults::Detached => {
177 return Err(DockerError::Container(
178 "Exec unexpectedly detached".to_string(),
179 ));
180 }
181 }
182
183 Ok(output)
184}
185
186pub async fn exec_command_exit_code(
203 client: &DockerClient,
204 container: &str,
205 cmd: Vec<&str>,
206) -> Result<i64, DockerError> {
207 let exec_config = CreateExecOptions {
208 attach_stdout: Some(true),
209 attach_stderr: Some(true),
210 cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
211 user: Some("root".to_string()),
212 ..Default::default()
213 };
214
215 let exec = client
216 .inner()
217 .create_exec(container, exec_config)
218 .await
219 .map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
220
221 let exec_id = exec.id.clone();
222
223 let start_config = StartExecOptions {
224 detach: false,
225 ..Default::default()
226 };
227
228 match client
230 .inner()
231 .start_exec(&exec.id, Some(start_config))
232 .await
233 .map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
234 {
235 StartExecResults::Attached { mut output, .. } => {
236 while output.next().await.is_some() {}
238 }
239 StartExecResults::Detached => {
240 return Err(DockerError::Container(
241 "Exec unexpectedly detached".to_string(),
242 ));
243 }
244 }
245
246 let inspect = client
248 .inner()
249 .inspect_exec(&exec_id)
250 .await
251 .map_err(|e| DockerError::Container(format!("Failed to inspect exec: {e}")))?;
252
253 let exit_code = inspect.exit_code.unwrap_or(-1);
255
256 Ok(exit_code)
257}
258
259#[cfg(test)]
260mod tests {
261 #[test]
266 fn test_command_patterns() {
267 let useradd_cmd = ["useradd", "-m", "-s", "/bin/bash", "testuser"];
269 assert_eq!(useradd_cmd.len(), 5);
270 assert_eq!(useradd_cmd[0], "useradd");
271
272 let id_cmd = ["id", "-u", "testuser"];
273 assert_eq!(id_cmd.len(), 3);
274
275 let chpasswd_cmd = ["chpasswd"];
276 assert_eq!(chpasswd_cmd.len(), 1);
277 }
278}