summaryrefslogtreecommitdiffstats
path: root/src/container.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/container.rs')
-rw-r--r--src/container.rs1819
1 files changed, 1819 insertions, 0 deletions
diff --git a/src/container.rs b/src/container.rs
new file mode 100644
index 0000000..60f7d61
--- /dev/null
+++ b/src/container.rs
@@ -0,0 +1,1819 @@
+//! Create and manage containers.
+//!
+//! API Reference: <https://docs.docker.com/engine/api/v1.41/#tag/Container>
+
+use std::{collections::HashMap, hash::Hash, io, iter::Peekable, path::Path, time::Duration};
+
+use futures_util::{
+ io::{AsyncRead, AsyncWrite},
+ stream::Stream,
+ TryStreamExt,
+};
+use hyper::Body;
+use mime::Mime;
+use serde::{Deserialize, Serialize};
+use serde_json::{json, Map, Value};
+use url::form_urlencoded;
+
+use crate::{
+ docker::Docker,
+ errors::{Error, Result},
+ exec::{Exec, ExecContainerOptions},
+ image::ContainerConfig,
+ network::NetworkSettings,
+ transport::Payload,
+ tty::{self, Multiplexer as TtyMultiPlexer},
+};
+
+#[cfg(feature = "chrono")]
+use crate::datetime::datetime_from_unix_timestamp;
+#[cfg(feature = "chrono")]
+use chrono::{DateTime, Utc};
+
+/// Interface for accessing and manipulating a docker container
+///
+/// [Api Reference](https://docs.docker.com/engine/api/v1.41/#tag/Container)
+pub struct Container<'docker> {
+ docker: &'docker Docker,
+ id: String,
+}
+
+impl<'docker> Container<'docker> {
+ /// Exports an interface exposing operations against a container instance
+ pub fn new<S>(
+ docker: &'docker Docker,
+ id: S,
+ ) -> Self
+ where
+ S: Into<String>,
+ {
+ Container {
+ docker,
+ id: id.into(),
+ }
+ }
+
+ /// a getter for the container id
+ pub fn id(&self) -> &str {
+ &self.id
+ }
+
+ /// Inspects the current docker container instance's details
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerInspect)
+ pub async fn inspect(&self) -> Result<ContainerDetails> {
+ self.docker
+ .get_json::<ContainerDetails>(&format!("/containers/{}/json", self.id)[..])
+ .await
+ }
+
+ /// Returns a `top` view of information about the container process
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerTop)
+ pub async fn top(
+ &self,
+ psargs: Option<&str>,
+ ) -> Result<Top> {
+ let mut path = vec![format!("/containers/{}/top", self.id)];
+ if let Some(ref args) = psargs {
+ let encoded = form_urlencoded::Serializer::new(String::new())
+ .append_pair("ps_args", args)
+ .finish();
+ path.push(encoded)
+ }
+ self.docker.get_json(&path.join("?")).await
+ }
+
+ /// Returns a stream of logs emitted but the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerLogs)
+ pub fn logs(
+ &self,
+ opts: &LogsOptions,
+ ) -> impl Stream<Item = Result<tty::TtyChunk>> + Unpin + 'docker {
+ let mut path = vec![format!("/containers/{}/logs", self.id)];
+ if let Some(query) = opts.serialize() {
+ path.push(query)
+ }
+
+ let stream = Box::pin(self.docker.stream_get(path.join("?")));
+
+ Box::pin(tty::decode(stream))
+ }
+
+ /// Attaches a multiplexed TCP stream to the container that can be used to read Stdout, Stderr and write Stdin.
+ async fn attach_raw(&self) -> Result<impl AsyncRead + AsyncWrite + Send + 'docker> {
+ self.docker
+ .stream_post_upgrade(
+ format!(
+ "/containers/{}/attach?stream=1&stdout=1&stderr=1&stdin=1",
+ self.id
+ ),
+ None,
+ )
+ .await
+ }
+
+ /// Attaches a [Multiplexer](crate::tty::Multiplexer) to the container.
+ ///
+ /// The [Multiplexer](crate::tty::Multiplexer) implements Stream for returning Stdout and
+ /// Stderr chunks. It also implements `[AsyncWrite]` for writing to Stdin.
+ ///
+ /// The multiplexer can be split into its read and write halves with the
+ /// [split](crate::tty::Multiplexer::split) method
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerAttach)
+ pub async fn attach(&self) -> Result<TtyMultiPlexer<'docker>> {
+ let tcp_stream = self.attach_raw().await?;
+
+ Ok(TtyMultiPlexer::new(tcp_stream))
+ }
+
+ /// Returns a set of changes made to the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerChanges)
+ pub async fn changes(&self) -> Result<Vec<Change>> {
+ self.docker
+ .get_json::<Vec<Change>>(&format!("/containers/{}/changes", self.id)[..])
+ .await
+ }
+
+ /// Exports the current docker container into a tarball
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerExport)
+ pub fn export(&self) -> impl Stream<Item = Result<Vec<u8>>> + 'docker {
+ self.docker
+ .stream_get(format!("/containers/{}/export", self.id))
+ .map_ok(|c| c.to_vec())
+ }
+
+ /// Returns a stream of stats specific to this container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerStats)
+ pub fn stats(&self) -> impl Stream<Item = Result<Stats>> + Unpin + 'docker {
+ let codec = futures_codec::LinesCodec {};
+
+ let reader = Box::pin(
+ self.docker
+ .stream_get(format!("/containers/{}/stats", self.id))
+ .map_err(|e| io::Error::new(io::ErrorKind::Other, e)),
+ )
+ .into_async_read();
+
+ Box::pin(
+ futures_codec::FramedRead::new(reader, codec)
+ .map_err(Error::IO)
+ .and_then(|s: String| async move {
+ serde_json::from_str(&s).map_err(Error::SerdeJsonError)
+ }),
+ )
+ }
+
+ /// Start the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerStart)
+ pub async fn start(&self) -> Result<()> {
+ self.docker
+ .post(&format!("/containers/{}/start", self.id)[..], None)
+ .await?;
+ Ok(())
+ }
+
+ /// Stop the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerStop)
+ pub async fn stop(
+ &self,
+ wait: Option<Duration>,
+ ) -> Result<()> {
+ let mut path = vec![format!("/containers/{}/stop", self.id)];
+ if let Some(w) = wait {
+ let encoded = form_urlencoded::Serializer::new(String::new())
+ .append_pair("t", &w.as_secs().to_string())
+ .finish();
+
+ path.push(encoded)
+ }
+ self.docker.post(&path.join("?"), None).await?;
+ Ok(())
+ }
+
+ /// Restart the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerRestart)
+ pub async fn restart(
+ &self,
+ wait: Option<Duration>,
+ ) -> Result<()> {
+ let mut path = vec![format!("/containers/{}/restart", self.id)];
+ if let Some(w) = wait {
+ let encoded = form_urlencoded::Serializer::new(String::new())
+ .append_pair("t", &w.as_secs().to_string())
+ .finish();
+ path.push(encoded)
+ }
+ self.docker.post(&path.join("?"), None).await?;
+ Ok(())
+ }
+
+ /// Kill the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerKill)
+ pub async fn kill(
+ &self,
+ signal: Option<&str>,
+ ) -> Result<()> {
+ let mut path = vec![format!("/containers/{}/kill", self.id)];
+ if let Some(sig) = signal {
+ let encoded = form_urlencoded::Serializer::new(String::new())
+ .append_pair("signal", &sig.to_owned())
+ .finish();
+ path.push(encoded)
+ }
+ self.docker.post(&path.join("?"), None).await?;
+ Ok(())
+ }
+
+ /// Rename the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerRename)
+ pub async fn rename(
+ &self,
+ name: &str,
+ ) -> Result<()> {
+ let query = form_urlencoded::Serializer::new(String::new())
+ .append_pair("name", name)
+ .finish();
+ self.docker
+ .post(
+ &format!("/containers/{}/rename?{}", self.id, query)[..],
+ None,
+ )
+ .await?;
+ Ok(())
+ }
+
+ /// Pause the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerPause)
+ pub async fn pause(&self) -> Result<()> {
+ self.docker
+ .post(&format!("/containers/{}/pause", self.id)[..], None)
+ .await?;
+ Ok(())
+ }
+
+ /// Unpause the container instance
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerUnpause)
+ pub async fn unpause(&self) -> Result<()> {
+ self.docker
+ .post(&format!("/containers/{}/unpause", self.id)[..], None)
+ .await?;
+ Ok(())
+ }
+
+ /// Wait until the container stops
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerWait)
+ pub async fn wait(&self) -> Result<Exit> {
+ self.docker
+ .post_json(format!("/containers/{}/wait", self.id), Payload::None)
+ .await
+ }
+
+ /// Delete the container instance
+ ///
+ /// Use remove instead to use the force/v options.
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerDelete)
+ pub async fn delete(&self) -> Result<()> {
+ self.docker
+ .delete(&format!("/containers/{}", self.id)[..])
+ .await?;
+ Ok(())
+ }
+
+ /// Delete the container instance (todo: force/v)
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerRemove)
+ pub async fn remove(
+ &self,
+ opts: RmContainerOptions,
+ ) -> Result<()> {
+ let mut path = vec![format!("/containers/{}", self.id)];
+ if let Some(query) = opts.serialize() {
+ path.push(query)
+ }
+ self.docker.delete(&path.join("?")).await?;
+ Ok(())
+ }
+
+ /// Execute a command in this container
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#tag/Exec)
+ pub fn exec(
+ &self,
+ opts: &ExecContainerOptions,
+ ) -> impl Stream<Item = Result<tty::TtyChunk>> + Unpin + 'docker {
+ Exec::create_and_start(self.docker, &self.id, opts)
+ }
+
+ /// Copy a file/folder from the container. The resulting stream is a tarball of the extracted
+ /// files.
+ ///
+ /// If `path` is not an absolute path, it is relative to the container’s root directory. The
+ /// resource specified by `path` must exist. To assert that the resource is expected to be a
+ /// directory, `path` should end in `/` or `/`. (assuming a path separator of `/`). If `path`
+ /// ends in `/.` then this indicates that only the contents of the path directory should be
+ /// copied. A symlink is always resolved to its target.
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerArchive)
+ pub fn copy_from(
+ &self,
+ path: &Path,
+ ) -> impl Stream<Item = Result<Vec<u8>>> + 'docker {
+ let path_arg = form_urlencoded::Serializer::new(String::new())
+ .append_pair("path", &path.to_string_lossy())
+ .finish();
+
+ let endpoint = format!("/containers/{}/archive?{}", self.id, path_arg);
+ self.docker.stream_get(endpoint).map_ok(|c| c.to_vec())
+ }
+
+ /// Copy a byte slice as file into (see `bytes`) the container.
+ ///
+ /// The file will be copied at the given location (see `path`) and will be owned by root
+ /// with access mask 644.
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/PutContainerArchive)
+ pub async fn copy_file_into<P: AsRef<Path>>(
+ &self,
+ path: P,
+ bytes: &[u8],
+ ) -> Result<()> {
+ let path = path.as_ref();
+
+ let mut ar = tar::Builder::new(Vec::new());
+ let mut header = tar::Header::new_gnu();
+ header.set_size(bytes.len() as u64);
+ header.set_mode(0o0644);
+ ar.append_data(
+ &mut header,
+ path.to_path_buf()
+ .iter()
+ .skip(1)
+ .collect::<std::path::PathBuf>(),
+ bytes,
+ )?;
+ let data = ar.into_inner()?;
+
+ self.copy_to(Path::new("/"), data.into()).await?;
+ Ok(())
+ }
+
+ /// Copy a tarball (see `body`) to the container.
+ ///
+ /// The tarball will be copied to the container and extracted at the given location (see `path`).
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/PutContainerArchive)
+ pub async fn copy_to(
+ &self,
+ path: &Path,
+ body: Body,
+ ) -> Result<()> {
+ let path_arg = form_urlencoded::Serializer::new(String::new())
+ .append_pair("path", &path.to_string_lossy())
+ .finish();
+
+ let mime = "application/x-tar".parse::<Mime>().unwrap();
+
+ self.docker
+ .put(
+ &format!("/containers/{}/archive?{}", self.id, path_arg),
+ Some((body, mime)),
+ )
+ .await?;
+ Ok(())
+ }
+}
+
+/// Interface for docker containers
+///
+/// [Api Reference](https://docs.docker.com/engine/api/v1.41/#tag/Containers)
+pub struct Containers<'docker> {
+ docker: &'docker Docker,
+}
+
+impl<'docker> Containers<'docker> {
+ /// Exports an interface for interacting with docker containers
+ pub fn new(docker: &'docker Docker) -> Self {
+ Containers { docker }
+ }
+
+ /// Lists the container instances on the docker host
+ ///
+ /// [Api Reference](https://docs.docker.com/engine/api/v1.41/#operation/ContainerList)
+ pub async fn list(
+ &self,
+ opts: &ContainerListOptions,
+ ) -> Result<Vec<ContainerInfo>> {
+ let mut path = vec!["/containers/json".to_owned()];
+ if let Some(query) = opts.serialize() {
+ path.push(query)
+ }
+ self.docker
+ .get_json::<Vec<ContainerInfo>>(&path.join("?"))
+ .await
+ }
+
+ /// Returns a reference to a set of operations available to a specific container instance
+ pub fn get<S>(
+ &self,
+ name: S,
+ ) -> Container<'docker>
+ where
+ S: Into<String>,
+ {
+ Container::new(self.docker, name)
+ }
+
+ /// Returns a builder interface for creating a new container instance
+ pub async fn create(
+ &self,
+ opts: &ContainerOptions,
+ ) -> Result<ContainerCreateInfo> {
+ let body: Body = opts.serialize()?.into();
+ let mut path = vec!["/containers/create".to_owned()];
+
+ if let Some(ref name) = opts.name {
+ path.push(
+ form_urlencoded::Serializer::new(String::new())
+ .append_pair("name", name)
+ .finish(),
+ );
+ }
+
+ self.docker
+ .post_json(&path.join("?"), Some((body, mime::APPLICATION_JSON)))
+ .await
+ }
+}
+
+/// Options for filtering container list results
+#[derive(Default, Debug)]
+pub struct ContainerListOptions {
+ params: HashMap<&'static str, String>,
+}
+
+impl ContainerListOptions {
+ /// return a new instance of a builder for options
+ pub fn builder() -> ContainerListOptionsBuilder {
+ ContainerListOptionsBuilder::default()
+ }
+
+ /// serialize options as a string. returns None if no options are defined
+ pub fn serialize(&self) -> Option<String> {
+ if self.params.is_empty() {
+ None
+ } else {
+ Some(
+ form_urlencoded::Serializer::new(String::new())
+ .extend_pairs(&self.params)
+ .finish(),
+ )
+ }
+ }
+}
+
+/// Filter options for container listings
+pub enum ContainerFilter {
+ ExitCode(u64),
+ Status(String),
+ LabelName(String),
+ Label(String, String),
+ Name(String),
+}
+
+/// Builder interface for `ContainerListOptions`
+#[derive(Default)]
+pub struct ContainerListOptionsBuilder {
+ params: HashMap<&'static str, String>,
+}
+
+impl ContainerListOptionsBuilder {
+ pub fn filter(
+ &mut self,
+ filters: Vec<ContainerFilter>,
+ ) -> &mut Self {
+ let mut param: HashMap<&str, Vec<String>> = HashMap::new();
+ for f in filters {
+ let (key, value) = match f {
+ ContainerFilter::ExitCode(c) => ("exited", c.to_string()),
+ ContainerFilter::Status(s) => ("status", s),
+ ContainerFilter::LabelName(n) => ("label", n),
+ ContainerFilter::Label(n, v) => ("label", format!("{}={}", n, v)),
+ ContainerFilter::Name(n) => ("name", n.to_string()),
+ };
+
+ param.entry(key).or_insert_with(Vec::new).push(value);
+ }
+ // structure is a a json encoded object mapping string keys to a list
+ // of string values
+ self.params
+ .insert("filters", serde_json::to_string(&param).unwrap());
+ self
+ }
+
+ pub fn all(&mut self) -> &mut Self {
+ self.params.insert("all", "true".to_owned());
+ self
+ }
+
+ pub fn since(
+ &mut self,
+ since: &str,
+ ) -> &mut Self {
+ self.params.insert("since", since.to_owned());
+ self
+ }
+
+ pub fn before(
+ &mut self,
+ before: &str,
+ ) -> &mut Self {
+ self.params.insert("before", before.to_owned());
+ self
+ }
+
+ pub fn sized(&mut self) -> &mut Self {
+ self.params.insert("size", "true".to_owned());
+ self
+ }
+
+ pub fn build(&self) -> ContainerListOptions {
+ ContainerListOptions {
+ params: self.params.clone(),
+ }
+ }
+}
+
+/// Interface for building a new docker container from an existing image
+#[derive(Serialize, Debug)]
+pub struct ContainerOptions {
+ pub name: Option<String>,
+ params: HashMap<&'static str, Value>,
+}
+
+/// Function to insert a JSON value into a tree where the desired
+/// location of the value is given as a path of JSON keys.
+fn insert<'a, I, V>(
+ key_path: &mut Peekable<I>,
+ value: &V,
+ parent_node: &mut Value,
+) where
+ V: Serialize,
+ I: Iterator<Item = &'a str>,
+{
+ let local_key = key_path.next().unwrap();
+
+ if key_path.peek().is_some() {
+ let node = parent_node
+ .as_object_mut()
+ .unwrap()
+ .entry(local_key.to_string())
+ .or_insert(Value::Object(Map::new()));
+
+ insert(key_path, value, node);
+ } else {
+ parent_node
+ .as_object_mut()
+ .unwrap()
+ .insert(local_key.to_string(), serde_json::to_value(value).unwrap());
+ }
+}
+
+impl ContainerOptions {
+ /// return a new instance of a builder for options
+ pub fn builder(name: &str) -> ContainerOptionsBuilder {
+ ContainerOptionsBuilder::new(name)
+ }
+
+ /// serialize options as a string. returns None if no options are defined
+ pub fn serialize(&self) -> Result<String> {
+ serde_json::to_string(&self.to_json()).map_err(Error::from)
+ }
+
+ fn to_json(&self) -> Value {
+ let mut body_members = Map::new();
+ // The HostConfig element gets initialized to an empty object,
+ // for backward compatibility.
+ body_members.insert("HostConfig".to_string(), Value::Object(Map::new()));
+ let mut body = Value::Object(body_members);
+ self.parse_from(&self.params, &mut body);
+ body
+ }
+
+ pub fn parse_from<'a, K, V>(
+ &self,
+ params: &'a HashMap<K, V>,
+ body: &mut Value,
+ ) where
+ &'a HashMap<K, V>: IntoIterator,
+ K: ToString + Eq + Hash,
+ V: Serialize,
+ {
+ for (k, v) in params.iter() {
+ let key_string = k.to_string();
+ insert(&mut key_string.split('.').peekable(), v, body)
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct ContainerOptionsBuilder {
+ name: Option<String>,
+ params: HashMap<&'static str, Value>,
+}
+
+impl ContainerOptionsBuilder {
+ pub(crate) fn new(image: &str) -> Self {
+ let mut params = HashMap::new();
+
+ params.insert("Image", Value::String(image.to_owned()));
+ ContainerOptionsBuilder { name: None, params }
+ }
+
+ pub fn name(
+ &mut self,
+ name: &str,
+ ) -> &mut Self {
+ self.name = Some(name.to_owned());
+ self
+ }
+
+ /// Specify the working dir (corresponds to the `-w` docker cli argument)
+ pub fn working_dir(
+ &mut self,
+ working_dir: &str,
+ ) -> &mut Self {
+ self.params.insert("WorkingDir", json!(working_dir));
+ self
+ }
+
+ /// Specify any bind mounts, taking the form of `/some/host/path:/some/container/path`
+ pub fn volumes(
+ &mut self,
+ volumes: Vec<&str>,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.Binds", json!(volumes));
+ self
+ }
+
+ /// enable all exposed ports on the container to be mapped to random, available, ports on the host
+ pub fn publish_all_ports(&mut self) -> &mut Self {
+ self.params
+ .insert("HostConfig.PublishAllPorts", json!(true));
+ self
+ }
+
+ pub fn expose(
+ &mut self,
+ srcport: u32,
+ protocol: &str,
+ hostport: u32,
+ ) -> &mut Self {
+ let mut exposedport: HashMap<String, String> = HashMap::new();
+ exposedport.insert("HostPort".to_string(), hostport.to_string());
+
+ // The idea here is to go thought the 'old' port binds and to apply them to the local
+ // 'port_bindings' variable, add the bind we want and replace the 'old' value
+ let mut port_bindings: HashMap<String, Value> = HashMap::new();
+ for (key, val) in self
+ .params
+ .get("HostConfig.PortBindings")
+ .unwrap_or(&json!(null))
+ .as_object()
+ .unwrap_or(&Map::new())
+ .iter()
+ {
+ port_bindings.insert(key.to_string(), json!(val));
+ }
+ port_bindings.insert(
+ format!("{}/{}", srcport, protocol),
+ json!(vec![exposedport]),
+ );
+
+ self.params
+ .insert("HostConfig.PortBindings", json!(port_bindings));
+
+ // Replicate the port bindings over to the exposed ports config
+ let mut exposed_ports: HashMap<String, Value> = HashMap::new();
+ let empty_config: HashMap<String, Value> = HashMap::new();
+ for key in port_bindings.keys() {
+ exposed_ports.insert(key.to_string(), json!(empty_config));
+ }
+
+ self.params.insert("ExposedPorts", json!(exposed_ports));
+
+ self
+ }
+
+ /// Publish a port in the container without assigning a port on the host
+ pub fn publish(
+ &mut self,
+ srcport: u32,
+ protocol: &str,
+ ) -> &mut Self {
+ /* The idea here is to go thought the 'old' port binds
+ * and to apply them to the local 'exposedport_bindings' variable,
+ * add the bind we want and replace the 'old' value */
+ let mut exposed_port_bindings: HashMap<String, Value> = HashMap::new();
+ for (key, val) in self
+ .params
+ .get("ExposedPorts")
+ .unwrap_or(&json!(null))
+ .as_object()
+ .unwrap_or(&Map::new())
+ .iter()
+ {
+ exposed_port_bindings.insert(key.to_string(), json!(val));
+ }
+ exposed_port_bindings.insert(format!("{}/{}", srcport, protocol), json!({}));
+
+ // Replicate the port bindings over to the exposed ports config
+ let mut exposed_ports: HashMap<String, Value> = HashMap::new();
+ let empty_config: HashMap<String, Value> = HashMap::new();
+ for key in exposed_port_bindings.keys() {
+ exposed_ports.insert(key.to_string(), json!(empty_config));
+ }
+
+ self.params.insert("ExposedPorts", json!(exposed_ports));
+
+ self
+ }
+
+ pub fn links(
+ &mut self,
+ links: Vec<&str>,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.Links", json!(links));
+ self
+ }
+
+ pub fn memory(
+ &mut self,
+ memory: u64,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.Memory", json!(memory));
+ self
+ }
+
+ /// Total memory limit (memory + swap) in bytes. Set to -1 (default) to enable unlimited swap.
+ pub fn memory_swap(
+ &mut self,
+ memory_swap: i64,
+ ) -> &mut Self {
+ self.params
+ .insert("HostConfig.MemorySwap", json!(memory_swap));
+ self
+ }
+
+ /// CPU quota in units of 10<sup>-9</sup> CPUs. Set to 0 (default) for there to be no limit.
+ ///
+ /// For example, setting `nano_cpus` to `500_000_000` results in the container being allocated
+ /// 50% of a single CPU, while `2_000_000_000` results in the container being allocated 2 CPUs.
+ pub fn nano_cpus(
+ &mut self,
+ nano_cpus: u64,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.NanoCpus", json!(nano_cpus));
+ self
+ }
+
+ /// CPU quota in units of CPUs. This is a wrapper around `nano_cpus` to do the unit conversion.
+ ///
+ /// See [`nano_cpus`](#method.nano_cpus).
+ pub fn cpus(
+ &mut self,
+ cpus: f64,
+ ) -> &mut Self {
+ self.nano_cpus((1_000_000_000.0 * cpus) as u64)
+ }
+
+ /// Sets an integer value representing the container's relative CPU weight versus other
+ /// containers.
+ pub fn cpu_shares(
+ &mut self,
+ cpu_shares: u32,
+ ) -> &mut Self {
+ self.params
+ .insert("HostConfig.CpuShares", json!(cpu_shares));
+ self
+ }
+
+ pub fn labels(
+ &mut self,
+ labels: &HashMap<&str, &str>,
+ ) -> &mut Self {
+ self.params.insert("Labels", json!(labels));
+ self
+ }
+
+ /// Whether to attach to `stdin`.
+ pub fn attach_stdin(
+ &mut self,
+ attach: bool,
+ ) -> &mut Self {
+ self.params.insert("AttachStdin", json!(attach));
+ self.params.insert("OpenStdin", json!(attach));
+ self
+ }
+
+ /// Whether to attach to `stdout`.
+ pub fn attach_stdout(
+ &mut self,
+ attach: bool,
+ ) -> &mut Self {
+ self.params.insert("AttachStdout", json!(attach));
+ self
+ }
+
+ /// Whether to attach to `stderr`.
+ pub fn attach_stderr(
+ &mut self,
+ attach: bool,
+ ) -> &mut Self {
+ self.params.insert("AttachStderr", json!(attach));
+ self
+ }
+
+ /// Whether standard streams should be attached to a TTY.
+ pub fn tty(
+ &mut self,
+ tty: bool,
+ ) -> &mut Self {
+ self.params.insert("Tty", json!(tty));
+ self
+ }
+
+ pub fn extra_hosts(
+ &mut self,
+ hosts: Vec<&str>,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.ExtraHosts", json!(hosts));
+ self
+ }
+
+ pub fn volumes_from(
+ &mut self,
+ volumes: Vec<&str>,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.VolumesFrom", json!(volumes));
+ self
+ }
+
+ pub fn network_mode(
+ &mut self,
+ network: &str,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.NetworkMode", json!(network));
+ self
+ }
+
+ pub fn env<E, S>(
+ &mut self,
+ envs: E,
+ ) -> &mut Self
+ where
+ S: AsRef<str> + Serialize,
+ E: AsRef<[S]> + Serialize,
+ {
+ self.params.insert("Env", json!(envs));
+ self
+ }
+
+ pub fn cmd(
+ &mut self,
+ cmds: Vec<&str>,
+ ) -> &mut Self {
+ self.params.insert("Cmd", json!(cmds));
+ self
+ }
+
+ pub fn entrypoint(
+ &mut self,
+ entrypoint: &str,
+ ) -> &mut Self {
+ self.params.insert("Entrypoint", json!(entrypoint));
+ self
+ }
+
+ pub fn capabilities(
+ &mut self,
+ capabilities: Vec<&str>,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.CapAdd", json!(capabilities));
+ self
+ }
+
+ pub fn devices(
+ &mut self,
+ devices: Vec<HashMap<String, String>>,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.Devices", json!(devices));
+ self
+ }
+
+ pub fn log_driver(
+ &mut self,
+ log_driver: &str,
+ ) -> &mut Self {
+ self.params
+ .insert("HostConfig.LogConfig.Type", json!(log_driver));
+ self
+ }
+
+ pub fn restart_policy(
+ &mut self,
+ name: &str,
+ maximum_retry_count: u64,
+ ) -> &mut Self {
+ self.params
+ .insert("HostConfig.RestartPolicy.Name", json!(name));
+ if name == "on-failure" {
+ self.params.insert(
+ "HostConfig.RestartPolicy.MaximumRetryCount",
+ json!(maximum_retry_count),
+ );
+ }
+ self
+ }
+
+ pub fn auto_remove(
+ &mut self,
+ set: bool,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.AutoRemove", json!(set));
+ self
+ }
+
+ /// Signal to stop a container as a string. Default is "SIGTERM".
+ pub fn stop_signal(
+ &mut self,
+ sig: &str,
+ ) -> &mut Self {
+ self.params.insert("StopSignal", json!(sig));
+ self
+ }
+
+ /// Signal to stop a container as an integer. Default is 15 (SIGTERM).
+ pub fn stop_signal_num(
+ &mut self,
+ sig: u64,
+ ) -> &mut Self {
+ self.params.insert("StopSignal", json!(sig));
+ self
+ }
+
+ /// Timeout to stop a container. Only seconds are counted. Default is 10s
+ pub fn stop_timeout(
+ &mut self,
+ timeout: Duration,
+ ) -> &mut Self {
+ self.params.insert("StopTimeout", json!(timeout.as_secs()));
+ self
+ }
+
+ pub fn userns_mode(
+ &mut self,
+ mode: &str,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.UsernsMode", json!(mode));
+ self
+ }
+
+ pub fn privileged(
+ &mut self,
+ set: bool,
+ ) -> &mut Self {
+ self.params.insert("HostConfig.Privileged", json!(set));
+ self
+ }
+
+ pub fn user(
+ &mut self,
+ user: &str,
+ ) -> &mut Self {
+ self.params.insert("User", json!(user));
+ self
+ }
+
+ pub fn build(&self) -> ContainerOptions {
+ ContainerOptions {
+ name: self.name.clone(),
+ params: self.params.clone(),
+ }
+ }
+}
+
+/// Options for controlling log request results
+#[derive(Default, Debug)]
+pub struct LogsOptions {
+ params: HashMap<&'static str, String>,
+}
+
+impl LogsOptions {
+ /// return a new instance of a builder for options
+ pub fn builder() -> LogsOptionsBuilder {
+ LogsOptionsBuilder::default()
+ }
+
+ /// serialize options as a string. returns None if no options are defined
+ pub fn serialize(&self) -> Option<String> {
+ if self.params.is_empty() {
+ None
+ } else {
+ Some(
+ form_urlencoded::Serializer::new(String::new())
+ .extend_pairs(&self.params)
+ .finish(),
+ )
+ }
+ }
+}
+
+/// Builder interface for `LogsOptions`
+#[derive(Default)]
+pub struct LogsOptionsBuilder {
+ params: HashMap<&'static str, String>,
+}
+
+impl LogsOptionsBuilder {
+ pub fn follow(
+ &mut self,
+ f: bool,
+ ) -> &mut Self {
+ self.params.insert("follow", f.to_string());
+ self
+ }
+
+ pub fn stdout(
+ &mut self,
+ s: bool,
+ ) -> &mut Self {
+ self.params.insert("stdout", s.to_string());
+ self
+ }
+
+ pub fn stderr(
+ &mut self,
+ s: bool,
+ ) -> &mut Self {
+ self.params.insert("stderr", s.to_string());
+ self
+ }
+
+ pub fn timestamps(
+ &mut self,
+ t: bool,
+ ) -> &mut Self {
+ self.params.insert("timestamps", t.to_string());
+ self
+ }
+
+ /// how_many can either be "all" or a to_string() of the number
+ pub fn tail(
+ &mut self,
+ how_many: &str,
+ ) -> &mut Self {
+ self.params.insert("tail", how_many.to_owned());
+ self
+ }
+
+ #[cfg(feature = "chrono")]
+ pub fn since<Tz>(
+ &mut self,
+ timestamp: &chrono::DateTime<Tz>,
+ ) -> &mut Self
+ where
+ Tz: chrono::TimeZone,
+ {
+ self.params
+ .insert("since", timestamp.timestamp().to_string());
+ self
+ }
+
+ #[cfg(not(feature = "chrono"))]
+ pub fn since(
+ &mut self,
+ timestamp: i64,
+ ) -> &mut Self {
+ self.params.insert("since", timestamp.to_string());
+ self
+ }
+
+ pub fn build(&self) -> LogsOptions {
+ LogsOptions {
+ params: self.params.clone(),
+ }
+ }
+}
+