DockerConfigReaderTest.java

/*-
 * -\-\-
 * docker-client
 * --
 * Copyright (C) 2016 - 2017 Spotify AB
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * -/-/-
 */

/*
 * Copyright (c) 2017
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.spotify.docker.client;

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import com.google.common.io.Resources;
import com.spotify.docker.client.DockerCredentialHelper.CredentialHelperDelegate;
import com.spotify.docker.client.messages.DockerCredentialHelperAuth;
import com.spotify.docker.client.messages.RegistryAuth;
import com.spotify.docker.client.messages.RegistryConfigs;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.commons.lang.RandomStringUtils;
import org.hamcrest.CustomTypeSafeMatcher;
import org.hamcrest.Matcher;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

@SuppressWarnings("deprecated")
public class DockerConfigReaderTest {

  private static final RegistryAuth DOCKER_AUTH_CONFIG = RegistryAuth.builder()
      .serverAddress("https://index.docker.io/v1/")
      .username("dockerman")
      .password("sw4gy0lo")
      .email("dockerman@hub.com")
      .build();

  private static final RegistryAuth MY_AUTH_CONFIG = RegistryAuth.builder()
      .serverAddress("https://narnia.mydock.io/v1/")
      .username("megaman")
      .password("riffraff")
      .email("megaman@mydock.com")
      .build();

  private static final RegistryAuth IDENTITY_TOKEN_AUTH_CONFIG = RegistryAuth.builder()
      .email("dockerman@hub.com")
      .serverAddress("docker.customdomain.com")
      .identityToken("52ce5fd5-eb60-42bf-931f-5eeec128211a")
      .build();

  @Rule
  public ExpectedException expectedException = ExpectedException.none();

  private final DockerConfigReader reader = new DockerConfigReader();

  private CredentialHelperDelegate credentialHelperDelegate;

  @Before
  public void setup() {
    credentialHelperDelegate = mock(CredentialHelperDelegate.class);
    DockerCredentialHelper.setCredentialHelperDelegate(credentialHelperDelegate);
  }

  @AfterClass
  public static void afterClass() {
    DockerCredentialHelper.restoreSystemCredentialHelperDelegate();
  }

  @Test
  public void testFromDockerConfig_FullConfig() throws Exception {
    final RegistryAuth registryAuth =
        reader.anyRegistryAuth(getTestFilePath("dockerConfig/fullConfig.json"));
    assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG));
  }

  @Test
  public void testFromDockerConfig_FullDockerCfg() throws Exception {
    final RegistryAuth registryAuth =
        reader.anyRegistryAuth(getTestFilePath("dockerConfig/fullDockerCfg"));
    assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG));
  }

  @Test
  public void testFromDockerConfig_IdentityToken() throws Exception {
    final RegistryAuth authConfig =
        reader.anyRegistryAuth(getTestFilePath("dockerConfig/identityTokenConfig.json"));
    assertThat(authConfig, equalTo(IDENTITY_TOKEN_AUTH_CONFIG));
  }

  @Test
  public void testFromDockerConfig_IncompleteConfig() throws Exception {
    final RegistryAuth registryAuth =
        reader.anyRegistryAuth(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.anyRegistryAuth(getTestFilePath("dockerConfig/wrongConfig1.json"));
    assertThat(registryAuth1, is(emptyRegistryAuth()));

    final RegistryAuth registryAuth2 =
        reader.anyRegistryAuth(getTestFilePath("dockerConfig/wrongConfig2.json"));
    assertThat(registryAuth2, is(emptyRegistryAuth()));
  }

  private static Matcher<RegistryAuth> emptyRegistryAuth() {
    return new CustomTypeSafeMatcher<RegistryAuth>("an empty RegistryAuth") {
      @Override
      protected boolean matchesSafely(final RegistryAuth item) {
        return item.email() == null
               && item.identityToken() == null
               && item.password() == null
               && item.email() == null;
      }
    };
  }

  @Test
  public void testFromDockerConfig_MissingConfigFile() throws Exception {
    final Path randomPath = Paths.get(RandomStringUtils.randomAlphanumeric(16) + ".json");
    expectedException.expect(FileNotFoundException.class);
    reader.anyRegistryAuth(randomPath);
  }

  @Test
  public void testFromDockerConfig_MultiConfig() throws Exception {
    final Path path = getTestFilePath("dockerConfig/multiConfig.json");

    final RegistryAuth myDockParsed = reader.authForRegistry(path, "https://narnia.mydock.io/v1/");
    assertThat(myDockParsed, equalTo(MY_AUTH_CONFIG));

    final RegistryAuth dockerIoParsed = reader.authForRegistry(path, "https://index.docker.io/v1/");
    assertThat(dockerIoParsed, equalTo(DOCKER_AUTH_CONFIG));
  }

  @Test
  public void testFromDockerConfig_AddressProtocol() throws IOException {
    final Path path = getTestFilePath("dockerConfig/protocolMissing.json");

    // Server address matches exactly what's in the config file
    final RegistryAuth noProto = reader.authForRegistry(path, "docker.example.com");
    assertThat(noProto.serverAddress(), equalTo("docker.example.com"));

    // Server address doesn't have a protocol but the entry in the config file does (https)
    final RegistryAuth httpsProto = reader.authForRegistry(path, "repo.example.com");
    assertThat(httpsProto.serverAddress(), equalTo("https://repo.example.com"));

    // Server address doesn't have a protocol but the entry in the config file does (http)
    final RegistryAuth httpProto = reader.authForRegistry(path, "local.example.com");
    assertThat(httpProto.serverAddress(), equalTo("http://local.example.com"));
  }

  private static Path getTestFilePath(final String path) {
    if (OsUtils.isLinux() || OsUtils.isOsX()) {
      return getLinuxPath(path);
    } else {
      return getWindowsPath(path);
    }
  }

  private static Path getWindowsPath(final String path) {
    final URL resource = DockerConfigReaderTest.class.getResource("/" + path);
    return Paths.get(resource.getPath().substring(1));
  }

  private static Path getLinuxPath(final String path) {
    return Paths.get(Resources.getResource(path).getPath());
  }

  @Test
  public void testParseRegistryConfigs() throws Exception {
    final Path path = getTestFilePath("dockerConfig/multiConfig.json");
    final RegistryConfigs configs = reader.authForAllRegistries(path);

    assertThat(configs.configs(), allOf(
        hasEntry("https://index.docker.io/v1/", DOCKER_AUTH_CONFIG),
        hasEntry("https://narnia.mydock.io/v1/", MY_AUTH_CONFIG)
    ));
  }

  @Test
  public void testParseNoAuths() throws Exception {
    final Path path = getTestFilePath("dockerConfig/noAuths.json");
    final RegistryConfigs configs = reader.authForAllRegistries(path);
    assertThat(configs, equalTo(RegistryConfigs.empty()));
  }

  @Test
  public void testCredHelpers() throws Exception {
    final Path path = getTestFilePath("dockerConfig/credHelpers.json");

    final String registry1 = "https://foo.io";
    final String registry2 = "https://adventure.zone";
    final String registry3 = "https://beyond.zone";
    final DockerCredentialHelperAuth testAuth1 =
            DockerCredentialHelperAuth.create(
                    "cool user",
                    "cool password",
                    registry1
            );
    final DockerCredentialHelperAuth testAuth2 =
            DockerCredentialHelperAuth.create(
                    "taako",
                    "lupe",
                    registry2
            );

    when(credentialHelperDelegate.get("a-cred-helper", registry1)).thenReturn(testAuth1);
    when(credentialHelperDelegate.get("magic-missile", registry2)).thenReturn(testAuth2);
    when(credentialHelperDelegate.get("elusive-helper", registry3)).thenReturn(null);

    final RegistryConfigs expected = RegistryConfigs.builder()
            .addConfig(registry1, testAuth1.toRegistryAuth())
            .addConfig(registry2, testAuth2.toRegistryAuth())
            .build();
    final RegistryConfigs configs = reader.authForAllRegistries(path);

    assertThat(configs, is(expected));
  }

  @Test
  public void testCredsStoreAndCredHelpersAndAuth() throws Exception {
    final Path path = getTestFilePath("dockerConfig/credsStoreAndCredHelpersAndAuth.json");

    // This registry is in the file, in the "auths" sections
    final String registry1 = DOCKER_AUTH_CONFIG.serverAddress();
    assertThat(reader.authForRegistry(path, registry1), is(DOCKER_AUTH_CONFIG));

    // This registry is in the "credHelpers" section. It will give us a
    // credsStore value which will trigger our mock and give us testAuth2.
    final String registry2 = "https://adventure.zone";
    final DockerCredentialHelperAuth testAuth2 =
        DockerCredentialHelperAuth.create(
            "taako",
            "lupe",
            registry2
        );
    when(credentialHelperDelegate.get("magic-missile", registry2)).thenReturn(testAuth2);
    assertThat(reader.authForRegistry(path, registry2), is(testAuth2.toRegistryAuth()));

    // This registry is not in the "auths" or anywhere else. It should default
    // to using the credsStore value, and our mock will return testAuth3.
    final String registry3 = "https://rush.in";
    final DockerCredentialHelperAuth testAuth3 =
        DockerCredentialHelperAuth.create(
            "magnus",
            "julia",
            registry3
        );
    when(credentialHelperDelegate.get("starblaster", registry3)).thenReturn(testAuth3);
    assertThat(reader.authForRegistry(path, registry3), is(testAuth3.toRegistryAuth()));

    // Finally, when we get auths for *all* registries in the file, we only expect
    // auths for the two registries that are explicitly mentioned.
    // Since registry1 is in the "auths" and registry2 is in the "credHelpers",
    // we will see auths for them.
    final RegistryConfigs registryConfigs = RegistryConfigs.builder()
            .addConfig(registry2, testAuth2.toRegistryAuth())
            .addConfig(registry1, DOCKER_AUTH_CONFIG)
            .build();
    assertThat(reader.authForAllRegistries(path), is(registryConfigs));
  }

  @Test
  public void testDuplicateServerInAuthsAndCredHelpers() throws Exception {
    final Path path = getTestFilePath("dockerConfig/duplicateServerInAuthsAndCredHelpers.json");

    final String registry = "https://index.docker.io/v1/";
    final DockerCredentialHelperAuth testAuth =
        DockerCredentialHelperAuth.create(
            "dockerman",
            "sw4gy0lo",
            registry
        );
    when(credentialHelperDelegate.get("magic-missile", registry)).thenReturn(testAuth);

    // Test that for duplicate registries, credHelpers is preferred over auths.
    final RegistryConfigs registryConfigs = RegistryConfigs.builder()
        .addConfig(registry, testAuth.toRegistryAuth())
        .build();
    assertThat(reader.authForAllRegistries(path), is(registryConfigs));
  }

  @Test
  public void testConfigFromEnv() throws IOException {
    DockerHost.SystemDelegate systemDelegate = mock(DockerHost.SystemDelegate.class);
    when(systemDelegate.getenv("DOCKER_CONFIG"))
      .thenReturn("src/test/resources/dockerConfigFromEnv");
    when(systemDelegate.getProperty("os.name")).thenReturn(System.getProperty("os.name"));
    DockerHost.setSystemDelegate(systemDelegate);
    try {
      DockerConfigReader dockerConfigReader = new DockerConfigReader();
      Path path = dockerConfigReader.defaultConfigPath();
      assertThat(path.toString().replace("\\", "/"), 
          equalTo("src/test/resources/dockerConfigFromEnv/config.json"));

      final RegistryAuth registryAuth = dockerConfigReader.anyRegistryAuth();
      assertThat(registryAuth, equalTo(DOCKER_AUTH_CONFIG));
    } finally {
      DockerHost.restoreSystemDelegate();
    }
  }
}