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

Add a rootless Docker strategy #2985

Merged
merged 17 commits into from
Jul 15, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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 +130,14 @@ private DockerClientProviderStrategy getOrInitializeStrategy() {
return strategy;
}

@UnstableAPI
public String getDockerUnixSocketPath() {
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,63 @@
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 = EnvironmentAndSystemPropertyClientProviderStrategy.PRIORITY + 20;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to be the highest priority? I'd put this lower than UnixSocketClientProviderStrategy, at least after #1771 is merged (so that DOCKER_HOST is always respected, non-rootless-docker is tried first, and automatically configured rootless docker is tried last of all)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want rootless DOCKER_HOST to take precedence (because we need to override the host ip) but yeah, I see what you mean.

I will make it have lower priority, but ensure that EnvAndSysPropsStrategy does not detect rootless host that easily

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update:
with overridable host, it is no longer a problem when EnvAndSysPropsStrategy detects the rootless DOCKER_HOST


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
public String getDockerHostIpAddress() {
return "localhost";
}

@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.load("c", LibC.class);

int getuid();
}

}
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂 for our tests, could we do this using the host socket URI?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to do string matching, especially given that the feature is experimental and may change the socket location.

Apparently, Docker returns rootless security capability from the info endpoint, just docker-java does not expose it (yet). I will add it to docker-java, so that we can remove this workaround, okay? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I was wondering if that would be the case. Let's not hold up this PR for the change in docker-java, though - it's OK as-is.

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