com.spotify.docker.client.DockerConfigReader.java Source code

Java tutorial

Introduction

Here is the source code for com.spotify.docker.client.DockerConfigReader.java

Source

/*-
 * -\-\-
 * 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 com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableCollection;
import com.spotify.docker.client.messages.DockerCredentialHelperAuth;
import com.spotify.docker.client.messages.RegistryAuth;
import com.spotify.docker.client.messages.RegistryConfigs;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DockerConfigReader {
    private static final Logger LOG = LoggerFactory.getLogger(DockerConfigReader.class);

    private static final ObjectMapper MAPPER = ObjectMapperProvider.objectMapper();

    /**
     * Parse the contents of the config file and generate all possible
     * {@link RegistryAuth}s, which are bundled into a {@link RegistryConfigs} instance.
     * @param configPath Path to config file.
     * @return All registry configs that can be generated from the config file
     * @throws IOException If the file cannot be read, or its JSON cannot be parsed
     * @deprecated Use {@link #authForAllRegistries(Path)} instead.
     */
    @Deprecated
    public RegistryConfigs fromConfig(final Path configPath) throws IOException {
        return authForAllRegistries(configPath);
    }

    /**
     * Returns the RegistryAuth for the config file for the given registry server name.
     *
     * @throws IllegalArgumentException if the config file does not contain registry auth info for the
     *                                  registry
     * @deprecated In favor of {@link #authForRegistry(Path, String)}
     */
    @Deprecated
    public RegistryAuth fromConfig(final Path configPath, final String serverAddress) throws IOException {
        return authForRegistry(configPath, serverAddress);
    }

    /**
     * Return a single RegistryAuth from the default config file.
     * If there is only one, it'll be that one.
     *
     * @return Some registry auth value.
     */
    public RegistryAuth anyRegistryAuth() throws IOException {
        return anyRegistryAuth(defaultConfigPath());
    }

    /**
     * Return a single RegistryAuth from the config file.
     * If there are multiple RegistryAuth entries, which entry is returned from this method
     * depends on hashing and should not be considered reliable.
     * If there is only one entry, however, that will be the one returned. This is the
     * primary use of this method, as a useful way to extract a RegistryAuth during testing.
     * In that environment, the contents of the config file are known and controlled. In a
     * production environment, the contents of the config file are less predictable.
     *
     * @param configPath Path to the docker config file.
     * @return Some registry auth value.
     */
    @VisibleForTesting
    RegistryAuth anyRegistryAuth(final Path configPath) throws IOException {
        final ImmutableCollection<RegistryAuth> registryAuths = authForAllRegistries(configPath).configs().values();
        return registryAuths.isEmpty() ? RegistryAuth.builder().build() : registryAuths.iterator().next();
    }

    /**
     * Parse the contents of the config file and generate all possible
     * {@link RegistryAuth}s, which are bundled into a {@link RegistryConfigs} instance.
     * @param configPath Path to config file.
     * @return All registry auths that can be generated from the config file
     * @throws IOException If the file cannot be read, or its JSON cannot be parsed
     */
    public RegistryConfigs authForAllRegistries(final Path configPath) throws IOException {
        checkNotNull(configPath);

        final DockerConfig config = MAPPER.readValue(configPath.toFile(), DockerConfig.class);
        if (config == null) {
            return RegistryConfigs.empty();
        }

        final RegistryConfigs.Builder registryConfigsBuilder = RegistryConfigs.builder();

        final Map<String, String> credHelpers = config.credHelpers();
        final boolean hasCredHelpers = credHelpers != null && !credHelpers.isEmpty();
        final Map<String, RegistryAuth> auths = config.auths();
        final boolean hasAuths = auths != null && !auths.isEmpty();
        final String credsStore = config.credsStore();
        final boolean hasCredsStore = credsStore != null;

        // First use the credHelpers, if there are any
        if (hasCredHelpers) {
            for (final Map.Entry<String, String> credHelpersEntry : credHelpers.entrySet()) {
                final String registry = credHelpersEntry.getKey();
                final String aCredsStore = credHelpersEntry.getValue();
                registryConfigsBuilder.addConfig(registry, authWithCredentialHelper(aCredsStore, registry));
            }
        }

        // If there are any objects in "auths", they could take two forms.
        // Older auths will map registry keys to objects with "auth" values, sometimes emails.
        // Newer auths will map registry keys to empty objects. They expect you
        // to use the credsStore to authenticate.
        if (hasAuths) {
            // We will use this empty RegistryAuth to check for empty auth values
            final RegistryAuth empty = RegistryAuth.builder().build();

            for (final Map.Entry<String, RegistryAuth> authEntry : auths.entrySet()) {
                final String registry = authEntry.getKey();
                final RegistryAuth registryAuth = authEntry.getValue();
                if (registryAuth == null || registryAuth.equals(empty)) {
                    // We have an empty object. Can we use credsStore?
                    if (hasCredsStore) {
                        registryConfigsBuilder.addConfig(registry, authWithCredentialHelper(credsStore, registry));
                    } // no else clause. If we can't fall back to credsStore, we can't auth.
                } else {
                    // The auth object isn't empty.
                    // We need to add the registry to its properties, then
                    // add it to the RegistryConfigs
                    registryConfigsBuilder.addConfig(registry,
                            registryAuth.toBuilder().serverAddress(registry).build());
                }
            }
        }

        // If there are no credHelpers or auths or credsStore, then the
        // config may be in a very old format. There aren't any keys for different
        // sections. The file is just a map of registries to auths.
        // In other words, it looks like a RegistryConfigs.
        // If we can map it to one, we'll return it.
        if (!(hasAuths || hasCredHelpers || hasCredsStore)) {
            try {
                return MAPPER.readValue(configPath.toFile(), RegistryConfigs.class);
            } catch (IOException ignored) {
                // Looks like that failed to parse.
                // Eat the exception, fall through, and return empty object.
            }
        }

        return registryConfigsBuilder.build();
    }

    /**
     * Generate {@link RegistryAuth} for the registry.
     *
     * @param configPath Path to the docker config file
     * @param registry Docker registry for which to generate auth
     * @return The generated authentication object
     */
    public RegistryAuth authForRegistry(final Path configPath, final String registry) throws IOException {
        checkNotNull(configPath);
        checkNotNull(registry);

        final DockerConfig config = MAPPER.readValue(configPath.toFile(), DockerConfig.class);
        if (config == null) {
            return RegistryAuth.builder().build();
        }

        final RegistryAuth registryAuth = authForRegistry(config, registry);
        if (registryAuth != null) {
            return registryAuth;
        }
        // If the given server address didn't have a protocol try adding a protocol to the address.
        // This handles cases where older versions of Docker included the protocol when writing
        // auth tokens to config.json.
        try {
            final URI serverAddressUri = new URI(registry);
            if (serverAddressUri.getScheme() == null) {
                for (String proto : Arrays.asList("https://", "http://")) {
                    final RegistryAuth protoRegistryAuth = authForRegistry(config, proto + registry);
                    if (protoRegistryAuth != null) {
                        return protoRegistryAuth;
                    }
                }
            }
        } catch (URISyntaxException e) {
            // Nothing to do, just let this fall through below
        }

        throw new IllegalArgumentException(
                "registry \"" + registry + "\" does not appear in config file at " + configPath);
    }

    private RegistryAuth authForRegistry(final DockerConfig config, final String registry) throws IOException {

        // If the registry shows up in "auths", return it
        final Map<String, RegistryAuth> auths = config.auths();
        if (auths != null && auths.get(registry) != null) {
            return auths.get(registry).toBuilder().serverAddress(registry).build();
        }

        // Else, we use a credential helper.
        final String credsStore = getCredentialStore(config, registry);
        if (credsStore != null) {
            return authWithCredentialHelper(credsStore, registry);
        }

        return null;
    }

    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;
        }
    }

    /**
     * Obtain auth using a credential helper.
     * @param credsStore The name of the credential helper
     * @param registry The registry for which we need to obtain auth
     * @return A RegistryAuth object with a username, password, and server.
     * @throws IOException This method attempts to execute
     *                     "docker-credential-" + credsStore + " get". If you don't have the
     *                     proper credential helper installed and on your path, this
     *                     will fail.
     */
    private RegistryAuth authWithCredentialHelper(final String credsStore, final String registry)
            throws IOException {
        final DockerCredentialHelperAuth dockerCredentialHelperAuth = DockerCredentialHelper.get(credsStore,
                registry);
        return dockerCredentialHelperAuth == null ? null : dockerCredentialHelperAuth.toRegistryAuth();
    }

    private String getCredentialStore(final DockerConfig config, final String registry) {
        checkNotNull(config, "Docker config cannot be null");
        checkNotNull(registry, "registry cannot be null");

        // Check for the registry in the credHelpers map first.
        // If it isn't there, default to credsStore.
        final Map<String, String> credHelpers = config.credHelpers();
        return (credHelpers != null && credHelpers.containsKey(registry)) ? credHelpers.get(registry)
                : config.credsStore();
    }
}