swiftide_docker_executor/
running_docker_executor.rsuse anyhow::Context as _;
use async_trait::async_trait;
use std::{path::Path, sync::Arc};
pub use swiftide_core::ToolExecutor;
use swiftide_core::{prelude::StreamExt as _, Command, CommandError, CommandOutput};
use tracing::info;
use uuid::Uuid;
use bollard::{
container::{
Config, CreateContainerOptions, LogOutput, RemoveContainerOptions, StartContainerOptions,
},
exec::{CreateExecOptions, StartExecResults},
image::BuildImageOptions,
secret::{ContainerState, ContainerStateStatusEnum},
};
use crate::{client::Client, ContextBuilder, DockerExecutorError};
#[derive(Clone, Debug)]
pub struct RunningDockerExecutor {
pub container_id: String,
pub(crate) docker: Arc<Client>,
}
impl From<RunningDockerExecutor> for Arc<dyn ToolExecutor> {
fn from(val: RunningDockerExecutor) -> Self {
Arc::new(val) as Arc<dyn ToolExecutor>
}
}
#[async_trait]
impl ToolExecutor for RunningDockerExecutor {
#[tracing::instrument(skip(self), err)]
async fn exec_cmd(&self, cmd: &Command) -> Result<CommandOutput, CommandError> {
match cmd {
Command::Shell(cmd) => self.exec_shell(cmd).await,
Command::ReadFile(path) => self.read_file(path).await,
Command::WriteFile(path, content) => self.write_file(path, content).await,
_ => unimplemented!(),
}
}
}
impl RunningDockerExecutor {
pub async fn start(
container_uuid: Uuid,
context_path: &Path,
dockerfile: &Path,
image_name: &str,
) -> Result<RunningDockerExecutor, DockerExecutorError> {
let docker = Client::lazy_client().await?;
tracing::warn!(
"Creating archive for context from {}",
context_path.display()
);
let context = ContextBuilder::from_path(context_path)?.build_tar().await?;
let tag = container_uuid
.to_string()
.split_once('-')
.map(|(tag, _)| tag)
.unwrap_or("latest")
.to_string();
let image_name_with_tag = format!("{image_name}:{tag}");
let build_options = BuildImageOptions {
t: image_name_with_tag.as_str(),
rm: true,
dockerfile: &dockerfile.to_string_lossy(),
..Default::default()
};
tracing::warn!("Building docker image with name {image_name}");
{
let mut build_stream = docker.build_image(build_options, None, Some(context.into()));
while let Some(log) = build_stream.next().await {
match log {
Ok(output) => {
if let Some(stream) = output.stream {
info!("{}", stream);
}
}
Err(e) => {
tracing::error!("Error building image: {e:#}");
return Err(e.into());
}
}
}
}
let socket_path = &docker.socket_path;
let config = Config {
image: Some(image_name_with_tag.as_str()),
cmd: Some(vec!["sleep", "infinity"]),
tty: Some(true),
entrypoint: Some(vec![""]),
host_config: Some(bollard::models::HostConfig {
auto_remove: Some(true),
binds: Some(vec![format!("{socket_path}:/var/run/docker.sock")]),
..Default::default()
}),
..Default::default()
};
let container_name = format!("{image_name}-{container_uuid}");
let create_options = CreateContainerOptions {
name: container_name.as_str(),
..Default::default()
};
tracing::warn!("Creating container from image {image_name}");
let container_id = docker
.create_container(Some(create_options), config)
.await?
.id;
tracing::warn!("Starting container {container_id}");
docker
.start_container(&container_id, None::<StartContainerOptions<String>>)
.await?;
Ok(RunningDockerExecutor {
container_id,
docker,
})
}
pub async fn container_state(&self) -> Result<ContainerState, DockerExecutorError> {
let container = self
.docker
.inspect_container(&self.container_id, None)
.await?;
container.state.ok_or_else(|| {
DockerExecutorError::ContainerStateMissing(self.container_id.to_string())
})
}
pub async fn is_running(&self) -> bool {
self.container_state()
.await
.map(|state| state.status == Some(ContainerStateStatusEnum::RUNNING))
.unwrap_or(false)
}
async fn exec_shell(&self, cmd: &str) -> Result<CommandOutput, CommandError> {
let cmd = vec!["sh", "-c", cmd];
tracing::debug!("Executing command {cmd}", cmd = cmd.join(" "));
let exec = self
.docker
.create_exec(
&self.container_id,
CreateExecOptions {
attach_stdout: Some(true),
attach_stderr: Some(true),
cmd: Some(cmd),
..Default::default()
},
)
.await
.context("Failed to create docker exec")?
.id;
let mut stdout = String::new();
let mut stderr = String::new();
if let StartExecResults::Attached { mut output, .. } = self
.docker
.start_exec(&exec, None)
.await
.context("Failed to start docker exec")?
{
while let Some(Ok(msg)) = output.next().await {
match msg {
LogOutput::StdErr { .. } => stderr.push_str(&msg.to_string()),
LogOutput::StdOut { .. } => stdout.push_str(&msg.to_string()),
_ => {
stderr
.push_str("Command appears to wait for input, which is not supported");
break;
}
}
}
} else {
todo!();
}
let exec_inspect = self
.docker
.inspect_exec(&exec)
.await
.context("Failed to inspect docker exec result")?;
let exit_code = exec_inspect.exit_code.unwrap_or(0);
let output = stdout.trim().to_string() + stderr.trim();
if exit_code == 0 {
Ok(output.into())
} else {
Err(CommandError::NonZeroExit(output.into()))
}
}
#[tracing::instrument(skip(self))]
async fn read_file(&self, path: &Path) -> Result<CommandOutput, CommandError> {
self.exec_shell(&format!("cat {}", path.display())).await
}
#[tracing::instrument(skip(self, content))]
async fn write_file(&self, path: &Path, content: &str) -> Result<CommandOutput, CommandError> {
let cmd = indoc::formatdoc! {r#"
cat << 'EOFKWAAK' > {path}
{content}
EOFKWAAK"#,
path = path.display(),
content = content.trim_end()
};
let write_file_result = self.exec_shell(&cmd).await;
if let Err(CommandError::NonZeroExit(write_file)) = &write_file_result {
if [
"no such file or directory",
"directory nonexistent",
"nonexistent directory",
]
.iter()
.any(|&s| write_file.output.to_lowercase().contains(s))
{
let path = path.parent().context("No parent directory")?;
let mkdircmd = format!("mkdir -p {}", path.display());
let _ = self.exec_shell(&mkdircmd).await?;
return self.exec_shell(&cmd).await;
}
}
write_file_result
}
}
impl Drop for RunningDockerExecutor {
fn drop(&mut self) {
tracing::warn!(
"Stopping container {container_id}",
container_id = self.container_id
);
let result = tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async {
self.docker
.remove_container(
&self.container_id,
Some(RemoveContainerOptions {
force: true,
v: true,
..Default::default()
}),
)
.await
})
});
if let Err(e) = result {
tracing::warn!(error = %e, "Error stopping container, might not be stopped");
}
}
}