Skip to content

Commit f8fd492

Browse files
Support docker context (#2036)
Resolve Docker Client Config by reading `DOCKER_CONTEXT` env var or `currentContext` from `~/.docker/config.json`. Co-authored-by: Eddú Meléndez Gonzales <eddu.melendez@gmail.com>
1 parent db508d8 commit f8fd492

File tree

12 files changed

+234
-29
lines changed

12 files changed

+234
-29
lines changed

docker-java-core/src/main/java/com/github/dockerjava/core/DefaultDockerClientConfig.java

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.github.dockerjava.api.model.AuthConfigurations;
66
import com.github.dockerjava.core.NameParser.HostnameReposName;
77
import com.github.dockerjava.core.NameParser.ReposTag;
8+
import java.util.Optional;
89
import org.apache.commons.lang3.StringUtils;
910
import org.apache.commons.lang3.SystemUtils;
1011
import org.apache.commons.lang3.builder.EqualsBuilder;
@@ -37,6 +38,8 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf
3738

3839
public static final String DOCKER_HOST = "DOCKER_HOST";
3940

41+
public static final String DOCKER_CONTEXT = "DOCKER_CONTEXT";
42+
4043
public static final String DOCKER_TLS_VERIFY = "DOCKER_TLS_VERIFY";
4144

4245
public static final String DOCKER_CONFIG = "DOCKER_CONFIG";
@@ -87,11 +90,13 @@ public class DefaultDockerClientConfig implements Serializable, DockerClientConf
8790

8891
private final RemoteApiVersion apiVersion;
8992

90-
private DockerConfigFile dockerConfig = null;
93+
private final DockerConfigFile dockerConfig;
9194

92-
DefaultDockerClientConfig(URI dockerHost, String dockerConfigPath, String apiVersion, String registryUrl,
93-
String registryUsername, String registryPassword, String registryEmail, SSLConfig sslConfig) {
95+
DefaultDockerClientConfig(URI dockerHost, DockerConfigFile dockerConfigFile, String dockerConfigPath, String apiVersion,
96+
String registryUrl, String registryUsername, String registryPassword, String registryEmail,
97+
SSLConfig sslConfig) {
9498
this.dockerHost = checkDockerHostScheme(dockerHost);
99+
this.dockerConfig = dockerConfigFile;
95100
this.dockerConfigPath = dockerConfigPath;
96101
this.apiVersion = RemoteApiVersion.parseConfigWithDefault(apiVersion);
97102
this.sslConfig = sslConfig;
@@ -174,6 +179,13 @@ private static Properties overrideDockerPropertiesWithEnv(Properties properties,
174179
}
175180
}
176181

182+
if (env.containsKey(DOCKER_CONTEXT)) {
183+
String value = env.get(DOCKER_CONTEXT);
184+
if (value != null && value.trim().length() != 0) {
185+
overriddenProperties.setProperty(DOCKER_CONTEXT, value);
186+
}
187+
}
188+
177189
for (Map.Entry<String, String> envEntry : env.entrySet()) {
178190
String envKey = envEntry.getKey();
179191
if (CONFIG_KEYS.contains(envKey)) {
@@ -258,13 +270,6 @@ public String getDockerConfigPath() {
258270

259271
@Nonnull
260272
public DockerConfigFile getDockerConfig() {
261-
if (dockerConfig == null) {
262-
try {
263-
dockerConfig = DockerConfigFile.loadConfig(getObjectMapper(), getDockerConfigPath());
264-
} catch (IOException e) {
265-
throw new DockerClientException("Failed to parse docker configuration file", e);
266-
}
267-
}
268273
return dockerConfig;
269274
}
270275

@@ -325,7 +330,7 @@ public static class Builder {
325330
private URI dockerHost;
326331

327332
private String apiVersion, registryUsername, registryPassword, registryEmail, registryUrl, dockerConfig,
328-
dockerCertPath;
333+
dockerCertPath, dockerContext;
329334

330335
private Boolean dockerTlsVerify;
331336

@@ -343,6 +348,7 @@ public Builder withProperties(Properties p) {
343348
}
344349

345350
return withDockerTlsVerify(p.getProperty(DOCKER_TLS_VERIFY))
351+
.withDockerContext(p.getProperty(DOCKER_CONTEXT))
346352
.withDockerConfig(p.getProperty(DOCKER_CONFIG))
347353
.withDockerCertPath(p.getProperty(DOCKER_CERT_PATH))
348354
.withApiVersion(p.getProperty(API_VERSION))
@@ -401,6 +407,11 @@ public final Builder withDockerConfig(String dockerConfig) {
401407
return this;
402408
}
403409

410+
public final Builder withDockerContext(String dockerContext) {
411+
this.dockerContext = dockerContext;
412+
return this;
413+
}
414+
404415
public final Builder withDockerTlsVerify(String dockerTlsVerify) {
405416
if (dockerTlsVerify != null) {
406417
String trimmed = dockerTlsVerify.trim();
@@ -443,14 +454,33 @@ public DefaultDockerClientConfig build() {
443454
sslConfig = customSslConfig;
444455
}
445456

457+
final DockerConfigFile dockerConfigFile = readDockerConfig();
458+
459+
final String context = (dockerContext != null) ? dockerContext : dockerConfigFile.getCurrentContext();
446460
URI dockerHostUri = dockerHost != null
447461
? dockerHost
448-
: URI.create(SystemUtils.IS_OS_WINDOWS ? WINDOWS_DEFAULT_DOCKER_HOST : DEFAULT_DOCKER_HOST);
462+
: resolveDockerHost(context);
449463

450-
return new DefaultDockerClientConfig(dockerHostUri, dockerConfig, apiVersion, registryUrl, registryUsername,
464+
return new DefaultDockerClientConfig(dockerHostUri, dockerConfigFile, dockerConfig, apiVersion, registryUrl, registryUsername,
451465
registryPassword, registryEmail, sslConfig);
452466
}
453467

468+
private DockerConfigFile readDockerConfig() {
469+
try {
470+
return DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), dockerConfig);
471+
} catch (IOException e) {
472+
throw new DockerClientException("Failed to parse docker configuration file", e);
473+
}
474+
}
475+
476+
private URI resolveDockerHost(String dockerContext) {
477+
return URI.create(Optional.ofNullable(dockerContext)
478+
.flatMap(context -> DockerContextMetaFile.resolveContextMetaFile(
479+
DockerClientConfig.getDefaultObjectMapper(), new File(dockerConfig), context))
480+
.flatMap(DockerContextMetaFile::host)
481+
.orElse(SystemUtils.IS_OS_WINDOWS ? WINDOWS_DEFAULT_DOCKER_HOST : DEFAULT_DOCKER_HOST));
482+
}
483+
454484
private String checkDockerCertPath(String dockerCertPath) {
455485
if (StringUtils.isEmpty(dockerCertPath)) {
456486
throw new DockerClientException(

docker-java-core/src/main/java/com/github/dockerjava/core/DockerConfigFile.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.fasterxml.jackson.databind.ObjectMapper;
66
import com.github.dockerjava.api.model.AuthConfig;
77
import com.github.dockerjava.api.model.AuthConfigurations;
8+
import java.util.Objects;
89
import org.apache.commons.io.FileUtils;
910
import org.apache.commons.lang3.StringUtils;
1011

@@ -29,6 +30,9 @@ public class DockerConfigFile {
2930
@JsonProperty
3031
private final Map<String, AuthConfig> auths;
3132

33+
@JsonProperty
34+
private String currentContext;
35+
3236
public DockerConfigFile() {
3337
this(new HashMap<>());
3438
}
@@ -46,6 +50,14 @@ void addAuthConfig(AuthConfig config) {
4650
auths.put(config.getRegistryAddress(), config);
4751
}
4852

53+
void setCurrentContext(String currentContext) {
54+
this.currentContext = currentContext;
55+
}
56+
57+
public String getCurrentContext() {
58+
return currentContext;
59+
}
60+
4961
@CheckForNull
5062
public AuthConfig resolveAuthConfig(@CheckForNull String hostname) {
5163
if (StringUtils.isEmpty(hostname) || AuthConfig.DEFAULT_SERVER_ADDRESS.equals(hostname)) {
@@ -104,14 +116,17 @@ public boolean equals(Object obj) {
104116
return false;
105117
} else if (!auths.equals(other.auths))
106118
return false;
119+
if (!Objects.equals(currentContext, other.currentContext)) {
120+
return false;
121+
}
107122
return true;
108123
}
109124

110125
// CHECKSTYLE:ON
111126

112127
@Override
113128
public String toString() {
114-
return "DockerConfigFile [auths=" + auths + "]";
129+
return "DockerConfigFile [auths=" + auths + ", currentContext='" + currentContext + "']";
115130
}
116131

117132
@Nonnull
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.github.dockerjava.core;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.google.common.hash.HashFunction;
6+
import com.google.common.hash.Hashing;
7+
import java.io.File;
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
import java.util.Optional;
11+
12+
public class DockerContextMetaFile {
13+
private static HashFunction metaHashFunction = Hashing.sha256();
14+
15+
@JsonProperty("Name")
16+
String name;
17+
18+
@JsonProperty("Endpoints")
19+
Endpoints endpoints;
20+
21+
public static class Endpoints {
22+
@JsonProperty("docker")
23+
Docker docker;
24+
25+
public static class Docker {
26+
@JsonProperty("Host")
27+
String host;
28+
29+
@JsonProperty("SkipTLSVerify")
30+
boolean skipTLSVerify;
31+
}
32+
}
33+
34+
public Optional<String> host() {
35+
if (endpoints != null && endpoints.docker != null) {
36+
return Optional.ofNullable(endpoints.docker.host);
37+
}
38+
return Optional.empty();
39+
}
40+
41+
public static Optional<DockerContextMetaFile> resolveContextMetaFile(ObjectMapper objectMapper, File dockerConfigPath, String context) {
42+
final File path = dockerConfigPath.toPath()
43+
.resolve("contexts")
44+
.resolve("meta")
45+
.resolve(metaHashFunction.hashString(context, StandardCharsets.UTF_8).toString())
46+
.resolve("meta.json")
47+
.toFile();
48+
return Optional.ofNullable(loadContextMetaFile(objectMapper, path));
49+
}
50+
51+
public static DockerContextMetaFile loadContextMetaFile(ObjectMapper objectMapper, File dockerContextMetaFile) {
52+
try {
53+
return parseContextMetaFile(objectMapper, dockerContextMetaFile);
54+
} catch (Exception exception) {
55+
return null;
56+
}
57+
}
58+
59+
public static DockerContextMetaFile parseContextMetaFile(ObjectMapper objectMapper, File dockerContextMetaFile) throws IOException {
60+
try {
61+
return objectMapper.readValue(dockerContextMetaFile, DockerContextMetaFile.class);
62+
} catch (IOException e) {
63+
throw new IOException("Failed to parse docker context meta file " + dockerContextMetaFile, e);
64+
}
65+
}
66+
}

docker-java/src/test/java/com/github/dockerjava/core/DefaultDockerClientConfigTest.java

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.github.dockerjava.core;
22

3-
import com.github.dockerjava.api.exception.DockerClientException;
43
import com.github.dockerjava.api.model.AuthConfig;
54
import com.github.dockerjava.api.model.AuthConfigurations;
65
import com.google.common.io.Resources;
6+
import java.io.IOException;
77
import org.apache.commons.lang3.SerializationUtils;
88
import org.junit.Test;
99

@@ -27,13 +27,26 @@
2727
public class DefaultDockerClientConfigTest {
2828

2929
public static final DefaultDockerClientConfig EXAMPLE_CONFIG = newExampleConfig();
30+
public static final DefaultDockerClientConfig EXAMPLE_CONFIG_FULLY_LOADED = newExampleConfigFullyLoaded();
3031

3132
private static DefaultDockerClientConfig newExampleConfig() {
32-
3333
String dockerCertPath = dockerCertPath();
34+
return new DefaultDockerClientConfig(URI.create("tcp://foo"), null, "dockerConfig", "apiVersion", "registryUrl",
35+
"registryUsername", "registryPassword", "registryEmail",
36+
new LocalDirectorySSLConfig(dockerCertPath));
37+
}
3438

35-
return new DefaultDockerClientConfig(URI.create("tcp://foo"), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail",
39+
private static DefaultDockerClientConfig newExampleConfigFullyLoaded() {
40+
try {
41+
String dockerCertPath = dockerCertPath();
42+
String dockerConfig = "dockerConfig";
43+
DockerConfigFile loadedConfigFile = DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), dockerConfig);
44+
return new DefaultDockerClientConfig(URI.create("tcp://foo"), loadedConfigFile, dockerConfig, "apiVersion", "registryUrl",
45+
"registryUsername", "registryPassword", "registryEmail",
3646
new LocalDirectorySSLConfig(dockerCertPath));
47+
} catch (IOException exception) {
48+
throw new RuntimeException(exception);
49+
}
3750
}
3851

3952
private static String homeDir() {
@@ -69,6 +82,37 @@ public void environmentDockerHost() throws Exception {
6982
assertEquals(config.getDockerHost(), URI.create("tcp://baz:8768"));
7083
}
7184

85+
@Test
86+
public void dockerContextFromConfig() throws Exception {
87+
// given home directory with docker contexts configured
88+
Properties systemProperties = new Properties();
89+
systemProperties.setProperty("user.home", "target/test-classes/dockerContextHomeDir");
90+
91+
// and an empty environment
92+
Map<String, String> env = new HashMap<>();
93+
94+
// when you build a config
95+
DefaultDockerClientConfig config = buildConfig(env, systemProperties);
96+
97+
assertEquals(URI.create("unix:///configcontext.sock"), config.getDockerHost());
98+
}
99+
100+
@Test
101+
public void dockerContextFromEnvironmentVariable() throws Exception {
102+
// given home directory with docker contexts
103+
Properties systemProperties = new Properties();
104+
systemProperties.setProperty("user.home", "target/test-classes/dockerContextHomeDir");
105+
106+
// and an environment variable that overrides docker context
107+
Map<String, String> env = new HashMap<>();
108+
env.put(DefaultDockerClientConfig.DOCKER_CONTEXT, "envvarcontext");
109+
110+
// when you build a config
111+
DefaultDockerClientConfig config = buildConfig(env, systemProperties);
112+
113+
assertEquals(URI.create("unix:///envvarcontext.sock"), config.getDockerHost());
114+
}
115+
72116
@Test
73117
public void environment() throws Exception {
74118

@@ -88,7 +132,7 @@ public void environment() throws Exception {
88132
DefaultDockerClientConfig config = buildConfig(env, new Properties());
89133

90134
// then we get the example object
91-
assertEquals(config, EXAMPLE_CONFIG);
135+
assertEquals(EXAMPLE_CONFIG_FULLY_LOADED, config);
92136
}
93137

94138
@Test
@@ -147,7 +191,7 @@ public void systemProperties() throws Exception {
147191
DefaultDockerClientConfig config = buildConfig(Collections.<String, String> emptyMap(), systemProperties);
148192

149193
// then it is the same as the example
150-
assertEquals(config, EXAMPLE_CONFIG);
194+
assertEquals(EXAMPLE_CONFIG_FULLY_LOADED, config);
151195

152196
}
153197

@@ -161,22 +205,22 @@ public void serializableTest() {
161205

162206
@Test()
163207
public void testSslContextEmpty() throws Exception {
164-
new DefaultDockerClientConfig(URI.create("tcp://foo"), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail",
208+
new DefaultDockerClientConfig(URI.create("tcp://foo"), new DockerConfigFile(), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail",
165209
null);
166210
}
167211

168212

169213

170214
@Test()
171215
public void testTlsVerifyAndCertPath() throws Exception {
172-
new DefaultDockerClientConfig(URI.create("tcp://foo"), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail",
216+
new DefaultDockerClientConfig(URI.create("tcp://foo"), new DockerConfigFile(), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail",
173217
new LocalDirectorySSLConfig(dockerCertPath()));
174218
}
175219

176220
@Test()
177221
public void testAnyHostScheme() throws Exception {
178222
URI dockerHost = URI.create("a" + UUID.randomUUID().toString().replace("-", "") + "://foo");
179-
new DefaultDockerClientConfig(dockerHost, "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail",
223+
new DefaultDockerClientConfig(dockerHost, new DockerConfigFile(), "dockerConfig", "apiVersion", "registryUrl", "registryUsername", "registryPassword", "registryEmail",
180224
null);
181225
}
182226

@@ -249,10 +293,12 @@ public void dockerHostSetExplicitlyIfSetToDefaultByUser() {
249293

250294

251295
@Test
252-
public void testGetAuthConfigurationsFromDockerCfg() throws URISyntaxException {
296+
public void testGetAuthConfigurationsFromDockerCfg() throws URISyntaxException, IOException {
253297
File cfgFile = new File(Resources.getResource("com.github.dockerjava.core/registry.v1").toURI());
298+
DockerConfigFile dockerConfigFile =
299+
DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), cfgFile.getAbsolutePath());
254300
DefaultDockerClientConfig clientConfig = new DefaultDockerClientConfig(URI.create(
255-
"unix://foo"), cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword",
301+
"unix://foo"), dockerConfigFile, cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword",
256302
"registryEmail", null);
257303

258304
AuthConfigurations authConfigurations = clientConfig.getAuthConfigurations();
@@ -265,10 +311,12 @@ public void testGetAuthConfigurationsFromDockerCfg() throws URISyntaxException {
265311
}
266312

267313
@Test
268-
public void testGetAuthConfigurationsFromConfigJson() throws URISyntaxException {
314+
public void testGetAuthConfigurationsFromConfigJson() throws URISyntaxException, IOException {
269315
File cfgFile = new File(Resources.getResource("com.github.dockerjava.core/registry.v2").toURI());
316+
DockerConfigFile dockerConfigFile =
317+
DockerConfigFile.loadConfig(DockerClientConfig.getDefaultObjectMapper(), cfgFile.getAbsolutePath());
270318
DefaultDockerClientConfig clientConfig = new DefaultDockerClientConfig(URI.create(
271-
"unix://foo"), cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword",
319+
"unix://foo"), dockerConfigFile, cfgFile.getAbsolutePath(), "apiVersion", "registryUrl", "registryUsername", "registryPassword",
272320
"registryEmail", null);
273321

274322
AuthConfigurations authConfigurations = clientConfig.getAuthConfigurations();

0 commit comments

Comments
 (0)