Skip to content

Commit

Permalink
Add a rootless Docker strategy (#2985)
Browse files Browse the repository at this point in the history
Closes #2943, #1770
  • Loading branch information
bsideup committed Jul 15, 2020
1 parent c137996 commit 50c778f
Show file tree
Hide file tree
Showing 13 changed files with 167 additions and 21 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/ci-rootless.yml
Original file line number Diff line number Diff line change
@@ -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"
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/org/testcontainers/DockerClientFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -580,7 +579,6 @@ interface DockerCompose {
*/
class ContainerisedDockerCompose extends GenericContainer<ContainerisedDockerCompose> implements DockerCompose {

private static final String DOCKER_SOCKET_PATH = "/var/run/docker.sock";
public static final char UNIX_PATH_SEPERATOR = ':';

public ContainerisedDockerCompose(List<File> composeFiles, String identifier) {
Expand All @@ -601,24 +599,18 @@ public ContainerisedDockerCompose(List<File> 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();
Expand Down Expand Up @@ -681,16 +673,26 @@ public DockerCompose withEnv(Map<String, String> 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<String, String> 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<String> absoluteDockerComposeFilePaths = composeFiles.stream()
.map(File::getAbsolutePath)
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public static String start(String hostIpAddress, DockerClient client) {
DockerClientFactory.instance().checkAndPullImage(client, ryukImage);

List<Bind> 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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ org.testcontainers.dockerclient.EnvironmentAndSystemPropertyClientProviderStrate
org.testcontainers.dockerclient.UnixSocketClientProviderStrategy
org.testcontainers.dockerclient.DockerMachineClientProviderStrategy
org.testcontainers.dockerclient.NpipeSocketClientProviderStrategy
org.testcontainers.dockerclient.RootlessDockerClientProviderStrategy
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -48,6 +50,15 @@ public static Iterable<Object[]> data() {
});
}

@Before
public void setUp() {
if (localMode) {
Assumptions.assumeThat(LocalDockerCompose.executableExists())
.as("docker-compose executable exists")
.isTrue();
}
}

@Test
public void test() {
try (DockerComposeContainer compose =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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);
}
Expand Down
12 changes: 6 additions & 6 deletions core/src/test/resources/auth-config/docker-credential-fake
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
#!/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",' \
' "Secret": "secret"' \
'}'
exit 0
fi
if [[ $inputLine == "registrytoken.example.com" ]]; then
if [ "$inputLine" = "registrytoken.example.com" ]; then
echo '{' \
' "ServerURL": "url",' \
' "Username": "<token>",' \
Expand Down
17 changes: 17 additions & 0 deletions docs/features/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

0 comments on commit 50c778f

Please sign in to comment.