diff --git a/.github/workflows/ci-rootless.yml b/.github/workflows/ci-rootless.yml new file mode 100644 index 00000000000..e752747b804 --- /dev/null +++ b/.github/workflows/ci-rootless.yml @@ -0,0 +1,31 @@ +name: CI-Docker-Rootless + +on: + pull_request: {} + push: { branches: [ master ] } + +jobs: + test: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: debug + run: id -u; whoami + - name: uninstall rootful Docker + run: sudo apt-get -q -y --purge remove moby-engine moby-buildx && sudo rm -rf /var/run/docker.sock + - name: install rootless Docker + run: curl -fsSL https://get.docker.com/rootless | sh + - name: start rootless Docker + run: PATH=$HOME/bin:$PATH XDG_RUNTIME_DIR=/tmp/docker-$(id -u) dockerd-rootless.sh --experimental --storage-driver vfs & + - name: Build with Gradle + run: XDG_RUNTIME_DIR=/tmp/docker-$(id -u) ./gradlew --no-daemon --scan testcontainers:test + - name: aggregate test reports with ciMate + if: always() + continue-on-error: true + env: + CIMATE_PROJECT_ID: 2348n4vl + CIMATE_CI_KEY: "CI / rootless Docker" + run: | + wget -q https://get.cimate.io/release/linux/cimate + chmod +x cimate + ./cimate "**/TEST-*.xml" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b965eb77dc9..668dde2b28c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,7 @@ jobs: run: docker image prune -af - name: Build and test with Gradle (${{matrix.gradle_args}}) run: | - ./gradlew --no-daemon --continue --scan --info ${{matrix.gradle_args}} + ./gradlew --no-daemon --continue --scan ${{matrix.gradle_args}} - name: Aggregate test reports with ciMate if: always() continue-on-error: true diff --git a/core/src/main/java/org/testcontainers/DockerClientFactory.java b/core/src/main/java/org/testcontainers/DockerClientFactory.java index baebe164c48..b9c4d508f25 100644 --- a/core/src/main/java/org/testcontainers/DockerClientFactory.java +++ b/core/src/main/java/org/testcontainers/DockerClientFactory.java @@ -18,6 +18,7 @@ import lombok.SneakyThrows; import lombok.Synchronized; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang.StringUtils; import org.testcontainers.dockerclient.DockerClientProviderStrategy; import org.testcontainers.dockerclient.DockerMachineClientProviderStrategy; import org.testcontainers.images.TimeLimitedLoggedPullImageResultCallback; @@ -29,6 +30,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -129,6 +131,19 @@ private DockerClientProviderStrategy getOrInitializeStrategy() { return strategy; } + @UnstableAPI + public String getDockerUnixSocketPath() { + String dockerSocketOverride = System.getenv("TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE"); + if (!StringUtils.isBlank(dockerSocketOverride)) { + return dockerSocketOverride; + } + + URI dockerHost = getOrInitializeStrategy().getTransportConfig().getDockerHost(); + return "unix".equals(dockerHost.getScheme()) + ? dockerHost.getRawPath() + : "/var/run/docker.sock"; + } + /** * * @return a new initialized Docker client diff --git a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java index c3fe9ab17f9..8c07c036d11 100644 --- a/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java +++ b/core/src/main/java/org/testcontainers/containers/DockerComposeContainer.java @@ -59,7 +59,6 @@ import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; -import static org.testcontainers.containers.BindMode.READ_ONLY; import static org.testcontainers.containers.BindMode.READ_WRITE; /** @@ -580,7 +579,6 @@ interface DockerCompose { */ class ContainerisedDockerCompose extends GenericContainer implements DockerCompose { - private static final String DOCKER_SOCKET_PATH = "/var/run/docker.sock"; public static final char UNIX_PATH_SEPERATOR = ':'; public ContainerisedDockerCompose(List composeFiles, String identifier) { @@ -601,24 +599,18 @@ public ContainerisedDockerCompose(List composeFiles, String identifier) { final String composeFileEnvVariableValue = Joiner.on(UNIX_PATH_SEPERATOR).join(absoluteDockerComposeFiles); // we always need the UNIX path separator logger().debug("Set env COMPOSE_FILE={}", composeFileEnvVariableValue); addEnv(ENV_COMPOSE_FILE, composeFileEnvVariableValue); - addFileSystemBind(pwd, containerPwd, READ_ONLY); + addFileSystemBind(pwd, containerPwd, READ_WRITE); // Ensure that compose can access docker. Since the container is assumed to be running on the same machine // as the docker daemon, just mapping the docker control socket is OK. // As there seems to be a problem with mapping to the /var/run directory in certain environments (e.g. CircleCI) // we map the socket file outside of /var/run, as just /docker.sock - addFileSystemBind(getDockerSocketHostPath(), "/docker.sock", READ_WRITE); + addFileSystemBind("/" + DockerClientFactory.instance().getDockerUnixSocketPath(), "/docker.sock", READ_WRITE); addEnv("DOCKER_HOST", "unix:///docker.sock"); setStartupCheckStrategy(new IndefiniteWaitOneShotStartupCheckStrategy()); setWorkingDirectory(containerPwd); } - private String getDockerSocketHostPath() { - return SystemUtils.IS_OS_WINDOWS - ? "/" + DOCKER_SOCKET_PATH - : DOCKER_SOCKET_PATH; - } - @Override public void invoke() { super.start(); @@ -681,16 +673,26 @@ public DockerCompose withEnv(Map env) { return this; } + @VisibleForTesting + static boolean executableExists() { + return CommandLine.executableExists(COMPOSE_EXECUTABLE); + } + @Override public void invoke() { // bail out early - if (!CommandLine.executableExists(COMPOSE_EXECUTABLE)) { + if (!executableExists()) { throw new ContainerLaunchException("Local Docker Compose not found. Is " + COMPOSE_EXECUTABLE + " on the PATH?"); } final Map environment = Maps.newHashMap(env); environment.put(ENV_PROJECT_NAME, identifier); + String dockerHost = System.getenv("DOCKER_HOST"); + if (dockerHost == null) { + dockerHost = "unix://" + DockerClientFactory.instance().getDockerUnixSocketPath(); + } + environment.put("DOCKER_HOST", dockerHost); final Stream absoluteDockerComposeFilePaths = composeFiles.stream() .map(File::getAbsolutePath) diff --git a/core/src/main/java/org/testcontainers/dockerclient/RootlessDockerClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/RootlessDockerClientProviderStrategy.java new file mode 100644 index 00000000000..cc6b23e506d --- /dev/null +++ b/core/src/main/java/org/testcontainers/dockerclient/RootlessDockerClientProviderStrategy.java @@ -0,0 +1,58 @@ +package org.testcontainers.dockerclient; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import org.apache.commons.lang.SystemUtils; + +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * + * @deprecated this class is used by the SPI and should not be used directly + */ +@Deprecated +public final class RootlessDockerClientProviderStrategy extends DockerClientProviderStrategy { + + public static final int PRIORITY = UnixSocketClientProviderStrategy.PRIORITY + 1; + + private Path getSocketPath() { + String xdgRuntimeDir = System.getenv("XDG_RUNTIME_DIR"); + if (xdgRuntimeDir == null) { + xdgRuntimeDir = "/run/user/" + LibC.INSTANCE.getuid(); + } + return Paths.get(xdgRuntimeDir).resolve("docker.sock"); + } + + @Override + public TransportConfig getTransportConfig() throws InvalidConfigurationException { + return TransportConfig.builder() + .dockerHost(URI.create("unix://" + getSocketPath().toString())) + .build(); + } + + @Override + protected boolean isApplicable() { + return SystemUtils.IS_OS_LINUX && Files.exists(getSocketPath()); + } + + @Override + public String getDescription() { + return "Rootless Docker accessed via Unix socket (" + getSocketPath() + ")"; + } + + @Override + protected int getPriority() { + return PRIORITY; + } + + private interface LibC extends Library { + + LibC INSTANCE = Native.loadLibrary("c", LibC.class); + + int getuid(); + } + +} diff --git a/core/src/main/java/org/testcontainers/dockerclient/UnixSocketClientProviderStrategy.java b/core/src/main/java/org/testcontainers/dockerclient/UnixSocketClientProviderStrategy.java index fd4ef4f9e6e..6f030ada279 100644 --- a/core/src/main/java/org/testcontainers/dockerclient/UnixSocketClientProviderStrategy.java +++ b/core/src/main/java/org/testcontainers/dockerclient/UnixSocketClientProviderStrategy.java @@ -41,7 +41,7 @@ public TransportConfig getTransportConfig() throws InvalidConfigurationException @Override protected boolean isApplicable() { - return SystemUtils.IS_OS_LINUX; + return SystemUtils.IS_OS_LINUX || SystemUtils.IS_OS_MAC; } @Override diff --git a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java index 5d89ab886e7..e9c0f68a82b 100644 --- a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java +++ b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java @@ -76,7 +76,7 @@ public static String start(String hostIpAddress, DockerClient client) { DockerClientFactory.instance().checkAndPullImage(client, ryukImage); List binds = new ArrayList<>(); - binds.add(new Bind("//var/run/docker.sock", new Volume("/var/run/docker.sock"))); + binds.add(new Bind("/" + DockerClientFactory.instance().getDockerUnixSocketPath(), new Volume("/var/run/docker.sock"))); String ryukContainerId = client.createContainerCmd(ryukImage) .withHostConfig(new HostConfig().withAutoRemove(true)) diff --git a/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy b/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy index 006c3ec7f82..d9cbad0ebff 100644 --- a/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy +++ b/core/src/main/resources/META-INF/services/org.testcontainers.dockerclient.DockerClientProviderStrategy @@ -2,3 +2,4 @@ org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrate org.testcontainers.dockerclient.UnixSocketClientProviderStrategy org.testcontainers.dockerclient.DockerMachineClientProviderStrategy org.testcontainers.dockerclient.NpipeSocketClientProviderStrategy +org.testcontainers.dockerclient.RootlessDockerClientProviderStrategy diff --git a/core/src/test/java/org/testcontainers/junit/DockerComposeOverridesTest.java b/core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java similarity index 90% rename from core/src/test/java/org/testcontainers/junit/DockerComposeOverridesTest.java rename to core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java index 6413694d1c5..9e9eb27b509 100644 --- a/core/src/test/java/org/testcontainers/junit/DockerComposeOverridesTest.java +++ b/core/src/test/java/org/testcontainers/containers/DockerComposeOverridesTest.java @@ -1,6 +1,8 @@ -package org.testcontainers.junit; +package org.testcontainers.containers; import com.google.common.util.concurrent.Uninterruptibles; +import org.assertj.core.api.Assumptions; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -48,6 +50,15 @@ public static Iterable data() { }); } + @Before + public void setUp() { + if (localMode) { + Assumptions.assumeThat(LocalDockerCompose.executableExists()) + .as("docker-compose executable exists") + .isTrue(); + } + } + @Test public void test() { try (DockerComposeContainer compose = diff --git a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java index 78a7a3b1bef..134bdf18f30 100644 --- a/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java +++ b/core/src/test/java/org/testcontainers/containers/GenericContainerTest.java @@ -2,13 +2,16 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse.ContainerState; +import com.github.dockerjava.api.model.Info; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.experimental.FieldDefaults; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; +import org.assertj.core.api.Assumptions; import org.junit.Test; import org.rnorth.ducttape.unreliables.Unreliables; +import org.testcontainers.DockerClientFactory; import org.testcontainers.containers.startupcheck.StartupCheckStrategy; import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy; @@ -21,6 +24,9 @@ public class GenericContainerTest { @Test public void shouldReportOOMAfterWait() { + Info info = DockerClientFactory.instance().client().infoCmd().exec(); + // Poor man's rootless Docker detection :D + Assumptions.assumeThat(info.getDriver()).doesNotContain("vfs"); try ( GenericContainer container = new GenericContainer<>() .withStartupCheckStrategy(new NoopStartupCheckStrategy()) diff --git a/core/src/test/java/org/testcontainers/dockerclient/DockerClientConfigUtilsTest.java b/core/src/test/java/org/testcontainers/dockerclient/DockerClientConfigUtilsTest.java index 353d6ea94e7..442c2efd950 100644 --- a/core/src/test/java/org/testcontainers/dockerclient/DockerClientConfigUtilsTest.java +++ b/core/src/test/java/org/testcontainers/dockerclient/DockerClientConfigUtilsTest.java @@ -1,6 +1,7 @@ package org.testcontainers.dockerclient; import com.github.dockerjava.api.DockerClient; +import org.assertj.core.api.Assumptions; import org.junit.Test; import org.testcontainers.DockerClientFactory; @@ -16,6 +17,10 @@ public class DockerClientConfigUtilsTest { @Test public void getDockerHostIpAddressShouldReturnLocalhostWhenUnixSocket() { + Assumptions.assumeThat(DockerClientConfigUtils.IN_A_CONTAINER) + .as("in a container") + .isFalse(); + String actual = DockerClientProviderStrategy.resolveDockerHostIpAddress(client, URI.create("unix:///var/run/docker.sock")); assertEquals("localhost", actual); } diff --git a/core/src/test/resources/auth-config/docker-credential-fake b/core/src/test/resources/auth-config/docker-credential-fake index 880009f2c2a..753e62ca347 100755 --- a/core/src/test/resources/auth-config/docker-credential-fake +++ b/core/src/test/resources/auth-config/docker-credential-fake @@ -1,21 +1,21 @@ -#!/bin/bash +#!/bin/sh -if [[ $1 != "get" ]]; then +if [ $1 != "get" ]; then exit 1 fi read inputLine -if [[ $inputLine == "registry2.example.com" ]]; then +if [ "$inputLine" = "registry2.example.com" ]; then echo Fake credentials not found on credentials store \'$inputLine\' 1>&2 exit 1 fi -if [[ $inputLine == "https://not.a.real.registry/url" ]]; then +if [ "$inputLine" = "https://not.a.real.registry/url" ]; then echo Fake credentials not found on credentials store \'$inputLine\' 1>&2 exit 1 fi -if [[ $inputLine == "registry.example.com" ]]; then +if [ "$inputLine" = "registry.example.com" ]; then echo '{' \ ' "ServerURL": "url",' \ ' "Username": "username",' \ @@ -23,7 +23,7 @@ if [[ $inputLine == "registry.example.com" ]]; then '}' exit 0 fi -if [[ $inputLine == "registrytoken.example.com" ]]; then +if [ "$inputLine" = "registrytoken.example.com" ]; then echo '{' \ ' "ServerURL": "url",' \ ' "Username": "",' \ diff --git a/docs/features/configuration.md b/docs/features/configuration.md index f0172b3c978..8b12f8e8d54 100644 --- a/docs/features/configuration.md +++ b/docs/features/configuration.md @@ -72,3 +72,20 @@ but does not allow starting privileged containers, you can turn off the Ryuk con > **pull.pause.timeout = 30** > By default Testcontainers will abort the pull of an image if the pull appears stalled (no data transferred) for longer than this duration (in seconds). + +## Customizing Docker host detection + +Testcontainers will attempt to detect the Docker environment and configure everything. + +However, sometimes a customization is required. For that, you can provide the following environment variables: + +> **DOCKER_HOST** = unix:///var/run/docker.sock +> See [Docker environment variables](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables) +> +> **TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE** +> Path to Docker's socket. Used by Ryuk, Docker Compose, and a few other containers that need to perform Docker actions. +> Example: `/var/run/docker-alt.sock` +> +> **TESTCONTAINERS_HOST_OVERRIDE** +> Docker's host on which ports are exposed. +> Example: `docker.svc.local`