Skip to content

Commit d127fd7

Browse files
authored
Add support for docker hub private registry credentials (#845)
* Fix authentication for use of base64-encoded credentials and credential helpers with private registries and docker hub * Update following review by @bsideup * Update to docker-java 3.1.0-rc-4 * fix shading
1 parent f589e60 commit d127fd7

16 files changed

+332
-63
lines changed

core/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ shadowJar {
3535
'META-INF/services/org.glassfish.hk2.extension.*',
3636
'META-INF/services/org.jvnet.hk2.external.generator.*',
3737
'META-INF/services/org.glassfish.jersey.internal.spi.*',
38+
'META-INF/services/java.security.Provider',
3839
'mozilla/public-suffix-list.txt',
3940
].each { exclude(it) }
4041

@@ -95,7 +96,7 @@ dependencies {
9596
exclude(group: "net.java.dev.jna")
9697
}
9798

98-
shaded ('com.github.docker-java:docker-java:3.1.0-rc-3') {
99+
shaded ('com.github.docker-java:docker-java:3.1.0-rc-4') {
99100
exclude(group: 'org.glassfish.jersey.core')
100101
exclude(group: 'org.glassfish.jersey.connectors')
101102
exclude(group: 'log4j')

core/src/jarFileTest/java/org/testcontainers/JarFileShadingTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ public void testMetaInf() throws Exception {
4242
);
4343

4444
assertThatFileList(root.resolve("META-INF").resolve("native")).containsOnly(
45-
"liborg-testcontainers-shaded-netty-transport-native-epoll.so",
46-
"liborg-testcontainers-shaded-netty-transport-native-kqueue.jnilib"
45+
"liborg-testcontainers-shaded-netty_transport_native_epoll_x86_64.so",
46+
"liborg-testcontainers-shaded-netty_transport_native_kqueue_x86_64.jnilib"
4747
);
4848
}
4949

core/src/main/java/org/testcontainers/dockerclient/DockerClientProviderStrategy.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.rnorth.ducttape.unreliables.Unreliables;
1313
import org.slf4j.Logger;
1414
import org.slf4j.LoggerFactory;
15+
import org.testcontainers.dockerclient.auth.AuthDelegatingDockerClientConfig;
1516
import org.testcontainers.dockerclient.transport.TestcontainersDockerCmdExecFactory;
1617
import org.testcontainers.dockerclient.transport.okhttp.OkHttpDockerCmdExecFactory;
1718
import org.testcontainers.utility.TestcontainersConfiguration;
@@ -39,6 +40,8 @@ public abstract class DockerClientProviderStrategy {
3940

4041
private static final AtomicBoolean FAIL_FAST_ALWAYS = new AtomicBoolean(false);
4142

43+
protected static final Logger LOGGER = LoggerFactory.getLogger(DockerClientProviderStrategy.class);
44+
4245
/**
4346
* @throws InvalidConfigurationException if this strategy fails
4447
*/
@@ -64,8 +67,6 @@ protected int getPriority() {
6467
return 0;
6568
}
6669

67-
protected static final Logger LOGGER = LoggerFactory.getLogger(DockerClientProviderStrategy.class);
68-
6970
/**
7071
* Determine the right DockerClientConfig to use for building clients by trial-and-error.
7172
*
@@ -166,7 +167,7 @@ public DockerClient getClient() {
166167

167168
protected DockerClient getClientForConfig(DockerClientConfig config) {
168169
DockerClientBuilder clientBuilder = DockerClientBuilder
169-
.getInstance(config);
170+
.getInstance(new AuthDelegatingDockerClientConfig(config));
170171

171172
String transportType = TestcontainersConfiguration.getInstance().getTransportType();
172173
if ("okhttp".equals(transportType)) {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.testcontainers.dockerclient.auth;
2+
3+
import com.github.dockerjava.api.model.AuthConfig;
4+
import com.github.dockerjava.core.DockerClientConfig;
5+
import lombok.experimental.Delegate;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.testcontainers.utility.DockerImageName;
8+
import org.testcontainers.utility.RegistryAuthLocator;
9+
10+
import static org.testcontainers.utility.AuthConfigUtil.toSafeString;
11+
12+
/**
13+
* Facade implementation for {@link DockerClientConfig} which overrides how authentication
14+
* configuration is obtained. A delegate {@link DockerClientConfig} will be called first
15+
* to try and obtain auth credentials, but after that {@link RegistryAuthLocator} will be
16+
* used to try and improve the auth resolution (e.g. using credential helpers).
17+
*/
18+
@Slf4j
19+
public class AuthDelegatingDockerClientConfig implements DockerClientConfig {
20+
21+
@Delegate(excludes = DelegateExclusions.class)
22+
private DockerClientConfig delegate;
23+
24+
public AuthDelegatingDockerClientConfig(DockerClientConfig delegate) {
25+
this.delegate = delegate;
26+
}
27+
28+
public AuthConfig effectiveAuthConfig(String imageName) {
29+
// allow docker-java auth config to be used as a fallback
30+
AuthConfig fallbackAuthConfig;
31+
try {
32+
fallbackAuthConfig = delegate.effectiveAuthConfig(imageName);
33+
} catch (Exception e) {
34+
log.debug("Delegate call to effectiveAuthConfig failed with cause: '{}'. " +
35+
"Resolution of auth config will continue using RegistryAuthLocator.",
36+
e.getMessage());
37+
fallbackAuthConfig = new AuthConfig();
38+
}
39+
40+
// try and obtain more accurate auth config using our resolution
41+
final DockerImageName parsed = new DockerImageName(imageName);
42+
final AuthConfig effectiveAuthConfig = RegistryAuthLocator.instance()
43+
.lookupAuthConfig(parsed, fallbackAuthConfig);
44+
45+
log.debug("Effective auth config [{}]", toSafeString(effectiveAuthConfig));
46+
return effectiveAuthConfig;
47+
}
48+
49+
private interface DelegateExclusions {
50+
AuthConfig effectiveAuthConfig(String imageName);
51+
}
52+
}

core/src/main/java/org/testcontainers/dockerclient/transport/TestcontainersDockerCmdExecFactory.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,16 +80,21 @@ public void init(DockerClientConfig dockerClientConfig) {
8080
bootstrap = new Bootstrap();
8181

8282
String scheme = dockerClientConfig.getDockerHost().getScheme();
83+
String host = "";
8384

8485
if ("unix".equals(scheme)) {
8586
nettyInitializer = new UnixDomainSocketInitializer();
87+
host = "DUMMY";
8688
} else if ("tcp".equals(scheme)) {
8789
nettyInitializer = new InetSocketInitializer();
90+
host = dockerClientConfig.getDockerHost().getHost() + ":"
91+
+ Integer.toString(dockerClientConfig.getDockerHost().getPort());
8892
}
8993

9094
eventLoopGroup = nettyInitializer.init(bootstrap, dockerClientConfig);
9195

92-
baseResource = new NettyWebTarget(this::connect).path(dockerClientConfig.getApiVersion().asWebPathPart());
96+
baseResource = new NettyWebTarget(this::connect, host)
97+
.path(dockerClientConfig.getApiVersion().asWebPathPart());
9398
}
9499

95100
private DuplexChannel connect() {

core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/NamedPipeSocketFactory.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
11
package org.testcontainers.dockerclient.transport.okhttp;
22

3+
import lombok.EqualsAndHashCode;
34
import lombok.SneakyThrows;
45
import lombok.Value;
56
import org.scalasbt.ipcsocket.Win32NamedPipeSocket;
67

78
import javax.net.SocketFactory;
8-
import java.io.FilterInputStream;
9-
import java.io.FilterOutputStream;
10-
import java.io.IOException;
11-
import java.io.InputStream;
12-
import java.io.OutputStream;
9+
import java.io.*;
1310
import java.net.InetAddress;
1411
import java.net.Socket;
1512
import java.net.SocketAddress;
1613

1714
@Value
15+
@EqualsAndHashCode(callSuper = false)
1816
public class NamedPipeSocketFactory extends SocketFactory {
1917

2018
String socketPath;

core/src/main/java/org/testcontainers/dockerclient/transport/okhttp/UnixSocketFactory.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.testcontainers.dockerclient.transport.okhttp;
22

3+
import lombok.EqualsAndHashCode;
34
import lombok.SneakyThrows;
45
import lombok.Value;
56
import org.scalasbt.ipcsocket.UnixDomainSocket;
@@ -15,6 +16,7 @@
1516
import java.net.SocketAddress;
1617

1718
@Value
19+
@EqualsAndHashCode(callSuper = false)
1820
public class UnixSocketFactory extends SocketFactory {
1921

2022
String socketPath;

core/src/main/java/org/testcontainers/images/RemoteDockerImage.java

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import com.github.dockerjava.api.DockerClient;
44
import com.github.dockerjava.api.command.ListImagesCmd;
55
import com.github.dockerjava.api.exception.DockerClientException;
6-
import com.github.dockerjava.api.model.AuthConfig;
76
import com.github.dockerjava.api.model.Image;
87
import com.github.dockerjava.core.command.PullImageResultCallback;
98
import lombok.NonNull;
@@ -15,7 +14,6 @@
1514
import org.testcontainers.utility.DockerImageName;
1615
import org.testcontainers.utility.DockerLoggerFactory;
1716
import org.testcontainers.utility.LazyFuture;
18-
import org.testcontainers.utility.RegistryAuthLocator;
1917

2018
import java.util.HashSet;
2119
import java.util.List;
@@ -95,14 +93,10 @@ protected final String resolve() {
9593

9694
// The image is not available locally - pull it
9795
try {
98-
final RegistryAuthLocator authLocator = new RegistryAuthLocator(dockerClient.authConfig());
99-
final AuthConfig effectiveAuthConfig = authLocator.lookupAuthConfig(imageName);
100-
10196
final PullImageResultCallback callback = new PullImageResultCallback();
10297
dockerClient
10398
.pullImageCmd(imageName.getUnversionedPart())
10499
.withTag(imageName.getVersionPart())
105-
.withAuthConfig(effectiveAuthConfig)
106100
.exec(callback);
107101
callback.awaitCompletion();
108102
AVAILABLE_IMAGE_NAME_CACHE.add(imageName);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.testcontainers.utility;
2+
3+
import com.github.dockerjava.api.model.AuthConfig;
4+
import com.google.common.base.MoreObjects;
5+
import lombok.experimental.UtilityClass;
6+
import org.jetbrains.annotations.NotNull;
7+
8+
import static com.google.common.base.Strings.isNullOrEmpty;
9+
10+
/**
11+
* TODO: Javadocs
12+
*/
13+
@UtilityClass
14+
public class AuthConfigUtil {
15+
public static String toSafeString(AuthConfig authConfig) {
16+
if (authConfig == null) {
17+
return "null";
18+
}
19+
20+
return MoreObjects.toStringHelper(authConfig)
21+
.add("username", authConfig.getUsername())
22+
.add("password", obfuscated(authConfig.getPassword()))
23+
.add("auth", obfuscated(authConfig.getAuth()))
24+
.add("email", authConfig.getEmail())
25+
.add("registryAddress", authConfig.getRegistryAddress())
26+
.add("registryToken", obfuscated(authConfig.getRegistrytoken()))
27+
.toString();
28+
}
29+
30+
@NotNull
31+
private static String obfuscated(String value) {
32+
return isNullOrEmpty(value) ? "blank" : "hidden non-blank value";
33+
}
34+
}

core/src/main/java/org/testcontainers/utility/DockerImageName.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import java.util.regex.Pattern;
99

10-
@EqualsAndHashCode
10+
@EqualsAndHashCode(exclude = "rawName")
1111
public final class DockerImageName {
1212

1313
/* Regex patterns used for validation */

core/src/main/java/org/testcontainers/utility/LogUtils.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import com.github.dockerjava.api.DockerClient;
44
import com.github.dockerjava.api.command.LogContainerCmd;
5+
import com.github.dockerjava.api.model.AuthConfig;
6+
import com.google.common.base.MoreObjects;
57
import lombok.experimental.UtilityClass;
68
import org.testcontainers.containers.output.FrameConsumerResultCallback;
79
import org.testcontainers.containers.output.OutputFrame;
810

911
import java.util.function.Consumer;
1012

13+
import static com.google.common.base.Strings.isNullOrEmpty;
1114
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR;
1215
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT;
1316

@@ -39,4 +42,5 @@ public void followOutput(DockerClient dockerClient, String containerId,
3942
public void followOutput(DockerClient dockerClient, String containerId, Consumer<OutputFrame> consumer) {
4043
followOutput(dockerClient, containerId, consumer, STDOUT, STDERR);
4144
}
45+
4246
}

0 commit comments

Comments
 (0)