1use std::{collections::HashMap, hash::Hash, io, iter::Peekable, path::Path, time::Duration};
6
7use futures_util::{
8 io::{AsyncRead, AsyncWrite},
9 stream::Stream,
10 TryStreamExt,
11};
12use hyper::Body;
13use mime::Mime;
14use serde::{Deserialize, Serialize};
15use serde_json::{json, Map, Value};
16use url::form_urlencoded;
17
18use crate::{
19 docker::Docker,
20 errors::{Error, Result},
21 exec::{Exec, ExecContainerOptions},
22 image::ContainerConfig,
23 network::NetworkSettings,
24 transport::Payload,
25 tty::{self, Multiplexer as TtyMultiPlexer},
26};
27
28#[cfg(feature = "chrono")]
29use crate::datetime::datetime_from_unix_timestamp;
30#[cfg(feature = "chrono")]
31use chrono::{DateTime, Utc};
32
33pub struct Container<'docker> {
37 docker: &'docker Docker,
38 id: String,
39}
40
41impl<'docker> Container<'docker> {
42 pub fn new<S>(
44 docker: &'docker Docker,
45 id: S,
46 ) -> Self
47 where
48 S: Into<String>,
49 {
50 Container {
51 docker,
52 id: id.into(),
53 }
54 }
55
56 pub fn id(&self) -> &str {
58 &self.id
59 }
60
61 pub async fn inspect(&self) -> Result<ContainerDetails> {
65 self.docker
66 .get_json::<ContainerDetails>(&format!("/containers/{}/json", self.id)[..])
67 .await
68 }
69
70 pub async fn top(
74 &self,
75 psargs: Option<&str>,
76 ) -> Result<Top> {
77 let mut path = vec![format!("/containers/{}/top", self.id)];
78 if let Some(ref args) = psargs {
79 let encoded = form_urlencoded::Serializer::new(String::new())
80 .append_pair("ps_args", args)
81 .finish();
82 path.push(encoded)
83 }
84 self.docker.get_json(&path.join("?")).await
85 }
86
87 pub fn logs(
91 &self,
92 opts: &LogsOptions,
93 ) -> impl Stream<Item = Result<tty::TtyChunk>> + Unpin + 'docker {
94 let mut path = vec![format!("/containers/{}/logs", self.id)];
95 if let Some(query) = opts.serialize() {
96 path.push(query)
97 }
98
99 let stream = Box::pin(self.docker.stream_get(path.join("?")));
100
101 Box::pin(tty::decode(stream))
102 }
103
104 async fn attach_raw(&self) -> Result<impl AsyncRead + AsyncWrite + Send + 'docker> {
106 self.docker
107 .stream_post_upgrade(
108 format!(
109 "/containers/{}/attach?stream=1&stdout=1&stderr=1&stdin=1",
110 self.id
111 ),
112 None,
113 )
114 .await
115 }
116
117 pub async fn attach(&self) -> Result<TtyMultiPlexer<'docker>> {
127 let tcp_stream = self.attach_raw().await?;
128
129 Ok(TtyMultiPlexer::new(tcp_stream))
130 }
131
132 pub async fn changes(&self) -> Result<Vec<Change>> {
136 self.docker
137 .get_json::<Vec<Change>>(&format!("/containers/{}/changes", self.id)[..])
138 .await
139 }
140
141 pub fn export(&self) -> impl Stream<Item = Result<Vec<u8>>> + 'docker {
145 self.docker
146 .stream_get(format!("/containers/{}/export", self.id))
147 .map_ok(|c| c.to_vec())
148 }
149
150 pub fn stats(&self) -> impl Stream<Item = Result<Stats>> + Unpin + 'docker {
154 let codec = futures_codec::LinesCodec {};
155
156 let reader = Box::pin(
157 self.docker
158 .stream_get(format!("/containers/{}/stats", self.id))
159 .map_err(|e| io::Error::new(io::ErrorKind::Other, e)),
160 )
161 .into_async_read();
162
163 Box::pin(
164 futures_codec::FramedRead::new(reader, codec)
165 .map_err(Error::IO)
166 .and_then(|s: String| async move {
167 serde_json::from_str(&s).map_err(Error::SerdeJsonError)
168 }),
169 )
170 }
171
172 pub async fn start(&self) -> Result<()> {
176 self.docker
177 .post(&format!("/containers/{}/start", self.id)[..], None)
178 .await?;
179 Ok(())
180 }
181
182 pub async fn stop(
186 &self,
187 wait: Option<Duration>,
188 ) -> Result<()> {
189 let mut path = vec![format!("/containers/{}/stop", self.id)];
190 if let Some(w) = wait {
191 let encoded = form_urlencoded::Serializer::new(String::new())
192 .append_pair("t", &w.as_secs().to_string())
193 .finish();
194
195 path.push(encoded)
196 }
197 self.docker.post(&path.join("?"), None).await?;
198 Ok(())
199 }
200
201 pub async fn restart(
205 &self,
206 wait: Option<Duration>,
207 ) -> Result<()> {
208 let mut path = vec![format!("/containers/{}/restart", self.id)];
209 if let Some(w) = wait {
210 let encoded = form_urlencoded::Serializer::new(String::new())
211 .append_pair("t", &w.as_secs().to_string())
212 .finish();
213 path.push(encoded)
214 }
215 self.docker.post(&path.join("?"), None).await?;
216 Ok(())
217 }
218
219 pub async fn kill(
223 &self,
224 signal: Option<&str>,
225 ) -> Result<()> {
226 let mut path = vec![format!("/containers/{}/kill", self.id)];
227 if let Some(sig) = signal {
228 let encoded = form_urlencoded::Serializer::new(String::new())
229 .append_pair("signal", &sig.to_owned())
230 .finish();
231 path.push(encoded)
232 }
233 self.docker.post(&path.join("?"), None).await?;
234 Ok(())
235 }
236
237 pub async fn rename(
241 &self,
242 name: &str,
243 ) -> Result<()> {
244 let query = form_urlencoded::Serializer::new(String::new())
245 .append_pair("name", name)
246 .finish();
247 self.docker
248 .post(
249 &format!("/containers/{}/rename?{}", self.id, query)[..],
250 None,
251 )
252 .await?;
253 Ok(())
254 }
255
256 pub async fn pause(&self) -> Result<()> {
260 self.docker
261 .post(&format!("/containers/{}/pause", self.id)[..], None)
262 .await?;
263 Ok(())
264 }
265
266 pub async fn unpause(&self) -> Result<()> {
270 self.docker
271 .post(&format!("/containers/{}/unpause", self.id)[..], None)
272 .await?;
273 Ok(())
274 }
275
276 pub async fn wait(&self) -> Result<Exit> {
280 self.docker
281 .post_json(format!("/containers/{}/wait", self.id), Payload::None)
282 .await
283 }
284
285 pub async fn delete(&self) -> Result<()> {
291 self.docker
292 .delete(&format!("/containers/{}", self.id)[..])
293 .await?;
294 Ok(())
295 }
296
297 pub async fn remove(
301 &self,
302 opts: RmContainerOptions,
303 ) -> Result<()> {
304 let mut path = vec![format!("/containers/{}", self.id)];
305 if let Some(query) = opts.serialize() {
306 path.push(query)
307 }
308 self.docker.delete(&path.join("?")).await?;
309 Ok(())
310 }
311
312 pub fn exec(
316 &self,
317 opts: &ExecContainerOptions,
318 ) -> impl Stream<Item = Result<tty::TtyChunk>> + Unpin + 'docker {
319 Exec::create_and_start(self.docker, &self.id, opts)
320 }
321
322 pub fn copy_from(
333 &self,
334 path: &Path,
335 ) -> impl Stream<Item = Result<Vec<u8>>> + 'docker {
336 let path_arg = form_urlencoded::Serializer::new(String::new())
337 .append_pair("path", &path.to_string_lossy())
338 .finish();
339
340 let endpoint = format!("/containers/{}/archive?{}", self.id, path_arg);
341 self.docker.stream_get(endpoint).map_ok(|c| c.to_vec())
342 }
343
344 pub async fn copy_file_into<P: AsRef<Path>>(
351 &self,
352 path: P,
353 bytes: &[u8],
354 ) -> Result<()> {
355 let path = path.as_ref();
356
357 let mut ar = tar::Builder::new(Vec::new());
358 let mut header = tar::Header::new_gnu();
359 header.set_size(bytes.len() as u64);
360 header.set_mode(0o0644);
361 ar.append_data(
362 &mut header,
363 path.to_path_buf()
364 .iter()
365 .skip(1)
366 .collect::<std::path::PathBuf>(),
367 bytes,
368 )?;
369 let data = ar.into_inner()?;
370
371 self.copy_to(Path::new("/"), data.into()).await?;
372 Ok(())
373 }
374
375 pub async fn copy_to(
381 &self,
382 path: &Path,
383 body: Body,
384 ) -> Result<()> {
385 let path_arg = form_urlencoded::Serializer::new(String::new())
386 .append_pair("path", &path.to_string_lossy())
387 .finish();
388
389 let mime = "application/x-tar".parse::<Mime>().unwrap();
390
391 self.docker
392 .put(
393 &format!("/containers/{}/archive?{}", self.id, path_arg),
394 Some((body, mime)),
395 )
396 .await?;
397 Ok(())
398 }
399}
400
401pub struct Containers<'docker> {
405 docker: &'docker Docker,
406}
407
408impl<'docker> Containers<'docker> {
409 pub fn new(docker: &'docker Docker) -> Self {
411 Containers { docker }
412 }
413
414 pub async fn list(
418 &self,
419 opts: &ContainerListOptions,
420 ) -> Result<Vec<ContainerInfo>> {
421 let mut path = vec!["/containers/json".to_owned()];
422 if let Some(query) = opts.serialize() {
423 path.push(query)
424 }
425 self.docker
426 .get_json::<Vec<ContainerInfo>>(&path.join("?"))
427 .await
428 }
429
430 pub fn get<S>(
432 &self,
433 name: S,
434 ) -> Container<'docker>
435 where
436 S: Into<String>,
437 {
438 Container::new(self.docker, name)
439 }
440
441 pub async fn create(
443 &self,
444 opts: &ContainerOptions,
445 ) -> Result<ContainerCreateInfo> {
446 let body: Body = opts.serialize()?.into();
447 let mut path = vec!["/containers/create".to_owned()];
448
449 if let Some(ref name) = opts.name {
450 path.push(
451 form_urlencoded::Serializer::new(String::new())
452 .append_pair("name", name)
453 .finish(),
454 );
455 }
456
457 self.docker
458 .post_json(&path.join("?"), Some((body, mime::APPLICATION_JSON)))
459 .await
460 }
461}
462
463#[derive(Default, Debug)]
465pub struct ContainerListOptions {
466 params: HashMap<&'static str, String>,
467}
468
469impl ContainerListOptions {
470 pub fn builder() -> ContainerListOptionsBuilder {
472 ContainerListOptionsBuilder::default()
473 }
474
475 pub fn serialize(&self) -> Option<String> {
477 if self.params.is_empty() {
478 None
479 } else {
480 Some(
481 form_urlencoded::Serializer::new(String::new())
482 .extend_pairs(&self.params)
483 .finish(),
484 )
485 }
486 }
487}
488
489pub enum ContainerFilter {
491 ExitCode(u64),
492 Status(String),
493 LabelName(String),
494 Label(String, String),
495 Name(String),
496}
497
498#[derive(Default)]
500pub struct ContainerListOptionsBuilder {
501 params: HashMap<&'static str, String>,
502}
503
504impl ContainerListOptionsBuilder {
505 pub fn filter(
506 &mut self,
507 filters: Vec<ContainerFilter>,
508 ) -> &mut Self {
509 let mut param: HashMap<&str, Vec<String>> = HashMap::new();
510 for f in filters {
511 let (key, value) = match f {
512 ContainerFilter::ExitCode(c) => ("exited", c.to_string()),
513 ContainerFilter::Status(s) => ("status", s),
514 ContainerFilter::LabelName(n) => ("label", n),
515 ContainerFilter::Label(n, v) => ("label", format!("{}={}", n, v)),
516 ContainerFilter::Name(n) => ("name", n.to_string()),
517 };
518
519 param.entry(key).or_insert_with(Vec::new).push(value);
520 }
521 self.params
524 .insert("filters", serde_json::to_string(¶m).unwrap());
525 self
526 }
527
528 pub fn all(&mut self) -> &mut Self {
529 self.params.insert("all", "true".to_owned());
530 self
531 }
532
533 pub fn since(
534 &mut self,
535 since: &str,
536 ) -> &mut Self {
537 self.params.insert("since", since.to_owned());
538 self
539 }
540
541 pub fn before(
542 &mut self,
543 before: &str,
544 ) -> &mut Self {
545 self.params.insert("before", before.to_owned());
546 self
547 }
548
549 pub fn sized(&mut self) -> &mut Self {
550 self.params.insert("size", "true".to_owned());
551 self
552 }
553
554 pub fn build(&self) -> ContainerListOptions {
555 ContainerListOptions {
556 params: self.params.clone(),
557 }
558 }
559}
560
561#[derive(Serialize, Debug)]
563pub struct ContainerOptions {
564 pub name: Option<String>,
565 params: HashMap<&'static str, Value>,
566}
567
568fn insert<'a, I, V>(
571 key_path: &mut Peekable<I>,
572 value: &V,
573 parent_node: &mut Value,
574) where
575 V: Serialize,
576 I: Iterator<Item = &'a str>,
577{
578 let local_key = key_path.next().unwrap();
579
580 if key_path.peek().is_some() {
581 let node = parent_node
582 .as_object_mut()
583 .unwrap()
584 .entry(local_key.to_string())
585 .or_insert(Value::Object(Map::new()));
586
587 insert(key_path, value, node);
588 } else {
589 parent_node
590 .as_object_mut()
591 .unwrap()
592 .insert(local_key.to_string(), serde_json::to_value(value).unwrap());
593 }
594}
595
596impl ContainerOptions {
597 pub fn builder(name: &str) -> ContainerOptionsBuilder {
599 ContainerOptionsBuilder::new(name)
600 }
601
602 pub fn serialize(&self) -> Result<String> {
604 serde_json::to_string(&self.to_json()).map_err(Error::from)
605 }
606
607 fn to_json(&self) -> Value {
608 let mut body_members = Map::new();
609 body_members.insert("HostConfig".to_string(), Value::Object(Map::new()));
612 let mut body = Value::Object(body_members);
613 self.parse_from(&self.params, &mut body);
614 body
615 }
616
617 pub fn parse_from<'a, K, V>(
618 &self,
619 params: &'a HashMap<K, V>,
620 body: &mut Value,
621 ) where
622 &'a HashMap<K, V>: IntoIterator,
623 K: ToString + Eq + Hash,
624 V: Serialize,
625 {
626 for (k, v) in params.iter() {
627 let key_string = k.to_string();
628 insert(&mut key_string.split('.').peekable(), v, body)
629 }
630 }
631}
632
633#[derive(Default)]
634pub struct ContainerOptionsBuilder {
635 name: Option<String>,
636 params: HashMap<&'static str, Value>,
637}
638
639impl ContainerOptionsBuilder {
640 pub(crate) fn new(image: &str) -> Self {
641 let mut params = HashMap::new();
642
643 params.insert("Image", Value::String(image.to_owned()));
644 ContainerOptionsBuilder { name: None, params }
645 }
646
647 pub fn name(
648 &mut self,
649 name: &str,
650 ) -> &mut Self {
651 self.name = Some(name.to_owned());
652 self
653 }
654
655 pub fn working_dir(
657 &mut self,
658 working_dir: &str,
659 ) -> &mut Self {
660 self.params.insert("WorkingDir", json!(working_dir));
661 self
662 }
663
664 pub fn volumes(
666 &mut self,
667 volumes: Vec<&str>,
668 ) -> &mut Self {
669 self.params.insert("HostConfig.Binds", json!(volumes));
670 self
671 }
672
673 pub fn publish_all_ports(&mut self) -> &mut Self {
675 self.params
676 .insert("HostConfig.PublishAllPorts", json!(true));
677 self
678 }
679
680 pub fn expose(
681 &mut self,
682 srcport: u32,
683 protocol: &str,
684 hostport: u32,
685 ) -> &mut Self {
686 let mut exposedport: HashMap<String, String> = HashMap::new();
687 exposedport.insert("HostPort".to_string(), hostport.to_string());
688
689 let mut port_bindings: HashMap<String, Value> = HashMap::new();
692 for (key, val) in self
693 .params
694 .get("HostConfig.PortBindings")
695 .unwrap_or(&json!(null))
696 .as_object()
697 .unwrap_or(&Map::new())
698 .iter()
699 {
700 port_bindings.insert(key.to_string(), json!(val));
701 }
702 port_bindings.insert(
703 format!("{}/{}", srcport, protocol),
704 json!(vec![exposedport]),
705 );
706
707 self.params
708 .insert("HostConfig.PortBindings", json!(port_bindings));
709
710 let mut exposed_ports: HashMap<String, Value> = HashMap::new();
712 let empty_config: HashMap<String, Value> = HashMap::new();
713 for key in port_bindings.keys() {
714 exposed_ports.insert(key.to_string(), json!(empty_config));
715 }
716
717 self.params.insert("ExposedPorts", json!(exposed_ports));
718
719 self
720 }
721
722 pub fn publish(
724 &mut self,
725 srcport: u32,
726 protocol: &str,
727 ) -> &mut Self {
728 let mut exposed_port_bindings: HashMap<String, Value> = HashMap::new();
732 for (key, val) in self
733 .params
734 .get("ExposedPorts")
735 .unwrap_or(&json!(null))
736 .as_object()
737 .unwrap_or(&Map::new())
738 .iter()
739 {
740 exposed_port_bindings.insert(key.to_string(), json!(val));
741 }
742 exposed_port_bindings.insert(format!("{}/{}", srcport, protocol), json!({}));
743
744 let mut exposed_ports: HashMap<String, Value> = HashMap::new();
746 let empty_config: HashMap<String, Value> = HashMap::new();
747 for key in exposed_port_bindings.keys() {
748 exposed_ports.insert(key.to_string(), json!(empty_config));
749 }
750
751 self.params.insert("ExposedPorts", json!(exposed_ports));
752
753 self
754 }
755
756 pub fn links(
757 &mut self,
758 links: Vec<&str>,
759 ) -> &mut Self {
760 self.params.insert("HostConfig.Links", json!(links));
761 self
762 }
763
764 pub fn memory(
765 &mut self,
766 memory: u64,
767 ) -> &mut Self {
768 self.params.insert("HostConfig.Memory", json!(memory));
769 self
770 }
771
772 pub fn memory_swap(
774 &mut self,
775 memory_swap: i64,
776 ) -> &mut Self {
777 self.params
778 .insert("HostConfig.MemorySwap", json!(memory_swap));
779 self
780 }
781
782 pub fn nano_cpus(
787 &mut self,
788 nano_cpus: u64,
789 ) -> &mut Self {
790 self.params.insert("HostConfig.NanoCpus", json!(nano_cpus));
791 self
792 }
793
794 pub fn cpus(
798 &mut self,
799 cpus: f64,
800 ) -> &mut Self {
801 self.nano_cpus((1_000_000_000.0 * cpus) as u64)
802 }
803
804 pub fn cpu_shares(
807 &mut self,
808 cpu_shares: u32,
809 ) -> &mut Self {
810 self.params
811 .insert("HostConfig.CpuShares", json!(cpu_shares));
812 self
813 }
814
815 pub fn labels(
816 &mut self,
817 labels: &HashMap<&str, &str>,
818 ) -> &mut Self {
819 self.params.insert("Labels", json!(labels));
820 self
821 }
822
823 pub fn attach_stdin(
825 &mut self,
826 attach: bool,
827 ) -> &mut Self {
828 self.params.insert("AttachStdin", json!(attach));
829 self.params.insert("OpenStdin", json!(attach));
830 self
831 }
832
833 pub fn attach_stdout(
835 &mut self,
836 attach: bool,
837 ) -> &mut Self {
838 self.params.insert("AttachStdout", json!(attach));
839 self
840 }
841
842 pub fn attach_stderr(
844 &mut self,
845 attach: bool,
846 ) -> &mut Self {
847 self.params.insert("AttachStderr", json!(attach));
848 self
849 }
850
851 pub fn tty(
853 &mut self,
854 tty: bool,
855 ) -> &mut Self {
856 self.params.insert("Tty", json!(tty));
857 self
858 }
859
860 pub fn extra_hosts(
861 &mut self,
862 hosts: Vec<&str>,
863 ) -> &mut Self {
864 self.params.insert("HostConfig.ExtraHosts", json!(hosts));
865 self
866 }
867
868 pub fn volumes_from(
869 &mut self,
870 volumes: Vec<&str>,
871 ) -> &mut Self {
872 self.params.insert("HostConfig.VolumesFrom", json!(volumes));
873 self
874 }
875
876 pub fn network_mode(
877 &mut self,
878 network: &str,
879 ) -> &mut Self {
880 self.params.insert("HostConfig.NetworkMode", json!(network));
881 self
882 }
883
884 pub fn env<E, S>(
885 &mut self,
886 envs: E,
887 ) -> &mut Self
888 where
889 S: AsRef<str> + Serialize,
890 E: AsRef<[S]> + Serialize,
891 {
892 self.params.insert("Env", json!(envs));
893 self
894 }
895
896 pub fn cmd(
897 &mut self,
898 cmds: Vec<&str>,
899 ) -> &mut Self {
900 self.params.insert("Cmd", json!(cmds));
901 self
902 }
903
904 pub fn entrypoint(
905 &mut self,
906 entrypoint: &str,
907 ) -> &mut Self {
908 self.params.insert("Entrypoint", json!(entrypoint));
909 self
910 }
911
912 pub fn capabilities(
913 &mut self,
914 capabilities: Vec<&str>,
915 ) -> &mut Self {
916 self.params.insert("HostConfig.CapAdd", json!(capabilities));
917 self
918 }
919
920 pub fn devices(
921 &mut self,
922 devices: Vec<HashMap<String, String>>,
923 ) -> &mut Self {
924 self.params.insert("HostConfig.Devices", json!(devices));
925 self
926 }
927
928 pub fn log_driver(
929 &mut self,
930 log_driver: &str,
931 ) -> &mut Self {
932 self.params
933 .insert("HostConfig.LogConfig.Type", json!(log_driver));
934 self
935 }
936
937 pub fn restart_policy(
938 &mut self,
939 name: &str,
940 maximum_retry_count: u64,
941 ) -> &mut Self {
942 self.params
943 .insert("HostConfig.RestartPolicy.Name", json!(name));
944 if name == "on-failure" {
945 self.params.insert(
946 "HostConfig.RestartPolicy.MaximumRetryCount",
947 json!(maximum_retry_count),
948 );
949 }
950 self
951 }
952
953 pub fn auto_remove(
954 &mut self,
955 set: bool,
956 ) -> &mut Self {
957 self.params.insert("HostConfig.AutoRemove", json!(set));
958 self
959 }
960
961 pub fn stop_signal(
963 &mut self,
964 sig: &str,
965 ) -> &mut Self {
966 self.params.insert("StopSignal", json!(sig));
967 self
968 }
969
970 pub fn stop_signal_num(
972 &mut self,
973 sig: u64,
974 ) -> &mut Self {
975 self.params.insert("StopSignal", json!(sig));
976 self
977 }
978
979 pub fn stop_timeout(
981 &mut self,
982 timeout: Duration,
983 ) -> &mut Self {
984 self.params.insert("StopTimeout", json!(timeout.as_secs()));
985 self
986 }
987
988 pub fn userns_mode(
989 &mut self,
990 mode: &str,
991 ) -> &mut Self {
992 self.params.insert("HostConfig.UsernsMode", json!(mode));
993 self
994 }
995
996 pub fn privileged(
997 &mut self,
998 set: bool,
999 ) -> &mut Self {
1000 self.params.insert("HostConfig.Privileged", json!(set));
1001 self
1002 }
1003
1004 pub fn user(
1005 &mut self,
1006 user: &str,
1007 ) -> &mut Self {
1008 self.params.insert("User", json!(user));
1009 self
1010 }
1011
1012 pub fn build(&self) -> ContainerOptions {
1013 ContainerOptions {
1014 name: self.name.clone(),
1015 params: self.params.clone(),
1016 }
1017 }
1018}
1019
1020#[derive(Default, Debug)]
1022pub struct LogsOptions {
1023 params: HashMap<&'static str, String>,
1024}
1025
1026impl LogsOptions {
1027 pub fn builder() -> LogsOptionsBuilder {
1029 LogsOptionsBuilder::default()
1030 }
1031
1032 pub fn serialize(&self) -> Option<String> {
1034 if self.params.is_empty() {
1035 None
1036 } else {
1037 Some(
1038 form_urlencoded::Serializer::new(String::new())
1039 .extend_pairs(&self.params)
1040 .finish(),
1041 )
1042 }
1043 }
1044}
1045
1046#[derive(Default)]
1048pub struct LogsOptionsBuilder {
1049 params: HashMap<&'static str, String>,
1050}
1051
1052impl LogsOptionsBuilder {
1053 pub fn follow(
1054 &mut self,
1055 f: bool,
1056 ) -> &mut Self {
1057 self.params.insert("follow", f.to_string());
1058 self
1059 }
1060
1061 pub fn stdout(
1062 &mut self,
1063 s: bool,
1064 ) -> &mut Self {
1065 self.params.insert("stdout", s.to_string());
1066 self
1067 }
1068
1069 pub fn stderr(
1070 &mut self,
1071 s: bool,
1072 ) -> &mut Self {
1073 self.params.insert("stderr", s.to_string());
1074 self
1075 }
1076
1077 pub fn timestamps(
1078 &mut self,
1079 t: bool,
1080 ) -> &mut Self {
1081 self.params.insert("timestamps", t.to_string());
1082 self
1083 }
1084
1085 pub fn tail(
1087 &mut self,
1088 how_many: &str,
1089 ) -> &mut Self {
1090 self.params.insert("tail", how_many.to_owned());
1091 self
1092 }
1093
1094 #[cfg(feature = "chrono")]
1095 pub fn since<Tz>(
1096 &mut self,
1097 timestamp: &chrono::DateTime<Tz>,
1098 ) -> &mut Self
1099 where
1100 Tz: chrono::TimeZone,
1101 {
1102 self.params
1103 .insert("since", timestamp.timestamp().to_string());
1104 self
1105 }
1106
1107 #[cfg(not(feature = "chrono"))]
1108 pub fn since(
1109 &mut self,
1110 timestamp: i64,
1111 ) -> &mut Self {
1112 self.params.insert("since", timestamp.to_string());
1113 self
1114 }
1115
1116 pub fn build(&self) -> LogsOptions {
1117 LogsOptions {
1118 params: self.params.clone(),
1119 }
1120 }
1121}
1122
1123#[derive(Default, Debug)]
1125pub struct RmContainerOptions {
1126 params: HashMap<&'static str, String>,
1127}
1128
1129impl RmContainerOptions {
1130 pub fn builder() -> RmContainerOptionsBuilder {
1132 RmContainerOptionsBuilder::default()
1133 }
1134
1135 pub fn serialize(&self) -> Option<String> {
1137 if self.params.is_empty() {
1138 None
1139 } else {
1140 Some(
1141 form_urlencoded::Serializer::new(String::new())
1142 .extend_pairs(&self.params)
1143 .finish(),
1144 )
1145 }
1146 }
1147}
1148
1149#[derive(Default)]
1151pub struct RmContainerOptionsBuilder {
1152 params: HashMap<&'static str, String>,
1153}
1154
1155impl RmContainerOptionsBuilder {
1156 pub fn force(
1157 &mut self,
1158 f: bool,
1159 ) -> &mut Self {
1160 self.params.insert("force", f.to_string());
1161 self
1162 }
1163
1164 pub fn volumes(
1165 &mut self,
1166 s: bool,
1167 ) -> &mut Self {
1168 self.params.insert("v", s.to_string());
1169 self
1170 }
1171
1172 pub fn build(&self) -> RmContainerOptions {
1173 RmContainerOptions {
1174 params: self.params.clone(),
1175 }
1176 }
1177}
1178
1179#[derive(Clone, Debug, Serialize, Deserialize)]
1180#[serde(rename_all = "PascalCase")]
1181pub struct ContainerInfo {
1182 #[cfg(feature = "chrono")]
1183 #[serde(deserialize_with = "datetime_from_unix_timestamp")]
1184 pub created: DateTime<Utc>,
1185 #[cfg(not(feature = "chrono"))]
1186 pub created: u64,
1187 pub command: String,
1188 pub id: String,
1189 pub image: String,
1190 #[serde(rename = "ImageID")]
1191 pub image_id: String,
1192 pub labels: HashMap<String, String>,
1193 pub names: Vec<String>,
1194 pub ports: Vec<Port>,
1195 pub state: String,
1196 pub status: String,
1197 pub size_rw: Option<i64>,
1198 pub size_root_fs: Option<i64>,
1199}
1200
1201#[derive(Clone, Debug, Serialize, Deserialize)]
1202#[serde(rename_all = "PascalCase")]
1203pub struct ContainerDetails {
1204 pub id: String,
1205 #[cfg(feature = "chrono")]
1206 pub created: DateTime<Utc>,
1207 #[cfg(not(feature = "chrono"))]
1208 pub created: String,
1209 pub path: String,
1210 pub args: Vec<String>,
1211 pub state: State,
1212 pub image: String,
1213 pub resolv_conf_path: String,
1214 pub hostname_path: String,
1215 pub hosts_path: String,
1216 pub log_path: String,
1217 pub name: String,
1218 pub restart_count: i64,
1219 pub driver: String,
1220 pub platform: String,
1221 pub mount_label: String,
1222 pub process_label: String,
1223 pub app_armor_profile: String,
1224 #[serde(rename = "ExecIDs")]
1225 pub exec_ids: Option<Vec<String>>,
1226 pub host_config: HostConfig,
1227 pub graph_driver: GraphDriverData,
1228 pub mounts: Vec<Mount>,
1229 pub config: ContainerConfig,
1230 pub network_settings: NetworkSettings,
1231}
1232
1233#[derive(Clone, Debug, Serialize, Deserialize)]
1234#[serde(rename_all = "PascalCase")]
1235pub struct GraphDriverData {
1236 pub name: String,
1237 pub data: HashMap<String, String>,
1238}
1239
1240#[derive(Clone, Debug, Serialize, Deserialize)]
1241#[serde(rename_all = "PascalCase")]
1242pub struct Mount {
1243 pub source: String,
1244 pub destination: String,
1245 pub mode: String,
1246 #[serde(rename = "RW")]
1247 pub rw: bool,
1248}
1249
1250#[derive(Clone, Debug, Serialize, Deserialize)]
1251#[serde(rename_all = "PascalCase")]
1252pub struct State {
1253 pub error: String,
1254 pub exit_code: u64,
1255 #[cfg(feature = "chrono")]
1256 pub finished_at: DateTime<Utc>,
1257 #[cfg(not(feature = "chrono"))]
1258 pub finished_at: String,
1259 #[serde(rename = "OOMKilled")]
1260 pub oom_killed: bool,
1261 pub paused: bool,
1262 pub pid: u64,
1263 pub restarting: bool,
1264 pub running: bool,
1265 #[cfg(feature = "chrono")]
1266 pub started_at: DateTime<Utc>,
1267 #[cfg(not(feature = "chrono"))]
1268 pub started_at: String,
1269 pub status: String,
1270}
1271
1272#[derive(Clone, Debug, Serialize, Deserialize)]
1273#[serde(rename_all = "PascalCase")]
1274pub struct HostConfig {
1275 pub cpu_shares: Option<i64>,
1276 pub memory: Option<i64>,
1277 pub cgroup_parent: Option<String>,
1278 pub blkio_weight_device: Option<Vec<ThrottleDevice>>,
1279 pub blkio_device_read_bps: Option<Vec<ThrottleDevice>>,
1280 pub blkio_device_write_bps: Option<Vec<ThrottleDevice>>,
1281 #[serde(rename = "BlkioDeviceReadIOps")]
1282 pub blkio_device_read_iops: Option<Vec<ThrottleDevice>>,
1283 #[serde(rename = "BlkioDeviceWriteIOps")]
1284 pub blkio_device_write_iops: Option<Vec<ThrottleDevice>>,
1285 pub cpu_period: Option<i64>,
1286 pub cpu_quota: Option<i64>,
1287 pub cpu_realtime_period: Option<i64>,
1288 pub cpu_realtime_runtime: Option<i64>,
1289 pub cpuset_cpus: Option<String>,
1290 pub cpuset_mems: Option<String>,
1291 pub devices: Option<Vec<DeviceMapping>>,
1292 pub device_cgroup_rules: Option<String>,
1293 pub device_requests: Option<Vec<DeviceRequest>>,
1294 #[serde(rename = "KernelMemoryTCP")]
1295 pub kernel_memory_tcp: i64,
1296 pub memory_reservation: Option<i64>,
1297 pub memory_swap: Option<i64>,
1298 pub memory_swappiness: Option<i64>,
1299 #[serde(rename = "NanoCPUs")]
1300 pub nano_cpus: Option<i64>,
1301 pub oom_kill_disable: bool,
1302 pub init: Option<bool>,
1303 pub pids_limit: Option<i64>,
1304 pub ulimits: Option<Vec<Ulimit>>,
1305 pub cpu_count: i64,
1306 pub cpu_percent: i64,
1307 #[serde(rename = "IOMaximumIOps")]
1308 pub io_maximum_iops: u64,
1309 #[serde(rename = "IOMaximumBandwith")]
1310 pub io_maximum_bandwith: Option<u64>,
1311 pub binds: Option<Vec<String>>,
1312 #[serde(rename = "ContainerIDFile")]
1313 pub container_id_file: String,
1314 pub log_config: LogConfig,
1315 pub network_mode: String,
1316 pub port_bindings: Option<PortMap>,
1317 pub restart_policy: RestartPolicy,
1318 pub auto_remove: bool,
1319 pub volume_driver: String,
1320 pub volumes_from: Option<Vec<String>>,
1321 pub mounts: Option<Vec<Mount>>,
1322 pub cap_add: Option<Vec<String>>,
1323 pub cap_drop: Option<Vec<String>>,
1324 pub dns: Option<Vec<String>>,
1325 pub dns_options: Option<Vec<String>>,
1326 pub dns_search: Option<Vec<String>>,
1327 pub extra_hosts: Option<Vec<String>>,
1328 pub group_add: Option<Vec<String>>,
1329 pub ipc_mode: String,
1330 pub cgroup: String,
1331 pub links: Option<Vec<String>>,
1332 pub oom_score_adj: i64,
1333 pub pid_mode: Option<String>,
1334 pub privileged: bool,
1335 pub publish_all_ports: bool,
1336 pub readonly_rootfs: Option<bool>,
1337 pub security_opt: Option<Vec<String>>,
1338 pub storage_opt: Option<HashMap<String, String>>,
1339 pub tmpfs: Option<HashMap<String, String>>,
1340 #[serde(rename = "UTSMode")]
1341 pub uts_mode: String,
1342 pub userns_mode: String,
1343 pub shm_size: u64,
1344 pub sysctls: Option<HashMap<String, String>>,
1345 pub runtime: String,
1346 pub console_size: Option<Vec<u64>>,
1347 pub isolation: String,
1348 pub masked_paths: Option<Vec<String>>,
1349 pub readonly_paths: Option<Vec<String>>,
1350}
1351
1352#[derive(Clone, Debug, Serialize, Deserialize)]
1353#[serde(rename_all = "PascalCase")]
1354pub struct ThrottleDevice {
1355 pub path: String,
1356 pub rate: u64,
1357}
1358
1359#[derive(Clone, Debug, Serialize, Deserialize)]
1360#[serde(rename_all = "PascalCase")]
1361pub struct RestartPolicy {
1362 pub name: String,
1363 pub maximum_retry_count: u64,
1364}
1365
1366pub type PortMap = HashMap<String, Option<Vec<PortBinding>>>;
1367
1368#[derive(Clone, Debug, Serialize, Deserialize)]
1369#[serde(rename_all = "PascalCase")]
1370pub struct PortBinding {
1371 pub host_ip: String,
1372 pub host_port: String,
1373}
1374
1375#[derive(Clone, Debug, Serialize, Deserialize)]
1376pub struct LogConfig {
1377 #[serde(rename = "Type")]
1378 pub type_: String,
1379 #[serde(rename = "Config")]
1380 pub config: HashMap<String, String>,
1381}
1382
1383#[derive(Clone, Debug, Serialize, Deserialize)]
1384#[serde(rename_all = "PascalCase")]
1385pub struct Ulimit {
1386 pub name: String,
1387 pub soft: u64,
1388 pub hard: u64,
1389}
1390
1391#[derive(Clone, Debug, Serialize, Deserialize)]
1392#[serde(rename_all = "PascalCase")]
1393pub struct DeviceMapping {
1394 pub path_on_host: Option<String>,
1395 pub path_in_container: Option<String>,
1396 pub cgroup_permissions: Option<String>,
1397}
1398
1399#[derive(Clone, Debug, Serialize, Deserialize)]
1400#[serde(rename_all = "PascalCase")]
1401pub struct DeviceRequest {
1402 pub driver: String,
1403 pub count: u64,
1404 #[serde(rename = "DeviceIDs")]
1405 pub device_ids: Vec<String>,
1406 pub capabilities: Vec<String>,
1407 pub options: Option<serde_json::Value>,
1408}
1409
1410#[derive(Clone, Debug, Serialize, Deserialize)]
1411#[serde(rename_all = "PascalCase")]
1412pub struct Port {
1413 pub ip: Option<String>,
1414 pub private_port: u64,
1415 pub public_port: Option<u64>,
1416 #[serde(rename = "Type")]
1417 pub typ: String,
1418}
1419
1420#[derive(Clone, Debug, Serialize, Deserialize)]
1421pub struct Stats {
1422 pub read: String,
1423 pub networks: HashMap<String, NetworkStats>,
1424 pub memory_stats: MemoryStats,
1425 pub blkio_stats: BlkioStats,
1426 pub cpu_stats: CpuStats,
1427}
1428
1429#[derive(Clone, Debug, Serialize, Deserialize)]
1430pub struct NetworkStats {
1431 pub rx_dropped: u64,
1432 pub rx_bytes: u64,
1433 pub rx_errors: u64,
1434 pub tx_packets: u64,
1435 pub tx_dropped: u64,
1436 pub rx_packets: u64,
1437 pub tx_errors: u64,
1438 pub tx_bytes: u64,
1439}
1440
1441#[derive(Clone, Debug, Serialize, Deserialize)]
1442pub struct MemoryStats {
1443 pub max_usage: u64,
1444 pub usage: u64,
1445 pub failcnt: Option<u64>,
1446 pub limit: u64,
1447 pub stats: MemoryStat,
1448}
1449
1450#[derive(Clone, Debug, Serialize, Deserialize)]
1451pub struct MemoryStat {
1452 pub total_pgmajfault: u64,
1453 pub cache: u64,
1454 pub mapped_file: u64,
1455 pub total_inactive_file: u64,
1456 pub pgpgout: u64,
1457 pub rss: u64,
1458 pub total_mapped_file: u64,
1459 pub writeback: u64,
1460 pub unevictable: u64,
1461 pub pgpgin: u64,
1462 pub total_unevictable: u64,
1463 pub pgmajfault: u64,
1464 pub total_rss: u64,
1465 pub total_rss_huge: u64,
1466 pub total_writeback: u64,
1467 pub total_inactive_anon: u64,
1468 pub rss_huge: u64,
1469 pub hierarchical_memory_limit: u64,
1470 pub hierarchical_memsw_limit: u64,
1471 pub total_pgfault: u64,
1472 pub total_active_file: u64,
1473 pub active_anon: u64,
1474 pub total_active_anon: u64,
1475 pub total_pgpgout: u64,
1476 pub total_cache: u64,
1477 pub inactive_anon: u64,
1478 pub active_file: u64,
1479 pub pgfault: u64,
1480 pub inactive_file: u64,
1481 pub total_pgpgin: u64,
1482}
1483
1484#[derive(Clone, Debug, Serialize, Deserialize)]
1485pub struct CpuStats {
1486 pub cpu_usage: CpuUsage,
1487 pub system_cpu_usage: u64,
1488 pub throttling_data: ThrottlingData,
1489}
1490
1491#[derive(Clone, Debug, Serialize, Deserialize)]
1492pub struct CpuUsage {
1493 pub percpu_usage: Vec<u64>,
1494 pub usage_in_usermode: u64,
1495 pub total_usage: u64,
1496 pub usage_in_kernelmode: u64,
1497}
1498
1499#[derive(Clone, Debug, Serialize, Deserialize)]
1500pub struct ThrottlingData {
1501 pub periods: u64,
1502 pub throttled_periods: u64,
1503 pub throttled_time: u64,
1504}
1505
1506#[derive(Clone, Debug, Serialize, Deserialize)]
1507pub struct BlkioStats {
1508 pub io_service_bytes_recursive: Vec<BlkioStat>,
1509 pub io_serviced_recursive: Vec<BlkioStat>,
1510 pub io_queue_recursive: Vec<BlkioStat>,
1511 pub io_service_time_recursive: Vec<BlkioStat>,
1512 pub io_wait_time_recursive: Vec<BlkioStat>,
1513 pub io_merged_recursive: Vec<BlkioStat>,
1514 pub io_time_recursive: Vec<BlkioStat>,
1515 pub sectors_recursive: Vec<BlkioStat>,
1516}
1517
1518#[derive(Clone, Debug, Serialize, Deserialize)]
1519pub struct BlkioStat {
1520 pub major: u64,
1521 pub minor: u64,
1522 pub op: String,
1523 pub value: u64,
1524}
1525
1526#[derive(Clone, Debug, Serialize, Deserialize)]
1527#[serde(rename_all = "PascalCase")]
1528pub struct Change {
1529 pub kind: u64,
1530 pub path: String,
1531}
1532
1533#[derive(Clone, Debug, Serialize, Deserialize)]
1534#[serde(rename_all = "PascalCase")]
1535pub struct Top {
1536 pub titles: Vec<String>,
1537 pub processes: Vec<Vec<String>>,
1538}
1539
1540#[derive(Clone, Debug, Serialize, Deserialize)]
1541#[serde(rename_all = "PascalCase")]
1542pub struct ContainerCreateInfo {
1543 pub id: String,
1544 pub warnings: Option<Vec<String>>,
1545}
1546
1547#[derive(Clone, Debug, Serialize, Deserialize)]
1548#[serde(rename_all = "PascalCase")]
1549pub struct Exit {
1550 pub status_code: u64,
1551}
1552
1553#[cfg(test)]
1554mod tests {
1555 use super::*;
1556 use crate::container::ContainerFilter::{ExitCode, Label, LabelName, Status};
1557
1558 #[test]
1559 fn container_options_simple() {
1560 let builder = ContainerOptionsBuilder::new("test_image");
1561 let options = builder.build();
1562
1563 assert_eq!(
1564 r#"{"HostConfig":{},"Image":"test_image"}"#,
1565 options.serialize().unwrap()
1566 );
1567 }
1568
1569 #[test]
1570 fn container_options_env() {
1571 let options = ContainerOptionsBuilder::new("test_image")
1572 .env(vec!["foo", "bar"])
1573 .build();
1574
1575 assert_eq!(
1576 r#"{"Env":["foo","bar"],"HostConfig":{},"Image":"test_image"}"#,
1577 options.serialize().unwrap()
1578 );
1579 }
1580
1581 #[test]
1582 fn container_options_env_dynamic() {
1583 let env: Vec<String> = ["foo", "bar", "baz"]
1584 .iter()
1585 .map(|s| String::from(*s))
1586 .collect();
1587
1588 let options = ContainerOptionsBuilder::new("test_image").env(&env).build();
1589
1590 assert_eq!(
1591 r#"{"Env":["foo","bar","baz"],"HostConfig":{},"Image":"test_image"}"#,
1592 options.serialize().unwrap()
1593 );
1594 }
1595
1596 #[test]
1597 fn container_options_user() {
1598 let options = ContainerOptionsBuilder::new("test_image")
1599 .user("alice")
1600 .build();
1601
1602 assert_eq!(
1603 r#"{"HostConfig":{},"Image":"test_image","User":"alice"}"#,
1604 options.serialize().unwrap()
1605 );
1606 }
1607
1608 #[test]
1609 fn container_options_host_config() {
1610 let options = ContainerOptionsBuilder::new("test_image")
1611 .network_mode("host")
1612 .auto_remove(true)
1613 .privileged(true)
1614 .build();
1615
1616 assert_eq!(
1617 r#"{"HostConfig":{"AutoRemove":true,"NetworkMode":"host","Privileged":true},"Image":"test_image"}"#,
1618 options.serialize().unwrap()
1619 );
1620 }
1621
1622 #[test]
1623 fn container_options_expose() {
1624 let options = ContainerOptionsBuilder::new("test_image")
1625 .expose(80, "tcp", 8080)
1626 .build();
1627 assert_eq!(
1628 r#"{"ExposedPorts":{"80/tcp":{}},"HostConfig":{"PortBindings":{"80/tcp":[{"HostPort":"8080"}]}},"Image":"test_image"}"#,
1629 options.serialize().unwrap()
1630 );
1631 let options = ContainerOptionsBuilder::new("test_image")
1633 .expose(80, "tcp", 8080)
1634 .expose(81, "tcp", 8081)
1635 .build();
1636 assert_eq!(
1637 r#"{"ExposedPorts":{"80/tcp":{},"81/tcp":{}},"HostConfig":{"PortBindings":{"80/tcp":[{"HostPort":"8080"}],"81/tcp":[{"HostPort":"8081"}]}},"Image":"test_image"}"#,
1638 options.serialize().unwrap()
1639 );
1640 }
1641
1642 #[test]
1643 fn container_options_publish() {
1644 let options = ContainerOptionsBuilder::new("test_image")
1645 .publish(80, "tcp")
1646 .build();
1647 assert_eq!(
1648 r#"{"ExposedPorts":{"80/tcp":{}},"HostConfig":{},"Image":"test_image"}"#,
1649 options.serialize().unwrap()
1650 );
1651 let options = ContainerOptionsBuilder::new("test_image")
1653 .publish(80, "tcp")
1654 .publish(81, "tcp")
1655 .build();
1656 assert_eq!(
1657 r#"{"ExposedPorts":{"80/tcp":{},"81/tcp":{}},"HostConfig":{},"Image":"test_image"}"#,
1658 options.serialize().unwrap()
1659 );
1660 }
1661
1662 #[test]
1664 fn container_options_publish_all_ports() {
1665 let options = ContainerOptionsBuilder::new("test_image")
1666 .publish_all_ports()
1667 .build();
1668
1669 assert_eq!(
1670 r#"{"HostConfig":{"PublishAllPorts":true},"Image":"test_image"}"#,
1671 options.serialize().unwrap()
1672 );
1673 }
1674
1675 #[test]
1677 fn container_options_nested() {
1678 let options = ContainerOptionsBuilder::new("test_image")
1679 .log_driver("fluentd")
1680 .build();
1681
1682 assert_eq!(
1683 r#"{"HostConfig":{"LogConfig":{"Type":"fluentd"}},"Image":"test_image"}"#,
1684 options.serialize().unwrap()
1685 );
1686 }
1687
1688 #[test]
1690 fn container_options_restart_policy() {
1691 let mut options = ContainerOptionsBuilder::new("test_image")
1692 .restart_policy("on-failure", 10)
1693 .build();
1694
1695 assert_eq!(
1696 r#"{"HostConfig":{"RestartPolicy":{"MaximumRetryCount":10,"Name":"on-failure"}},"Image":"test_image"}"#,
1697 options.serialize().unwrap()
1698 );
1699
1700 options = ContainerOptionsBuilder::new("test_image")
1701 .restart_policy("always", 0)
1702 .build();
1703
1704 assert_eq!(
1705 r#"{"HostConfig":{"RestartPolicy":{"Name":"always"}},"Image":"test_image"}"#,
1706 options.serialize().unwrap()
1707 );
1708 }
1709
1710 #[test]
1711 fn container_list_options_multiple_labels() {
1712 let options = ContainerListOptions::builder()
1713 .filter(vec![
1714 Label("label1".to_string(), "value".to_string()),
1715 LabelName("label2".to_string()),
1716 ])
1717 .build();
1718
1719 let form = form_urlencoded::Serializer::new(String::new())
1720 .append_pair("filters", r#"{"label":["label1=value","label2"]}"#)
1721 .finish();
1722
1723 assert_eq!(form, options.serialize().unwrap())
1724 }
1725
1726 #[test]
1727 fn container_list_options_exit_code() {
1728 let options = ContainerListOptions::builder()
1729 .filter(vec![ExitCode(0)])
1730 .build();
1731
1732 let form = form_urlencoded::Serializer::new(String::new())
1733 .append_pair("filters", r#"{"exited":["0"]}"#)
1734 .finish();
1735
1736 assert_eq!(form, options.serialize().unwrap())
1737 }
1738
1739 #[test]
1740 fn container_list_options_status() {
1741 let options = ContainerListOptions::builder()
1742 .filter(vec![Status("running".to_string())])
1743 .build();
1744
1745 let form = form_urlencoded::Serializer::new(String::new())
1746 .append_pair("filters", r#"{"status":["running"]}"#)
1747 .finish();
1748
1749 assert_eq!(form, options.serialize().unwrap())
1750 }
1751
1752 #[test]
1753 fn container_list_options_combined() {
1754 let options = ContainerListOptions::builder()
1755 .all()
1756 .filter(vec![
1757 Label("label1".to_string(), "value".to_string()),
1758 LabelName("label2".to_string()),
1759 ExitCode(0),
1760 Status("running".to_string()),
1761 ])
1762 .build();
1763
1764 let serialized = options.serialize().unwrap();
1765
1766 assert!(serialized.contains("all=true"));
1767 assert!(serialized.contains("filters="));
1768 assert!(serialized.contains("%22label%22%3A%5B%22label1%3Dvalue%22%2C%22label2%22%5D"));
1769 assert!(serialized.contains("%22status%22%3A%5B%22running%22%5D"));
1770 assert!(serialized.contains("%22exited%22%3A%5B%220%22%5D"));
1771 }
1772
1773 #[cfg(feature = "chrono")]
1774 #[test]
1775 fn logs_options() {
1776 let timestamp = chrono::NaiveDateTime::from_timestamp(2_147_483_647, 0);
1777 let since = chrono::DateTime::<chrono::Utc>::from_utc(timestamp, chrono::Utc);
1778
1779 let options = LogsOptionsBuilder::default()
1780 .follow(true)
1781 .stdout(true)
1782 .stderr(true)
1783 .timestamps(true)
1784 .tail("all")
1785 .since(&since)
1786 .build();
1787
1788 let serialized = options.serialize().unwrap();
1789
1790 assert!(serialized.contains("follow=true"));
1791 assert!(serialized.contains("stdout=true"));
1792 assert!(serialized.contains("stderr=true"));
1793 assert!(serialized.contains("timestamps=true"));
1794 assert!(serialized.contains("tail=all"));
1795 assert!(serialized.contains("since=2147483647"));
1796 }
1797
1798 #[cfg(not(feature = "chrono"))]
1799 #[test]
1800 fn logs_options() {
1801 let options = LogsOptionsBuilder::default()
1802 .follow(true)
1803 .stdout(true)
1804 .stderr(true)
1805 .timestamps(true)
1806 .tail("all")
1807 .since(2_147_483_647)
1808 .build();
1809
1810 let serialized = options.serialize().unwrap();
1811
1812 assert!(serialized.contains("follow=true"));
1813 assert!(serialized.contains("stdout=true"));
1814 assert!(serialized.contains("stderr=true"));
1815 assert!(serialized.contains("timestamps=true"));
1816 assert!(serialized.contains("tail=all"));
1817 assert!(serialized.contains("since=2147483647"));
1818 }
1819}