1use 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
21pub 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 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 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 let body_result = opts.serialize();
96
97 let container_endpoint = format!("/containers/{}/exec", container_id);
100
101 Box::pin(
102 async move {
103 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 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 pub fn start(&self) -> impl Stream<Item = Result<tty::TtyChunk>> + 'docker {
142 let docker = self.docker;
145 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 pub async fn inspect(&self) -> Result<ExecDetails> {
166 self.docker
167 .get_json(&format!("/exec/{}/json", &self.id)[..])
168 .await
169 }
170
171 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 pub fn builder() -> ExecContainerOptionsBuilder {
199 ExecContainerOptionsBuilder::default()
200 }
201
202 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 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 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 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 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#[derive(Serialize, Debug)]
287pub struct ExecResizeOptions {
288 params: HashMap<&'static str, Value>,
289}
290
291impl ExecResizeOptions {
292 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 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}