All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.rnorth.testcontainers.containers.AbstractContainer Maven / Gradle / Ivy

package org.rnorth.testcontainers.containers;

import com.spotify.docker.client.*;
import com.spotify.docker.client.messages.*;
import org.rnorth.testcontainers.utility.PathOperations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.Executors;

import static org.rnorth.testcontainers.utility.CommandLine.runShellCommand;

/**
 * @author richardnorth
 */
public abstract class AbstractContainer {

    private static final Logger LOGGER = LoggerFactory.getLogger("TestContainer");

    protected String dockerHostIpAddress;
    protected String containerId;
    protected DockerClient dockerClient;
    protected String tag = "latest";
    private boolean normalTermination = false;

    public void start() {

        try {
            DefaultDockerClient.Builder builder = DefaultDockerClient.builder();

            customizeBuilderForOs(builder);

            dockerClient = builder.build();

            pullImageIfNeeded(getDockerImageName());

            ContainerConfig containerConfig = getContainerConfig();

            HostConfig.Builder hostConfigBuilder = HostConfig.builder()
                    .publishAllPorts(true);
            customizeHostConfigBuilder(hostConfigBuilder);
            HostConfig hostConfig = hostConfigBuilder.build();

            LOGGER.info("Creating container for image: {}", getDockerImageName());
            ContainerCreation containerCreation = dockerClient.createContainer(containerConfig);

            containerId = containerCreation.id();
            dockerClient.startContainer(containerId, hostConfig);
            LOGGER.info("Starting container with ID: {}", containerId);

            ContainerInfo containerInfo = dockerClient.inspectContainer(containerId);

            containerIsStarting(containerInfo);

            waitUntilContainerStarted();
            LOGGER.info("Container started");

            // If the container stops before the after() method, its termination was unexpected
            Executors.newSingleThreadExecutor().submit(new Runnable() {
                @Override
                public void run() {
                    Exception caughtException = null;
                    try {
                        dockerClient.waitContainer(containerId);
                    } catch (DockerException | InterruptedException e) {
                        caughtException = e;
                    }

                    if (!normalTermination) {
                        throw new RuntimeException("Container exited unexpectedly", caughtException);
                    }
                }
            });

            // If the JVM stops without the container being stopped, try and stop the container
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
                    AbstractContainer.this.stop();
                }
            }));
        } catch (Exception e) {
            LOGGER.error("Could not start container", e);
        }
    }

    protected void customizeHostConfigBuilder(HostConfig.Builder hostConfigBuilder) {

    }

    private void pullImageIfNeeded(final String imageName) throws DockerException, InterruptedException {
        List images = dockerClient.listImages(DockerClient.ListImagesParam.create("name", getDockerImageName()));
        for (Image image : images) {
            if (image.repoTags().contains(imageName)) {
                // the image exists
                return;
            }
        }

        LOGGER.info("Pulling docker image: {}. Please be patient; this may take some time but only needs to be done once.", imageName);
        dockerClient.pull(getDockerImageName(), new ProgressHandler() {
            @Override
            public void progress(ProgressMessage message) throws DockerException {
                if (message.error() != null) {
                    if (message.error().contains("404") || message.error().contains("not found")) {
                        throw new ImageNotFoundException(imageName, message.toString());
                    } else {
                        throw new ImagePullFailedException(imageName, message.toString());
                    }
                }
            }
        });
    }

    public void stop() {
        try {
            LOGGER.info("Stopping container: {}", containerId);
            normalTermination = true;
            dockerClient.killContainer(containerId);
            dockerClient.removeContainer(containerId, true);
        } catch (DockerException | InterruptedException e) {
            LOGGER.debug("Error encountered shutting down container (ID: {}) - it may not have been stopped, or may already be stopped: {}", containerId, e.getMessage());
        }
    }

    protected Path createVolumeDirectory(boolean temporary) throws IOException {
        File file = new File(".tmp-volume");
        file.mkdirs();
        final Path directory = file.toPath();

        if (temporary) Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                PathOperations.recursiveDeleteDir(directory);
            }
        }));

        return directory;
    }

    protected abstract void containerIsStarting(ContainerInfo containerInfo);

    protected abstract String getLivenessCheckPort();

    protected abstract ContainerConfig getContainerConfig();

    protected abstract String getDockerImageName();

    /**
     * Wait until the container has started. The default implementation simply
     * waits for a port to start listening; subclasses may override if more
     * sophisticated behaviour is required.
     */
    protected void waitUntilContainerStarted() {
        waitForListeningPort(dockerHostIpAddress, getLivenessCheckPort());
    }

    protected void waitForListeningPort(String ipAddress, String port) {

        if (port == null) {
            return;
        }

        for (int i = 0; i < 6000; i++) {
            try {
                new Socket(ipAddress, Integer.valueOf(port));
                return;
            } catch (IOException e) {
                try {
                    Thread.sleep(100L);
                } catch (InterruptedException ignored) {
                }
            }
        }
        throw new IllegalStateException("Timed out waiting for container port to open (" + ipAddress + ":" + port + " should be listening)");
    }

    private void customizeBuilderForOs(DefaultDockerClient.Builder builder) throws Exception {
        if (System.getProperty("os.name").toLowerCase().contains("mac")) {
            // Running on a Mac therefore use boot2docker
            runShellCommand("/usr/local/bin/boot2docker", "up");
            dockerHostIpAddress = runShellCommand("/usr/local/bin/boot2docker", "ip");

            builder.uri("https://" + dockerHostIpAddress + ":2376")
                    .dockerCertificates(new DockerCertificates(Paths.get(System.getProperty("user.home") + "/.boot2docker/certs/boot2docker-vm")));
        } else {
            dockerHostIpAddress = "127.0.0.1";
        }
    }

    public void setTag(String tag) {
        this.tag = tag != null ? tag : "latest";
    }

    public String getId() {
        return containerId;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy