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

pl.domzal.junit.docker.rule.DockerRule Maven / Gradle / Ivy

There is a newer version: 0.6.0
Show newest version
package pl.domzal.junit.docker.rule;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Rule;
import org.junit.rules.ExternalResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.spotify.docker.client.shaded.com.google.common.collect.Lists;
import com.spotify.docker.client.DefaultDockerClient;
import com.spotify.docker.client.DockerClient;
import com.spotify.docker.client.DockerClient.ListImagesParam;
import com.spotify.docker.client.DockerClient.LogsParam;
import com.spotify.docker.client.LogStream;
import com.spotify.docker.client.exceptions.DockerCertificateException;
import com.spotify.docker.client.exceptions.DockerException;
import com.spotify.docker.client.exceptions.DockerRequestException;
import com.spotify.docker.client.exceptions.ImageNotFoundException;
import com.spotify.docker.client.messages.ContainerConfig;
import com.spotify.docker.client.messages.ContainerCreation;
import com.spotify.docker.client.messages.ContainerInfo;
import com.spotify.docker.client.messages.ContainerState;
import com.spotify.docker.client.messages.HostConfig;
import com.spotify.docker.client.messages.Image;
import com.spotify.docker.client.messages.PortBinding;

import pl.domzal.junit.docker.rule.ex.ImagePullException;
import pl.domzal.junit.docker.rule.ex.PortNotExposedException;
import pl.domzal.junit.docker.rule.wait.LineListener;
import pl.domzal.junit.docker.rule.wait.LineListenerProxy;
import pl.domzal.junit.docker.rule.wait.LogChecker;
import pl.domzal.junit.docker.rule.wait.StartCondition;
import pl.domzal.junit.docker.rule.wait.StartConditionCheck;

/**
 * Simple docker container junit {@link Rule}.
* Instances should be created via builder: *
 *  @Rule
 *  DockerRule container = DockerRule.builder()
 *      . //configuration directives
 *      .build();
 * 
*
* Inspired by and loosely based on osheeshel/DockerContainerRule. */ public class DockerRule extends ExternalResource { private static Logger log = LoggerFactory.getLogger(DockerRule.class); private static final int STOP_TIMEOUT = 5; private static final int SHORT_ID_LEN = 12; private final DockerRuleBuilder builder; private final String imageNameWithTag; private final DockerClient dockerClient; private ContainerCreation container; private String containerShortId; private String containerIp; private String containerGateway; private Map> containerPorts; private ContainerInfo containerInfo; private DockerLogs dockerLogs; private boolean isStarted = false; DockerRule(DockerRuleBuilder builder) { this.builder = builder; this.imageNameWithTag = imageNameWithTag(builder.imageName()); try { dockerClient = DefaultDockerClient.fromEnv().build(); log.debug("server.info: {}", dockerClient.info()); log.debug("server.version: {}", dockerClient.version()); if (builder.imageAlwaysPull() || ! imageAvaliable(dockerClient, imageNameWithTag)) { dockerClient.pull(imageNameWithTag); } } catch (ImageNotFoundException e) { throw new ImagePullException(String.format("Image '%s' not found", imageNameWithTag), e); } catch (DockerCertificateException | DockerException | InterruptedException e) { throw new IllegalStateException(e); } } /** * Builder to specify parameters and produce {@link DockerRule} instance. */ public static DockerRuleBuilder builder() { return new DockerRuleBuilder(); } /** * Create and start container.
* This is {@link ExternalResource#before()} made available as public - it may be helpful in scenarios * when you want to use {@link DockerRule} and operate it manually. */ @Override public final void before() throws Throwable { HostConfig.Builder hostConfigBuilder = HostConfig.builder() .publishAllPorts(builder.publishAllPorts())// .portBindings(builder.hostPortBindings())// .binds(builder.binds())// .links(links()); if (builder.restartPolicy() != null) { hostConfigBuilder.restartPolicy(builder.restartPolicy().getRestartPolicy()); } HostConfig hostConfig = hostConfigBuilder .extraHosts(builder.extraHosts())// .build(); ContainerConfig containerConfig = ContainerConfig.builder()// .hostConfig(hostConfig)// .image(imageNameWithTag)// .env(builder.env())// .networkDisabled(false)// .exposedPorts(builder.containerExposedPorts()) .entrypoint(builder.entrypoint()) .labels(builder.getLabels()) .cmd(builder.cmd()).build(); try { if (StringUtils.isNotBlank(builder.name())) { this.container = dockerClient.createContainer(containerConfig, builder.name()); } else { this.container = dockerClient.createContainer(containerConfig); } this.containerShortId = StringUtils.left(container.id(), SHORT_ID_LEN); log.info("container {} created, id {}, short id {}", imageNameWithTag, container.id(), containerShortId); log.debug("rule before {}", containerShortId); dockerClient.startContainer(container.id()); log.debug("{} started", containerShortId); LineListenerProxy proxyLineListener = new LineListenerProxy(); attachLogs(dockerClient, container.id(), proxyLineListener); ContainerInfo containerInfo = dockerClient.inspectContainer(container.id()); containerIp = containerInfo.networkSettings().ipAddress(); containerPorts = containerInfo.networkSettings().ports(); containerGateway = containerInfo.networkSettings().gateway(); this.containerInfo = containerInfo; executeWaitForConditions(proxyLineListener); logNetworkSettings(); isStarted = true; } catch (DockerRequestException e) { throw new IllegalStateException(e.getResponseBody(), e); } catch (DockerException | InterruptedException e) { throw new IllegalStateException(e); } } private boolean isStarted() { return isStarted; } private List links() { List resolvedLinks = new ArrayList<>(); resolvedLinks.addAll(builder.staticLinks()); for (Pair dynamicLink : builder.getDynamicLinks()) { DockerRule rule = dynamicLink.getKey(); String alias = dynamicLink.getValue(); if (!rule.isStarted()) { throw new IllegalStateException(String.format("container linked via alias '%s' is not started, make sure rule definitions assures target container will be started first", alias)); } resolvedLinks.add(rule.getContainerId() + ":" + alias); } return resolvedLinks; } private void executeWaitForConditions(LineListenerProxy proxyLineListener) throws TimeoutException { List conditions = Lists.newArrayList(); for (StartCondition conditionBuilder : builder.getWaitFor()) { conditions.add(conditionBuilder.build(this)); } registerConditionLineListeners(conditions, proxyLineListener); // execute waiting for (StartConditionCheck condition : conditions) { WaitForContainer.waitForCondition(condition, builder.waitForSeconds(), describe()); } } private void registerConditionLineListeners(List conditions, LineListenerProxy proxyLineListener) { for (StartConditionCheck condition : conditions) { if (condition instanceof LineListener) { proxyLineListener.add((LineListener) condition); } } } Integer findExternalPort(Integer internalPort) { try { return Integer.parseInt(findExternalPort(Integer.toString(internalPort))); } catch (NumberFormatException e) { throw new IllegalStateException("Internal rule problem - unable to parse exposed port number", e); } } private String findExternalPort(String internalPort) { String portAndProtocol = Ports.portWithProtocol(internalPort); if (! (containerPorts.containsKey(portAndProtocol) && containerPorts.get(portAndProtocol)!=null && containerPorts.get(portAndProtocol).size()>0)) { throw new PortNotExposedException(String.format("Port %s is not exposed (exposed port info: %s)", portAndProtocol, containerPorts)); } List portBindings = containerPorts.get(portAndProtocol); String firstExposedPort = portBindings.get(0).hostPort(); if (portBindings.size() > 1) { log.warn("{} port {} is bound to multiple external ports, assuming first one: {}", containerShortId, internalPort, firstExposedPort); } return firstExposedPort; } private void attachLogs(DockerClient dockerClient, String containerId, LineListener lineListener) throws IOException, InterruptedException { dockerLogs = new DockerLogs(dockerClient, containerId, lineListener); if (builder.stdoutWriter()!=null) { dockerLogs.setStdoutWriter(builder.stdoutWriter()); } if (builder.stderrWriter()!=null) { dockerLogs.setStderrWriter(builder.stderrWriter()); } dockerLogs.start(); } private boolean imageAvaliable(DockerClient dockerClient, String imageName) throws DockerException, InterruptedException { String imageNameWithTag = imageNameWithTag(imageName); List listImages = dockerClient.listImages(ListImagesParam.danglingImages(false)); for (Image image : listImages) { if (image.repoTags() != null && image.repoTags().contains(imageNameWithTag)) { log.debug("image '{}' found", imageNameWithTag); return true; } } log.debug("image '{}' not found", imageNameWithTag); return false; } private String imageNameWithTag(String imageName) { if (! StringUtils.contains(imageName, ':')) { return imageName + ":latest"; } else { return imageName; } } /** * Stop and remove container.
* This is {@link ExternalResource#before()} made available as public - it may be helpful in scenarios * when you want to use {@link DockerRule} and operate it manually. */ @Override public final void after() { log.debug("after {}", containerShortId); try { dockerLogs.close(); ContainerState state = dockerClient.inspectContainer(container.id()).state(); log.debug("{} state {}", containerShortId, state); if (state.running()) { if (builder.stopOptions().contains(StopOption.KILL)) { dockerClient.killContainer(container.id()); log.info("{} killed", containerShortId); } else { dockerClient.stopContainer(container.id(), STOP_TIMEOUT); log.info("{} stopped", containerShortId); } } if (builder.stopOptions().contains(StopOption.REMOVE)) { dockerClient.removeContainer(container.id(), DockerClient.RemoveContainerParam.removeVolumes()); log.info("{} deleted", containerShortId); container = null; } } catch (DockerException | InterruptedException e) { throw new IllegalStateException(e); } } /** * Address of docker host. Please note this is address of docker host as seen by docker client library * so it may not be valid docker host address in different contexts. *
* For example, if tests are run in unix-like environment with docker host on the same machine, * it will contain 'localhost' and will not point to docker host from inside container. * In such cases one should use {@link #getDockerContainerGateway()}. */ public final String getDockerHost() { return dockerClient.getHost(); } /** * Address of docker container gateway. */ public final String getDockerContainerGateway() { return containerGateway; } /** * Address of docker container. */ public String getContainerIp() { return containerIp; } /** * Get host dynamic port given container port was mapped to. * * @param containerPort Container port. Typically it matches Dockerfile EXPOSE directive. * @return Host port container port is published on. */ public final String getExposedContainerPort(String containerPort) { return findExternalPort(containerPort); } private void logNetworkSettings() { log.info("{} docker host: {}, ip: {}, gateway: {}, exposed ports: {}", containerShortId, dockerClient.getHost(), containerIp, containerGateway, containerPorts); } private String describe() { if (StringUtils.isAllBlank(containerShortId, builder.name(), builder.imageName())) { return super.toString(); } return (StringUtils.isNotBlank(builder.name()) ? String.format("'%s' ", builder.name()) : "") + (StringUtils.isNotBlank(containerShortId) ? containerShortId + " " : "") + builder.imageName(); } /** * Stop and wait till given string will show in container output. * * @param searchString String to wait for in container output. * @param waitTime Wait time. * @throws TimeoutException On wait timeout. * * @deprecated Use {@link #waitForLogMessage(String, int)} instead. */ public void waitFor(final String searchString, int waitTime) throws TimeoutException { waitForLogMessage(searchString, waitTime); } /** * Stop and wait till given string will show in container output. * * @param logSearchString String to wait for in container output. * @param waitTime Wait time. * @throws TimeoutException On wait timeout. */ public void waitForLogMessage(final String logSearchString, int waitTime) throws TimeoutException { WaitForContainer.waitForCondition(new LogChecker(this, logSearchString), waitTime, describe()); } /** * Block until container exit. */ public void waitForExit() throws InterruptedException { try { dockerClient.waitContainer(container.id()); } catch (DockerException e) { throw new IllegalStateException(e); } } /** * Container log. */ public String getLog() { try (LogStream stream = dockerClient.logs(container.id(), LogsParam.stdout(), LogsParam.stderr());) { String fullLog = stream.readFully(); if (log.isTraceEnabled()) { log.trace("{} full log: {}", containerShortId, StringUtils.replace(fullLog, "\n", "|")); } return fullLog; } catch (DockerException | InterruptedException e) { throw new IllegalStateException(e); } } /** * Id of container (null if it is not yet been created or has been stopped). */ public String getContainerId() { return (container!=null ? container.id() : null); } /** * Underlying library @{link ContainerInfo} data structure returned by {@link DockerClient#inspectContainer(String)} at container start. * * @return Started container info or null if container was not yet started. */ public ContainerInfo getContainerInfo() { return containerInfo; } /** * {@link DockerClient} for direct container manipulation. */ public DockerClient getDockerClient() { return dockerClient; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy