use crate::api::{ContainerStatus, Filter, ImageName, Labels};
use std::{
collections::HashMap,
hash::Hash,
iter::Peekable,
str::{self, FromStr},
string::ToString,
time::Duration,
};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use crate::{Error, Result};
pub enum Health {
Starting,
Healthy,
Unhealthy,
None,
}
impl AsRef<str> for Health {
fn as_ref(&self) -> &str {
match &self {
Health::Starting => "starting",
Health::Healthy => "healthy",
Health::Unhealthy => "unhealthy",
Health::None => "none",
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Isolation {
Default,
Process,
HyperV,
}
impl AsRef<str> for Isolation {
fn as_ref(&self) -> &str {
match &self {
Isolation::Default => "default",
Isolation::Process => "process",
Isolation::HyperV => "hyperv",
}
}
}
pub enum ContainerFilter {
Ancestor(ImageName),
Before(String),
ExitCode(u64),
Health(Health),
Id(String),
Isolation(Isolation),
IsTask(bool),
LabelKey(String),
Label(String, String),
Name(String),
Publish(PublishPort),
Network(String),
Since(String),
Status(ContainerStatus),
Volume(String),
}
impl Filter for ContainerFilter {
fn query_key_val(&self) -> (&'static str, String) {
use ContainerFilter::*;
match &self {
Ancestor(name) => ("ancestor", name.to_string()),
Before(before) => ("before", before.to_owned()),
ExitCode(c) => ("exit", c.to_string()),
Health(health) => ("health", health.as_ref().to_string()),
Id(id) => ("id", id.to_owned()),
Isolation(isolation) => ("isolation", isolation.as_ref().to_string()),
IsTask(is_task) => ("is-task", is_task.to_string()),
LabelKey(key) => ("label", key.to_owned()),
Label(key, val) => ("label", format!("{}={}", key, val)),
Name(name) => ("name", name.to_owned()),
Publish(port) => ("publsh", port.to_string()),
Network(net) => ("net", net.to_owned()),
Since(since) => ("since", since.to_owned()),
Status(s) => ("status", s.as_ref().to_string()),
Volume(vol) => ("volume", vol.to_owned()),
}
}
}
impl_opts_builder!(url => ContainerList);
impl ContainerListOptsBuilder {
impl_filter_func!(
ContainerFilter
);
impl_url_bool_field!(
all => "all"
);
impl_url_str_field!(since: S => "since");
impl_url_str_field!(before: B => "before");
impl_url_bool_field!(
sized => "size"
);
}
#[derive(Serialize, Debug)]
pub struct ContainerCreateOpts {
name: Option<String>,
params: HashMap<&'static str, Value>,
}
fn insert<'a, I, V>(key_path: &mut Peekable<I>, value: &V, parent_node: &mut Value)
where
V: Serialize,
I: Iterator<Item = &'a str>,
{
if let Some(local_key) = key_path.next() {
if key_path.peek().is_some() {
if let Some(node) = parent_node.as_object_mut() {
let node = node
.entry(local_key.to_string())
.or_insert(Value::Object(Map::new()));
insert(key_path, value, node);
}
} else if let Some(node) = parent_node.as_object_mut() {
node.insert(
local_key.to_string(),
serde_json::to_value(value).unwrap_or_default(),
);
}
}
}
impl ContainerCreateOpts {
pub fn builder<N>(name: N) -> ContainerOptsBuilder
where
N: AsRef<str>,
{
ContainerOptsBuilder::new(name.as_ref())
}
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();
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
}
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)
}
}
pub(crate) fn name(&self) -> &Option<String> {
&self.name
}
}
#[derive(Default)]
pub struct ContainerOptsBuilder {
name: Option<String>,
params: HashMap<&'static str, Value>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Protocol {
Tcp,
Udp,
Sctp,
}
impl AsRef<str> for Protocol {
fn as_ref(&self) -> &str {
match &self {
Self::Tcp => "tcp",
Self::Udp => "udp",
Self::Sctp => "sctp",
}
}
}
impl FromStr for Protocol {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"tcp" => Ok(Protocol::Tcp),
"udp" => Ok(Protocol::Udp),
"sctp" => Ok(Protocol::Sctp),
proto => Err(Error::InvalidProtocol(proto.into())),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PublishPort {
port: u32,
protocol: Protocol,
}
impl PublishPort {
pub fn tcp(port: u32) -> Self {
Self {
port,
protocol: Protocol::Tcp,
}
}
pub fn udp(port: u32) -> Self {
Self {
port,
protocol: Protocol::Udp,
}
}
pub fn sctp(port: u32) -> Self {
Self {
port,
protocol: Protocol::Sctp,
}
}
}
impl FromStr for PublishPort {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let mut elems = s.split('/');
let port = elems
.next()
.ok_or_else(|| Error::InvalidPort("missing port number".into()))
.and_then(|port| {
port.parse::<u32>()
.map_err(|e| Error::InvalidPort(format!("expected port number - {}", e)))
})?;
let protocol = elems
.next()
.ok_or_else(|| Error::InvalidPort("missing protocol".into()))
.and_then(Protocol::from_str)?;
Ok(PublishPort { port, protocol })
}
}
impl ToString for PublishPort {
fn to_string(&self) -> String {
format!("{}/{}", self.port, self.protocol.as_ref())
}
}
impl ContainerOptsBuilder {
pub(crate) fn new(image: &str) -> Self {
let mut params = HashMap::new();
params.insert("Image", Value::String(image.to_owned()));
ContainerOptsBuilder { name: None, params }
}
pub fn name<N>(&mut self, name: N) -> &mut Self
where
N: Into<String>,
{
self.name = Some(name.into());
self
}
pub fn publish_all_ports(&mut self) -> &mut Self {
self.params
.insert("HostConfig.PublishAllPorts", Value::Bool(true));
self
}
pub fn expose(&mut self, srcport: PublishPort, hostport: u32) -> &mut Self {
let mut exposedport: HashMap<String, String> = HashMap::new();
exposedport.insert("HostPort".to_string(), hostport.to_string());
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(srcport.to_string(), json!(vec![exposedport]));
self.params
.insert("HostConfig.PortBindings", json!(port_bindings));
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
}
pub fn publish(&mut self, port: PublishPort) -> &mut Self {
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(port.to_string(), json!({}));
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
}
impl_str_field!(
working_dir: W => "WorkingDir"
);
impl_vec_field!(
volumes: V => "HostConfig.Binds"
);
impl_vec_field!(links: L => "HostConfig.Links");
impl_field!(memory: u64 => "HostConfig.Memory");
impl_field!(
memory_swap: i64 => "HostConfig.MemorySwap"
);
impl_field!(
nano_cpus: u64 => "HostConfig.NanoCpus"
);
pub fn cpus(&mut self, cpus: f64) -> &mut Self {
self.nano_cpus((1_000_000_000.0 * cpus) as u64)
}
impl_field!(
cpu_shares: u32 => "HostConfig.CpuShares");
impl_map_field!(json labels: L => "Labels");
pub fn attach_stdin(&mut self, attach: bool) -> &mut Self {
self.params.insert("AttachStdin", json!(attach));
self.params.insert("OpenStdin", json!(attach));
self
}
impl_field!(
attach_stdout: bool => "AttachStdout");
impl_field!(
attach_stderr: bool => "AttachStderr");
impl_field!(
tty: bool => "Tty");
impl_vec_field!(extra_hosts: H => "HostConfig.ExtraHosts");
impl_vec_field!(volumes_from: V => "HostConfig.VolumesFrom");
impl_str_field!(network_mode: M => "HostConfig.NetworkMode");
impl_vec_field!(env: E => "Env");
impl_vec_field!(cmd: C => "Cmd");
impl_vec_field!(entrypoint: E => "Entrypoint");
impl_vec_field!(capabilities: C => "HostConfig.CapAdd");
pub fn devices(&mut self, devices: Vec<Labels>) -> &mut Self {
self.params.insert("HostConfig.Devices", json!(devices));
self
}
impl_str_field!(log_driver: L => "HostConfig.LogConfig.Type");
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
}
impl_field!(auto_remove: bool => "HostConfig.AutoRemove");
impl_str_field!(
stop_signal: S => "StopSignal");
impl_field!(
stop_signal_num: u64 => "StopSignal");
impl_field!(
stop_timeout: Duration => "StopTimeout");
impl_str_field!(userns_mode: M => "HostConfig.UsernsMode");
impl_field!(privileged: bool => "HostConfig.Privileged");
impl_str_field!(user: U => "User");
pub fn build(&self) -> ContainerCreateOpts {
ContainerCreateOpts {
name: self.name.clone(),
params: self.params.clone(),
}
}
}
impl_opts_builder!(url => RmContainer);
impl RmContainerOptsBuilder {
impl_url_bool_field!(
force => "force"
);
impl_url_bool_field!(
volumes => "v"
);
impl_url_bool_field!(
link => "link"
);
}
impl_opts_builder!(url => ContainerPrune);
pub enum ContainerPruneFilter {
Until(String),
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
UntilDate(chrono::DateTime<chrono::Utc>),
LabelKey(String),
Label(String, String),
}
impl Filter for ContainerPruneFilter {
fn query_key_val(&self) -> (&'static str, String) {
use ContainerPruneFilter::*;
match &self {
Until(until) => ("until", until.to_owned()),
#[cfg(feature = "chrono")]
UntilDate(until) => ("until", until.timestamp().to_string()),
LabelKey(label) => ("label", label.to_owned()),
Label(key, val) => ("label", format!("{}={}", key, val)),
}
}
}
impl ContainerPruneOptsBuilder {
impl_filter_func!(ContainerPruneFilter);
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_case {
($opts:expr, $want:expr) => {
let opts = $opts.build();
pretty_assertions::assert_eq!($want, opts.serialize().unwrap())
};
}
#[test]
fn create_container_opts() {
test_case!(
ContainerOptsBuilder::new("test_image"),
r#"{"HostConfig":{},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").env(vec!["foo", "bar"]),
r#"{"Env":["foo","bar"],"HostConfig":{},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").env(&["foo", "bar", "baz"]),
r#"{"Env":["foo","bar","baz"],"HostConfig":{},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").env(std::iter::once("test")),
r#"{"Env":["test"],"HostConfig":{},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").user("alice"),
r#"{"HostConfig":{},"Image":"test_image","User":"alice"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image")
.network_mode("host")
.auto_remove(true)
.privileged(true),
r#"{"HostConfig":{"AutoRemove":true,"NetworkMode":"host","Privileged":true},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").expose(PublishPort::tcp(80), 8080),
r#"{"ExposedPorts":{"80/tcp":{}},"HostConfig":{"PortBindings":{"80/tcp":[{"HostPort":"8080"}]}},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image")
.expose(PublishPort::udp(80), 8080)
.expose(PublishPort::sctp(81), 8081),
r#"{"ExposedPorts":{"80/udp":{},"81/sctp":{}},"HostConfig":{"PortBindings":{"80/udp":[{"HostPort":"8080"}],"81/sctp":[{"HostPort":"8081"}]}},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image")
.publish(PublishPort::udp(80))
.publish(PublishPort::sctp(6969))
.publish(PublishPort::tcp(1337)),
r#"{"ExposedPorts":{"1337/tcp":{},"6969/sctp":{},"80/udp":{}},"HostConfig":{},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").publish_all_ports(),
r#"{"HostConfig":{"PublishAllPorts":true},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").log_driver("fluentd"),
r#"{"HostConfig":{"LogConfig":{"Type":"fluentd"}},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").restart_policy("on-failure", 10),
r#"{"HostConfig":{"RestartPolicy":{"MaximumRetryCount":10,"Name":"on-failure"}},"Image":"test_image"}"#
);
test_case!(
ContainerOptsBuilder::new("test_image").restart_policy("always", 0),
r#"{"HostConfig":{"RestartPolicy":{"Name":"always"}},"Image":"test_image"}"#
);
}
}