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}