crankshaft_docker/
service.rs1use std::collections::HashMap;
4#[cfg(unix)]
5use std::os::unix::process::ExitStatusExt as _;
6#[cfg(windows)]
7use std::os::windows::process::ExitStatusExt as _;
8use std::path::PathBuf;
9use std::process::ExitStatus;
10use std::time::Duration;
11
12use bollard::Docker;
13use bollard::models::ContainerWaitResponse;
14use bollard::models::TaskState;
15use bollard::query_parameters::InspectContainerOptions;
16use bollard::query_parameters::ListTasksOptions;
17use bollard::query_parameters::LogsOptionsBuilder;
18use bollard::query_parameters::WaitContainerOptions;
19
20mod builder;
21
22pub use builder::Builder;
23use crankshaft_events::Event;
24use futures::StreamExt;
25use tokio::fs::File;
26use tokio::time::sleep;
27use tracing::debug;
28use tracing::info;
29use tracing::trace;
30
31use crate::Error;
32use crate::EventOptions;
33use crate::Result;
34use crate::container::ExecutionResult;
35use crate::container::write_logs;
36
37pub struct Service {
47 client: Docker,
50
51 id: String,
53
54 stdout: Option<PathBuf>,
56
57 stderr: Option<PathBuf>,
59}
60
61impl Service {
62 pub fn new(
67 client: Docker,
68 id: String,
69 stdout: Option<PathBuf>,
70 stderr: Option<PathBuf>,
71 ) -> Self {
72 Self {
73 client,
74 id,
75 stdout,
76 stderr,
77 }
78 }
79
80 pub fn id(&self) -> &str {
82 &self.id
83 }
84
85 pub async fn run(
87 &self,
88 task_name: &str,
89 events: Option<EventOptions>,
90 ) -> Result<ExecutionResult> {
91 let stdout = match &self.stdout {
92 Some(path) => Some((
93 path.as_path(),
94 File::create(path).await.map_err(|e| {
95 Error::Message(format!(
96 "failed to create stdout file `{path}`: {e}",
97 path = path.display()
98 ))
99 })?,
100 )),
101 None => None,
102 };
103
104 let stderr = match &self.stderr {
105 Some(path) => Some((
106 path.as_path(),
107 File::create(path).await.map_err(|e| {
108 Error::Message(format!(
109 "failed to create stderr file `{path}`: {e}",
110 path = path.display()
111 ))
112 })?,
113 )),
114 None => None,
115 };
116
117 let (image, container_id, exit_code) = loop {
118 trace!(
119 "polling tasks for service `{id}` (task `{task_name}`)",
120 id = self.id
121 );
122
123 let tasks = self
125 .client
126 .list_tasks(Some(ListTasksOptions {
127 filters: Some(HashMap::from_iter([(
128 String::from("service"),
129 vec![self.id.to_owned()],
130 )])),
131 }))
132 .await
133 .map_err(Error::Docker)?;
134
135 if tasks.is_empty() {
136 sleep(Duration::from_millis(100)).await;
138 continue;
139 }
140
141 assert_eq!(
142 tasks.len(),
143 1,
144 "Docker service task count should always be 1"
145 );
146
147 let task = tasks.into_iter().next().unwrap();
148 let image = task
149 .spec
150 .as_ref()
151 .and_then(|spec| spec.container_spec.as_ref())
152 .and_then(|spec| spec.image.clone())
153 .expect("Docker reported a container without a state");
154
155 let status = task.status.ok_or_else(|| {
156 Error::Message("Docker daemon reported a task with no status".into())
157 })?;
158
159 match status.state {
160 Some(TaskState::NEW)
161 | Some(TaskState::PENDING)
162 | Some(TaskState::ALLOCATED)
163 | Some(TaskState::ASSIGNED)
164 | Some(TaskState::ACCEPTED)
165 | Some(TaskState::READY)
166 | Some(TaskState::PREPARING)
167 | Some(TaskState::STARTING)
168 | None => {
169 trace!(
170 "task has not yet started for service `{id}` (task `{task_name}`)",
171 id = self.id
172 );
173
174 sleep(Duration::from_secs(1)).await;
177 }
178 Some(TaskState::RUNNING) | Some(TaskState::COMPLETE) | Some(TaskState::FAILED) => {
179 let container_status = status.container_status.ok_or_else(|| {
180 Error::Message(
181 "Docker daemon reported a task with no container status".into(),
182 )
183 })?;
184
185 let container_id = container_status.container_id.ok_or_else(|| {
186 Error::Message("Docker reported a task with no container id".into())
187 })?;
188
189 if let Some(events) = &events {
190 events
191 .sender
192 .send(Event::TaskContainerCreated {
193 id: events.task_id,
194 container: container_id.clone(),
195 })
196 .ok();
197 }
198
199 info!(
200 "service `{id}` (task `{task_name}`) has started container `{container_id}",
201 id = self.id
202 );
203
204 if let Some(events) = &events
205 && events.send_start
206 {
207 events
208 .sender
209 .send(Event::TaskStarted { id: events.task_id })
210 .ok();
211 }
212
213 let stdout_enabled = self.stdout.is_some()
215 || events.as_ref().is_some_and(|e| e.user_config.send_stdout);
216 let stderr_enabled = self.stderr.is_some()
217 || events.as_ref().is_some_and(|e| e.user_config.send_stderr);
218 if stdout_enabled || stderr_enabled {
219 let logs = self.client.logs(
220 &container_id,
221 Some(
222 LogsOptionsBuilder::new()
223 .stdout(stdout_enabled)
224 .stderr(stderr_enabled)
225 .follow(true)
226 .build(),
227 ),
228 );
229
230 write_logs(logs, stdout, stderr, events.as_ref()).await?;
232 }
233
234 if status.state == Some(TaskState::RUNNING) {
235 let mut wait_stream = self
237 .client
238 .wait_container(&container_id, None::<WaitContainerOptions>);
239
240 match wait_stream.next().await {
241 Some(Ok(ContainerWaitResponse {
242 status_code: code, ..
243 }))
244 | Some(Err(bollard::errors::Error::DockerContainerWaitError {
245 code,
246 ..
247 })) => {
248 break (image, container_id, code);
249 }
250 Some(Err(e)) => return Err(e.into()),
251 None => {
252 let container = self
254 .client
255 .inspect_container(
256 &container_id,
257 None::<InspectContainerOptions>,
258 )
259 .await
260 .map_err(Error::Docker)?;
261
262 break (
263 image,
264 container_id,
265 container
266 .state
267 .ok_or_else(|| {
268 Error::Message(
269 "Docker reported a container without a state"
270 .into(),
271 )
272 })?
273 .exit_code
274 .ok_or_else(|| {
275 Error::Message(
276 "Docker reported a finished contained without an \
277 exit code"
278 .into(),
279 )
280 })?,
281 );
282 }
283 }
284 } else {
285 break (
286 image,
287 container_id,
288 container_status.exit_code.ok_or_else(|| {
289 Error::Message(format!(
290 "Docker reported a {kind} task with no exit code",
291 kind = if status.state == Some(TaskState::FAILED) {
292 "failed"
293 } else {
294 "completed"
295 }
296 ))
297 })?,
298 );
299 }
300 }
301 Some(TaskState::SHUTDOWN)
302 | Some(TaskState::REJECTED)
303 | Some(TaskState::ORPHANED)
304 | Some(TaskState::REMOVE) => {
305 return Err(Error::Message(format!(
306 "Docker task failed: {msg}",
307 msg = status
308 .err
309 .as_deref()
310 .or(status.message.as_deref())
311 .unwrap_or("no error message was provided by the Docker daemon")
312 )));
313 }
314 }
315 };
316
317 #[cfg(unix)]
319 let status = ExitStatus::from_raw((exit_code as i32) << 8);
320
321 #[cfg(windows)]
322 let status = ExitStatus::from_raw(exit_code as u32);
323
324 info!(
325 "container `{container_id}` for service `{id}` (task `{task_name}`) has exited with \
326 {status}",
327 id = self.id
328 );
329
330 if let Some(events) = &events {
331 events
332 .sender
333 .send(Event::TaskContainerExited {
334 id: events.task_id,
335 container: container_id,
336 exit_status: status,
337 })
338 .ok();
339 }
340
341 Ok(ExecutionResult { image, status })
342 }
343
344 pub async fn delete(&self) -> Result<()> {
346 debug!("deleting Docker service `{id}`", id = self.id);
347 self.client
348 .delete_service(&self.id)
349 .await
350 .map_err(Error::Docker)?;
351
352 Ok(())
353 }
354}