Open baghai opened 5 days ago
Alright, I preliminarily dismissed that I am running Podman, not Docker. As it turns out, this error is related to Podman.
With the following code, I am viewing the network settings of the container.
use testcontainers::{
core::{IntoContainerPort, WaitFor},
core::client,
runners::AsyncRunner,
GenericImage,
};
#[tokio::main]
async fn main() {
let container = GenericImage::new("redis", "7.2.4")
.with_exposed_port(6379.tcp())
.with_wait_for(WaitFor::message_on_stdout("Ready to accept connections"))
.start()
.await
.unwrap();
let docker = client::docker_client_instance().await.unwrap();
let id = container.id();
let ports = docker
.inspect_container(id, None)
.await
.unwrap()
.network_settings
.unwrap_or_default()
.ports;
// .map(Ports::try_from)
// .transpose()?
// .unwrap_or_default();
println!("{:?}", ports);
let host = container.get_host().await.unwrap();
let port = container.get_host_port_ipv4(6379).await.unwrap();
let url = format!("redis://{host}:{port}");
println!("Redis is running on {}", url);
}
Here is the output with Podman (Fedora 41):
$ cargo run
Compiling quickstart v0.1.0 (/tmp/testcontainers-rs-quickstart)
Finished dev [unoptimized + debuginfo] target(s) in 6.70s
Running `target/debug/quickstart`
Some({"6379/tcp": Some([PortBinding { host_ip: Some(""), host_port: Some("44971") }])})
thread 'main' panicked at src/main.rs:34:57:
called `Result::unwrap()` on an `Err` value: PortNotExposed { id: "33272a500a0a9f34126fb2f752df14f68b0649ec824a86dc753a79661031e99a", port: Tcp(6379) }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
And here is the output with Docker (Ubuntu 24.04.1 + Docker 24.0.7 from App Center):
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/quickstart`
Some({"6379/tcp": Some([PortBinding { host_ip: Some("0.0.0.0"), host_port: Some("32770") }, PortBinding { host_ip: Some("::"), host_port: Some("32770") }])})
Redis is running on redis://localhost:32770
To spell it out, the host_ip
differs. While Docker provides both 0.0.0.0
and ::
, Podman only has ""
.
I will try to figure out why that is and what can be done about this.
The code responsible for extracting port and host information is in Ports::try_from
. As is, this code does not handle the case that Podman returns.
One can get the list of running containers with this:
$ curl --unix-socket /run/user/1000/podman/podman.sock http://v1.47/containers/json
Port mapping output from Podman:
$ curl --silent --unix-socket /run/user/1000/podman/podman.sock http://v1.47/containers/70fdd33d3a13f4fb68dab82431c614d0d46b3e224c703a3ce314acfad675a2bc/json|jq .NetworkSettings.Ports
{
"6379/tcp": [
{
"HostIp": "",
"HostPort": "44971"
}
]
}
Here the output with Docker:
$ curl --silent --unix-socket /var/run/docker.sock http://v1.47/containers/17e344a692c1121f76f65efa712a1c4302f09666eeef8077795937b799454723/json |jq .NetworkSettings.Ports
{
"6379/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "32770"
},
{
"HostIp": "::",
"HostPort": "32770"
}
]
}
Here is the official API documentation: https://docs.docker.com/reference/api/engine/version/v1.47/#tag/Container/operation/ContainerInspect
The description of .NetworkSettings.Ports
is as follows:
PortMap describes the mapping of container ports to host ports, using the container's port-number and protocol as key in the format
/ , for example, 80/udp. If a container's port is mapped for multiple protocols, separate entries are added to the mapping table.
As shown in the previous comment, Ports
is an array of a object with two fields (quoting from the documentation that I have linked above):
HostIp
[string]: Host IP address that the container's port is mapped to.HostPort
[string]: Host port number that the container's port is mapped to.Does Podman follow this definition? I am not sure.
Shall testcontainers-rs have a workaround for Podman? I will now look into other implementations of testcontainers to see if and how they account for the output Podman is providing.
In testcontainers-go
, the HostIp
seems to be ignored: DockerContainer.MappedPort()
The following issue suggests that the intention of Docker is to map an exposed port to the same port on both, IPv4 and IPv6: https://github.com/moby/moby/issues/42442
That's a good research, thank you!
I think we need to improve the experience with podman.
Also, Testonctiners usually have separate guide for alternative engines, e.g Go version. We need to improve our documentation
My temporal workaround is the following modification.
From 7ba724182ee9d4383dfb1bdde8c0b6b1d7395196 Mon Sep 17 00:00:00 2001
From: baghai <184649356+baghai@users.noreply.github.com>
Date: Mon, 21 Oct 2024 13:50:07 +0200
Subject: [PATCH] Ports: work around for Podman
---
testcontainers/src/core/ports.rs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/testcontainers/src/core/ports.rs b/testcontainers/src/core/ports.rs
index 0a8e377..6eece89 100644
--- a/testcontainers/src/core/ports.rs
+++ b/testcontainers/src/core/ports.rs
@@ -96,6 +96,14 @@ impl TryFrom<PortMap> for Ports {
.parse()
.map_err(PortMappingError::FailedToParseHostPort)?;
+ // FIXME QUICK AND DIRTY WORKAROUND FOR PODMAN
+ // Podman has empty HostIp.
+ // Does Podman ensure that both IPv4 and IPv6 are on the same port?
+ if binding.host_ip.clone().unwrap() == "" {
+ ipv4_mapping.insert(container_port, host_port);
+ ipv6_mapping.insert(container_port, host_port);
+ }
+
// switch on the IP version of the `HostIp`
let mapping = match binding.host_ip.map(|ip| ip.parse()) {
Some(Ok(IpAddr::V4(_))) => {
--
2.43.5
I will try to come up with a good solution to this problem. Assuming that there are or will be more situations where workarounds for Podman are required, I would like to find a method to detect if Podman or Docker is used, then switch on that outcome. Alternatively, it may be valid to do something similar as testcontainers-go is doing, i.e. ignore the differentiation of IPv4 and IPv6. My gut feeling is that this is the worse option.
Alternatively, it may be valid to do something similar as testcontainers-go is doing, i.e. ignore the differentiation of IPv4 and IPv6
Personally, I find the differentiation useful, it provides better control IMO. But we can try to find a compromise
As a continuation of #749, I am trying to get a very simple example working. Unfortunately, I seem to be misusing the functions and am unable to figure out how to expose a port and then access the exposed port.
This is the example I am trying to make work. It is based on the example from the Quickstart.
Unfortunately, it does not work.
How can a port be exposed and then used?