diff --git a/.github/workflows/ci-rootless-podman.yml b/.github/workflows/ci-rootless-podman.yml new file mode 100644 index 00000000000..829b786c6c6 --- /dev/null +++ b/.github/workflows/ci-rootless-podman.yml @@ -0,0 +1,51 @@ +name: CI-Podman-Rootless + +on: + pull_request: {} + push: { branches: [ main ] } + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Uninstall unwanted packages + run: sudo apt-get -q -y --purge remove podman moby-engine moby-buildx && sudo rm -rf /var/run/docker.sock + - name: Set XDG_RUNTIME_DIR + run: echo "XDG_RUNTIME_DIR=/run/user/$UID" >> $GITHUB_ENV + - name: Create registries.conf + # allow pulling images without a registry specified and allow pulling from insecure local registry + run: | + mkdir -p $HOME/.config/containers + echo 'unqualified-search-registries = ["docker.io"]' > $HOME/.config/containers/registries.conf + echo '' >> $HOME/.config/containers/registries.conf + echo '[[registry]]' >> $HOME/.config/containers/registries.conf + echo 'location = "localhost:50001"' >> $HOME/.config/containers/registries.conf + echo 'insecure = true' >> $HOME/.config/containers/registries.conf + - name: Istall latest podman release + # see https://podman.io/getting-started/installation#ubuntu + run: | + sudo mkdir -p /etc/apt/keyrings + curl -fsSL https://download.opensuse.org/repositories/devel:kubic:libcontainers:unstable/xUbuntu_$(lsb_release -rs)/Release.key \ + | gpg --dearmor \ + | sudo tee /etc/apt/keyrings/devel_kubic_libcontainers_unstable.gpg > /dev/null + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/devel_kubic_libcontainers_unstable.gpg]\ + https://download.opensuse.org/repositories/devel:kubic:libcontainers:unstable/xUbuntu_$(lsb_release -rs)/ /" \ + | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list > /dev/null + sudo apt-get update -qq + sudo apt-get -qq -y install podman + - name: Print podman environment information + run: podman info + - name: Enable podman socket + run: systemctl --user enable --now podman.socket + - name: Build with Gradle + run: ./gradlew --no-daemon --scan -Dtest.profile=podman testcontainers:test + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: test report + path: ~/work/testcontainers-java/testcontainers-java/core/build/reports/tests/** diff --git a/build.gradle b/build.gradle index 7405154491b..df9c9e5268c 100644 --- a/build.gradle +++ b/build.gradle @@ -97,6 +97,11 @@ subprojects { failOnPassedAfterRetry = false } } + + // podman does not support compose + if (System.properties['test.profile'] == 'podman') { + exclude '**/*DockerCompose*' + } } tasks.withType(Test).all { diff --git a/core/src/main/java/org/testcontainers/DockerClientFactory.java b/core/src/main/java/org/testcontainers/DockerClientFactory.java index d4b40193ca2..a584071db8c 100644 --- a/core/src/main/java/org/testcontainers/DockerClientFactory.java +++ b/core/src/main/java/org/testcontainers/DockerClientFactory.java @@ -22,6 +22,7 @@ import org.apache.commons.lang3.SystemUtils; import org.testcontainers.dockerclient.DockerClientProviderStrategy; import org.testcontainers.dockerclient.DockerMachineClientProviderStrategy; +import org.testcontainers.dockerclient.RootlessPodmanClientProviderStrategy; import org.testcontainers.dockerclient.TransportConfig; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.images.TimeLimitedLoggedPullImageResultCallback; @@ -366,4 +367,9 @@ public boolean isUsing(Class providerStr public Info getInfo() { return getOrInitializeStrategy().getInfo(); } + + public boolean supportsCompose() { + // podman does not support compose + return !(getOrInitializeStrategy() instanceof RootlessPodmanClientProviderStrategy); + } } diff --git a/core/src/main/java/org/testcontainers/utility/TestEnvironment.java b/core/src/main/java/org/testcontainers/utility/TestEnvironment.java index ee58199540b..21c4a9ffb63 100644 --- a/core/src/main/java/org/testcontainers/utility/TestEnvironment.java +++ b/core/src/main/java/org/testcontainers/utility/TestEnvironment.java @@ -17,6 +17,10 @@ public static boolean dockerApiAtLeast(String minimumVersion) { return current.compareTo(min) >= 0; } + public static boolean clientSupportsCompose() { + return DockerClientFactory.instance().supportsCompose(); + } + public static boolean dockerExecutionDriverSupportsExec() { String executionDriver = DockerClientFactory.instance().getActiveExecutionDriver(); diff --git a/core/src/test/java/org/testcontainers/DockerRegistryContainer.java b/core/src/test/java/org/testcontainers/DockerRegistryContainer.java index 956bb9fff9e..219ee02644e 100644 --- a/core/src/test/java/org/testcontainers/DockerRegistryContainer.java +++ b/core/src/test/java/org/testcontainers/DockerRegistryContainer.java @@ -37,7 +37,7 @@ public DockerRegistryContainer(@NonNull Future image) { @Override protected void configure() { super.configure(); - withEnv("REGISTRY_HTTP_ADDR", "127.0.0.1:0"); + withEnv("REGISTRY_HTTP_ADDR", "127.0.0.1:50001"); withCreateContainerCmdModifier(cmd -> { cmd.getHostConfig().withNetworkMode("host"); }); @@ -77,7 +77,7 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) { ); } - endpoint = getHost() + ":" + port.get(); + endpoint = "http://" + getHost() + ":" + port.get(); } public DockerImageName createImage() { @@ -96,7 +96,7 @@ public DockerImageName createImage(String originalImage, String tag) { String dummyImageId = client.inspectImageCmd(originalImage).exec().getId(); DockerImageName imageName = DockerImageName - .parse(getEndpoint() + "/" + Base58.randomString(6).toLowerCase()) + .parse(getEndpoint().replaceFirst("http://", "") + "/" + Base58.randomString(6).toLowerCase()) .withTag(tag); // push the image to the registry diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java index 12f5810bc27..317eeba42ca 100644 --- a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java @@ -25,6 +25,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -40,6 +41,8 @@ public void shouldReportOOMAfterWait() { Info info = DockerClientFactory.instance().client().infoCmd().exec(); // Poor man's rootless Docker detection :D Assumptions.assumeThat(info.getSecurityOptions()).doesNotContain("rootless"); + // setting swappiness is not allowed for cgroups v2 + Assumptions.assumeThat(info.getRawValues().get("CgroupVersion")).isNotEqualTo("2"); try ( GenericContainer container = new GenericContainer<>(TestImages.TINY_IMAGE) .withStartupCheckStrategy(new NoopStartupCheckStrategy()) @@ -136,7 +139,13 @@ public void shouldOnlyPublishExposedPorts() { .getHostConfig() .getPortBindings() .getBindings(); - assertThat(hostBindings).as("only 1 port is bound on the host (published)").hasSize(1); + // podman also returns unbound ports, but sets the binding value to null + List boundPorts = hostBindings + .values() + .stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + assertThat(boundPorts).as("only 1 port is bound on the host (published)").hasSize(1); Integer mappedPort = container.getMappedPort(8080); assertThat(mappedPort != 8080).as("port 8080 is bound to a different port on the host").isTrue(); diff --git a/core/src/test/java/org/testcontainers/containers/NetworkTest.java b/core/src/test/java/org/testcontainers/containers/NetworkTest.java index 099bef20ed7..cbf06baf066 100644 --- a/core/src/test/java/org/testcontainers/containers/NetworkTest.java +++ b/core/src/test/java/org/testcontainers/containers/NetworkTest.java @@ -1,5 +1,6 @@ package org.testcontainers.containers; +import com.github.dockerjava.api.model.Network.Ipam; import org.junit.Rule; import org.junit.Test; import org.junit.experimental.runners.Enclosed; @@ -65,7 +66,19 @@ public void testNetworkSupport() throws Exception { @Test public void testBuilder() { - try (Network network = Network.builder().driver("macvlan").build()) { + try ( + Network network = Network + .builder() + .driver("macvlan") + .createNetworkCmdModifier(cmd -> { + cmd.withIpam( + // mcvlan needs a subnet or podman will refuse to create the network + // https://docs.podman.io/en/latest/markdown/podman-network-create.1.html#driver-d + new Ipam().withConfig(new Ipam.Config().withSubnet("192.168.100.1/25")) + ); + }) + .build() + ) { String id = network.getId(); assertThat( DockerClientFactory.instance().client().inspectNetworkCmd().withNetworkId(id).exec().getDriver() @@ -78,7 +91,19 @@ public void testBuilder() { @Test public void testModifiers() { try ( - Network network = Network.builder().createNetworkCmdModifier(cmd -> cmd.withDriver("macvlan")).build() + Network network = Network + .builder() + .createNetworkCmdModifier(cmd -> { + cmd + .withDriver("macvlan") + .withIpam( + new Ipam() + // mcvlan needs a subnet or podman will refuse to create the network + // https://docs.podman.io/en/latest/markdown/podman-network-create.1.html#driver-d + .withConfig(new Ipam.Config().withSubnet("192.168.100.1/25")) + ); + }) + .build() ) { String id = network.getId(); assertThat( diff --git a/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java index 0cbf257e9b5..7ef82f28079 100644 --- a/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java +++ b/core/src/test/java/org/testcontainers/containers/wait/strategy/DockerHealthcheckWaitStrategyTest.java @@ -27,7 +27,7 @@ public void setUp() { ) .withFileFromClasspath("Dockerfile", "health-wait-strategy-dockerfile/Dockerfile") ) - .waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(3))); + .waitingFor(Wait.forHealthcheck().withStartupTimeout(Duration.ofSeconds(5))); } @Test diff --git a/core/src/test/java/org/testcontainers/junit/DockerNetworkModeTest.java b/core/src/test/java/org/testcontainers/junit/DockerNetworkModeTest.java index b048e4716fd..97aafb80bdb 100644 --- a/core/src/test/java/org/testcontainers/junit/DockerNetworkModeTest.java +++ b/core/src/test/java/org/testcontainers/junit/DockerNetworkModeTest.java @@ -27,8 +27,11 @@ public void testNoNetworkContainer() { container.start(); NetworkSettings networkSettings = container.getContainerInfo().getNetworkSettings(); - assertThat(networkSettings.getNetworks()).as("only one network is set").hasSize(1); - assertThat(networkSettings.getNetworks()).as("network is 'none'").containsKey("none"); + assertThat(networkSettings.getNetworks()) + .as("only one network is set") + .allSatisfy((name, containerNetwork) -> { + assertThat(name).as("network is 'none'").isEqualTo("none"); + }); } } @@ -43,8 +46,11 @@ public void testHostNetworkContainer() { container.start(); NetworkSettings networkSettings = container.getContainerInfo().getNetworkSettings(); - assertThat(networkSettings.getNetworks()).as("only one network is set").hasSize(1); - assertThat(networkSettings.getNetworks()).as("network is 'host'").containsKey("host"); + assertThat(networkSettings.getNetworks()) + .as("only one network is set") + .allSatisfy((name, containerNetwork) -> { + assertThat(networkSettings.getNetworks()).as("network is 'host'").containsKey("host"); + }); } } } diff --git a/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java b/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java index 0e00dd87bf7..21ba4fe7e34 100644 --- a/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java +++ b/core/src/test/java/org/testcontainers/utility/AuthenticatedImagePullTest.java @@ -3,6 +3,7 @@ import com.github.dockerjava.api.model.AuthConfig; import org.intellij.lang.annotations.Language; import org.junit.AfterClass; +import org.junit.Assume; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -63,14 +64,14 @@ public static void beforeClass() throws Exception { final AuthConfig authConfig = new AuthConfig() .withUsername("testuser") .withPassword("notasecret") - .withRegistryAddress("http://" + testRegistryAddress); + .withRegistryAddress(testRegistryAddress); // Replace the RegistryAuthLocator singleton with our mock, for the duration of this test final RegistryAuthLocator mockAuthLocator = Mockito.mock(RegistryAuthLocator.class); RegistryAuthLocator.setInstance(mockAuthLocator); when( mockAuthLocator.lookupAuthConfig( - argThat(argument -> testRegistryAddress.equals(argument.getRegistry())), + argThat(argument -> testRegistryAddress.replaceFirst("http://", "").equals(argument.getRegistry())), any() ) ) @@ -117,6 +118,7 @@ public void testThatAuthLocatorIsUsedForDockerfileBuild() throws IOException { @Test public void testThatAuthLocatorIsUsedForDockerComposePull() throws IOException { + Assume.assumeTrue(TestEnvironment.clientSupportsCompose()); // Prepare a simple temporary Docker Compose manifest which requires our custom private image Path tempFile = getLocalTempFile(".docker-compose.yml"); @Language("yaml") diff --git a/docs/examples/junit4/generic/src/test/java/generic/CmdModifierTest.java b/docs/examples/junit4/generic/src/test/java/generic/CmdModifierTest.java index a32f23ca8c2..314098bfb7a 100644 --- a/docs/examples/junit4/generic/src/test/java/generic/CmdModifierTest.java +++ b/docs/examples/junit4/generic/src/test/java/generic/CmdModifierTest.java @@ -2,6 +2,7 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.model.Info; +import org.assertj.core.api.Assumptions; import org.junit.Rule; import org.junit.Test; import org.testcontainers.DockerClientFactory; @@ -48,6 +49,9 @@ public void testHostnameModified() throws IOException, InterruptedException { @Test public void testMemoryLimitModified() throws IOException, InterruptedException { + Info info = DockerClientFactory.instance().client().infoCmd().exec(); + // setting swap is not allowed for cgroups v2 + Assumptions.assumeThat(info.getRawValues().get("CgroupVersion")).isNotEqualTo("2"); final Container.ExecResult execResult = memoryLimitedRedis.execInContainer("cat", getMemoryLimitFilePath()); assertThat(execResult.getStdout().trim()).isEqualTo(String.valueOf(memoryInBytes)); }