/*- * -\-\- * docker-client * -- * Copyright (C) 2016 Spotify AB * Copyright (C) 2016 Thoughtworks, Inc * -- * 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.Charsets.UTF_8; import static com.google.common.base.Strings.isNullOrEmpty; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Lists.newArrayList; import static com.spotify.docker.client.DefaultDockerClient.NO_TIMEOUT; import static com.spotify.docker.client.DockerClient.EventsParam.since; import static com.spotify.docker.client.DockerClient.EventsParam.type; import static com.spotify.docker.client.DockerClient.EventsParam.until; import static com.spotify.docker.client.DockerClient.ListContainersParam.allContainers; import static com.spotify.docker.client.DockerClient.ListContainersParam.withLabel; import static com.spotify.docker.client.DockerClient.ListContainersParam.withStatusCreated; import static com.spotify.docker.client.DockerClient.ListContainersParam.withStatusExited; import static com.spotify.docker.client.DockerClient.ListContainersParam.withStatusPaused; import static com.spotify.docker.client.DockerClient.ListContainersParam.withStatusRunning; import static com.spotify.docker.client.DockerClient.ListImagesParam.allImages; import static com.spotify.docker.client.DockerClient.ListImagesParam.byName; import static com.spotify.docker.client.DockerClient.ListImagesParam.danglingImages; import static com.spotify.docker.client.DockerClient.ListImagesParam.digests; import static com.spotify.docker.client.DockerClient.ListVolumesParam.dangling; import static com.spotify.docker.client.DockerClient.ListVolumesParam.driver; import static com.spotify.docker.client.DockerClient.ListVolumesParam.name; import static com.spotify.docker.client.DockerClient.LogsParam.follow; import static com.spotify.docker.client.DockerClient.LogsParam.since; import static com.spotify.docker.client.DockerClient.LogsParam.stderr; import static com.spotify.docker.client.DockerClient.LogsParam.stdout; import static com.spotify.docker.client.DockerClient.LogsParam.tail; import static com.spotify.docker.client.DockerClient.LogsParam.timestamps; import static com.spotify.docker.client.VersionCompare.compareVersion; import static com.spotify.docker.client.messages.Event.Type.CONTAINER; import static com.spotify.docker.client.messages.Event.Type.IMAGE; import static com.spotify.docker.client.messages.Event.Type.NETWORK; import static com.spotify.docker.client.messages.Event.Type.VOLUME; import static com.spotify.docker.client.messages.Network.Type.BUILTIN; import static com.spotify.docker.client.messages.RemovedImage.Type.UNTAGGED; import static com.spotify.docker.client.messages.swarm.PortConfig.PROTOCOL_TCP; import static com.spotify.docker.client.messages.swarm.RestartPolicy.RESTART_POLICY_ANY; 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; import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.any; import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.anything; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.either; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItems; import static org.hamcrest.Matchers.hasKey; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isEmptyOrNullString; import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.collection.IsEmptyCollection.empty; import static org.hamcrest.collection.IsMapContaining.hasEntry; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.util.StdDateFormat; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Throwables; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import com.google.common.io.Resources; import com.google.common.util.concurrent.SettableFuture; import com.spotify.docker.client.DockerClient.AttachParameter; import com.spotify.docker.client.DockerClient.BuildParam; import com.spotify.docker.client.DockerClient.EventsParam; import com.spotify.docker.client.DockerClient.ExecCreateParam; import com.spotify.docker.client.DockerClient.ListImagesParam; import com.spotify.docker.client.DockerClient.ListNetworksParam; import com.spotify.docker.client.exceptions.BadParamException; import com.spotify.docker.client.exceptions.ConflictException; import com.spotify.docker.client.exceptions.ContainerNotFoundException; import com.spotify.docker.client.exceptions.ContainerRenameConflictException; import com.spotify.docker.client.exceptions.DockerException; import com.spotify.docker.client.exceptions.DockerRequestException; import com.spotify.docker.client.exceptions.DockerTimeoutException; import com.spotify.docker.client.exceptions.ImageNotFoundException; import com.spotify.docker.client.exceptions.ImagePushFailedException; import com.spotify.docker.client.exceptions.NetworkNotFoundException; import com.spotify.docker.client.exceptions.NotFoundException; import com.spotify.docker.client.exceptions.UnsupportedApiVersionException; import com.spotify.docker.client.exceptions.VolumeNotFoundException; import com.spotify.docker.client.messages.AttachedNetwork; import com.spotify.docker.client.messages.Container; import com.spotify.docker.client.messages.ContainerChange; import com.spotify.docker.client.messages.ContainerConfig; import com.spotify.docker.client.messages.ContainerConfig.Healthcheck; import com.spotify.docker.client.messages.ContainerCreation; import com.spotify.docker.client.messages.ContainerExit; import com.spotify.docker.client.messages.ContainerInfo; import com.spotify.docker.client.messages.ContainerMount; import com.spotify.docker.client.messages.ContainerStats; import com.spotify.docker.client.messages.ContainerUpdate; import com.spotify.docker.client.messages.Device; import com.spotify.docker.client.messages.EndpointConfig; import com.spotify.docker.client.messages.EndpointConfig.EndpointIpamConfig; import com.spotify.docker.client.messages.Event; import com.spotify.docker.client.messages.ExecCreation; import com.spotify.docker.client.messages.ExecState; import com.spotify.docker.client.messages.HostConfig; import com.spotify.docker.client.messages.HostConfig.Bind; import com.spotify.docker.client.messages.HostConfig.Ulimit; import com.spotify.docker.client.messages.Image; import com.spotify.docker.client.messages.ImageHistory; import com.spotify.docker.client.messages.ImageInfo; import com.spotify.docker.client.messages.ImageSearchResult; import com.spotify.docker.client.messages.Info; import com.spotify.docker.client.messages.Ipam; import com.spotify.docker.client.messages.IpamConfig; import com.spotify.docker.client.messages.LogConfig; import com.spotify.docker.client.messages.Network; import com.spotify.docker.client.messages.NetworkConfig; import com.spotify.docker.client.messages.NetworkConnection; import com.spotify.docker.client.messages.NetworkCreation; import com.spotify.docker.client.messages.PortBinding; import com.spotify.docker.client.messages.ProcessConfig; import com.spotify.docker.client.messages.ProgressMessage; import com.spotify.docker.client.messages.RegistryAuth; import com.spotify.docker.client.messages.RemovedImage; import com.spotify.docker.client.messages.ServiceCreateResponse; import com.spotify.docker.client.messages.TopResults; import com.spotify.docker.client.messages.Version; import com.spotify.docker.client.messages.Volume; import com.spotify.docker.client.messages.VolumeList; import com.spotify.docker.client.messages.mount.BindOptions; import com.spotify.docker.client.messages.mount.Mount; import com.spotify.docker.client.messages.mount.TmpfsOptions; import com.spotify.docker.client.messages.mount.VolumeOptions; import com.spotify.docker.client.messages.swarm.CaConfig; import com.spotify.docker.client.messages.swarm.ContainerSpec; import com.spotify.docker.client.messages.swarm.DispatcherConfig; import com.spotify.docker.client.messages.swarm.Driver; import com.spotify.docker.client.messages.swarm.EncryptionConfig; import com.spotify.docker.client.messages.swarm.Endpoint; import com.spotify.docker.client.messages.swarm.EndpointSpec; import com.spotify.docker.client.messages.swarm.EngineConfig; import com.spotify.docker.client.messages.swarm.EnginePlugin; import com.spotify.docker.client.messages.swarm.NetworkAttachmentConfig; import com.spotify.docker.client.messages.swarm.Node; import com.spotify.docker.client.messages.swarm.NodeDescription; import com.spotify.docker.client.messages.swarm.NodeSpec; import com.spotify.docker.client.messages.swarm.OrchestrationConfig; import com.spotify.docker.client.messages.swarm.Placement; import com.spotify.docker.client.messages.swarm.PortConfig; import com.spotify.docker.client.messages.swarm.PortConfig.PortConfigPublishMode; import com.spotify.docker.client.messages.swarm.RaftConfig; import com.spotify.docker.client.messages.swarm.ReplicatedService; import com.spotify.docker.client.messages.swarm.ResourceRequirements; import com.spotify.docker.client.messages.swarm.RestartPolicy; import com.spotify.docker.client.messages.swarm.Secret; import com.spotify.docker.client.messages.swarm.SecretBind; import com.spotify.docker.client.messages.swarm.SecretCreateResponse; import com.spotify.docker.client.messages.swarm.SecretFile; import com.spotify.docker.client.messages.swarm.SecretSpec; import com.spotify.docker.client.messages.swarm.Service; import com.spotify.docker.client.messages.swarm.ServiceMode; 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; import com.spotify.docker.client.messages.swarm.TaskSpec; import com.spotify.docker.client.messages.swarm.UnlockKey; import com.spotify.docker.client.messages.swarm.UpdateConfig; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; import java.net.URLEncoder; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import javax.annotation.Nullable; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.pool.PoolStats; import org.glassfish.jersey.apache.connector.ApacheClientProperties; import org.glassfish.jersey.internal.util.Base64; import org.hamcrest.CustomTypeSafeMatcher; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TestName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class DefaultDockerClientTest { private static final String BUSYBOX = "busybox"; private static final String BUSYBOX_LATEST = BUSYBOX + ":latest"; private static final String BUSYBOX_BUILDROOT_2013_08_1 = BUSYBOX + ":buildroot-2013.08.1"; private static final String MEMCACHED = "rohan/memcached-mini"; private static final String MEMCACHED_LATEST = MEMCACHED + ":latest"; private static final String CIRROS_PRIVATE = "dxia/cirros-private"; private static final String CIRROS_PRIVATE_LATEST = CIRROS_PRIVATE + ":latest"; private static final boolean CIRCLECI = !isNullOrEmpty(getenv("CIRCLECI")); private static final boolean TRAVIS = "true".equals(getenv("TRAVIS")); private static final String AUTH_USERNAME = "dxia2"; private static final String AUTH_PASSWORD = "Tv38KLPd]M"; private static final Logger log = LoggerFactory.getLogger(DefaultDockerClientTest.class); @Rule public final ExpectedException exception = ExpectedException.none(); @Rule public final TestName testName = new TestName(); 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' if (testName.getMethodName().endsWith("NoTimeout")) { builder.readTimeoutMillis(5000); } else { builder.readTimeoutMillis(120000); } dockerEndpoint = builder.uri(); sut = builder.build(); dockerApiVersion = sut.version().apiVersion(); System.out.printf("- %s\n", testName.getMethodName()); } @After public void tearDown() throws Exception { if (dockerApiVersionAtLeast("1.24")) { try { final List<Service> services = sut.listServices(); for (final Service service : services) { if (service.spec().name().startsWith(nameTag)) { sut.removeService(service.id()); } } } catch (DockerException e) { log.warn("Ignoring DockerException in teardown", e); } } // Remove containers final List<Container> containers = sut.listContainers(); for (final Container container : containers) { final ContainerInfo info = sut.inspectContainer(container.id()); if (info != null && info.name().startsWith(nameTag)) { try { sut.killContainer(info.id()); } catch (DockerRequestException e) { // Docker 1.6 sometimes fails to kill a container because it disappears. // https://github.com/docker/docker/issues/12738 log.warn("Failed to kill container {}", info.id(), e); } } } // Close the client sut.close(); } private void requireDockerApiVersionAtLeast(final String required, final String functionality) throws Exception { final String msg = String.format( "Docker API should be at least v%s to support %s but runtime version is %s", required, functionality, dockerApiVersion); assumeTrue(msg, dockerApiVersionAtLeast(required)); } private void requireStorageDriverNotAufs() throws Exception { Info info = sut.info(); assumeFalse(info.storageDriver().equals("aufs")); } private boolean dockerApiVersionAtLeast(final String expected) throws Exception { return compareVersion(dockerApiVersion, expected) >= 0; } private boolean dockerApiVersionLessThan(final String expected) throws Exception { return compareVersion(dockerApiVersion, expected) < 0; } private void requireDockerApiVersionLessThan(final String required, final String functionality) throws Exception { final String actualVersion = sut.version().apiVersion(); final String msg = String.format( "Docker API should be less than v%s to support %s but runtime version is %s", required, functionality, actualVersion); assumeTrue(msg, dockerApiVersionLessThan(required)); } private boolean dockerApiVersionNot(final String expected) { return compareVersion(dockerApiVersion, expected) != 0; } private boolean dockerApiVersionEquals(final String expected) { return compareVersion(dockerApiVersion, expected) == 0; } private void requireDockerApiVersionNot(final String version, final String msg) { assumeTrue(msg, dockerApiVersionNot(version)); } @Test public void testSearchImage() throws Exception { requireDockerApiVersionNot("1.19", "Docker 1.7.x sends the wrong Content-Type header for " + "/images/search. So we skip this test."); // when final List<ImageSearchResult> searchResult = sut.searchImages(BUSYBOX); // then assertThat(searchResult.size(), greaterThan(0)); } @Test public void testPullWithTag() throws Exception { sut.pull(BUSYBOX_BUILDROOT_2013_08_1); } @Test(expected = ImageNotFoundException.class) public void testPullBadImage() throws Exception { // The Docker daemon on CircleCI won't throw ImageNotFoundException for some reason... assumeFalse(CIRCLECI); sut.pull(randomName()); } @Test(expected = ImageNotFoundException.class) public void testPullPrivateRepoWithoutAuth() throws Exception { sut.pull(CIRROS_PRIVATE_LATEST); } @Test public void testBuildImageIdWithBuildargs() throws Exception { requireDockerApiVersionAtLeast("1.21", "build args"); final String dockerDirectory = Resources.getResource("dockerDirectoryWithBuildargs").getPath(); final String buildargs = "{\"testargument\":\"22-12-2015\"}"; final BuildParam buildParam = BuildParam.create("buildargs", URLEncoder.encode(buildargs, "UTF-8")); sut.build( Paths.get(dockerDirectory), "test-buildargs", buildParam ); } @Test public void testHealthCheck() throws Exception { requireDockerApiVersionAtLeast("1.24", "health check"); // Create image final String dockerDirectory = Resources.getResource("dockerDirectoryWithHealthCheck") .getPath(); final String imageId = sut.build( Paths.get(dockerDirectory), "test-healthcheck" ); // Inpect image to check healthcheck configuration final ImageInfo imageInfo = sut.inspectImage(imageId); assertNotNull(imageInfo.config().healthcheck()); assertEquals(Arrays.asList("CMD-SHELL", "exit 1"), imageInfo.config().healthcheck().test()); // Create container based on this image to check initial container health state final ContainerConfig config = ContainerConfig.builder() .image("test-healthcheck") .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); sut.startContainer(creation.id()); final ContainerInfo containerInfo = sut.inspectContainer(creation.id()); assertNotNull(containerInfo.state().health()); assertEquals("starting", containerInfo.state().health().status()); } @SuppressWarnings("emptyCatchBlock") @Test public void testFailedPullDoesNotLeakConn() throws Exception { log.info("Connection pool stats: " + getClientConnectionPoolStats(sut).toString()); // Pull a non-existent image 10 times and check that the number of leased connections is still 0 // I.e. check that we are not leaking connections. for (int i = 0; i < 10; i++) { try { sut.pull(BUSYBOX + ":" + randomName()); } catch (ImageNotFoundException ignored) { } log.info("Connection pool stats: " + getClientConnectionPoolStats(sut).toString()); } assertThat(getClientConnectionPoolStats(sut).getLeased(), equalTo(0)); } @Test public void testPullByDigest() throws Exception { // The current Docker client on CircleCI does allow you to pull images by digest. assumeFalse(CIRCLECI); // note: this digest may change over time, the value here may disappear from hub.docker.com sut.pull(BUSYBOX + "@sha256:4a887a2326ec9e0fa90cce7b4764b0e627b5d6afcb81a3f73c85dc29cea00048"); } @Test public void testSave() throws Exception { // Ensure the local Docker instance has the busybox image so that save() will work sut.pull(BUSYBOX_LATEST); final File imageFile = save(BUSYBOX); assertTrue(imageFile.length() > 0); } @Test public void testLoad() throws Exception { // Ensure the local Docker instance has the busybox image so that save() will work sut.pull(BUSYBOX_LATEST); // duplicate busybox with another name final String image1 = BUSYBOX + "test1" + System.nanoTime() + ":latest"; final String image2 = BUSYBOX + "test2" + System.nanoTime() + ":latest"; try (InputStream imagePayload = new BufferedInputStream(new FileInputStream(save(BUSYBOX_LATEST)))) { sut.create(image1, imagePayload); } try (InputStream imagePayload = new BufferedInputStream(new FileInputStream(save(BUSYBOX_LATEST)))) { sut.create(image2, imagePayload); } final File imagesFile = save(image1, image2); // Remove image from the local Docker instance to test the load sut.removeImage(image1); sut.removeImage(image2); // Try to inspect deleted images and make sure ImageNotFoundException is thrown try { sut.inspectImage(image1); fail("inspectImage should have thrown ImageNotFoundException"); } catch (ImageNotFoundException e) { // we should get exception because we deleted image } try { sut.inspectImage(image2); fail("inspectImage should have thrown ImageNotFoundException"); } catch (ImageNotFoundException e) { // we should get exception because we deleted image } final List<ProgressMessage> messages = new ArrayList<>(); final Set<String> loadedImages; try (InputStream imageFileInputStream = new FileInputStream(imagesFile)) { loadedImages = sut.load(imageFileInputStream, new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { messages.add(message); } }); } if (dockerApiVersionAtLeast("1.24")) { // Verify that both images are loaded assertEquals(loadedImages.size(), 2); assertTrue(loadedImages.contains(image1)); assertTrue(loadedImages.contains(image2)); } if (dockerApiVersionAtLeast("1.23")) { // Verify that we have multiple messages, and each one has a non-null field assertThat(messages, not(empty())); for (final ProgressMessage message : messages) { assertTrue(message.error() != null || message.id() != null || message.progress() != null || message.progressDetail() != null || message.status() != null || message.stream() != null); } } // Try to inspect created images and make sure ImageNotFoundException is not thrown try { sut.inspectImage(image1); sut.inspectImage(image2); } catch (ImageNotFoundException e) { fail("image not properly loaded in the local Docker instance"); } // Clean created image sut.removeImage(image1); sut.removeImage(image2); } private File save(final String ... images) throws Exception { final File tmpDir = new File(System.getProperty("java.io.tmpdir")); assertTrue("Temp directory " + tmpDir.getAbsolutePath() + " does not exist", tmpDir.exists()); final File imageFile = new File(tmpDir, "busybox-" + System.nanoTime() + ".tar"); //noinspection ResultOfMethodCallIgnored imageFile.createNewFile(); imageFile.deleteOnExit(); final byte[] buffer = new byte[2048]; int read; try (OutputStream imageOutput = new BufferedOutputStream(new FileOutputStream(imageFile))) { try (InputStream imageInput = sut.save(images)) { while ((read = imageInput.read(buffer)) > -1) { imageOutput.write(buffer, 0, read); } } } return imageFile; } @Test public void testCreate() throws Exception { // Ensure the local Docker instance has the busybox image so that save() will work sut.pull(BUSYBOX_LATEST); final File imageFile = save(BUSYBOX); final String image = BUSYBOX + "test" + System.nanoTime(); try (InputStream imagePayload = new BufferedInputStream(new FileInputStream(imageFile))) { sut.create(image, imagePayload); } final Collection<Image> images = Collections2.filter(sut.listImages(), new Predicate<Image>() { @Override public boolean apply(final Image img) { return img.repoTags() != null && img.repoTags().contains(image + ":latest"); } }); assertThat(images.size(), greaterThan(0)); for (final Image img : images) { sut.removeImage(img.id()); } } @Test public void testPingReturnsOk() throws Exception { final String pingResponse = sut.ping(); assertThat(pingResponse, equalTo("OK")); } @Test public void testVersion() throws Exception { final Version version = sut.version(); assertThat(version.apiVersion(), not(isEmptyOrNullString())); assertThat(version.arch(), not(isEmptyOrNullString())); assertThat(version.gitCommit(), not(isEmptyOrNullString())); assertThat(version.goVersion(), not(isEmptyOrNullString())); assertThat(version.kernelVersion(), not(isEmptyOrNullString())); assertThat(version.os(), not(isEmptyOrNullString())); assertThat(version.version(), not(isEmptyOrNullString())); if (dockerApiVersionAtLeast("1.22")) { assertThat(version.buildTime(), not(isEmptyOrNullString())); } } @Test public void testAuth() throws Exception { final int statusCode = sut.auth(registryAuth); assertThat(statusCode, equalTo(200)); } @Test public void testBadAuth() throws Exception { final RegistryAuth badRegistryAuth = RegistryAuth.builder() .username(AUTH_USERNAME) .password("foobar") .build(); final int statusCode = sut.auth(badRegistryAuth); assertThat(statusCode, equalTo(401)); } @Test public void testMissingAuthParam() throws Exception { requireDockerApiVersionLessThan("1.23", "https://github.com/docker/docker/issues/24093"); final RegistryAuth badRegistryAuth = RegistryAuth.builder() .username(AUTH_USERNAME) .build(); final int statusCode = sut.auth(badRegistryAuth); assertThat(statusCode, equalTo(500)); } @Test @SuppressWarnings("deprecation") public void testInfo() throws Exception { final Info info = sut.info(); assertThat(info.containers(), is(anything())); assertThat(info.debug(), is(anything())); assertThat(info.dockerRootDir(), not(isEmptyOrNullString())); assertThat(info.storageDriver(), not(isEmptyOrNullString())); assertThat(info.driverStatus(), is(anything())); if (dockerApiVersionLessThan("1.23")) { // Execution driver was removed in 1.24 https://github.com/docker/docker/pull/24501 // But it also shows up as "" in 1.23, and I don't know why - JF assertThat(info.executionDriver(), not(isEmptyOrNullString())); } assertThat(info.id(), not(isEmptyOrNullString())); assertThat(info.ipv4Forwarding(), is(anything())); assertThat(info.images(), greaterThan(-1)); assertThat(info.indexServerAddress(), not(isEmptyOrNullString())); if (dockerApiVersionLessThan("1.23")) { // Init path seems to have been removed in API 1.23. // Still documented as of 2016-09-26, but InitPath field is not in /info - JF assertThat(info.initPath(), not(isEmptyOrNullString())); } assertThat(info.initSha1(), is(anything())); assertThat(info.kernelVersion(), not(isEmptyOrNullString())); assertThat(info.labels(), is(anything())); assertThat(info.memTotal(), greaterThan(0L)); assertThat(info.memoryLimit(), not(nullValue())); assertThat(info.cpus(), greaterThan(0)); assertThat(info.eventsListener(), is(anything())); assertThat(info.fileDescriptors(), is(anything())); assertThat(info.goroutines(), is(anything())); assertThat(info.name(), not(isEmptyOrNullString())); assertThat(info.operatingSystem(), not(isEmptyOrNullString())); assertThat(info.registryConfig(), notNullValue()); assertThat(info.registryConfig().indexConfigs(), hasKey("docker.io")); assertThat(info.swapLimit(), not(nullValue())); if (dockerApiVersionAtLeast("1.18")) { assertThat(info.httpProxy(), is(anything())); assertThat(info.httpsProxy(), is(anything())); assertThat(info.noProxy(), is(anything())); assertThat(info.systemTime(), not(nullValue())); } if (dockerApiVersionAtLeast("1.19")) { assertThat(info.cpuCfsPeriod(), is(anything())); assertThat(info.cpuCfsQuota(), is(anything())); assertThat(info.experimentalBuild(), is(anything())); assertThat(info.oomKillDisable(), is(anything())); } if (dockerApiVersionAtLeast("1.21")) { assertThat(info.clusterStore(), is(anything())); assertEquals(info.serverVersion(), sut.version().version()); } if (dockerApiVersionAtLeast("1.22")) { assertThat(info.architecture(), not(isEmptyOrNullString())); assertThat(info.containersRunning(), is(anything())); assertThat(info.containersStopped(), is(anything())); assertThat(info.containersPaused(), is(anything())); assertThat(info.osType(), not(isEmptyOrNullString())); assertThat(info.systemStatus(), is(anything())); } if (dockerApiVersionAtLeast("1.23")) { assertThat(info.cgroupDriver(), not(isEmptyOrNullString())); assertThat(info.kernelMemory(), is(anything())); } } @Test public void testRemoveImage() throws Exception { // Don't remove images on CircleCI. Their version of Docker causes failures when pulling an // image that shares layers with an image that has been removed. This causes tests after this // one to fail. assumeFalse(CIRCLECI); sut.pull("dxia/cirros:latest"); sut.pull("dxia/cirros:0.3.0"); final String imageLatest = "dxia/cirros:latest"; final String imageVersion = "dxia/cirros:0.3.0"; final Set<RemovedImage> removedImages = Sets.newHashSet(); removedImages.addAll(sut.removeImage(imageLatest)); removedImages.addAll(sut.removeImage(imageVersion)); assertThat(removedImages, hasItems( RemovedImage.create(UNTAGGED, imageLatest), RemovedImage.create(UNTAGGED, imageVersion) )); // Try to inspect deleted image and make sure ImageNotFoundException is thrown try { sut.inspectImage(imageLatest); fail("inspectImage should have thrown ImageNotFoundException"); } catch (ImageNotFoundException e) { // we should get exception because we deleted image } } @Test public void testTag() throws Exception { sut.pull(BUSYBOX_LATEST); // Tag image final String newImageName = "test-repo:testTag"; sut.tag(BUSYBOX, newImageName); // Verify tag was successful by trying to remove it. final RemovedImage removedImage = getOnlyElement(sut.removeImage(newImageName)); assertThat(removedImage, equalTo(RemovedImage.create(UNTAGGED, newImageName))); } @Test public void testTagForce() throws Exception { sut.pull(BUSYBOX_LATEST); sut.pull(BUSYBOX_BUILDROOT_2013_08_1); final String name = "test-repo/tag-force:sometag"; // Assign name to first image sut.tag(BUSYBOX_LATEST, name); // Force-re-assign tag to another image sut.tag(BUSYBOX_BUILDROOT_2013_08_1, name, true); // Verify that re-tagging was successful final RemovedImage removedImage = getOnlyElement(sut.removeImage(name)); assertThat(removedImage, is(RemovedImage.create(UNTAGGED, name))); } @Test public void testInspectImage() throws Exception { sut.pull(BUSYBOX_BUILDROOT_2013_08_1); final ImageInfo info = sut.inspectImage(BUSYBOX_BUILDROOT_2013_08_1); assertThat(info, notNullValue()); assertThat(info.architecture(), not(isEmptyOrNullString())); assertThat(info.author(), not(isEmptyOrNullString())); assertThat(info.config(), notNullValue()); assertThat(info.container(), not(isEmptyOrNullString())); assertThat(info.containerConfig(), notNullValue()); assertThat(info.comment(), notNullValue()); assertThat(info.created(), notNullValue()); assertThat(info.dockerVersion(), not(isEmptyOrNullString())); assertThat(info.id(), not(isEmptyOrNullString())); assertThat(info.os(), equalTo("linux")); //noinspection StatementWithEmptyBody if (dockerApiVersionLessThan("1.22")) { assertThat(info.parent(), not(isEmptyOrNullString())); } else { // The "parent" field can be empty because of changes in // image storage in 1.10. See https://github.com/docker/docker/issues/19650. } assertThat(info.size(), notNullValue()); assertThat(info.virtualSize(), notNullValue()); } @Test public void testCustomProgressMessageHandler() throws Exception { final List<ProgressMessage> messages = new ArrayList<>(); sut.pull(BUSYBOX_LATEST, new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { messages.add(message); } }); // Verify that we have multiple messages, and each one has a non-null field assertThat(messages, not(empty())); for (final ProgressMessage message : messages) { assertTrue(message.error() != null || message.id() != null || message.progress() != null || message.progressDetail() != null || message.status() != null || message.stream() != null); } } @Test public void testBuildImageId() throws Exception { final String dockerDirectory = Resources.getResource("dockerDirectory").getPath(); final AtomicReference<String> imageIdFromMessage = new AtomicReference<>(); final String returnedImageId = sut.build( Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { final String imageId = message.buildImageId(); if (imageId != null) { imageIdFromMessage.set(imageId); } } }); assertThat(returnedImageId, is(imageIdFromMessage.get())); } @Test public void testBuildImageIdPathToDockerFile() throws Exception { final String dockerDirectory = Resources.getResource("dockerDirectory").getPath(); final AtomicReference<String> imageIdFromMessage = new AtomicReference<>(); final String returnedImageId = sut.build( Paths.get(dockerDirectory), "test", "innerDir/innerDockerfile", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { final String imageId = message.buildImageId(); if (imageId != null) { imageIdFromMessage.set(imageId); } } }); assertThat(returnedImageId, is(imageIdFromMessage.get())); } @Test public void testBuildImageIdWithAuth() throws Exception { final String dockerDirectory = Resources.getResource("dockerDirectory").getPath(); final AtomicReference<String> imageIdFromMessage = new AtomicReference<>(); final DefaultDockerClient sut2 = DefaultDockerClient.fromEnv() .registryAuth(registryAuth) .build(); final String returnedImageId = sut2.build( Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { final String imageId = message.buildImageId(); if (imageId != null) { imageIdFromMessage.set(imageId); } } }); assertThat(returnedImageId, is(imageIdFromMessage.get())); } @SuppressWarnings("EmptyCatchBlock") @Test public void testFailedBuildDoesNotLeakConn() throws Exception { final String dockerDirectory = Resources.getResource("dockerDirectoryNonExistentImage").getPath(); log.info("Connection pool stats: " + getClientConnectionPoolStats(sut).toString()); // Build an image from a bad Dockerfile 10 times and check that the number of // leased connections is still 0. // I.e. check that we are not leaking connections. for (int i = 0; i < 10; i++) { try { sut.build(Paths.get(dockerDirectory), "test"); } catch (DockerException ignored) { } log.info("Connection pool stats: " + getClientConnectionPoolStats(sut).toString()); } assertThat(getClientConnectionPoolStats(sut).getLeased(), equalTo(0)); } @Test public void testBuildName() throws Exception { final String imageName = "test-build-name"; final String dockerDirectory = Resources.getResource("dockerDirectory").getPath(); final String imageId = sut.build(Paths.get(dockerDirectory), imageName); final ImageInfo info = sut.inspectImage(imageName); final String expectedId = dockerApiVersionLessThan("1.22") ? imageId : "sha256:" + imageId; assertThat(info.id(), startsWith(expectedId)); } @Test public void testBuildWithPull() throws Exception { requireDockerApiVersionAtLeast("1.19", "build with pull"); final String dockerDirectory = Resources.getResource("dockerDirectory").getPath(); final String pullMsg = "Pulling from"; // Build once to make sure we have cached images. sut.build(Paths.get(dockerDirectory)); // Build again with PULL set, and verify we pulled the base image final AtomicBoolean pulled = new AtomicBoolean(false); sut.build(Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { if (!isNullOrEmpty(message.status()) && message.status().contains(pullMsg)) { pulled.set(true); } } }, BuildParam.pullNewerImage()); assertTrue(pulled.get()); } @Test public void testBuildNoCache() throws Exception { final String dockerDirectory = Resources.getResource("dockerDirectory").getPath(); final String usingCache = "Using cache"; // Build once to make sure we have cached images. sut.build(Paths.get(dockerDirectory)); // Build again and make sure we used cached image by parsing output. final AtomicBoolean usedCache = new AtomicBoolean(false); sut.build(Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { if (message.stream().contains(usingCache)) { usedCache.set(true); } } }); assertTrue(usedCache.get()); // Build again with NO_CACHE set, and verify we don't use cache. sut.build(Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { assertThat(message.stream(), not(containsString(usingCache))); } }, BuildParam.noCache()); } @Test public void testBuildNoRm() throws Exception { final String dockerDirectory = Resources.getResource("dockerDirectory").getPath(); final String removingContainers = "Removing intermediate container"; // Test that intermediate containers are removed with FORCE_RM by parsing output. We must // set NO_CACHE so that docker will generate some containers to remove. final AtomicBoolean removedContainer = new AtomicBoolean(false); sut.build(Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { if (containsIgnoreCase(message.stream(), removingContainers)) { removedContainer.set(true); } } }, BuildParam.noCache(), BuildParam.forceRm()); assertTrue(removedContainer.get()); // Set NO_RM and verify we don't get message that containers were removed. sut.build(Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { assertThat(message.stream(), not(containsString(removingContainers))); } }, BuildParam.noCache(), BuildParam.rm(false)); } @Test public void testBuildNoTimeout() throws Exception { // The Dockerfile specifies a sleep of 10s during the build // Returned image id is last piece of output, so this confirms stream did not timeout final String dockerDirectory = Resources.getResource("dockerDirectorySleeping").getPath(); final String returnedImageId = sut.build( Paths.get(dockerDirectory), "test", new ProgressHandler() { @Override public void progress(ProgressMessage message) throws DockerException { log.info(message.stream()); } }, BuildParam.noCache()); assertTrue(returnedImageId != null); } @Test public void testGetImageIdFromBuild() { // Include a new line because that's what docker returns. final ProgressMessage message1 = ProgressMessage.builder() .stream("Successfully built 2d6e00052167\n") .build(); assertThat(message1.buildImageId(), is("2d6e00052167")); final ProgressMessage message2 = ProgressMessage.builder().id("123").build(); assertThat(message2.buildImageId(), nullValue()); final ProgressMessage message3 = ProgressMessage.builder().stream("Step 2 : CMD[]").build(); assertThat(message3.buildImageId(), nullValue()); } @Test public void testAnsiProgressHandler() throws Exception { final ByteArrayOutputStream out = new ByteArrayOutputStream(); sut.pull(BUSYBOX_LATEST, new AnsiProgressHandler(new PrintStream(out))); // The progress handler uses ascii escape characters to move the cursor around to nicely print // progress bars. This is hard to test programmatically, so let's just verify the output // contains some expected phrases. final String pullingStr = dockerApiVersionAtLeast("1.20") ? "Pulling from library/busybox" : "Pulling from busybox"; assertThat(out.toString(), allOf(containsString(pullingStr), containsString("Image is up to date"))); } @Test public void testExportContainer() throws Exception { // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); final ImmutableSet.Builder<String> files = ImmutableSet.builder(); try (TarArchiveInputStream tarStream = new TarArchiveInputStream(sut.exportContainer(id))) { TarArchiveEntry entry; while ((entry = tarStream.getNextTarEntry()) != null) { files.add(entry.getName()); } } // Check that some common files exist assertThat(files.build(), both(hasItem("bin/")).and(hasItem("bin/sh"))); } @Test @SuppressWarnings("deprecation") public void testCopyContainer() throws Exception { requireDockerApiVersionLessThan("1.24", "failCopyToContainer"); // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); final ImmutableSet.Builder<String> files = ImmutableSet.builder(); try (final TarArchiveInputStream tarStream = new TarArchiveInputStream(sut.copyContainer(id, "/bin"))) { TarArchiveEntry entry; while ((entry = tarStream.getNextTarEntry()) != null) { files.add(entry.getName()); } } // Check that some common files exist assertThat(files.build(), both(hasItem("bin/")).and(hasItem("bin/wc"))); } @Test @SuppressWarnings("deprecation") public void testFailCopyContainer() throws Exception { requireDockerApiVersionAtLeast("1.24", "failCopyToContainer"); exception.expect(UnsupportedApiVersionException.class); // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.copyContainer(id, "/bin"); } @Test public void testArchiveContainer() throws Exception { requireDockerApiVersionAtLeast("1.20", "copyToContainer"); // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); final ImmutableSet.Builder<String> files = ImmutableSet.builder(); try (final TarArchiveInputStream tarStream = new TarArchiveInputStream(sut.archiveContainer(id, "/bin"))) { TarArchiveEntry entry; while ((entry = tarStream.getNextTarEntry()) != null) { files.add(entry.getName()); } } // Check that some common files exist assertThat(files.build(), both(hasItem("bin/")).and(hasItem("bin/wc"))); } @Test public void testFailArchiveContainer() throws Exception { requireDockerApiVersionLessThan("1.20", "failCopyToContainer"); exception.expect(UnsupportedApiVersionException.class); // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.archiveContainer(id, "/bin"); } @Test public void testCopyToContainer() throws Exception { requireDockerApiVersionAtLeast("1.20", "copyToContainer"); // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder().image(BUSYBOX_LATEST).build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String containerId = creation.id(); final String dockerDirectory = Resources.getResource("dockerSslDirectory").getPath(); try { sut.copyToContainer(Paths.get(dockerDirectory), containerId, "/tmp"); } catch (Exception e) { fail("error to copy files to container"); } } @Test public void testCopyToContainerWithTarInputStream() throws Exception { requireDockerApiVersionAtLeast("1.20", "copyToContainer"); // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder().image(BUSYBOX_LATEST).build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String containerId = creation.id(); try (final InputStream tarStream = Resources.getResource("dockerCopyToContainer.tar.gz").openStream()) { sut.copyToContainer(tarStream, containerId, "/tmp"); } catch (Exception e) { fail("error to copy files to container"); } } @Test public void testCommitContainer() throws Exception { // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); final String tag = randomName(); final ContainerCreation dockerClientTest = sut.commitContainer(id, "mosheeshel/busybox", tag, config, "CommitedByTest-" + tag, "DockerClientTest"); final ImageInfo imageInfo = sut.inspectImage(dockerClientTest.id()); assertThat(imageInfo.author(), is("DockerClientTest")); assertThat(imageInfo.comment(), is("CommitedByTest-" + tag)); } @Test public void testStopContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); // Must be running { final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(true)); } sut.stopContainer(containerId, 5); // Must no longer be running { final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(false)); } } @Test public void testTopProcessesOfContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); // Ensure that it's running so we can check the active processes { final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(true)); } final TopResults topResults = sut.topContainer(containerId, null); assertThat(topResults.titles(), not(Matchers.empty())); // there could be one or two processes running, depending on if we happen to catch it in // between sleeps assertThat(topResults.processes(), hasSize(greaterThanOrEqualTo(1))); assertThat(topResults.titles(), either(hasItem("CMD")).or(hasItem("COMMAND"))); final List<String> firstProcessStatus = topResults.processes().get(0); assertThat("All processes will run as 'root'", firstProcessStatus, hasItem("root")); } @Test public void testRestartContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); // Must be running { final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(true)); } final ContainerInfo tempContainerInfo = sut.inspectContainer(containerId); final Integer originalPid = tempContainerInfo.state().pid(); sut.restartContainer(containerId); // Should be running with short run time { final ContainerInfo containerInfoLatest = sut.inspectContainer(containerId); assertTrue(containerInfoLatest.state().running()); assertThat(containerInfoLatest.state().pid(), not(equalTo(originalPid))); } } @Test public void testKillContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); // Must be running final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(true)); sut.killContainer(containerId); // Should not be running final ContainerInfo containerInfoLatest = sut.inspectContainer(containerId); assertFalse(containerInfoLatest.state().running()); } @Test public void testKillContainerWithSignals() throws Exception { requireDockerApiVersionAtLeast("1.18", "killContainerWithSignals"); sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); // Must be running final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(true)); final ContainerInfo tempContainerInfo = sut.inspectContainer(containerId); final Integer originalPid = tempContainerInfo.state().pid(); // kill with SIGKILL sut.killContainer(containerId, DockerClient.Signal.SIGKILL); // Should not be running final ContainerInfo containerInfoLatest = sut.inspectContainer(containerId); assertFalse(containerInfoLatest.state().running()); } @Test @SuppressWarnings("deprecation") public void integrationTest() throws Exception { // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); assertThat(creation.warnings(), anyOf(is(empty()), is(nullValue()))); assertThat(id, is(any(String.class))); // Inspect using container ID { final ContainerInfo info = sut.inspectContainer(id); assertThat(info.id(), equalTo(id)); assertThat(info.config().image(), equalTo(config.image())); assertThat(info.config().cmd(), equalTo(config.cmd())); } // Inspect using container name { final ContainerInfo info = sut.inspectContainer(name); assertThat(info.config().image(), equalTo(config.image())); assertThat(info.config().cmd(), equalTo(config.cmd())); } // Start container sut.startContainer(id); final String dockerDirectory = Resources.getResource("dockerSslDirectory").getPath(); // Copy files to container // Docker API should be at least v1.20 to support extracting an archive of files or folders // to a directory in a container if (dockerApiVersionAtLeast("1.20")) { try { sut.copyToContainer(Paths.get(dockerDirectory), id, "/tmp"); } catch (Exception e) { fail("error copying files to container"); } // Copy the same files from container final ImmutableSet.Builder<String> filesDownloaded = ImmutableSet.builder(); try (TarArchiveInputStream tarStream = new TarArchiveInputStream( dockerApiVersionLessThan("1.24") ? sut.copyContainer(id, "/tmp") : sut.archiveContainer(id, "/tmp"))) { TarArchiveEntry entry; while ((entry = tarStream.getNextTarEntry()) != null) { filesDownloaded.add(entry.getName()); } } // Check that we got back what we put in final File folder = new File(dockerDirectory); final File[] files = folder.listFiles(); if (files != null) { for (final File file : files) { if (!file.isDirectory()) { Boolean found = false; for (final String fileDownloaded : filesDownloaded.build()) { if (fileDownloaded.contains(file.getName())) { found = true; } } assertTrue(found); } } } } // Kill container sut.killContainer(id); try { // Remove the container sut.removeContainer(id); } catch (DockerRequestException e) { // CircleCI doesn't let you remove a container :( if (!CIRCLECI) { // Verify that the container is gone exception.expect(ContainerNotFoundException.class); sut.inspectContainer(id); } } } @Test public void interruptTest() throws Exception { // Pull image sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); // Start container sut.startContainer(id); // Wait for container on a thread final ExecutorService executorService = Executors.newSingleThreadExecutor(); final SettableFuture<Boolean> started = SettableFuture.create(); final SettableFuture<Boolean> interrupted = SettableFuture.create(); final Future<ContainerExit> exitFuture = executorService.submit(new Callable<ContainerExit>() { @Override public ContainerExit call() throws Exception { try { started.set(true); return sut.waitContainer(id); } catch (InterruptedException e) { interrupted.set(true); throw e; } } }); // Interrupt waiting thread started.get(); executorService.shutdownNow(); try { exitFuture.get(); fail(); } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(InterruptedException.class)); } // Verify that the thread was interrupted assertThat(interrupted.get(), is(true)); } @SuppressWarnings("EmptyCatchBlock") @Test public void testFailedPushDoesNotLeakConn() throws Exception { log.info("Connection pool stats: " + getClientConnectionPoolStats(sut).toString()); // Push a non-existent image 10 times and check that the number of // leased connections is still 0. // I.e. check that we are not leaking connections. for (int i = 0; i < 10; i++) { try { sut.push("foobarboooboo" + randomName()); } catch (ImagePushFailedException ignored) { } log.info("Connection pool stats: " + getClientConnectionPoolStats(sut).toString()); } assertThat(getClientConnectionPoolStats(sut).getLeased(), equalTo(0)); } @Test(expected = DockerException.class) public void testConnectTimeout() throws Exception { // Attempt to connect to reserved IP -> should timeout try (final DefaultDockerClient connectTimeoutClient = DefaultDockerClient.builder() .uri("http://240.0.0.1:2375") .connectTimeoutMillis(100) .readTimeoutMillis(NO_TIMEOUT) .build()) { connectTimeoutClient.version(); } } @Test(expected = DockerTimeoutException.class) public void testReadTimeout() throws Exception { try (final ServerSocket socket = new ServerSocket()) { // Bind and listen but do not accept -> read will time out. socket.bind(new InetSocketAddress("127.0.0.1", 0)); awaitConnectable(socket.getInetAddress(), socket.getLocalPort()); final DockerClient connectTimeoutClient = DefaultDockerClient.builder() .uri("http://127.0.0.1:" + socket.getLocalPort()) .connectTimeoutMillis(NO_TIMEOUT) .readTimeoutMillis(100) .build(); connectTimeoutClient.version(); } } @Test(expected = DockerTimeoutException.class) public void testConnectionRequestTimeout() throws Exception { final int connectionPoolSize = 1; final int callableCount = connectionPoolSize * 100; final ExecutorService executor = Executors.newCachedThreadPool(); final CompletionService<ContainerExit> completion = new ExecutorCompletionService<>(executor); // Spawn and wait on many more containers than the connection pool size. // This should cause a timeout once the connection pool is exhausted. try (final DockerClient dockerClient = DefaultDockerClient.fromEnv() .connectionPoolSize(connectionPoolSize) .build()) { // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String name = randomName(); final ContainerCreation creation = dockerClient.createContainer(config, name); final String id = creation.id(); // Start the container dockerClient.startContainer(id); // Submit a bunch of waitContainer requests for (int i = 0; i < callableCount; i++) { //noinspection unchecked completion.submit(new Callable<ContainerExit>() { @Override public ContainerExit call() throws Exception { return dockerClient.waitContainer(id); } }); } // Wait for the requests to complete or throw expected exception for (int i = 0; i < callableCount; i++) { try { completion.take().get(); } catch (ExecutionException e) { Throwables.propagateIfInstanceOf(e.getCause(), DockerTimeoutException.class); throw e; } } } finally { executor.shutdown(); } } @Test public void testWaitContainer() throws Exception { sut.pull(BUSYBOX_LATEST); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); // Start the container sut.startContainer(id); // Wait for container on a thread final ExecutorService executorService = Executors.newSingleThreadExecutor(); final Future<ContainerExit> exitFuture = executorService.submit(new Callable<ContainerExit>() { @Override public ContainerExit call() throws Exception { return sut.waitContainer(id); } }); // Wait for 40 seconds, then kill the container Thread.sleep(40000); sut.killContainer(id); // Ensure that waiting on the container worked without exception exitFuture.get(); } @Test public void testInspectContainerWithExposedPorts() throws Exception { sut.pull(MEMCACHED_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(MEMCACHED_LATEST) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); sut.startContainer(container.id()); final ContainerInfo containerInfo = sut.inspectContainer(container.id()); assertThat(containerInfo, notNullValue()); assertThat(containerInfo.networkSettings().ports(), hasEntry("11211/tcp", Collections.<PortBinding>emptyList())); } @Test public void testInspectContainerWithSecurityOpts() throws Exception { final String userLabel = "label:user:dxia"; final String roleLabel = "label:role:foo"; final String typeLabel = "label:type:bar"; final String levelLabel = "label:level:9001"; sut.pull(MEMCACHED_LATEST); final HostConfig hostConfig = HostConfig.builder() .securityOpt(userLabel, roleLabel, typeLabel, levelLabel) .build(); final ContainerConfig config = ContainerConfig.builder() .image(MEMCACHED_LATEST) .hostConfig(hostConfig) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); sut.startContainer(container.id()); final ContainerInfo containerInfo = sut.inspectContainer(container.id()); assertThat(containerInfo, notNullValue()); assertThat(containerInfo.hostConfig().securityOpt(), hasItems(userLabel, roleLabel, typeLabel, levelLabel)); } @Test public void testContainerWithHostConfig() throws Exception { requireDockerApiVersionAtLeast("1.18", "Container creation with HostConfig"); sut.pull(BUSYBOX_LATEST); final boolean privileged = true; final boolean publishAllPorts = true; final String dns = "1.2.3.4"; final List<Ulimit> ulimits = newArrayList( Ulimit.builder() .name("nofile") .soft(1024L) .hard(2048L) .build() ); final Device expectedDevice = Device.builder() .pathOnHost(".") .pathInContainer("/foo") .cgroupPermissions("mrw") .build(); final HostConfig.Builder hostConfigBuilder = HostConfig.builder() .privileged(privileged) .publishAllPorts(publishAllPorts) .dns(dns) .dnsSearch("domain1", "domain2") .devices(expectedDevice) .ulimits(ulimits); if (dockerApiVersionAtLeast("1.21")) { hostConfigBuilder.dnsOptions("some", "options"); } final HostConfig expected = hostConfigBuilder.build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); assertThat(actual.privileged(), equalTo(expected.privileged())); assertThat(actual.publishAllPorts(), equalTo(expected.publishAllPorts())); assertThat(actual.dns(), equalTo(expected.dns())); if (dockerApiVersionAtLeast("1.21")) { assertThat(actual.dnsOptions(), equalTo(expected.dnsOptions())); } assertThat(actual.dnsSearch(), equalTo(expected.dnsSearch())); assertEquals(ulimits, actual.ulimits()); assertThat(actual.devices(), contains(expectedDevice)); } @Test public void testContainerWithCpuOptions() throws Exception { requireDockerApiVersionAtLeast("1.18", "Container creation with cpu options"); sut.pull(BUSYBOX_LATEST); final HostConfig expected = HostConfig.builder() .cpuShares(4096L) .cpusetCpus("0,1") .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); assertThat(actual.cpuShares(), equalTo(expected.cpuShares())); assertThat(actual.cpusetCpus(), equalTo(expected.cpusetCpus())); } @Test public void testContainerWithMoreCpuOptions() throws Exception { requireDockerApiVersionAtLeast("1.19", "Container creation with more cpu options"); sut.pull(BUSYBOX_LATEST); final HostConfig expected = HostConfig.builder() .cpuShares(4096L) .cpuPeriod(100000L) .cpuQuota(50000L) .cpusetCpus("0,1") .cpusetMems("0") .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); assertThat(actual.cpuShares(), equalTo(expected.cpuShares())); assertThat(actual.cpuPeriod(), equalTo(expected.cpuPeriod())); assertThat(actual.cpuQuota(), equalTo(expected.cpuQuota())); assertThat(actual.cpusetCpus(), equalTo(expected.cpusetCpus())); } @Test public void testContainerWithBlkioOptions() throws Exception { requireDockerApiVersionAtLeast("1.19", "Container creation with blkio options"); sut.pull(BUSYBOX_LATEST); final HostConfig.Builder hostConfigBuilder = HostConfig.builder(); if (dockerApiVersionAtLeast("1.19")) { hostConfigBuilder.blkioWeight(300); } if (dockerApiVersionAtLeast("1.22")) { // TODO (dxia) Some kernels don't support blkio weight. How detect to skip this check? // hostConfigBuilder.blkioWeightDevice(ImmutableList.of( // HostConfig.BlkioWeightDevice.builder().path("/dev/random").weight(500).build(), // HostConfig.BlkioWeightDevice.builder().path("/dev/urandom").weight(200).build() // )); final List<HostConfig.BlkioDeviceRate> deviceRates = ImmutableList.of( HostConfig.BlkioDeviceRate.builder().path("/dev/loop0").rate(1024).build() ); hostConfigBuilder.blkioDeviceReadBps(deviceRates); hostConfigBuilder.blkioDeviceWriteBps(deviceRates); hostConfigBuilder.blkioDeviceReadIOps(deviceRates); hostConfigBuilder.blkioDeviceWriteIOps(deviceRates); } final HostConfig expected = hostConfigBuilder.build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); if (dockerApiVersionAtLeast("1.19")) { // TODO (dxia) Some kernels don't support blkio weight. How detect to skip this check? // assertThat(actual.blkioWeight(), equalTo(expected.blkioWeight())); } if (dockerApiVersionAtLeast("1.22")) { // TODO (dxia) Some kernels don't support blkio weight device. How detect to skip this check? // assertThat(actual.blkioWeightDevice(), equalTo(expected.blkioWeightDevice())); assertThat(actual.blkioDeviceReadBps(), equalTo(expected.blkioDeviceReadBps())); assertThat(actual.blkioDeviceWriteBps(), equalTo(expected.blkioDeviceWriteBps())); assertThat(actual.blkioDeviceReadIOps(), equalTo(expected.blkioDeviceReadIOps())); assertThat(actual.blkioDeviceWriteBps(), equalTo(expected.blkioDeviceWriteBps())); } } @Test public void testContainerWithMemoryOptions() throws Exception { requireDockerApiVersionNot("1.21", "For some reason this test fails on TravisCI."); sut.pull(BUSYBOX_LATEST); final HostConfig.Builder hostConfigBuilder = HostConfig.builder() .memory(4194304L) .memorySwap(5000000L); if (dockerApiVersionAtLeast("1.20")) { hostConfigBuilder.memorySwappiness(42); } final HostConfig expected = hostConfigBuilder.build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); if (dockerApiVersionAtLeast("1.19")) { // TODO (dxia) Although 1.18 docs implies these two settings are supported by that API // version, travis-CI fails on these two checks. It's not a big deal since we'll probably // stop supporting 1.18 soon. assertThat(actual.memory(), equalTo(expected.memory())); assertThat(actual.memorySwap(), equalTo(expected.memorySwap())); } if (dockerApiVersionAtLeast("1.20")) { assertThat(actual.memorySwappiness(), equalTo(expected.memorySwappiness())); } } @Test public void testContainerWithAppArmorLogs() throws Exception { requireDockerApiVersionAtLeast("1.21", "StopSignal and AppArmorProfile"); sut.pull(BUSYBOX_LATEST); final boolean privileged = true; final boolean publishAllPorts = true; final String dns = "1.2.3.4"; final HostConfig expected = HostConfig.builder().privileged(privileged) .publishAllPorts(publishAllPorts).dns(dns).cpuShares(4096L).build(); final String stopSignal = "SIGTERM"; final ContainerConfig config = ContainerConfig.builder().image(BUSYBOX_LATEST) .hostConfig(expected).stopSignal(stopSignal).build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final ContainerInfo inspection = sut.inspectContainer(id); final HostConfig actual = inspection.hostConfig(); assertThat(actual.privileged(), equalTo(expected.privileged())); assertThat(actual.publishAllPorts(), equalTo(expected.publishAllPorts())); assertThat(actual.dns(), equalTo(expected.dns())); assertThat(actual.cpuShares(), equalTo(expected.cpuShares())); assertThat(sut.inspectContainer(id).config().stopSignal(), equalTo(config.stopSignal())); assertThat(inspection.appArmorProfile(), equalTo("")); assertThat(inspection.execIds(), equalTo(null)); assertThat(inspection.logPath(), containsString(id + "-json.log")); assertThat(inspection.restartCount(), equalTo(0L)); assertThat(inspection.mounts().isEmpty(), equalTo(true)); // Wait for the container to exit sut.waitContainer(id); final List<Container> containers = sut.listContainers(allContainers(), withStatusExited()); Container targetCont = null; for (final Container container : containers) { if (container.id().equals(id)) { targetCont = container; break; } } assertNotNull(targetCont); assertThat(targetCont.imageId(), equalTo(inspection.image())); } @Test public void testContainerWithCpuQuota() throws Exception { requireDockerApiVersionAtLeast("1.19", "Container Creation with HostConfig"); assumeFalse(CIRCLECI); sut.pull(BUSYBOX_LATEST); final boolean privileged = true; final boolean publishAllPorts = true; final String dns = "1.2.3.4"; final HostConfig expected = HostConfig.builder() .privileged(privileged) .publishAllPorts(publishAllPorts) .dns(dns) .cpuQuota(50000L) .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); assertThat(actual.privileged(), equalTo(expected.privileged())); assertThat(actual.publishAllPorts(), equalTo(expected.publishAllPorts())); assertThat(actual.dns(), equalTo(expected.dns())); assertThat(actual.cpuQuota(), equalTo(expected.cpuQuota())); } @Test public void testUpdateContainer() throws Exception { requireDockerApiVersionAtLeast("1.22", "update container"); final String containerName = randomName(); final HostConfig hostConfig = HostConfig.builder() .cpuShares(256L) .build(); final ContainerConfig config = ContainerConfig.builder() .hostConfig(hostConfig) .image(BUSYBOX_LATEST) .build(); sut.pull(BUSYBOX_LATEST); final ContainerCreation container = sut.createContainer(config, containerName); final ContainerInfo containerInfo = sut.inspectContainer(container.id()); assertThat(containerInfo.hostConfig().cpuShares(), is(256L)); final HostConfig newHostConfig = HostConfig.builder() .cpuShares(512L) .build(); final ContainerUpdate containerUpdate = sut.updateContainer(containerInfo.id(), newHostConfig); assertThat(containerUpdate.warnings(), is(nullValue())); final ContainerInfo newContainerInfo = sut.inspectContainer(container.id()); assertThat(newContainerInfo.hostConfig().cpuShares(), is(512L)); } @Test(timeout = 5000) public void testEventStream() throws Exception { // In this test we open an event stream, do stuff, and check that // the events for the stuff we did got pushed over the stream requireDockerApiVersionNot("1.19", "Docker 1.7.x has a bug that breaks DockerClient.events(). " + "So we skip this test."); try (final EventStream eventStream = getImageAndContainerEventStream()) { final String containerName = randomName(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); // Image pull sut.pull(BUSYBOX_LATEST); assertTrue("Docker did not return any events. " + "Expected to see an event for pulling an image.", eventStream.hasNext()); imageEventAssertions(eventStream.next(), BUSYBOX_LATEST, "pull"); // Container create final ContainerCreation container = sut.createContainer(config, containerName); final String containerId = container.id(); assertTrue("Docker did not return enough events. " + "Expected to see an event for creating a container.", eventStream.hasNext()); containerEventAssertions(eventStream.next(), containerId, containerName, "create", BUSYBOX_LATEST); // Container start / container die sut.startContainer(containerId); assertTrue("Docker did not return enough events. " + "Expected to see an event for starting a container.", eventStream.hasNext()); containerEventAssertions(eventStream.next(), containerId, containerName, "start", BUSYBOX_LATEST); assertTrue("Docker did not return enough events. " + "Expected to see an event for the container finishing.", eventStream.hasNext()); containerEventAssertions(eventStream.next(), containerId, containerName, "die", BUSYBOX_LATEST); // Container destroy sut.removeContainer(container.id()); assertTrue("Docker did not return enough events. " + "Expected to see an event for removing the container.", eventStream.hasNext()); containerEventAssertions(eventStream.next(), containerId, containerName, "destroy", BUSYBOX_LATEST); // assertFalse("Expect no more image or container events", eventStream.hasNext()); // NOTE: we cannot make this assertion here. It is a valid assertion, because there // are no more events in the stream. However, the connection is still open. Calling // hasNext() on a stream with an open connection and no events will hang indefinitely. // This will trigger the test's timeout, causing an ERROR and a test failure. } } @Test public void testEventStreamPolling() throws Exception { // In this test we do stuff, then open an event stream for the // time window where we did the stuff, and make sure all the events // we did are in there final String containerName = randomName(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); // Wait once to clean our event "palette" of events from other tests Thread.sleep(1000); final Date start = new Date(); final long startTime = start.getTime() / 1000; sut.pull(BUSYBOX_LATEST); final ContainerCreation container = sut.createContainer(config, containerName); final String containerId = container.id(); sut.startContainer(containerId); await().until(containerIsRunning(sut, containerId), is(false)); sut.removeContainer(containerId); // Wait again to ensure we get back events for everything we did Thread.sleep(1000); final Date end = new Date(); final long endTime = end.getTime() / 1000; // By reading the event stream into a list, we can retain all the events // but still ensure that we are able to close the stream. // In other words, the HTTP connection has been closed. final List<Event> eventList; try (final EventStream stream = getImageAndContainerEventStream(since(startTime), until(endTime))) { eventList = Lists.newArrayList(stream); } assertNotNull(eventList); assertThat(eventList, hasSize(5)); imageEventAssertions(eventList.get(0), BUSYBOX_LATEST, "pull"); // create and start event assertions containerEventAssertions(eventList.get(1), containerId, containerName, "create", BUSYBOX_LATEST); containerEventAssertions(eventList.get(2), containerId, containerName, "start", BUSYBOX_LATEST); containerEventAssertions(eventList.get(3), containerId, containerName, "die", BUSYBOX_LATEST); containerEventAssertions(eventList.get(4), containerId, containerName, "destroy", BUSYBOX_LATEST); } @Test(timeout = 10000) public void testEventTypes() throws Exception { requireDockerApiVersionAtLeast("1.22", "Event types"); final String volumeName = randomName(); final String containerName = randomName(); final String mountPath = "/anywhere"; final Volume volume = Volume.builder().name(volumeName).build(); final HostConfig hostConfig = HostConfig.builder() .binds(Bind.from(volume).to(mountPath).build()) .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(hostConfig) .build(); // Wait once to clean our event "palette" of events from other tests Thread.sleep(1000); final Date start = new Date(); final long startTime = start.getTime() / 1000; sut.pull(BUSYBOX_LATEST); sut.createVolume(volume); final ContainerCreation container = sut.createContainer(config, containerName); final String containerId = container.id(); sut.startContainer(containerId); await().until(containerIsRunning(sut, containerId), is(false)); sut.removeContainer(containerId); // Wait again to ensure we get back events for everything we did Thread.sleep(1000); final Date end = new Date(); final long endTime = end.getTime() / 1000; // Image events try (final EventStream stream = sut.events(since(startTime), until(endTime), type(IMAGE))) { assertTrue("Docker did not return any image events.", stream.hasNext()); imageEventAssertions(stream.next(), BUSYBOX_LATEST, "pull"); assertFalse("Expect no more image events", stream.hasNext()); } // Container events try (final EventStream stream = sut.events(since(startTime), until(endTime), type(CONTAINER))) { assertTrue("Docker did not return any container events.", stream.hasNext()); containerEventAssertions(stream.next(), containerId, containerName, "create", BUSYBOX_LATEST); assertTrue("Docker did not return enough events. " + "Expected to see an event for starting a container.", stream.hasNext()); containerEventAssertions(stream.next(), containerId, containerName, "start", BUSYBOX_LATEST); assertTrue("Docker did not return enough events. " + "Expected to see an event for the container finishing.", stream.hasNext()); containerEventAssertions(stream.next(), containerId, containerName, "die", BUSYBOX_LATEST); assertTrue("Docker did not return enough events. " + "Expected to see an event for removing the container.", stream.hasNext()); containerEventAssertions(stream.next(), containerId, containerName, "destroy", BUSYBOX_LATEST); assertFalse("Expect no more container events", stream.hasNext()); } // Volume events try (final EventStream stream = sut.events(since(startTime), until(endTime), type(VOLUME))) { assertTrue("Docker did not return any volume events.", stream.hasNext()); final Event volumeCreate = stream.next(); assertEquals(VOLUME, volumeCreate.type()); assertEquals("create", volumeCreate.action()); assertEquals(volumeName, volumeCreate.actor().id()); assertThat(volumeCreate.actor().attributes(), hasEntry("driver", "local")); assertNotNull(volumeCreate.timeNano()); assertTrue("Docker did not return enough volume events." + "Expected a volume mount event.", stream.hasNext()); final Event volumeMount = stream.next(); assertEquals(VOLUME, volumeMount.type()); assertEquals("mount", volumeMount.action()); assertEquals(volumeName, volumeMount.actor().id()); final Map<String, String> mountAttributes = volumeMount.actor().attributes(); assertThat(mountAttributes, hasEntry("driver", "local")); assertThat(mountAttributes, hasEntry("container", containerId)); assertThat(mountAttributes, hasEntry("destination", mountPath)); assertThat(mountAttributes, hasEntry("read/write", "true")); assertThat(mountAttributes, hasKey("propagation")); // Default value is system-dependent assertNotNull(volumeMount.timeNano()); assertTrue("Docker did not return enough volume events." + "Expected a volume unmount event.", stream.hasNext()); final Event volumeUnmount = stream.next(); assertEquals(VOLUME, volumeUnmount.type()); assertEquals("unmount", volumeUnmount.action()); assertEquals(volumeName, volumeUnmount.actor().id()); assertThat(volumeUnmount.actor().attributes(), hasEntry("driver", "local")); assertThat(volumeUnmount.actor().attributes(), hasEntry("container", containerId)); assertNotNull(volumeUnmount.timeNano()); assertFalse("Expect no more volume events", stream.hasNext()); } // Network events try (final EventStream stream = sut.events(since(startTime), until(endTime), type(NETWORK))) { assertTrue("Docker did not return any network events.", stream.hasNext()); final Event networkConnect = stream.next(); assertEquals(NETWORK, networkConnect.type()); assertEquals("connect", networkConnect.action()); assertNotNull(networkConnect.actor().id()); // not sure how to get the network id assertThat(networkConnect.actor().attributes(), hasEntry("container", containerId)); assertThat(networkConnect.actor().attributes(), hasEntry("name", "bridge")); assertThat(networkConnect.actor().attributes(), hasEntry("type", "bridge")); assertTrue("Docker did not return enough network events." + "Expected a network disconnect event.", stream.hasNext()); final Event networkDisconnect = stream.next(); assertEquals(NETWORK, networkDisconnect.type()); assertEquals("disconnect", networkDisconnect.action()); assertEquals(networkDisconnect.actor().id(), networkDisconnect.actor().id()); assertThat(networkDisconnect.actor().attributes(), hasEntry("container", containerId)); assertThat(networkDisconnect.actor().attributes(), hasEntry("name", "bridge")); assertThat(networkDisconnect.actor().attributes(), hasEntry("type", "bridge")); assertFalse("Expect no more network events", stream.hasNext()); } } private EventStream getImageAndContainerEventStream(final EventsParam... eventsParams) throws Exception { // I only want my event streams to contain image and container events. // For API 1.22 and greater, we can use the type() filter. But for earlier versions, // that filter didn't exist, so we should not add it. if (dockerApiVersionAtLeast("1.22")) { final int originalNumberOfParams = eventsParams.length; final EventsParam[] eventsParamsWithTypes = Arrays.copyOf(eventsParams, originalNumberOfParams + 2); eventsParamsWithTypes[originalNumberOfParams] = type(IMAGE); eventsParamsWithTypes[originalNumberOfParams + 1] = type(CONTAINER); return sut.events(eventsParamsWithTypes); } return sut.events(eventsParams); } @Test public void testEventFiltersWithSpaces() throws Exception { requireDockerApiVersionAtLeast("1.24", "Docker events and health check"); sut.pull(BUSYBOX_LATEST); final String containerName = randomName(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sleep", "5") // Generate some healthy_status events .healthcheck(Healthcheck.create(ImmutableList.of("CMD-SHELL", "true"), 1000000000L, 1000000000L, 3)) .build(); final long startTime = new Date().getTime() / 1000; final ContainerCreation container = sut.createContainer(config, containerName); final String containerId = container.id(); sut.startContainer(containerId); sut.waitContainer(containerId); sut.removeContainer(containerId); final long endTime = new Date().getTime() / 1000; try (final EventStream stream = sut.events(since(startTime), until(endTime), EventsParam.event("health_status: healthy") )) { assertTrue("Docker did not return any container events.", stream.hasNext()); containerEventAssertions(stream.next(), containerId, containerName, "health_status: healthy", BUSYBOX_LATEST); } } @SuppressWarnings("deprecation") private void imageEventAssertions(final Event event, final String imageName, final String action) throws Exception { assertThat(event.time(), notNullValue()); if (dockerApiVersionAtLeast("1.22")) { assertEquals(IMAGE, event.type()); assertEquals(action, event.action()); assertEquals(imageName, event.actor().id()); assertNotNull(event.timeNano()); } else { assertEquals(action, event.status()); assertThat(event.id(), equalTo(imageName)); } } @SuppressWarnings("deprecation") private void containerEventAssertions(final Event event, final String containerId, final String containerName, final String action, final String imageName) throws Exception { assertThat(event.time(), notNullValue()); if (dockerApiVersionAtLeast("1.22")) { assertEquals(CONTAINER, event.type()); assertEquals(action, event.action()); assertNotNull(event.actor()); assertEquals(containerId, event.actor().id()); final Map<String, String> attributes = event.actor().attributes(); assertThat(attributes, hasEntry("image", imageName)); assertThat(attributes, hasEntry("name", containerName)); assertNotNull(event.timeNano()); } else { assertEquals(action, event.status()); assertEquals(containerId, event.id()); assertEquals(imageName, event.from()); } } @Test public void testListImages() throws Exception { sut.pull(BUSYBOX_LATEST); final List<Image> images = sut.listImages(); assertThat(images.size(), greaterThan(0)); // Verify that image contains valid values Image busybox = null; for (final Image image : images) { if (image.repoTags() != null && image.repoTags().contains(BUSYBOX_LATEST)) { busybox = image; } } assertNotNull(busybox); assertThat(busybox.virtualSize(), greaterThan(0L)); assertThat(busybox.created(), not(isEmptyOrNullString())); assertThat(busybox.id(), not(isEmptyOrNullString())); assertThat(busybox.repoTags(), notNullValue()); assertThat(busybox.repoTags().size(), greaterThan(0)); assertThat(BUSYBOX_LATEST, isIn(busybox.repoTags())); if (dockerApiVersionLessThan("1.22")) { assertThat(busybox.parentId(), not(isEmptyOrNullString())); } final List<Image> imagesWithDigests = sut.listImages(digests()); assertThat(imagesWithDigests.size(), greaterThan(0)); busybox = null; for (final Image image : imagesWithDigests) { if (image.repoTags() != null && image.repoTags().contains(BUSYBOX_LATEST)) { busybox = image; } } assertNotNull(busybox); if (dockerApiVersionLessThan("1.22")) { assertThat(busybox.repoDigests(), notNullValue()); } // Using allImages() should give us more images final List<Image> allImages = sut.listImages(allImages()); assertThat(allImages.size(), greaterThan(images.size())); // Including just dangling images should give us fewer images final List<Image> danglingImages = sut.listImages(danglingImages()); assertThat(danglingImages.size(), lessThan(images.size())); // Specifying both allImages() and danglingImages() should give us only dangling images final List<Image> allAndDanglingImages = sut.listImages(allImages(), danglingImages()); assertThat(allAndDanglingImages.size(), equalTo(danglingImages.size())); // Can list by name final List<Image> imagesByName = sut.listImages(byName(BUSYBOX)); assertThat(imagesByName.size(), greaterThan(0)); final Set<String> repoTags = Sets.newHashSet(); for (final Image imageByName : imagesByName) { if (imageByName.repoTags() != null) { repoTags.addAll(imageByName.repoTags()); } } assertThat(BUSYBOX_LATEST, isIn(repoTags)); } @Test public void testDockerDateFormat() throws Exception { // This is the created date for busybox converted from nanoseconds to milliseconds final Date expected = new StdDateFormat().parse("2015-09-18T17:44:28.145Z"); // Verify the formatter works when used with the client sut.pull(BUSYBOX_BUILDROOT_2013_08_1); final ImageInfo imageInfo = sut.inspectImage(BUSYBOX_BUILDROOT_2013_08_1); assertThat(imageInfo.created(), equalTo(expected)); } @SuppressWarnings("EmptyCatchBlock") @Test public void testSsl() throws Exception { assumeFalse(TRAVIS); // Build a run a container that contains a Docker instance configured with our SSL cert/key final String imageName = "test-docker-ssl"; final String expose = "2376/tcp"; final String dockerDirectory = Resources.getResource("dockerSslDirectory").getPath(); sut.build(Paths.get(dockerDirectory), imageName); final HostConfig hostConfig = HostConfig.builder() .privileged(true) .publishAllPorts(true) .build(); final ContainerConfig containerConfig = ContainerConfig.builder() .image(imageName) .exposedPorts(expose) .hostConfig(hostConfig) .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); // Determine where the Docker instance inside the container we just started is exposed final String host; if (dockerEndpoint.getScheme().equalsIgnoreCase("unix")) { host = "localhost"; } else { host = dockerEndpoint.getHost(); } final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(true)); final String port = containerInfo.networkSettings().ports().get(expose).get(0).hostPort(); // Try to connect using SSL and our known cert/key final DockerCertificates certs = new DockerCertificates(Paths.get(dockerDirectory)); final DockerClient c = new DefaultDockerClient(URI.create(format("https://%s:%s", host, port)), certs); // We need to wait for the docker process inside the docker container to be ready to accept // connections on the port. Otherwise, this test will fail. // Even though we've checked that the container is running, this doesn't mean the process // inside the container is ready. final long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(TimeUnit.MINUTES.toMillis(1)); while (System.nanoTime() < deadline) { try (final Socket ignored = new Socket(host, Integer.parseInt(port))) { break; } catch (IOException ignored) { } Thread.sleep(500); } assertThat(c.ping(), equalTo("OK")); sut.stopContainer(containerId, 10); } @Test public void testPauseContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); // Must be running { final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().running(), equalTo(true)); } sut.pauseContainer(containerId); // Must be paused { final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().paused(), equalTo(true)); } sut.unpauseContainer(containerId); // Must no longer be paused { final ContainerInfo containerInfo = sut.inspectContainer(containerId); assertThat(containerInfo.state().paused(), equalTo(false)); } } @Test public void testExtraHosts() throws Exception { requireDockerApiVersionAtLeast("1.15", "Container Creation with HostConfig.ExtraHosts"); sut.pull(BUSYBOX_LATEST); final HostConfig expected = HostConfig.builder() .extraHosts("extrahost:1.2.3.4") .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .cmd("sh", "-c", "cat /etc/hosts | grep extrahost") .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); assertThat(actual.extraHosts(), equalTo(expected.extraHosts())); final String logs; try (LogStream stream = sut.logs(id, stdout(), stderr())) { logs = stream.readFully(); } assertThat(logs, containsString("1.2.3.4")); } @Test(expected = IllegalArgumentException.class) public void testInvalidExtraHosts() throws Exception { final HostConfig expected = HostConfig.builder() .extraHosts("extrahost") .build(); } @Test public void testLogDriver() throws Exception { requireDockerApiVersionAtLeast("1.21", "Container Creation with HostConfig.LogConfig"); sut.pull(BUSYBOX_LATEST); final String name = randomName(); final Map<String, String> logOptions = new HashMap<>(); logOptions.put("max-size", "10k"); logOptions.put("max-file", "2"); logOptions.put("labels", name); final LogConfig logConfig = LogConfig.create("json-file", logOptions); assertThat(logConfig.logType(), equalTo("json-file")); assertThat(logConfig.logOptions(), equalTo(logOptions)); final HostConfig expected = HostConfig.builder() .logConfig(logConfig) .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(expected) .build(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); sut.startContainer(id); final HostConfig actual = sut.inspectContainer(id).hostConfig(); assertThat(actual.logConfig(), equalTo(expected.logConfig())); } @Test @SuppressWarnings("deprecation") public void testContainerVolumesOldStyle() throws Exception { requireDockerApiVersionLessThan( "1.20", "Creating a container with volumes and inspecting volumes in old style"); sut.pull(BUSYBOX_LATEST); final HostConfig hostConfig = HostConfig.builder() .binds(Bind.from("/local/path") .to("/remote/path") .build()) .build(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .addVolume("/foo") .hostConfig(hostConfig) .build(); final String id = sut.createContainer(volumeConfig, randomName()).id(); final ContainerInfo volumeContainer = sut.inspectContainer(id); final List<String> expectedDestinations = newArrayList("/foo", "/remote/path"); final Set<String> actualDestinations = volumeContainer.volumes().keySet(); // To make sure two sets are equal, when they may be in different orders, // we check that each one contains all the elements of the other. // Equivalent to, in math, proving two sets are one-to-one by proving // they are injective ("into") and surjective ("onto"). assertThat(actualDestinations, everyItem(isIn(expectedDestinations))); assertThat(expectedDestinations, everyItem(isIn(actualDestinations))); // The local paths returned from ContainerInfo.volumes() are paths in the docker // file system. So they are not predictable (at least by me, the test writer, // John Flavin.) However, the local path we asked for will always be included as part of // the path that is returned. So we can just check that one in the list of items // we got back contains our expected path. final String expectedLocalPath = "/local/path"; assertThat(volumeContainer.volumes().values(), hasItem(containsString(expectedLocalPath))); assertThat(volumeContainer.config().volumeNames(), hasItem("/foo")); } @Test public void testContainerVolumeNoCopy() throws Exception { requireDockerApiVersionAtLeast( "1.23", "Creating a container with volumes with nocopy mode"); sut.pull(BUSYBOX_LATEST); final String aVolumeName = "avolume"; final String aVolumeTo = "/some/path"; final String nocopyVolumeName = "avolume2"; final String nocopyVolumeTo = "/some/other/path"; sut.createVolume(Volume.builder().name(aVolumeName).build()); sut.createVolume(Volume.builder().name(nocopyVolumeName).build()); final HostConfig hostConfig = HostConfig.builder() .appendBinds(Bind.from(aVolumeName) .to(aVolumeTo) .readOnly(true) .build()) .appendBinds(Bind.from(nocopyVolumeName) .to(nocopyVolumeTo) .noCopy(true) .build()) .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(hostConfig) .build(); final String id = sut.createContainer(config, randomName()).id(); final ContainerInfo info = sut.inspectContainer(id); final List<ContainerMount> mounts = info.mounts(); assertThat(mounts.size(), equalTo(2)); { final ContainerMount aMount = Iterables.find(mounts, new Predicate<ContainerMount>() { @Override public boolean apply(ContainerMount mount) { return !("nocopy".equals(mount.mode())); } }, null); assertThat("Could not find a mount (without nocopy)", aMount, notNullValue()); assertThat(aMount.mode(), is(equalTo("ro"))); assertThat(aMount.rw(), is(false)); assertThat(aMount.source(), containsString("/" + aVolumeName + "/")); assertThat(aMount.destination(), is(equalTo(aVolumeTo))); } { final ContainerMount nocopyMount = Iterables.find(mounts, new Predicate<ContainerMount>() { @Override public boolean apply(ContainerMount mount) { return "nocopy".equals(mount.mode()); } }, null); assertThat("Could not find mount (with nocopy)", nocopyMount, notNullValue()); assertThat(nocopyMount.mode(), is(equalTo("nocopy"))); assertThat(nocopyMount.rw(), is(true)); assertThat(nocopyMount.source(), containsString("/" + nocopyVolumeName + "/")); assertThat(nocopyMount.destination(), is(equalTo(nocopyVolumeTo))); } } @Test public void testContainerVolumes() throws Exception { requireDockerApiVersionAtLeast( "1.20", "Creating a container with volumes and inspecting volumes"); sut.pull(BUSYBOX_LATEST); final String namedVolumeName = "aVolume"; final String namedVolumeFrom = "/a/host/path"; final String namedVolumeTo = "/a/destination/path"; final String bindObjectFrom = "/some/path"; final String bindObjectTo = "/some/other/path"; final String bindStringFrom = "/local/path"; final String bindStringTo = "/remote/path"; final String anonVolumeTo = "/foo"; final Volume volume = Volume.builder() .name(namedVolumeName) .mountpoint(namedVolumeFrom) .build(); sut.createVolume(volume); final Bind bindUsingVolume = Bind.from(volume) .to(namedVolumeTo) .build(); final Bind bind = Bind.from(bindObjectFrom) .to(bindObjectTo) .readOnly(true) .build(); final HostConfig hostConfig = HostConfig.builder() .appendBinds(bind) .appendBinds(bindStringFrom + ":" + bindStringTo) .appendBinds(bindUsingVolume) .build(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .addVolume(anonVolumeTo) .hostConfig(hostConfig) .build(); final String id = sut.createContainer(volumeConfig, randomName()).id(); final ContainerInfo containerInfo = sut.inspectContainer(id); final List<ContainerMount> mounts = containerInfo.mounts(); assertThat(mounts.size(), equalTo(4)); { final ContainerMount bindObjectMount = Iterables.find(mounts, new Predicate<ContainerMount>() { @Override public boolean apply(ContainerMount mount) { return bindObjectFrom.equals(mount.source()); } }, null); assertThat("Did not find mount from bind object", bindObjectMount, notNullValue()); assertThat(bindObjectMount.source(), is(bindObjectFrom)); assertThat(bindObjectMount.destination(), is(bindObjectTo)); assertThat(bindObjectMount.driver(), isEmptyOrNullString()); assertThat(bindObjectMount.rw(), is(false)); assertThat(bindObjectMount.mode(), is(equalTo("ro"))); if (dockerApiVersionAtLeast("1.25")) { assertThat(bindObjectMount.name(), isEmptyOrNullString()); assertThat(bindObjectMount.propagation(), isEmptyOrNullString()); } else if (dockerApiVersionAtLeast("1.22")) { assertThat(bindObjectMount.name(), isEmptyOrNullString()); assertThat(bindObjectMount.propagation(), is(equalTo("rprivate"))); } else { assertThat(bindObjectMount.name(), is(nullValue())); assertThat(bindObjectMount.propagation(), is(nullValue())); } if (dockerApiVersionAtLeast("1.26")) { assertThat(bindObjectMount.type(), is(equalTo("bind"))); assertThat(bindObjectMount.driver(), isEmptyOrNullString()); } else { assertThat(bindObjectMount.type(), is(nullValue())); assertThat(bindObjectMount.driver(), is(nullValue())); } } { final ContainerMount bindStringMount = Iterables.find(mounts, new Predicate<ContainerMount>() { @Override public boolean apply(ContainerMount mount) { return bindStringFrom.equals(mount.source()); } }, null); assertThat("Did not find mount from bind string", bindStringMount, notNullValue()); assertThat(bindStringMount.source(), is(equalTo(bindStringFrom))); assertThat(bindStringMount.destination(), is(equalTo(bindStringTo))); assertThat(bindStringMount.driver(), isEmptyOrNullString()); assertThat(bindStringMount.rw(), is(true)); assertThat(bindStringMount.mode(), is(equalTo(""))); if (dockerApiVersionAtLeast("1.25")) { assertThat(bindStringMount.name(), isEmptyOrNullString()); assertThat(bindStringMount.propagation(), isEmptyOrNullString()); } else if (dockerApiVersionAtLeast("1.22")) { assertThat(bindStringMount.name(), isEmptyOrNullString()); assertThat(bindStringMount.propagation(), is(equalTo("rprivate"))); } else { assertThat(bindStringMount.name(), is(nullValue())); assertThat(bindStringMount.propagation(), is(nullValue())); } if (dockerApiVersionAtLeast("1.26")) { assertThat(bindStringMount.type(), is(equalTo("bind"))); assertThat(bindStringMount.driver(), isEmptyOrNullString()); } else { assertThat(bindStringMount.type(), is(nullValue())); assertThat(bindStringMount.driver(), is(nullValue())); } } { final ContainerMount namedVolumeMount = Iterables.find(mounts, new Predicate<ContainerMount>() { @Override public boolean apply(ContainerMount mount) { return namedVolumeTo.equals(mount.destination()); } }, null); assertThat("Did not find mount from named volume", namedVolumeMount, notNullValue()); assertThat(namedVolumeMount.name(), is(equalTo(namedVolumeName))); assertThat(namedVolumeMount.source(), containsString("/" + namedVolumeName + "/")); assertThat(namedVolumeMount.destination(), is(equalTo(namedVolumeTo))); assertThat(namedVolumeMount.rw(), is(true)); assertThat(namedVolumeMount.mode(), is(equalTo("z"))); assertThat(namedVolumeMount.name(), is(namedVolumeName)); assertThat(namedVolumeMount.driver(), is(equalTo("local"))); if (dockerApiVersionAtLeast("1.25")) { assertThat(namedVolumeMount.propagation(), isEmptyOrNullString()); } else if (dockerApiVersionAtLeast("1.22")) { assertThat(namedVolumeMount.propagation(), is(equalTo("rprivate"))); } else { assertThat(namedVolumeMount.propagation(), is(nullValue())); } if (dockerApiVersionAtLeast("1.26")) { assertThat(namedVolumeMount.type(), is(equalTo("volume"))); } else { assertThat(namedVolumeMount.type(), is(nullValue())); } } { final ContainerMount anonVolumeMount = Iterables.find(mounts, new Predicate<ContainerMount>() { @Override public boolean apply(ContainerMount mount) { return anonVolumeTo.equals(mount.destination()); } }, null); assertThat("Did not find mount from anonymous volume", anonVolumeMount, notNullValue()); assertThat(anonVolumeMount.source(), containsString("/" + anonVolumeMount.name() + "/")); assertThat(anonVolumeMount.destination(), is(equalTo(anonVolumeTo))); assertThat(anonVolumeMount.mode(), isEmptyOrNullString()); assertThat(anonVolumeMount.rw(), is(true)); assertThat(anonVolumeMount.mode(), is(equalTo(""))); assertThat(anonVolumeMount.name(), not(isEmptyOrNullString())); assertThat(anonVolumeMount.driver(), is(equalTo("local"))); if (dockerApiVersionAtLeast("1.22")) { assertThat(anonVolumeMount.propagation(), isEmptyOrNullString()); } else { assertThat(anonVolumeMount.propagation(), is(nullValue())); } if (dockerApiVersionAtLeast("1.26")) { assertThat(anonVolumeMount.type(), is(equalTo("volume"))); } else { assertThat(anonVolumeMount.type(), is(nullValue())); } } assertThat(containerInfo.config().volumeNames(), hasItem(anonVolumeTo)); } @Test public void testVolumesFrom() throws Exception { sut.pull(BUSYBOX_LATEST); final String volumeContainer = randomName(); final String mountContainer = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .addVolume("/foo") .cmd("touch", "/foo/bar") .build(); sut.createContainer(volumeConfig, volumeContainer); sut.startContainer(volumeContainer); sut.waitContainer(volumeContainer); final HostConfig mountHostConfig = HostConfig.builder() .volumesFrom(volumeContainer) .build(); final ContainerConfig mountConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(mountHostConfig) .cmd("ls", "/foo") .build(); sut.createContainer(mountConfig, mountContainer); sut.startContainer(mountContainer); sut.waitContainer(mountContainer); final ContainerInfo info = sut.inspectContainer(mountContainer); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); final String logs; try (LogStream stream = sut.logs(info.id(), stdout(), stderr())) { logs = stream.readFully(); } assertThat(logs, containsString("bar")); } @Test public void testAttachContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final String volumeContainer = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .addVolume("/foo") // TODO (mbrown): remove sleep - added to make sure container is still alive when attaching //.cmd("ls", "-la") .cmd("sh", "-c", "ls -la; sleep 3") .build(); sut.createContainer(volumeConfig, volumeContainer); sut.startContainer(volumeContainer); final String logs; try (LogStream stream = sut.attachContainer(volumeContainer, AttachParameter.LOGS, AttachParameter.STDOUT, AttachParameter.STDERR, AttachParameter.STREAM)) { logs = stream.readFully(); } assertThat(logs, containsString("total")); sut.waitContainer(volumeContainer); final ContainerInfo info = sut.inspectContainer(volumeContainer); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); } @Test public void testAttachExitedContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final String volumeContainer = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("ls", "-la") .build(); final ContainerCreation container = sut.createContainer(volumeConfig, volumeContainer); sut.startContainer(volumeContainer); exception.expect(IllegalStateException.class); exception.expectMessage(containsString("is not running")); await().until(containerIsRunning(sut, container.id()), is(false)); sut.attachContainer(volumeContainer, AttachParameter.LOGS, AttachParameter.STDOUT, AttachParameter.STDERR, AttachParameter.STREAM); } @Test public void testLogNoTimeout() throws Exception { final String volumeContainer = createSleepingContainer(); final StringBuffer result = new StringBuffer(); try (final LogStream stream = sut.logs(volumeContainer, stdout(), stderr(), follow())) { try { while (stream.hasNext()) { final String r = UTF_8.decode(stream.next().content()).toString(); log.info(r); result.append(r); } } catch (Exception e) { log.info(e.getMessage()); } } verifyNoTimeoutContainer(volumeContainer, result); } @Test public void testAttachLogNoTimeout() throws Exception { final String volumeContainer = createSleepingContainer(); final StringBuffer result = new StringBuffer(); try (final LogStream stream = sut.attachContainer( volumeContainer, AttachParameter.STDOUT, AttachParameter.STDERR, AttachParameter.STREAM, AttachParameter.STDIN)) { try { while (stream.hasNext()) { final String r = UTF_8.decode(stream.next().content()).toString(); log.info(r); result.append(r); } } catch (Exception e) { log.info(e.getMessage()); } } verifyNoTimeoutContainer(volumeContainer, result); } @Test public void testLogsNoStdOut() throws Exception { sut.pull(BUSYBOX_LATEST); final String container = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "echo This message goes to stdout && echo This message goes to stderr 1>&2") .build(); sut.createContainer(volumeConfig, container); sut.startContainer(container); sut.waitContainer(container); final ContainerInfo info = sut.inspectContainer(container); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); final String logs; try (LogStream stream = sut.logs(info.id(), stdout(false), stderr())) { logs = stream.readFully(); } assertThat(logs, containsString("This message goes to stderr")); assertThat(logs, not(containsString("This message goes to stdout"))); } @Test public void testLogsNoStdErr() throws Exception { sut.pull(BUSYBOX_LATEST); final String container = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "echo This message goes to stdout && echo This message goes to stderr 1>&2") .build(); sut.createContainer(volumeConfig, container); sut.startContainer(container); sut.waitContainer(container); final ContainerInfo info = sut.inspectContainer(container); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); final String logs; try (LogStream stream = sut.logs(info.id(), stdout(), stderr(false))) { logs = stream.readFully(); } assertThat(logs, containsString("This message goes to stdout")); assertThat(logs, not(containsString("This message goes to stderr"))); } @Test public void testLogsTimestamps() throws Exception { sut.pull(BUSYBOX_LATEST); final String container = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("echo", "This message should have a timestamp") .build(); sut.createContainer(volumeConfig, container); sut.startContainer(container); sut.waitContainer(container); final ContainerInfo info = sut.inspectContainer(container); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); final String logs; try (LogStream stream = sut.logs(info.id(), stdout(), stderr(), timestamps())) { logs = stream.readFully(); } final Pattern timestampPattern = Pattern.compile( "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+Z.*$", Pattern.DOTALL); assertTrue(timestampPattern.matcher(logs).matches()); assertThat(logs, containsString("This message should have a timestamp")); } @Test public void testLogsTail() throws Exception { sut.pull(BUSYBOX_LATEST); final String container = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "echo 1 && echo 2 && echo 3 && echo 4") .build(); sut.createContainer(volumeConfig, container); sut.startContainer(container); sut.waitContainer(container); final ContainerInfo info = sut.inspectContainer(container); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); final String logs; try (LogStream stream = sut.logs(info.id(), stdout(), stderr(), tail(2))) { logs = stream.readFully(); } assertThat(logs, not(containsString("1"))); assertThat(logs, not(containsString("2"))); assertThat(logs, containsString("3")); assertThat(logs, containsString("4")); } @Test public void testLogsSince() throws Exception { requireDockerApiVersionAtLeast("1.19", "/logs?since=timestamp"); sut.pull(BUSYBOX_LATEST); final String container = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("echo", "This was printed too late") .build(); sut.createContainer(volumeConfig, container); sut.startContainer(container); sut.waitContainer(container); final ContainerInfo info = sut.inspectContainer(container); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); final String logs; // Get logs since the current timestamp. This should return nothing. try (LogStream stream = sut.logs(info.id(), stdout(), stderr(), since((int) (System.currentTimeMillis() / 1000L)))) { logs = stream.readFully(); } assertThat(logs, not(containsString("This message was printed too late"))); } @Test public void testLogsTty() throws DockerException, InterruptedException { final String container = randomName(); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .attachStdout(true) .tty(true) .cmd("sh", "-c", "ls") .build(); sut.createContainer(containerConfig, container); sut.startContainer(container); final LogStream logStream = sut.logs(container, DockerClient.LogsParam.stdout()); while (logStream.hasNext()) { final String line = UTF_8.decode(logStream.next().content()).toString(); log.info(line); } sut.waitContainer(container); final ContainerInfo info = sut.inspectContainer(container); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); } @Test(expected = ContainerNotFoundException.class) public void testStartBadContainer() throws Exception { sut.startContainer(randomName()); } @Test(expected = ImageNotFoundException.class) public void testCreateContainerWithBadImage() throws Exception { final ContainerConfig config = ContainerConfig.builder() .image(randomName()) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String name = randomName(); sut.createContainer(config, name); } @Test public void testCreateContainerNameMatcher() throws Exception { final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "echo hello world") .build(); final String goodName = "aBc1.2-3_"; sut.createContainer(config, goodName); // Bad names final String oneCharacter = "a"; exception.expect(invalidContainerNameException(oneCharacter)); sut.createContainer(config, oneCharacter); final String invalidCharacter = "abc~"; exception.expect(invalidContainerNameException(invalidCharacter)); sut.createContainer(config, invalidCharacter); final String invalidFirstCharacter = ".a"; exception.expect(invalidContainerNameException(invalidFirstCharacter)); sut.createContainer(config, invalidFirstCharacter); } private static Matcher<IllegalArgumentException> invalidContainerNameException(final String containerName) { final String exceptionMessage = String.format("Invalid container name: \"%s\"", containerName); final String description = "for container name " + containerName; return new CustomTypeSafeMatcher<IllegalArgumentException>(description) { @Override protected boolean matchesSafely(final IllegalArgumentException ex) { return ex.getMessage().equals(exceptionMessage); } }; } @Test(expected = ContainerNotFoundException.class) public void testKillBadContainer() throws Exception { sut.killContainer(randomName()); } @Test(expected = ContainerNotFoundException.class) public void testKillBadContainerWithSignal() throws Exception { sut.killContainer(randomName(), DockerClient.Signal.SIGKILL); } @Test(expected = ContainerNotFoundException.class) public void testPauseBadContainer() throws Exception { sut.pauseContainer(randomName()); } @Test(expected = ContainerNotFoundException.class) public void testRemoveBadContainer() throws Exception { sut.removeContainer(randomName()); } @Test(expected = ContainerNotFoundException.class) public void testRestartBadContainer() throws Exception { sut.restartContainer(randomName()); } @Test(expected = ContainerNotFoundException.class) public void testStopBadContainer() throws Exception { sut.stopContainer(randomName(), 10); } @Test(expected = ImageNotFoundException.class) public void testTagBadImage() throws Exception { sut.tag(randomName(), randomName()); } @Test(expected = ContainerNotFoundException.class) public void testUnpauseBadContainer() throws Exception { sut.unpauseContainer(randomName()); } @Test(expected = ImageNotFoundException.class) public void testRemoveBadImage() throws Exception { sut.removeImage(randomName()); } @Test public void testExec() throws Exception { requireDockerApiVersionAtLeast("1.15", "Exec"); // CircleCI uses lxc, doesn't support exec - https://circleci.com/docs/docker/#docker-exec assumeFalse(CIRCLECI); sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); final ExecCreation execCreation = sut.execCreate( containerId, new String[] {"ls", "-la"}, ExecCreateParam.attachStdout(), ExecCreateParam.attachStderr()); final String execId = execCreation.id(); log.info("execId = {}", execId); try (final LogStream stream = sut.execStart(execId)) { final String output = stream.readFully(); log.info("Result:\n{}", output); assertThat(output, containsString("total")); } } @Test public void testExecCreateOnNonRunningContainer() throws Exception { requireDockerApiVersionAtLeast("1.15", "Exec"); // Execution driver is removed in Docker API >= 1.24 if (dockerApiVersionLessThan("1.24")) { // CircleCI uses lxc, doesn't support exec - https://circleci.com/docs/docker/#docker-exec assumeFalse(CIRCLECI); } sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("ls", "-la") .build(); final ContainerCreation container = sut.createContainer(containerConfig, randomName()); exception.expect(IllegalStateException.class); exception.expectMessage(containsString("is not running")); sut.execCreate(container.id(), new String[] {"ls", "-la"}); sut.startContainer(container.id()); await().until(containerIsRunning(sut, container.id()), is(false)); sut.execCreate(container.id(), new String[] {"ls", "-la"}); } @Test public void testExecInspect() throws Exception { requireDockerApiVersionAtLeast("1.16", "Exec Inspect"); // CircleCI uses lxc, doesn't support exec - https://circleci.com/docs/docker/#docker-exec assumeFalse(CIRCLECI); sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); final List<ExecCreateParam> createParams = newArrayList( ExecCreateParam.attachStdout(), ExecCreateParam.attachStderr(), ExecCreateParam.attachStdin(), ExecCreateParam.tty()); // some functionality in this test depends on API 1.19 (exec user) final boolean execUserSupported = dockerApiVersionAtLeast("1.19"); if (execUserSupported) { createParams.add(ExecCreateParam.user("1000")); } final ExecCreation execCreation = sut.execCreate( containerId, new String[] {"sh", "-c", "exit 2"}, createParams.toArray(new ExecCreateParam[createParams.size()])); final String execId = execCreation.id(); log.info("execId = {}", execId); final ExecState notStarted = sut.execInspect(execId); assertThat(notStarted.id(), is(execId)); assertThat(notStarted.running(), is(false)); if (dockerApiVersionLessThan("1.22")) { assertThat(notStarted.exitCode(), is(0)); } else { assertThat(notStarted.exitCode(), nullValue()); } assertThat(notStarted.openStdin(), is(true)); assertThat(notStarted.openStderr(), is(true)); assertThat(notStarted.openStdout(), is(true)); try (final LogStream stream = sut.execStart(execId)) { stream.readFully(); } final ExecState started = sut.execInspect(execId); assertThat(started.id(), is(execId)); assertThat(started.running(), is(false)); assertThat(started.exitCode(), is(2)); assertThat(started.openStdin(), is(true)); assertThat(started.openStderr(), is(true)); assertThat(started.openStdout(), is(true)); final ProcessConfig processConfig = started.processConfig(); assertThat(processConfig.privileged(), is(false)); if (execUserSupported) { assertThat(processConfig.user(), is("1000")); } assertThat(processConfig.tty(), is(true)); assertThat(processConfig.entrypoint(), is("sh")); assertThat(processConfig.arguments(), Matchers.<List<String>>is(ImmutableList.of("-c", "exit 2"))); if (dockerApiVersionLessThan("1.22")) { final ContainerInfo containerInfo = started.container(); assertThat(containerInfo.path(), is("sh")); assertThat(containerInfo.args(), Matchers.<List<String>>is(ImmutableList.of("-c", "while :; do sleep 1; done"))); assertThat(containerInfo.config().image(), is(BUSYBOX_LATEST)); } else { assertNotNull(started.containerId(), "containerId"); } } @Test public void testExecInspectNoUser() throws Exception { requireDockerApiVersionAtLeast("1.16", "Exec Inspect"); sut.pull(BUSYBOX_LATEST); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); sut.startContainer(containerId); final List<ExecCreateParam> createParams = newArrayList( ExecCreateParam.attachStdout(), ExecCreateParam.attachStderr(), ExecCreateParam.attachStdin(), ExecCreateParam.tty()); final ExecCreation execCreation = sut.execCreate( containerId, new String[] {"sh", "-c", "exit 2"}, createParams.toArray(new ExecCreateParam[createParams.size()])); final String execId = execCreation.id(); log.info("execId = {}", execId); try (final LogStream stream = sut.execStart(execId)) { stream.readFully(); } final ExecState state = sut.execInspect(execId); assertThat(state.id(), is(execId)); final ProcessConfig processConfig = state.processConfig(); assertThat(processConfig.user(), isEmptyOrNullString()); } @Test public void testListContainers() throws Exception { sut.pull(BUSYBOX_LATEST); final String label = "foo"; final String labelValue = "bar"; final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .labels(ImmutableMap.of(label, labelValue)) .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); // filters={"status":["created"]} // can only filter by created status in docker API version >= 1.20 - the status of "created" // did not exist in docker prior to 1.8.0 final DockerClient.ListContainersParam[] createdParams = dockerApiVersionAtLeast("1.20") ? new DockerClient.ListContainersParam[] {allContainers(), withStatusCreated()} : new DockerClient.ListContainersParam[] {allContainers()}; final List<Container> created = sut.listContainers(createdParams); assertThat("listContainers is unexpectedly empty", created, not(empty())); assertThat(containerId, isIn(containersToIds(created))); // filters={"status":["running"]} sut.startContainer(containerId); final List<Container> running = sut.listContainers(withStatusRunning()); assertThat(containerId, isIn(containersToIds(running))); // filters={"status":["paused"]} sut.pauseContainer(containerId); final List<Container> paused = sut.listContainers(withStatusPaused()); assertThat(containerId, isIn(containersToIds(paused))); // filters={"status":["exited"]} sut.unpauseContainer(containerId); sut.stopContainer(containerId, 0); final List<Container> allExited = sut.listContainers(allContainers(), withStatusExited()); assertThat(containerId, isIn(containersToIds(allExited))); // filters={"status":["created","paused","exited"]} // Will work, i.e. multiple "status" filters are ORed final List<Container> multipleStati = sut.listContainers( allContainers(), withStatusCreated(), withStatusPaused(), withStatusExited()); assertThat(containerId, isIn(containersToIds(multipleStati))); // filters={"status":["exited"],"labels":["foo=bar"]} // Shows that labels play nicely with other filters final List<Container> statusAndLabels = sut.listContainers( allContainers(), withStatusExited(), withLabel(label, labelValue)); assertThat(containerId, isIn(containersToIds(statusAndLabels))); if (dockerApiVersionAtLeast("1.21")) { for (final Container c : running) { assertThat(c.imageId(), is(notNullValue())); } } if (dockerApiVersionAtLeast("1.22")) { for (final Container c : running) { assertThat(c.networkSettings(), is(notNullValue())); } } if (dockerApiVersionAtLeast("1.23")) { for (final Container c : running) { assertThat(c.state(), equalTo("running")); assertThat(c.mounts(), is(notNullValue())); } } } @Test public void testContainerLabels() throws Exception { requireDockerApiVersionAtLeast("1.18", "labels"); sut.pull(BUSYBOX_LATEST); final Map<String, String> labels = ImmutableMap.of( "name", "starship", "foo", "bar" ); // Create container final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .labels(labels) .cmd("sleep", "1000") .build(); final String name = randomName(); final ContainerCreation creation = sut.createContainer(config, name); final String id = creation.id(); // Start the container sut.startContainer(id); final ContainerInfo containerInfo = sut.inspectContainer(id); assertThat(containerInfo.config().labels(), is(labels)); final Map<String, String> labels2 = ImmutableMap.of( "name", "starship", "foo", "baz" ); // Create second container with different labels final ContainerConfig config2 = ContainerConfig.builder() .image(BUSYBOX_LATEST) .labels(labels2) .cmd("sleep", "1000") .build(); final String name2 = randomName(); final ContainerCreation creation2 = sut.createContainer(config2, name2); final String id2 = creation2.id(); // Start the second container sut.startContainer(id2); final ContainerInfo containerInfo2 = sut.inspectContainer(id2); assertThat(containerInfo2.config().labels(), is(labels2)); // Check that both containers are listed when we filter with a "name" label final List<Container> containers = sut.listContainers(withLabel("name")); final List<String> ids = containersToIds(containers); assertThat(ids.size(), equalTo(2)); assertThat(ids, containsInAnyOrder(id, id2)); // Check that the first container is listed when we filter with a "foo=bar" label final List<Container> barContainers = sut.listContainers(withLabel("foo", "bar")); final List<String> barIds = containersToIds(barContainers); assertThat(barIds.size(), equalTo(1)); assertThat(barIds, contains(id)); // Check that the second container is listed when we filter with a "foo=baz" label final List<Container> bazContainers = sut.listContainers(withLabel("foo", "baz")); final List<String> bazIds = containersToIds(bazContainers); assertThat(bazIds.size(), equalTo(1)); assertThat(bazIds, contains(id2)); // Check that no containers are listed when we filter with a "foo=qux" label final List<Container> quxContainers = sut.listContainers(withLabel("foo", "qux")); assertThat(quxContainers.size(), equalTo(0)); } @Test public void testImageLabels() throws Exception { requireDockerApiVersionAtLeast("1.17", "image labels"); final String dockerDirectory = Resources.getResource("dockerDirectoryWithImageLabels").getPath(); // Create test images final String barDir = (new File(dockerDirectory, "barDir")).toString(); final String barName = randomName(); final String barId = sut.build(Paths.get(barDir), barName); final String bazName = randomName(); final String bazDir = (new File(dockerDirectory, "bazDir")).toString(); final String bazId = sut.build(Paths.get(bazDir), bazName); // Check that both test images are listed when we filter with a "name" label final List<Image> nameImages = sut.listImages(ListImagesParam.withLabel("name")); final List<String> nameIds = dockerApiVersionLessThan("1.22") ? imagesToShortIds(nameImages) : imagesToShortIdsAndRemoveSha256(nameImages); assertThat(barId, isIn(nameIds)); assertThat(bazId, isIn(nameIds)); // Check that the first image is listed when we filter with a "foo=bar" label final List<Image> barImages = sut.listImages( ListImagesParam.withLabel("foo", "bar")); final List<String> barIds = dockerApiVersionLessThan("1.22") ? imagesToShortIds(barImages) : imagesToShortIdsAndRemoveSha256(barImages); assertThat(barId, isIn(barIds)); // Check that we find the first image again when searching with the full // set of labels in a Map final List<Image> barImages2 = sut.listImages( ListImagesParam.withLabel("foo", "bar"), ListImagesParam.withLabel("name", "testtesttest")); final List<String> barIds2 = dockerApiVersionLessThan("1.22") ? imagesToShortIds(barImages2) : imagesToShortIdsAndRemoveSha256(barImages2); assertThat(barId, isIn(barIds2)); // Check that the second image is listed when we filter with a "foo=baz" label final List<Image> bazImages = sut.listImages( ListImagesParam.withLabel("foo", "baz")); final List<String> bazIds = dockerApiVersionLessThan("1.22") ? imagesToShortIds(bazImages) : imagesToShortIdsAndRemoveSha256(bazImages); assertThat(bazId, isIn(bazIds)); // Check that no containers are listed when we filter with a "foo=qux" label final List<Image> quxImages = sut.listImages( ListImagesParam.withLabel("foo", "qux")); assertThat(quxImages, hasSize(0)); // Clean up test images sut.removeImage(barName, true, true); sut.removeImage(bazName, true, true); } @Test public void testMacAddress() throws Exception { requireDockerApiVersionAtLeast("1.18", "Mac Address"); sut.pull(MEMCACHED_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sleep", "1000") .macAddress("12:34:56:78:9a:bc") .build(); final ContainerCreation container = sut.createContainer(config, randomName()); sut.startContainer(container.id()); final ContainerInfo containerInfo = sut.inspectContainer(container.id()); assertThat(containerInfo, notNullValue()); assertThat(containerInfo.config().macAddress(), equalTo("12:34:56:78:9a:bc")); } @Test public void testStats() throws Exception { requireDockerApiVersionAtLeast("1.19", "stats without streaming"); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final ContainerCreation container = sut.createContainer(config, randomName()); sut.startContainer(container.id()); final ContainerStats stats = sut.stats(container.id()); assertThat(stats.read(), notNullValue()); assertThat(stats.precpuStats(), notNullValue()); assertThat(stats.cpuStats(), notNullValue()); assertThat(stats.memoryStats(), notNullValue()); assertThat(stats.blockIoStats(), notNullValue()); if (dockerApiVersionLessThan("1.21")) { assertThat(stats.network(), notNullValue()); } else { assertThat(stats.networks(), notNullValue()); } } @Test public void testNetworks() throws Exception { requireDockerApiVersionAtLeast("1.21", "createNetwork and listNetworks"); assumeFalse(CIRCLECI); final String networkName = randomName(); final IpamConfig ipamConfig = IpamConfig.create("192.168.0.0/24", "192.168.0.0/24", "192.168.0.1"); final Ipam ipam = Ipam.builder() .driver("default") .config(singletonList(ipamConfig)) .build(); final Map<String, String> labels = ImmutableMap.of( "name", "starship", "foo", "bar"); final NetworkConfig networkConfig = NetworkConfig.builder().name(networkName).driver("bridge").checkDuplicate(true).ipam(ipam) .internal(false).enableIPv6(false).labels(labels) .build(); final NetworkCreation networkCreation = sut.createNetwork(networkConfig); assertThat(networkCreation.id(), is(notNullValue())); assertThat(networkCreation.warnings(), is(nullValue())); final List<Network> networks = sut.listNetworks(); assertTrue(networks.size() > 0); Network network = null; for (final Network n : networks) { if (n.name().equals(networkName)) { network = n; } } assertThat(network, is(notNullValue())); //noinspection ConstantConditions assertThat(network.id(), is(notNullValue())); assertThat(sut.inspectNetwork(network.id()).name(), is(networkName)); assertThat(network.ipam(), equalTo(ipam)); if (dockerApiVersionAtLeast("1.23")) { assertThat(network.internal(), is(false)); assertThat(network.enableIPv6(), is(false)); assertThat(network.labels(), is(labels)); } sut.removeNetwork(network.id()); exception.expect(NetworkNotFoundException.class); sut.inspectNetwork(network.id()); } @Test public void testFilterNetworks() throws Exception { requireDockerApiVersionAtLeast("1.22", "networks"); final NetworkConfig network1Config = NetworkConfig.builder().checkDuplicate(true) .name(randomName()).labels(ImmutableMap.of("is-test", "true")).build(); final NetworkConfig network2Config = NetworkConfig.builder().checkDuplicate(true) .name(randomName()).labels(ImmutableMap.of("is-test", "")).build(); final Network network1 = createNetwork(network1Config); final Network network2 = createNetwork(network2Config); final Network hostNetwork = getHostNetwork(); List<Network> networks; // filter by id networks = sut.listNetworks(ListNetworksParam.byNetworkId(network1.id())); assertThat(networks, hasItem(network1)); assertThat(networks, not(hasItem(network2))); // filter by name networks = sut.listNetworks(ListNetworksParam.byNetworkName(network1.name())); assertThat(networks, hasItem(network1)); assertThat(networks, not(hasItem(network2))); // filter by type networks = sut.listNetworks(ListNetworksParam.withType(BUILTIN)); assertThat(networks, hasItem(hostNetwork)); assertThat(networks, not(hasItems(network1, network2))); networks = sut.listNetworks(ListNetworksParam.builtInNetworks()); assertThat(networks, hasItem(hostNetwork)); assertThat(networks, not(hasItems(network1, network2))); networks = sut.listNetworks(ListNetworksParam.customNetworks()); assertThat(networks, not(hasItem(hostNetwork))); assertThat(networks, hasItems(network1, network2)); // filter by driver if (dockerApiVersionAtLeast("1.24")) { networks = sut.listNetworks(ListNetworksParam.withDriver("bridge")); assertThat(networks, not(hasItem(hostNetwork))); assertThat(networks, hasItems(network1, network2)); networks = sut.listNetworks(ListNetworksParam.withDriver("host")); assertThat(networks, hasItem(hostNetwork)); assertThat(networks, not(hasItems(network1, network2))); } // filter by label if (dockerApiVersionAtLeast("1.24")) { networks = sut.listNetworks(ListNetworksParam.withLabel("is-test")); assertThat(networks, not(hasItem(hostNetwork))); assertThat(networks, hasItems(network1, network2)); networks = sut.listNetworks(ListNetworksParam.withLabel("is-test", "true")); assertThat(networks, hasItem(network1)); assertThat(networks, not(hasItem(network2))); networks = sut.listNetworks(ListNetworksParam.withLabel("is-test", "false")); assertThat(networks, not(hasItems(network1, network2))); } sut.removeNetwork(network1.id()); sut.removeNetwork(network2.id()); } private Network createNetwork(final NetworkConfig networkConfig) throws DockerException, InterruptedException { final NetworkCreation networkCreation = sut.createNetwork(networkConfig); assertThat(networkCreation.id(), is(notNullValue())); assertThat(networkCreation.warnings(), is(nullValue())); return sut.inspectNetwork(networkCreation.id()); } private Network getHostNetwork() throws DockerException, InterruptedException { final List<Network> networks = sut.listNetworks(); for (final Network network : networks) { if (network.driver().equals("host")) { return network; } } throw new AssertionError("could not find host network"); } @Test public void testNetworkDrivers() throws Exception { requireDockerApiVersionAtLeast("1.21", "networks"); NetworkConfig.Builder networkConfigBuilder = NetworkConfig.builder(); if (dockerApiVersionEquals("1.24")) { // workaround for https://github.com/docker/docker/issues/25735 networkConfigBuilder = networkConfigBuilder.ipam( Ipam.create("default", Collections.<IpamConfig>emptyList())); } final NetworkConfig bridgeDriverConfig = networkConfigBuilder.name(randomName()) .driver("bridge").build(); final NetworkCreation bridgeDriverCreation = sut.createNetwork(bridgeDriverConfig); assertThat(bridgeDriverCreation, notNullValue()); assertThat(bridgeDriverCreation.id(), notNullValue()); assertThat(bridgeDriverCreation.warnings(), anyOf(nullValue(String.class), equalTo(""))); sut.removeNetwork(bridgeDriverCreation.id()); if (dockerApiVersionAtLeast("1.24")) { // These network drivers only exist in later versions final NetworkConfig macvlanDriverConfig = networkConfigBuilder.name(randomName()) .driver("macvlan").build(); final NetworkCreation macvlanDriverCreation = sut.createNetwork(macvlanDriverConfig); assertThat(macvlanDriverCreation, notNullValue()); assertThat(macvlanDriverCreation.id(), notNullValue()); assertThat(macvlanDriverCreation.warnings(), anyOf(nullValue(String.class), equalTo(""))); sut.removeNetwork(macvlanDriverCreation.id()); final NetworkConfig overlayDriverConfig = networkConfigBuilder.name(randomName()) .driver("overlay").build(); final NetworkCreation overlayDriverCreation = sut.createNetwork(overlayDriverConfig); assertThat(overlayDriverCreation, notNullValue()); assertThat(overlayDriverCreation.id(), notNullValue()); assertThat(overlayDriverCreation.warnings(), anyOf(nullValue(String.class), equalTo(""))); sut.removeNetwork(overlayDriverCreation.id()); } } @Test public void testNetworksConnectContainer() throws Exception { requireDockerApiVersionAtLeast("1.21", "createNetwork and listNetworks"); assumeFalse(CIRCLECI); sut.pull(BUSYBOX_LATEST); final String networkName = randomName(); final String containerName = randomName(); final NetworkCreation networkCreation = sut.createNetwork(NetworkConfig.builder().name(networkName).build()); assertThat(networkCreation.id(), is(notNullValue())); final ContainerConfig containerConfig = ContainerConfig.builder().image(BUSYBOX_LATEST).cmd("sh", "-c", "while :; do sleep 1; done") .build(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); assertThat(containerCreation.id(), is(notNullValue())); sut.startContainer(containerCreation.id()); sut.connectToNetwork(containerCreation.id(), networkCreation.id()); Network network = sut.inspectNetwork(networkCreation.id()); assertThat(network.containers().size(), equalTo(1)); final Network.Container container = network.containers().get(containerCreation.id()); assertThat(container, notNullValue()); if (dockerApiVersionAtLeast("1.22")) { assertThat(container.name(), notNullValue()); } assertThat(container.macAddress(), notNullValue()); assertThat(container.ipv4Address(), notNullValue()); assertThat(container.ipv6Address(), notNullValue()); final ContainerInfo containerInfo = sut.inspectContainer(containerCreation.id()); assertThat(containerInfo.networkSettings().networks().size(), is(2)); final AttachedNetwork attachedNetwork = containerInfo.networkSettings().networks().get(networkName); assertThat(attachedNetwork, is(notNullValue())); if (dockerApiVersionAtLeast("1.22")) { assertThat(attachedNetwork.networkId(), is(notNullValue())); } assertThat(attachedNetwork.endpointId(), is(notNullValue())); assertThat(attachedNetwork.gateway(), is(notNullValue())); assertThat(attachedNetwork.ipAddress(), is(notNullValue())); assertThat(attachedNetwork.ipPrefixLen(), is(notNullValue())); assertThat(attachedNetwork.macAddress(), is(notNullValue())); assertThat(attachedNetwork.ipv6Gateway(), is(notNullValue())); assertThat(attachedNetwork.globalIPv6Address(), is(notNullValue())); assertThat(attachedNetwork.globalIPv6PrefixLen(), greaterThanOrEqualTo(0)); sut.disconnectFromNetwork(containerCreation.id(), networkCreation.id()); network = sut.inspectNetwork(networkCreation.id()); assertThat(network.containers().size(), equalTo(0)); sut.stopContainer(containerCreation.id(), 1); sut.removeContainer(containerCreation.id()); sut.removeNetwork(networkCreation.id()); } @Test public void testNetworksConnectContainerWithEndpointConfig() throws Exception { requireDockerApiVersionAtLeast("1.22", "createNetwork and listNetworks"); assumeFalse(CIRCLECI); final String networkName = randomName(); final String containerName = randomName(); final String subnet = "172.20.0.0/16"; final String ipRange = "172.20.10.0/24"; final String gateway = "172.20.10.11"; final IpamConfig ipamConfigToCreate = IpamConfig.create(subnet, ipRange, gateway); final Ipam ipamToCreate = Ipam.builder() .driver("default") .config(Lists.newArrayList(ipamConfigToCreate)) .build(); final NetworkConfig networkingConfig = NetworkConfig.builder() .name(networkName) .ipam(ipamToCreate) .build(); final NetworkCreation networkCreation = sut.createNetwork(networkingConfig); assertThat(networkCreation.id(), is(notNullValue())); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .build(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); assertThat(containerCreation.id(), is(notNullValue())); sut.startContainer(containerCreation.id()); // Those are some of the extra parameters that can be set along with the network connection final String ip = "172.20.10.1"; final String dummyAlias = "value-does-not-matter"; final EndpointConfig endpointConfig = EndpointConfig.builder() .ipamConfig(EndpointIpamConfig.builder().ipv4Address(ip).build()) .aliases(ImmutableList.<String>of(dummyAlias)) .build(); final NetworkConnection networkConnection = NetworkConnection.builder() .containerId(containerCreation.id()) .endpointConfig(endpointConfig) .build(); sut.connectToNetwork(networkCreation.id(), networkConnection); Network network = sut.inspectNetwork(networkCreation.id()); Network.Container networkContainer = network.containers().get(containerCreation.id()); assertThat(network.containers().size(), equalTo(1)); assertThat(networkContainer, notNullValue()); final ContainerInfo containerInfo = sut.inspectContainer(containerCreation.id()); assertThat(containerInfo.networkSettings().networks(), is(notNullValue())); assertThat(containerInfo.networkSettings().networks().size(), is(2)); assertThat(containerInfo.networkSettings().networks(), hasKey(networkName)); final AttachedNetwork attachedNetwork = containerInfo.networkSettings().networks().get(networkName); assertThat(attachedNetwork, is(notNullValue())); assertThat(attachedNetwork.networkId(), is(equalTo(networkCreation.id()))); assertThat(attachedNetwork.endpointId(), is(notNullValue())); assertThat(attachedNetwork.gateway(), is(equalTo(gateway))); assertThat(attachedNetwork.ipAddress(), is(equalTo(ip))); assertThat(attachedNetwork.ipPrefixLen(), is(notNullValue())); assertThat(attachedNetwork.macAddress(), is(notNullValue())); assertThat(attachedNetwork.ipv6Gateway(), is(notNullValue())); assertThat(attachedNetwork.globalIPv6Address(), is(notNullValue())); assertThat(attachedNetwork.globalIPv6PrefixLen(), greaterThanOrEqualTo(0)); assertThat(attachedNetwork.aliases(), is(notNullValue())); assertThat(dummyAlias, isIn(attachedNetwork.aliases())); sut.disconnectFromNetwork(containerCreation.id(), networkCreation.id()); network = sut.inspectNetwork(networkCreation.id()); assertThat(network.containers().size(), equalTo(0)); sut.stopContainer(containerCreation.id(), 1); sut.removeContainer(containerCreation.id()); sut.removeNetwork(networkCreation.id()); } @Test public void testRestartPolicyAlways() throws Exception { testRestartPolicy(HostConfig.RestartPolicy.always()); } @Test public void testRestartUnlessStopped() throws Exception { testRestartPolicy(HostConfig.RestartPolicy.unlessStopped()); } @Test public void testRestartOnFailure() throws Exception { testRestartPolicy(HostConfig.RestartPolicy.onFailure(5)); } @Test public void testRenameContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final String originalName = randomName(); final String newName = randomName(); // Create a container with originalName final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final ContainerCreation creation = sut.createContainer(config, originalName); final String id = creation.id(); assertThat(sut.inspectContainer(id).name(), equalToIgnoreLeadingSlash(originalName)); // Rename to newName sut.renameContainer(id, newName); assertThat(sut.inspectContainer(id).name(), equalToIgnoreLeadingSlash(newName)); // We should no longer find a container with originalName try { sut.inspectContainer(originalName); fail("There should be no container with name " + originalName); } catch (ContainerNotFoundException e) { // Note, even though property in ContainerNotFoundException is named containerId, // in this case it holds the name, since that is what we passed to inspectContainer. assertThat(e.getContainerId(), equalToIgnoreLeadingSlash(originalName)); } // Try to rename to a disallowed name (not matching /?[a-zA-Z0-9_-]+). // Should get IllegalArgumentException. final String badName = "abc123.!*"; try { sut.renameContainer(id, badName); fail("We should not be able to rename a container " + badName); } catch (IllegalArgumentException ignored) { // Pass } // Try to rename to null try { sut.renameContainer(id, null); fail("We should not be able to rename a container null"); } catch (IllegalArgumentException ignored) { // Pass } // Create another container with originalName final ContainerConfig config2 = ContainerConfig.builder() .image(BUSYBOX_LATEST) .build(); final ContainerCreation creation2 = sut.createContainer(config2, originalName); final String id2 = creation2.id(); assertThat(sut.inspectContainer(id2).name(), equalToIgnoreLeadingSlash(originalName)); // Try to rename another container to newName. Should get a ContainerRenameConflictException. try { sut.renameContainer(id2, newName); fail("We should not be able to rename container " + id2 + " to " + newName); } catch (ContainerRenameConflictException e) { assertThat(e.getContainerId(), equalTo(id2)); assertThat(e.getNewName(), equalToIgnoreLeadingSlash(newName)); } catch (DockerRequestException ignored) { // This is a docker bug. It responds with HTTP 500 when it should be HTTP 409. // See https://github.com/docker/docker/issues/21016. // Fixed in version 1.11.0, so we should see a lower version. assertThat(compareVersion(sut.version().version(), "1.11.0"), lessThan(0)); } // Rename a non-existent id. Should get ContainerNotFoundException. final String badId = "no_container_with_this_id_should_exist_otherwise_things_are_weird"; try { sut.renameContainer(badId, randomName()); fail("There should be no container with id " + badId); } catch (ContainerNotFoundException e) { assertThat(e.getContainerId(), equalTo(badId)); } } @Test public void testInspectContainerChanges() throws Exception { sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("/bin/sh", "-c", "echo foo > /tmp/foo.txt") .build(); final ContainerCreation creation = sut.createContainer(config, randomName()); final String id = creation.id(); sut.startContainer(id); final ContainerChange expected = ContainerChange.create("/tmp/foo.txt", 1); assertThat(expected, isIn(sut.inspectContainerChanges(id))); } @Test public void testResizeTty() throws Exception { sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("/bin/sh", "-c", "while :; do sleep 1; done") .build(); final ContainerCreation creation = sut.createContainer(config, randomName()); final String id = creation.id(); try { sut.resizeTty(id, 100, 0); fail("Should get an exception resizing TTY with width=0"); } catch (BadParamException e) { final Map<String, String> params = e.getParams(); assertThat(params, hasKey("w")); assertEquals("0", params.get("w")); } try { sut.resizeTty(id, 100, 80); fail("Should get an exception resizing TTY for non-running container"); } catch (DockerRequestException e) { if (dockerApiVersionLessThan("1.20")) { assertEquals( String.format("Cannot resize container %s, container is not running\n", id), e.getResponseBody()); } else if (dockerApiVersionLessThan("1.24")) { assertEquals(String.format("Container %s is not running\n", id), e.getResponseBody()); } else { final ObjectMapper mapper = new ObjectMapperProvider().getContext(DefaultDockerClientTest.class); final Map<String, String> jsonMessage = mapper.readValue(e.getResponseBody(), new TypeReference<Map<String, String>>() { }); assertThat(jsonMessage, hasKey("message")); assertEquals(String.format("Container %s is not running", id), jsonMessage.get("message")); } } sut.startContainer(id); sut.resizeTty(id, 100, 80); // We didn't get an exception, so everything went fine } @Test public void testHistory() throws Exception { sut.pull(BUSYBOX_LATEST); final List<ImageHistory> imageHistoryList = sut.history(BUSYBOX_LATEST); assertThat(imageHistoryList, hasSize(2)); final ImageHistory busyboxHistory = imageHistoryList.get(0); assertThat(busyboxHistory.id(), not(isEmptyOrNullString())); assertNotNull(busyboxHistory.created()); assertThat(busyboxHistory.createdBy(), startsWith("/bin/sh -c #(nop)")); assertThat(BUSYBOX_LATEST, isIn(busyboxHistory.tags())); assertEquals(0L, busyboxHistory.size().longValue()); assertThat(busyboxHistory.comment(), isEmptyOrNullString()); } @Test public void testCreateVolume() throws Exception { requireDockerApiVersionAtLeast("1.21", "volumes"); // Create bare volume final Volume blankVolume = sut.createVolume(); assertThat(blankVolume, not(nullValue())); sut.removeVolume(blankVolume); // Create volume with attributes final ImmutableMap<String, String> labels = ImmutableMap.of("foo", "bar"); final String volName = randomName(); final Volume toCreate; if (dockerApiVersionLessThan("1.23")) { toCreate = Volume.builder() .name(volName) .driver("local") .build(); } else { toCreate = Volume.builder() .name(volName) .driver("local") .labels(labels) .build(); } final Volume created = sut.createVolume(toCreate); assertEquals(toCreate.name(), created.name()); assertEquals(toCreate.driver(), created.driver()); assertEquals(toCreate.driverOpts(), created.driverOpts()); // mountpoint gets set by server regardless of whatever we ask for assertNotEquals(toCreate.mountpoint(), created.mountpoint()); if (dockerApiVersionAtLeast("1.23")) { assertEquals(labels, created.labels()); } if (dockerApiVersionAtLeast("1.24")) { assertEquals("local", created.scope()); } sut.removeVolume(created); } @Test public void testInspectVolume() throws Exception { requireDockerApiVersionAtLeast("1.21", "volumes"); final Volume volume = sut.createVolume(); assertEquals(volume, sut.inspectVolume(volume.name())); sut.removeVolume(volume); final String badVolumeName = "this-is-a-very-unlikely-volume-name"; exception.expect(VolumeNotFoundException.class); exception.expect(volumeNotFoundExceptionWithName(badVolumeName)); sut.inspectVolume(badVolumeName); } private static Matcher<VolumeNotFoundException> volumeNotFoundExceptionWithName( final String volumeName) { final String description = "for volume name " + volumeName; return new CustomTypeSafeMatcher<VolumeNotFoundException>(description) { @Override protected boolean matchesSafely(final VolumeNotFoundException ex) { return ex.getVolumeName().equals(volumeName); } }; } @Test public void testListVolumes() throws Exception { requireDockerApiVersionAtLeast("1.21", "volumes"); final Volume volume = sut.createVolume(); final String volumeName = volume.name(); final String volumeDriver = volume.driver(); final VolumeList volumeList = sut.listVolumes(); if (volumeList.warnings() != null && volumeList.warnings().isEmpty()) { for (final String warning : volumeList.warnings()) { log.warn(warning); } } assertThat(volume, isIn(volumeList.volumes())); final VolumeList volumeListWithDangling = sut.listVolumes(dangling()); if (volumeListWithDangling.warnings() != null && !volumeListWithDangling.warnings().isEmpty()) { for (final String warning : volumeListWithDangling.warnings()) { log.warn(warning); } } assertThat(volume, isIn(volumeListWithDangling.volumes())); if (dockerApiVersionAtLeast("1.24")) { final VolumeList volumeListByName = sut.listVolumes(name(volumeName)); if (volumeListByName.warnings() != null && !volumeListByName.warnings().isEmpty()) { for (final String warning : volumeListByName.warnings()) { log.warn(warning); } } assertThat(volume, isIn(volumeListByName.volumes())); final VolumeList volumeListByDriver = sut.listVolumes(driver(volumeDriver)); if (volumeListByDriver.warnings() != null && !volumeListByDriver.warnings().isEmpty()) { for (final String warning : volumeListByDriver.warnings()) { log.warn(warning); } } assertThat(volume, isIn(volumeListByDriver.volumes())); } if (dockerApiVersionAtLeast("1.24")) { assertEquals("local", volume.scope()); assertThat(volume.status(), is(anything())); // I don't know what is in the status object - JF } sut.removeVolume(volume); } @Test public void testRemoveVolume() throws Exception { requireDockerApiVersionAtLeast("1.21", "volumes"); // Create a volume and remove it final Volume volume1 = sut.createVolume(); sut.removeVolume(volume1); // Remove non-existent volume exception.expect(VolumeNotFoundException.class); exception.expect(volumeNotFoundExceptionWithName(volume1.name())); sut.removeVolume(volume1); // Create a volume, assign it to a container, and try to remove it. // Should get a ConflictException. final Volume volume2 = sut.createVolume(); final HostConfig hostConfig = HostConfig.builder() .binds(Bind.from(volume2).to("/tmp").build()) .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX) .hostConfig(hostConfig) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); exception.expect(ConflictException.class); sut.removeVolume(volume2); // Clean up sut.removeContainer(container.id()); sut.removeVolume(volume2); } private static Matcher<String> equalToIgnoreLeadingSlash(final String expected) { final String description = "a String equal to " + expected + ", ignoring any leading '/'"; return new CustomTypeSafeMatcher<String>(description) { @Override protected boolean matchesSafely(final String actual) { return actual.startsWith("/") ? actual.substring(1).equals(expected) : actual.equals(expected); } }; } private void testRestartPolicy(HostConfig.RestartPolicy restartPolicy) throws Exception { sut.pull(BUSYBOX_LATEST); final HostConfig hostConfig = HostConfig.builder() .restartPolicy(restartPolicy) .build(); final ContainerConfig containerConfig = ContainerConfig.builder() .image(BUSYBOX_LATEST) // make sure the container's busy doing something upon startup .cmd("sh", "-c", "while :; do sleep 1; done") .hostConfig(hostConfig) .build(); final String containerName = randomName(); final ContainerCreation containerCreation = sut.createContainer(containerConfig, containerName); final String containerId = containerCreation.id(); final ContainerInfo info = sut.inspectContainer(containerId); assertThat(info.hostConfig().restartPolicy().name(), is(restartPolicy.name())); final Integer retryCount = restartPolicy.maxRetryCount() == null ? 0 : restartPolicy.maxRetryCount(); assertThat(info.hostConfig().restartPolicy().maxRetryCount(), is(retryCount)); } @Test public void testIpcMode() throws Exception { requireDockerApiVersionAtLeast("1.18", "IpcMode"); final HostConfig hostConfig = HostConfig.builder() .ipcMode("host") .build(); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .cmd("sh", "-c", "while :; do sleep 1; done") .hostConfig(hostConfig) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final String containerId = container.id(); sut.startContainer(containerId); final ContainerInfo info = sut.inspectContainer(containerId); assertThat(info.hostConfig().ipcMode(), is("host")); } @Test public void testShmSize() throws Exception { requireDockerApiVersionAtLeast("1.22", "ShmSize"); // Pull image sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .shmSize(10000000L) .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().shmSize(), is(10000000L)); } @Test public void testOomKillDisable() throws Exception { requireDockerApiVersionAtLeast("1.20", "OomKillDisable"); // Pull image sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .oomKillDisable(true) // Defaults to false .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().oomKillDisable(), is(true)); } @Test public void testOomScoreAdj() throws Exception { requireDockerApiVersionAtLeast("1.22", "OomScoreAdj"); // Pull image sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .oomScoreAdj(500) // Defaults to 0 .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().oomScoreAdj(), is(500)); } @Test public void testPidsLimit() throws Exception { if (OsUtils.isLinux()) { assumeTrue("Linux kernel must be at least 4.3.", compareVersion(System.getProperty("os.version"), "4.3") >= 0); } requireDockerApiVersionAtLeast("1.23", "PidsLimit"); // Pull image sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .pidsLimit(100) // Defaults to -1 .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().pidsLimit(), is(100)); } @Test public void testTmpfs() throws Exception { requireDockerApiVersionAtLeast("1.22", "TmpFs"); // Pull image sut.pull(BUSYBOX_LATEST); final ImmutableMap<String, String> tmpfs = ImmutableMap.of("/tmp", "rw,noexec,nosuid,size=50m"); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .tmpfs(tmpfs) .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().tmpfs(), is(tmpfs)); } @Test public void testReadonlyRootfs() throws Exception { // Pull image sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .readonlyRootfs(true) // Defaults to -1 .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().readonlyRootfs(), is(true)); } @Test public void testStorageOpt() throws Exception { requireDockerApiVersionAtLeast("1.24", "StorageOpt"); requireStorageDriverNotAufs(); // Pull image sut.pull(BUSYBOX_LATEST); final ImmutableMap<String, String> storageOpt = ImmutableMap.of("size", "20G"); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .storageOpt(storageOpt) .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().storageOpt().size(), is(1)); assertThat(info.hostConfig().storageOpt().get("size"), is("20G")); } @Test public void testAutoRemoveWhenSetToTrue() throws Exception { requireDockerApiVersionAtLeast("1.25", "AutoRemove"); // Container should be removed after it is stopped (new since API v.1.25) // Pull image sut.pull(BUSYBOX_LATEST); final ContainerConfig config = ContainerConfig.builder() .image(BUSYBOX_LATEST) .hostConfig(HostConfig.builder() .autoRemove(true) // Default is false .build()) .build(); final ContainerCreation container = sut.createContainer(config, randomName()); final ContainerInfo info = sut.inspectContainer(container.id()); assertThat(info.hostConfig().autoRemove(), is(true)); sut.startContainer(container.id()); sut.stopContainer(container.id(), 5); await().until(containerIsRunning(sut, container.id()), is(false)); // A ContainerNotFoundException should be thrown since the container is removed when it stops exception.expect(ContainerNotFoundException.class); sut.inspectContainer(container.id()); } @Test public void testInspectSwarm() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final Swarm swarm = sut.inspectSwarm(); assertThat(swarm.createdAt(), is(notNullValue())); assertThat(swarm.updatedAt(), is(notNullValue())); assertThat(swarm.id(), is(not(isEmptyOrNullString()))); assertThat(swarm.joinTokens().worker(), is(not(isEmptyOrNullString()))); assertThat(swarm.joinTokens().manager(), is(not(isEmptyOrNullString()))); } @Test public void testInitAndLeaveSwarm() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); try { sut.leaveSwarm(true); } catch (final Exception ex) { // ignored } // Test initializing swarm without SwarmSpec final String nodeId = sut.initSwarm(SwarmInit.builder() .advertiseAddr("127.0.0.1") .listenAddr("0.0.0.0:2377") .build() ); assertThat(nodeId, is(notNullValue())); sut.leaveSwarm(true); // Test initializing swarm with SwarmSpec final String nodeId2 = sut.initSwarm(SwarmInit.builder() .advertiseAddr("127.0.0.1") .listenAddr("0.0.0.0:2377") .swarmSpec(SwarmSpec.builder() .caConfig(CaConfig.builder().build()) .dispatcher(DispatcherConfig.builder().build()) .labels(Collections.<String, String>emptyMap()) .raft(RaftConfig.builder().build()) .encryptionConfig(EncryptionConfig.builder().autoLockManagers(true).build()) .taskDefaults(TaskDefaults.builder().build()) .build() ) .build() ); assertThat(nodeId2, is(notNullValue())); } @Test public void testUpdateSwarm() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final Swarm swarm = sut.inspectSwarm(); final ThreadLocalRandom random = ThreadLocalRandom.current(); final Map<String, String> newLabels = ImmutableMap.of("foo", "bar"); final OrchestrationConfig newOrchestration = OrchestrationConfig.builder() .taskHistoryRetentionLimit(random.nextInt(1, 10)) .build(); final RaftConfig newRaft = RaftConfig.builder() .snapshotInterval(random.nextInt(1, 10000)) .keepOldSnapshots(random.nextInt(1, 10)) .logEntriesForSlowFollowers(random.nextInt(1, 1000)) .electionTick(random.nextInt(1, 10)) .heartbeatTick(random.nextInt(1, 10)) .build(); final DispatcherConfig newDispatcher = DispatcherConfig.builder() .heartbeatPeriod(random.nextLong(1, 5000000000L)) .build(); final CaConfig newCa = CaConfig.builder() .nodeCertExpiry(random.nextLong(1, 7776000000000000L)) .build(); final EncryptionConfig newEncryption = EncryptionConfig.builder() .autoLockManagers(true) .build(); final TaskDefaults newTaskDefaults = TaskDefaults.builder() .build(); final SwarmSpec updatedSpec = SwarmSpec.builder() .name(swarm.swarmSpec().name() + "2") .labels(newLabels) .orchestration(newOrchestration) .raft(newRaft) .dispatcher(newDispatcher) .caConfig(newCa) .encryptionConfig(newEncryption) .taskDefaults(newTaskDefaults) .build(); sut.updateSwarm(swarm.version().index(), updatedSpec); final Swarm updatedSwarm = sut.inspectSwarm(); assertThat(updatedSwarm.id(), equalTo(swarm.id())); final SwarmSpec newSpec = updatedSwarm.swarmSpec(); if (dockerApiVersionLessThan("1.25")) { assertThat(newSpec.name(), equalTo(updatedSpec.name())); assertThat(newSpec.labels(), equalTo(updatedSpec.labels())); assertThat(newSpec.orchestration(), equalTo(updatedSpec.orchestration())); assertThat(newSpec.raft(), equalTo(updatedSpec.raft())); assertThat(newSpec.dispatcher(), equalTo(updatedSpec.dispatcher())); assertThat(newSpec.caConfig(), equalTo(updatedSpec.caConfig())); assertThat(newSpec.encryptionConfig(), is(nullValue())); assertThat(newSpec.taskDefaults(), equalTo(updatedSpec.taskDefaults())); } else { assertThat(newSpec, equalTo(updatedSpec)); } // Return swarm back to old settings sut.updateSwarm(updatedSwarm.version().index(), swarm.swarmSpec()); } @Test public void testUnlockKey() throws Exception { requireDockerApiVersionAtLeast("1.25", "swarm locking support"); final UnlockKey unlockKey = sut.unlockKey(); assertThat(unlockKey, is(notNullValue())); } @Test public void testCreateServiceWithNetwork() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final String networkName = randomName(); final String serviceName = randomName(); NetworkConfig.Builder networkConfigBuilder = NetworkConfig.builder() .driver("overlay") .name(networkName); if (dockerApiVersionEquals("1.24")) { // workaround for https://github.com/docker/docker/issues/25735 networkConfigBuilder = networkConfigBuilder .ipam(Ipam.create("default", Collections.<IpamConfig>emptyList())); } final NetworkCreation networkCreation = sut.createNetwork(networkConfigBuilder.build()); final String networkId = networkCreation.id(); assertThat(networkId, is(notNullValue())); final TaskSpec taskSpec = TaskSpec.builder() .containerSpec(ContainerSpec.builder().image("alpine") .command(new String[] { "ping", "-c1000", "localhost" }).build()) .build(); final ServiceSpec spec = ServiceSpec.builder().name(serviceName) .taskTemplate(taskSpec).mode(ServiceMode.withReplicas(1L)) .networks(NetworkAttachmentConfig.builder().target(networkName).build()) .build(); final ServiceCreateResponse response = sut.createService(spec); assertThat(response.id(), is(notNullValue())); final Service inspectService = sut.inspectService(serviceName); assertThat(inspectService.spec().networks().size(), is(1)); assertThat(Iterables.find(inspectService.spec().networks(), new Predicate<NetworkAttachmentConfig>() { @Override public boolean apply(NetworkAttachmentConfig config) { return networkId.equals(config.target()); } }, null), is(notNullValue())); sut.removeService(serviceName); sut.removeNetwork(networkName); } @Test public void testCreateService() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final ServiceSpec spec = createServiceSpec(randomName()); final ServiceCreateResponse response = sut.createService(spec); assertThat(response.id(), is(notNullValue())); } @Test public void testSecretOperations() throws Exception { requireDockerApiVersionAtLeast("1.25", "secret support"); for (final Secret secret : sut.listSecrets()) { sut.deleteSecret(secret.id()); } assertThat(sut.listSecrets().size(), equalTo(0)); final String secretData = Base64.encodeAsString("testdata".getBytes()); final Map<String, String> labels = ImmutableMap.of("foo", "bar", "1", "a"); final SecretSpec secretSpec = SecretSpec.builder() .name("asecret") .data(secretData) .labels(labels) .build(); final SecretCreateResponse response = sut.createSecret(secretSpec); final String secretId = response.id(); assertThat(secretId, is(notNullValue())); final SecretSpec secretSpecConflict = SecretSpec.builder() .name("asecret") .data(secretData) .labels(labels) .build(); try { sut.createSecret(secretSpecConflict); fail("Should fail due to secret name conflict"); } catch (DockerRequestException ex) { // Ignored; Docker should return status code 409, but it doesn't for some reason. } final SecretSpec secretSpecInvalidData = SecretSpec.builder() .name("asecret2") .data("plainData") .labels(labels) .build(); try { sut.createSecret(secretSpecInvalidData); fail("Should fail due to non base64 data"); } catch (DockerException ex) { // Ignored } final Secret secret = sut.inspectSecret(secretId); final List<Secret> secrets = sut.listSecrets(); assertThat(secrets.size(), equalTo(1)); assertThat(secrets, hasItem(secret)); sut.deleteSecret(secretId); assertThat(sut.listSecrets().size(), equalTo(0)); try { sut.inspectSecret(secretId); fail("Should fail because of non-existant secret ID"); } catch (NotFoundException ex) { // Ignored } try { sut.deleteSecret(secretId); fail("Should fail because of non-existant secret ID"); } catch (DockerRequestException | NotFoundException ex) { // Ignored; Docker should return status code 404, // but it doesn't for some reason in versions < 17.04. // So we catch 2 different exceptions here. } } @Test public void testCreateServiceWithDefaults() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final String serviceName = randomName(); final TaskSpec taskSpec = TaskSpec .builder() .containerSpec(ContainerSpec.builder() .image("alpine") .command(new String[] {"ping", "-c1000", "localhost"}) .mounts(Mount.builder() .volumeOptions(VolumeOptions.builder() .driverConfig(com.spotify.docker.client.messages.mount.Driver.builder().build()) .build()) .bindOptions(BindOptions.builder().build()) .tmpfsOptions(TmpfsOptions.builder().build()) .build()) .build()) .resources(ResourceRequirements.builder().build()) .restartPolicy(RestartPolicy.builder().build()) .placement(Placement.create(null)) .networks(NetworkAttachmentConfig.builder().build()) .logDriver(Driver.builder().build()) .build(); final ServiceMode serviceMode = ServiceMode.builder() .replicated(ReplicatedService.builder().build()) .build(); final ServiceSpec serviceSpec = ServiceSpec.builder() .name(serviceName) .taskTemplate(taskSpec) .mode(serviceMode) .updateConfig(UpdateConfig.create(null, null, null)) .networks(Collections.<NetworkAttachmentConfig>emptyList()) .endpointSpec(EndpointSpec.builder() .addPort(PortConfig.builder().build()) .build()) .build(); final ServiceCreateResponse response = sut.createService(serviceSpec); assertThat(response.id(), is(notNullValue())); sut.listTasks(); final Service service = sut.inspectService(serviceName); final ServiceSpec actualServiceSpec = service.spec(); assertThat(actualServiceSpec.mode().replicated().replicas(), equalTo(1L)); assertThat(actualServiceSpec.taskTemplate().logDriver().options(), equalTo(Collections.<String, String>emptyMap())); assertThat(actualServiceSpec.endpointSpec().mode(), equalTo(EndpointSpec.Mode.RESOLUTION_MODE_VIP)); assertThat(actualServiceSpec.updateConfig().failureAction(), equalTo("pause")); final PortConfig.Builder portConfigBuilder = PortConfig.builder() .protocol(PROTOCOL_TCP); if (dockerApiVersionAtLeast("1.25")) { // Ingress publish mode is the default for ports in API versions >= 1.25 portConfigBuilder.publishMode(PortConfigPublishMode.INGRESS); } final PortConfig expectedPortConfig = portConfigBuilder.build(); assertThat(actualServiceSpec.endpointSpec().ports(), contains(expectedPortConfig)); assertThat(service.endpoint().spec().ports(), contains(expectedPortConfig)); final ContainerSpec containerSpec = actualServiceSpec.taskTemplate().containerSpec(); assertThat(containerSpec.labels(), equalTo(Collections.<String, String>emptyMap())); assertThat(containerSpec.mounts().size(), equalTo(1)); final Mount mount = containerSpec.mounts().get(0); assertThat(mount.type(), equalTo("bind")); final VolumeOptions volumeOptions = mount.volumeOptions(); assertThat(volumeOptions.noCopy(), nullValue()); assertThat(volumeOptions.labels(), nullValue()); final com.spotify.docker.client.messages.mount.Driver driver = volumeOptions.driverConfig(); assertThat(driver.name(), nullValue()); assertThat(driver.options(), nullValue()); final RestartPolicy restartPolicy = actualServiceSpec.taskTemplate().restartPolicy(); assertThat(restartPolicy.condition(), equalTo(RESTART_POLICY_ANY)); assertThat(restartPolicy.maxAttempts(), equalTo(0)); } @Test public void testCreateServiceWithSecretHostnameHostsAndHealthcheck() throws Exception { requireDockerApiVersionAtLeast("1.26", "swarm support"); final String hostname = "tshost-{{.Task.Slot}}"; final String[] hosts = {"127.0.0.1 test.local", "127.0.0.1 test"}; final String[] healthcheckCmd = {"ping", "-c", "1", "127.0.0.1"}; for (final Secret secret : sut.listSecrets()) { sut.deleteSecret(secret.id()); } assertThat(sut.listSecrets().size(), equalTo(0)); final String secretData = Base64.encodeAsString("testdata".getBytes()); final Map<String, String> labels = ImmutableMap.of("foo", "bar", "1", "a"); final SecretSpec secretSpec = SecretSpec.builder() .name("asecret") .data(secretData) .labels(labels) .build(); final SecretCreateResponse secretResponse = sut.createSecret(secretSpec); final String secretId = secretResponse.id(); assertThat(secretId, is(notNullValue())); final SecretFile secretFile = SecretFile.builder() .name("bsecret") .uid("1001") .gid("1002") .mode(0640L) .build(); final SecretBind secretBind = SecretBind.builder() .file(secretFile) .secretId(secretId) .secretName("asecret") .build(); final String[] commandLine = {"ping", "-c4", "localhost"}; final TaskSpec taskSpec = TaskSpec .builder() .containerSpec(ContainerSpec.builder().image("alpine") .secrets(Arrays.asList(secretBind)) .hostname(hostname) .hosts(Arrays.asList(hosts)) .healthcheck(Healthcheck.create(Arrays.asList(healthcheckCmd), 30L, 3L, 3)) .command(commandLine).build()) .build(); final String serviceName = randomName(); final ServiceSpec spec = ServiceSpec.builder() .name(serviceName) .taskTemplate(taskSpec) .build(); final ServiceCreateResponse response = sut.createService(spec); final Service service = sut.inspectService(response.id()); assertThat(service.spec().name(), is(serviceName)); final Matcher<String> imageMatcher = dockerApiVersionLessThan("1.25") ? is("alpine") : startsWith("alpine:latest@sha256:"); assertThat(service.spec().taskTemplate().containerSpec().image(), imageMatcher); assertThat(service.spec().taskTemplate().containerSpec().hostname(), is(hostname)); assertThat(service.spec().taskTemplate().containerSpec().hosts(), containsInAnyOrder(hosts)); assertThat(service.spec().taskTemplate().containerSpec().secrets().size(), equalTo(1)); SecretBind secret = service.spec().taskTemplate().containerSpec().secrets().get(0); assertThat(secret.secretId(), equalTo(secretId)); assertThat(secret.secretName(), equalTo("asecret")); assertThat(secret.file().name(), equalTo("bsecret")); assertThat(secret.file().uid(), equalTo("1001")); assertThat(secret.file().gid(), equalTo("1002")); assertThat(secret.file().mode(), equalTo(0640L)); assertThat(service.spec().taskTemplate().containerSpec().healthcheck().test(), equalTo(Arrays.asList(healthcheckCmd))); assertThat(service.spec().taskTemplate().containerSpec().healthcheck().interval(), equalTo(30L)); assertThat(service.spec().taskTemplate().containerSpec().healthcheck().timeout(), equalTo(3L)); assertThat(service.spec().taskTemplate().containerSpec().healthcheck().retries(), equalTo(3)); } @Test public void testInspectService() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final String[] commandLine = {"ping", "-c4", "localhost"}; final TaskSpec taskSpec = TaskSpec .builder() .containerSpec(ContainerSpec.builder().image("alpine") .command(commandLine).build()) .logDriver(Driver.builder().name("json-file").addOption("max-file", "3") .addOption("max-size", "10M").build()) .resources(ResourceRequirements.builder() .limits(com.spotify.docker.client.messages.swarm.Resources.builder() .memoryBytes(10 * 1024 * 1024L).build()) .build()) .restartPolicy(RestartPolicy.builder().condition("on-failure") .delay(10000000L).maxAttempts(10).build()) .build(); final EndpointSpec endpointSpec = EndpointSpec.builder() .addPort(PortConfig.builder() .name("web") .protocol("tcp") .publishedPort(8080) .targetPort(80) .build()) .build(); final ServiceMode serviceMode = ServiceMode.withReplicas(4); final String serviceName = randomName(); final ServiceSpec spec = ServiceSpec.builder() .name(serviceName) .taskTemplate(taskSpec) .mode(serviceMode) .endpointSpec(endpointSpec) .build(); final ServiceCreateResponse response = sut.createService(spec); final Service service = sut.inspectService(response.id()); assertThat(service.spec().name(), is(serviceName)); final Matcher<String> imageMatcher = dockerApiVersionLessThan("1.25") ? is("alpine") : startsWith("alpine:latest@sha256:"); assertThat(service.spec().taskTemplate().containerSpec().image(), imageMatcher); assertThat(service.spec().taskTemplate().containerSpec().command(), equalTo(Arrays.asList(commandLine))); } @Test public void testInspectServiceEndpoint() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final String name = randomName(); final String imageName = "demo/test"; final PortConfig.Builder portConfigBuilder = PortConfig.builder() .name("web") .protocol("tcp") .publishedPort(8080) .targetPort(80); if (dockerApiVersionAtLeast("1.25")) { portConfigBuilder.publishMode(PortConfigPublishMode.INGRESS); } final PortConfig expectedPort1 = portConfigBuilder.build(); final ServiceSpec spec = ServiceSpec.builder() .name(name) .endpointSpec(EndpointSpec.builder() .addPort(expectedPort1) .addPort(PortConfig.builder() .targetPort(22) .publishMode(PortConfigPublishMode.HOST) .build()) .build()) .taskTemplate(TaskSpec.builder() .containerSpec(ContainerSpec.builder() .image(imageName) .build()) .build()) .build(); sut.createService(spec); final Service service = sut.inspectService(name); final Endpoint endpoint = service.endpoint(); final PortConfig.Builder portConfigBuilder2 = PortConfig.builder() .targetPort(22) .protocol("tcp"); if (dockerApiVersionAtLeast("1.25")) { portConfigBuilder2.publishMode(PortConfigPublishMode.HOST); } final PortConfig expectedPort2Spec = portConfigBuilder2.build(); assertThat(endpoint.spec().ports(), containsInAnyOrder(expectedPort1, expectedPort2Spec)); // API versions less than 1.25 get assigned a random published port and have null publish mode final Matcher<Integer> publishedPortMatcher; final Matcher<PortConfigPublishMode> publishModeMatcher; if (dockerApiVersionLessThan("1.25")) { publishedPortMatcher = any(Integer.class); publishModeMatcher = nullValue(PortConfigPublishMode.class); } else { publishedPortMatcher = nullValue(Integer.class); publishModeMatcher = equalTo(PortConfigPublishMode.HOST); } //noinspection unchecked assertThat(endpoint.ports(), containsInAnyOrder( equalTo(expectedPort1), portConfigWith(nullValue(String.class), equalTo("tcp"), equalTo(22), publishedPortMatcher, publishModeMatcher))); //noinspection ConstantConditions assertThat(endpoint.virtualIps().size(), equalTo(1)); } @Test public void testUpdateService() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final ServiceSpec spec = createServiceSpec(randomName()); final ServiceCreateResponse response = sut.createService(spec); assertThat(response.id(), is(notNullValue())); Service service = sut.inspectService(response.id()); assertThat(service.spec().mode().replicated().replicas(), is(4L)); // update service with same spec, but bump the number of replicas by 1 sut.updateService(response.id(), service.version().index(), ServiceSpec.builder() .name(service.spec().name()) .taskTemplate(service.spec().taskTemplate()) .mode(ServiceMode.withReplicas(5)) .endpointSpec(service.spec().endpointSpec()) .updateConfig(service.spec().updateConfig()) .build()); service = sut.inspectService(response.id()); assertThat(service.spec().mode().replicated().replicas(), is(5L)); } @Test public void testListServices() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); List<Service> services = sut.listServices(); final int startingNumServices = services.size(); final ServiceSpec spec = createServiceSpec(randomName()); sut.createService(spec); services = sut.listServices(); assertThat(services.size(), is(startingNumServices + 1)); } @Test public void testListServicesFilterById() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final ServiceSpec spec = createServiceSpec(randomName()); final ServiceCreateResponse response = sut.createService(spec); final List<Service> services = sut .listServices(Service.find().serviceId(response.id()).build()); assertThat(services.size(), is(1)); assertThat(services.get(0).id(), is(response.id())); } @Test public void testListServicesFilterByName() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final String serviceName = randomName(); final ServiceSpec spec = createServiceSpec(serviceName); sut.createService(spec); final List<Service> services = sut.listServices(Service.find().serviceName(serviceName).build()); assertThat(services.size(), is(1)); assertThat(services.get(0).spec().name(), is(serviceName)); } @Test public void testListServicesFilterByLabel() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final String serviceName = randomName(); Map<String, String> labels = new HashMap<>(); labels.put("foo", "bar"); final ServiceSpec spec = createServiceSpec(serviceName, labels); sut.createService(spec); final List<Service> services = sut.listServices(Service.find().addLabel("foo", "bar").build()); assertThat(services.size(), is(1)); assertThat(services.get(0).spec().labels().get("foo"), is("bar")); final List<Service> notFoundServices = sut.listServices(Service.find() .addLabel("bar", "foo").build()); assertThat(notFoundServices.size(), is(0)); } @Test public void testRemoveService() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final ServiceSpec spec = createServiceSpec(randomName()); final ServiceCreateResponse response = sut.createService(spec); assertThat(sut.listServices(), is(not(empty()))); sut.removeService(response.id()); assertThat(sut.listServices(), is(empty())); } @Test public void testInspectTask() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final Date start = new Date(); final ServiceSpec serviceSpec = createServiceSpec(randomName()); assertThat(sut.listTasks().size(), is(0)); final ServiceCreateResponse serviceCreateResponse = sut.createService(serviceSpec); await().until(numberOfTasks(sut), is(greaterThan(0))); final Task someTask = sut.listTasks().get(0); final Task inspectedTask = sut.inspectTask(someTask.id()); final Date now = new Date(); assertThat(inspectedTask.id(), notNullValue()); assertThat(inspectedTask.version().index(), allOf(notNullValue(), greaterThan(0L))); assertThat(inspectedTask.createdAt(), allOf(notNullValue(), greaterThanOrEqualTo(start), lessThanOrEqualTo(now))); assertThat(inspectedTask.updatedAt(), allOf(notNullValue(), greaterThanOrEqualTo(start), lessThanOrEqualTo(now))); assertThat(inspectedTask.slot(), allOf(notNullValue(), greaterThan(0))); assertThat(inspectedTask.status(), notNullValue()); assertThat(inspectedTask.name(), nullValue()); assertEquals(serviceCreateResponse.id(), inspectedTask.serviceId()); if (serviceSpec.labels() == null || serviceSpec.labels().isEmpty()) { // Hamcrest has generally bad support for "is null or empty", // and no support at all for empty maps assertTrue(inspectedTask.labels() == null || inspectedTask.labels().isEmpty()); } else { assertEquals(serviceSpec.labels(), inspectedTask.labels()); } assertThat(inspectedTask.desiredState(), is(anything())); assertThat(inspectedTask.networkAttachments(), is(anything())); final TaskSpec taskSpecTemplate = serviceSpec.taskTemplate(); final TaskSpec taskSpecActual = inspectedTask.spec(); assertEquals(taskSpecTemplate.resources(), taskSpecActual.resources()); assertEquals(taskSpecTemplate.restartPolicy(), taskSpecActual.restartPolicy()); assertEquals(taskSpecTemplate.placement(), taskSpecActual.placement()); assertEquals(taskSpecTemplate.networks(), taskSpecActual.networks()); assertEquals(taskSpecTemplate.logDriver(), taskSpecActual.logDriver()); final ContainerSpec containerSpecTemplate = taskSpecTemplate.containerSpec(); final ContainerSpec containerSpecActual = taskSpecActual.containerSpec(); assertThat(containerSpecActual.image(), dockerApiVersionLessThan("1.25") ? is(containerSpecTemplate.image()) : startsWith(containerSpecTemplate.image() + ":latest@sha256:")); assertEquals(containerSpecTemplate.labels(), containerSpecActual.labels()); assertEquals(containerSpecTemplate.command(), containerSpecActual.command()); assertEquals(containerSpecTemplate.args(), containerSpecActual.args()); assertEquals(containerSpecTemplate.env(), containerSpecActual.env()); assertEquals(containerSpecTemplate.dir(), containerSpecActual.dir()); assertEquals(containerSpecTemplate.user(), containerSpecActual.user()); assertEquals(containerSpecTemplate.groups(), containerSpecActual.groups()); assertEquals(containerSpecTemplate.tty(), containerSpecActual.tty()); assertEquals(containerSpecTemplate.mounts(), containerSpecActual.mounts()); assertEquals(containerSpecTemplate.stopGracePeriod(), containerSpecActual.stopGracePeriod()); } @Test public void testListTasks() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final ServiceSpec spec = createServiceSpec(randomName()); assertThat(sut.listTasks().size(), is(0)); sut.createService(spec); await().until(numberOfTasks(sut), is(greaterThan(0))); assertThat(sut.listTasks().size(), is(4)); } @Test public void testListTasksWithGlobalService() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final int startingNumTasks = sut.listTasks().size(); final TaskSpec taskSpec = TaskSpec.builder() .containerSpec(ContainerSpec.builder().image("alpine") .command(new String[] { "ping", "-c1000", "localhost" }).build()) .build(); final ServiceSpec spec = ServiceSpec.builder() .name(randomName()) .taskTemplate(taskSpec) .mode(ServiceMode.withGlobal()) .build(); final ServiceCreateResponse response = sut.createService(spec); assertThat(response.id(), is(notNullValue())); await().until(numberOfTasks(sut), is(greaterThan(startingNumTasks))); sut.listTasks(); } @Test public void testListTaskWithCriteria() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final ServiceSpec spec = createServiceSpec(randomName()); assertThat(sut.listTasks().size(), is(0)); sut.createService(spec); await().until(numberOfTasks(sut), is(greaterThan(0))); final Task transientTask = sut.listTasks().get(1); await().until(taskState(transientTask.id(), sut), is(transientTask.desiredState())); final Task task = sut.inspectTask(transientTask.id()); final List<Task> tasksWithId = sut.listTasks(Task.find().taskId(task.id()).build()); assertThat(tasksWithId.size(), is(1)); assertThat(tasksWithId.get(0), equalTo(task)); final List<Task> tasksWithServiceName = sut.listTasks(Task.find().serviceName(spec.name()).build()); assertThat(tasksWithServiceName.size(), is(greaterThanOrEqualTo(1))); final Set<String> taskIds = Sets.newHashSet( Lists.transform(tasksWithServiceName, new Function<Task, String>() { @Nullable @Override public String apply(@Nullable final Task task) { return task == null ? null : task.id(); } })); assertThat(task.id(), isIn(taskIds)); } @Test public void testListNodes() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); List<Node> nodes = sut.listNodes(); assertThat(nodes.size(), greaterThanOrEqualTo(1)); Node nut = nodes.get(0); Date now = new Date(); assertThat(nut.id(), allOf(notNullValue(), not(""))); assertThat(nut.version().index(), allOf(notNullValue(), greaterThan(0L))); assertThat(nut.createdAt(), allOf(notNullValue(), lessThanOrEqualTo(now))); assertThat(nut.updatedAt(), allOf(notNullValue(), lessThanOrEqualTo(now))); NodeSpec specs = nut.spec(); assertThat(specs, notNullValue()); assertThat(specs.name(), is(anything())); // Can be null if not set assertThat(specs.labels(), is(anything())); // Can be null if not set assertThat(specs.role(), isIn(new String [] {"manager", "worker"})); assertThat(specs.availability(), isIn(new String [] {"active", "pause", "drain"})); NodeDescription desc = nut.description(); assertThat(desc.hostname(), allOf(notNullValue(), not(""))); assertThat(desc.platform(), notNullValue()); assertThat(desc.platform().architecture(), allOf(notNullValue(), not(""))); assertThat(desc.platform().os(), allOf(notNullValue(), not(""))); assertThat(desc.resources(), notNullValue()); assertThat(desc.resources().memoryBytes(), greaterThan(0L)); assertThat(desc.resources().nanoCpus(), greaterThan(0L)); EngineConfig engine = desc.engine(); assertThat(engine, notNullValue()); assertThat(engine.engineVersion(), allOf(notNullValue(), not(""))); assertThat(engine.labels(), is(anything())); assertThat(engine.plugins().size(), greaterThanOrEqualTo(0)); for (EnginePlugin plugin : engine.plugins()) { assertThat(plugin.type(), allOf(notNullValue(), not(""))); assertThat(plugin.name(), allOf(notNullValue(), not(""))); } } @SuppressWarnings("ConstantConditions") @Test public void testMountTmpfsOptions() throws Exception { requireDockerApiVersionAtLeast("1.24", "swarm support"); final long expectedSizeBytes = 100000L; final int expectedMode = 777; final String serviceName = randomName(); final TaskSpec taskSpec = TaskSpec .builder() .containerSpec(ContainerSpec.builder() .image("alpine") .command(new String[] {"ping", "-c1000", "localhost"}) .mounts(Mount.builder() .tmpfsOptions(TmpfsOptions.builder() .sizeBytes(expectedSizeBytes) .mode(expectedMode) .build()) .build()) .build()) .build(); final ServiceSpec serviceSpec = ServiceSpec.builder() .name(serviceName) .taskTemplate(taskSpec) .build(); final int startingNumTasks = sut.listServices().size(); final ServiceCreateResponse response = sut.createService(serviceSpec); assertThat(response.id(), is(notNullValue())); await().until(numberOfTasks(sut), is(greaterThan(startingNumTasks))); final Service service = sut.inspectService(serviceName); final ServiceSpec actualServiceSpec = service.spec(); final TmpfsOptions tmpfsOptions = actualServiceSpec.taskTemplate().containerSpec() .mounts().get(0).tmpfsOptions(); // TODO (dxia) Why is it null on travis-ci? if (tmpfsOptions != null) { assertThat(tmpfsOptions.sizeBytes(), equalTo(expectedSizeBytes)); assertThat(tmpfsOptions.mode(), equalTo(expectedMode)); } } private ServiceSpec createServiceSpec(final String serviceName) { return this.createServiceSpec(serviceName, new HashMap<String, String>()); } private ServiceSpec createServiceSpec(final String serviceName, final Map<String, String> labels) { final TaskSpec taskSpec = TaskSpec .builder() .containerSpec(ContainerSpec.builder().image("alpine") .command(new String[] {"ping", "-c1000", "localhost"}).build()) .build(); final ServiceMode serviceMode = ServiceMode.withReplicas(4); return ServiceSpec.builder().name(serviceName).taskTemplate(taskSpec) .mode(serviceMode) .labels(labels) .build(); } private String randomName() { return nameTag + '-' + toHexString(ThreadLocalRandom.current().nextLong()); } private void awaitConnectable(final InetAddress address, final int port) throws InterruptedException { while (true) { try (Socket ignored = new Socket(address, port)) { return; } catch (IOException e) { Thread.sleep(100); } } } private PoolStats getClientConnectionPoolStats(final DefaultDockerClient client) { return ((PoolingHttpClientConnectionManager) client.getClient().getConfiguration() .getProperty(ApacheClientProperties.CONNECTION_MANAGER)).getTotalStats(); } private String createSleepingContainer() throws Exception { sut.pull(BUSYBOX_LATEST); final String volumeContainer = randomName(); final ContainerConfig volumeConfig = ContainerConfig.builder().image(BUSYBOX_LATEST) .cmd("sh", "-c", "for i in `seq 1 7`; do " + "sleep ${i} ;" + "echo \"Seen output after ${i} seconds.\" ;" + "done;" + "echo Finished ;") .build(); sut.createContainer(volumeConfig, volumeContainer); sut.startContainer(volumeContainer); return volumeContainer; } private void verifyNoTimeoutContainer(final String volumeContainer, final StringBuffer result) throws Exception { log.info("Reading has finished, waiting for program to end."); sut.waitContainer(volumeContainer); final ContainerInfo info = sut.inspectContainer(volumeContainer); assertThat(result.toString().contains("Finished"), is(true)); assertThat(info.state().running(), is(false)); assertThat(info.state().exitCode(), is(0)); } private List<String> containersToIds(final List<Container> containers) { final Function<Container, String> containerToId = new Function<Container, String>() { @Override public String apply(final Container container) { return container.id(); } }; return Lists.transform(containers, containerToId); } private List<String> imagesToShortIds(final List<Image> images) { final Function<Image, String> imageToShortId = new Function<Image, String>() { @Override public String apply(final Image image) { return image.id().substring(0, 12); } }; return Lists.transform(images, imageToShortId); } private List<String> imagesToShortIdsAndRemoveSha256(final List<Image> images) { final Function<Image, String> imageToShortId = new Function<Image, String>() { @Override public String apply(final Image image) { return image.id().replaceFirst("sha256:", "").substring(0, 12); } }; return Lists.transform(images, imageToShortId); } private Callable<Boolean> containerIsRunning(final DockerClient client, final String containerId) { return new Callable<Boolean>() { public Boolean call() throws Exception { try { final ContainerInfo containerInfo = client.inspectContainer(containerId); return containerInfo.state().running(); } catch (ContainerNotFoundException ignored) { // Ignore exception. If container is not found, it is not running. return false; } } }; } private Callable<Integer> numberOfTasks(final DockerClient client) { return new Callable<Integer>() { public Integer call() throws Exception { return client.listTasks().size(); } }; } private Callable<String> taskState(final String taskId, final DockerClient client) { return new Callable<String>() { public String call() throws Exception { return client.inspectTask(taskId).status().state(); } }; } private static Matcher<PortConfig> portConfigWith( final Matcher<String> nameMatcher, final Matcher<String> protocolMatcher, final Matcher<Integer> targetPortMatcher, final Matcher<Integer> publishedPortMatcher, final Matcher<PortConfigPublishMode> publishModeMatcher) { final String description = "for PortConfig"; return new CustomTypeSafeMatcher<PortConfig>(description) { @Override protected boolean matchesSafely(final PortConfig portConfig) { return nameMatcher.matches(portConfig.name()) && protocolMatcher.matches(portConfig.protocol()) && targetPortMatcher.matches(portConfig.targetPort()) && publishedPortMatcher.matches(portConfig.publishedPort()) && publishModeMatcher.matches(portConfig.publishMode()); } }; } }