diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18348389c..a81dc62c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,43 @@
# Changelog
+## 8.6.0 (not yet released)
+
+### Revamped support for authentication
+This version introduces a new way to configure DefaultDockerClient to use
+authentication - the RegistryAuthSupplier interface.
+
+Historically, a single RegistryAuth instance was configured in
+DefaultDockerClient at construction-time and the instance would be used
+throughout the lifetime of the DefaultDockerClient instance. Many of the static
+factory methods in the RegistryAuth class would use the first auth element
+found in the docker client config file, and a DefaultDockerClient configured
+with `dockerAuth(true)` would have this behavior enabled by default.
+
+Inspired by a desire to be able to integrate with pushing and pulling images to
+Google Container Registry (where the docker client config file contains
+short-lived access tokens), the previous behavior has been removed and is
+replaced by RegistryAuthSupplier. DefaultDockerClient will now invoke the
+appropriate method on the configured RegistryAuthSupplier instance before each
+API operation that requires authentication. This allows for use of
+authentication info that is dynamic and changes during the lifetime of the
+DefaultDockerClient instance.
+
+The docker-client library contains an implementation of this interface that
+returns static RegistryAuth instances (NoOpRegistryAuthSupplier, which is
+configured for you if you use the old method `registryAuth(RegistryAuth)` in
+DefaultDockerClient.Builder) and an implementation for refreshing GCR access
+tokens with `gcloud docker -a`. We suggest that users implement this interface
+themselves if there is a need to customize the behavior.
+
+The new class DockerConfigReader replaces the static factory methods from
+RegistryAuth.
+
+The following methods are deprecated and will be removed in a future release:
+- DefaultDockerClient.Builder.registryAuth(RegistryAuth)
+- all overloads of RegistryAuth.fromDockerConfig(...)
+
+[740](https://github.com/spotify/docker-client/issues/740), [759](https://github.com/spotify/docker-client/pull/759)
+
## 8.5.0 (released May 18 2017)
### Removal of deprecated methods
diff --git a/pom.xml b/pom.xml
index 4b3ed640c..8b5470b70 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
docker-client
- 8.5.1-SNAPSHOT
+ 8.6.0-SNAPSHOT
jar
docker-client
A docker client
@@ -121,6 +121,11 @@
commons-compress
1.9
+
+ commons-io
+ commons-io
+ 2.5
+
org.apache.httpcomponents
httpclient
diff --git a/src/main/java/com/spotify/docker/client/DefaultDockerClient.java b/src/main/java/com/spotify/docker/client/DefaultDockerClient.java
index 8393402dc..054d3d5cf 100644
--- a/src/main/java/com/spotify/docker/client/DefaultDockerClient.java
+++ b/src/main/java/com/spotify/docker/client/DefaultDockerClient.java
@@ -47,6 +47,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
@@ -97,6 +98,7 @@
import com.spotify.docker.client.messages.NetworkCreation;
import com.spotify.docker.client.messages.ProgressMessage;
import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryAuthSupplier;
import com.spotify.docker.client.messages.RegistryConfigs;
import com.spotify.docker.client.messages.RemovedImage;
import com.spotify.docker.client.messages.ServiceCreateResponse;
@@ -324,7 +326,7 @@ public void progress(ProgressMessage message) throws DockerException {
private final URI uri;
private final String apiVersion;
- private final RegistryAuth registryAuth;
+ private final RegistryAuthSupplier registryAuthSupplier;
private final Map headers;
@@ -398,7 +400,12 @@ protected DefaultDockerClient(final Builder builder) {
.property(ApacheClientProperties.CONNECTION_MANAGER, cm)
.property(ApacheClientProperties.REQUEST_CONFIG, requestConfig);
- this.registryAuth = builder.registryAuth;
+
+ if (builder.registryAuthSupplier == null) {
+ this.registryAuthSupplier = new NoOpRegistryAuthSupplier();
+ } else {
+ this.registryAuthSupplier = builder.registryAuthSupplier;
+ }
this.client = ClientBuilder.newBuilder()
.withConfig(config)
@@ -1165,17 +1172,16 @@ public InputStream saveMultiple(final String... images)
throws DockerException, IOException, InterruptedException {
final WebTarget resource = resource().path("images").path("get");
- if (images != null) {
- for (final String image : images) {
- resource.queryParam("names", urlEncode(image));
- }
+ for (final String image : images) {
+ resource.queryParam("names", urlEncode(image));
}
return request(
GET,
InputStream.class,
resource,
- resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader(registryAuth))
+ resource.request(APPLICATION_JSON_TYPE).header("X-Registry-Auth", authHeader(
+ registryAuthSupplier.authFor(images[0])))
);
}
@@ -1187,7 +1193,7 @@ public void pull(final String image) throws DockerException, InterruptedExceptio
@Override
public void pull(final String image, final ProgressHandler handler)
throws DockerException, InterruptedException {
- pull(image, registryAuth, handler);
+ pull(image, registryAuthSupplier.authFor(image), handler);
}
@Override
@@ -1241,7 +1247,7 @@ public void push(final String image, final RegistryAuth registryAuth)
@Override
public void push(final String image, final ProgressHandler handler)
throws DockerException, InterruptedException {
- push(image, handler, registryAuth);
+ push(image, handler, registryAuthSupplier.authFor(image));
}
@Override
@@ -1360,18 +1366,7 @@ public String build(final Path directory, final String name, final String docker
}
// Convert auth to X-Registry-Config format
- final RegistryConfigs registryConfigs;
- if (registryAuth == null) {
- registryConfigs = RegistryConfigs.empty();
- } else {
- registryConfigs = RegistryConfigs.create(singletonMap(
- registryAuth.serverAddress(),
- RegistryConfigs.RegistryConfig.create(
- registryAuth.serverAddress(),
- registryAuth.username(),
- registryAuth.password(),
- registryAuth.email())));
- }
+ final RegistryConfigs registryConfigs = registryAuthSupplier.authForBuild();
try (final CompressedDirectory compressedDirectory = CompressedDirectory.create(directory);
final InputStream fileStream = Files.newInputStream(compressedDirectory.file());
@@ -1784,8 +1779,7 @@ public void unlock(final UnlockKey unlockKey) throws DockerException, Interrupte
@Override
public ServiceCreateResponse createService(ServiceSpec spec)
throws DockerException, InterruptedException {
-
- return createService(spec, registryAuth);
+ return createService(spec, registryAuthSupplier.authForSwarm());
}
@Override
@@ -2514,6 +2508,10 @@ public static Builder fromEnv() throws DockerCertificateException {
public static class Builder {
+ public static final String ERROR_MESSAGE =
+ "LOGIC ERROR: DefaultDockerClient does not support being built "
+ + "with both `registryAuth` and `registryAuthSupplier`. "
+ + "Please build with at most one of these options.";
private URI uri;
private String apiVersion;
private long connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS;
@@ -2522,6 +2520,7 @@ public static class Builder {
private DockerCertificatesStore dockerCertificatesStore;
private boolean dockerAuth;
private RegistryAuth registryAuth;
+ private RegistryAuthSupplier registryAuthSupplier;
private Map headers = new HashMap<>();
public URI uri() {
@@ -2632,7 +2631,9 @@ public boolean dockerAuth() {
*
* @param dockerAuth tells if Docker auth info should be used
* @return Builder
+ * @deprecated in favor of {@link #registryAuthSupplier(RegistryAuthSupplier)}
*/
+ @Deprecated
public Builder dockerAuth(final boolean dockerAuth) {
this.dockerAuth = dockerAuth;
return this;
@@ -2647,16 +2648,40 @@ public RegistryAuth registryAuth() {
*
* @param registryAuth RegistryAuth object
* @return Builder
+ *
+ * @deprecated in favor of {@link #registryAuthSupplier(RegistryAuthSupplier)}
*/
+ @Deprecated
public Builder registryAuth(final RegistryAuth registryAuth) {
+ if (this.registryAuthSupplier != null) {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
this.registryAuth = registryAuth;
+
+ // stuff the static RegistryAuth into a RegistryConfigs instance to maintain what
+ // DefaultDockerClient used to do with the RegistryAuth before we introduced the
+ // RegistryAuthSupplier
+ final RegistryConfigs configs = RegistryConfigs.create(singletonMap(
+ MoreObjects.firstNonNull(registryAuth.serverAddress(), ""),
+ registryAuth
+ ));
+
+ this.registryAuthSupplier = new NoOpRegistryAuthSupplier(registryAuth, configs);
+ return this;
+ }
+
+ public Builder registryAuthSupplier(final RegistryAuthSupplier registryAuthSupplier) {
+ if (this.registryAuthSupplier != null) {
+ throw new IllegalStateException(ERROR_MESSAGE);
+ }
+ this.registryAuthSupplier = registryAuthSupplier;
return this;
}
public DefaultDockerClient build() {
- if (dockerAuth) {
+ if (dockerAuth && registryAuthSupplier == null && registryAuth == null) {
try {
- this.registryAuth = RegistryAuth.fromDockerConfig().build();
+ registryAuth(RegistryAuth.fromDockerConfig().build());
} catch (IOException e) {
log.warn("Unable to use Docker auth info", e);
}
diff --git a/src/main/java/com/spotify/docker/client/DockerConfigReader.java b/src/main/java/com/spotify/docker/client/DockerConfigReader.java
new file mode 100644
index 000000000..b18b98a95
--- /dev/null
+++ b/src/main/java/com/spotify/docker/client/DockerConfigReader.java
@@ -0,0 +1,119 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.base.Preconditions;
+import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryConfigs;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Map;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DockerConfigReader {
+ private static final Logger LOG = LoggerFactory.getLogger(DockerConfigReader.class);
+
+ private static final ObjectMapper MAPPER = ObjectMapperProvider.objectMapper();
+
+ /** Returns all RegistryConfig instances from the configuration file. */
+ public RegistryConfigs fromConfig(Path configPath) throws IOException {
+ return parseDockerConfig(configPath);
+ }
+
+ public RegistryAuth fromConfig(Path configPath, String serverAddress) throws IOException {
+ return parseDockerConfig(configPath, serverAddress);
+ }
+
+ /**
+ * @deprecated do not use - only exists for backwards compatibility. Use {@link #fromConfig(Path)}
+ * instead.
+ */
+ @Deprecated
+ public RegistryAuth fromFirstConfig(Path configPath) throws IOException {
+ return parseDockerConfig(configPath, null);
+ }
+
+ private RegistryAuth parseDockerConfig(final Path configPath, String serverAddress)
+ throws IOException {
+ checkNotNull(configPath);
+
+ final Map configs = parseDockerConfig(configPath).configs();
+ if (serverAddress != null && configs.containsKey(serverAddress) ) {
+ return configs.get(serverAddress);
+ }
+
+ if (isNullOrEmpty(serverAddress)) {
+ if (configs.isEmpty()) {
+ return RegistryAuth.builder().build();
+ }
+ LOG.warn("Returning first entry from docker config file - use fromConfig(Path) instead, "
+ + "this behavior is deprecated and will soon be removed");
+ return configs.values().iterator().next();
+ }
+
+ throw new IllegalArgumentException(
+ "serverAddress=" + serverAddress + " does not appear in config file at " + configPath);
+ }
+
+ private RegistryConfigs parseDockerConfig(final Path configPath) throws IOException {
+ checkNotNull(configPath);
+ return MAPPER.treeToValue(extractAuthJson(configPath), RegistryConfigs.class);
+ }
+
+ public Path defaultConfigPath() {
+ final String home = System.getProperty("user.home");
+ final Path dockerConfig = Paths.get(home, ".docker", "config.json");
+ final Path dockerCfg = Paths.get(home, ".dockercfg");
+
+ if (Files.exists(dockerConfig)) {
+ LOG.debug("Using configfile: {}", dockerConfig);
+ return dockerConfig;
+ } else {
+ LOG.debug("Using configfile: {} ", dockerCfg);
+ return dockerCfg;
+ }
+ }
+
+ private ObjectNode extractAuthJson(final Path configPath) throws IOException {
+ final JsonNode config = MAPPER.readTree(configPath.toFile());
+
+ Preconditions.checkState(config.isObject(),
+ "config file contents are not a JSON Object, instead it is a %s", config.getNodeType());
+
+ if (config.has("auths")) {
+ final JsonNode auths = config.get("auths");
+ Preconditions.checkState(auths.isObject(),
+ "config file contents are not a JSON Object, instead it is a %s", auths.getNodeType());
+ return (ObjectNode) auths;
+ }
+
+ return (ObjectNode) config;
+ }
+}
diff --git a/src/main/java/com/spotify/docker/client/NoOpRegistryAuthSupplier.java b/src/main/java/com/spotify/docker/client/NoOpRegistryAuthSupplier.java
new file mode 100644
index 000000000..eebd534e6
--- /dev/null
+++ b/src/main/java/com/spotify/docker/client/NoOpRegistryAuthSupplier.java
@@ -0,0 +1,61 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 - 2017 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client;
+
+import com.spotify.docker.client.exceptions.DockerException;
+import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryAuthSupplier;
+import com.spotify.docker.client.messages.RegistryConfigs;
+
+/**
+ * Wraps a RegistryAuth with the RegistryAuthSupplier interface.
+ */
+public class NoOpRegistryAuthSupplier implements RegistryAuthSupplier {
+
+ private final RegistryAuth registryAuth;
+ private final RegistryConfigs configsForBuild;
+
+ public NoOpRegistryAuthSupplier(final RegistryAuth registryAuth,
+ final RegistryConfigs configsForBuild) {
+ this.registryAuth = registryAuth;
+ this.configsForBuild = configsForBuild;
+ }
+
+ public NoOpRegistryAuthSupplier() {
+ registryAuth = null;
+ configsForBuild = null;
+ }
+
+ @Override
+ public RegistryAuth authFor(String imageName) throws DockerException {
+ return registryAuth;
+ }
+
+ @Override
+ public RegistryAuth authForSwarm() {
+ return registryAuth;
+ }
+
+ @Override
+ public RegistryConfigs authForBuild() {
+ return configsForBuild;
+ }
+}
diff --git a/src/main/java/com/spotify/docker/client/gcr/GCloudProcess.java b/src/main/java/com/spotify/docker/client/gcr/GCloudProcess.java
new file mode 100644
index 000000000..0f4b554e1
--- /dev/null
+++ b/src/main/java/com/spotify/docker/client/gcr/GCloudProcess.java
@@ -0,0 +1,31 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client.gcr;
+
+import java.io.IOException;
+
+public class GCloudProcess {
+
+ public Process runGcloudDocker() throws IOException {
+ return Runtime.getRuntime().exec("gcloud docker -a");
+ }
+
+}
diff --git a/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java b/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java
new file mode 100644
index 000000000..ace09d900
--- /dev/null
+++ b/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java
@@ -0,0 +1,78 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client.gcr;
+
+import com.spotify.docker.client.DockerConfigReader;
+import com.spotify.docker.client.exceptions.DockerException;
+import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryAuthSupplier;
+import com.spotify.docker.client.messages.RegistryConfigs;
+import java.io.IOException;
+import java.nio.file.Path;
+import org.apache.commons.lang.NotImplementedException;
+
+public class GoogleContainerRegistryAuthSupplier implements RegistryAuthSupplier {
+
+ private final DockerConfigReader reader;
+ private final GoogleContainerRegistryCredRefresher refresher;
+ private final Path configPath;
+
+ public GoogleContainerRegistryAuthSupplier() {
+ this(new DockerConfigReader(),
+ new GoogleContainerRegistryCredRefresher(new GCloudProcess()),
+ new DockerConfigReader().defaultConfigPath());
+ }
+
+ public GoogleContainerRegistryAuthSupplier(
+ final DockerConfigReader reader,
+ final GoogleContainerRegistryCredRefresher refresher,
+ final Path configPath) {
+ this.reader = reader;
+ this.refresher = refresher;
+ this.configPath = configPath;
+ }
+
+ @Override
+ public RegistryAuth authFor(String imageName) throws DockerException {
+ try {
+ String registryName = "https://" + imageName.split("/")[0];
+ refresher.refresh();
+ return reader.fromConfig(configPath, registryName);
+ } catch (IOException ex) {
+ throw new DockerException(ex);
+ }
+ }
+
+ @Override
+ public RegistryAuth authForSwarm() {
+ throw new NotImplementedException();
+ }
+
+ @Override
+ public RegistryConfigs authForBuild() throws DockerException {
+ try {
+ refresher.refresh();
+ return reader.fromConfig(reader.defaultConfigPath());
+ } catch (IOException ex) {
+ throw new DockerException(ex);
+ }
+ }
+}
diff --git a/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresher.java b/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresher.java
new file mode 100644
index 000000000..6a96e8149
--- /dev/null
+++ b/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresher.java
@@ -0,0 +1,47 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client.gcr;
+
+import com.spotify.docker.client.gcr.GCloudProcess;
+import java.io.IOException;
+import org.apache.commons.io.IOUtils;
+
+public class GoogleContainerRegistryCredRefresher {
+
+ private final GCloudProcess gcloudProcess;
+
+ public GoogleContainerRegistryCredRefresher(GCloudProcess gcloudProcess) {
+ this.gcloudProcess = gcloudProcess;
+ }
+
+ public void refresh() throws IOException {
+ Process process = gcloudProcess.runGcloudDocker();
+ try {
+ if (process.waitFor() != 0) {
+ throw new IOException(IOUtils.toString(process.getErrorStream(), "UTF-8"));
+ }
+ } catch (InterruptedException ex) {
+ throw new IOException(ex);
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java b/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java
index 3dc40f633..846cbca45 100644
--- a/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java
+++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java
@@ -22,43 +22,23 @@
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Strings.isNullOrEmpty;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
-import com.spotify.docker.client.ObjectMapperProvider;
-
+import com.spotify.docker.client.DockerConfigReader;
import java.io.IOException;
-import java.nio.file.Files;
import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Iterator;
-
import javax.annotation.Nullable;
-
import org.glassfish.jersey.internal.util.Base64;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
@AutoValue
@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, setterVisibility = NONE)
public abstract class RegistryAuth {
- private static final Logger LOG = LoggerFactory.getLogger(RegistryAuth.class);
-
- private static final String DEFAULT_REGISTRY = "https://index.docker.io/v1/";
- private static final String DUMMY_EMAIL = "1234@5678.com";
-
- @SuppressWarnings("FieldCanBeLocal")
- private static final ObjectMapper MAPPER =
- new ObjectMapperProvider().getContext(RegistryAuth.class);
-
@Nullable
@JsonProperty("Username")
public abstract String username();
@@ -74,6 +54,7 @@ public abstract class RegistryAuth {
@JsonProperty("Email")
public abstract String email();
+ @Nullable
@JsonProperty("ServerAddress")
public abstract String serverAddress();
@@ -81,8 +62,9 @@ public abstract class RegistryAuth {
@JsonProperty("IdentityToken")
public abstract String identityToken();
+ @Override
public final String toString() {
- return MoreObjects.toStringHelper(this)
+ return MoreObjects.toStringHelper(RegistryAuth.class)
.add("username", username())
// don't log the password or email
.add("serverAddress", serverAddress())
@@ -99,10 +81,13 @@ public final String toString() {
*
* @return a {@link Builder}
* @throws IOException when we can't parse the docker config file
+ * @deprecated in favor of registryAuthSupplier
*/
- @SuppressWarnings("unused")
+ @Deprecated
+ @SuppressWarnings({"deprecated", "unused"})
public static Builder fromDockerConfig() throws IOException {
- return parseDockerConfig(defaultConfigPath(), null);
+ DockerConfigReader dockerCfgReader = new DockerConfigReader();
+ return dockerCfgReader.fromFirstConfig(dockerCfgReader.defaultConfigPath()).toBuilder();
}
/**
@@ -116,7 +101,9 @@ public static Builder fromDockerConfig() throws IOException {
*/
@SuppressWarnings("unused")
public static Builder fromDockerConfig(final String serverAddress) throws IOException {
- return parseDockerConfig(defaultConfigPath(), serverAddress);
+ DockerConfigReader dockerCfgReader = new DockerConfigReader();
+ return dockerCfgReader
+ .fromConfig(dockerCfgReader.defaultConfigPath(), serverAddress).toBuilder();
}
/**
@@ -129,7 +116,8 @@ public static Builder fromDockerConfig(final String serverAddress) throws IOExce
*/
@VisibleForTesting
static Builder fromDockerConfig(final Path configPath) throws IOException {
- return parseDockerConfig(configPath, null);
+ DockerConfigReader dockerCfgReader = new DockerConfigReader();
+ return dockerCfgReader.fromConfig(configPath, null).toBuilder();
}
/**
@@ -144,91 +132,52 @@ static Builder fromDockerConfig(final Path configPath) throws IOException {
@VisibleForTesting
static Builder fromDockerConfig(final Path configPath, final String serverAddress)
throws IOException {
- return parseDockerConfig(configPath, serverAddress);
+ DockerConfigReader dockerConfigReader = new DockerConfigReader();
+ return dockerConfigReader.fromConfig(configPath, serverAddress).toBuilder();
}
- private static Path defaultConfigPath() {
- final String home = System.getProperty("user.home");
- final Path dockerConfig = Paths.get(home, ".docker", "config.json");
- final Path dockerCfg = Paths.get(home, ".dockercfg");
-
- if (Files.exists(dockerConfig)) {
- LOG.debug("Using configfile: {}", dockerConfig);
- return dockerConfig;
- } else if (Files.exists(dockerCfg)) {
- LOG.debug("Using configfile: {} ", dockerCfg);
- return dockerCfg;
+ @JsonCreator
+ public static RegistryAuth create(@JsonProperty("username") String username,
+ @JsonProperty("password") String password,
+ @JsonProperty("email") final String email,
+ @JsonProperty("serverAddress") final String serverAddress,
+ @JsonProperty("identityToken") final String identityToken,
+ @JsonProperty("auth") final String auth) {
+
+ final Builder builder;
+ if (auth != null) {
+ builder = forAuthToken(auth);
} else {
- throw new RuntimeException(
- "Could not find a docker config. Please run 'docker login' to create one");
+ builder = builder()
+ .username(username)
+ .password(password);
}
+ return builder
+ .email(email)
+ .serverAddress(serverAddress)
+ .identityToken(identityToken)
+ .build();
}
- private static RegistryAuth.Builder parseDockerConfig(final Path configPath, String serverAddress)
- throws IOException {
- checkNotNull(configPath);
- final RegistryAuth.Builder authBuilder = RegistryAuth.builder();
- final JsonNode authJson = extractAuthJson(configPath);
-
- if (isNullOrEmpty(serverAddress)) {
- final Iterator servers = authJson.fieldNames();
- if (servers.hasNext()) {
- serverAddress = servers.next();
- }
- } else {
- if (!authJson.has(serverAddress)) {
- LOG.error("Could not find auth config for {}. Returning empty builder", serverAddress);
- return RegistryAuth.builder().serverAddress(serverAddress);
- }
- }
+ /** Construct a Builder based upon the "auth" field of the docker client config file. */
+ public static Builder forAuthToken(String auth) {
+ final String[] authParams = Base64.decodeAsString(auth).split(":");
- final JsonNode serverAuth = authJson.get(serverAddress);
- if (serverAuth != null && serverAuth.has("auth")) {
- authBuilder.serverAddress(serverAddress);
- final String authString = serverAuth.get("auth").asText();
- final String[] authParams = Base64.decodeAsString(authString).split(":");
-
- if (authParams.length == 2) {
- authBuilder.username(authParams[0].trim());
- authBuilder.password(authParams[1].trim());
- } else if (serverAuth.has("identityToken")) {
- authBuilder.identityToken(serverAuth.get("identityToken").asText());
- return authBuilder;
- } else {
- LOG.warn("Failed to parse auth string for {}", serverAddress);
- return authBuilder;
- }
- } else {
- LOG.warn("Could not find auth field for {}", serverAddress);
- return authBuilder;
- }
-
- if (serverAuth.has("email")) {
- authBuilder.email(serverAuth.get("email").asText());
- }
-
- return authBuilder;
- }
-
- private static JsonNode extractAuthJson(final Path configPath) throws IOException {
- final JsonNode config = MAPPER.readTree(configPath.toFile());
-
- if (config.has("auths")) {
- return config.get("auths");
+ if (authParams.length != 2) {
+ return builder();
}
-
- return config;
+ return builder()
+ .username(authParams[0].trim())
+ .password(authParams[1].trim());
}
public static Builder builder() {
- return new AutoValue_RegistryAuth.Builder()
- // Default to the public Docker registry.
- .serverAddress(DEFAULT_REGISTRY)
- .email(DUMMY_EMAIL);
+ return new AutoValue_RegistryAuth.Builder();
}
@AutoValue.Builder
public abstract static class Builder {
+
public abstract Builder username(final String username);
public abstract Builder password(final String password);
diff --git a/src/main/java/com/spotify/docker/client/messages/RegistryAuthSupplier.java b/src/main/java/com/spotify/docker/client/messages/RegistryAuthSupplier.java
new file mode 100644
index 000000000..b0bae4da3
--- /dev/null
+++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuthSupplier.java
@@ -0,0 +1,41 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client.messages;
+
+import com.spotify.docker.client.exceptions.DockerException;
+
+public interface RegistryAuthSupplier {
+
+ /**
+ * Returns a RegistryAuth object that works with a given registry's API [e.g. GCR].
+ */
+ RegistryAuth authFor(String imageName) throws DockerException;
+
+ /**
+ * Returns a RegistryAuth object that is valid for a Docker Swarm context [i.e. not tied
+ * to specific image]. It's unnecessary if it's not planned to use this AuthSupplier to pull
+ * images for Swarm.
+ */
+ RegistryAuth authForSwarm() throws DockerException;
+
+ /** Authentication info to pass in the X-Registry-Config header when building an image. */
+ RegistryConfigs authForBuild() throws DockerException;
+}
diff --git a/src/main/java/com/spotify/docker/client/messages/RegistryConfigs.java b/src/main/java/com/spotify/docker/client/messages/RegistryConfigs.java
index 0479404db..1958112dd 100644
--- a/src/main/java/com/spotify/docker/client/messages/RegistryConfigs.java
+++ b/src/main/java/com/spotify/docker/client/messages/RegistryConfigs.java
@@ -25,14 +25,11 @@
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.auto.value.AutoValue;
-import com.google.common.base.MoreObjects;
-
import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
import java.util.Collections;
import java.util.Map;
-import javax.annotation.Nullable;
/**
* A formatted string passed in X-Registry-Config request header.
@@ -60,73 +57,36 @@
@JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, setterVisibility = NONE)
public abstract class RegistryConfigs {
- private static final RegistryConfigs EMPTY =
- RegistryConfigs.create(Collections.emptyMap());
-
public static RegistryConfigs empty() {
- return EMPTY;
+ return RegistryConfigs.create(Collections.emptyMap());
}
- public abstract ImmutableMap configs();
-
- @AutoValue
- public abstract static class RegistryConfig {
-
- // The address of the repository
- @JsonProperty("serveraddress")
- public abstract String serverAddress();
-
- @Nullable
- @JsonProperty("username")
- public abstract String username();
-
- @Nullable
- @JsonProperty("password")
- public abstract String password();
-
- @Nullable
- @JsonProperty("email")
- public abstract String email();
-
- // Not used but must be supplied
- @JsonProperty("auth")
- public abstract String auth();
+ public abstract ImmutableMap configs();
- public static RegistryConfig create(
- final String serveraddress,
- final String username,
- final String password,
- final String email) {
- return create(serveraddress, username, password, email, "");
- }
-
- @JsonCreator
- static RegistryConfig create(
- @JsonProperty("serveraddress") final String serveraddress,
- @JsonProperty("username") final String username,
- @JsonProperty("password") final String password,
- @JsonProperty("email") final String email,
- @JsonProperty("auth") final String auth) {
- return new AutoValue_RegistryConfigs_RegistryConfig(serveraddress, username, password, email,
- auth);
- }
-
- // Override @AutoValue to not leak password
- @Override
- public String toString() {
- return MoreObjects.toStringHelper(this)
- .add("serverAddress", serverAddress())
- .add("username", username())
- .add("email", email())
- .add("auth", auth())
- .toString();
+ @JsonCreator
+ public static RegistryConfigs create(final Map configs) {
+ if (configs == null) {
+ return new AutoValue_RegistryConfigs(ImmutableMap.of());
}
- }
- @JsonCreator
- public static RegistryConfigs create(final Map configs) {
- final ImmutableMap configsT =
- configs == null ? ImmutableMap.of() : ImmutableMap.copyOf(configs);
- return new AutoValue_RegistryConfigs(configsT);
+ // need to add serverAddress to each RegistryAuth instance; it is not available when
+ // Jackson is deserializing the RegistryAuth field
+ final Map transformedMap = Maps.transformEntries(configs,
+ new Maps.EntryTransformer() {
+ @Override
+ public RegistryAuth transformEntry(final String key, final RegistryAuth value) {
+ if (value == null) {
+ return null;
+ }
+ if (value.serverAddress() == null) {
+ return value.toBuilder()
+ .serverAddress(key)
+ .build();
+ }
+ return value;
+ }
+ });
+
+ return new AutoValue_RegistryConfigs(ImmutableMap.copyOf(transformedMap));
}
}
diff --git a/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java b/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java
index 7baf14309..d5bd74c30 100644
--- a/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java
+++ b/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java
@@ -60,7 +60,6 @@
import static java.lang.Long.toHexString;
import static java.lang.String.format;
import static java.lang.System.getenv;
-import static java.util.Collections.emptyMap;
import static java.util.Collections.singletonList;
import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
import static org.awaitility.Awaitility.await;
@@ -214,7 +213,6 @@
import com.spotify.docker.client.messages.swarm.ServiceSpec;
import com.spotify.docker.client.messages.swarm.Swarm;
import com.spotify.docker.client.messages.swarm.SwarmInit;
-import com.spotify.docker.client.messages.swarm.SwarmJoin;
import com.spotify.docker.client.messages.swarm.SwarmSpec;
import com.spotify.docker.client.messages.swarm.Task;
import com.spotify.docker.client.messages.swarm.TaskDefaults;
@@ -312,19 +310,22 @@ public class DefaultDockerClientTest {
@Rule
public final TestName testName = new TestName();
+ private final RegistryAuth registryAuth = RegistryAuth.builder()
+ .username(AUTH_USERNAME)
+ .password(AUTH_PASSWORD)
+ .email("1234@example.com")
+ .build();
+
private final String nameTag = toHexString(ThreadLocalRandom.current().nextLong());
private URI dockerEndpoint;
private DefaultDockerClient sut;
- private RegistryAuth registryAuth;
-
private String dockerApiVersion;
@Before
public void setup() throws Exception {
- registryAuth = RegistryAuth.builder().username(AUTH_USERNAME).password(AUTH_PASSWORD).build();
final DefaultDockerClient.Builder builder = DefaultDockerClient.fromEnv();
// Make it easier to test no read timeout occurs by using a smaller value
// Such test methods should end in 'NoTimeout'
@@ -689,6 +690,7 @@ public void testAuth() throws Exception {
public void testBadAuth() throws Exception {
final RegistryAuth badRegistryAuth = RegistryAuth.builder()
.username(AUTH_USERNAME)
+ .email("user@example.com") // docker < 1.11 requires email to be set in RegistryAuth
.password("foobar")
.build();
final int statusCode = sut.auth(badRegistryAuth);
diff --git a/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java b/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java
index 1f24f6c82..e010d3088 100644
--- a/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java
+++ b/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java
@@ -24,16 +24,31 @@
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
+import com.google.common.io.BaseEncoding;
+import com.google.common.io.Resources;
+import com.spotify.docker.client.exceptions.DockerCertificateException;
+import com.spotify.docker.client.gcr.GoogleContainerRegistryAuthSupplier;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.HostConfig;
+import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryAuthSupplier;
+import com.spotify.docker.client.messages.RegistryConfigs;
import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -43,7 +58,9 @@
import okio.Buffer;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.ExpectedException;
/**
* Tests DefaultDockerClient against a {@link okhttp3.mockwebserver.MockWebServer} instance, so
@@ -69,8 +86,12 @@
public class DefaultDockerClientUnitTest {
private final MockWebServer server = new MockWebServer();
+
private DefaultDockerClient.Builder builder;
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
@Before
public void setup() throws Exception {
server.start();
@@ -142,6 +163,18 @@ private static JsonNode toJson(Buffer buffer) throws IOException {
return ObjectMapperProvider.objectMapper().readTree(buffer.inputStream());
}
+ private static JsonNode toJson(byte[] bytes) throws IOException {
+ return ObjectMapperProvider.objectMapper().readTree(bytes);
+ }
+
+ private static JsonNode toJson(Object object) throws IOException {
+ return ObjectMapperProvider.objectMapper().valueToTree(object);
+ }
+
+ private static ObjectNode createObjectNode() {
+ return ObjectMapperProvider.objectMapper().createObjectNode();
+ }
+
@Test
@SuppressWarnings("unchecked")
public void testCapAddAndDrop() throws Exception {
@@ -188,4 +221,83 @@ private static Set childrenTextNodes(ArrayNode arrayNode) {
return texts;
}
+ @Test
+ @SuppressWarnings("deprecated")
+ public void buildThrowsIfRegistryAuthandRegistryAuthSupplierAreBothSpecified()
+ throws DockerCertificateException {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("LOGIC ERROR");
+
+ DefaultDockerClient.builder()
+ .registryAuth(RegistryAuth.builder().identityToken("hello").build())
+ .registryAuthSupplier(new GoogleContainerRegistryAuthSupplier())
+ .build();
+ }
+
+ @Test
+ public void testBuildPassesMultipleRegistryConfigs() throws Exception {
+ final RegistryConfigs registryConfigs = RegistryConfigs.create(ImmutableMap.of(
+ "server1", RegistryAuth.builder()
+ .serverAddress("server1")
+ .username("u1")
+ .password("p1")
+ .email("e1")
+ .build(),
+
+ "server2", RegistryAuth.builder()
+ .serverAddress("server2")
+ .username("u2")
+ .password("p2")
+ .email("e2")
+ .build()
+ ));
+
+ final RegistryAuthSupplier authSupplier = mock(RegistryAuthSupplier.class);
+ when(authSupplier.authForBuild()).thenReturn(registryConfigs);
+
+ final DefaultDockerClient client = builder.registryAuthSupplier(authSupplier)
+ .build();
+
+ // build() calls /version to check what format of header to send
+ server.enqueue(new MockResponse()
+ .setResponseCode(200)
+ .addHeader("Content-Type", "application/json")
+ .setBody(
+ createObjectNode()
+ .put("ApiVersion", "1.20")
+ .put("Arch", "foobar")
+ .put("GitCommit", "foobar")
+ .put("GoVersion", "foobar")
+ .put("KernelVersion", "foobar")
+ .put("Os", "foobar")
+ .put("Version", "1.20")
+ .toString()
+ )
+ );
+
+ // TODO (mbrown): what to return for build response?
+ server.enqueue(new MockResponse()
+ .setResponseCode(200)
+ );
+
+ final Path path = Paths.get(Resources.getResource("dockerDirectory").toURI());
+
+ client.build(path);
+
+ final RecordedRequest versionRequest = takeRequestImmediately();
+ assertThat(versionRequest.getMethod(), is("GET"));
+ assertThat(versionRequest.getPath(), is("/version"));
+
+ final RecordedRequest buildRequest = takeRequestImmediately();
+ assertThat(buildRequest.getMethod(), is("POST"));
+ assertThat(buildRequest.getPath(), is("/build"));
+
+ final String registryConfigHeader = buildRequest.getHeader("X-Registry-Config");
+ assertThat(registryConfigHeader, is(not(nullValue())));
+
+ // check that the JSON in the header is equivalent to what we mocked out above from
+ // the registryAuthSupplier
+ final JsonNode headerJsonNode = toJson(BaseEncoding.base64().decode(registryConfigHeader));
+ assertThat(headerJsonNode, is(toJson(registryConfigs.configs())));
+ }
}
diff --git a/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java b/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java
new file mode 100644
index 000000000..1fd5da2e7
--- /dev/null
+++ b/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java
@@ -0,0 +1,190 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 - 2017 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+/*
+ * Copyright (c) 2017
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.spotify.docker.client;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.is;
+
+import com.google.common.io.Resources;
+import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryConfigs;
+import java.io.FileNotFoundException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.commons.lang.RandomStringUtils;
+import org.hamcrest.CustomTypeSafeMatcher;
+import org.hamcrest.Matcher;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+@SuppressWarnings("deprecated")
+public class DockerConfigReaderTest {
+
+ private static final RegistryAuth DOCKER_AUTH_CONFIG = RegistryAuth.builder()
+ .serverAddress("https://index.docker.io/v1/")
+ .username("dockerman")
+ .password("sw4gy0lo")
+ .email("dockerman@hub.com")
+ .build();
+
+ private static final RegistryAuth MY_AUTH_CONFIG = RegistryAuth.builder()
+ .serverAddress("https://narnia.mydock.io/v1/")
+ .username("megaman")
+ .password("riffraff")
+ .email("megaman@mydock.com")
+ .build();
+
+ private static final RegistryAuth IDENTITY_TOKEN_AUTH_CONFIG = RegistryAuth.builder()
+ .email("dockerman@hub.com")
+ .serverAddress("docker.customdomain.com")
+ .identityToken("52ce5fd5-eb60-42bf-931f-5eeec128211a")
+ .build();
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private final DockerConfigReader reader = new DockerConfigReader();
+
+ @Test
+ public void testFromDockerConfig_FullConfig() throws Exception {
+ final RegistryAuth registryAuth =
+ reader.fromFirstConfig(getTestFilePath("dockerConfig/fullConfig.json"));
+ assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG));
+ }
+
+ @Test
+ public void testFromDockerConfig_FullDockerCfg() throws Exception {
+ final RegistryAuth registryAuth =
+ reader.fromFirstConfig(getTestFilePath("dockerConfig/fullDockerCfg"));
+ assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG));
+ }
+
+ @Test
+ public void testFromDockerConfig_IdentityToken() throws Exception {
+ final RegistryAuth authConfig =
+ reader.fromFirstConfig(getTestFilePath("dockerConfig/identityTokenConfig.json"));
+ assertThat(authConfig, equalTo(IDENTITY_TOKEN_AUTH_CONFIG));
+ }
+
+ @Test
+ public void testFromDockerConfig_IncompleteConfig() throws Exception {
+ final RegistryAuth registryAuth =
+ reader.fromFirstConfig(getTestFilePath("dockerConfig/incompleteConfig.json"));
+
+ final RegistryAuth expected = RegistryAuth.builder()
+ .email("dockerman@hub.com")
+ .serverAddress("https://different.docker.io/v1/")
+ .build();
+
+ assertThat(registryAuth, is(expected));
+ }
+
+ @Test
+ public void testFromDockerConfig_WrongConfigs() throws Exception {
+ final RegistryAuth registryAuth1 =
+ reader.fromFirstConfig(getTestFilePath("dockerConfig/wrongConfig1.json"));
+ assertThat(registryAuth1, is(emptyRegistryAuth()));
+
+ final RegistryAuth registryAuth2 =
+ reader.fromFirstConfig(getTestFilePath("dockerConfig/wrongConfig2.json"));
+ assertThat(registryAuth2, is(emptyRegistryAuth()));
+ }
+
+ private static Matcher emptyRegistryAuth() {
+ return new CustomTypeSafeMatcher("an empty RegistryAuth") {
+ @Override
+ protected boolean matchesSafely(final RegistryAuth item) {
+ return item.email() == null
+ && item.identityToken() == null
+ && item.password() == null
+ && item.email() == null;
+ }
+ };
+ }
+
+ @Test
+ public void testFromDockerConfig_MissingConfigFile() throws Exception {
+ final Path randomPath = Paths.get(RandomStringUtils.randomAlphanumeric(16) + ".json");
+ expectedException.expect(FileNotFoundException.class);
+ reader.fromFirstConfig(randomPath);
+ }
+
+ @Test
+ public void testFromDockerConfig_MultiConfig() throws Exception {
+ final Path path = getTestFilePath("dockerConfig/multiConfig.json");
+
+ final RegistryAuth myDockParsed = reader.fromConfig(path, "https://narnia.mydock.io/v1/");
+ assertThat(myDockParsed, equalTo(MY_AUTH_CONFIG));
+
+ final RegistryAuth dockerIoParsed = reader.fromConfig(path, "https://index.docker.io/v1/");
+ assertThat(dockerIoParsed, equalTo(DOCKER_AUTH_CONFIG));
+ }
+
+ private static Path getTestFilePath(final String path) {
+ if (OsUtils.isLinux() || OsUtils.isOsX()) {
+ return getLinuxPath(path);
+ } else {
+ return getWindowsPath(path);
+ }
+ }
+
+ private static Path getWindowsPath(final String path) {
+ final URL resource = DockerConfigReaderTest.class.getResource("/" + path);
+ return Paths.get(resource.getPath().substring(1));
+ }
+
+ private static Path getLinuxPath(final String path) {
+ return Paths.get(Resources.getResource(path).getPath());
+ }
+
+ @Test
+ public void testParseRegistryConfigs() throws Exception {
+ final Path path = getTestFilePath("dockerConfig/multiConfig.json");
+ final RegistryConfigs configs = reader.fromConfig(path);
+
+ assertThat(configs.configs(), allOf(
+ hasEntry("https://index.docker.io/v1/", DOCKER_AUTH_CONFIG),
+ hasEntry("https://narnia.mydock.io/v1/", MY_AUTH_CONFIG)
+ ));
+ }
+}
diff --git a/src/test/java/com/spotify/docker/client/NoOpRegistryAuthSupplierTest.java b/src/test/java/com/spotify/docker/client/NoOpRegistryAuthSupplierTest.java
new file mode 100644
index 000000000..c3629be16
--- /dev/null
+++ b/src/test/java/com/spotify/docker/client/NoOpRegistryAuthSupplierTest.java
@@ -0,0 +1,47 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 - 2017 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+
+import com.spotify.docker.client.exceptions.DockerException;
+import com.spotify.docker.client.messages.RegistryAuth;
+import org.junit.Test;
+
+public class NoOpRegistryAuthSupplierTest {
+
+ @Test
+ public void authForReturnsWrappedAuthRegistry() throws DockerException {
+ RegistryAuth registryAuth = mock(RegistryAuth.class);
+ NoOpRegistryAuthSupplier noOpRegistryAuthSupplier = new NoOpRegistryAuthSupplier(registryAuth,
+ null);
+ assertEquals(registryAuth, noOpRegistryAuthSupplier.authFor("doesn't matter"));
+ }
+
+ @Test
+ public void authForReturnsNullForEmptyConstructor() throws DockerException {
+ NoOpRegistryAuthSupplier noOpRegistryAuthSupplier = new NoOpRegistryAuthSupplier();
+ assertNull(noOpRegistryAuthSupplier.authFor("any"));
+ assertNull(noOpRegistryAuthSupplier.authForBuild());
+ }
+}
diff --git a/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java
new file mode 100644
index 000000000..8c32c768b
--- /dev/null
+++ b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java
@@ -0,0 +1,80 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2017 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client.gcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.spotify.docker.client.DockerConfigReader;
+import com.spotify.docker.client.messages.RegistryAuth;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+public class GoogleContainerRegistryAuthSupplierTest {
+
+ @Test
+ public void authForRefreshesCredsBeforeReadingConfigFile() throws Exception {
+ DockerConfigReader dockerCfgReader = mock(DockerConfigReader.class);
+ GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher =
+ mock(GoogleContainerRegistryCredRefresher.class);
+ Path path = mock(Path.class);
+ InOrder inOrder = inOrder(googleContainerRegistryCredRefresher, dockerCfgReader);
+
+ GoogleContainerRegistryAuthSupplier googleContainerRegistryAuthSupplier =
+ new GoogleContainerRegistryAuthSupplier(
+ dockerCfgReader, googleContainerRegistryCredRefresher, path);
+
+ googleContainerRegistryAuthSupplier.authFor("us.gcr.io/awesome-project/example-image");
+ inOrder.verify(googleContainerRegistryCredRefresher).refresh();
+ inOrder.verify(dockerCfgReader).fromConfig(any(Path.class), anyString());
+ }
+
+ @Test
+ public void authForReturnsRegistryAuthThatMatchesRegistryName() throws Exception {
+ DockerConfigReader dockerCfgReader = mock(DockerConfigReader.class);
+ GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher =
+ mock(GoogleContainerRegistryCredRefresher.class);
+ Path path = mock(Path.class);
+
+ RegistryAuth expected =
+ RegistryAuth.builder().email("no@no.com").identityToken("authorific").build();
+
+ when(dockerCfgReader.fromConfig(any(Path.class), eq("https://us.gcr.io"))).thenReturn(expected);
+
+ GoogleContainerRegistryAuthSupplier googleContainerRegistryAuthSupplier =
+ new GoogleContainerRegistryAuthSupplier(
+ dockerCfgReader, googleContainerRegistryCredRefresher, path);
+
+ RegistryAuth registryAuth = googleContainerRegistryAuthSupplier
+ .authFor("us.gcr.io/awesome-project/example-image");
+
+ assertEquals(expected.email(), registryAuth.email());
+ assertEquals(expected.identityToken(), registryAuth.identityToken());
+
+ }
+
+}
diff --git a/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java
new file mode 100644
index 000000000..3d89b4409
--- /dev/null
+++ b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java
@@ -0,0 +1,78 @@
+/*-
+ * -\-\-
+ * docker-client
+ * --
+ * Copyright (C) 2016 Spotify AB
+ * --
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * -/-/-
+ */
+
+package com.spotify.docker.client.gcr;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class GoogleContainerRegistryCredRefresherTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+
+ @Test
+ public void refreshShellsOutToGCloudCli() throws IOException, InterruptedException {
+ Process process = mock(Process.class);
+ when(process.waitFor()).thenReturn(0);
+
+ GCloudProcess gcloudProcess = mock(GCloudProcess.class);
+ when(gcloudProcess.runGcloudDocker()).thenReturn(process);
+
+ GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher =
+ new GoogleContainerRegistryCredRefresher(gcloudProcess);
+
+ googleContainerRegistryCredRefresher.refresh();
+ verify(gcloudProcess).runGcloudDocker();
+
+ }
+
+ @Test
+ public void refreshThrowsIfSuccessCodeIsntReturnedFromCommand()
+ throws InterruptedException, IOException {
+ thrown.expect(IOException.class);
+ thrown.expectMessage("ERROR: (gcloud.docker)");
+
+ GCloudProcess gcloudProcess = mock(GCloudProcess.class);
+
+ Process failedProc = mock(Process.class);
+ when(failedProc.waitFor()).thenReturn(1);
+ when(failedProc.getErrorStream())
+ .thenReturn(
+ new ByteArrayInputStream("ERROR: (gcloud.docker)".getBytes(StandardCharsets.UTF_8)));
+
+ when(gcloudProcess.runGcloudDocker()).thenReturn(failedProc);
+
+ GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher =
+ new GoogleContainerRegistryCredRefresher(gcloudProcess);
+
+ googleContainerRegistryCredRefresher.refresh();
+ }
+
+}
diff --git a/src/test/java/com/spotify/docker/client/messages/RegistryAuthTest.java b/src/test/java/com/spotify/docker/client/messages/RegistryAuthTest.java
deleted file mode 100644
index cf4c0aa52..000000000
--- a/src/test/java/com/spotify/docker/client/messages/RegistryAuthTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*-
- * -\-\-
- * docker-client
- * --
- * Copyright (C) 2016 Spotify AB
- * --
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * -/-/-
- */
-
-package com.spotify.docker.client.messages;
-
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import com.google.common.io.Resources;
-import com.spotify.docker.client.OsUtils;
-
-import java.io.FileNotFoundException;
-import java.net.URL;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-import org.apache.commons.lang.RandomStringUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-public class RegistryAuthTest {
-
- private static final RegistryAuth DOCKER_AUTH_CONFIG = RegistryAuth.builder()
- .serverAddress("https://index.docker.io/v1/")
- .username("dockerman")
- .password("sw4gy0lo")
- .email("dockerman@hub.com")
- .build();
-
- private static final RegistryAuth MY_AUTH_CONFIG = RegistryAuth.builder()
- .serverAddress("https://narnia.mydock.io/v1/")
- .username("megaman")
- .password("riffraff")
- .email("megaman@mydock.com")
- .build();
-
- private static final RegistryAuth IDENTITY_TOKEN_AUTH_CONFIG = RegistryAuth.builder()
- .serverAddress("docker.customdomain.com")
- .identityToken("52ce5fd5-eb60-42bf-931f-5eeec128211a")
- .build();
-
- private static final RegistryAuth EMPTY_AUTH_CONFIG = RegistryAuth.builder().build();
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- @Test
- public void testFromDockerConfig_FullConfig() throws Exception {
- final RegistryAuth registryAuth = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/fullConfig.json")).build();
- assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG));
- }
-
- @Test
- public void testFromDockerConfig_FullDockerCfg() throws Exception {
- final RegistryAuth registryAuth = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/fullDockerCfg")).build();
- assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG));
- }
-
- @Test
- public void testFromDockerConfig_IdentityToken() throws Exception {
- final RegistryAuth authConfig = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/identityTokenConfig.json")).build();
- assertThat(authConfig, equalTo(IDENTITY_TOKEN_AUTH_CONFIG));
- }
-
- @Test
- public void testFromDockerConfig_IncompleteConfig() throws Exception {
- final RegistryAuth registryAuth = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/incompleteConfig.json")).build();
- assertThat(registryAuth, equalTo(EMPTY_AUTH_CONFIG));
- }
-
- @Test
- public void testFromDockerConfig_WrongConfigs() throws Exception {
- final RegistryAuth registryAuth1 = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/wrongConfig1.json")).build();
- assertThat(registryAuth1, equalTo(EMPTY_AUTH_CONFIG));
-
- final RegistryAuth registryAuth2 = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/wrongConfig2.json")).build();
- assertThat(registryAuth2, equalTo(EMPTY_AUTH_CONFIG));
- }
-
- @Test
- public void testFromDockerConfig_MissingConfigFile() throws Exception {
- final Path randomPath = Paths.get(RandomStringUtils.randomAlphanumeric(16) + ".json");
- expectedException.expect(FileNotFoundException.class);
- RegistryAuth.fromDockerConfig(randomPath).build();
- }
-
- @Test
- public void testFromDockerConfig_MultiConfig() throws Exception {
- final RegistryAuth myDockParsed = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/multiConfig.json"), "https://narnia.mydock.io/v1/").build();
- assertThat(myDockParsed, equalTo(MY_AUTH_CONFIG));
- final RegistryAuth dockerIoParsed = RegistryAuth.fromDockerConfig(getTestFilePath(
- "dockerConfig/multiConfig.json"), "https://index.docker.io/v1/").build();
- assertThat(dockerIoParsed, equalTo(DOCKER_AUTH_CONFIG));
- }
-
- private static Path getTestFilePath(final String path) {
- if (OsUtils.isLinux() || OsUtils.isOsX()) {
- return getLinuxPath(path);
- } else {
- return getWindowsPath(path);
- }
- }
-
- private static Path getWindowsPath(final String path) {
- final URL resource = RegistryAuthTest.class.getResource("/" + path);
- return Paths.get(resource.getPath().substring(1));
- }
-
- private static Path getLinuxPath(final String path) {
- return Paths.get(Resources.getResource(path).getPath());
- }
-}
diff --git a/src/test/java/com/spotify/docker/it/PushPullIT.java b/src/test/java/com/spotify/docker/it/PushPullIT.java
index aa43a44e4..95fa244e2 100644
--- a/src/test/java/com/spotify/docker/it/PushPullIT.java
+++ b/src/test/java/com/spotify/docker/it/PushPullIT.java
@@ -21,6 +21,7 @@
package com.spotify.docker.it;
import static com.google.common.base.Strings.isNullOrEmpty;
+import static java.lang.System.getenv;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.CoreMatchers.isA;
@@ -34,12 +35,14 @@
import com.spotify.docker.client.exceptions.ContainerNotFoundException;
import com.spotify.docker.client.exceptions.DockerException;
import com.spotify.docker.client.exceptions.ImagePushFailedException;
+import com.spotify.docker.client.gcr.GoogleContainerRegistryAuthSupplier;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.ContainerCreation;
import com.spotify.docker.client.messages.ContainerInfo;
import com.spotify.docker.client.messages.HostConfig;
import com.spotify.docker.client.messages.PortBinding;
import com.spotify.docker.client.messages.RegistryAuth;
+import com.spotify.docker.client.messages.RegistryAuthSupplier;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
@@ -47,6 +50,7 @@
import java.util.concurrent.Callable;
import javax.ws.rs.NotAuthorizedException;
import org.junit.After;
+import org.junit.Assume;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
@@ -223,6 +227,27 @@ public void testPushHubPublicImageWithAuth() throws Exception {
client.push(HUB_PUBLIC_IMAGE);
}
+
+ @Test
+ public void testPushGCRPrivateImageWithAuth() throws Exception {
+ String gcrPrivateImage = getenv("GCR_PRIVATE_IMAGE");
+ Assume.assumeTrue("WARNING: Integration test for GCR has not run. "
+ + "Set env variable GCR_PRIVATE_IMAGE "
+ + "(e.g. GCR_PRIVATE_IMAGE=us.gcr.io/my-project/busybox) to run.",
+ gcrPrivateImage != null);
+
+ final String dockerDirectory = Resources.getResource("dockerDirectory").getPath();
+ RegistryAuthSupplier registryAuthSupplier = new GoogleContainerRegistryAuthSupplier();
+ final DockerClient client = DefaultDockerClient
+ .fromEnv()
+ .registryAuthSupplier(registryAuthSupplier)
+ .build();
+
+ client.build(Paths.get(dockerDirectory), gcrPrivateImage);
+ client.push(gcrPrivateImage);
+ client.pull(gcrPrivateImage);
+ }
+
@Test
public void testPushHubPrivateImageWithAuth() throws Exception {
// Push an image to a private repo on Docker Hub and check it succeeds