docker_sdk/
exec.rs

1//! Run new commands inside running containers.
2//!
3//! API Reference: <https://docs.docker.com/engine/api/v1.41/#tag/Exec>
4
5use std::{
6    collections::{BTreeMap, HashMap},
7    hash::Hash,
8    iter,
9};
10
11use futures_util::{stream::Stream, TryFutureExt};
12use hyper::Body;
13use serde::{Deserialize, Serialize};
14use serde_json::{json, Value};
15
16use crate::{
17    errors::{Error, Result},
18    tty, Docker,
19};
20
21/// Interface for docker exec instance
22///
23/// [Api Reference](https://docs.docker.com/engine/api/v1.41/#tag/Exec)
24pub struct Exec<'docker> {
25    docker: &'docker Docker,
26    id: String,
27}
28
29impl<'docker> Exec<'docker> {
30    fn new<S>(
31        docker: &'docker Docker,
32        id: S,
33    ) -> Self
34    where
35        S: Into<String>,
36    {
37        Exec {
38            docker,
39            id: id.into(),
40        }
41    }
42
43    /// Creates a new exec instance that will be executed in a container with id == container_id
44    ///
45    /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerExec)
46    pub async fn create(
47        docker: &'docker Docker,
48        container_id: &str,
49        opts: &ExecContainerOptions,
50    ) -> Result<Exec<'docker>> {
51        #[derive(serde::Deserialize)]
52        #[serde(rename_all = "PascalCase")]
53        struct Response {
54            id: String,
55        }
56
57        let body: Body = opts.serialize()?.into();
58
59        let id = docker
60            .post_json(
61                &format!("/containers/{}/exec", container_id),
62                Some((body, mime::APPLICATION_JSON)),
63            )
64            .await
65            .map(|resp: Response| resp.id)?;
66
67        Ok(Exec::new(docker, id))
68    }
69
70    // This exists for Container::exec()
71    //
72    // We need to combine `Exec::create` and `Exec::start` into one method because otherwise you
73    // needlessly tie the Stream to the lifetime of `container_id` and `opts`. This is because
74    // `Exec::create` is async so it must occur inside of the `async move` block. However, this
75    // means that `container_id` and `opts` are both expected to be alive in the returned stream
76    // because we can't do the work of creating an endpoint from `container_id` or serializing
77    // `opts`. By doing this work outside of the stream, we get owned values that we can then move
78    // into the stream and have the lifetimes work out as you would expect.
79    //
80    // Yes, it is sad that we can't do the easy method and thus have some duplicated code.
81    pub(crate) fn create_and_start(
82        docker: &'docker Docker,
83        container_id: &str,
84        opts: &ExecContainerOptions,
85    ) -> impl Stream<Item = Result<tty::TtyChunk>> + Unpin + 'docker {
86        #[derive(serde::Deserialize)]
87        #[serde(rename_all = "PascalCase")]
88        struct Response {
89            id: String,
90        }
91
92        // To not tie the lifetime of `opts` to the stream, we do the serializing work outside of
93        // the stream. But for backwards compatability, we have to return the error inside of the
94        // stream.
95        let body_result = opts.serialize();
96
97        // To not tie the lifetime of `container_id` to the stream, we convert it to an (owned)
98        // endpoint outside of the stream.
99        let container_endpoint = format!("/containers/{}/exec", container_id);
100
101        Box::pin(
102            async move {
103                // Bubble up the error inside the stream for backwards compatability
104                let body: Body = body_result?.into();
105
106                let exec_id = docker
107                    .post_json(&container_endpoint, Some((body, mime::APPLICATION_JSON)))
108                    .await
109                    .map(|resp: Response| resp.id)?;
110
111                let stream = Box::pin(docker.stream_post(
112                    format!("/exec/{}/start", exec_id),
113                    Some(("{}".into(), mime::APPLICATION_JSON)),
114                    None::<iter::Empty<_>>,
115                ));
116
117                Ok(tty::decode(stream))
118            }
119            .try_flatten_stream(),
120        )
121    }
122
123    /// Get a reference to a set of operations available to an already created exec instance.
124    ///
125    /// It's in callers responsibility to ensure that exec instance with specified id actually
126    /// exists. Use [Exec::create](Exec::create) to ensure that the exec instance is created
127    /// beforehand.
128    pub async fn get<S>(
129        docker: &'docker Docker,
130        id: S,
131    ) -> Exec<'docker>
132    where
133        S: Into<String>,
134    {
135        Exec::new(docker, id)
136    }
137
138    /// Starts this exec instance returning a multiplexed tty stream
139    ///
140    /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ExecStart)
141    pub fn start(&self) -> impl Stream<Item = Result<tty::TtyChunk>> + 'docker {
142        // We must take ownership of the docker reference to not needlessly tie the stream to the
143        // lifetime of `self`.
144        let docker = self.docker;
145        // We convert `self.id` into the (owned) endpoint outside of the stream to not needlessly
146        // tie the stream to the lifetime of `self`.
147        let endpoint = format!("/exec/{}/start", &self.id);
148        Box::pin(
149            async move {
150                let stream = Box::pin(docker.stream_post(
151                    endpoint,
152                    Some(("{}".into(), mime::APPLICATION_JSON)),
153                    None::<iter::Empty<_>>,
154                ));
155
156                Ok(tty::decode(stream))
157            }
158            .try_flatten_stream(),
159        )
160    }
161
162    /// Inspect this exec instance to aquire detailed information
163    ///
164    /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ExecInpsect)
165    pub async fn inspect(&self) -> Result<ExecDetails> {
166        self.docker
167            .get_json(&format!("/exec/{}/json", &self.id)[..])
168            .await
169    }
170
171    /// Resize the TTY session used by an exec instance. This only works if the exec was created
172    /// with `tty` enabled.
173    ///
174    /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ExecResize)
175    pub async fn resize(
176        &self,
177        opts: &ExecResizeOptions,
178    ) -> Result<()> {
179        let body: Body = opts.serialize()?.into();
180
181        self.docker
182            .post_json(
183                &format!("/exec/{}/resize", &self.id)[..],
184                Some((body, mime::APPLICATION_JSON)),
185            )
186            .await
187    }
188}
189
190#[derive(Serialize, Debug)]
191pub struct ExecContainerOptions {
192    params: HashMap<&'static str, Vec<String>>,
193    params_bool: HashMap<&'static str, bool>,
194}
195
196impl ExecContainerOptions {
197    /// return a new instance of a builder for options
198    pub fn builder() -> ExecContainerOptionsBuilder {
199        ExecContainerOptionsBuilder::default()
200    }
201
202    /// serialize options as a string. returns None if no options are defined
203    pub fn serialize(&self) -> Result<String> {
204        let mut body = serde_json::Map::new();
205
206        for (k, v) in &self.params {
207            body.insert(
208                (*k).to_owned(),
209                serde_json::to_value(v).map_err(Error::SerdeJsonError)?,
210            );
211        }
212
213        for (k, v) in &self.params_bool {
214            body.insert(
215                (*k).to_owned(),
216                serde_json::to_value(v).map_err(Error::SerdeJsonError)?,
217            );
218        }
219
220        serde_json::to_string(&body).map_err(Error::from)
221    }
222}
223
224#[derive(Default)]
225pub struct ExecContainerOptionsBuilder {
226    params: HashMap<&'static str, Vec<String>>,
227    params_bool: HashMap<&'static str, bool>,
228}
229
230impl ExecContainerOptionsBuilder {
231    /// Command to run, as an array of strings
232    pub fn cmd(
233        &mut self,
234        cmds: Vec<&str>,
235    ) -> &mut Self {
236        for cmd in cmds {
237            self.params
238                .entry("Cmd")
239                .or_insert_with(Vec::new)
240                .push(cmd.to_owned());
241        }
242        self
243    }
244
245    /// A list of environment variables in the form "VAR=value"
246    pub fn env(
247        &mut self,
248        envs: Vec<&str>,
249    ) -> &mut Self {
250        for env in envs {
251            self.params
252                .entry("Env")
253                .or_insert_with(Vec::new)
254                .push(env.to_owned());
255        }
256        self
257    }
258
259    /// Attach to stdout of the exec command
260    pub fn attach_stdout(
261        &mut self,
262        stdout: bool,
263    ) -> &mut Self {
264        self.params_bool.insert("AttachStdout", stdout);
265        self
266    }
267
268    /// Attach to stderr of the exec command
269    pub fn attach_stderr(
270        &mut self,
271        stderr: bool,
272    ) -> &mut Self {
273        self.params_bool.insert("AttachStderr", stderr);
274        self
275    }
276
277    pub fn build(&self) -> ExecContainerOptions {
278        ExecContainerOptions {
279            params: self.params.clone(),
280            params_bool: self.params_bool.clone(),
281        }
282    }
283}
284
285/// Interface for creating volumes
286#[derive(Serialize, Debug)]
287pub struct ExecResizeOptions {
288    params: HashMap<&'static str, Value>,
289}
290
291impl ExecResizeOptions {
292    /// serialize options as a string. returns None if no options are defined
293    pub fn serialize(&self) -> Result<String> {
294        serde_json::to_string(&self.params).map_err(Error::from)
295    }
296
297    pub fn parse_from<'a, K, V>(
298        &self,
299        params: &'a HashMap<K, V>,
300        body: &mut BTreeMap<String, Value>,
301    ) where
302        &'a HashMap<K, V>: IntoIterator,
303        K: ToString + Eq + Hash,
304        V: Serialize,
305    {
306        for (k, v) in params.iter() {
307            let key = k.to_string();
308            let value = serde_json::to_value(v).unwrap();
309
310            body.insert(key, value);
311        }
312    }
313
314    /// return a new instance of a builder for options
315    pub fn builder() -> ExecResizeOptionsBuilder {
316        ExecResizeOptionsBuilder::new()
317    }
318}
319
320#[derive(Default)]
321pub struct ExecResizeOptionsBuilder {
322    params: HashMap<&'static str, Value>,
323}
324
325impl ExecResizeOptionsBuilder {
326    pub(crate) fn new() -> Self {
327        let params = HashMap::new();
328        ExecResizeOptionsBuilder { params }
329    }
330
331    pub fn height(
332        &mut self,
333        height: u64,
334    ) -> &mut Self {
335        self.params.insert("Name", json!(height));
336        self
337    }
338
339    pub fn width(
340        &mut self,
341        width: u64,
342    ) -> &mut Self {
343        self.params.insert("Name", json!(width));
344        self
345    }
346
347    pub fn build(&self) -> ExecResizeOptions {
348        ExecResizeOptions {
349            params: self.params.clone(),
350        }
351    }
352}
353
354#[derive(Clone, Debug, Serialize, Deserialize)]
355#[serde(rename_all = "PascalCase")]
356pub struct ExecDetails {
357    pub can_remove: bool,
358    #[serde(rename = "ContainerID")]
359    pub container_id: String,
360    pub detach_keys: String,
361    pub exit_code: Option<u64>,
362    #[serde(rename = "ID")]
363    pub id: String,
364    pub open_stderr: bool,
365    pub open_stdin: bool,
366    pub open_stdout: bool,
367    pub process_config: ProcessConfig,
368    pub running: bool,
369    pub pid: u64,
370}
371
372#[derive(Clone, Debug, Serialize, Deserialize)]
373pub struct ProcessConfig {
374    pub arguments: Vec<String>,
375    pub entrypoint: String,
376    pub privileged: bool,
377    pub tty: bool,
378    pub user: Option<String>,
379}