From 675cf088deeb62d361fe63a95ae9c6b56de4821b Mon Sep 17 00:00:00 2001 From: David Morhovich Date: Wed, 17 May 2017 17:20:19 -0400 Subject: [PATCH 1/8] Adds support for RegistryAuthSupplier. Adds support for RegistryAuthSupplier. Client code can consume this using DefaultDockerClient builder. Provides support for authenticating against GCR to allow pushes/pulls from GCR. --- pom.xml | 5 + .../docker/client/DefaultDockerClient.java | 47 +++++-- .../docker/client/DockerConfigReader.java | 117 ++++++++++++++++++ .../client/NoOpRegistryAuthSupplier.java | 53 ++++++++ .../docker/client/gcr/GCloudProcess.java | 31 +++++ .../GoogleContainerRegistryAuthSupplier.java | 67 ++++++++++ .../GoogleContainerRegistryCredRefresher.java | 47 +++++++ .../docker/client/messages/RegistryAuth.java | 94 ++------------ .../client/messages/RegistryAuthSupplier.java | 37 ++++++ .../client/DefaultDockerClientTest.java | 2 - .../client/DefaultDockerClientUnitTest.java | 19 +++ .../client/NoOpRegistryAuthSupplierTest.java | 46 +++++++ ...ogleContainerRegistryAuthSupplierTest.java | 82 ++++++++++++ ...gleContainerRegistryCredRefresherTest.java | 80 ++++++++++++ .../com/spotify/docker/it/PushPullIT.java | 25 ++++ 15 files changed, 657 insertions(+), 95 deletions(-) create mode 100644 src/main/java/com/spotify/docker/client/DockerConfigReader.java create mode 100644 src/main/java/com/spotify/docker/client/NoOpRegistryAuthSupplier.java create mode 100644 src/main/java/com/spotify/docker/client/gcr/GCloudProcess.java create mode 100644 src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java create mode 100644 src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresher.java create mode 100644 src/main/java/com/spotify/docker/client/messages/RegistryAuthSupplier.java create mode 100644 src/test/java/com/spotify/docker/client/NoOpRegistryAuthSupplierTest.java create mode 100644 src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java create mode 100644 src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java diff --git a/pom.xml b/pom.xml index 4b3ed640c..7bb74293d 100644 --- a/pom.xml +++ b/pom.xml @@ -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..3539b676a 100644 --- a/src/main/java/com/spotify/docker/client/DefaultDockerClient.java +++ b/src/main/java/com/spotify/docker/client/DefaultDockerClient.java @@ -97,6 +97,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 +325,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 +399,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(null); + } else { + this.registryAuthSupplier = builder.registryAuthSupplier; + } this.client = ClientBuilder.newBuilder() .withConfig(config) @@ -1165,17 +1171,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 +1192,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 +1246,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 @@ -1349,6 +1354,7 @@ public String build(final Path directory, final String name, final String docker WebTarget resource = noTimeoutResource().path("build"); + final RegistryAuth registryAuth = registryAuthSupplier.authFor(name); for (final BuildParam param : params) { resource = resource.queryParam(param.name(), param.value()); } @@ -1784,8 +1790,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 +2519,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 +2531,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() { @@ -2647,9 +2657,24 @@ public RegistryAuth registryAuth() { * * @param registryAuth RegistryAuth object * @return Builder + * + * @deprecated in favor of registryAuthSupplier */ + @Deprecated public Builder registryAuth(final RegistryAuth registryAuth) { + if (this.registryAuthSupplier != null) { + throw new IllegalStateException(ERROR_MESSAGE); + } this.registryAuth = registryAuth; + this.registryAuthSupplier = new NoOpRegistryAuthSupplier(registryAuth); + return this; + } + + public Builder registryAuthSupplier(final RegistryAuthSupplier registryAuthSupplier) { + if (this.registryAuthSupplier != null) { + throw new IllegalStateException(ERROR_MESSAGE); + } + this.registryAuthSupplier = registryAuthSupplier; return this; } 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..258f10e7b --- /dev/null +++ b/src/main/java/com/spotify/docker/client/DockerConfigReader.java @@ -0,0 +1,117 @@ +/*- + * -\-\- + * 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.spotify.docker.client.messages.RegistryAuth; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Iterator; +import org.glassfish.jersey.internal.util.Base64; +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(); + + + public RegistryAuth fromComfig(Path configPath, String serverAddress) throws IOException { + return parseDockerConfig(configPath, serverAddress).build(); + } + + public RegistryAuth.Builder parseDockerConfig(final Path configPath, String serverAddress) + throws IOException { + checkNotNull(configPath); + final RegistryAuth.Builder authBuilder = RegistryAuth.builder(); + final JsonNode authJson = this.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); + } + } + + 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; + } + + 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; + } + } + + public JsonNode extractAuthJson(final Path configPath) throws IOException { + final JsonNode config = MAPPER.readTree(configPath.toFile()); + + if (config.has("auths")) { + return config.get("auths"); + } + + return 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..d716fcb00 --- /dev/null +++ b/src/main/java/com/spotify/docker/client/NoOpRegistryAuthSupplier.java @@ -0,0 +1,53 @@ +/*- + * -\-\- + * 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; + +/** + * Wraps a RegistryAuth with the RegistryAuthSupplier interface. + */ +public class NoOpRegistryAuthSupplier implements RegistryAuthSupplier { + + private final RegistryAuth registryAuth; + + public NoOpRegistryAuthSupplier(RegistryAuth registryAuth) { + this.registryAuth = registryAuth; + } + + public NoOpRegistryAuthSupplier() { + registryAuth = null; + + } + + @Override + public RegistryAuth authFor(String imageName) throws DockerException { + return registryAuth; + } + + @Override + public RegistryAuth authForSwarm() { + return registryAuth; + } + +} 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..6a5f48d47 --- /dev/null +++ b/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java @@ -0,0 +1,67 @@ +/*- + * -\-\- + * 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 java.io.IOException; +import java.nio.file.Path; +import org.apache.commons.lang.NotImplementedException; + +public class GoogleContainerRegistryAuthSupplier implements RegistryAuthSupplier { + + private final DockerConfigReader dockerCfgReader; + private final GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher; + private final Path configPath; + + public GoogleContainerRegistryAuthSupplier() { + this(new DockerConfigReader(), + new GoogleContainerRegistryCredRefresher(new GCloudProcess()), + new DockerConfigReader().defaultConfigPath()); + } + + public GoogleContainerRegistryAuthSupplier(DockerConfigReader dockerCfgReader, + GoogleContainerRegistryCredRefresher + googleContainerRegistryCredRefresher, + Path configPath) { + this.dockerCfgReader = dockerCfgReader; + this.googleContainerRegistryCredRefresher = googleContainerRegistryCredRefresher; + this.configPath = configPath; + } + + @Override + public RegistryAuth authFor(String imageName) throws DockerException { + try { + String registryName = "https://" + imageName.split("/")[0]; + googleContainerRegistryCredRefresher.refresh(); + return dockerCfgReader.fromComfig(configPath, registryName); + } catch (IOException ex) { + throw new DockerException(ex); + } + } + + @Override + public RegistryAuth authForSwarm() { + throw new NotImplementedException(); + } +} 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..820c894ba 100644 --- a/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java +++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java @@ -23,26 +23,21 @@ 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.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.DockerConfigReader; import com.spotify.docker.client.ObjectMapperProvider; 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; @@ -99,10 +94,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 */ + @Deprecated @SuppressWarnings("unused") public static Builder fromDockerConfig() throws IOException { - return parseDockerConfig(defaultConfigPath(), null); + DockerConfigReader dockerCfgReader = new DockerConfigReader(); + return dockerCfgReader.fromComfig(dockerCfgReader.defaultConfigPath(), null).toBuilder(); } /** @@ -116,7 +114,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 + .fromComfig(dockerCfgReader.defaultConfigPath(), serverAddress).toBuilder(); } /** @@ -129,7 +129,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.fromComfig(configPath, null).toBuilder(); } /** @@ -144,81 +145,10 @@ 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.fromComfig(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; - } else { - throw new RuntimeException( - "Could not find a docker config. Please run 'docker login' to create one"); - } - } - - 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); - } - } - - 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"); - } - - return config; - } public static Builder builder() { return new AutoValue_RegistryAuth.Builder() 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..6662cc9e6 --- /dev/null +++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuthSupplier.java @@ -0,0 +1,37 @@ +/*- + * -\-\- + * 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(); +} diff --git a/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java b/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java index 7baf14309..0197a556e 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; diff --git a/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java b/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java index 1f24f6c82..afb896ed8 100644 --- a/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java +++ b/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java @@ -31,8 +31,11 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +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 java.io.IOException; import java.util.HashSet; import java.util.Set; @@ -43,7 +46,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 @@ -71,6 +76,9 @@ 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(); @@ -188,4 +196,15 @@ private static Set childrenTextNodes(ArrayNode arrayNode) { return texts; } + @Test + 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(); + } } 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..a8e5ec508 --- /dev/null +++ b/src/test/java/com/spotify/docker/client/NoOpRegistryAuthSupplierTest.java @@ -0,0 +1,46 @@ +/*- + * -\-\- + * 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); + assertEquals(registryAuth, noOpRegistryAuthSupplier.authFor("doesn't matter")); + } + + @Test + public void authForReturnsNullForEmptyConstructor() throws DockerException { + NoOpRegistryAuthSupplier noOpRegistryAuthSupplier = new NoOpRegistryAuthSupplier(); + assertNull(noOpRegistryAuthSupplier.authFor("any")); + } + +} \ No newline at end of file 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..acab83478 --- /dev/null +++ b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java @@ -0,0 +1,82 @@ +/*- + * -\-\- + * 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.gcr.GoogleContainerRegistryAuthSupplier; +import com.spotify.docker.client.gcr.GoogleContainerRegistryCredRefresher; +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).fromComfig(any(Path.class), anyString()); + } + + @Test + public void authForReturnsRegisteryAuthThatMatchesRegisteryName() 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.fromComfig(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..b8570af4e --- /dev/null +++ b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java @@ -0,0 +1,80 @@ +/*- + * -\-\- + * 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 com.spotify.docker.client.gcr.GCloudProcess; +import com.spotify.docker.client.gcr.GoogleContainerRegistryCredRefresher; +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/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 From c47cb5c1c05b6ab0b7673236adc900a1b49c0ca8 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Mon, 22 May 2017 14:15:01 -0400 Subject: [PATCH 2/8] add authForBuild() method to RegistryAuthSupplier the Build method in the Docker Remote API allows for passing *all* of the RegistryAuth structs that the client knows about, this updates the RegistryAuthSupplier interface to match that. --- .../docker/client/DefaultDockerClient.java | 28 +++--- .../docker/client/DockerConfigReader.java | 37 +++++++- .../client/NoOpRegistryAuthSupplier.java | 12 ++- .../GoogleContainerRegistryAuthSupplier.java | 31 +++++-- .../docker/client/messages/RegistryAuth.java | 46 ++++++--- .../client/messages/RegistryAuthSupplier.java | 6 +- .../client/messages/RegistryConfigs.java | 89 +++++------------- .../client/DefaultDockerClientUnitTest.java | 93 +++++++++++++++++++ ...hTest.java => DockerConfigReaderTest.java} | 80 +++++++++++----- .../client/NoOpRegistryAuthSupplierTest.java | 7 +- ...ogleContainerRegistryAuthSupplierTest.java | 8 +- ...gleContainerRegistryCredRefresherTest.java | 4 +- 12 files changed, 294 insertions(+), 147 deletions(-) rename src/test/java/com/spotify/docker/client/{messages/RegistryAuthTest.java => DockerConfigReaderTest.java} (60%) diff --git a/src/main/java/com/spotify/docker/client/DefaultDockerClient.java b/src/main/java/com/spotify/docker/client/DefaultDockerClient.java index 3539b676a..d27436e61 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; @@ -401,7 +402,7 @@ protected DefaultDockerClient(final Builder builder) { if (builder.registryAuthSupplier == null) { - this.registryAuthSupplier = new NoOpRegistryAuthSupplier(null); + this.registryAuthSupplier = new NoOpRegistryAuthSupplier(); } else { this.registryAuthSupplier = builder.registryAuthSupplier; } @@ -1354,7 +1355,6 @@ public String build(final Path directory, final String name, final String docker WebTarget resource = noTimeoutResource().path("build"); - final RegistryAuth registryAuth = registryAuthSupplier.authFor(name); for (final BuildParam param : params) { resource = resource.queryParam(param.name(), param.value()); } @@ -1366,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()); @@ -2666,7 +2655,16 @@ public Builder registryAuth(final RegistryAuth registryAuth) { throw new IllegalStateException(ERROR_MESSAGE); } this.registryAuth = registryAuth; - this.registryAuthSupplier = new NoOpRegistryAuthSupplier(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; } diff --git a/src/main/java/com/spotify/docker/client/DockerConfigReader.java b/src/main/java/com/spotify/docker/client/DockerConfigReader.java index 258f10e7b..24470d23a 100644 --- a/src/main/java/com/spotify/docker/client/DockerConfigReader.java +++ b/src/main/java/com/spotify/docker/client/DockerConfigReader.java @@ -25,7 +25,10 @@ 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; @@ -40,12 +43,25 @@ public class DockerConfigReader { 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 fromComfig(Path configPath, String serverAddress) throws IOException { + public RegistryAuth fromConfig(Path configPath, String serverAddress) throws IOException { return parseDockerConfig(configPath, serverAddress).build(); } - public RegistryAuth.Builder parseDockerConfig(final Path configPath, String 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).build(); + } + + private RegistryAuth.Builder parseDockerConfig(final Path configPath, String serverAddress) throws IOException { checkNotNull(configPath); final RegistryAuth.Builder authBuilder = RegistryAuth.builder(); @@ -91,6 +107,11 @@ public RegistryAuth.Builder parseDockerConfig(final Path configPath, String serv return authBuilder; } + 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"); @@ -105,13 +126,19 @@ public Path defaultConfigPath() { } } - public JsonNode extractAuthJson(final Path configPath) throws IOException { + 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")) { - return config.get("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 config; + 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 index d716fcb00..eebd534e6 100644 --- a/src/main/java/com/spotify/docker/client/NoOpRegistryAuthSupplier.java +++ b/src/main/java/com/spotify/docker/client/NoOpRegistryAuthSupplier.java @@ -23,6 +23,7 @@ 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. @@ -30,14 +31,17 @@ public class NoOpRegistryAuthSupplier implements RegistryAuthSupplier { private final RegistryAuth registryAuth; + private final RegistryConfigs configsForBuild; - public NoOpRegistryAuthSupplier(RegistryAuth registryAuth) { + public NoOpRegistryAuthSupplier(final RegistryAuth registryAuth, + final RegistryConfigs configsForBuild) { this.registryAuth = registryAuth; + this.configsForBuild = configsForBuild; } public NoOpRegistryAuthSupplier() { registryAuth = null; - + configsForBuild = null; } @Override @@ -50,4 +54,8 @@ public RegistryAuth authForSwarm() { return registryAuth; } + @Override + public RegistryConfigs authForBuild() { + return configsForBuild; + } } diff --git a/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java b/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java index 6a5f48d47..ace09d900 100644 --- a/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java +++ b/src/main/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplier.java @@ -24,14 +24,15 @@ 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 dockerCfgReader; - private final GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher; + private final DockerConfigReader reader; + private final GoogleContainerRegistryCredRefresher refresher; private final Path configPath; public GoogleContainerRegistryAuthSupplier() { @@ -40,12 +41,12 @@ public GoogleContainerRegistryAuthSupplier() { new DockerConfigReader().defaultConfigPath()); } - public GoogleContainerRegistryAuthSupplier(DockerConfigReader dockerCfgReader, - GoogleContainerRegistryCredRefresher - googleContainerRegistryCredRefresher, - Path configPath) { - this.dockerCfgReader = dockerCfgReader; - this.googleContainerRegistryCredRefresher = googleContainerRegistryCredRefresher; + public GoogleContainerRegistryAuthSupplier( + final DockerConfigReader reader, + final GoogleContainerRegistryCredRefresher refresher, + final Path configPath) { + this.reader = reader; + this.refresher = refresher; this.configPath = configPath; } @@ -53,8 +54,8 @@ public GoogleContainerRegistryAuthSupplier(DockerConfigReader dockerCfgReader, public RegistryAuth authFor(String imageName) throws DockerException { try { String registryName = "https://" + imageName.split("/")[0]; - googleContainerRegistryCredRefresher.refresh(); - return dockerCfgReader.fromComfig(configPath, registryName); + refresher.refresh(); + return reader.fromConfig(configPath, registryName); } catch (IOException ex) { throw new DockerException(ex); } @@ -64,4 +65,14 @@ public RegistryAuth authFor(String imageName) throws DockerException { 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/messages/RegistryAuth.java b/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java index 820c894ba..e94ccf92c 100644 --- a/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java +++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java @@ -22,9 +22,9 @@ 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 com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.auto.value.AutoValue; @@ -32,21 +32,15 @@ import com.google.common.base.MoreObjects; import com.spotify.docker.client.DockerConfigReader; import com.spotify.docker.client.ObjectMapperProvider; - import java.io.IOException; import java.nio.file.Path; - import javax.annotation.Nullable; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.glassfish.jersey.internal.util.Base64; @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"; @@ -69,6 +63,7 @@ public abstract class RegistryAuth { @JsonProperty("Email") public abstract String email(); + @Nullable @JsonProperty("ServerAddress") public abstract String serverAddress(); @@ -97,10 +92,10 @@ public final String toString() { * @deprecated in favor of registryAuthSupplier */ @Deprecated - @SuppressWarnings("unused") + @SuppressWarnings({"deprecated", "unused"}) public static Builder fromDockerConfig() throws IOException { DockerConfigReader dockerCfgReader = new DockerConfigReader(); - return dockerCfgReader.fromComfig(dockerCfgReader.defaultConfigPath(), null).toBuilder(); + return dockerCfgReader.fromFirstConfig(dockerCfgReader.defaultConfigPath()).toBuilder(); } /** @@ -116,7 +111,7 @@ public static Builder fromDockerConfig() throws IOException { public static Builder fromDockerConfig(final String serverAddress) throws IOException { DockerConfigReader dockerCfgReader = new DockerConfigReader(); return dockerCfgReader - .fromComfig(dockerCfgReader.defaultConfigPath(), serverAddress).toBuilder(); + .fromConfig(dockerCfgReader.defaultConfigPath(), serverAddress).toBuilder(); } /** @@ -130,7 +125,7 @@ public static Builder fromDockerConfig(final String serverAddress) throws IOExce @VisibleForTesting static Builder fromDockerConfig(final Path configPath) throws IOException { DockerConfigReader dockerCfgReader = new DockerConfigReader(); - return dockerCfgReader.fromComfig(configPath, null).toBuilder(); + return dockerCfgReader.fromConfig(configPath, null).toBuilder(); } /** @@ -146,9 +141,33 @@ static Builder fromDockerConfig(final Path configPath) throws IOException { static Builder fromDockerConfig(final Path configPath, final String serverAddress) throws IOException { DockerConfigReader dockerConfigReader = new DockerConfigReader(); - return dockerConfigReader.fromComfig(configPath, serverAddress).toBuilder(); + return dockerConfigReader.fromConfig(configPath, serverAddress).toBuilder(); } + @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) { + + if (auth != null && username == null && password == null) { + final String[] authParams = Base64.decodeAsString(auth).split(":"); + + if (authParams.length == 2) { + username = authParams[0].trim(); + password = authParams[1].trim(); + } + } + return builder() + .username(username) + .password(password) + .email(email) + .serverAddress(serverAddress) + .identityToken(identityToken) + .build(); + } public static Builder builder() { return new AutoValue_RegistryAuth.Builder() @@ -159,6 +178,7 @@ public static Builder 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 index 6662cc9e6..b0bae4da3 100644 --- a/src/main/java/com/spotify/docker/client/messages/RegistryAuthSupplier.java +++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuthSupplier.java @@ -23,6 +23,7 @@ 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]. */ @@ -33,5 +34,8 @@ public interface RegistryAuthSupplier { * to specific image]. It's unnecessary if it's not planned to use this AuthSupplier to pull * images for Swarm. */ - RegistryAuth authForSwarm(); + 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..81c45071a 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,33 @@ @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.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/DefaultDockerClientUnitTest.java b/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java index afb896ed8..e010d3088 100644 --- a/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java +++ b/src/test/java/com/spotify/docker/client/DefaultDockerClientUnitTest.java @@ -24,19 +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; @@ -74,6 +86,7 @@ public class DefaultDockerClientUnitTest { private final MockWebServer server = new MockWebServer(); + private DefaultDockerClient.Builder builder; @Rule @@ -150,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 { @@ -197,6 +222,7 @@ private static Set childrenTextNodes(ArrayNode arrayNode) { } @Test + @SuppressWarnings("deprecated") public void buildThrowsIfRegistryAuthandRegistryAuthSupplierAreBothSpecified() throws DockerCertificateException { thrown.expect(IllegalStateException.class); @@ -207,4 +233,71 @@ public void buildThrowsIfRegistryAuthandRegistryAuthSupplierAreBothSpecified() .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/messages/RegistryAuthTest.java b/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java similarity index 60% rename from src/test/java/com/spotify/docker/client/messages/RegistryAuthTest.java rename to src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java index cf4c0aa52..d00f4bab9 100644 --- a/src/test/java/com/spotify/docker/client/messages/RegistryAuthTest.java +++ b/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java @@ -2,7 +2,7 @@ * -\-\- * docker-client * -- - * Copyright (C) 2016 Spotify AB + * 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. @@ -18,25 +18,44 @@ * -/-/- */ -package com.spotify.docker.client.messages; +/* + * 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 com.google.common.io.Resources; -import com.spotify.docker.client.OsUtils; - +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.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; -public class RegistryAuthTest { +@SuppressWarnings("deprecated") +public class DockerConfigReaderTest { private static final RegistryAuth DOCKER_AUTH_CONFIG = RegistryAuth.builder() .serverAddress("https://index.docker.io/v1/") @@ -62,42 +81,44 @@ public class RegistryAuthTest { @Rule public ExpectedException expectedException = ExpectedException.none(); + private final DockerConfigReader reader = new DockerConfigReader(); + @Test public void testFromDockerConfig_FullConfig() throws Exception { - final RegistryAuth registryAuth = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/fullConfig.json")).build(); + 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 = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/fullDockerCfg")).build(); + final RegistryAuth registryAuth = reader.fromFirstConfig(getTestFilePath( + "dockerConfig/fullDockerCfg")); assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG)); } @Test public void testFromDockerConfig_IdentityToken() throws Exception { - final RegistryAuth authConfig = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/identityTokenConfig.json")).build(); + 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 = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/incompleteConfig.json")).build(); + final RegistryAuth registryAuth = reader.fromFirstConfig(getTestFilePath( + "dockerConfig/incompleteConfig.json")); assertThat(registryAuth, equalTo(EMPTY_AUTH_CONFIG)); } @Test public void testFromDockerConfig_WrongConfigs() throws Exception { - final RegistryAuth registryAuth1 = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/wrongConfig1.json")).build(); + final RegistryAuth registryAuth1 = reader.fromFirstConfig(getTestFilePath( + "dockerConfig/wrongConfig1.json")); assertThat(registryAuth1, equalTo(EMPTY_AUTH_CONFIG)); - final RegistryAuth registryAuth2 = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/wrongConfig2.json")).build(); + final RegistryAuth registryAuth2 = reader.fromFirstConfig(getTestFilePath( + "dockerConfig/wrongConfig2.json")); assertThat(registryAuth2, equalTo(EMPTY_AUTH_CONFIG)); } @@ -105,16 +126,16 @@ public void testFromDockerConfig_WrongConfigs() throws Exception { public void testFromDockerConfig_MissingConfigFile() throws Exception { final Path randomPath = Paths.get(RandomStringUtils.randomAlphanumeric(16) + ".json"); expectedException.expect(FileNotFoundException.class); - RegistryAuth.fromDockerConfig(randomPath).build(); + reader.fromFirstConfig(randomPath); } @Test public void testFromDockerConfig_MultiConfig() throws Exception { - final RegistryAuth myDockParsed = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/multiConfig.json"), "https://narnia.mydock.io/v1/").build(); + final RegistryAuth myDockParsed = reader.fromConfig(getTestFilePath( + "dockerConfig/multiConfig.json"), "https://narnia.mydock.io/v1/"); assertThat(myDockParsed, equalTo(MY_AUTH_CONFIG)); - final RegistryAuth dockerIoParsed = RegistryAuth.fromDockerConfig(getTestFilePath( - "dockerConfig/multiConfig.json"), "https://index.docker.io/v1/").build(); + final RegistryAuth dockerIoParsed = reader.fromConfig(getTestFilePath( + "dockerConfig/multiConfig.json"), "https://index.docker.io/v1/"); assertThat(dockerIoParsed, equalTo(DOCKER_AUTH_CONFIG)); } @@ -127,11 +148,22 @@ private static Path getTestFilePath(final String path) { } private static Path getWindowsPath(final String path) { - final URL resource = RegistryAuthTest.class.getResource("/" + 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 index a8e5ec508..c3629be16 100644 --- a/src/test/java/com/spotify/docker/client/NoOpRegistryAuthSupplierTest.java +++ b/src/test/java/com/spotify/docker/client/NoOpRegistryAuthSupplierTest.java @@ -33,7 +33,8 @@ public class NoOpRegistryAuthSupplierTest { @Test public void authForReturnsWrappedAuthRegistry() throws DockerException { RegistryAuth registryAuth = mock(RegistryAuth.class); - NoOpRegistryAuthSupplier noOpRegistryAuthSupplier = new NoOpRegistryAuthSupplier(registryAuth); + NoOpRegistryAuthSupplier noOpRegistryAuthSupplier = new NoOpRegistryAuthSupplier(registryAuth, + null); assertEquals(registryAuth, noOpRegistryAuthSupplier.authFor("doesn't matter")); } @@ -41,6 +42,6 @@ public void authForReturnsWrappedAuthRegistry() throws DockerException { public void authForReturnsNullForEmptyConstructor() throws DockerException { NoOpRegistryAuthSupplier noOpRegistryAuthSupplier = new NoOpRegistryAuthSupplier(); assertNull(noOpRegistryAuthSupplier.authFor("any")); + assertNull(noOpRegistryAuthSupplier.authForBuild()); } - -} \ No newline at end of file +} diff --git a/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java index acab83478..8c32c768b 100644 --- a/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java +++ b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryAuthSupplierTest.java @@ -29,8 +29,6 @@ import static org.mockito.Mockito.when; import com.spotify.docker.client.DockerConfigReader; -import com.spotify.docker.client.gcr.GoogleContainerRegistryAuthSupplier; -import com.spotify.docker.client.gcr.GoogleContainerRegistryCredRefresher; import com.spotify.docker.client.messages.RegistryAuth; import java.nio.file.Path; import org.junit.Test; @@ -52,11 +50,11 @@ public void authForRefreshesCredsBeforeReadingConfigFile() throws Exception { googleContainerRegistryAuthSupplier.authFor("us.gcr.io/awesome-project/example-image"); inOrder.verify(googleContainerRegistryCredRefresher).refresh(); - inOrder.verify(dockerCfgReader).fromComfig(any(Path.class), anyString()); + inOrder.verify(dockerCfgReader).fromConfig(any(Path.class), anyString()); } @Test - public void authForReturnsRegisteryAuthThatMatchesRegisteryName() throws Exception { + public void authForReturnsRegistryAuthThatMatchesRegistryName() throws Exception { DockerConfigReader dockerCfgReader = mock(DockerConfigReader.class); GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher = mock(GoogleContainerRegistryCredRefresher.class); @@ -65,7 +63,7 @@ public void authForReturnsRegisteryAuthThatMatchesRegisteryName() throws Excepti RegistryAuth expected = RegistryAuth.builder().email("no@no.com").identityToken("authorific").build(); - when(dockerCfgReader.fromComfig(any(Path.class), eq("https://us.gcr.io"))).thenReturn(expected); + when(dockerCfgReader.fromConfig(any(Path.class), eq("https://us.gcr.io"))).thenReturn(expected); GoogleContainerRegistryAuthSupplier googleContainerRegistryAuthSupplier = new GoogleContainerRegistryAuthSupplier( diff --git a/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java index b8570af4e..3d89b4409 100644 --- a/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java +++ b/src/test/java/com/spotify/docker/client/gcr/GoogleContainerRegistryCredRefresherTest.java @@ -24,8 +24,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.spotify.docker.client.gcr.GCloudProcess; -import com.spotify.docker.client.gcr.GoogleContainerRegistryCredRefresher; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -74,7 +72,7 @@ public void refreshThrowsIfSuccessCodeIsntReturnedFromCommand() GoogleContainerRegistryCredRefresher googleContainerRegistryCredRefresher = new GoogleContainerRegistryCredRefresher(gcloudProcess); - googleContainerRegistryCredRefresher.refresh();; + googleContainerRegistryCredRefresher.refresh(); } } From e0c978c5bff19a667909f616786a1b1172edb08e Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Mon, 22 May 2017 16:22:10 -0400 Subject: [PATCH 3/8] consolidate config file parsing in DockerConfigReader use the same method for parsing the config file in both code paths --- .../docker/client/DockerConfigReader.java | 55 +++++------------ .../docker/client/messages/RegistryAuth.java | 19 ++---- .../docker/client/DockerConfigReaderTest.java | 59 +++++++++++++------ 3 files changed, 59 insertions(+), 74 deletions(-) diff --git a/src/main/java/com/spotify/docker/client/DockerConfigReader.java b/src/main/java/com/spotify/docker/client/DockerConfigReader.java index 24470d23a..b18b98a95 100644 --- a/src/main/java/com/spotify/docker/client/DockerConfigReader.java +++ b/src/main/java/com/spotify/docker/client/DockerConfigReader.java @@ -33,8 +33,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Iterator; -import org.glassfish.jersey.internal.util.Base64; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,7 +48,7 @@ public RegistryConfigs fromConfig(Path configPath) throws IOException { } public RegistryAuth fromConfig(Path configPath, String serverAddress) throws IOException { - return parseDockerConfig(configPath, serverAddress).build(); + return parseDockerConfig(configPath, serverAddress); } /** @@ -58,53 +57,29 @@ public RegistryAuth fromConfig(Path configPath, String serverAddress) throws IOE */ @Deprecated public RegistryAuth fromFirstConfig(Path configPath) throws IOException { - return parseDockerConfig(configPath, null).build(); + return parseDockerConfig(configPath, null); } - private RegistryAuth.Builder parseDockerConfig(final Path configPath, String serverAddress) + private RegistryAuth parseDockerConfig(final Path configPath, String serverAddress) throws IOException { checkNotNull(configPath); - final RegistryAuth.Builder authBuilder = RegistryAuth.builder(); - final JsonNode authJson = this.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); - } + final Map configs = parseDockerConfig(configPath).configs(); + if (serverAddress != null && configs.containsKey(serverAddress) ) { + return configs.get(serverAddress); } - 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; + if (isNullOrEmpty(serverAddress)) { + if (configs.isEmpty()) { + return RegistryAuth.builder().build(); } - } else { - LOG.warn("Could not find auth field for {}", serverAddress); - return authBuilder; - } - - if (serverAuth.has("email")) { - authBuilder.email(serverAuth.get("email").asText()); + 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(); } - return authBuilder; + throw new IllegalArgumentException( + "serverAddress=" + serverAddress + " does not appear in config file at " + configPath); } private RegistryConfigs parseDockerConfig(final Path configPath) throws IOException { 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 e94ccf92c..5fbabc014 100644 --- a/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java +++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java @@ -26,12 +26,10 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -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.DockerConfigReader; -import com.spotify.docker.client.ObjectMapperProvider; import java.io.IOException; import java.nio.file.Path; import javax.annotation.Nullable; @@ -41,13 +39,6 @@ @JsonAutoDetect(fieldVisibility = ANY, getterVisibility = NONE, setterVisibility = NONE) public abstract class RegistryAuth { - 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(); @@ -71,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()) @@ -152,7 +144,7 @@ public static RegistryAuth create(@JsonProperty("username") String username, @JsonProperty("identityToken") final String identityToken, @JsonProperty("auth") final String auth) { - if (auth != null && username == null && password == null) { + if (auth != null) { final String[] authParams = Base64.decodeAsString(auth).split(":"); if (authParams.length == 2) { @@ -170,10 +162,7 @@ public static RegistryAuth create(@JsonProperty("username") String username, } 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 diff --git a/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java b/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java index d00f4bab9..1fd5da2e7 100644 --- a/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java +++ b/src/test/java/com/spotify/docker/client/DockerConfigReaderTest.java @@ -41,6 +41,7 @@ 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; @@ -50,6 +51,8 @@ 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; @@ -72,12 +75,11 @@ public class DockerConfigReaderTest { .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(); - private static final RegistryAuth EMPTY_AUTH_CONFIG = RegistryAuth.builder().build(); - @Rule public ExpectedException expectedException = ExpectedException.none(); @@ -92,34 +94,52 @@ public void testFromDockerConfig_FullConfig() throws Exception { @Test public void testFromDockerConfig_FullDockerCfg() throws Exception { - final RegistryAuth registryAuth = reader.fromFirstConfig(getTestFilePath( - "dockerConfig/fullDockerCfg")); + 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")); + 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")); - assertThat(registryAuth, equalTo(EMPTY_AUTH_CONFIG)); + 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, equalTo(EMPTY_AUTH_CONFIG)); + final RegistryAuth registryAuth1 = + reader.fromFirstConfig(getTestFilePath("dockerConfig/wrongConfig1.json")); + assertThat(registryAuth1, is(emptyRegistryAuth())); - final RegistryAuth registryAuth2 = reader.fromFirstConfig(getTestFilePath( - "dockerConfig/wrongConfig2.json")); - assertThat(registryAuth2, equalTo(EMPTY_AUTH_CONFIG)); + 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 @@ -131,11 +151,12 @@ public void testFromDockerConfig_MissingConfigFile() throws Exception { @Test public void testFromDockerConfig_MultiConfig() throws Exception { - final RegistryAuth myDockParsed = reader.fromConfig(getTestFilePath( - "dockerConfig/multiConfig.json"), "https://narnia.mydock.io/v1/"); + 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(getTestFilePath( - "dockerConfig/multiConfig.json"), "https://index.docker.io/v1/"); + + final RegistryAuth dockerIoParsed = reader.fromConfig(path, "https://index.docker.io/v1/"); assertThat(dockerIoParsed, equalTo(DOCKER_AUTH_CONFIG)); } From 1621c3678f4b74a2eb29672a98a0840bd606f376 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Mon, 22 May 2017 16:42:44 -0400 Subject: [PATCH 4/8] update CHANGELOG about RegistryAuthSupplier and bump library version to 8.6.0 as significant new functionality is added/deprecated here --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ pom.xml | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) 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 7bb74293d..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 From 1b513bf225b87795b75f3d730ebdf8c66ae0e161 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Mon, 22 May 2017 16:50:56 -0400 Subject: [PATCH 5/8] ensure build() works as expected when dockerAuth is true --- .../com/spotify/docker/client/DefaultDockerClient.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/spotify/docker/client/DefaultDockerClient.java b/src/main/java/com/spotify/docker/client/DefaultDockerClient.java index d27436e61..054d3d5cf 100644 --- a/src/main/java/com/spotify/docker/client/DefaultDockerClient.java +++ b/src/main/java/com/spotify/docker/client/DefaultDockerClient.java @@ -2631,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,7 +2649,7 @@ public RegistryAuth registryAuth() { * @param registryAuth RegistryAuth object * @return Builder * - * @deprecated in favor of registryAuthSupplier + * @deprecated in favor of {@link #registryAuthSupplier(RegistryAuthSupplier)} */ @Deprecated public Builder registryAuth(final RegistryAuth registryAuth) { @@ -2677,9 +2679,9 @@ public Builder registryAuthSupplier(final RegistryAuthSupplier registryAuthSuppl } 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); } From 9cd8837039c90a615cda8ecfbccaa4b383c9e586 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Mon, 22 May 2017 16:52:54 -0400 Subject: [PATCH 6/8] fix Findbugs warning in RegistryConfigs --- .../com/spotify/docker/client/messages/RegistryConfigs.java | 3 +++ 1 file changed, 3 insertions(+) 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 81c45071a..1958112dd 100644 --- a/src/main/java/com/spotify/docker/client/messages/RegistryConfigs.java +++ b/src/main/java/com/spotify/docker/client/messages/RegistryConfigs.java @@ -75,6 +75,9 @@ public static RegistryConfigs create(final Map 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) From 246f6a1be93faf5a66c4a35b7311536b1aa749a5 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Tue, 23 May 2017 11:38:17 -0400 Subject: [PATCH 7/8] ensure RegistryAuth email is set in tests testAuth and testBadAuth is failing on Docker 1.9 and 1.10 because RegistryAuth.email is not set when we send it to the daemon --- .../spotify/docker/client/DefaultDockerClientTest.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java b/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java index 0197a556e..d5bd74c30 100644 --- a/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java +++ b/src/test/java/com/spotify/docker/client/DefaultDockerClientTest.java @@ -310,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' @@ -687,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); From 073ad2321c57b752c26ef1f1f743ef6ffcf8b079 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Tue, 23 May 2017 13:23:46 -0400 Subject: [PATCH 8/8] RegistryAuth: add method for building from auth token --- .../docker/client/messages/RegistryAuth.java | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) 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 5fbabc014..846cbca45 100644 --- a/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java +++ b/src/main/java/com/spotify/docker/client/messages/RegistryAuth.java @@ -144,23 +144,33 @@ public static RegistryAuth create(@JsonProperty("username") String username, @JsonProperty("identityToken") final String identityToken, @JsonProperty("auth") final String auth) { + final Builder builder; if (auth != null) { - final String[] authParams = Base64.decodeAsString(auth).split(":"); - - if (authParams.length == 2) { - username = authParams[0].trim(); - password = authParams[1].trim(); - } + builder = forAuthToken(auth); + } else { + builder = builder() + .username(username) + .password(password); } - return builder() - .username(username) - .password(password) + return builder .email(email) .serverAddress(serverAddress) .identityToken(identityToken) .build(); } + /** 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(":"); + + if (authParams.length != 2) { + return builder(); + } + return builder() + .username(authParams[0].trim()) + .password(authParams[1].trim()); + } + public static Builder builder() { return new AutoValue_RegistryAuth.Builder(); }