use std::{
borrow::Cow,
collections::BTreeMap,
fmt::{Debug, Formatter},
net::IpAddr,
time::Duration,
};
use crate::{
Error, Image,
core::{
ContainerState, copy::CopyToContainer, healthcheck::Healthcheck, image::exec::ExecCommand,
logs::consumer::LogConsumer, mounts::Mount, ports::ContainerPort, wait::WaitFor,
},
};
pub(crate) const DEFAULT_STARTUP_TIMEOUT: Duration = Duration::from_secs(60);
#[must_use]
pub struct ContainerRequest<I: Image> {
pub(crate) image: I,
pub(crate) overridden_cmd: Vec<String>,
pub(crate) image_name: Option<String>,
pub(crate) image_tag: Option<String>,
pub(crate) container_name: Option<String>,
pub(crate) hostname: Option<String>,
pub(crate) network: Option<String>,
pub(crate) labels: BTreeMap<String, String>,
pub(crate) env_vars: BTreeMap<String, String>,
pub(crate) hosts: BTreeMap<String, ExtraHost>,
pub(crate) mounts: Vec<Mount>,
pub(crate) health_check: Option<Healthcheck>,
pub(crate) copy_to_sources: Vec<CopyToContainer>,
pub(crate) ports: Option<Vec<PortMapping>>,
pub(crate) privileged: bool,
pub(crate) readonly_rootfs: bool,
pub(crate) cap_add: Option<Vec<String>>,
pub(crate) cap_drop: Option<Vec<String>>,
pub(crate) shm_size: Option<u64>,
pub(crate) ready_conditions: Option<Vec<WaitFor>>,
pub(crate) startup_timeout: Option<Duration>,
pub(crate) working_dir: Option<String>,
pub(crate) user: Option<String>,
pub(crate) open_stdin: Option<bool>,
pub(crate) log_consumers: Vec<Box<dyn LogConsumer + 'static>>,
pub(crate) init: bool,
pub(crate) platform: Option<String>,
pub(crate) ssh: bool,
pub(crate) masked_paths: Option<Vec<String>>,
pub(crate) readonly_paths: Option<Vec<String>>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PortMapping {
pub(crate) host_port: u16,
pub(crate) container_port: ContainerPort,
}
#[derive(Debug, Clone, Copy)]
pub enum ExtraHost {
Addr(IpAddr),
HostGateway,
}
impl std::fmt::Display for ExtraHost {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtraHost::Addr(a) => write!(f, "{a}"),
ExtraHost::HostGateway => write!(f, "host-gateway"),
}
}
}
impl<I: Image> ContainerRequest<I> {
pub fn image(&self) -> &I {
&self.image
}
pub fn network(&self) -> &Option<String> {
&self.network
}
pub fn labels(&self) -> &BTreeMap<String, String> {
&self.labels
}
pub fn container_name(&self) -> &Option<String> {
&self.container_name
}
pub fn hostname(&self) -> Option<&str> {
self.hostname.as_deref()
}
pub fn env_vars(&self) -> impl Iterator<Item = (Cow<'_, str>, Cow<'_, str>)> {
self.image
.env_vars()
.into_iter()
.map(|(name, val)| (name.into(), val.into()))
.chain(
self.env_vars
.iter()
.map(|(name, val)| (name.into(), val.into())),
)
}
pub fn hosts(&self) -> impl Iterator<Item = (Cow<'_, str>, &ExtraHost)> {
self.hosts.iter().map(|(name, host)| (name.into(), host))
}
pub fn mounts(&self) -> impl Iterator<Item = &Mount> {
self.image.mounts().into_iter().chain(self.mounts.iter())
}
pub fn health_check(&self) -> Option<&Healthcheck> {
self.health_check.as_ref()
}
pub fn copy_to_sources(&self) -> impl Iterator<Item = &CopyToContainer> {
self.image
.copy_to_sources()
.into_iter()
.chain(self.copy_to_sources.iter())
}
pub fn ports(&self) -> Option<&Vec<PortMapping>> {
self.ports.as_ref()
}
pub fn privileged(&self) -> bool {
self.privileged
}
pub fn readonly_rootfs(&self) -> bool {
self.readonly_rootfs
}
pub fn cap_add(&self) -> Option<&Vec<String>> {
self.cap_add.as_ref()
}
pub fn cap_drop(&self) -> Option<&Vec<String>> {
self.cap_drop.as_ref()
}
pub fn shm_size(&self) -> Option<u64> {
self.shm_size
}
pub fn entrypoint(&self) -> Option<&str> {
self.image.entrypoint()
}
pub fn cmd(&self) -> impl Iterator<Item = Cow<'_, str>> {
if !self.overridden_cmd.is_empty() {
let front: Vec<Cow<'_, str>> = self.overridden_cmd.iter().map(Cow::from).collect();
CmdIter {
front: front.into_iter(),
back: Vec::new().into_iter(),
}
} else {
let back: Vec<Cow<'_, str>> = self.image.cmd().into_iter().map(Into::into).collect();
CmdIter {
front: Vec::new().into_iter(),
back: back.into_iter(),
}
}
}
pub fn descriptor(&self) -> String {
let original_name = self.image.name();
let original_tag = self.image.tag();
let name = self.image_name.as_deref().unwrap_or(original_name);
let tag = self.image_tag.as_deref().unwrap_or(original_tag);
format!("{name}:{tag}")
}
pub fn ready_conditions(&self) -> Vec<WaitFor> {
self.ready_conditions
.clone()
.unwrap_or_else(|| self.image.ready_conditions())
}
pub fn expose_ports(&self) -> &[ContainerPort] {
self.image.expose_ports()
}
pub fn exec_after_start(
&self,
cs: ContainerState,
) -> std::result::Result<Vec<ExecCommand>, Error> {
self.image.exec_after_start(cs)
}
pub fn startup_timeout(&self) -> Option<Duration> {
self.startup_timeout
}
pub fn working_dir(&self) -> Option<&str> {
self.working_dir.as_deref()
}
pub fn user(&self) -> Option<&str> {
self.user.as_deref()
}
pub fn open_stdin(&self) -> Option<bool> {
self.open_stdin
}
pub fn init(&self) -> bool {
self.init
}
pub fn platform(&self) -> &Option<String> {
&self.platform
}
pub fn ssh(&self) -> bool {
self.ssh
}
pub fn masked_paths(&self) -> Option<&Vec<String>> {
self.masked_paths.as_ref()
}
pub fn readonly_paths(&self) -> Option<&Vec<String>> {
self.readonly_paths.as_ref()
}
}
impl<I: Image> From<I> for ContainerRequest<I> {
fn from(image: I) -> Self {
Self {
image,
overridden_cmd: Vec::new(),
image_name: None,
image_tag: None,
container_name: None,
hostname: None,
network: None,
labels: BTreeMap::default(),
env_vars: BTreeMap::default(),
hosts: BTreeMap::default(),
mounts: Vec::new(),
health_check: None,
copy_to_sources: Vec::new(),
ports: None,
privileged: false,
readonly_rootfs: false,
cap_add: None,
cap_drop: None,
shm_size: None,
ready_conditions: None,
startup_timeout: None,
working_dir: None,
user: None,
open_stdin: None,
log_consumers: vec![],
init: false,
platform: None,
ssh: false,
masked_paths: None,
readonly_paths: None,
}
}
}
impl PortMapping {
pub(crate) fn new(local: u16, internal: ContainerPort) -> Self {
Self {
host_port: local,
container_port: internal,
}
}
pub fn host_port(&self) -> u16 {
self.host_port
}
pub fn container_port(&self) -> ContainerPort {
self.container_port
}
}
pub(crate) fn reject_duplicate_mapped_ports(
ports: &[PortMapping],
) -> crate::core::error::Result<()> {
let mut seen: BTreeMap<ContainerPort, u16> = BTreeMap::new();
for p in ports {
if let Some(previous) = seen.insert(p.container_port, p.host_port) {
return Err(Error::other(format!(
"duplicate container port mapping: container port {} is mapped to \
both host ports {previous} and {}",
p.container_port, p.host_port
)));
}
}
Ok(())
}
impl<I: Image + Debug> Debug for ContainerRequest<I> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut repr = f.debug_struct("ContainerRequest");
repr.field("image", &self.image)
.field("overridden_cmd", &self.overridden_cmd)
.field("image_name", &self.image_name)
.field("image_tag", &self.image_tag)
.field("container_name", &self.container_name)
.field("hostname", &self.hostname)
.field("network", &self.network)
.field("labels", &self.labels)
.field("env_vars", &self.env_vars)
.field("hosts", &self.hosts)
.field("mounts", &self.mounts)
.field("health_check", &self.health_check)
.field("ports", &self.ports)
.field("privileged", &self.privileged)
.field("readonly_rootfs", &self.readonly_rootfs)
.field("cap_add", &self.cap_add)
.field("cap_drop", &self.cap_drop)
.field("shm_size", &self.shm_size)
.field("startup_timeout", &self.startup_timeout)
.field("working_dir", &self.working_dir)
.field("user", &self.user)
.field("open_stdin", &self.open_stdin)
.field("init", &self.init)
.field("platform", &self.platform)
.field("ssh", &self.ssh)
.field("masked_paths", &self.masked_paths)
.field("readonly_paths", &self.readonly_paths);
repr.finish()
}
}
pub(crate) struct CmdIter<'a> {
pub(crate) front: std::vec::IntoIter<Cow<'a, str>>,
pub(crate) back: std::vec::IntoIter<Cow<'a, str>>,
}
impl<'a> Iterator for CmdIter<'a> {
type Item = Cow<'a, str>;
fn next(&mut self) -> Option<Self::Item> {
self.front.next().or_else(|| self.back.next())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reject_duplicate_mapped_ports_detects_same_container_port() {
let ports = vec![
PortMapping::new(8080, ContainerPort::Tcp(80)),
PortMapping::new(8081, ContainerPort::Tcp(80)),
];
let err = reject_duplicate_mapped_ports(&ports).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("duplicate container port mapping"),
"エラーに duplicate container port mapping を含むこと: {msg}"
);
assert!(
msg.contains("80/tcp"),
"エラーにコンテナポートを含むこと: {msg}"
);
assert!(
msg.find("8080")
.expect("先に登録したホストポート 8080 が現れること")
< msg
.find("8081")
.expect("後に登録したホストポート 8081 が現れること"),
"先に登録したホストポートが先に現れること: {msg}"
);
}
#[test]
fn reject_duplicate_mapped_ports_detects_identical_mapping() {
let ports = vec![
PortMapping::new(8080, ContainerPort::Tcp(80)),
PortMapping::new(8080, ContainerPort::Tcp(80)),
];
reject_duplicate_mapped_ports(&ports).expect_err("完全同一マッピングも重複であること");
}
#[test]
fn reject_duplicate_mapped_ports_detects_triple_mapping() {
let ports = vec![
PortMapping::new(8080, ContainerPort::Tcp(80)),
PortMapping::new(8081, ContainerPort::Tcp(80)),
PortMapping::new(8082, ContainerPort::Tcp(80)),
];
let err = reject_duplicate_mapped_ports(&ports).unwrap_err();
assert!(
err.to_string().contains("8080") && err.to_string().contains("8081"),
"最初の競合ペア (8080 / 8081) を報告すること: {err}"
);
}
#[test]
fn reject_duplicate_mapped_ports_detects_sctp_duplicate() {
let ports = vec![
PortMapping::new(8080, ContainerPort::Sctp(80)),
PortMapping::new(8081, ContainerPort::Sctp(80)),
];
reject_duplicate_mapped_ports(&ports).expect_err("SCTP の重複もエラーになること");
}
#[test]
fn reject_duplicate_mapped_ports_keeps_different_protocol() {
let ports = vec![
PortMapping::new(8080, ContainerPort::Tcp(80)),
PortMapping::new(8081, ContainerPort::Udp(80)),
PortMapping::new(8082, ContainerPort::Sctp(80)),
];
reject_duplicate_mapped_ports(&ports).expect("異プロトコルは重複とみなさないこと");
}
#[test]
fn reject_duplicate_mapped_ports_accepts_single_mapping() {
let ports = vec![PortMapping::new(8080, ContainerPort::Tcp(80))];
reject_duplicate_mapped_ports(&ports).expect("単一マッピングはエラーにならないこと");
}
#[test]
fn reject_duplicate_mapped_ports_accepts_empty() {
reject_duplicate_mapped_ports(&[]).expect("空リストはエラーにならないこと");
}
}