/*-
* -\-\-
* 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.it;
import static com.google.common.base.Strings.isNullOrEmpty;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.hamcrest.CoreMatchers.isA;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Resources;
import com.spotify.docker.Polling;
import com.spotify.docker.client.DefaultDockerClient;
import com.spotify.docker.client.DockerClient;
import com.spotify.docker.client.DockerClient.BuildParam;
import com.spotify.docker.client.DockerClient.RemoveContainerParam;
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.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 java.nio.file.Paths;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import javax.ws.rs.NotAuthorizedException;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestName;
/**
* These integration tests check we can push images to and pull from a private registry running as a
* local container. Some tests in this class also check we can push to and pull from Docker Hub.
* N.B. Docker Hub rate limits pushes, so they might fail if you run them too often :)
*/
@SuppressWarnings("AbbreviationAsWordInName")
public class PushPullIT {
private static final int LONG_WAIT_SECONDS = 400;
private static final int SECONDS_TO_WAIT_BEFORE_KILL = 120;
private static final String REGISTRY_IMAGE = "registry:2";
private static final String REGISTRY_NAME = "registry";
private static final String LOCAL_AUTH_USERNAME = "testuser";
private static final String LOCAL_AUTH_PASSWORD = "testpassword";
private static final String LOCAL_IMAGE = "localhost:5000/testuser/test-image:latest";
private static final String LOCAL_AUTH_USERNAME_2 = "testusertwo";
private static final String LOCAL_AUTH_PASSWORD_2 = "testpasswordtwo";
private static final String LOCAL_IMAGE_2 = "localhost:5000/testusertwo/test-image:latest";
// Using a dummy individual's test account because organizations
// cannot have private repos on Docker Hub.
private static final String HUB_AUTH_USERNAME = "dxia4";
private static final String HUB_AUTH_PASSWORD = "03yDT6Yee4iFaggi";
private static final String HUB_PUBLIC_IMAGE =
"dxia4/docker-client-test-push-public-image-with-auth";
private static final String HUB_PRIVATE_IMAGE =
"dxia4/docker-client-test-push-private-image-with-auth";
private static final String HUB_AUTH_USERNAME2 = "dxia2";
private static final String HUB_AUTH_PASSWORD2 = "Tv38KLPd]M";
private static final String CIRROS_PRIVATE = "dxia/cirros-private";
private static final String CIRROS_PRIVATE_LATEST = CIRROS_PRIVATE + ":latest";
private DockerClient client;
private String registryContainerId;
@Rule
public final TestName testName = new TestName();
@Rule
public final ExpectedException exception = ExpectedException.none();
@BeforeClass
public static void before() throws Exception {
// Pull the registry image down once before any test methods in this class run
DefaultDockerClient.fromEnv().build().pull(REGISTRY_IMAGE);
}
@Before
public void setup() throws Exception {
final RegistryAuth registryAuth = RegistryAuth.builder()
.username(LOCAL_AUTH_USERNAME)
.password(LOCAL_AUTH_PASSWORD)
.build();
client = DefaultDockerClient
.fromEnv()
.registryAuth(registryAuth)
.build();
System.out.printf("- %s\n", testName.getMethodName());
}
@After
@SuppressWarnings("deprecated")
public void tearDown() throws Exception {
if (!isNullOrEmpty(registryContainerId)) {
client.stopContainer(registryContainerId, SECONDS_TO_WAIT_BEFORE_KILL);
client.removeContainer(registryContainerId, RemoveContainerParam.removeVolumes());
awaitStopped(client, registryContainerId);
}
}
@Test
public void testPushImageToPrivateAuthedRegistryWithoutAuth() throws Exception {
registryContainerId = startAuthedRegistry(client);
// Make a DockerClient without RegistryAuth
final DefaultDockerClient client = DefaultDockerClient.fromEnv().build();
// Push an image to the private registry and check it fails
final String dockerDirectory = Resources.getResource("dockerDirectory").getPath();
client.build(Paths.get(dockerDirectory), LOCAL_IMAGE);
exception.expect(ImagePushFailedException.class);
client.push(LOCAL_IMAGE);
}
@Test
public void testPushImageToPrivateAuthedRegistryWithAuth() throws Exception {
registryContainerId = startAuthedRegistry(client);
// Push an image to the private registry and check it succeeds
final String dockerDirectory = Resources.getResource("dockerDirectory").getPath();
client.build(Paths.get(dockerDirectory), LOCAL_IMAGE);
client.tag(LOCAL_IMAGE, LOCAL_IMAGE_2);
client.push(LOCAL_IMAGE);
// Push the same image again under a different user
final RegistryAuth registryAuth = RegistryAuth.builder()
.username(LOCAL_AUTH_USERNAME_2)
.password(LOCAL_AUTH_PASSWORD_2)
.build();
client.push(LOCAL_IMAGE_2, registryAuth);
// We should be able to pull it again
client.pull(LOCAL_IMAGE);
client.pull(LOCAL_IMAGE_2);
}
@Test
public void testPushImageToPrivateUnauthedRegistryWithoutAuth() throws Exception {
registryContainerId = startUnauthedRegistry(client);
// Make a DockerClient without RegistryAuth
final DefaultDockerClient client = DefaultDockerClient.fromEnv().build();
// Push an image to the private registry and check it succeeds
final String dockerDirectory = Resources.getResource("dockerDirectory").getPath();
client.build(Paths.get(dockerDirectory), LOCAL_IMAGE);
client.push(LOCAL_IMAGE);
// We should be able to pull it again
client.pull(LOCAL_IMAGE);
}
@Test
public void testPushImageToPrivateUnauthedRegistryWithAuth() throws Exception {
registryContainerId = startUnauthedRegistry(client);
// Push an image to the private registry and check it succeeds
final String dockerDirectory = Resources.getResource("dockerDirectory").getPath();
client.build(Paths.get(dockerDirectory), LOCAL_IMAGE);
client.push(LOCAL_IMAGE);
// We should be able to pull it again
client.pull(LOCAL_IMAGE);
}
private static String startUnauthedRegistry(final DockerClient client) throws Exception {
final Map<String, List<PortBinding>> ports = Collections.singletonMap(
"5000/tcp", Collections.singletonList(PortBinding.of("0.0.0.0", 5000)));
final HostConfig hostConfig = HostConfig.builder().portBindings(ports)
.build();
final ContainerConfig containerConfig = ContainerConfig.builder()
.image(REGISTRY_IMAGE)
.hostConfig(hostConfig)
.build();
return startAndAwaitContainer(client, containerConfig, REGISTRY_NAME);
}
@Test
public void testPushHubPublicImageWithAuth() throws Exception {
// Push an image to a public repo on Docker Hub and check it succeeds
final String dockerDirectory = Resources.getResource("dockerDirectory").getPath();
final DockerClient client = DefaultDockerClient
.fromEnv()
.registryAuth(RegistryAuth.builder()
.username(HUB_AUTH_USERNAME)
.password(HUB_AUTH_PASSWORD)
.build())
.build();
client.build(Paths.get(dockerDirectory), HUB_PUBLIC_IMAGE);
client.push(HUB_PUBLIC_IMAGE);
}
@Test
public void testPushHubPrivateImageWithAuth() throws Exception {
// Push an image to a private repo on Docker Hub and check it succeeds
final String dockerDirectory = Resources.getResource("dockerDirectory").getPath();
final DockerClient client = DefaultDockerClient
.fromEnv()
.registryAuth(RegistryAuth.builder()
.username(HUB_AUTH_USERNAME)
.password(HUB_AUTH_PASSWORD)
.build())
.build();
client.build(Paths.get(dockerDirectory), HUB_PRIVATE_IMAGE);
client.push(HUB_PRIVATE_IMAGE);
}
@Test
public void testPullHubPrivateRepoWithBadAuth() throws Exception {
final RegistryAuth badRegistryAuth = RegistryAuth.builder()
.username(HUB_AUTH_USERNAME2)
.password("foobar")
.build();
exception.expect(DockerException.class);
exception.expectCause(isA(NotAuthorizedException.class));
client.pull(CIRROS_PRIVATE_LATEST, badRegistryAuth);
}
@Test
public void testBuildHubPrivateRepoWithAuth() throws Exception {
final String dockerDirectory = Resources.getResource("dockerDirectoryNeedsAuth").getPath();
final RegistryAuth registryAuth = RegistryAuth.builder()
.username(HUB_AUTH_USERNAME2)
.password(HUB_AUTH_PASSWORD2)
.build();
final DefaultDockerClient client = DefaultDockerClient.fromEnv()
.registryAuth(registryAuth)
.build();
client.build(Paths.get(dockerDirectory), "testauth", BuildParam.pullNewerImage());
}
@Test
public void testPullHubPrivateRepoWithAuth() throws Exception {
final RegistryAuth registryAuth = RegistryAuth.builder()
.username(HUB_AUTH_USERNAME2)
.password(HUB_AUTH_PASSWORD2)
.build();
client.pull("dxia2/scratch-private:latest", registryAuth);
}
private static String startAuthedRegistry(final DockerClient client) throws Exception {
final Map<String, List<PortBinding>> ports = Collections.singletonMap(
"5000/tcp", Collections.singletonList(PortBinding.of("0.0.0.0", 5000)));
final HostConfig hostConfig = HostConfig.builder().portBindings(ports)
.binds(ImmutableList.of(
Resources.getResource("dockerRegistry/auth").getPath() + ":/auth",
Resources.getResource("dockerRegistry/certs").getPath() + ":/certs"
))
/*
* Mounting volumes requires special permissions on Docker >= 1.10.
* Until a proper Seccomp profile is in place, run container privileged.
*/
.privileged(true)
.build();
final ContainerConfig containerConfig = ContainerConfig.builder()
.image(REGISTRY_IMAGE)
.hostConfig(hostConfig)
.env(ImmutableList.of(
"REGISTRY_AUTH=htpasswd",
"REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm",
"REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd",
"REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt",
"REGISTRY_HTTP_TLS_KEY=/certs/domain.key",
"REGISTRY_HTTP_SECRET=super-secret"
))
.build();
return startAndAwaitContainer(client, containerConfig, REGISTRY_NAME);
}
private static String startAndAwaitContainer(final DockerClient client,
final ContainerConfig containerConfig,
final String containerName)
throws Exception {
final ContainerCreation creation = client.createContainer(containerConfig, containerName);
final String containerId = creation.id();
client.startContainer(containerId);
awaitRunning(client, containerId);
return containerId;
}
private static void awaitRunning(final DockerClient client, final String containerId)
throws Exception {
Polling.await(LONG_WAIT_SECONDS, SECONDS, new Callable<Object>() {
@Override
public Object call() throws Exception {
final ContainerInfo containerInfo = client.inspectContainer(containerId);
return containerInfo.state().running() ? true : null;
}
});
}
private static void awaitStopped(final DockerClient client,
final String containerId)
throws Exception {
Polling.await(LONG_WAIT_SECONDS, SECONDS, new Callable<Object>() {
@Override
public Object call() throws Exception {
boolean containerRemoved = false;
try {
client.inspectContainer(containerId);
} catch (ContainerNotFoundException e) {
containerRemoved = true;
}
return containerRemoved ? true : null;
}
});
}
}