swiftide_docker_executor/
running_docker_executor.rs1use anyhow::Context as _;
2use async_trait::async_trait;
3use bollard::{
4 container::LogOutput,
5 query_parameters::{InspectContainerOptions, RemoveContainerOptions},
6 secret::{ContainerState, ContainerStateStatusEnum},
7};
8use codegen::shell_executor_client::ShellExecutorClient;
9use std::{collections::HashMap, path::Path, sync::Arc};
10pub use swiftide_core::ToolExecutor;
11use swiftide_core::{Command, CommandError, CommandOutput, Loader as _, prelude::StreamExt as _};
12
13use crate::{
14 ContextBuilder, ContextError, DockerExecutor, DockerExecutorError, client::Client,
15 container_configurator::ContainerConfigurator, container_starter::ContainerStarter,
16 dockerfile_manager::DockerfileManager, image_builder::ImageBuilder,
17};
18
19pub mod codegen {
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 pub(crate) env_clear: bool,
31 pub(crate) remove_env: Vec<String>,
32 pub(crate) env: HashMap<String, String>,
33}
34
35impl From<RunningDockerExecutor> for Arc<dyn ToolExecutor> {
36 fn from(val: RunningDockerExecutor) -> Self {
37 Arc::new(val) as Arc<dyn ToolExecutor>
38 }
39}
40
41#[async_trait]
42impl ToolExecutor for RunningDockerExecutor {
43 #[tracing::instrument(skip(self), err)]
44 async fn exec_cmd(&self, cmd: &Command) -> Result<CommandOutput, CommandError> {
45 match cmd {
46 Command::Shell(cmd) => self.exec_shell(cmd).await,
47 Command::ReadFile(path) => self.read_file(path).await,
48 Command::WriteFile(path, content) => self.write_file(path, content).await,
49 _ => unimplemented!(),
50 }
51 }
52
53 async fn stream_files(
54 &self,
55 path: &Path,
56 extensions: Option<Vec<String>>,
57 ) -> anyhow::Result<swiftide_core::indexing::IndexingStream> {
58 let extensions = extensions.unwrap_or_default();
59 Ok(self.as_file_loader(path, extensions).into_stream())
60 }
61}
62
63impl RunningDockerExecutor {
64 pub async fn start(
66 builder: &DockerExecutor,
67 ) -> Result<RunningDockerExecutor, DockerExecutorError> {
68 let docker = Client::lazy_client().await?;
69
70 let mut tmp_dockerfile_name = None;
72 let mut image_name = builder.image_name.clone();
73 let dockerfile = &builder.dockerfile;
74 let context_path = &builder.context_path;
75 let user = builder.user.as_deref();
76 let container_uuid = builder.container_uuid;
77
78 if let Some(dockerfile) = dockerfile {
80 let dockerfile_manager = DockerfileManager::new(context_path);
82 let tmp_dockerfile = dockerfile_manager.prepare_dockerfile(dockerfile).await?;
83
84 tracing::warn!(
86 "Creating archive for context from {}",
87 context_path.display()
88 );
89 let context = ContextBuilder::from_path(context_path, tmp_dockerfile.path())?
90 .build_tar()
91 .await?;
92
93 tracing::debug!("Context build with size: {} bytes", context.len());
94
95 let tmp_dockerfile_name_inner = tmp_dockerfile
96 .path()
97 .file_name()
98 .ok_or_else(|| {
99 ContextError::CustomDockerfile("Could not read custom dockerfile".to_string())
100 })
101 .map(|s| s.to_string_lossy().to_string())?;
102
103 drop(tmp_dockerfile); let tag = container_uuid
107 .to_string()
108 .split_once('-')
109 .map(|(tag, _)| tag)
110 .unwrap_or("latest")
111 .to_string();
112
113 let image_builder = ImageBuilder::new(docker.clone());
114 let image_name_with_tag = image_builder
115 .build_image(
116 context,
117 tmp_dockerfile_name_inner.as_ref(),
118 &image_name,
119 &tag,
120 )
121 .await?;
122
123 image_name = image_name_with_tag;
124 tmp_dockerfile_name = Some(tmp_dockerfile_name_inner);
125 }
126
127 let container_config = ContainerConfigurator::new(docker.socket_path.clone())
129 .create_container_config(&image_name, user);
130
131 tracing::info!("Starting container with image: {image_name} and uuid: {container_uuid}");
133 let container_starter = ContainerStarter::new(docker.clone());
134 let (container_id, host_port) = container_starter
135 .start_container(&image_name, &container_uuid, container_config)
136 .await?;
137
138 let executor = RunningDockerExecutor {
141 container_id,
142 docker,
143 host_port,
144 env_clear: builder.env_clear,
145 remove_env: builder.remove_env.clone(),
146 env: builder.env.clone(),
147 };
148
149 if let Some(tmp_dockerfile_name) = tmp_dockerfile_name {
150 executor
151 .exec_shell(&format!("rm {}", tmp_dockerfile_name.as_str()))
152 .await
153 .context("failed to remove temporary dockerfile")
154 .map_err(DockerExecutorError::Start)?;
155 }
156
157 Ok(executor)
158 }
159
160 pub async fn container_state(&self) -> Result<ContainerState, DockerExecutorError> {
164 let container = self
165 .docker
166 .inspect_container(&self.container_id, None::<InspectContainerOptions>)
167 .await?;
168
169 container.state.ok_or_else(|| {
170 DockerExecutorError::ContainerStateMissing(self.container_id.to_string())
171 })
172 }
173
174 pub async fn is_running(&self) -> bool {
178 self.container_state()
179 .await
180 .map(|state| state.status == Some(ContainerStateStatusEnum::RUNNING))
181 .unwrap_or(false)
182 }
183
184 pub async fn logs(&self) -> Result<Vec<String>, DockerExecutorError> {
186 let mut logs = Vec::new();
187 let mut stream = self.docker.logs(
188 &self.container_id,
189 Some(bollard::query_parameters::LogsOptions {
190 follow: false,
191 stdout: true,
192 stderr: true,
193 tail: "all".to_string(),
194 ..Default::default()
195 }),
196 );
197
198 while let Some(log_result) = stream.next().await {
199 match log_result {
200 Ok(log_output) => match log_output {
201 LogOutput::Console { message }
202 | LogOutput::StdOut { message }
203 | LogOutput::StdErr { message } => {
204 logs.push(String::from_utf8_lossy(&message).trim().to_string());
205 }
206 _ => {}
207 },
208 Err(e) => tracing::error!("Error retrieving logs: {e}"), }
210 }
211
212 Ok(logs)
213 }
214
215 async fn exec_shell(&self, cmd: &str) -> Result<CommandOutput, CommandError> {
216 let mut client =
217 ShellExecutorClient::connect(format!("http://127.0.0.1:{}", self.host_port))
218 .await
219 .map_err(anyhow::Error::from)?;
220
221 let request = tonic::Request::new(codegen::ShellRequest {
222 command: cmd.to_string(),
223 env_clear: self.env_clear,
224 env_remove: self.remove_env.clone(),
225 envs: self.env.clone(),
226 });
227
228 let response = client
229 .exec_shell(request)
230 .await
231 .map_err(anyhow::Error::from)?;
232
233 let codegen::ShellResponse {
234 stdout,
235 stderr,
236 exit_code,
237 } = response.into_inner();
238
239 let output = stdout.trim().to_string() + stderr.trim();
241 if exit_code == 0 {
243 Ok(output.into())
244 } else {
245 Err(CommandError::NonZeroExit(output.into()))
246 }
247 }
248
249 #[tracing::instrument(skip(self))]
250 async fn read_file(&self, path: &Path) -> Result<CommandOutput, CommandError> {
251 self.exec_shell(&format!("cat {}", path.display())).await
252 }
253
254 #[tracing::instrument(skip(self, content))]
255 async fn write_file(&self, path: &Path, content: &str) -> Result<CommandOutput, CommandError> {
256 let cmd = indoc::formatdoc! {r#"
257 cat << 'EOFKWAAK' > {path}
258 {content}
259 EOFKWAAK"#,
260 path = path.display(),
261 content = content.trim_end()
262
263 };
264
265 let write_file_result = self.exec_shell(&cmd).await;
266
267 if let Err(CommandError::NonZeroExit(write_file)) = &write_file_result {
269 if [
270 "no such file or directory",
271 "directory nonexistent",
272 "nonexistent directory",
273 ]
274 .iter()
275 .any(|&s| write_file.output.to_lowercase().contains(s))
276 {
277 let path = path.parent().context("No parent directory")?;
278 let mkdircmd = format!("mkdir -p {}", path.display());
279 let _ = self.exec_shell(&mkdircmd).await?;
280
281 return self.exec_shell(&cmd).await;
282 }
283 }
284
285 write_file_result
286 }
287}
288
289impl Drop for RunningDockerExecutor {
290 fn drop(&mut self) {
291 tracing::warn!(
292 "Stopping container {container_id}",
293 container_id = self.container_id
294 );
295 let result = tokio::task::block_in_place(|| {
296 tokio::runtime::Handle::current().block_on(async {
297 self.docker
298 .remove_container(
299 &self.container_id,
300 Some(RemoveContainerOptions {
301 force: true,
302 v: true,
303 ..Default::default()
304 }),
305 )
306 .await
307 })
308 });
309
310 if let Err(e) = result {
311 tracing::warn!(error = %e, "Error stopping container, might not be stopped");
312 }
313 }
314}