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}