Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.AuthConfig;
import com.google.common.annotations.VisibleForTesting;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -33,6 +34,8 @@ public class RegistryAuthLocator {

private static final String DEFAULT_REGISTRY_NAME = "https://index.docker.io/v1/";

private static final String DOCKER_AUTH_ENV_VAR = "DOCKER_AUTH_CONFIG";

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

private static RegistryAuthLocator instance;
Expand All @@ -43,6 +46,8 @@ public class RegistryAuthLocator {

private final File configFile;

private final String configEnv;

private final Map<String, Optional<AuthConfig>> cache = new ConcurrentHashMap<>();

/**
Expand All @@ -54,11 +59,13 @@ public class RegistryAuthLocator {
@VisibleForTesting
RegistryAuthLocator(
File configFile,
String configEnv,
String commandPathPrefix,
String commandExtension,
Map<String, String> notFoundMessageHolderReference
) {
this.configFile = configFile;
this.configEnv = configEnv;
this.commandPathPrefix = commandPathPrefix;
this.commandExtension = commandExtension;

Expand All @@ -72,6 +79,7 @@ protected RegistryAuthLocator() {
.getenv()
.getOrDefault("DOCKER_CONFIG", System.getProperty("user.home") + "/.docker");
this.configFile = new File(dockerConfigLocation + "/config.json");
this.configEnv = System.getenv(DOCKER_AUTH_ENV_VAR);
this.commandPathPrefix = "";
this.commandExtension = "";

Expand Down Expand Up @@ -131,15 +139,8 @@ public AuthConfig lookupAuthConfig(DockerImageName dockerImageName, AuthConfig d
}

private Optional<AuthConfig> lookupUncachedAuthConfig(String registryName, DockerImageName dockerImageName) {
log.debug(
"RegistryAuthLocator has configFile: {} ({}) and commandPathPrefix: {}",
configFile,
configFile.exists() ? "exists" : "does not exist",
commandPathPrefix
);

try {
final JsonNode config = OBJECT_MAPPER.readTree(configFile);
final JsonNode config = getDockerAuthConfig();
log.debug("registryName [{}] for dockerImageName [{}]", registryName, dockerImageName);

// use helper preferentially (per https://docs.docker.com/engine/reference/commandline/cli/)
Expand All @@ -162,15 +163,43 @@ private Optional<AuthConfig> lookupUncachedAuthConfig(String registryName, Docke
}
} catch (Exception e) {
log.info(
"Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: {}, configFile: {}. Falling back to docker-java default behaviour. Exception message: {}",
"Failure when attempting to lookup auth config. Please ignore if you don't have images in an authenticated registry. Details: (dockerImageName: {}, configFile: {}, configEnv: {}). Falling back to docker-java default behaviour. Exception message: {}",
dockerImageName,
configFile,
DOCKER_AUTH_ENV_VAR,
e.getMessage()
);
}
return Optional.empty();
}

private JsonNode getDockerAuthConfig() throws Exception {
log.debug(
"RegistryAuthLocator has configFile: {} ({}) configEnv: {} ({}) and commandPathPrefix: {}",
configFile,
configFile.exists() ? "exists" : "does not exist",
DOCKER_AUTH_ENV_VAR,
configEnv != null ? "exists" : "does not exist",
commandPathPrefix
);

if (configEnv != null) {
log.debug("RegistryAuthLocator reading from environment variable: {}", DOCKER_AUTH_ENV_VAR);
return OBJECT_MAPPER.readTree(configEnv);
} else if (configFile.exists()) {
log.debug("RegistryAuthLocator reading from configFile: {}", configFile);
return OBJECT_MAPPER.readTree(configFile);
}

throw new NotFoundException(
"No config supplied. Checked in order: " +
configFile +
" (file not found), " +
DOCKER_AUTH_ENV_VAR +
" (not set)"
);
}

private AuthConfig findExistingAuthConfig(final JsonNode config, final String reposName) throws Exception {
final Map.Entry<String, JsonNode> entry = findAuthNode(config, reposName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import com.github.dockerjava.api.model.AuthConfig;
import com.google.common.io.Resources;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.SystemUtils;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

Expand All @@ -16,7 +20,7 @@
public class RegistryAuthLocatorTest {

@Test
public void lookupAuthConfigWithoutCredentials() throws URISyntaxException {
public void lookupAuthConfigWithoutCredentials() throws URISyntaxException, IOException {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why was IOException added to all the throws clauses of the tests?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

final RegistryAuthLocator authLocator = createTestAuthLocator("config-empty.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -32,7 +36,7 @@ public void lookupAuthConfigWithoutCredentials() throws URISyntaxException {
}

@Test
public void lookupAuthConfigWithBasicAuthCredentials() throws URISyntaxException {
public void lookupAuthConfigWithBasicAuthCredentials() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-basic-auth.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -48,7 +52,7 @@ public void lookupAuthConfigWithBasicAuthCredentials() throws URISyntaxException
}

@Test
public void lookupAuthConfigWithJsonKeyCredentials() throws URISyntaxException {
public void lookupAuthConfigWithJsonKeyCredentials() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-json-key.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -64,7 +68,8 @@ public void lookupAuthConfigWithJsonKeyCredentials() throws URISyntaxException {
}

@Test
public void lookupAuthConfigWithJsonKeyCredentialsPartialMatchShouldGiveNoResult() throws URISyntaxException {
public void lookupAuthConfigWithJsonKeyCredentialsPartialMatchShouldGiveNoResult()
throws URISyntaxException, IOException {
// contains entry for registry.example.com
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-json-key.json");

Expand All @@ -78,7 +83,7 @@ public void lookupAuthConfigWithJsonKeyCredentialsPartialMatchShouldGiveNoResult
}

@Test
public void lookupAuthConfigUsingStore() throws URISyntaxException {
public void lookupAuthConfigUsingStore() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-store.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -98,7 +103,7 @@ public void lookupAuthConfigUsingStore() throws URISyntaxException {
}

@Test
public void lookupAuthConfigUsingHelper() throws URISyntaxException {
public void lookupAuthConfigUsingHelper() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-helper.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -118,7 +123,7 @@ public void lookupAuthConfigUsingHelper() throws URISyntaxException {
}

@Test
public void lookupAuthConfigUsingHelperWithToken() throws URISyntaxException {
public void lookupAuthConfigUsingHelperWithToken() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-helper-using-token.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -132,7 +137,7 @@ public void lookupAuthConfigUsingHelperWithToken() throws URISyntaxException {
}

@Test
public void lookupUsingHelperEmptyAuth() throws URISyntaxException {
public void lookupUsingHelperEmptyAuth() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-empty-auth-with-helper.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -152,7 +157,7 @@ public void lookupUsingHelperEmptyAuth() throws URISyntaxException {
}

@Test
public void lookupNonEmptyAuthWithHelper() throws URISyntaxException {
public void lookupNonEmptyAuthWithHelper() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-existing-auth-with-helper.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
Expand All @@ -172,7 +177,7 @@ public void lookupNonEmptyAuthWithHelper() throws URISyntaxException {
}

@Test
public void lookupAuthConfigWithCredentialsNotFound() throws URISyntaxException {
public void lookupAuthConfigWithCredentialsNotFound() throws URISyntaxException, IOException {
Map<String, String> notFoundMessagesReference = new HashMap<>();
final RegistryAuthLocator authLocator = createTestAuthLocator(
"config-with-store.json",
Expand All @@ -198,7 +203,7 @@ public void lookupAuthConfigWithCredentialsNotFound() throws URISyntaxException
}

@Test
public void lookupAuthConfigWithCredStoreEmpty() throws URISyntaxException {
public void lookupAuthConfigWithCredStoreEmpty() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-with-store-empty.json");

DockerImageName dockerImageName = DockerImageName.parse("registry2.example.com/org/repo");
Expand All @@ -207,18 +212,106 @@ public void lookupAuthConfigWithCredStoreEmpty() throws URISyntaxException {
assertThat(authConfig.getAuth()).as("CredStore field will be ignored, because value is blank").isNull();
}

@Test
public void lookupAuthConfigFromEnvVarWithCredStoreEmpty() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator(null, "config-with-store-empty.json");

DockerImageName dockerImageName = DockerImageName.parse("registry2.example.com/org/repo");
final AuthConfig authConfig = authLocator.lookupAuthConfig(dockerImageName, new AuthConfig());

assertThat(authConfig.getAuth()).as("CredStore field will be ignored, because value is blank").isNull();
}

@Test
public void lookupAuthConfigWithoutConfigFile() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator(null);

final AuthConfig authConfig = authLocator.lookupAuthConfig(
DockerImageName.parse("unauthenticated.registry.org/org/repo"),
new AuthConfig()
);

assertThat(authConfig.getRegistryAddress())
.as("Default docker registry URL is set on auth config")
.isEqualTo("https://index.docker.io/v1/");
assertThat(authConfig.getUsername()).as("No username is set").isNull();
assertThat(authConfig.getPassword()).as("No password is set").isNull();
}

@Test
public void lookupAuthConfigRespectsCheckOrderPreference() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator("config-empty.json", "config-basic-auth.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
DockerImageName.parse("registry.example.com/org/repo"),
new AuthConfig()
);

assertThat(authConfig.getRegistryAddress())
.as("Default docker registry URL is set on auth config")
.isEqualTo("https://registry.example.com");
assertThat(authConfig.getUsername()).as("Username is set").isEqualTo("user");
assertThat(authConfig.getPassword()).as("Password is set").isEqualTo("pass");
}

@Test
public void lookupAuthConfigFromEnvironmentVariable() throws URISyntaxException, IOException {
final RegistryAuthLocator authLocator = createTestAuthLocator(null, "config-basic-auth.json");

final AuthConfig authConfig = authLocator.lookupAuthConfig(
DockerImageName.parse("registry.example.com/org/repo"),
new AuthConfig()
);

assertThat(authConfig.getRegistryAddress())
.as("Default docker registry URL is set on auth config")
.isEqualTo("https://registry.example.com");
assertThat(authConfig.getUsername()).as("Username is set").isEqualTo("user");
assertThat(authConfig.getPassword()).as("Password is set").isEqualTo("pass");
}

@NotNull
private RegistryAuthLocator createTestAuthLocator(String configName, String envConfigName)
throws URISyntaxException, IOException {
return createTestAuthLocator(configName, envConfigName, new HashMap<>());
}

@NotNull
private RegistryAuthLocator createTestAuthLocator(String configName) throws URISyntaxException {
return createTestAuthLocator(configName, new HashMap<>());
private RegistryAuthLocator createTestAuthLocator(String configName) throws URISyntaxException, IOException {
return createTestAuthLocator(configName, null, new HashMap<>());
}

@NotNull
private RegistryAuthLocator createTestAuthLocator(String configName, Map<String, String> notFoundMessagesReference)
throws URISyntaxException {
final File configFile = new File(Resources.getResource("auth-config/" + configName).toURI());
throws URISyntaxException, IOException {
return createTestAuthLocator(configName, null, notFoundMessagesReference);
}

String commandPathPrefix = configFile.getParentFile().getAbsolutePath() + "/";
@NotNull
private RegistryAuthLocator createTestAuthLocator(
String configName,
String envConfigName,
Map<String, String> notFoundMessagesReference
) throws URISyntaxException, IOException {
File configFile = null;
String commandPathPrefix = "";
String commandExtension = "";
String configEnv = null;

if (configName != null) {
configFile = new File(Resources.getResource("auth-config/" + configName).toURI());

commandPathPrefix = configFile.getParentFile().getAbsolutePath() + "/";
} else {
configFile = new File(new URI("file:///not-exists.json"));
}

if (envConfigName != null) {
final File envConfigFile = new File(Resources.getResource("auth-config/" + envConfigName).toURI());
configEnv = FileUtils.readFileToString(envConfigFile, StandardCharsets.UTF_8);

commandPathPrefix = envConfigFile.getParentFile().getAbsolutePath() + "/";
}

if (SystemUtils.IS_OS_WINDOWS) {
commandPathPrefix += "win/";
Expand All @@ -228,6 +321,12 @@ private RegistryAuthLocator createTestAuthLocator(String configName, Map<String,
commandExtension = ".bat";
}

return new RegistryAuthLocator(configFile, commandPathPrefix, commandExtension, notFoundMessagesReference);
return new RegistryAuthLocator(
configFile,
configEnv,
commandPathPrefix,
commandExtension,
notFoundMessagesReference
);
}
}
9 changes: 9 additions & 0 deletions docs/supported_docker_environment/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,12 @@ Testcontainers will try to connect to a Docker daemon using the following strate
* `DOCKER_CERT_PATH=~/.docker`
* If Docker Machine is installed, the docker machine environment for the *first* machine found. Docker Machine needs to be on the PATH for this to succeed.
* If you're going to run your tests inside a container, please read [Patterns for running tests inside a docker container](continuous_integration/dind_patterns.md) first.

## Docker registry authentication

Testcontainers will try to authenticate to registries with supplied config using the following strategies in order:

* Environment variables:
* `DOCKER_AUTH_CONFIG`
* Docker config
* At location specified in `DOCKER_CONFIG` or at `{HOME}/.docker/config.json`