diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/DefaultDockerClientConfig.java b/docker-java-core/src/main/java/com/github/dockerjava/core/DefaultDockerClientConfig.java index 7f17295f3..731051b0d 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/DefaultDockerClientConfig.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/DefaultDockerClientConfig.java @@ -5,6 +5,7 @@ import com.github.dockerjava.api.model.AuthConfigurations; import com.github.dockerjava.core.NameParser.HostnameReposName; import com.github.dockerjava.core.NameParser.ReposTag; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.builder.EqualsBuilder; @@ -37,6 +38,8 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf public static final String DOCKER_HOST = "DOCKER_HOST"; + public static final String DOCKER_CONTEXT = "DOCKER_CONTEXT"; + public static final String DOCKER_TLS_VERIFY = "DOCKER_TLS_VERIFY"; public static final String DOCKER_CONFIG = "DOCKER_CONFIG"; @@ -87,11 +90,13 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf private final RemoteApiVersion apiVersion; - private DockerConfigFile dockerConfig = null; + private final DockerConfigFile dockerConfig; - DefaultDockerClientConfig(URI dockerHost, String dockerConfigPath, String apiVersion, String registryUrl, - String registryUsername, String registryPassword, String registryEmail, SSLConfig sslConfig) { + DefaultDockerClientConfig(URI dockerHost, DockerConfigFile dockerConfigFile, String dockerConfigPath, String apiVersion, + String registryUrl, String registryUsername, String registryPassword, String registryEmail, + SSLConfig sslConfig) { this.dockerHost = checkDockerHostScheme(dockerHost); + this.dockerConfig = dockerConfigFile; this.dockerConfigPath = dockerConfigPath; this.apiVersion = RemoteApiVersion.parseConfigWithDefault(apiVersion); this.sslConfig = sslConfig; @@ -174,6 +179,13 @@ private static Properties overrideDockerPropertiesWithEnv(Properties properties, } } + if (env.containsKey(DOCKER_CONTEXT)) { + String value = env.get(DOCKER_CONTEXT); + if (value != null && value.trim().length() != 0) { + overriddenProperties.setProperty(DOCKER_CONTEXT, value); + } + } + for (Map.Entry envEntry : env.entrySet()) { String envKey = envEntry.getKey(); if (CONFIG_KEYS.contains(envKey)) { @@ -258,13 +270,6 @@ public String getDockerConfigPath() { @Nonnull public DockerConfigFile getDockerConfig() { - if (dockerConfig == null) { - try { - dockerConfig = DockerConfigFile.loadConfig(getObjectMapper(), getDockerConfigPath()); - } catch (IOException e) { - throw new DockerClientException("Failed to parse docker configuration file", e); - } - } return dockerConfig; } @@ -325,7 +330,7 @@ public static class Builder { private URI dockerHost; private String apiVersion, registryUsername, registryPassword, registryEmail, registryUrl, dockerConfig, - dockerCertPath; + dockerCertPath, dockerContext; private Boolean dockerTlsVerify; @@ -343,6 +348,7 @@ public Builder withProperties(Properties p) { } return withDockerTlsVerify(p.getProperty(DOCKER_TLS_VERIFY)) + .withDockerContext(p.getProperty(DOCKER_CONTEXT)) .withDockerConfig(p.getProperty(DOCKER_CONFIG)) .withDockerCertPath(p.getProperty(DOCKER_CERT_PATH)) .withApiVersion(p.getProperty(API_VERSION)) @@ -401,6 +407,11 @@ public final Builder withDockerConfig(String dockerConfig) { return this; } + public final Builder withDockerContext(String dockerContext) { + this.dockerContext = dockerContext; + return this; + } + public final Builder withDockerTlsVerify(String dockerTlsVerify) { if (dockerTlsVerify != null) { String trimmed = dockerTlsVerify.trim(); @@ -443,14 +454,33 @@ public DefaultDockerClientConfig build() { sslConfig = customSslConfig; } + final DockerConfigFile dockerConfigFile = readDockerConfig(); + + final String context = (dockerContext != null) ? dockerContext : dockerConfigFile.getCurrentContext(); URI dockerHostUri = dockerHost != null ? dockerHost - : URI.create(SystemUtils.IS_OS_WINDOWS ? WINDOWS_DEFAULT_DOCKER_HOST : DEFAULT_DOCKER_HOST); + : resolveDockerHost(context); - return new DefaultDockerClientConfig(dockerHostUri, dockerConfig, apiVersion, registryUrl, registryUsername, + return new DefaultDockerClientConfig(dockerHostUri, dockerConfigFile, dockerConfig, apiVersion, registryUrl, registryUsername, registryPassword, registryEmail, sslConfig); } + private DockerConfigFile readDockerConfig() { + try { + return DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), dockerConfig); + } catch (IOException e) { + throw new DockerClientException("Failed to parse docker configuration file", e); + } + } + + private URI resolveDockerHost(String dockerContext) { + return URI.create(Optional.ofNullable(dockerContext) + .flatMap(context -> DockerContextMetaFile.resolveContextMetaFile( + DockerClientConfig.getDefaultObjectMapper(), new File(dockerConfig), context)) + .flatMap(DockerContextMetaFile::host) + .orElse(SystemUtils.IS_OS_WINDOWS ? WINDOWS_DEFAULT_DOCKER_HOST : DEFAULT_DOCKER_HOST)); + } + private String checkDockerCertPath(String dockerCertPath) { if (StringUtils.isEmpty(dockerCertPath)) { throw new DockerClientException( diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/DockerConfigFile.java b/docker-java-core/src/main/java/com/github/dockerjava/core/DockerConfigFile.java index eb5a94e2e..825796a74 100644 --- a/docker-java-core/src/main/java/com/github/dockerjava/core/DockerConfigFile.java +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/DockerConfigFile.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.AuthConfigurations; +import java.util.Objects; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.StringUtils; @@ -29,6 +30,9 @@ public class DockerConfigFile { @JsonProperty private final Map auths; + @JsonProperty + private String currentContext; + public DockerConfigFile() { this(new HashMap<>()); } @@ -46,6 +50,14 @@ void addAuthConfig(AuthConfig config) { auths.put(config.getRegistryAddress(), config); } + void setCurrentContext(String currentContext) { + this.currentContext = currentContext; + } + + public String getCurrentContext() { + return currentContext; + } + @CheckForNull public AuthConfig resolveAuthConfig(@CheckForNull String hostname) { if (StringUtils.isEmpty(hostname) || AuthConfig.DEFAULT_SERVER_ADDRESS.equals(hostname)) { @@ -104,6 +116,9 @@ public boolean equals(Object obj) { return false; } else if (!auths.equals(other.auths)) return false; + if (!Objects.equals(currentContext, other.currentContext)) { + return false; + } return true; } @@ -111,7 +126,7 @@ public boolean equals(Object obj) { @Override public String toString() { - return "DockerConfigFile [auths=" + auths + "]"; + return "DockerConfigFile [auths=" + auths + ", currentContext='" + currentContext + "']"; } @Nonnull diff --git a/docker-java-core/src/main/java/com/github/dockerjava/core/DockerContextMetaFile.java b/docker-java-core/src/main/java/com/github/dockerjava/core/DockerContextMetaFile.java new file mode 100644 index 000000000..a52304c8e --- /dev/null +++ b/docker-java-core/src/main/java/com/github/dockerjava/core/DockerContextMetaFile.java @@ -0,0 +1,66 @@ +package com.github.dockerjava.core; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.hash.HashFunction; +import com.google.common.hash.Hashing; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +public class DockerContextMetaFile { + private static HashFunction metaHashFunction = Hashing.sha256(); + + @JsonProperty("Name") + String name; + + @JsonProperty("Endpoints") + Endpoints endpoints; + + public static class Endpoints { + @JsonProperty("docker") + Docker docker; + + public static class Docker { + @JsonProperty("Host") + String host; + + @JsonProperty("SkipTLSVerify") + boolean skipTLSVerify; + } + } + + public Optional host() { + if (endpoints != null && endpoints.docker != null) { + return Optional.ofNullable(endpoints.docker.host); + } + return Optional.empty(); + } + + public static Optional resolveContextMetaFile(ObjectMapper objectMapper, File dockerConfigPath, String context) { + final File path = dockerConfigPath.toPath() + .resolve("contexts") + .resolve("meta") + .resolve(metaHashFunction.hashString(context, StandardCharsets.UTF_8).toString()) + .resolve("meta.json") + .toFile(); + return Optional.ofNullable(loadContextMetaFile(objectMapper, path)); + } + + public static DockerContextMetaFile loadContextMetaFile(ObjectMapper objectMapper, File dockerContextMetaFile) { + try { + return parseContextMetaFile(objectMapper, dockerContextMetaFile); + } catch (Exception exception) { + return null; + } + } + + public static DockerContextMetaFile parseContextMetaFile(ObjectMapper objectMapper, File dockerContextMetaFile) throws IOException { + try { + return objectMapper.readValue(dockerContextMetaFile, DockerContextMetaFile.class); + } catch (IOException e) { + throw new IOException("Failed to parse docker context meta file " + dockerContextMetaFile, e); + } + } +} diff --git a/docker-java/src/test/java/com/github/dockerjava/core/DefaultDockerClientConfigTest.java b/docker-java/src/test/java/com/github/dockerjava/core/DefaultDockerClientConfigTest.java index 02b7e34ae..d5b751145 100644 --- a/docker-java/src/test/java/com/github/dockerjava/core/DefaultDockerClientConfigTest.java +++ b/docker-java/src/test/java/com/github/dockerjava/core/DefaultDockerClientConfigTest.java @@ -1,9 +1,9 @@ package com.github.dockerjava.core; -import com.github.dockerjava.api.exception.DockerClientException; import com.github.dockerjava.api.model.AuthConfig; import com.github.dockerjava.api.model.AuthConfigurations; import com.google.common.io.Resources; +import java.io.IOException; import org.apache.commons.lang3.SerializationUtils; import org.junit.Test; @@ -27,13 +27,26 @@ public class DefaultDockerClientConfigTest { public static final DefaultDockerClientConfig EXAMPLE_CONFIG = newExampleConfig(); + public static final DefaultDockerClientConfig EXAMPLE_CONFIG_FULLY_LOADED = newExampleConfigFullyLoaded(); private static DefaultDockerClientConfig newExampleConfig() { - String dockerCertPath = dockerCertPath(); + return new DefaultDockerClientConfig(URI.create("tcp://foo"), null, "dockerConfig", "apiVersion", "registryUrl", + "registryUsername", "registryPassword", "registryEmail", + new LocalDirectorySSLConfig(dockerCertPath)); + } - return new DefaultDockerClientConfig(URI.create("tcp://foo"), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", + private static DefaultDockerClientConfig newExampleConfigFullyLoaded() { + try { + String dockerCertPath = dockerCertPath(); + String dockerConfig = "dockerConfig"; + DockerConfigFile loadedConfigFile = DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), dockerConfig); + return new DefaultDockerClientConfig(URI.create("tcp://foo"), loadedConfigFile, dockerConfig, "apiVersion", "registryUrl", + "registryUsername", "registryPassword", "registryEmail", new LocalDirectorySSLConfig(dockerCertPath)); + } catch (IOException exception) { + throw new RuntimeException(exception); + } } private static String homeDir() { @@ -69,6 +82,37 @@ public void environmentDockerHost() throws Exception { assertEquals(config.getDockerHost(), URI.create("tcp://baz:8768")); } + @Test + public void dockerContextFromConfig() throws Exception { + // given home directory with docker contexts configured + Properties systemProperties = new Properties(); + systemProperties.setProperty("user.home", "target/test-classes/dockerContextHomeDir"); + + // and an empty environment + Map env = new HashMap<>(); + + // when you build a config + DefaultDockerClientConfig config = buildConfig(env, systemProperties); + + assertEquals(URI.create("unix:///configcontext.sock"), config.getDockerHost()); + } + + @Test + public void dockerContextFromEnvironmentVariable() throws Exception { + // given home directory with docker contexts + Properties systemProperties = new Properties(); + systemProperties.setProperty("user.home", "target/test-classes/dockerContextHomeDir"); + + // and an environment variable that overrides docker context + Map env = new HashMap<>(); + env.put(DefaultDockerClientConfig.DOCKER_CONTEXT, "envvarcontext"); + + // when you build a config + DefaultDockerClientConfig config = buildConfig(env, systemProperties); + + assertEquals(URI.create("unix:///envvarcontext.sock"), config.getDockerHost()); + } + @Test public void environment() throws Exception { @@ -88,7 +132,7 @@ public void environment() throws Exception { DefaultDockerClientConfig config = buildConfig(env, new Properties()); // then we get the example object - assertEquals(config, EXAMPLE_CONFIG); + assertEquals(EXAMPLE_CONFIG_FULLY_LOADED, config); } @Test @@ -147,7 +191,7 @@ public void systemProperties() throws Exception { DefaultDockerClientConfig config = buildConfig(Collections. emptyMap(), systemProperties); // then it is the same as the example - assertEquals(config, EXAMPLE_CONFIG); + assertEquals(EXAMPLE_CONFIG_FULLY_LOADED, config); } @@ -161,7 +205,7 @@ public void serializableTest() { @Test() public void testSslContextEmpty() throws Exception { - new DefaultDockerClientConfig(URI.create("tcp://foo"), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", + new DefaultDockerClientConfig(URI.create("tcp://foo"), new DockerConfigFile(), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", null); } @@ -169,14 +213,14 @@ public void testSslContextEmpty() throws Exception { @Test() public void testTlsVerifyAndCertPath() throws Exception { - new DefaultDockerClientConfig(URI.create("tcp://foo"), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", + new DefaultDockerClientConfig(URI.create("tcp://foo"), new DockerConfigFile(), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", new LocalDirectorySSLConfig(dockerCertPath())); } @Test() public void testAnyHostScheme() throws Exception { URI dockerHost = URI.create("a" + UUID.randomUUID().toString().replace("-", "") + "://foo"); - new DefaultDockerClientConfig(dockerHost, "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", + new DefaultDockerClientConfig(dockerHost, new DockerConfigFile(), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", null); } @@ -249,10 +293,12 @@ public void dockerHostSetExplicitlyIfSetToDefaultByUser() { @Test - public void testGetAuthConfigurationsFromDockerCfg() throws URISyntaxException { + public void testGetAuthConfigurationsFromDockerCfg() throws URISyntaxException, IOException { File cfgFile = new File(Resources.getResource("com.github.dockerjava.core/registry.v1").toURI()); + DockerConfigFile dockerConfigFile = + DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), cfgFile.getAbsolutePath()); DefaultDockerClientConfig clientConfig = new DefaultDockerClientConfig(URI.create( - "unix://foo"), cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword", + "unix://foo"), dockerConfigFile, cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", null); AuthConfigurations authConfigurations = clientConfig.getAuthConfigurations(); @@ -265,10 +311,12 @@ public void testGetAuthConfigurationsFromDockerCfg() throws URISyntaxException { } @Test - public void testGetAuthConfigurationsFromConfigJson() throws URISyntaxException { + public void testGetAuthConfigurationsFromConfigJson() throws URISyntaxException, IOException { File cfgFile = new File(Resources.getResource("com.github.dockerjava.core/registry.v2").toURI()); + DockerConfigFile dockerConfigFile = + DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), cfgFile.getAbsolutePath()); DefaultDockerClientConfig clientConfig = new DefaultDockerClientConfig(URI.create( - "unix://foo"), cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword", + "unix://foo"), dockerConfigFile, cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail", null); AuthConfigurations authConfigurations = clientConfig.getAuthConfigurations(); diff --git a/docker-java/src/test/java/com/github/dockerjava/core/DockerClientBuilderTest.java b/docker-java/src/test/java/com/github/dockerjava/core/DockerClientBuilderTest.java index be0bfda8a..43e700fd8 100644 --- a/docker-java/src/test/java/com/github/dockerjava/core/DockerClientBuilderTest.java +++ b/docker-java/src/test/java/com/github/dockerjava/core/DockerClientBuilderTest.java @@ -1,7 +1,12 @@ package com.github.dockerjava.core; +import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.DockerCmdExecFactory; +import com.github.dockerjava.api.model.PushResponseItem; +import com.github.dockerjava.httpclient5.ApacheDockerHttpClient; +import com.github.dockerjava.transport.DockerHttpClient; +import java.time.Duration; import org.junit.Test; import java.util.ArrayList; diff --git a/docker-java/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java b/docker-java/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java index 08f658d52..8ff17857f 100644 --- a/docker-java/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java +++ b/docker-java/src/test/java/com/github/dockerjava/core/DockerClientImplTest.java @@ -12,7 +12,8 @@ public class DockerClientImplTest { @Test public void configuredInstanceAuthConfig() throws Exception { // given a config with null serverAddress - DefaultDockerClientConfig dockerClientConfig = new DefaultDockerClientConfig(URI.create("tcp://foo"), null, null, null, "", "", "", null); + DefaultDockerClientConfig dockerClientConfig = new DefaultDockerClientConfig(URI.create("tcp://foo"), + new DockerConfigFile(), null, null, null, "", "", "", null); DockerClientImpl dockerClient = DockerClientImpl.getInstance(dockerClientConfig); // when we get the auth config diff --git a/docker-java/src/test/java/com/github/dockerjava/core/DockerConfigFileTest.java b/docker-java/src/test/java/com/github/dockerjava/core/DockerConfigFileTest.java index 83bc124e9..ce1a59cc0 100644 --- a/docker-java/src/test/java/com/github/dockerjava/core/DockerConfigFileTest.java +++ b/docker-java/src/test/java/com/github/dockerjava/core/DockerConfigFileTest.java @@ -150,6 +150,14 @@ public void validDockerConfig() throws IOException { assertThat(runTest("validDockerConfig"), is(expected)); } + @Test + public void validDockerConfigWithCurrentContext() throws IOException { + DockerConfigFile expected = new DockerConfigFile(); + expected.setCurrentContext("expectedContext"); + + assertThat(runTest("validDockerConfigWithCurrentContext"), is(expected)); + } + @Test public void nonExistent() throws IOException { DockerConfigFile expected = new DockerConfigFile(); diff --git a/docker-java/src/test/resources/dockerContextHomeDir/.docker/config.json b/docker-java/src/test/resources/dockerContextHomeDir/.docker/config.json new file mode 100644 index 000000000..5bb2ebebe --- /dev/null +++ b/docker-java/src/test/resources/dockerContextHomeDir/.docker/config.json @@ -0,0 +1,4 @@ +{ + "auths": {}, + "currentContext": "configcontext" +} diff --git a/docker-java/src/test/resources/dockerContextHomeDir/.docker/contexts/meta/51699a7c75211315f1dbf6ecc40dfb0ffdd4ee11ecb2ce7853c9751aea1f9444/meta.json b/docker-java/src/test/resources/dockerContextHomeDir/.docker/contexts/meta/51699a7c75211315f1dbf6ecc40dfb0ffdd4ee11ecb2ce7853c9751aea1f9444/meta.json new file mode 100644 index 000000000..c6456d6b8 --- /dev/null +++ b/docker-java/src/test/resources/dockerContextHomeDir/.docker/contexts/meta/51699a7c75211315f1dbf6ecc40dfb0ffdd4ee11ecb2ce7853c9751aea1f9444/meta.json @@ -0,0 +1,12 @@ +{ + "Name": "envvarcontext", + "Metadata": { + "Description": "envvarcontext" + }, + "Endpoints": { + "docker": { + "Host": "unix:///envvarcontext.sock", + "SkipTLSVerify": false + } + } +} diff --git a/docker-java/src/test/resources/dockerContextHomeDir/.docker/contexts/meta/d090e08f0c9167acd72adef6d9fa07ec2de3a873cdd545dd8cb7fc7a10a1331a/meta.json b/docker-java/src/test/resources/dockerContextHomeDir/.docker/contexts/meta/d090e08f0c9167acd72adef6d9fa07ec2de3a873cdd545dd8cb7fc7a10a1331a/meta.json new file mode 100644 index 000000000..adff3b1c9 --- /dev/null +++ b/docker-java/src/test/resources/dockerContextHomeDir/.docker/contexts/meta/d090e08f0c9167acd72adef6d9fa07ec2de3a873cdd545dd8cb7fc7a10a1331a/meta.json @@ -0,0 +1,12 @@ +{ + "Name": "configcontext", + "Metadata": { + "Description": "configcontext" + }, + "Endpoints": { + "docker": { + "Host": "unix:///configcontext.sock", + "SkipTLSVerify": false + } + } +} diff --git a/docker-java/src/test/resources/someHomeDir/.docker/config.json b/docker-java/src/test/resources/someHomeDir/.docker/config.json index 630394039..02ed0cf7f 100644 --- a/docker-java/src/test/resources/someHomeDir/.docker/config.json +++ b/docker-java/src/test/resources/someHomeDir/.docker/config.json @@ -1,9 +1,9 @@ { "auths":{ "https://index.docker.io/v1/":{ - "auth":"XXXX=", + "auth":"dXNlcm5hbWU6cGFzc3dvcmQ=", "email":"foo.bar@test.com" } } -} \ No newline at end of file +} diff --git a/docker-java/src/test/resources/testAuthConfigFile/validDockerConfigWithCurrentContext/config.json b/docker-java/src/test/resources/testAuthConfigFile/validDockerConfigWithCurrentContext/config.json new file mode 100644 index 000000000..8c5963f87 --- /dev/null +++ b/docker-java/src/test/resources/testAuthConfigFile/validDockerConfigWithCurrentContext/config.json @@ -0,0 +1,4 @@ +{ + "auths": {}, + "currentContext": "expectedContext" +}