bollard_next/
exec.rs

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