rs_docker_api_rs/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 single response of stats specific to this container instance.
86    pub async fn stats_no_stream(&self, one_shot: Option<bool>) -> Result<models::ContainerStats200Response> {
87        let mut ep = format!("/containers/{}/stats?stream=false", self.id);
88        match one_shot {
89            None => {}
90            Some(param) => { ep.push_str(format!("&one-shot={}", param).as_str()); }
91        }
92        self.docker.get_json(&ep).await
93    }}
94
95    api_doc! { Container => Stats
96    |
97    /// Returns a stream of stats specific to this container instance.
98    pub fn stats(&self) -> impl Stream<Item = Result<serde_json::Value>> + Unpin + '_ {
99        let codec = asynchronous_codec::LinesCodec {};
100
101        let reader = Box::pin(
102            self.docker
103                .get_stream(format!("/containers/{}/stats", self.id))
104                .map_err(|e| io::Error::new(io::ErrorKind::Other, e)),
105        )
106        .into_async_read();
107
108        Box::pin(
109            asynchronous_codec::FramedRead::new(reader, codec)
110                .map_err(Error::IO)
111                .and_then(|s: String| async move {
112                    log::trace!("{}", s);
113                    serde_json::from_str(&s).map_err(Error::SerdeJsonError)
114                }),
115        )
116    }}
117
118    api_doc! { Container => Start
119    |
120    /// Start the container instance.
121    pub async fn start(&self) -> Result<()> {
122        self.docker
123            .post_string(
124                &format!("/containers/{}/start", self.id),
125                Payload::empty(),
126                Headers::none(),
127            )
128            .await
129            .map(|_| ())
130    }}
131
132    api_doc! { Container => Stop
133    |
134    /// Stop the container instance.
135    pub async fn stop(&self, opts: &ContainerStopOpts) -> Result<()> {
136        let ep = construct_ep(format!("/containers/{}/stop", self.id), opts.serialize());
137        self.docker
138            .post_string(&ep, Payload::empty(), Headers::none())
139            .await
140            .map(|_| ())
141    }}
142
143    api_doc! { Container => Restart
144    |
145    /// Restart the container instance.
146    pub async fn restart(&self, opts: &ContainerRestartOpts) -> Result<()> {
147        let ep = construct_ep(format!("/containers/{}/restart", self.id), opts.serialize());
148        self.docker
149            .post_string(&ep, Payload::empty(), Headers::none())
150            .await
151            .map(|_| ())
152    }}
153
154    api_doc! { Container => Kill
155    |
156    /// Kill the container instance.
157    pub async fn kill(&self, signal: Option<&str>) -> Result<()> {
158        let mut ep = format!("/containers/{}/kill", self.id);
159        if let Some(sig) = signal {
160            append_query(&mut ep, encoded_pair("signal", sig));
161        }
162        self.docker
163            .post_string(&ep, Payload::empty(), Headers::none())
164            .await
165            .map(|_| ())
166    }}
167
168    api_doc! { Container => Rename
169    |
170    /// Rename the container instance.
171    pub async fn rename(&self, name: &str) -> Result<()> {
172        self.docker
173            .post_string(
174                &format!(
175                    "/containers/{}/rename?{}",
176                    self.id,
177                    encoded_pair("name", name)
178                ),
179                Payload::empty(),
180                Headers::none(),
181            )
182            .await
183            .map(|_| ())
184    }}
185
186    api_doc! { Container => Pause
187    |
188    /// Pause the container instance.
189    pub async fn pause(&self) -> Result<()> {
190        self.docker
191            .post_string(
192                &format!("/containers/{}/pause", self.id),
193                Payload::empty(),
194                Headers::none(),
195            )
196            .await
197            .map(|_| ())
198    }}
199
200    api_doc! { Container => Unpause
201    |
202    /// Unpause the container instance.
203    pub async fn unpause(&self) -> Result<()> {
204        self.docker
205            .post_string(
206                &format!("/containers/{}/unpause", self.id),
207                Payload::empty(),
208                Headers::none(),
209            )
210            .await
211            .map(|_| ())
212    }}
213
214    api_doc! { Container => Wait
215    |
216    /// Wait until the container stops.
217    pub async fn wait(&self) -> Result<models::ContainerWaitResponse> {
218        self.docker
219            .post_json(
220                format!("/containers/{}/wait", self.id),
221                Payload::empty(),
222                Headers::none(),
223            )
224            .await
225    }}
226
227    api_doc! { Exec
228    |
229    /// Execute a command in this container.
230    pub async fn exec(
231        &self,
232        create_opts: &ExecCreateOpts,
233        start_opts: &ExecStartOpts,
234    ) ->  Result<tty::Multiplexer> {
235        Exec::create_and_start(self.docker.clone(), &self.id, create_opts, start_opts).await
236    }}
237
238    api_doc! { Container => Archive
239    |
240    /// Copy a file/folder from the container.  The resulting stream is a tarball of the extracted
241    /// files.
242    ///
243    /// If `path` is not an absolute path, it is relative to the container’s root directory. The
244    /// resource specified by `path` must exist. To assert that the resource is expected to be a
245    /// directory, `path` should end in `/` or `/`. (assuming a path separator of `/`). If `path`
246    /// ends in `/.`  then this indicates that only the contents of the path directory should be
247    /// copied.  A symlink is always resolved to its target.
248    pub fn copy_from(&self, path: impl AsRef<Path>) -> impl Stream<Item = Result<Vec<u8>>> + '_ {
249        self.docker
250            .get_stream(format!(
251                "/containers/{}/archive?{}",
252                self.id,
253                encoded_pair("path", path.as_ref().to_string_lossy())
254            ))
255            .map_ok(|c| c.to_vec())
256    }}
257
258    api_doc! { PutContainer => Archive
259    |
260    /// Copy a byte slice as file into (see `bytes`) the container.
261    ///
262    /// The file will be copied at the given location (see `path`) and will be owned by root
263    /// with access mask 644.
264    pub async fn copy_file_into<P: AsRef<Path>>(&self, path: P, bytes: &[u8]) -> Result<()> {
265        let path = path.as_ref();
266
267        let mut ar = tar::Builder::new(Vec::new());
268        let mut header = tar::Header::new_gnu();
269        header.set_size(bytes.len() as u64);
270        header.set_mode(0o0644);
271        ar.append_data(
272            &mut header,
273            path.to_path_buf()
274                .iter()
275                .skip(1)
276                .collect::<std::path::PathBuf>(),
277            bytes,
278        )?;
279        let data = ar.into_inner()?;
280
281        self.copy_to(Path::new("/"), data.into()).await.map(|_| ())
282    }}
283
284    api_doc! { PutContainer => Archive
285    |
286    /// Copy a tarball (see `body`) to the container.
287    ///
288    /// The tarball will be copied to the container and extracted at the given location (see `path`).
289    pub async fn copy_to(&self, path: &Path, body: Body) -> Result<()> {
290        self.docker
291            .put(
292                &format!(
293                    "/containers/{}/archive?{}",
294                    self.id,
295                    encoded_pair("path", path.to_string_lossy())
296                ),
297                Payload::XTar(body),
298            )
299            .await
300            .map(|_| ())
301    }}
302
303    api_doc! { Container => ArchiveInfo
304    |
305    /// Get information about files in a container.
306    pub async fn stat_file<P>(&self, path: P) -> Result<String>
307    where
308        P: AsRef<Path>,
309    {
310        static PATH_STAT_HEADER: &str = "X-Docker-Container-Path-Stat";
311        let resp = self
312            .docker
313            .head(&format!(
314                "/containers/{}/archive?{}",
315                self.id,
316                encoded_pair("path", path.as_ref().to_string_lossy())
317            ))
318            .await?;
319        if let Some(header) = resp.headers().get(PATH_STAT_HEADER) {
320            let header = header.to_str().map_err(|e| {
321                Error::InvalidResponse(format!("response header was invalid - {e}"))
322            })?;
323
324            base64::decode(header)
325                .map_err(|e| {
326                    Error::InvalidResponse(format!("expected header to be valid base64 - {e}"))
327                })
328                .and_then(|s| {
329                    str::from_utf8(s.as_slice())
330                        .map(str::to_string)
331                        .map_err(|e| {
332                            Error::InvalidResponse(format!(
333                                "expected header to be valid utf8 - {e}"
334                            ))
335                        })
336                })
337        } else {
338            Err(Error::InvalidResponse(format!("missing `{PATH_STAT_HEADER}` header")))
339        }
340    }}
341
342    api_doc! { Image => Commit
343    |
344    /// Create a new image from this container
345    pub async fn commit(&self, opts: &ContainerCommitOpts, config: Option<&models::ContainerConfig>) -> Result<String> {
346        #[derive(Deserialize)]
347        struct IdStruct {
348            #[serde(rename = "Id")]
349            id: String,
350        }
351
352        let payload = if let Some(config) = config {
353            Payload::Json(serde_json::to_string(config)?)
354        } else {
355            Payload::Json("{}".into()) // empty json
356        };
357
358        self.docker
359            .post_json(
360                format!(
361                    "/commit?{}",
362                    opts.with_container(self.id().as_ref())
363                        .serialize()
364                        .unwrap_or_default()
365                ),
366                payload,
367                Headers::none(),
368            )
369            .await
370            .map(|id: IdStruct| id.id)
371    }}
372}
373
374impl Containers {
375    impl_api_ep! {__: Container, resp
376        List -> "/containers/json", models::ContainerSummary
377        Prune -> "/containers/prune", models::ContainerPrune200Response
378    }
379
380    api_doc! { Containers => Create
381    |
382    /// Create a container
383    pub async fn create(&self, opts: &ContainerCreateOpts) -> Result<Container> {
384        let ep = if let Some(name) = opts.name() {
385            construct_ep("/containers/create", Some(encoded_pair("name", name)))
386        } else {
387            "/containers/create".to_owned()
388        };
389        self.docker
390            .post_json(&ep, Payload::Json(opts.serialize_vec()?), Headers::none())
391            .await
392            .map(|resp: rs_docker_api_stubs::models::ContainerCreateResponse| {
393                Container::new(self.docker.clone(), resp.id)
394            })
395    }}
396}