swiftide_docker_executor/
running_docker_executor.rs1use anyhow::Context as _;
2use async_trait::async_trait;
3use bollard::{
4 container::RemoveContainerOptions,
5 secret::{ContainerState, ContainerStateStatusEnum},
6};
7use shell::shell_executor_client::ShellExecutorClient;
8use std::{path::Path, sync::Arc};
9pub use swiftide_core::ToolExecutor;
10use swiftide_core::{Command, CommandError, CommandOutput};
11use uuid::Uuid;
12
13use crate::{
14 client::Client, container_configurator::ContainerConfigurator,
15 container_starter::ContainerStarter, dockerfile_manager::DockerfileManager,
16 image_builder::ImageBuilder, ContextBuilder, DockerExecutorError,
17};
18
19pub mod shell {
20 tonic::include_proto!("shell");
21}
22
23#[derive(Clone, Debug)]
24pub struct RunningDockerExecutor {
25 pub container_id: String,
26 pub(crate) docker: Arc<Client>,
27 pub host_port: String,
28}
29
30impl From<RunningDockerExecutor> for Arc<dyn ToolExecutor> {
31 fn from(val: RunningDockerExecutor) -> Self {
32 Arc::new(val) as Arc<dyn ToolExecutor>
33 }
34}
35
36#[async_trait]
37impl ToolExecutor for RunningDockerExecutor {
38 #[tracing::instrument(skip(self), err)]
39 async fn exec_cmd(&self, cmd: &Command) -> Result<CommandOutput, CommandError> {
40 match cmd {
41 Command::Shell(cmd) => self.exec_shell(cmd).await,
42 Command::ReadFile(path) => self.read_file(path).await,
43 Command::WriteFile(path, content) => self.write_file(path, content).await,
44 _ => unimplemented!(),
45 }
46 }
47}
48
49impl RunningDockerExecutor {
50 pub async fn start(
52 container_uuid: Uuid,
53 context_path: &Path,
54 dockerfile: &Path,
55 image_name: &str,
56 ) -> Result<RunningDockerExecutor, DockerExecutorError> {
57 let docker = Client::lazy_client().await?;
58
59 let dockerfile_manager = DockerfileManager::new(context_path);
61 let tmp_dockerfile = dockerfile_manager.prepare_dockerfile(dockerfile).await?;
62
63 tracing::warn!(
65 "Creating archive for context from {}",
66 context_path.display()
67 );
68 let context = ContextBuilder::from_path(context_path)?.build_tar().await?;
69
70 let tag = container_uuid
72 .to_string()
73 .split_once('-')
74 .map(|(tag, _)| tag)
75 .unwrap_or("latest")
76 .to_string();
77
78 let image_builder = ImageBuilder::new(docker.clone());
79 let image_name_with_tag = image_builder
80 .build_image(
81 context_path,
82 context,
83 tmp_dockerfile.path(),
84 image_name,
85 &tag,
86 )
87 .await?;
88
89 let container_config = ContainerConfigurator::new(docker.socket_path.clone())
91 .create_container_config(&image_name_with_tag);
92
93 let container_starter = ContainerStarter::new(docker.clone());
95 let (container_id, host_port) = container_starter
96 .start_container(image_name, &container_uuid, container_config)
97 .await?;
98
99 let executor = RunningDockerExecutor {
102 container_id,
103 docker,
104 host_port,
105 };
106
107 let Some(tmp_dockerfile_path) = tmp_dockerfile
109 .path()
110 .file_name()
111 .map(|s| s.to_string_lossy().to_string())
112 else {
113 return Ok(executor);
114 };
115
116 drop(tmp_dockerfile); executor
118 .exec_shell(&format!("rm {}", tmp_dockerfile_path))
119 .await
120 .context("failed to remove temporary dockerfile")
121 .map_err(DockerExecutorError::Start)?;
122
123 Ok(executor)
124 }
125
126 pub async fn container_state(&self) -> Result<ContainerState, DockerExecutorError> {
130 let container = self
131 .docker
132 .inspect_container(&self.container_id, None)
133 .await?;
134
135 container.state.ok_or_else(|| {
136 DockerExecutorError::ContainerStateMissing(self.container_id.to_string())
137 })
138 }
139
140 pub async fn is_running(&self) -> bool {
144 self.container_state()
145 .await
146 .map(|state| state.status == Some(ContainerStateStatusEnum::RUNNING))
147 .unwrap_or(false)
148 }
149
150 async fn exec_shell(&self, cmd: &str) -> Result<CommandOutput, CommandError> {
151 let mut client =
152 ShellExecutorClient::connect(format!("http://127.0.0.1:{}", self.host_port))
153 .await
154 .map_err(anyhow::Error::from)?;
155
156 let request = tonic::Request::new(shell::ShellRequest {
157 command: cmd.to_string(),
158 });
159
160 let response = client
161 .exec_shell(request)
162 .await
163 .map_err(anyhow::Error::from)?;
164
165 let shell::ShellResponse {
166 stdout,
167 stderr,
168 exit_code,
169 } = response.into_inner();
170
171 let output = stdout.trim().to_string() + stderr.trim();
173 if exit_code == 0 {
175 Ok(output.into())
176 } else {
177 Err(CommandError::NonZeroExit(output.into()))
178 }
179 }
180
181 #[tracing::instrument(skip(self))]
182 async fn read_file(&self, path: &Path) -> Result<CommandOutput, CommandError> {
183 self.exec_shell(&format!("cat {}", path.display())).await
184 }
185
186 #[tracing::instrument(skip(self, content))]
187 async fn write_file(&self, path: &Path, content: &str) -> Result<CommandOutput, CommandError> {
188 let cmd = indoc::formatdoc! {r#"
189 cat << 'EOFKWAAK' > {path}
190 {content}
191 EOFKWAAK"#,
192 path = path.display(),
193 content = content.trim_end()
194
195 };
196
197 let write_file_result = self.exec_shell(&cmd).await;
198
199 if let Err(CommandError::NonZeroExit(write_file)) = &write_file_result {
201 if [
202 "no such file or directory",
203 "directory nonexistent",
204 "nonexistent directory",
205 ]
206 .iter()
207 .any(|&s| write_file.output.to_lowercase().contains(s))
208 {
209 let path = path.parent().context("No parent directory")?;
210 let mkdircmd = format!("mkdir -p {}", path.display());
211 let _ = self.exec_shell(&mkdircmd).await?;
212
213 return self.exec_shell(&cmd).await;
214 }
215 }
216
217 write_file_result
218 }
219}
220
221impl Drop for RunningDockerExecutor {
222 fn drop(&mut self) {
223 tracing::warn!(
224 "Stopping container {container_id}",
225 container_id = self.container_id
226 );
227 let result = tokio::task::block_in_place(|| {
228 tokio::runtime::Handle::current().block_on(async {
229 self.docker
230 .remove_container(
231 &self.container_id,
232 Some(RemoveContainerOptions {
233 force: true,
234 v: true,
235 ..Default::default()
236 }),
237 )
238 .await
239 })
240 });
241
242 if let Err(e) = result {
243 tracing::warn!(error = %e, "Error stopping container, might not be stopped");
244 }
245 }
246}