Skip to content

Commit

Permalink
feat: add image hook to run commands before ready (#766)
Browse files Browse the repository at this point in the history
Hey there, i was implementing a module for Dex and needed something like
[containerIsStarting in the Java
implementation](https://github.com/testcontainers/testcontainers-java/blob/46fe67029112790b74fa694aa782a5e19277b09d/core/src/main/java/org/testcontainers/containers/GenericContainer.java#L707).
I wrote a test and confirmed it works with [my module
implementation](https://github.com/yuri-becker/testcontainers-rs-modules-community/blob/419e3fddab318e7af90b5452a5979058085822b7/src/dex/mod.rs#L176)
– see
testcontainers/testcontainers-rs-modules-community#286
for that PR.

Please let me know what i can improve to get this merged. I hope it is
okay to implement this without an issue :)
  • Loading branch information
yuri-becker authored Feb 6, 2025
1 parent a43ad42 commit b979cde
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 15 deletions.
65 changes: 63 additions & 2 deletions testcontainers/src/core/containers/async_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ where
network: Option<Arc<Network>>,
) -> Result<ContainerAsync<I>> {
let container = Self::construct(id, docker_client, container_req, network);
let state = ContainerState::from_container(&container).await?;
for cmd in container.image().exec_before_ready(state)? {
container.exec(cmd).await?;
}
let ready_conditions = container.image().ready_conditions();
container.block_until_ready(ready_conditions).await?;
Ok(container)
Expand Down Expand Up @@ -270,7 +274,7 @@ where
/// Starts the container.
pub async fn start(&self) -> Result<()> {
self.docker_client.start(&self.id).await?;
let state = ContainerState::new(self.id(), self.ports().await?);
let state = ContainerState::from_container(self).await?;
for cmd in self.image.exec_after_start(state)? {
self.exec(cmd).await?;
}
Expand Down Expand Up @@ -418,7 +422,12 @@ where
mod tests {
use tokio::io::AsyncBufReadExt;

use crate::{images::generic::GenericImage, runners::AsyncRunner};
use crate::{
core::{ContainerPort, ContainerState, ExecCommand, WaitFor},
images::generic::GenericImage,
runners::AsyncRunner,
Image,
};

#[tokio::test]
async fn async_logs_are_accessible() -> anyhow::Result<()> {
Expand Down Expand Up @@ -667,4 +676,56 @@ mod tests {
.await
.map_err(anyhow::Error::from)
}

#[cfg(feature = "http_wait")]
#[tokio::test]
async fn exec_before_ready_is_ran() {
use crate::core::wait::HttpWaitStrategy;

struct ExecBeforeReady {}

impl Image for ExecBeforeReady {
fn name(&self) -> &str {
"testcontainers/helloworld"
}

fn tag(&self) -> &str {
"1.2.0"
}

fn ready_conditions(&self) -> Vec<WaitFor> {
vec![WaitFor::http(
HttpWaitStrategy::new("/ping")
.with_port(ContainerPort::Tcp(8080))
.with_expected_status_code(200u16),
)]
}

fn expose_ports(&self) -> &[ContainerPort] {
&[ContainerPort::Tcp(8080)]
}

#[allow(unused)]
fn exec_before_ready(
&self,
cs: ContainerState,
) -> crate::core::error::Result<Vec<ExecCommand>> {
Ok(vec![ExecCommand::new(vec![
"/bin/sh",
"-c",
"echo 'exec_before_ready ran!' > /opt/hello",
])])
}
}

let container = ExecBeforeReady {};
let container = container.start().await.unwrap();
let mut exec_result = container
.exec(ExecCommand::new(vec!["cat", "/opt/hello"]))
.await
.unwrap();
let stdout = exec_result.stdout_to_vec().await.unwrap();
let output = String::from_utf8(stdout).unwrap();
assert_eq!(output, "exec_before_ready ran!\n");
}
}
42 changes: 30 additions & 12 deletions testcontainers/src/core/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ pub use exec::ExecCommand;
pub use image_ext::ImageExt;
#[cfg(feature = "reusable-containers")]
pub use image_ext::ReuseDirective;
use url::Host;

use crate::{
core::{
copy::CopyToContainer,
error::Result,
mounts::Mount,
ports::{ContainerPort, Ports},
WaitFor,
},
TestcontainersError,
ContainerAsync, TestcontainersError,
};

mod exec;
Expand Down Expand Up @@ -92,32 +94,48 @@ where
/// This method is useful when certain re-configuration is required after the start
/// of container for the container to be considered ready for use in tests.
#[allow(unused_variables)]
fn exec_after_start(
&self,
cs: ContainerState,
) -> Result<Vec<ExecCommand>, TestcontainersError> {
fn exec_after_start(&self, cs: ContainerState) -> Result<Vec<ExecCommand>> {
Ok(Default::default())
}

/// Returns commands that will be executed after the container has started, but before the
/// [Image::ready_conditions] are awaited for.
///
/// Use this when you, e.g., need to configure something based on the container's ports and host
/// (for example an application that needs to know its own address).
#[allow(unused_variables)]
fn exec_before_ready(&self, cs: ContainerState) -> Result<Vec<ExecCommand>> {
Ok(Default::default())
}
}

#[derive(Debug)]
pub struct ContainerState {
id: String,
host: Host,
ports: Ports,
}

impl ContainerState {
pub fn new(id: impl Into<String>, ports: Ports) -> Self {
Self {
id: id.into(),
ports,
}
pub async fn from_container<I>(container: &ContainerAsync<I>) -> Result<Self>
where
I: Image,
{
Ok(Self {
id: container.id().into(),
host: container.get_host().await?,
ports: container.ports().await?,
})
}

pub fn host(&self) -> &Host {
&self.host
}

/// Returns the host port for the given internal container's port (`IPv4`).
///
/// Results in an error ([`TestcontainersError::PortNotExposed`]) if the port is not exposed.
pub fn host_port_ipv4(&self, internal_port: ContainerPort) -> Result<u16, TestcontainersError> {
pub fn host_port_ipv4(&self, internal_port: ContainerPort) -> Result<u16> {
self.ports
.map_to_host_port_ipv4(internal_port)
.ok_or_else(|| TestcontainersError::PortNotExposed {
Expand All @@ -129,7 +147,7 @@ impl ContainerState {
/// Returns the host port for the given internal container's port (`IPv6`).
///
/// Results in an error ([`TestcontainersError::PortNotExposed`]) if the port is not exposed.
pub fn host_port_ipv6(&self, internal_port: ContainerPort) -> Result<u16, TestcontainersError> {
pub fn host_port_ipv6(&self, internal_port: ContainerPort) -> Result<u16> {
self.ports
.map_to_host_port_ipv6(internal_port)
.ok_or_else(|| TestcontainersError::PortNotExposed {
Expand Down
2 changes: 1 addition & 1 deletion testcontainers/src/runners/async_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ where
let container =
ContainerAsync::new(container_id, client.clone(), container_req, network).await?;

let state = ContainerState::new(container.id(), container.ports().await?);
let state = ContainerState::from_container(&container).await?;
for cmd in container.image().exec_after_start(state)? {
container.exec(cmd).await?;
}
Expand Down

0 comments on commit b979cde

Please sign in to comment.