use bollard::exec::{CreateExecOptions, StartExecOptions, StartExecResults};
use futures_util::StreamExt;
use tokio::io::AsyncWriteExt;
use super::profile::remap_container_name;
use super::{DockerClient, DockerError};
pub async fn exec_command(
client: &DockerClient,
container: &str,
cmd: Vec<&str>,
) -> Result<String, DockerError> {
let container = remap_container_name(container);
let exec_config = CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
user: Some("root".to_string()),
..Default::default()
};
let exec = client
.inner()
.create_exec(&container, exec_config)
.await
.map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
let start_config = StartExecOptions {
detach: false,
..Default::default()
};
let mut output = String::new();
match client
.inner()
.start_exec(&exec.id, Some(start_config))
.await
.map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
{
StartExecResults::Attached {
output: mut stream, ..
} => {
while let Some(result) = stream.next().await {
match result {
Ok(log_output) => {
output.push_str(&log_output.to_string());
}
Err(e) => {
return Err(DockerError::Container(format!(
"Error reading exec output: {e}"
)));
}
}
}
}
StartExecResults::Detached => {
return Err(DockerError::Container(
"Exec unexpectedly detached".to_string(),
));
}
}
Ok(output)
}
pub async fn exec_command_with_status(
client: &DockerClient,
container: &str,
cmd: Vec<&str>,
) -> Result<(String, i64), DockerError> {
let container = remap_container_name(container);
let exec_config = CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
user: Some("root".to_string()),
..Default::default()
};
let exec = client
.inner()
.create_exec(&container, exec_config)
.await
.map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
let exec_id = exec.id.clone();
let start_config = StartExecOptions {
detach: false,
..Default::default()
};
let mut output = String::new();
match client
.inner()
.start_exec(&exec.id, Some(start_config))
.await
.map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
{
StartExecResults::Attached {
output: mut stream, ..
} => {
while let Some(result) = stream.next().await {
match result {
Ok(log_output) => {
output.push_str(&log_output.to_string());
}
Err(e) => {
return Err(DockerError::Container(format!(
"Error reading exec output: {e}"
)));
}
}
}
}
StartExecResults::Detached => {
return Err(DockerError::Container(
"Exec unexpectedly detached".to_string(),
));
}
}
let inspect = client
.inner()
.inspect_exec(&exec_id)
.await
.map_err(|e| DockerError::Container(format!("Failed to inspect exec: {e}")))?;
let exit_code = inspect.exit_code.unwrap_or(-1);
Ok((output, exit_code))
}
pub async fn exec_command_with_stdin(
client: &DockerClient,
container: &str,
cmd: Vec<&str>,
stdin_data: &str,
) -> Result<String, DockerError> {
let container = remap_container_name(container);
let exec_config = CreateExecOptions {
attach_stdin: Some(true),
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
user: Some("root".to_string()),
..Default::default()
};
let exec = client
.inner()
.create_exec(&container, exec_config)
.await
.map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
let start_config = StartExecOptions {
detach: false,
..Default::default()
};
let mut output = String::new();
match client
.inner()
.start_exec(&exec.id, Some(start_config))
.await
.map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
{
StartExecResults::Attached {
output: mut stream,
input: mut input_sink,
} => {
input_sink
.write_all(stdin_data.as_bytes())
.await
.map_err(|e| DockerError::Container(format!("Failed to write to stdin: {e}")))?;
input_sink
.shutdown()
.await
.map_err(|e| DockerError::Container(format!("Failed to close stdin: {e}")))?;
while let Some(result) = stream.next().await {
match result {
Ok(log_output) => {
output.push_str(&log_output.to_string());
}
Err(e) => {
return Err(DockerError::Container(format!(
"Error reading exec output: {e}"
)));
}
}
}
}
StartExecResults::Detached => {
return Err(DockerError::Container(
"Exec unexpectedly detached".to_string(),
));
}
}
Ok(output)
}
pub async fn exec_command_exit_code(
client: &DockerClient,
container: &str,
cmd: Vec<&str>,
) -> Result<i64, DockerError> {
let container = remap_container_name(container);
let exec_config = CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(cmd.iter().map(|s| s.to_string()).collect()),
user: Some("root".to_string()),
..Default::default()
};
let exec = client
.inner()
.create_exec(&container, exec_config)
.await
.map_err(|e| DockerError::Container(format!("Failed to create exec: {e}")))?;
let exec_id = exec.id.clone();
let start_config = StartExecOptions {
detach: false,
..Default::default()
};
match client
.inner()
.start_exec(&exec.id, Some(start_config))
.await
.map_err(|e| DockerError::Container(format!("Failed to start exec: {e}")))?
{
StartExecResults::Attached { mut output, .. } => {
while output.next().await.is_some() {}
}
StartExecResults::Detached => {
return Err(DockerError::Container(
"Exec unexpectedly detached".to_string(),
));
}
}
let inspect = client
.inner()
.inspect_exec(&exec_id)
.await
.map_err(|e| DockerError::Container(format!("Failed to inspect exec: {e}")))?;
let exit_code = inspect.exit_code.unwrap_or(-1);
Ok(exit_code)
}
#[cfg(test)]
mod tests {
#[test]
fn test_command_patterns() {
let useradd_cmd = ["useradd", "-m", "-s", "/bin/bash", "testuser"];
assert_eq!(useradd_cmd.len(), 5);
assert_eq!(useradd_cmd[0], "useradd");
let id_cmd = ["id", "-u", "testuser"];
assert_eq!(id_cmd.len(), 3);
let chpasswd_cmd = ["chpasswd"];
assert_eq!(chpasswd_cmd.len(), 1);
}
}