Skip to content

Commit

Permalink
Support DOCKER_AUTH_CONFIG env var (#6238)
Browse files Browse the repository at this point in the history
Co-authored-by: Eddú Meléndez <eddu.melendez@gmail.com>
  • Loading branch information
roseo1 and eddumelendez committed Feb 18, 2023
1 parent 5b738c2 commit f591ac7
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 26 deletions.
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
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 {
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
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`

0 comments on commit f591ac7

Please sign in to comment.