docker_api/api/
container.rs

1//! Create and manage containers.
2use crate::opts::{
3    ContainerCommitOpts, ContainerCreateOpts, ContainerListOpts, ContainerPruneOpts,
4    ContainerRemoveOpts, ContainerRestartOpts, ContainerStopOpts, ExecStartOpts,
5};
6use crate::{models, stream};
7
8use std::{io, path::Path, str};
9
10use futures_util::{Stream, TryStreamExt};
11use hyper::Body;
12use serde::Deserialize;
13
14use crate::{
15    api::Exec,
16    conn::{tty, Headers, Payload},
17    opts::ExecCreateOpts,
18    Error, Result,
19};
20use containers_api::url::{append_query, construct_ep, encoded_pair};
21
22impl_api_ty!(Container => id);
23
24impl Container {
25    impl_api_ep! {container: Container, resp
26        Inspect -> &format!("/containers/{}/json", container.id), models::ContainerInspect200Response
27        Logs -> &format!("/containers/{}/logs", container.id), ()
28        DeleteWithOpts -> &format!("/containers/{}", container.id), String, delete
29    }
30
31    api_doc! { Container => Top
32    |
33    /// Returns a `top` view of information about the container process.
34    /// On Unix systems, this is done by running the ps command. This endpoint is not supported on Windows.
35    pub async fn top(&self, psargs: Option<&str>) -> Result<models::ContainerTop200Response> {
36        let mut ep = format!("/containers/{}/top", self.id);
37        if let Some(ref args) = psargs {
38            append_query(&mut ep, encoded_pair("ps_args", args));
39        }
40        self.docker.get_json(&ep).await
41    }}
42
43    api_doc! { Container => Attach
44    |
45    /// Attaches a [`TtyMultiplexer`](TtyMultiplexer) to the container.
46    ///
47    /// The [`TtyMultiplexer`](TtyMultiplexer) implements Stream for returning Stdout and Stderr chunks. It also implements [`AsyncWrite`](futures_util::io::AsyncWrite) for writing to Stdin.
48    ///
49    /// The multiplexer can be split into its read and write halves with the [`split`](TtyMultiplexer::split) method
50    pub async fn attach(&self) -> Result<tty::Multiplexer> {
51        let inspect = self.inspect().await?;
52        let is_tty = inspect.config.and_then(|c| c.tty).unwrap_or_default();
53        stream::attach(
54            self.docker.clone(),
55            format!(
56                "/containers/{}/attach?stream=1&stdout=1&stderr=1&stdin=1",
57                self.id
58            ),
59            Payload::empty(),
60            is_tty,
61        )
62        .await
63    }}
64
65    api_doc! { Container => Changes
66    |
67    /// Returns a set of changes made to the container instance.
68    pub async fn changes(&self) -> Result<Option<models::ContainerChanges200Response>> {
69        self.docker
70            .get_json(&format!("/containers/{}/changes", self.id))
71            .await
72    }}
73
74    api_doc! { Container => Export
75    |
76    /// Exports the current docker container into a tarball.
77    pub fn export(&self) -> impl Stream<Item = Result<Vec<u8>>> + '_ {
78        self.docker
79            .get_stream(format!("/containers/{}/export", self.id))
80            .map_ok(|c| c.to_vec())
81    }}
82
83    api_doc! { Container => Stats
84    |
85    /// Returns a stream of stats specific to this container instance.
86    pub fn stats(&self) -> impl Stream<Item = Result<serde_json::Value>> + Unpin + '_ {
87        let codec = asynchronous_codec::LinesCodec {};
88
89        let reader = Box::pin(
90            self.docker
91                .get_stream(format!("/containers/{}/stats", self.id))
92                .map_err(|e| io::Error::new(io::ErrorKind::Other, e)),
93        )
94        .into_async_read();
95
96        Box::pin(
97            asynchronous_codec::FramedRead::new(reader, codec)
98                .map_err(Error::IO)
99                .and_then(|s: String| async move {
100                    log::trace!("{}", s);
101                    serde_json::from_str(&s).map_err(Error::SerdeJsonError)
102                }),
103        )
104    }}
105
106    api_doc! { Container => Start
107    |
108    /// Start the container instance.
109    pub async fn start(&self) -> Result<()> {
110        self.docker
111            .post_string(
112                &format!("/containers/{}/start", self.id),
113                Payload::empty(),
114                Headers::none(),
115            )
116            .await
117            .map(|_| ())
118    }}
119
120    api_doc! { Container => Stop
121    |
122    /// Stop the container instance.
123    pub async fn stop(&self, opts: &ContainerStopOpts) -> Result<()> {
124        let ep = construct_ep(format!("/containers/{}/stop", self.id), opts.serialize());
125        self.docker
126            .post_string(&ep, Payload::empty(), Headers::none())
127            .await
128            .map(|_| ())
129    }}
130
131    api_doc! { Container => Restart
132    |
133    /// Restart the container instance.
134    pub async fn restart(&self, opts: &ContainerRestartOpts) -> Result<()> {
135        let ep = construct_ep(format!("/containers/{}/restart", self.id), opts.serialize());
136        self.docker
137            .post_string(&ep, Payload::empty(), Headers::none())
138            .await
139            .map(|_| ())
140    }}
141
142    api_doc! { Container => Kill
143    |
144    /// Kill the container instance.
145    pub async fn kill(&self, signal: Option<&str>) -> Result<()> {
146        let mut ep = format!("/containers/{}/kill", self.id);
147        if let Some(sig) = signal {
148            append_query(&mut ep, encoded_pair("signal", sig));
149        }
150        self.docker
151            .post_string(&ep, Payload::empty(), Headers::none())
152            .await
153            .map(|_| ())
154    }}
155
156    api_doc! { Container => Rename
157    |
158    /// Rename the container instance.
159    pub async fn rename(&self, name: &str) -> Result<()> {
160        self.docker
161            .post_string(
162                &format!(
163                    "/containers/{}/rename?{}",
164                    self.id,
165                    encoded_pair("name", name)
166                ),
167                Payload::empty(),
168                Headers::none(),
169            )
170            .await
171            .map(|_| ())
172    }}
173
174    api_doc! { Container => Pause
175    |
176    /// Pause the container instance.
177    pub async fn pause(&self) -> Result<()> {
178        self.docker
179            .post_string(
180                &format!("/containers/{}/pause", self.id),
181                Payload::empty(),
182                Headers::none(),
183            )
184            .await
185            .map(|_| ())
186    }}
187
188    api_doc! { Container => Unpause
189    |
190    /// Unpause the container instance.
191    pub async fn unpause(&self) -> Result<()> {
192        self.docker
193            .post_string(
194                &format!("/containers/{}/unpause", self.id),
195                Payload::empty(),
196                Headers::none(),
197            )
198            .await
199            .map(|_| ())
200    }}
201
202    api_doc! { Container => Wait
203    |
204    /// Wait until the container stops.
205    pub async fn wait(&self) -> Result<models::ContainerWaitResponse> {
206        self.docker
207            .post_json(
208                format!("/containers/{}/wait", self.id),
209                Payload::empty(),
210                Headers::none(),
211            )
212            .await
213    }}
214
215    api_doc! { Exec
216    |
217    /// Execute a command in this container.
218    pub async fn exec(
219        &self,
220        create_opts: &ExecCreateOpts,
221        start_opts: &ExecStartOpts,
222    ) ->  Result<tty::Multiplexer> {
223        Exec::create_and_start(self.docker.clone(), &self.id, create_opts, start_opts).await
224    }}
225
226    api_doc! { Container => Archive
227    |
228    /// Copy a file/folder from the container.  The resulting stream is a tarball of the extracted
229    /// files.
230    ///
231    /// If `path` is not an absolute path, it is relative to the container’s root directory. The
232    /// resource specified by `path` must exist. To assert that the resource is expected to be a
233    /// directory, `path` should end in `/` or `/`. (assuming a path separator of `/`). If `path`
234    /// ends in `/.`  then this indicates that only the contents of the path directory should be
235    /// copied.  A symlink is always resolved to its target.
236    pub fn copy_from(&self, path: impl AsRef<Path>) -> impl Stream<Item = Result<Vec<u8>>> + '_ {
237        self.docker
238            .get_stream(format!(
239                "/containers/{}/archive?{}",
240                self.id,
241                encoded_pair("path", path.as_ref().to_string_lossy())
242            ))
243            .map_ok(|c| c.to_vec())
244    }}
245
246    api_doc! { PutContainer => Archive
247    |
248    /// Copy a byte slice as file into (see `bytes`) the container.
249    ///
250    /// The file will be copied at the given location (see `path`) and will be owned by root
251    /// with access mask 644.
252    pub async fn copy_file_into<P: AsRef<Path>>(&self, path: P, bytes: &[u8]) -> Result<()> {
253        let path = path.as_ref();
254
255        let mut ar = tar::Builder::new(Vec::new());
256        let mut header = tar::Header::new_gnu();
257        header.set_size(bytes.len() as u64);
258        header.set_mode(0o0644);
259        ar.append_data(
260            &mut header,
261            path.to_path_buf()
262                .iter()
263                .skip(1)
264                .collect::<std::path::PathBuf>(),
265            bytes,
266        )?;
267        let data = ar.into_inner()?;
268
269        self.copy_to(Path::new("/"), data.into()).await.map(|_| ())
270    }}
271
272    api_doc! { PutContainer => Archive
273    |
274    /// Copy a tarball (see `body`) to the container.
275    ///
276    /// The tarball will be copied to the container and extracted at the given location (see `path`).
277    pub async fn copy_to(&self, path: &Path, body: Body) -> Result<()> {
278        self.docker
279            .put(
280                &format!(
281                    "/containers/{}/archive?{}",
282                    self.id,
283                    encoded_pair("path", path.to_string_lossy())
284                ),
285                Payload::XTar(body),
286            )
287            .await
288            .map(|_| ())
289    }}
290
291    api_doc! { Container => ArchiveInfo
292    |
293    /// Get information about files in a container.
294    pub async fn stat_file<P>(&self, path: P) -> Result<String>
295    where
296        P: AsRef<Path>,
297    {
298        static PATH_STAT_HEADER: &str = "X-Docker-Container-Path-Stat";
299        let resp = self
300            .docker
301            .head(&format!(
302                "/containers/{}/archive?{}",
303                self.id,
304                encoded_pair("path", path.as_ref().to_string_lossy())
305            ))
306            .await?;
307        if let Some(header) = resp.headers().get(PATH_STAT_HEADER) {
308            let header = header.to_str().map_err(|e| {
309                Error::InvalidResponse(format!("response header was invalid - {e}"))
310            })?;
311
312            base64::decode(header)
313                .map_err(|e| {
314                    Error::InvalidResponse(format!("expected header to be valid base64 - {e}"))
315                })
316                .and_then(|s| {
317                    str::from_utf8(s.as_slice())
318                        .map(str::to_string)
319                        .map_err(|e| {
320                            Error::InvalidResponse(format!(
321                                "expected header to be valid utf8 - {e}"
322                            ))
323                        })
324                })
325        } else {
326            Err(Error::InvalidResponse(format!("missing `{PATH_STAT_HEADER}` header")))
327        }
328    }}
329
330    api_doc! { Image => Commit
331    |
332    /// Create a new image from this container
333    pub async fn commit(&self, opts: &ContainerCommitOpts, config: Option<&models::ContainerConfig>) -> Result<String> {
334        #[derive(Deserialize)]
335        struct IdStruct {
336            #[serde(rename = "Id")]
337            id: String,
338        }
339
340        let payload = if let Some(config) = config {
341            Payload::Json(serde_json::to_string(config)?)
342        } else {
343            Payload::Json("{}".into()) // empty json
344        };
345
346        self.docker
347            .post_json(
348                format!(
349                    "/commit?{}",
350                    opts.with_container(self.id().as_ref())
351                        .serialize()
352                        .unwrap_or_default()
353                ),
354                payload,
355                Headers::none(),
356            )
357            .await
358            .map(|id: IdStruct| id.id)
359    }}
360}
361
362impl Containers {
363    impl_api_ep! {__: Container, resp
364        List -> "/containers/json", models::ContainerSummary
365        Prune -> "/containers/prune", models::ContainerPrune200Response
366    }
367
368    api_doc! { Containers => Create
369    |
370    /// Create a container
371    pub async fn create(&self, opts: &ContainerCreateOpts) -> Result<Container> {
372        let ep = if let Some(name) = opts.name() {
373            construct_ep("/containers/create", Some(encoded_pair("name", name)))
374        } else {
375            "/containers/create".to_owned()
376        };
377        self.docker
378            .post_json(&ep, Payload::Json(opts.serialize_vec()?), Headers::none())
379            .await
380            .map(|resp: models::ContainerCreateResponse| {
381                Container::new(self.docker.clone(), resp.id)
382            })
383    }}
384}