Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use exposed port #750

Open
ghost opened this issue Oct 17, 2024 · 7 comments
Open

Unable to use exposed port #750

ghost opened this issue Oct 17, 2024 · 7 comments

Comments

@ghost
Copy link

ghost commented Oct 17, 2024

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.

$ cat src/main.rs
use testcontainers::{
    core::{IntoContainerPort, WaitFor},
    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 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);
}

Unfortunately, it does not work.

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.07s
     Running `target/debug/asdf`
thread 'main' panicked at src/main.rs:17:57:
called `Result::unwrap()` on an `Err` value: PortNotExposed { id: "98ae2d954adcbbcc99dbeb7bcccfd5aa6d3507e0d3b36f741849d6b589934ec4", port: Tcp(6379) }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

How can a port be exposed and then used?

@ghost
Copy link
Author

ghost commented Oct 18, 2024

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.

@ghost
Copy link
Author

ghost commented Oct 18, 2024

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"
    }
  ]
}

@ghost
Copy link
Author

ghost commented Oct 19, 2024

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.

@ghost
Copy link
Author

ghost commented Oct 19, 2024

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: moby/moby#42442

@DDtKey
Copy link
Collaborator

DDtKey commented Oct 19, 2024

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

@ghost
Copy link
Author

ghost commented Oct 21, 2024

My temporal workaround is the following modification.

From 7ba724182ee9d4383dfb1bdde8c0b6b1d7395196 Mon Sep 17 00:00:00 2001
From: baghai <[email protected]>
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.

@DDtKey
Copy link
Collaborator

DDtKey commented Oct 21, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant