docker_api/opts/
container.rs

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
91/// Filter Opts for container listings
92pub enum ContainerFilter {
93    Ancestor(ImageName),
94    /// Container ID or name.
95    Before(String),
96    /// Containers with the specified exit code.
97    ExitCode(u64),
98    Health(Health),
99    /// The container's ID.
100    Id(String),
101    /// Applies only to Windows daemon.
102    Isolation(Isolation),
103    IsTask(bool),
104    /// Label in the form of `label=key`.
105    LabelKey(String),
106    /// Label in the form of `label=key=val`.
107    Label(String, String),
108    /// The container's name.
109    Name(String),
110    Publish(PublishPort),
111    /// Network ID or name.
112    Network(String),
113    /// Container ID or name.
114    Since(String),
115    Status(ContainerStatus),
116    /// Volume name or mount point destination.
117    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        /// Filter the list of containers by one of the enum variants.
148        ContainerFilter
149    );
150
151    impl_url_bool_field!(
152        /// If set to true all containers will be returned
153        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        /// If set to true the sizes of the containers will be returned
162        sized => "size"
163    );
164}
165
166/// Interface for building a new docker container from an existing image
167#[derive(Serialize, Debug, Clone)]
168pub struct ContainerCreateOpts {
169    name: Option<String>,
170    params: HashMap<&'static str, Value>,
171}
172
173/// Function to insert a JSON value into a tree where the desired
174/// location of the value is given as a path of JSON keys.
175fn 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    /// Returns a builder for creating a new container.
200    pub fn builder() -> ContainerCreateOptsBuilder {
201        ContainerCreateOptsBuilder::default()
202    }
203
204    /// Serialize options as a JSON string.
205    pub fn serialize(&self) -> Result<String> {
206        serde_json::to_string(&self.to_json()).map_err(Error::from)
207    }
208
209    /// Serialize options as a JSON bytes.
210    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        // The HostConfig element gets initialized to an empty object,
217        // for backward compatibility.
218        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)]
248/// Network protocol on which a port can be exposed.
249pub 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)]
279/// Structure used to expose a port on a container with [`expose`](ContainerCreateOptsBuilder::expose) or
280/// [`publish`](ContainerCreateOptsBuilder::publish).
281pub struct PublishPort {
282    port: u32,
283    protocol: Protocol,
284}
285
286impl PublishPort {
287    /// Expose a TCP port.
288    pub fn tcp(port: u32) -> Self {
289        Self {
290            port,
291            protocol: Protocol::Tcp,
292        }
293    }
294
295    /// Expose a UDP port.
296    pub fn udp(port: u32) -> Self {
297        Self {
298            port,
299            protocol: Protocol::Udp,
300        }
301    }
302
303    // Expose a SCTP port.
304    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)]
341/// Structure used to bind a host port to a container port with [`expose`](ContainerCreateOptsBuilder::expose)
342pub struct HostPort {
343    port: u32,
344    ip: Option<String>,
345}
346
347impl HostPort {
348    /// Bind a host port to a container port
349    pub fn new(port: u32) -> Self {
350        Self { port, ip: None }
351    }
352
353    /// Bind a host port and a specific host IP to a container port
354    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
377/// IPC sharing mode for the container.
378pub enum IpcMode {
379    /// "none": own private IPC namespace, with /dev/shm not mounted
380    None,
381    /// "private": own private IPC namespace
382    Private,
383    /// "shareable": own private IPC namespace, with a possibility to share it with other containers
384    Shareable,
385    /// "container:<name|id>": join another (shareable) container's IPC namespace
386    Container(String),
387    /// "host": use the host system's IPC namespace
388    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
403/// PID (Process) Namespace mode for the container.
404pub enum PidMode {
405    /// "container:<name|id>": joins another container's PID namespace
406    Container(String),
407    /// "host": use the host's PID namespace inside the container
408    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    /// Set the name of the container.
429    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    /// enable all exposed ports on the container to be mapped to random, available, ports on the host
438    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        // The idea here is to go thought the 'old' port binds and to apply them to the local
453        // 'port_bindings' variable, add the bind we want and replace the 'old' value
454        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        // Replicate the port bindings over to the exposed ports config
471        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    /// Publish a port in the container without assigning a port on the host
483    pub fn publish(mut self, port: PublishPort) -> Self {
484        /* The idea here is to go thought the 'old' port binds
485         * and to apply them to the local 'exposedport_bindings' variable,
486         * add the bind we want and replace the 'old' value */
487        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        // Replicate the port bindings over to the exposed ports config
501        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        /// Specify the working dir (corresponds to the `-w` docker cli argument)
514        working_dir => "WorkingDir"
515    );
516
517    impl_str_field!(
518        /// The name (or reference) of the image to use when creating the container
519        image => "Image"
520    );
521
522    impl_vec_field!(
523        /// Specify a Vec of string values to customize labels for MLS systems, such as SELinux.
524        security_options => "HostConfig.SecurityOpt"
525    );
526
527    impl_vec_field!(
528        /// Specify any bind mounts, taking the form of `/some/host/path:/some/container/path`
529        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        /// Total memory limit (memory + swap) in bytes. Set to -1 (default) to enable unlimited swap.
538        memory_swap: i64 => "HostConfig.MemorySwap"
539    );
540
541    impl_field!(
542        /// CPU quota in units of 10<sup>-9</sup> CPUs. Set to 0 (default) for there to be no limit.
543        ///
544        /// For example, setting `nano_cpus` to `500_000_000` results in the container being allocated
545        /// 50% of a single CPU, while `2_000_000_000` results in the container being allocated 2 CPUs.
546        nano_cpus: u64 => "HostConfig.NanoCpus"
547    );
548
549    /// CPU quota in units of CPUs. This is a wrapper around `nano_cpus` to do the unit conversion.
550    ///
551    /// See [`nano_cpus`](#method.nano_cpus).
552    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    /// Sets an integer value representing the container's relative CPU weight versus other containers.
558    cpu_shares: u32 => "HostConfig.CpuShares");
559
560    impl_map_field!(json labels => "Labels");
561
562    /// Whether to attach to `stdin`.
563    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    /// Whether to attach to `stdout`.
571    attach_stdout: bool => "AttachStdout");
572
573    impl_field!(
574    /// Whether to attach to `stderr`.
575    attach_stderr: bool => "AttachStderr");
576
577    impl_field!(
578    /// Whether standard streams should be attached to a TTY.
579    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    /// Signal to stop a container as a string. Default is \"SIGTERM\"
620    stop_signal => "StopSignal");
621
622    impl_field!(
623    /// Signal to stop a container as an integer. Default is 15 (SIGTERM).
624    stop_signal_num: u64 => "StopSignal");
625
626    impl_field!(
627    /// Timeout to stop a container. Only seconds are counted. Default is 10s
628    stop_timeout: Duration => "StopTimeout");
629
630    impl_str_field!(userns_mode => "HostConfig.UsernsMode");
631
632    impl_field!(privileged: bool => "HostConfig.Privileged");
633
634    impl_str_field!(user => "User");
635
636    pub fn build(&self) -> ContainerCreateOpts {
637        ContainerCreateOpts {
638            name: self.name.clone(),
639            params: self.params.clone(),
640        }
641    }
642
643    impl_str_field!(
644    /// The hostname to use for the container, as a valid RFC 1123 hostname.
645        hostname => "Hostname"
646    );
647
648    impl_str_field!(
649    /// The domain name to use for the container.
650        domainname => "Domainname"
651    );
652
653    impl_str_enum_field!(
654    /// IPC sharing mode for the container. Default is "private" or "shareable", depending on daemon version.
655        ipc: IpcMode => "HostConfig.IpcMode"
656    );
657
658    impl_str_enum_field!(
659    /// Set the PID (Process) Namespace mode for the container.
660        pid: PidMode => "HostConfig.PidMode"
661    );
662
663    impl_field!(
664    /// Represents the container's networking configuration for each of its interfaces.
665        network_config: NetworkingConfig => "NetworkingConfig"
666    );
667
668    impl_str_field!(
669        /// Runtime to use for this container like "nvidia"
670        runtime => "HostConfig.Runtime"
671    );
672
673    impl_field!(
674        /// Requested list of available devices with capabilities
675        device_requests: Vec<DeviceRequest> => "HostConfig.DeviceRequests"
676    );
677}
678
679impl_opts_builder!(url => ContainerRemove);
680
681impl ContainerRemoveOptsBuilder {
682    impl_url_bool_field!(
683        /// If the container is running, kill it before removing it.
684        force => "force"
685    );
686
687    impl_url_bool_field!(
688        /// Remove anonymous volumes associated with the container.
689        volumes => "v"
690    );
691
692    impl_url_bool_field!(
693        /// Remove the specified link associated with the container.
694        link => "link"
695    );
696}
697
698impl_opts_builder!(url => ContainerPrune);
699
700pub enum ContainerPruneFilter {
701    /// Prune containers created before this timestamp. The <timestamp> can be Unix timestamps,
702    /// date formatted timestamps, or Go duration strings (e.g. 10m, 1h30m) computed relative to
703    /// the daemon machine’s time.
704    Until(String),
705    #[cfg(feature = "chrono")]
706    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
707    /// Prune containers created before this timestamp. Same as `Until` but takes a datetime object.
708    UntilDate(chrono::DateTime<chrono::Utc>),
709    /// Label in the form of `label=key`.
710    LabelKey(String),
711    /// Label in the form of `label=key=val`.
712    Label(String, String),
713}
714
715impl Filter for ContainerPruneFilter {
716    fn query_item(&self) -> FilterItem {
717        use ContainerPruneFilter::*;
718        match &self {
719            Until(until) => FilterItem::new("until", until.to_owned()),
720            #[cfg(feature = "chrono")]
721            UntilDate(until) => FilterItem::new("until", until.timestamp().to_string()),
722            LabelKey(label) => FilterItem::new("label", label.to_owned()),
723            Label(key, val) => FilterItem::new("label", format!("{key}={val}")),
724        }
725    }
726}
727
728impl ContainerPruneOptsBuilder {
729    impl_filter_func!(ContainerPruneFilter);
730}
731
732impl_opts_builder!(url => ContainerCommit);
733
734impl ContainerCommitOpts {
735    pub(crate) fn with_container(&self, id: &str) -> Self {
736        // not exactly a nice solution but temporary
737        let mut s = self.clone();
738        s.params.insert("container", id.to_owned());
739        s
740    }
741}
742
743impl ContainerCommitOptsBuilder {
744    impl_url_str_field!(
745        /// Repository name for the created image
746        repo => "repo"
747    );
748    impl_url_str_field!(
749        /// Tag name for the created image
750        tag => "tag"
751    );
752    impl_url_str_field!(
753        /// Commit message
754        comment => "comment"
755    );
756    impl_url_str_field!(
757        /// Author of the image (e.g., John Hannibal Smith <hannibal@a-team.com>)
758        author => "author"
759    );
760    impl_url_bool_field!(
761        /// Whether to pause the container before committing
762        pause => "pause"
763    );
764    impl_url_str_field!(
765        /// Dockerfile instructions to apply while committing
766        changes => "changes"
767    );
768}
769
770impl_opts_builder!(url => ContainerStop);
771
772impl ContainerStopOptsBuilder {
773    impl_url_str_field!(
774        /// Signal to send to the container as an integer or string (e.g. `SIGINT`).
775        signal => "signal"
776    );
777
778    /// Duration to wait before stopping the container
779    pub fn wait(mut self, duration: Duration) -> Self {
780        self.params.insert("t", duration.as_secs().to_string());
781        self
782    }
783}
784
785impl_opts_builder!(url => ContainerRestart);
786
787impl ContainerRestartOptsBuilder {
788    impl_url_str_field!(
789        /// Signal to send to the container as an integer or string (e.g. `SIGINT`).
790        signal => "signal"
791    );
792
793    /// Duration to wait before restarting the container
794    pub fn wait(mut self, duration: Duration) -> Self {
795        self.params.insert("t", duration.as_secs().to_string());
796        self
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    macro_rules! test_case {
805        ($opts:expr, $want:expr) => {
806            let opts = $opts.build();
807
808            pretty_assertions::assert_eq!($want, opts.serialize().unwrap())
809        };
810    }
811
812    #[test]
813    fn create_container_opts() {
814        test_case!(
815            ContainerCreateOptsBuilder::default().image("test_image"),
816            r#"{"HostConfig":{},"Image":"test_image"}"#
817        );
818
819        test_case!(
820            ContainerCreateOptsBuilder::default()
821                .image("test_image")
822                .env(vec!["foo", "bar"]),
823            r#"{"Env":["foo","bar"],"HostConfig":{},"Image":"test_image"}"#
824        );
825
826        test_case!(
827            ContainerCreateOptsBuilder::default()
828                .image("test_image")
829                .env(["foo", "bar", "baz"]),
830            r#"{"Env":["foo","bar","baz"],"HostConfig":{},"Image":"test_image"}"#
831        );
832
833        test_case!(
834            ContainerCreateOptsBuilder::default()
835                .image("test_image")
836                .env(std::iter::once("test")),
837            r#"{"Env":["test"],"HostConfig":{},"Image":"test_image"}"#
838        );
839
840        test_case!(
841            ContainerCreateOptsBuilder::default()
842                .image("test_image")
843                .user("alice"),
844            r#"{"HostConfig":{},"Image":"test_image","User":"alice"}"#
845        );
846
847        test_case!(
848            ContainerCreateOptsBuilder::default()
849                .image("test_image")
850                .network_mode("host")
851                .auto_remove(true)
852                .privileged(true),
853            r#"{"HostConfig":{"AutoRemove":true,"NetworkMode":"host","Privileged":true},"Image":"test_image"}"#
854        );
855
856        test_case!(
857            ContainerCreateOptsBuilder::default()
858                .image("test_image")
859                .expose(PublishPort::tcp(80), 8080),
860            r#"{"ExposedPorts":{"80/tcp":{}},"HostConfig":{"PortBindings":{"80/tcp":[{"HostPort":"8080"}]}},"Image":"test_image"}"#
861        );
862
863        test_case!(
864            ContainerCreateOptsBuilder::default()
865                .image("test_image")
866                .expose(PublishPort::udp(80), 8080)
867                .expose(PublishPort::sctp(81), 8081),
868            r#"{"ExposedPorts":{"80/udp":{},"81/sctp":{}},"HostConfig":{"PortBindings":{"80/udp":[{"HostPort":"8080"}],"81/sctp":[{"HostPort":"8081"}]}},"Image":"test_image"}"#
869        );
870
871        test_case!(
872            ContainerCreateOptsBuilder::default()
873                .image("test_image")
874                .publish(PublishPort::udp(80))
875                .publish(PublishPort::sctp(6969))
876                .publish(PublishPort::tcp(1337)),
877            r#"{"ExposedPorts":{"1337/tcp":{},"6969/sctp":{},"80/udp":{}},"HostConfig":{},"Image":"test_image"}"#
878        );
879
880        test_case!(
881            ContainerCreateOptsBuilder::default()
882                .image("test_image")
883                .expose(
884                    PublishPort::tcp(80),
885                    "[::1]:8080".parse::<SocketAddr>().unwrap()
886                ),
887            r#"{"ExposedPorts":{"80/tcp":{}},"HostConfig":{"PortBindings":{"80/tcp":[{"HostIp":"::1","HostPort":"8080"}]}},"Image":"test_image"}"#
888        );
889
890        test_case!(
891            ContainerCreateOptsBuilder::default()
892                .image("test_image")
893                .publish_all_ports(),
894            r#"{"HostConfig":{"PublishAllPorts":true},"Image":"test_image"}"#
895        );
896
897        test_case!(
898            ContainerCreateOptsBuilder::default()
899                .image("test_image")
900                .log_driver("fluentd"),
901            r#"{"HostConfig":{"LogConfig":{"Type":"fluentd"}},"Image":"test_image"}"#
902        );
903
904        test_case!(
905            ContainerCreateOptsBuilder::default()
906                .image("test_image")
907                .log_driver_config(vec![("tag", "container-tag")]),
908            r#"{"HostConfig":{"LogConfig":{"Config":{"tag":"container-tag"}}},"Image":"test_image"}"#
909        );
910
911        test_case!(
912            ContainerCreateOptsBuilder::default()
913                .image("test_image")
914                .restart_policy("on-failure", 10),
915            r#"{"HostConfig":{"RestartPolicy":{"MaximumRetryCount":10,"Name":"on-failure"}},"Image":"test_image"}"#
916        );
917
918        test_case!(
919            ContainerCreateOptsBuilder::default()
920                .image("test_image")
921                .restart_policy("always", 0),
922            r#"{"HostConfig":{"RestartPolicy":{"Name":"always"}},"Image":"test_image"}"#
923        );
924    }
925}