package com.github.junit5docker; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.command.InspectVolumeResponse; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.ExposedPort; import com.github.dockerjava.api.model.Ports; import com.github.dockerjava.core.DockerClientBuilder; import com.github.dockerjava.core.command.PullImageResultCallback; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import static com.github.dockerjava.core.DefaultDockerClientConfig.createDefaultConfigBuilder; import static com.github.junit5docker.assertions.CountDownLatchAssertions.assertThat; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonList; import static java.util.Optional.ofNullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @DisplayName("Default docker client's ") public class DefaultDockerClientIT { private static final int DEFAULT_DOCKER_ENV_NUMBER = 1; private DefaultDockerClient defaultDockerClient = new DefaultDockerClient(); private DockerClient dockerClient = DockerClientBuilder .getInstance(createDefaultConfigBuilder().withApiVersion("1.22")) .build(); private List<Container> existingContainers; @BeforeEach public void getExistingContainers() { existingContainers = dockerClient.listContainersCmd().exec(); } @AfterEach public void stopAndRemoveStartedContainers() { dockerClient.listContainersCmd().exec().stream() .filter(container -> !existingContainers.contains(container)) .forEach(container -> { dockerClient.stopContainerCmd(container.getId()).exec(); dockerClient.removeContainerCmd(container.getId()).exec(); }); } private void ensureImageExists(String wantedImage) { try { dockerClient.inspectImageCmd(wantedImage).exec(); } catch (NotFoundException e) { dockerClient.pullImageCmd(wantedImage).exec(new PullImageResultCallback()).awaitSuccess(); } } @Nested @DisplayName("startContainer method") class StartContainerMethod { @Nested @DisplayName("with image already pulled should") class WithImageAlreadyPulled { private static final String WANTED_IMAGE = "faustxvi/simple-two-ports:latest"; @BeforeEach public void ensureImageIsPulled() { ensureImageExists(WANTED_IMAGE); } @Test @DisplayName("start a container without ports") public void shouldStartContainer() { String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap()); assertThat(dockerClient.listContainersCmd().exec()).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); assertThat(startedContainer.getConfig().getImage()).isEqualTo(WANTED_IMAGE); } @Test @DisplayName("start a container with one port") public void shouldStartContainerWithOnePort() { String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap(), new PortBinding(8081, 8080)); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); Ports ports = startedContainer.getHostConfig().getPortBindings(); assertThat(ports).isNotNull(); Map<ExposedPort, Ports.Binding[]> portBindings = ports.getBindings(); assertThat(portBindings).hasSize(1) .containsKeys(new ExposedPort(8080)); assertThat(portBindings.get(new ExposedPort(8080))).hasSize(1) .extracting(Ports.Binding::getHostPortSpec) .contains("8081"); } @Test @DisplayName("start a container with environment variables >:)") public void shouldStartContainerWithEnvironmentVariables() { Map<String, String> environments = new HashMap<>(); environments.put("khaled", "souf"); environments.put("abdellah", "stagiaire"); String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, environments); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); List<String> envs = Arrays.asList(startedContainer.getConfig().getEnv()); assertThat(envs).hasSize(2 + DEFAULT_DOCKER_ENV_NUMBER) .contains("khaled=souf", "abdellah=stagiaire"); } } @Nested @DisplayName("with image not pulled should") class WithImageNotPulled { private static final String WANTED_IMAGE = "faustxvi/simple-two-ports:latest"; @BeforeEach public void ensureContainerIsNotPresent() { try { String imageToRemove = dockerClient.inspectImageCmd(WANTED_IMAGE).exec().getId(); dockerClient.removeImageCmd(imageToRemove).exec(); } catch (NotFoundException e) { // not found, no problems } } @Test @DisplayName("start a container after pulling the image") public void shouldStartContainer() { String containerId = defaultDockerClient.startContainer(WANTED_IMAGE, emptyMap()); assertThat(dockerClient.listContainersCmd().exec()).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); assertThat(startedContainer.getConfig().getImage()).isEqualTo(WANTED_IMAGE); } @Nested @DisplayName("with a bug in docker-java should") class WithABugInDockerJava { @Test @DisplayName("add latest to the image name if none is given") public void shouldStartLatestContainer() { String containerId = defaultDockerClient.startContainer("faustxvi/simple-two-ports", emptyMap()); List<Container> currentContainers = dockerClient.listContainersCmd().exec(); assertThat(currentContainers).hasSize(existingContainers.size() + 1); InspectContainerResponse startedContainer = dockerClient.inspectContainerCmd(containerId).exec(); assertThat(startedContainer.getConfig().getImage()).isEqualTo(WANTED_IMAGE); } } } } @Nested @DisplayName("stopAndRemove method") class StopAndRemoveContainerMethod { @Nested @DisplayName("without volumes") class WithOutVolumes { private static final String WANTED_IMAGE = "faustxvi/simple-two-ports:latest"; private String containerId; @BeforeEach public void startAContainer() { ensureImageExists(WANTED_IMAGE); containerId = dockerClient.createContainerCmd(WANTED_IMAGE).exec().getId(); dockerClient.startContainerCmd(containerId).exec(); } @Test @DisplayName("should remove the container") public void shouldRemoveTheContainer() { defaultDockerClient.stopAndRemoveContainer(containerId); assertThat(dockerClient.listContainersCmd().exec()).hasSize(existingContainers.size()); assertThatExceptionOfType(NotFoundException.class) .isThrownBy(() -> dockerClient.inspectContainerCmd(containerId).exec()); } } @Nested @DisplayName("with volumes") class WithVolumes { private static final String WANTED_IMAGE = "faustxvi/with-volume:latest"; private List<InspectVolumeResponse> existingVolumes; private String containerId; @BeforeEach public void startAContainer() { ensureImageExists(WANTED_IMAGE); existingVolumes = volumes(); containerId = dockerClient.createContainerCmd(WANTED_IMAGE).exec().getId(); dockerClient.startContainerCmd(containerId).exec(); } @Test @DisplayName("should remove the container's volumes") public void shouldRemoveVolumes() { defaultDockerClient.stopAndRemoveContainer(containerId); assertThat(volumes()).hasSameSizeAs(existingVolumes); } private List<InspectVolumeResponse> volumes() { return ofNullable(dockerClient.listVolumesCmd().exec().getVolumes()).orElseGet(ArrayList::new); } } } @Nested @DisplayName("log method") class LogMethod { @Nested @DisplayName("with a working image") class WithAWorkingImage { private static final String WANTED_IMAGE = "faustxvi/open-port-later"; private String containerId; @BeforeEach public void startAContainer() { ensureImageExists(WANTED_IMAGE); } @Test public void shouldGiveLogsInStream() { containerId = dockerClient.createContainerCmd(WANTED_IMAGE).withEnv(singletonList("WAITING_TIME=1ms")) .exec() .getId(); dockerClient.startContainerCmd(containerId).exec(); Stream<String> logs = defaultDockerClient.logs(containerId); Optional<String> firstLine = logs.findFirst(); assertThat(firstLine).isPresent() .hasValueSatisfying("started"::equals); } } @Nested @DisplayName("with a buggy image") class WithABuggyImage { private static final String WANTED_IMAGE = "faustxvi/log-and-quit"; private String containerId; @BeforeEach public void startAContainer() { ensureImageExists(WANTED_IMAGE); containerId = dockerClient.createContainerCmd(WANTED_IMAGE) .exec() .getId(); dockerClient.startContainerCmd(containerId).exec(); } @Test @DisplayName("should close stream when logs finish") public void shouldCloseWhenContainerCloses() throws InterruptedException { Stream<String> logs = defaultDockerClient.logs(containerId); ExecutorService executor = Executors.newSingleThreadExecutor(); CountDownLatch streamReadStarted = new CountDownLatch(1); CountDownLatch streamClosed = new CountDownLatch(1); executor .submit(() -> { logs.peek((t) -> streamReadStarted.countDown()) .filter((l) -> false).findFirst(); streamClosed.countDown(); }); try { streamReadStarted.await(); assertThat(streamClosed) .overridingErrorMessage("Log stream should have been closed") .isDownBefore(1, TimeUnit.SECONDS); } finally { executor.shutdown(); } } } } }