Skip to content

[Enhancement]: Containers declared with @Container can not depend od each other mapped ports using method withEnv(). Use Supplier<String> for env values #8823

@simpletasks

Description

@simpletasks

Module

Core

Proposal

When configuring test case like:

    @Container
    public static final GenericContainer<?> ACTIVE_MQ_CONTAINER = new GenericContainer<>(ACTIVE_MQ_IMAGE)
            .withExposedPorts(61616, 8161, 5672)
            .withExtraHost("host.docker.internal", "host-gateway")
            .withAccessToHost(true)
            .withNetwork(NETWORK);
            
            
    @Container
    private static final GenericContainer<?> MOCK_SERVER = new GenericContainer<>(MOCK_SERVER_IMAGE)
            .withExposedPorts(1080)
            .withNetwork(NETWORK)
            .withExtraHost("host.docker.internal", "host-gateway")
            .withAccessToHost(true); 
            
    @Container
    private static final GenericContainer MS_SQL_CONTAINER = new GenericContainer<>(MS_SQL_IMAGE)
            .withEnv("ACCEPT_EULA", "Y")
            .withEnv("SA_PASSWORD", "yourStrong(!)Password")
            .withNetwork(NETWORK)
            .withExposedPorts(1433)
            .withNetworkAliases("base");

    @Container
    private static final GenericContainer<?> APP_SERVER = new GenericContainer<>(APP_IMAGE)
            .dependsOn(MS_SQL_CONTAINER,MOCK_SERVER, ACTIVE_MQ_CONTAINER)
            .withEnv("ConnectionStrings__AzureServiceBus", "amqp://host.docker.internal:" + ACTIVE_MQ_CONTAINER.getMappedPort(5672))
...

dependency will fail because the first three Testcontainers are not started and line:
.withEnv("ConnectionStrings__AzureServiceBus", "amqp://host.docker.internal:" + ACTIVE_MQ_CONTAINER.getMappedPort(5672))
will throw an error because can not read the value from the not-started container.

Automated init sequence when using @container annotation can be replaced with:

 private static final GenericContainer<?> FIRST_CONTAINER = new GenericContainer<>(
        JUnitJupiterTestImages.HTTPD_IMAGE).withExposedPorts(80);

    private static final GenericContainer<?> SECOND_CONTAINER = new GenericContainer<>(
        JUnitJupiterTestImages.HTTPD_IMAGE).withExposedPorts(80);

    private static final GenericContainer<?> APP_CONTAINER = new GenericContainer<>(JUnitJupiterTestImages.HTTPD_IMAGE)
        .dependsOn(FIRST_CONTAINER, SECOND_CONTAINER).withExposedPorts(80)
              .withEnv(FIRST_DEPENDENCY_ENV_KEY, String.valueOf(FIRST_CONTAINER.getFirstMappedPort()))
              .withEnv(SECOND_DEPENDENCY_ENV_KEY, String.valueOf(SECOND_CONTAINER.getFirstMappedPort()));

    @BeforeAll
    public static void setupWithException() {
                FIRST_CONTAINER.start();
                SECOND_CONTAINER.start();
                APP_CONTAINER.start();

A possible solution is to defer the resolution of dependency of ACTIVE_MQ_CONTAINER.getMappedPort(5672)) to a read stage of the dependent container (APP_SERVER container startup time).

Using Supplier<String> instead of String for type of value in Env Map. With this change, the example from above will work.

The current workaround is to start containers manually without @ Container annotation and using manually written checks 'is container started'. Containers in the test class must be declared in an ordered way.
Code snippet for manual container start-check:

    public static final GenericContainer<?> ACTIVE_MQ_CONTAINER;

    static {
        ACTIVE_MQ_CONTAINER = new GenericContainer<>(ACTIVE_MQ_IMAGE)
                .withExposedPorts(61616, 8161, 5672)
                .withExtraHost("host.docker.internal", "host-gateway")
                .withAccessToHost(true)
                .withNetwork(NETWORK);
        ACTIVE_MQ_CONTAINER.start();
    }

    static {
        boolean started = false;
        while (!started) {
            try {
                log.info("ACTIVE_MQ_CONTAINER.getFirstMappedPort(): " + ACTIVE_MQ_CONTAINER.getMappedPort(5672));
                started = true;
            } catch (Exception e) {
                // nothing
                log.info("in loop error");
            }
        }
    }

alternative is:

 private static final GenericContainer<?> FIRST_CONTAINER = new GenericContainer<>(
        JUnitJupiterTestImages.HTTPD_IMAGE).withExposedPorts(80);

    private static final GenericContainer<?> SECOND_CONTAINER = new GenericContainer<>(
        JUnitJupiterTestImages.HTTPD_IMAGE).withExposedPorts(80);

    private static final GenericContainer<?> APP_CONTAINER = new GenericContainer<>(JUnitJupiterTestImages.HTTPD_IMAGE)
        .dependsOn(FIRST_CONTAINER, SECOND_CONTAINER).withExposedPorts(80);

    @BeforeAll
    public static void setupWithException() {
                FIRST_CONTAINER.start();
                SECOND_CONTAINER.start();
                // read mapped ports after containers are started
                APP_CONTAINER
                     .withEnv(FIRST_DEPENDENCY_ENV_KEY, String.valueOf(FIRST_CONTAINER.getFirstMappedPort()))
                     .withEnv(SECOND_DEPENDENCY_ENV_KEY, String.valueOf(SECOND_CONTAINER.getFirstMappedPort()));

                APP_CONTAINER.start();

Expected behavior should be:

    @Container
    public static final GenericContainer<?> ACTIVE_MQ_CONTAINER = new GenericContainer<>(ACTIVE_MQ_IMAGE)
            .withExposedPorts(61616, 8161, 5672);
            
            
    @Container
    private static final GenericContainer<?> MOCK_SERVER = new GenericContainer<>(MOCK_SERVER_IMAGE)
            .withExposedPorts(1080);
            
    @Container
    private static final GenericContainer MS_SQL_CONTAINER = new GenericContainer<>(MS_SQL_IMAGE)
            .withEnv("ACCEPT_EULA", "Y")
            .withEnv("SA_PASSWORD", "yourStrong(!)Password")
            .withNetwork(NETWORK)
            .withExposedPorts(1433);

    @Container
    private static final GenericContainer<?> APP_SERVER = new GenericContainer<>(APP_IMAGE)
            .dependsOn(MS_SQL_CONTAINER,MOCK_SERVER, ACTIVE_MQ_CONTAINER)
            .withEnv("ConnectionStrings__AzureServiceBus", () -> "amqp://host.docker.internal:" + ACTIVE_MQ_CONTAINER.getMappedPort(5672))
...

Last line:
.withEnv("ConnectionStrings__AzureServiceBus", "amqp://host.docker.internal:" + ACTIVE_MQ_CONTAINER.getMappedPort(5672))
replaced by deferred value retrieval:
.withEnv("ConnectionStrings__AzureServiceBus", () -> "amqp://host.docker.internal:" + ACTIVE_MQ_CONTAINER.getMappedPort(5672))

ensures proper startup order when one container depends on some runtime value of another container.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions