stackify_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_field!(
635        /// Run an init inside the container that forwards signals and reaps processes. This field is
636        /// omitted if empty, and the default (as configured on the daemon) is used.
637        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    /// The hostname to use for the container, as a valid RFC 1123 hostname.
651        hostname => "Hostname"
652    );
653
654    impl_str_field!(
655    /// The domain name to use for the container.
656        domainname => "Domainname"
657    );
658
659    impl_str_enum_field!(
660    /// IPC sharing mode for the container. Default is "private" or "shareable", depending on daemon version.
661        ipc: IpcMode => "HostConfig.IpcMode"
662    );
663
664    impl_str_enum_field!(
665    /// Set the PID (Process) Namespace mode for the container.
666        pid: PidMode => "HostConfig.PidMode"
667    );
668
669    impl_field!(
670    /// Represents the container's networking configuration for each of its interfaces.
671        network_config: NetworkingConfig => "NetworkingConfig"
672    );
673
674    impl_str_field!(
675        /// Runtime to use for this container like "nvidia"
676        runtime => "HostConfig.Runtime"
677    );
678
679    impl_field!(
680        /// Requested list of available devices with capabilities
681        device_requests: Vec<DeviceRequest> => "HostConfig.DeviceRequests"
682    );
683}
684
685impl_opts_builder!(url => ContainerRemove);
686
687impl ContainerRemoveOptsBuilder {
688    impl_url_bool_field!(
689        /// If the container is running, kill it before removing it.
690        force => "force"
691    );
692
693    impl_url_bool_field!(
694        /// Remove anonymous volumes associated with the container.
695        volumes => "v"
696    );
697
698    impl_url_bool_field!(
699        /// Remove the specified link associated with the container.
700        link => "link"
701    );
702}
703
704impl_opts_builder!(url => ContainerPrune);
705
706pub enum ContainerPruneFilter {
707    /// Prune containers created before this timestamp. The <timestamp> can be Unix timestamps,
708    /// date formatted timestamps, or Go duration strings (e.g. 10m, 1h30m) computed relative to
709    /// the daemon machine’s time.
710    Until(String),
711    #[cfg(feature = "chrono")]
712    #[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
713    /// Prune containers created before this timestamp. Same as `Until` but takes a datetime object.
714    UntilDate(chrono::DateTime<chrono::Utc>),
715    /// Label in the form of `label=key`.
716    LabelKey(String),
717    /// Label in the form of `label=key=val`.
718    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        // not exactly a nice solution but temporary
743        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        /// Repository name for the created image
752        repo => "repo"
753    );
754    impl_url_str_field!(
755        /// Tag name for the created image
756        tag => "tag"
757    );
758    impl_url_str_field!(
759        /// Commit message
760        comment => "comment"
761    );
762    impl_url_str_field!(
763        /// Author of the image (e.g., John Hannibal Smith <hannibal@a-team.com>)
764        author => "author"
765    );
766    impl_url_bool_field!(
767        /// Whether to pause the container before committing
768        pause => "pause"
769    );
770    impl_url_str_field!(
771        /// Dockerfile instructions to apply while committing
772        changes => "changes"
773    );
774}
775
776impl_opts_builder!(url => ContainerStop);
777
778impl ContainerStopOptsBuilder {
779    impl_url_str_field!(
780        /// Signal to send to the container as an integer or string (e.g. `SIGINT`).
781        signal => "signal"
782    );
783
784    /// Duration to wait before stopping the container
785    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 to send to the container as an integer or string (e.g. `SIGINT`).
796        signal => "signal"
797    );
798
799    /// Duration to wait before restarting the container
800    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}