Skip to content

Commit a1b52f1

Browse files
committed
feat(async, network): add support for aliases, dynamic connect & disconnect
1 parent 1cb9e92 commit a1b52f1

File tree

8 files changed

+102
-22
lines changed

8 files changed

+102
-22
lines changed

testcontainers/src/core/client.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ pub enum ClientError {
101101
#[error("failed to remove a network: {0}")]
102102
RemoveNetwork(BollardError),
103103

104+
#[error("failed to connect container to network")]
105+
ConnectionError(BollardError),
106+
#[error("failed to disconnect container from network")]
107+
DisconnectionError(BollardError),
108+
104109
#[error("failed to initialize exec command: {0}")]
105110
InitExec(BollardError),
106111
#[error("failed to inspect exec command: {0}")]
@@ -114,7 +119,7 @@ pub enum ClientError {
114119
/// The internal client.
115120
pub(crate) struct Client {
116121
pub(crate) config: env::Config,
117-
bollard: Docker,
122+
pub(crate) bollard: Docker,
118123
}
119124

120125
impl Client {
@@ -429,11 +434,11 @@ impl Client {
429434
match result {
430435
Ok(r) => {
431436
if let Some(s) = r.stream {
432-
log::info!("{}", s);
437+
log::info!("{s}");
433438
}
434439
}
435440
Err(err) => {
436-
log::error!("{:?}", err);
441+
log::error!("{err:?}");
437442
return Err(ClientError::BuildImage {
438443
descriptor: descriptor.into(),
439444
err,

testcontainers/src/core/containers/async_container.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{fmt, net::IpAddr, pin::Pin, str::FromStr, sync::Arc, time::Duration};
22

3+
use bollard::models::{EndpointSettings, NetworkConnectRequest, NetworkDisconnectRequest};
34
use tokio::io::{AsyncBufRead, AsyncReadExt};
45
use tokio_stream::StreamExt;
56

@@ -8,7 +9,7 @@ use crate::{
89
async_drop,
910
client::Client,
1011
env,
11-
error::{ContainerMissingInfo, ExecError, Result, TestcontainersError},
12+
error::{ClientError, ContainerMissingInfo, ExecError, Result, TestcontainersError},
1213
network::Network,
1314
ports::Ports,
1415
wait::WaitStrategy,
@@ -59,7 +60,9 @@ where
5960
network: Option<Arc<Network>>,
6061
) -> Result<ContainerAsync<I>> {
6162
let ready_conditions = container_req.ready_conditions();
62-
let container = Self::construct(id, docker_client, container_req, network);
63+
let mut container = Self::construct(id, docker_client, container_req, network.clone());
64+
container.connect(network).await?;
65+
6366
let state = ContainerState::from_container(&container).await?;
6467
for cmd in container.image().exec_before_ready(state)? {
6568
container.exec(cmd).await?;
@@ -68,6 +71,63 @@ where
6871
Ok(container)
6972
}
7073

74+
pub async fn connect(&mut self, net: Option<Arc<Network>>) -> Result<()> {
75+
let container_id = self.id().into();
76+
let net = if let Some(net) = net {
77+
net
78+
} else {
79+
return Ok(());
80+
};
81+
82+
let net_aliases_vec = net.aliases.clone().into_iter().collect();
83+
let endpoint_settings = EndpointSettings {
84+
aliases: Some(net_aliases_vec),
85+
..Default::default()
86+
};
87+
88+
let bollard_netword_connect_request = NetworkConnectRequest {
89+
container: Some(container_id),
90+
endpoint_config: Some(endpoint_settings),
91+
};
92+
self.docker_client
93+
.bollard
94+
.connect_network(&net.name, bollard_netword_connect_request)
95+
.await
96+
.map_err(|err| TestcontainersError::Client(ClientError::ConnectionError(err)))?;
97+
98+
Ok(())
99+
}
100+
101+
pub async fn disconnect(&mut self, name: &str) -> Result<()> {
102+
let container_id = self.id().into();
103+
let network_disconnect_request = NetworkDisconnectRequest {
104+
container: Some(container_id),
105+
force: Some(false),
106+
};
107+
self.docker_client
108+
.bollard
109+
.disconnect_network(name, network_disconnect_request)
110+
.await
111+
.map_err(|err| TestcontainersError::Client(ClientError::DisconnectionError(err)))?;
112+
113+
Ok(())
114+
}
115+
116+
pub async fn force_disconnect(&mut self, name: &str) -> Result<()> {
117+
let container_id = self.id().into();
118+
let network_disconnect_request = NetworkDisconnectRequest {
119+
container: Some(container_id),
120+
force: Some(true),
121+
};
122+
self.docker_client
123+
.bollard
124+
.disconnect_network(name, network_disconnect_request)
125+
.await
126+
.map_err(|err| TestcontainersError::Client(ClientError::DisconnectionError(err)))?;
127+
128+
Ok(())
129+
}
130+
71131
pub(crate) fn construct(
72132
id: String,
73133
docker_client: Arc<Client>,
@@ -221,7 +281,7 @@ where
221281
cmd_ready_condition,
222282
} = cmd;
223283

224-
log::debug!("Executing command {:?}", cmd);
284+
log::debug!("Executing command {cmd:?}",);
225285

226286
let mut exec = self.docker_client.exec(&self.id, cmd).await?;
227287
self.block_until_ready(container_ready_conditions).await?;
@@ -442,7 +502,7 @@ where
442502
match command {
443503
env::Command::Remove => {
444504
if let Err(e) = client.rm(&id).await {
445-
log::error!("Failed to remove container on drop: {}", e);
505+
log::error!("Failed to remove container on drop: {e}");
446506
}
447507
}
448508
env::Command::Keep => {}
@@ -573,7 +633,7 @@ mod tests {
573633
while let Some(result) = stderr_lines.next_line().await.transpose() {
574634
match result {
575635
Ok(line) => {
576-
log::debug!(target: "container", "[{container_id}]:{}", line);
636+
log::debug!(target: "container", "[{container_id}]:{line}");
577637
}
578638
Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => {
579639
log::debug!(target: "container", "[{container_id}] EOF");

testcontainers/src/core/containers/request.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{
22
borrow::Cow,
3-
collections::BTreeMap,
3+
collections::{BTreeMap, HashSet},
44
fmt::{Debug, Formatter},
55
net::IpAddr,
66
time::Duration,
@@ -25,6 +25,7 @@ pub struct ContainerRequest<I: Image> {
2525
pub(crate) image_tag: Option<String>,
2626
pub(crate) container_name: Option<String>,
2727
pub(crate) network: Option<String>,
28+
pub(crate) network_aliases: HashSet<String>,
2829
pub(crate) labels: BTreeMap<String, String>,
2930
pub(crate) env_vars: BTreeMap<String, String>,
3031
pub(crate) hosts: BTreeMap<String, Host>,
@@ -228,6 +229,7 @@ impl<I: Image> From<I> for ContainerRequest<I> {
228229
image_tag: None,
229230
container_name: None,
230231
network: None,
232+
network_aliases: HashSet::new(),
231233
labels: BTreeMap::default(),
232234
env_vars: BTreeMap::default(),
233235
hosts: BTreeMap::default(),

testcontainers/src/core/image/image_ext.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ pub trait ImageExt<I: Image> {
6969
/// Sets the network the container will be connected to.
7070
fn with_network(self, network: impl Into<String>) -> ContainerRequest<I>;
7171

72+
// Adds the specified network alias to the container.
73+
fn with_net_alias(self, network_alias: impl Into<String>) -> ContainerRequest<I>;
74+
7275
/// Adds the specified label to the container.
7376
///
7477
/// **Note**: all keys in the `org.testcontainers.*` namespace should be regarded
@@ -249,6 +252,12 @@ impl<RI: Into<ContainerRequest<I>>, I: Image> ImageExt<I> for RI {
249252
}
250253
}
251254

255+
fn with_net_alias(self, network_alias: impl Into<String>) -> ContainerRequest<I> {
256+
let mut container_req = self.into();
257+
container_req.network_aliases.insert(network_alias.into());
258+
container_req
259+
}
260+
252261
fn with_label(self, key: impl Into<String>, value: impl Into<String>) -> ContainerRequest<I> {
253262
let mut container_req = self.into();
254263

testcontainers/src/core/network.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::{
2-
collections::HashMap,
2+
collections::{HashMap, HashSet},
33
fmt,
44
sync::{Arc, OnceLock, Weak},
55
};
@@ -19,16 +19,18 @@ fn created_networks() -> &'static Mutex<HashMap<String, Weak<Network>>> {
1919
CREATED_NETWORKS.get_or_init(|| Mutex::new(HashMap::new()))
2020
}
2121

22-
pub(crate) struct Network {
23-
name: String,
24-
id: String,
25-
client: Arc<Client>,
22+
pub struct Network {
23+
pub name: String,
24+
pub id: String,
25+
pub(crate) client: Arc<Client>,
26+
pub aliases: HashSet<String>,
2627
}
2728

2829
impl Network {
2930
pub(crate) async fn new(
3031
name: impl Into<String>,
3132
client: Arc<Client>,
33+
aliases: HashSet<String>,
3234
) -> Result<Option<Arc<Self>>, ClientError> {
3335
let name = name.into();
3436
let mut guard = created_networks().lock().await;
@@ -46,6 +48,7 @@ impl Network {
4648
name: name.clone(),
4749
id,
4850
client,
51+
aliases,
4952
});
5053

5154
guard.insert(name, Arc::downgrade(&created));

testcontainers/src/core/ports.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,17 +101,13 @@ impl TryFrom<PortMap> for Ports {
101101
let mapping = match binding.host_ip.map(|ip| ip.parse()) {
102102
Some(Ok(IpAddr::V4(_))) => {
103103
log::debug!(
104-
"Registering IPv4 port mapping: {} -> {}",
105-
container_port,
106-
host_port
104+
"Registering IPv4 port mapping: {container_port} -> {host_port}",
107105
);
108106
&mut ipv4_mapping
109107
}
110108
Some(Ok(IpAddr::V6(_))) => {
111109
log::debug!(
112-
"Registering IPv6 port mapping: {} -> {}",
113-
container_port,
114-
host_port
110+
"Registering IPv6 port mapping: {container_port} -> {host_port}",
115111
);
116112
&mut ipv6_mapping
117113
}

testcontainers/src/runners/async_runner.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,12 @@ where
172172
host_config.network_mode = Some(network.to_string());
173173
host_config
174174
});
175-
Network::new(network, client.clone()).await?
175+
Network::new(
176+
network,
177+
client.clone(),
178+
container_req.network_aliases.clone(),
179+
)
180+
.await?
176181
} else {
177182
None
178183
};

testcontainers/tests/async_runner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ async fn async_copy_files_to_container() -> anyhow::Result<()> {
284284
let mut out = String::new();
285285
container.stdout(false).read_to_string(&mut out).await?;
286286

287-
println!("{}", out);
287+
println!("{out}");
288288
assert!(out.contains("foofoofoo"));
289289
assert!(out.contains("barbarbar"));
290290

0 commit comments

Comments
 (0)