use crate::{
composition::LogOptions,
container::PendingContainer,
docker::{ContainerState, Docker},
waitfor::{wait_for_message, MessageSource},
DockerTestError,
};
use bollard::models::{PortBinding, PortMap};
use serde::Serialize;
use std::{
collections::HashMap,
convert::TryFrom,
net::{IpAddr, Ipv4Addr},
str::FromStr,
sync::{Arc, Mutex},
};
#[derive(Clone, Debug)]
pub struct OperationalContainer {
pub(crate) client: Docker,
pub(crate) handle: String,
pub(crate) id: String,
pub(crate) name: String,
pub(crate) ip: std::net::Ipv4Addr,
pub(crate) ports: HostPortMappings,
pub(crate) is_static: bool,
pub(crate) log_options: Option<LogOptions>,
pub(crate) assumed_state: Arc<Mutex<ContainerState>>,
}
#[derive(Clone, Debug, Default)]
pub(crate) struct HostPortMappings {
mappings: HashMap<u32, (Ipv4Addr, u32)>,
}
#[derive(thiserror::Error, Debug, PartialEq, Clone)]
pub(crate) enum HostPortMappingError {
#[error("failed to extract host port from docker details, malformed ip/protocol key: {0}")]
HostPortKey(String),
#[error("port mapping did not contain a host port or host ip")]
NoMapping,
#[error("failed to convert host ip/port to appropriate types: {0}")]
Conversion(String),
}
impl TryFrom<PortMap> for HostPortMappings {
type Error = HostPortMappingError;
fn try_from(p: PortMap) -> Result<HostPortMappings, Self::Error> {
let mut map: HashMap<u32, (Ipv4Addr, u32)> = HashMap::new();
for (host_port_string, ports) in p.into_iter() {
if let Some(port_bindings) = ports {
let split: Vec<&str> = host_port_string.split('/').collect();
if split.len() < 2 {
return Err(HostPortMappingError::HostPortKey(host_port_string));
}
let host_port = u32::from_str(split[0])
.map_err(|e| HostPortMappingError::Conversion(e.to_string()))?;
for binding in port_bindings {
if let Some((ip, port)) = from_port_binding(binding)? {
map.entry(host_port).or_insert((ip, port));
}
}
}
}
Ok(HostPortMappings { mappings: map })
}
}
fn from_port_binding(ports: PortBinding) -> Result<Option<(Ipv4Addr, u32)>, HostPortMappingError> {
match (ports.host_ip, ports.host_port) {
(Some(ip), Some(port)) => {
let ip = IpAddr::from_str(&ip)
.map_err(|e| HostPortMappingError::Conversion(e.to_string()))?;
match ip {
IpAddr::V4(ipv4) => {
let parsed_port = u32::from_str(&port)
.map_err(|e| HostPortMappingError::Conversion(e.to_string()))?;
Ok(Some((ipv4, parsed_port)))
}
IpAddr::V6(_) => Ok(None),
}
}
_ => Err(HostPortMappingError::NoMapping),
}
}
impl OperationalContainer {
pub fn name(&self) -> &str {
&self.name
}
pub fn id(&self) -> &str {
&self.id
}
pub fn ip(&self) -> &std::net::Ipv4Addr {
&self.ip
}
pub fn host_port(&self, exposed_port: u32) -> Option<&(Ipv4Addr, u32)> {
self.ports.mappings.get(&exposed_port)
}
pub fn host_port_unchecked(&self, exposed_port: u32) -> &(Ipv4Addr, u32) {
self.ports.mappings.get(&exposed_port).unwrap()
}
pub async fn pause(&self) {
self.goto_state(ContainerState::Paused, &[ContainerState::Running])
.unwrap();
self.client.pause(&self.name).await.unwrap();
}
pub async fn kill(&self) {
self.goto_state(
ContainerState::Exited,
&[ContainerState::Running, ContainerState::Paused],
)
.unwrap();
self.client.kill(&self.name).await.unwrap();
}
pub async fn unpause(&self) {
self.goto_state(ContainerState::Running, &[ContainerState::Paused])
.unwrap();
self.client.unpause(&self.name).await.unwrap();
}
pub async fn assert_message<T>(&self, message: T, source: MessageSource, timeout: u16)
where
T: Into<String> + Serialize,
{
if let Err(e) = wait_for_message(
&self.client,
&self.id,
&self.handle,
source,
message,
timeout,
)
.await
{
panic!("{}", e)
}
}
fn goto_state(
&self,
dest: ContainerState,
allowed: &[ContainerState],
) -> Result<(), DockerTestError> {
let mut state = self.assumed_state.lock().unwrap();
println!("{state}");
if !allowed.contains(&state) {
Err(DockerTestError::ContainerState {
current: *state,
tried_to_enter: dest,
})
} else {
*state = dest;
Ok(())
}
}
}
impl From<PendingContainer> for OperationalContainer {
fn from(container: PendingContainer) -> OperationalContainer {
OperationalContainer {
client: container.client,
handle: container.handle,
id: container.id,
name: container.name,
ip: std::net::Ipv4Addr::UNSPECIFIED,
ports: HostPortMappings::default(),
is_static: container.is_static,
log_options: container.log_options,
assumed_state: Arc::new(Mutex::new(container.expected_state)),
}
}
}