1use crate::models::{DeviceRequest, Labels, NetworkingConfig};
2use crate::opts::ImageName;
3use containers_api::opts::{Filter, FilterItem};
4use containers_api::{
5 impl_field, impl_filter_func, impl_map_field, impl_opts_builder, impl_str_enum_field,
6 impl_str_field, impl_url_bool_field, impl_url_str_field, impl_vec_field,
7};
8
9use std::net::SocketAddr;
10use std::{
11 collections::HashMap,
12 hash::Hash,
13 iter::Peekable,
14 str::{self, FromStr},
15 string::ToString,
16 time::Duration,
17};
18
19use serde::{Deserialize, Serialize};
20use serde_json::{json, Map, Value};
21
22use crate::{Error, Result};
23
24pub enum Health {
25 Starting,
26 Healthy,
27 Unhealthy,
28 None,
29}
30
31impl AsRef<str> for Health {
32 fn as_ref(&self) -> &str {
33 match &self {
34 Health::Starting => "starting",
35 Health::Healthy => "healthy",
36 Health::Unhealthy => "unhealthy",
37 Health::None => "none",
38 }
39 }
40}
41
42#[derive(Clone, Debug, Default, Serialize, Deserialize)]
43#[serde(rename_all = "lowercase")]
44pub enum Isolation {
45 #[serde(alias = "")]
46 #[default]
47 Default,
48 Process,
49 HyperV,
50}
51
52impl AsRef<str> for Isolation {
53 fn as_ref(&self) -> &str {
54 match &self {
55 Isolation::Default => "default",
56 Isolation::Process => "process",
57 Isolation::HyperV => "hyperv",
58 }
59 }
60}
61
62#[derive(Clone, Debug, Serialize, Deserialize)]
63#[serde(rename_all = "lowercase")]
64pub enum ContainerStatus {
65 Created,
66 Configured,
67 Restarting,
68 Running,
69 Removing,
70 Paused,
71 Exited,
72 Dead,
73}
74
75impl AsRef<str> for ContainerStatus {
76 fn as_ref(&self) -> &str {
77 use ContainerStatus::*;
78 match &self {
79 Created => "created",
80 Configured => "configured",
81 Restarting => "restarting",
82 Running => "running",
83 Removing => "removing",
84 Paused => "paused",
85 Exited => "exited",
86 Dead => "dead",
87 }
88 }
89}
90
91pub enum ContainerFilter {
93 Ancestor(ImageName),
94 Before(String),
96 ExitCode(u64),
98 Health(Health),
99 Id(String),
101 Isolation(Isolation),
103 IsTask(bool),
104 LabelKey(String),
106 Label(String, String),
108 Name(String),
110 Publish(PublishPort),
111 Network(String),
113 Since(String),
115 Status(ContainerStatus),
116 Volume(String),
118}
119
120impl Filter for ContainerFilter {
121 fn query_item(&self) -> FilterItem {
122 use ContainerFilter::*;
123 match &self {
124 Ancestor(name) => FilterItem::new("ancestor", name.to_string()),
125 Before(before) => FilterItem::new("before", before.to_owned()),
126 ExitCode(c) => FilterItem::new("exit", c.to_string()),
127 Health(health) => FilterItem::new("health", health.as_ref().to_string()),
128 Id(id) => FilterItem::new("id", id.to_owned()),
129 Isolation(isolation) => FilterItem::new("isolation", isolation.as_ref().to_string()),
130 IsTask(is_task) => FilterItem::new("is-task", is_task.to_string()),
131 LabelKey(key) => FilterItem::new("label", key.to_owned()),
132 Label(key, val) => FilterItem::new("label", format!("{key}={val}")),
133 Name(name) => FilterItem::new("name", name.to_owned()),
134 Publish(port) => FilterItem::new("publsh", port.to_string()),
135 Network(net) => FilterItem::new("net", net.to_owned()),
136 Since(since) => FilterItem::new("since", since.to_owned()),
137 Status(s) => FilterItem::new("status", s.as_ref().to_string()),
138 Volume(vol) => FilterItem::new("volume", vol.to_owned()),
139 }
140 }
141}
142
143impl_opts_builder!(url => ContainerList);
144
145impl ContainerListOptsBuilder {
146 impl_filter_func!(
147 ContainerFilter
149 );
150
151 impl_url_bool_field!(
152 all => "all"
154 );
155
156 impl_url_str_field!(since => "since");
157
158 impl_url_str_field!(before => "before");
159
160 impl_url_bool_field!(
161 sized => "size"
163 );
164}
165
166#[derive(Serialize, Debug, Clone)]
168pub struct ContainerCreateOpts {
169 name: Option<String>,
170 params: HashMap<&'static str, Value>,
171}
172
173fn insert<'a, I, V>(key_path: &mut Peekable<I>, value: &V, parent_node: &mut Value)
176where
177 V: Serialize,
178 I: Iterator<Item = &'a str>,
179{
180 if let Some(local_key) = key_path.next() {
181 if key_path.peek().is_some() {
182 if let Some(node) = parent_node.as_object_mut() {
183 let node = node
184 .entry(local_key.to_string())
185 .or_insert(Value::Object(Map::new()));
186
187 insert(key_path, value, node);
188 }
189 } else if let Some(node) = parent_node.as_object_mut() {
190 node.insert(
191 local_key.to_string(),
192 serde_json::to_value(value).unwrap_or_default(),
193 );
194 }
195 }
196}
197
198impl ContainerCreateOpts {
199 pub fn builder() -> ContainerCreateOptsBuilder {
201 ContainerCreateOptsBuilder::default()
202 }
203
204 pub fn serialize(&self) -> Result<String> {
206 serde_json::to_string(&self.to_json()).map_err(Error::from)
207 }
208
209 pub fn serialize_vec(&self) -> Result<Vec<u8>> {
211 serde_json::to_vec(&self.to_json()).map_err(Error::from)
212 }
213
214 fn to_json(&self) -> Value {
215 let mut body_members = Map::new();
216 body_members.insert("HostConfig".to_string(), Value::Object(Map::new()));
219 let mut body = Value::Object(body_members);
220 self.parse_from(&self.params, &mut body);
221 body
222 }
223
224 fn parse_from<'a, K, V>(&self, params: &'a HashMap<K, V>, body: &mut Value)
225 where
226 &'a HashMap<K, V>: IntoIterator,
227 K: ToString + Eq + Hash,
228 V: Serialize,
229 {
230 for (k, v) in params.iter() {
231 let key_string = k.to_string();
232 insert(&mut key_string.split('.').peekable(), v, body)
233 }
234 }
235
236 pub(crate) fn name(&self) -> Option<&str> {
237 self.name.as_deref()
238 }
239}
240
241#[derive(Default)]
242pub struct ContainerCreateOptsBuilder {
243 name: Option<String>,
244 params: HashMap<&'static str, Value>,
245}
246
247#[derive(Clone, Debug, Serialize, Deserialize)]
248pub enum Protocol {
250 Tcp,
251 Udp,
252 Sctp,
253}
254
255impl AsRef<str> for Protocol {
256 fn as_ref(&self) -> &str {
257 match &self {
258 Self::Tcp => "tcp",
259 Self::Udp => "udp",
260 Self::Sctp => "sctp",
261 }
262 }
263}
264
265impl FromStr for Protocol {
266 type Err = Error;
267
268 fn from_str(s: &str) -> Result<Self> {
269 match s {
270 "tcp" => Ok(Protocol::Tcp),
271 "udp" => Ok(Protocol::Udp),
272 "sctp" => Ok(Protocol::Sctp),
273 proto => Err(Error::InvalidProtocol(proto.into())),
274 }
275 }
276}
277
278#[derive(Clone, Debug, Serialize, Deserialize)]
279pub struct PublishPort {
282 port: u32,
283 protocol: Protocol,
284}
285
286impl PublishPort {
287 pub fn tcp(port: u32) -> Self {
289 Self {
290 port,
291 protocol: Protocol::Tcp,
292 }
293 }
294
295 pub fn udp(port: u32) -> Self {
297 Self {
298 port,
299 protocol: Protocol::Udp,
300 }
301 }
302
303 pub fn sctp(port: u32) -> Self {
305 Self {
306 port,
307 protocol: Protocol::Sctp,
308 }
309 }
310}
311
312impl FromStr for PublishPort {
313 type Err = Error;
314
315 fn from_str(s: &str) -> Result<Self> {
316 let mut elems = s.split('/');
317 let port = elems
318 .next()
319 .ok_or_else(|| Error::InvalidPort("missing port number".into()))
320 .and_then(|port| {
321 port.parse::<u32>()
322 .map_err(|e| Error::InvalidPort(format!("expected port number - {e}")))
323 })?;
324
325 let protocol = elems
326 .next()
327 .ok_or_else(|| Error::InvalidPort("missing protocol".into()))
328 .and_then(Protocol::from_str)?;
329
330 Ok(PublishPort { port, protocol })
331 }
332}
333
334impl ToString for PublishPort {
335 fn to_string(&self) -> String {
336 format!("{}/{}", self.port, self.protocol.as_ref())
337 }
338}
339
340#[derive(Clone, Debug, Serialize, Deserialize)]
341pub struct HostPort {
343 port: u32,
344 ip: Option<String>,
345}
346
347impl HostPort {
348 pub fn new(port: u32) -> Self {
350 Self { port, ip: None }
351 }
352
353 pub fn with_ip(port: u32, ip: String) -> Self {
355 Self { port, ip: Some(ip) }
356 }
357}
358
359impl From<u32> for HostPort {
360 fn from(value: u32) -> Self {
361 HostPort {
362 port: value,
363 ip: None,
364 }
365 }
366}
367
368impl From<SocketAddr> for HostPort {
369 fn from(value: SocketAddr) -> Self {
370 Self {
371 port: value.port().into(),
372 ip: Some(value.ip().to_string()),
373 }
374 }
375}
376
377pub enum IpcMode {
379 None,
381 Private,
383 Shareable,
385 Container(String),
387 Host,
389}
390
391impl ToString for IpcMode {
392 fn to_string(&self) -> String {
393 match &self {
394 IpcMode::None => String::from("none"),
395 IpcMode::Private => String::from("private"),
396 IpcMode::Shareable => String::from("shareable"),
397 IpcMode::Container(id) => format!("container:{}", id),
398 IpcMode::Host => String::from("host"),
399 }
400 }
401}
402
403pub enum PidMode {
405 Container(String),
407 Host,
409}
410
411impl ToString for PidMode {
412 fn to_string(&self) -> String {
413 match &self {
414 PidMode::Container(id) => format!("container:{}", id),
415 PidMode::Host => String::from("host"),
416 }
417 }
418}
419
420impl ContainerCreateOptsBuilder {
421 pub fn new(name: impl Into<String>) -> Self {
422 Self {
423 params: Default::default(),
424 name: Some(name.into()),
425 }
426 }
427
428 pub fn name<N>(mut self, name: N) -> Self
430 where
431 N: Into<String>,
432 {
433 self.name = Some(name.into());
434 self
435 }
436
437 pub fn publish_all_ports(mut self) -> Self {
439 self.params
440 .insert("HostConfig.PublishAllPorts", Value::Bool(true));
441 self
442 }
443
444 pub fn expose<P: Into<HostPort>>(mut self, srcport: PublishPort, hostport: P) -> Self {
445 let mut exposedport: HashMap<String, String> = HashMap::new();
446 let hostport = hostport.into();
447 exposedport.insert("HostPort".to_string(), hostport.port.to_string());
448 if let Some(ip) = hostport.ip {
449 exposedport.insert("HostIp".to_string(), ip);
450 }
451
452 let mut port_bindings: HashMap<String, Value> = HashMap::new();
455 for (key, val) in self
456 .params
457 .get("HostConfig.PortBindings")
458 .unwrap_or(&json!(null))
459 .as_object()
460 .unwrap_or(&Map::new())
461 .iter()
462 {
463 port_bindings.insert(key.to_string(), json!(val));
464 }
465 port_bindings.insert(srcport.to_string(), json!(vec![exposedport]));
466
467 self.params
468 .insert("HostConfig.PortBindings", json!(port_bindings));
469
470 let mut exposed_ports: HashMap<String, Value> = HashMap::new();
472 let empty_config: HashMap<String, Value> = HashMap::new();
473 for key in port_bindings.keys() {
474 exposed_ports.insert(key.to_string(), json!(empty_config));
475 }
476
477 self.params.insert("ExposedPorts", json!(exposed_ports));
478
479 self
480 }
481
482 pub fn publish(mut self, port: PublishPort) -> Self {
484 let mut exposed_port_bindings: HashMap<String, Value> = HashMap::new();
488 for (key, val) in self
489 .params
490 .get("ExposedPorts")
491 .unwrap_or(&json!(null))
492 .as_object()
493 .unwrap_or(&Map::new())
494 .iter()
495 {
496 exposed_port_bindings.insert(key.to_string(), json!(val));
497 }
498 exposed_port_bindings.insert(port.to_string(), json!({}));
499
500 let mut exposed_ports: HashMap<String, Value> = HashMap::new();
502 let empty_config: HashMap<String, Value> = HashMap::new();
503 for key in exposed_port_bindings.keys() {
504 exposed_ports.insert(key.to_string(), json!(empty_config));
505 }
506
507 self.params.insert("ExposedPorts", json!(exposed_ports));
508
509 self
510 }
511
512 impl_str_field!(
513 working_dir => "WorkingDir"
515 );
516
517 impl_str_field!(
518 image => "Image"
520 );
521
522 impl_vec_field!(
523 security_options => "HostConfig.SecurityOpt"
525 );
526
527 impl_vec_field!(
528 volumes => "HostConfig.Binds"
530 );
531
532 impl_vec_field!(links => "HostConfig.Links");
533
534 impl_field!(memory: u64 => "HostConfig.Memory");
535
536 impl_field!(
537 memory_swap: i64 => "HostConfig.MemorySwap"
539 );
540
541 impl_field!(
542 nano_cpus: u64 => "HostConfig.NanoCpus"
547 );
548
549 pub fn cpus(self, cpus: f64) -> Self {
553 self.nano_cpus((1_000_000_000.0 * cpus) as u64)
554 }
555
556 impl_field!(
557 cpu_shares: u32 => "HostConfig.CpuShares");
559
560 impl_map_field!(json labels => "Labels");
561
562 pub fn attach_stdin(mut self, attach: bool) -> Self {
564 self.params.insert("AttachStdin", json!(attach));
565 self.params.insert("OpenStdin", json!(attach));
566 self
567 }
568
569 impl_field!(
570 attach_stdout: bool => "AttachStdout");
572
573 impl_field!(
574 attach_stderr: bool => "AttachStderr");
576
577 impl_field!(
578 tty: bool => "Tty");
580
581 impl_vec_field!(extra_hosts => "HostConfig.ExtraHosts");
582
583 impl_vec_field!(volumes_from => "HostConfig.VolumesFrom");
584
585 impl_str_field!(network_mode => "HostConfig.NetworkMode");
586
587 impl_vec_field!(env => "Env");
588
589 impl_vec_field!(command => "Cmd");
590
591 impl_vec_field!(entrypoint => "Entrypoint");
592
593 impl_vec_field!(capabilities => "HostConfig.CapAdd");
594
595 pub fn devices(mut self, devices: Vec<Labels>) -> Self {
596 self.params.insert("HostConfig.Devices", json!(devices));
597 self
598 }
599
600 impl_str_field!(log_driver => "HostConfig.LogConfig.Type");
601
602 impl_map_field!(json log_driver_config => "HostConfig.LogConfig.Config");
603
604 pub fn restart_policy(mut self, name: &str, maximum_retry_count: u64) -> Self {
605 self.params
606 .insert("HostConfig.RestartPolicy.Name", json!(name));
607 if name == "on-failure" {
608 self.params.insert(
609 "HostConfig.RestartPolicy.MaximumRetryCount",
610 json!(maximum_retry_count),
611 );
612 }
613 self
614 }
615
616 impl_field!(auto_remove: bool => "HostConfig.AutoRemove");
617
618 impl_str_field!(
619 stop_signal => "StopSignal");
621
622 impl_field!(
623 stop_signal_num: u64 => "StopSignal");
625
626 impl_field!(
627 stop_timeout: Duration => "StopTimeout");
629
630 impl_str_field!(userns_mode => "HostConfig.UsernsMode");
631
632 impl_field!(privileged: bool => "HostConfig.Privileged");
633
634 impl_field!(
635 init: bool => "HostConfig.Init"
638 );
639
640 impl_str_field!(user => "User");
641
642 pub fn build(&self) -> ContainerCreateOpts {
643 ContainerCreateOpts {
644 name: self.name.clone(),
645 params: self.params.clone(),
646 }
647 }
648
649 impl_str_field!(
650 hostname => "Hostname"
652 );
653
654 impl_str_field!(
655 domainname => "Domainname"
657 );
658
659 impl_str_enum_field!(
660 ipc: IpcMode => "HostConfig.IpcMode"
662 );
663
664 impl_str_enum_field!(
665 pid: PidMode => "HostConfig.PidMode"
667 );
668
669 impl_field!(
670 network_config: NetworkingConfig => "NetworkingConfig"
672 );
673
674 impl_str_field!(
675 runtime => "HostConfig.Runtime"
677 );
678
679 impl_field!(
680 device_requests: Vec<DeviceRequest> => "HostConfig.DeviceRequests"
682 );
683}
684
685impl_opts_builder!(url => ContainerRemove);
686
687impl ContainerRemoveOptsBuilder {
688 impl_url_bool_field!(
689 force => "force"
691 );
692
693 impl_url_bool_field!(
694 volumes => "v"
696 );
697
698 impl_url_bool_field!(
699 link => "link"
701 );
702}
703
704impl_opts_builder!(url => ContainerPrune);
705
706pub enum ContainerPruneFilter {
707 Until(String),
711 #[cfg(feature = "chrono")]
712 #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
713 UntilDate(chrono::DateTime<chrono::Utc>),
715 LabelKey(String),
717 Label(String, String),
719}
720
721impl Filter for ContainerPruneFilter {
722 fn query_item(&self) -> FilterItem {
723 use ContainerPruneFilter::*;
724 match &self {
725 Until(until) => FilterItem::new("until", until.to_owned()),
726 #[cfg(feature = "chrono")]
727 UntilDate(until) => FilterItem::new("until", until.timestamp().to_string()),
728 LabelKey(label) => FilterItem::new("label", label.to_owned()),
729 Label(key, val) => FilterItem::new("label", format!("{key}={val}")),
730 }
731 }
732}
733
734impl ContainerPruneOptsBuilder {
735 impl_filter_func!(ContainerPruneFilter);
736}
737
738impl_opts_builder!(url => ContainerCommit);
739
740impl ContainerCommitOpts {
741 pub(crate) fn with_container(&self, id: &str) -> Self {
742 let mut s = self.clone();
744 s.params.insert("container", id.to_owned());
745 s
746 }
747}
748
749impl ContainerCommitOptsBuilder {
750 impl_url_str_field!(
751 repo => "repo"
753 );
754 impl_url_str_field!(
755 tag => "tag"
757 );
758 impl_url_str_field!(
759 comment => "comment"
761 );
762 impl_url_str_field!(
763 author => "author"
765 );
766 impl_url_bool_field!(
767 pause => "pause"
769 );
770 impl_url_str_field!(
771 changes => "changes"
773 );
774}
775
776impl_opts_builder!(url => ContainerStop);
777
778impl ContainerStopOptsBuilder {
779 impl_url_str_field!(
780 signal => "signal"
782 );
783
784 pub fn wait(mut self, duration: Duration) -> Self {
786 self.params.insert("t", duration.as_secs().to_string());
787 self
788 }
789}
790
791impl_opts_builder!(url => ContainerRestart);
792
793impl ContainerRestartOptsBuilder {
794 impl_url_str_field!(
795 signal => "signal"
797 );
798
799 pub fn wait(mut self, duration: Duration) -> Self {
801 self.params.insert("t", duration.as_secs().to_string());
802 self
803 }
804}
805
806#[cfg(test)]
807mod tests {
808 use super::*;
809
810 macro_rules! test_case {
811 ($opts:expr, $want:expr) => {
812 let opts = $opts.build();
813
814 pretty_assertions::assert_eq!($want, opts.serialize().unwrap())
815 };
816 }
817
818 #[test]
819 fn create_container_opts() {
820 test_case!(
821 ContainerCreateOptsBuilder::default().image("test_image"),
822 r#"{"HostConfig":{},"Image":"test_image"}"#
823 );
824
825 test_case!(
826 ContainerCreateOptsBuilder::default()
827 .image("test_image")
828 .env(vec!["foo", "bar"]),
829 r#"{"Env":["foo","bar"],"HostConfig":{},"Image":"test_image"}"#
830 );
831
832 test_case!(
833 ContainerCreateOptsBuilder::default()
834 .image("test_image")
835 .env(["foo", "bar", "baz"]),
836 r#"{"Env":["foo","bar","baz"],"HostConfig":{},"Image":"test_image"}"#
837 );
838
839 test_case!(
840 ContainerCreateOptsBuilder::default()
841 .image("test_image")
842 .env(std::iter::once("test")),
843 r#"{"Env":["test"],"HostConfig":{},"Image":"test_image"}"#
844 );
845
846 test_case!(
847 ContainerCreateOptsBuilder::default()
848 .image("test_image")
849 .user("alice"),
850 r#"{"HostConfig":{},"Image":"test_image","User":"alice"}"#
851 );
852
853 test_case!(
854 ContainerCreateOptsBuilder::default()
855 .image("test_image")
856 .network_mode("host")
857 .auto_remove(true)
858 .privileged(true),
859 r#"{"HostConfig":{"AutoRemove":true,"NetworkMode":"host","Privileged":true},"Image":"test_image"}"#
860 );
861
862 test_case!(
863 ContainerCreateOptsBuilder::default()
864 .image("test_image")
865 .expose(PublishPort::tcp(80), 8080),
866 r#"{"ExposedPorts":{"80/tcp":{}},"HostConfig":{"PortBindings":{"80/tcp":[{"HostPort":"8080"}]}},"Image":"test_image"}"#
867 );
868
869 test_case!(
870 ContainerCreateOptsBuilder::default()
871 .image("test_image")
872 .expose(PublishPort::udp(80), 8080)
873 .expose(PublishPort::sctp(81), 8081),
874 r#"{"ExposedPorts":{"80/udp":{},"81/sctp":{}},"HostConfig":{"PortBindings":{"80/udp":[{"HostPort":"8080"}],"81/sctp":[{"HostPort":"8081"}]}},"Image":"test_image"}"#
875 );
876
877 test_case!(
878 ContainerCreateOptsBuilder::default()
879 .image("test_image")
880 .publish(PublishPort::udp(80))
881 .publish(PublishPort::sctp(6969))
882 .publish(PublishPort::tcp(1337)),
883 r#"{"ExposedPorts":{"1337/tcp":{},"6969/sctp":{},"80/udp":{}},"HostConfig":{},"Image":"test_image"}"#
884 );
885
886 test_case!(
887 ContainerCreateOptsBuilder::default()
888 .image("test_image")
889 .expose(
890 PublishPort::tcp(80),
891 "[::1]:8080".parse::<SocketAddr>().unwrap()
892 ),
893 r#"{"ExposedPorts":{"80/tcp":{}},"HostConfig":{"PortBindings":{"80/tcp":[{"HostIp":"::1","HostPort":"8080"}]}},"Image":"test_image"}"#
894 );
895
896 test_case!(
897 ContainerCreateOptsBuilder::default()
898 .image("test_image")
899 .publish_all_ports(),
900 r#"{"HostConfig":{"PublishAllPorts":true},"Image":"test_image"}"#
901 );
902
903 test_case!(
904 ContainerCreateOptsBuilder::default()
905 .image("test_image")
906 .log_driver("fluentd"),
907 r#"{"HostConfig":{"LogConfig":{"Type":"fluentd"}},"Image":"test_image"}"#
908 );
909
910 test_case!(
911 ContainerCreateOptsBuilder::default()
912 .image("test_image")
913 .log_driver_config(vec![("tag", "container-tag")]),
914 r#"{"HostConfig":{"LogConfig":{"Config":{"tag":"container-tag"}}},"Image":"test_image"}"#
915 );
916
917 test_case!(
918 ContainerCreateOptsBuilder::default()
919 .image("test_image")
920 .restart_policy("on-failure", 10),
921 r#"{"HostConfig":{"RestartPolicy":{"MaximumRetryCount":10,"Name":"on-failure"}},"Image":"test_image"}"#
922 );
923
924 test_case!(
925 ContainerCreateOptsBuilder::default()
926 .image("test_image")
927 .restart_policy("always", 0),
928 r#"{"HostConfig":{"RestartPolicy":{"Name":"always"}},"Image":"test_image"}"#
929 );
930 }
931}