Skip to main content

bollard/
exec.rs

1//! Exec API: Run new commands inside running containers
2
3use bollard_stubs::models::ExecConfig;
4use bytes::Bytes;
5use futures_util::TryStreamExt;
6use http::header::{CONNECTION, UPGRADE};
7use http::request::Builder;
8use http_body_util::Full;
9use hyper::Method;
10use serde_derive::{Deserialize, Serialize};
11
12use super::Docker;
13
14use crate::container::LogOutput;
15use crate::docker::BodyType;
16use crate::errors::Error;
17use crate::models::ExecInspectResponse;
18use crate::read::NewlineLogOutputDecoder;
19use futures_core::Stream;
20use std::fmt::{Debug, Formatter};
21use std::pin::Pin;
22use tokio::io::AsyncWrite;
23use tokio_util::codec::FramedRead;
24
25/// Exec configuration used in the [Create Exec API](Docker::create_exec())
26#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
27#[serde(rename_all = "PascalCase")]
28pub struct CreateExecOptions<T>
29where
30    T: Into<String> + serde::ser::Serialize,
31{
32    /// Attach to `stdin` of the exec command.
33    pub attach_stdin: Option<bool>,
34    /// Attach to stdout of the exec command.
35    pub attach_stdout: Option<bool>,
36    /// Attach to stderr of the exec command.
37    pub attach_stderr: Option<bool>,
38    /// Allocate a pseudo-TTY.
39    pub tty: Option<bool>,
40    /// Override the key sequence for detaching a container. Format is a single character `[a-Z]`
41    /// or `ctrl-<value>` where `<value>` is one of: `a-z`, `@`, `^`, `[`, `,` or `_`.
42    pub detach_keys: Option<T>,
43    /// A list of environment variables in the form `["VAR=value", ...].`
44    pub env: Option<Vec<T>>,
45    /// Command to run, as a string or array of strings.
46    pub cmd: Option<Vec<T>>,
47    /// Runs the exec process with extended privileges.
48    pub privileged: Option<bool>,
49    /// The user, and optionally, group to run the exec process inside the container. Format is one
50    /// of: `user`, `user:group`, `uid`, or `uid:gid`.
51    pub user: Option<T>,
52    /// The working directory for the exec process inside the container.
53    pub working_dir: Option<T>,
54}
55
56impl<T> From<CreateExecOptions<T>> for ExecConfig
57where
58    T: Into<String> + serde::ser::Serialize,
59{
60    fn from(opts: CreateExecOptions<T>) -> Self {
61        ExecConfig {
62            attach_stdin: opts.attach_stdin,
63            attach_stdout: opts.attach_stdout,
64            attach_stderr: opts.attach_stderr,
65            detach_keys: opts.detach_keys.map(Into::into),
66            tty: opts.tty,
67            env: opts.env.map(|v| v.into_iter().map(Into::into).collect()),
68            cmd: opts.cmd.map(|v| v.into_iter().map(Into::into).collect()),
69            privileged: opts.privileged,
70            user: opts.user.map(Into::into),
71            working_dir: opts.working_dir.map(Into::into),
72            ..Default::default()
73        }
74    }
75}
76
77/// Result type for the [Create Exec API](Docker::create_exec())
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79#[serde(rename_all = "PascalCase")]
80#[allow(missing_docs)]
81pub struct CreateExecResults {
82    pub id: String,
83}
84
85/// Exec configuration used in the [Create Exec API](Docker::create_exec())
86#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "PascalCase")]
88pub struct StartExecOptions {
89    /// Detach from the command.
90    pub detach: bool,
91    /// Allocate a pseudo-TTY.
92    pub tty: bool,
93    /// The maximum size for a line of output. The default is 8 * 1024 (roughly 1024 characters).
94    pub output_capacity: Option<usize>,
95}
96
97/// Result type for the [Start Exec API](Docker::start_exec())
98#[allow(missing_docs)]
99pub enum StartExecResults {
100    Attached {
101        output: Pin<Box<dyn Stream<Item = Result<LogOutput, Error>> + Send>>,
102        input: Pin<Box<dyn AsyncWrite + Send>>,
103    },
104    Detached,
105}
106
107impl Debug for StartExecResults {
108    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109        match self {
110            StartExecResults::Attached { .. } => write!(f, "StartExecResults::Attached"),
111            StartExecResults::Detached => write!(f, "StartExecResults::Detached"),
112        }
113    }
114}
115
116/// Resize configuration used in the [Resize Exec API](Docker::resize_exec())
117#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
118#[serde(rename_all = "PascalCase")]
119pub struct ResizeExecOptions {
120    /// Height of the TTY session in characters
121    #[serde(rename = "h")]
122    pub height: u16,
123    /// Width of the TTY session in characters
124    #[serde(rename = "w")]
125    pub width: u16,
126}
127
128impl From<ResizeExecOptions> for crate::query_parameters::ResizeExecOptions {
129    fn from(opts: ResizeExecOptions) -> Self {
130        crate::query_parameters::ResizeExecOptionsBuilder::default()
131            .w(opts.width as i32)
132            .h(opts.height as i32)
133            .build()
134    }
135}
136
137impl Docker {
138    /// ---
139    ///
140    /// # Create Exec
141    ///
142    /// Run a command inside a running container.
143    ///
144    /// # Arguments
145    ///
146    ///  - Container name as string slice.
147    ///  - [Create Exec Options](CreateExecOptions) struct.
148    ///
149    /// # Returns
150    ///
151    ///  - A [Create Exec Results](CreateExecResults) struct, wrapped in a
152    ///    Future.
153    ///
154    /// # Examples
155    ///
156    /// ```rust
157    /// # use bollard::Docker;
158    /// # let docker = Docker::connect_with_http_defaults().unwrap();
159    ///
160    /// use bollard::exec::CreateExecOptions;
161    ///
162    /// use std::default::Default;
163    ///
164    /// let config = CreateExecOptions {
165    ///     cmd: Some(vec!["ps", "-ef"]),
166    ///     attach_stdout: Some(true),
167    ///     ..Default::default()
168    /// };
169    ///
170    /// docker.create_exec("hello-world", config);
171    /// ```
172    pub async fn create_exec(
173        &self,
174        container_name: &str,
175        config: impl Into<ExecConfig>,
176    ) -> Result<CreateExecResults, Error> {
177        let url = format!("/containers/{container_name}/exec");
178
179        let req = self.build_request(
180            &url,
181            Builder::new().method(Method::POST),
182            None::<String>,
183            Docker::serialize_payload(Some(config.into())),
184        );
185
186        self.process_into_value(req).await
187    }
188
189    /// ---
190    ///
191    /// # Start Exec
192    ///
193    /// Starts a previously set up exec instance. If detach is true, this endpoint returns
194    /// immediately after starting the command.
195    ///
196    /// # Arguments
197    ///
198    ///  - The ID of the previously created exec configuration.
199    ///
200    /// # Returns
201    ///
202    ///  - [Log Output](LogOutput) enum, wrapped in a Stream.
203    ///
204    /// # Examples
205    ///
206    /// ```rust
207    /// # use bollard::Docker;
208    /// # let docker = Docker::connect_with_http_defaults().unwrap();
209    ///
210    /// # use bollard::exec::CreateExecOptions;
211    /// # use std::default::Default;
212    ///
213    /// # let config = CreateExecOptions {
214    /// #     cmd: Some(vec!["ps", "-ef"]),
215    /// #     attach_stdout: Some(true),
216    /// #     ..Default::default()
217    /// # };
218    ///
219    /// async {
220    ///     let message = docker.create_exec("hello-world", config).await.unwrap();
221    ///     use bollard::exec::StartExecOptions;
222    ///     docker.start_exec(&message.id, None::<StartExecOptions>);
223    /// };
224    /// ```
225    pub async fn start_exec(
226        &self,
227        exec_id: &str,
228        config: Option<StartExecOptions>,
229    ) -> Result<StartExecResults, Error> {
230        let url = format!("/exec/{exec_id}/start");
231
232        match config {
233            Some(StartExecOptions { detach: true, .. }) => {
234                let req = self.build_request(
235                    &url,
236                    Builder::new().method(Method::POST),
237                    None::<String>,
238                    Docker::serialize_payload(config),
239                );
240
241                self.process_into_unit(req).await?;
242                Ok(StartExecResults::Detached)
243            }
244            _ => {
245                let capacity = match config {
246                    Some(StartExecOptions {
247                        output_capacity: Some(capacity),
248                        ..
249                    }) => capacity,
250                    _ => 8 * 1024,
251                };
252
253                let req = self.build_request(
254                    &url,
255                    Builder::new()
256                        .method(Method::POST)
257                        .header(CONNECTION, "Upgrade")
258                        .header(UPGRADE, "tcp"),
259                    None::<String>,
260                    Docker::serialize_payload(config.or_else(|| {
261                        Some(StartExecOptions {
262                            ..Default::default()
263                        })
264                    })),
265                );
266
267                let (read, write) = self.process_upgraded(req).await?;
268
269                let log =
270                    FramedRead::with_capacity(read, NewlineLogOutputDecoder::new(true), capacity)
271                        .map_err(|e| e.into());
272
273                Ok(StartExecResults::Attached {
274                    output: Box::pin(log),
275                    input: Box::pin(write),
276                })
277            }
278        }
279    }
280
281    /// ---
282    ///
283    /// # Inspect Exec
284    ///
285    /// Return low-level information about an exec instance.
286    ///
287    /// # Arguments
288    ///
289    ///  - The ID of the previously created exec configuration.
290    ///
291    /// # Returns
292    ///
293    ///  - An [Exec Inspect Response](ExecInspectResponse) struct, wrapped in a Future.
294    ///
295    /// # Examples
296    ///
297    /// ```rust
298    /// # use bollard::Docker;
299    /// # let docker = Docker::connect_with_http_defaults().unwrap();
300    ///
301    /// # use bollard::exec::CreateExecOptions;
302    /// # use std::default::Default;
303    ///
304    /// # let config = CreateExecOptions {
305    /// #     cmd: Some(vec!["ps", "-ef"]),
306    /// #     attach_stdout: Some(true),
307    /// #     ..Default::default()
308    /// # };
309    ///
310    /// async {
311    ///     let message = docker.create_exec("hello-world", config).await.unwrap();
312    ///     docker.inspect_exec(&message.id);
313    /// };
314    /// ```
315    pub async fn inspect_exec(&self, exec_id: &str) -> Result<ExecInspectResponse, Error> {
316        let url = format!("/exec/{exec_id}/json");
317
318        let req = self.build_request(
319            &url,
320            Builder::new().method(Method::GET),
321            None::<String>,
322            Ok(BodyType::Left(Full::new(Bytes::new()))),
323        );
324
325        self.process_into_value(req).await
326    }
327
328    /// ---
329    ///
330    /// # Resize Exec
331    ///
332    /// Resize the TTY session used by an exec instance. This endpoint only works if `tty` was specified as part of creating and starting the exec instance.
333    ///
334    /// # Arguments
335    ///
336    ///  - The ID of the previously created exec configuration.
337    ///  - [Resize Exec Options](ResizeExecOptions) struct.
338    ///
339    /// # Examples
340    ///
341    /// ```rust
342    /// # use bollard::Docker;
343    /// # let docker = Docker::connect_with_http_defaults().unwrap();
344    /// #
345    /// # use bollard::exec::{CreateExecOptions, ResizeExecOptions};
346    /// # use std::default::Default;
347    /// #
348    /// # let config = CreateExecOptions {
349    /// #     cmd: Some(vec!["ps", "-ef"]),
350    /// #     attach_stdout: Some(true),
351    /// #     ..Default::default()
352    /// # };
353    /// #
354    /// async {
355    ///     let message = docker.create_exec("hello-world", config).await.unwrap();
356    ///     docker.resize_exec(&message.id, ResizeExecOptions {
357    ///         width: 80,
358    ///         height: 60
359    ///     });
360    /// };
361    /// ```
362    pub async fn resize_exec(
363        &self,
364        exec_id: &str,
365        options: impl Into<crate::query_parameters::ResizeExecOptions>,
366    ) -> Result<(), Error> {
367        let url = format!("/exec/{exec_id}/resize");
368
369        let req = self.build_request(
370            &url,
371            Builder::new().method(Method::POST),
372            Some(options.into()),
373            Ok(BodyType::Left(Full::new(Bytes::new()))),
374        );
375
376        self.process_into_unit(req).await
377    }
378}