From cfc2a6afc36214028cecce527d2c14b85ff98238 Mon Sep 17 00:00:00 2001 From: igor Date: Sat, 21 Sep 2024 13:00:21 +0200 Subject: [PATCH] feat: allow retrieve host port for generic image (#32) * feat: allow retrieve host port for generic image * fix(compose): skip wait and port mapping if a compose service not found Replace a panic by a warning --- rustainers/src/compose/inner.rs | 26 +++++++++++++----- rustainers/src/compose/service.rs | 5 ++++ rustainers/src/images/mod.rs | 44 +++++++++++++++++++++++++++++++ rustainers/src/port/error.rs | 4 +++ rustainers/tests/images.rs | 23 ++++++++++++++-- 5 files changed, 93 insertions(+), 9 deletions(-) diff --git a/rustainers/src/compose/inner.rs b/rustainers/src/compose/inner.rs index b1ba8da..788e1a8 100644 --- a/rustainers/src/compose/inner.rs +++ b/rustainers/src/compose/inner.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::time::Duration; use async_trait::async_trait; -use tracing::info; +use tracing::{info, warn}; use crate::cmd::Cmd; use crate::runner::InnerRunner; @@ -45,17 +45,29 @@ pub(crate) trait InnerComposeRunner: InnerRunner { // Wait let interval = options.wait_interval; for (service, wait) in wait_strategies { - debug_assert!(services.contains(service)); - #[allow(clippy::indexing_slicing)] - let id = services[service]; + let Some(id) = services.get(service) else { + warn!( + ?service, + ?wait, + ?services, + "Compose service {service} not found, skip wait strategy" + ); + continue; + }; self.wait_service_ready(service, id, wait, interval).await?; } // Port mapping for (service, mapping) in port_mappings { - debug_assert!(services.contains(service)); - #[allow(clippy::indexing_slicing)] - let id = services[service]; + let Some(id) = services.get(service) else { + warn!( + ?service, + ?mapping, + ?services, + "Compose service {service} not found, skip port mapping" + ); + continue; + }; let port = self.port(id, mapping.container_port).await?; mapping.bind_port(port).await; } diff --git a/rustainers/src/compose/service.rs b/rustainers/src/compose/service.rs index 45d3efe..8090574 100644 --- a/rustainers/src/compose/service.rs +++ b/rustainers/src/compose/service.rs @@ -65,6 +65,11 @@ impl Services { pub fn contains_all(&self, services: &[ComposeService]) -> bool { services.iter().all(|svc| self.contains(svc)) } + + /// Get the container id of a service + pub fn get(&self, service: &ComposeService) -> Option { + self.0.get(service).copied() + } } impl Index<&ComposeService> for Services { diff --git a/rustainers/src/images/mod.rs b/rustainers/src/images/mod.rs index 108e281..0c1ba36 100644 --- a/rustainers/src/images/mod.rs +++ b/rustainers/src/images/mod.rs @@ -3,9 +3,12 @@ mod postgres; use indexmap::IndexMap; +use crate::Container; use crate::ContainerStatus; use crate::ExposedPort; use crate::ImageReference; +use crate::Port; +use crate::PortError; use crate::RunnableContainer; use crate::RunnableContainerBuilder; use crate::ToRunnableContainer; @@ -32,6 +35,27 @@ mod nats; pub use self::nats::*; /// A Generic Image +/// +/// ```rust, no_run +/// # async fn run() -> anyhow::Result<()> { +/// use rustainers::{ImageName, WaitStrategy}; +/// use rustainers::images::GenericImage; +/// +/// let name = ImageName::new("docker.io/nginx"); +/// let container_port = 80; +/// +/// let mut nginx = GenericImage::new(name); +/// nginx.add_port_mapping(container_port); +/// nginx.set_wait_strategy(WaitStrategy::http("/")); +/// +/// # let runner = rustainers::runner::Runner::auto()?; +/// let container = runner.start(nginx).await?; +/// +/// let port = container.host_port(container_port).await?; +/// // ... +/// # Ok(()) +/// # } +/// ``` #[derive(Debug)] pub struct GenericImage(RunnableContainer); @@ -92,3 +116,23 @@ impl ToRunnableContainer for GenericImage { } } } + +impl Container { + /// Find the host port for a container port + /// + /// # Errors + /// + /// Fail if there is no mapping with the container port + /// Could fail if the port is not bind + pub async fn host_port(&self, container_port: impl Into) -> Result { + let container_port = container_port.into(); + + for mapping in &self.0.port_mappings { + if mapping.container_port == container_port { + return mapping.host_port().await; + } + } + + Err(PortError::ContainerPortNotFound(container_port)) + } +} diff --git a/rustainers/src/port/error.rs b/rustainers/src/port/error.rs index dfa5900..b56704b 100644 --- a/rustainers/src/port/error.rs +++ b/rustainers/src/port/error.rs @@ -13,6 +13,10 @@ pub enum PortError { #[error("Container port {0} not bind")] PortNotBindYet(Port), + /// The container port not found + #[error("Container port {0} not found")] + ContainerPortNotFound(Port), + /// The container is failing #[error(transparent)] RunnerError(#[from] RunnerError), diff --git a/rustainers/tests/images.rs b/rustainers/tests/images.rs index 3119def..99bf55e 100644 --- a/rustainers/tests/images.rs +++ b/rustainers/tests/images.rs @@ -7,9 +7,9 @@ use rstest::rstest; use tokio::task::JoinSet; use tracing::{debug, info}; -use rustainers::images::{Minio, Mongo, Mosquitto, Nats, Postgres, Redis}; +use rustainers::images::{GenericImage, Minio, Mongo, Mosquitto, Nats, Postgres, Redis}; use rustainers::runner::{RunOption, Runner}; -use rustainers::{ExposedPort, Port}; +use rustainers::{ExposedPort, ImageName, Port, WaitStrategy}; mod common; pub use self::common::*; @@ -228,3 +228,22 @@ async fn test_mosquitto_endpoint(runner: &Runner) -> anyhow::Result<()> { check!(endpoint == "mqtt://127.0.0.1:9127"); Ok(()) } + +#[rstest] +#[tokio::test] +async fn test_generic_image(runner: &Runner) -> anyhow::Result<()> { + let options = RunOption::builder().with_remove(true).build(); + let name = ImageName::new("docker.io/nginx"); + let mut nginx = GenericImage::new(name); + let container_port = 80; + nginx.add_port_mapping(container_port); + nginx.set_wait_strategy(WaitStrategy::http("/")); + + let container = runner.start_with_options(nginx, options).await?; + debug!("Started {container}"); + + let host_port = container.host_port(container_port).await; + let_assert!(Ok(_) = host_port); + + Ok(()) +}