Skip to content

Commit ef687cb

Browse files
committed
feat: Add image hook to run commands before ready
1 parent a43ad42 commit ef687cb

File tree

3 files changed

+87
-15
lines changed

3 files changed

+87
-15
lines changed

testcontainers/src/core/containers/async_container.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ where
5959
network: Option<Arc<Network>>,
6060
) -> Result<ContainerAsync<I>> {
6161
let container = Self::construct(id, docker_client, container_req, network);
62+
let state = ContainerState::from_container(&container).await?;
63+
for cmd in container.image().exec_before_ready(state)? {
64+
container.exec(cmd).await?;
65+
}
6266
let ready_conditions = container.image().ready_conditions();
6367
container.block_until_ready(ready_conditions).await?;
6468
Ok(container)
@@ -270,7 +274,7 @@ where
270274
/// Starts the container.
271275
pub async fn start(&self) -> Result<()> {
272276
self.docker_client.start(&self.id).await?;
273-
let state = ContainerState::new(self.id(), self.ports().await?);
277+
let state = ContainerState::from_container(self).await?;
274278
for cmd in self.image.exec_after_start(state)? {
275279
self.exec(cmd).await?;
276280
}
@@ -418,7 +422,12 @@ where
418422
mod tests {
419423
use tokio::io::AsyncBufReadExt;
420424

421-
use crate::{images::generic::GenericImage, runners::AsyncRunner};
425+
use crate::{
426+
core::{ContainerPort, ContainerState, ExecCommand, WaitFor},
427+
images::generic::GenericImage,
428+
runners::AsyncRunner,
429+
Image,
430+
};
422431

423432
#[tokio::test]
424433
async fn async_logs_are_accessible() -> anyhow::Result<()> {
@@ -667,4 +676,49 @@ mod tests {
667676
.await
668677
.map_err(anyhow::Error::from)
669678
}
679+
680+
#[tokio::test]
681+
async fn exec_before_ready_is_ran() {
682+
struct ExecBeforeReady {}
683+
684+
impl Image for ExecBeforeReady {
685+
fn name(&self) -> &str {
686+
"testcontainers/helloworld"
687+
}
688+
689+
fn tag(&self) -> &str {
690+
"1.2.0"
691+
}
692+
693+
fn ready_conditions(&self) -> Vec<WaitFor> {
694+
vec![WaitFor::Nothing]
695+
}
696+
697+
fn expose_ports(&self) -> &[ContainerPort] {
698+
&[ContainerPort::Tcp(8080)]
699+
}
700+
701+
#[allow(unused)]
702+
fn exec_before_ready(
703+
&self,
704+
cs: ContainerState,
705+
) -> crate::core::error::Result<Vec<ExecCommand>> {
706+
Ok(vec![ExecCommand::new(vec![
707+
"/bin/sh",
708+
"-c",
709+
"echo 'exec_before_ready ran!' > /opt/hello",
710+
])])
711+
}
712+
}
713+
714+
let container = ExecBeforeReady {};
715+
let container = container.start().await.unwrap();
716+
let mut exec_result = container
717+
.exec(ExecCommand::new(vec!["cat", "/opt/hello"]))
718+
.await
719+
.unwrap();
720+
let stdout = exec_result.stdout_to_vec().await.unwrap();
721+
let output = String::from_utf8(stdout).unwrap();
722+
assert_eq!(output, "exec_before_ready ran!\n");
723+
}
670724
}

testcontainers/src/core/image.rs

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ pub use exec::ExecCommand;
44
pub use image_ext::ImageExt;
55
#[cfg(feature = "reusable-containers")]
66
pub use image_ext::ReuseDirective;
7+
use url::Host;
78

89
use crate::{
910
core::{
1011
copy::CopyToContainer,
12+
error::Result,
1113
mounts::Mount,
1214
ports::{ContainerPort, Ports},
1315
WaitFor,
1416
},
15-
TestcontainersError,
17+
ContainerAsync, TestcontainersError,
1618
};
1719

1820
mod exec;
@@ -92,32 +94,48 @@ where
9294
/// This method is useful when certain re-configuration is required after the start
9395
/// of container for the container to be considered ready for use in tests.
9496
#[allow(unused_variables)]
95-
fn exec_after_start(
96-
&self,
97-
cs: ContainerState,
98-
) -> Result<Vec<ExecCommand>, TestcontainersError> {
97+
fn exec_after_start(&self, cs: ContainerState) -> Result<Vec<ExecCommand>> {
98+
Ok(Default::default())
99+
}
100+
101+
/// Returns commands that will be executed after the container has started, but before the
102+
/// [Image::ready_conditions] are awaited for.
103+
///
104+
/// Use this when you, e.g., need to configure something based on the container's ports and host
105+
/// (for example an application that needs to know its own address).
106+
#[allow(unused_variables)]
107+
fn exec_before_ready(&self, cs: ContainerState) -> Result<Vec<ExecCommand>> {
99108
Ok(Default::default())
100109
}
101110
}
102111

103112
#[derive(Debug)]
104113
pub struct ContainerState {
105114
id: String,
115+
host: Host,
106116
ports: Ports,
107117
}
108118

109119
impl ContainerState {
110-
pub fn new(id: impl Into<String>, ports: Ports) -> Self {
111-
Self {
112-
id: id.into(),
113-
ports,
114-
}
120+
pub async fn from_container<I>(container: &ContainerAsync<I>) -> Result<Self>
121+
where
122+
I: Image,
123+
{
124+
Ok(Self {
125+
id: container.id().into(),
126+
host: container.get_host().await?,
127+
ports: container.ports().await?,
128+
})
129+
}
130+
131+
pub fn host(&self) -> &Host {
132+
&self.host
115133
}
116134

117135
/// Returns the host port for the given internal container's port (`IPv4`).
118136
///
119137
/// Results in an error ([`TestcontainersError::PortNotExposed`]) if the port is not exposed.
120-
pub fn host_port_ipv4(&self, internal_port: ContainerPort) -> Result<u16, TestcontainersError> {
138+
pub fn host_port_ipv4(&self, internal_port: ContainerPort) -> Result<u16> {
121139
self.ports
122140
.map_to_host_port_ipv4(internal_port)
123141
.ok_or_else(|| TestcontainersError::PortNotExposed {
@@ -129,7 +147,7 @@ impl ContainerState {
129147
/// Returns the host port for the given internal container's port (`IPv6`).
130148
///
131149
/// Results in an error ([`TestcontainersError::PortNotExposed`]) if the port is not exposed.
132-
pub fn host_port_ipv6(&self, internal_port: ContainerPort) -> Result<u16, TestcontainersError> {
150+
pub fn host_port_ipv6(&self, internal_port: ContainerPort) -> Result<u16> {
133151
self.ports
134152
.map_to_host_port_ipv6(internal_port)
135153
.ok_or_else(|| TestcontainersError::PortNotExposed {

testcontainers/src/runners/async_runner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ where
313313
let container =
314314
ContainerAsync::new(container_id, client.clone(), container_req, network).await?;
315315

316-
let state = ContainerState::new(container.id(), container.ports().await?);
316+
let state = ContainerState::from_container(&container).await?;
317317
for cmd in container.image().exec_after_start(state)? {
318318
container.exec(cmd).await?;
319319
}

0 commit comments

Comments
 (0)