Java tutorial
/* * Copyright 2015 Steffen Folman Srensen * * 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 dk.sublife.docker.integration; import com.google.common.collect.ImmutableList; import com.spotify.docker.client.DockerClient; import com.spotify.docker.client.DockerException; import com.spotify.docker.client.DockerRequestException; import com.spotify.docker.client.ImageNotFoundException; import com.spotify.docker.client.LogStream; import com.spotify.docker.client.messages.ContainerConfig; import com.spotify.docker.client.messages.ContainerCreation; import com.spotify.docker.client.messages.ContainerInfo; import com.spotify.docker.client.messages.HostConfig; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import java.io.IOException; import java.net.UnknownHostException; import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import static com.spotify.docker.client.DockerClient.LogsParam.stderr; import static com.spotify.docker.client.DockerClient.LogsParam.stdout; /** * Docker container class. * <p/> * Implement this class to create a docker container designed for use with * integration testing. */ abstract public class Container implements InitializingBean, DisposableBean { /** * slf4j logger instance. */ private static final Logger LOGGER = LoggerFactory.getLogger(Container.class); /** * Docker container instance. */ private ContainerCreation container; /** * Docker client. */ @Autowired private DockerClient dockerClient; /** * Docker host config */ @Autowired private HostConfig hostConfig; private boolean isUp = false; @Value("${dk.sublife.dk.docker.integration.waitForTimeout:60}") private Integer waitForTimeout; /** * Create docker container config. * * @return docker container config */ abstract protected ContainerConfig createContainerConfig() throws Exception; /** * Check if service is up and running. * <p/> * Retry policies is enforced by the waitFor method. * * @return true if service is up and running */ abstract public boolean isUp(); /** * Post create actions. * * Implement this method to perform post startup actions. * This method is executed after docker container is created. This is the * first time it is possible to inspect the container. This allows for * retrieval of the ip address and for example copy files into the container * before the is actually started. * * @return boolean true if everything went according to plan */ protected boolean postCreateContainer() { return true; } /** * Post container start actions. * * Implement this method to perform post container startup actions. * This method is executed after the container is started but before isUp has been run to verify that the container * is fully started. * * @return boolean true if everything went according to plan */ protected boolean postStartContainer() { return true; } /** * Post startup actions. * * Implement this method to perform post startup actions. * This method is executed after isUp returns true but before waitFor returns. * * @return boolean postStartup actions. */ protected boolean postStartup() { return true; } /** * Create default docker host configuration. * <p/> * Default host configuration can be overwritten by creating a bean which * exposes a {@link HostConfig} instance. * <p/> * To create a custom host config for a container simply overwrite this method. * * @return host config */ protected HostConfig createHostConfig() { return hostConfig; } /** * Create Container config builder from image. * * @param image docker image * @param env Environment variables * @return Container config builder */ protected ContainerConfig.Builder image(final String image, String... env) { return ContainerConfig.builder().hostConfig(createHostConfig()).env(ImmutableList.copyOf(env)).image(image); } /** * Wait for isUp method call is satisfied. * <p/> * Overwrite this method to change the default wait timeout, before returning * errors. the default timout is 60 seconds. * * @return true if images is running * @throws InterruptedException */ public boolean waitFor() throws Exception { return waitFor(waitForTimeout); } /** * Wait for isUp method call is satisfied. * * @param timoutSeconds seconds before failing * @return true when service is available * @throws InterruptedException */ protected boolean waitFor(long timoutSeconds) throws Exception { final Instant start = Instant.now(); final ContainerInfo inspect = inspect(); final String name = inspect.name(); final String image = inspect.config().image(); if (LOGGER.isInfoEnabled() && !isUp) { LOGGER.info("Waiting for container is up: {}{}", image, name); } while (!isUp) { try { if (inspect().state().running()) { isUp = isUp(); if (isUp) { if (LOGGER.isInfoEnabled()) { LOGGER.info("Running post startup actions..."); } if (!postStartup()) { throw new RuntimeException("Post startup failed!"); } } } else { LOGGER.error("Container is not up: {}{}", image, name); logFromContainer(); throw new RuntimeException("Container died."); } } catch (RuntimeException e) { throw e; } catch (Exception e) { LOGGER.info(e.getMessage()); } final long seconds = Duration.between(start, Instant.now()).getSeconds(); if (seconds > timoutSeconds) { try { if (inspect().state().running()) { LOGGER.error("Container is running but not up: {}{}", image, name); logFromContainer(); } } catch (final Exception e) { LOGGER.error("Error inspecting container!", e); } throw new RuntimeException("Wait time exceeded."); } Thread.sleep(5000); } if (LOGGER.isInfoEnabled()) { LOGGER.info("container is up {}{}", image, name); } return true; } protected void logFromContainer() throws DockerException, InterruptedException { final ContainerInfo inspect = inspect(); final String id = inspect.id(); final String name = inspect.name(); final String image = inspect.config().image(); try (final LogStream logs = dockerClient.logs(id, stderr(), stdout())) { final String fullLog = logs.readFully(); LOGGER.error("Container logs from {}{}:\n {}", image, name, fullLog); } } /** * Get docker container ip address * * @return IP Address * @throws DockerException * @throws InterruptedException * @throws UnknownHostException */ public String address() throws DockerException, InterruptedException, UnknownHostException { return inspect().networkSettings().ipAddress(); } /** * Get docker container name * * @return container name * @throws DockerException * @throws InterruptedException * @throws UnknownHostException */ public String name() throws DockerException, InterruptedException, UnknownHostException { return inspect().name(); } /** * Inspect docker container. * * @return * @throws DockerException * @throws InterruptedException */ public ContainerInfo inspect() throws DockerException, InterruptedException { return dockerClient.inspectContainer(container.id()); } /** * Pull docker image. * * @param images * @throws DockerException * @throws InterruptedException */ protected void pull(final String images) throws DockerException, InterruptedException { dockerClient.pull(images); } /** * Invoked by a BeanFactory after it has set all bean properties supplied * (and satisfied BeanFactoryAware and ApplicationContextAware). * <p>This method allows the bean instance to perform initialization only * possible when all bean properties have been set and to throw an * exception in the event of misconfiguration. * * @throws Exception in the event of misconfiguration (such * as failure to set an essential property) or if initialization fails. */ @Override synchronized public void afterPropertiesSet() throws Exception { final ContainerConfig containerConfig = createContainerConfig(); this.container = createContainer(containerConfig); try { if (!postCreateContainer()) { throw new RuntimeException("Post create container failed!"); } LOGGER.info("Starting container: image: {}, name: {}, address: {}", containerConfig.image(), name(), address()); startContainer(); try { if (!postStartContainer()) { throw new RuntimeException("Post start container failed!"); } } catch (final Exception postStartContainerException) { killContainer(); throw postStartContainerException; } } catch (final Exception postCreateContainerException) { removeContainer(); throw new RuntimeException(postCreateContainerException); } } protected ContainerCreation createContainer(final ContainerConfig containerConfig) throws DockerException, InterruptedException { try { pull(containerConfig.image()); } catch (DockerException e) { LOGGER.warn("Unable to fetch docker image: {}", e.getMessage()); LOGGER.warn(e.toString()); } return dockerClient.createContainer(containerConfig); } protected void startContainer() throws DockerException, InterruptedException, UnknownHostException { final String id = container.id(); try { inspect().config().env().forEach(s -> LOGGER.info("Environment Variable: {}", s)); } catch (NullPointerException e) { LOGGER.info("No environment variables set for container"); } dockerClient.startContainer(id); } /** * Invoked by a BeanFactory on destruction of a singleton. * * @throws Exception in case of shutdown errors. * Exceptions will get logged but not rethrown to allow * other beans to release their resources too. */ @Override public void destroy() throws Exception { try { killContainer(); } finally { removeContainer(); } } protected void killContainer() throws DockerException, InterruptedException { final String id = container.id(); final String name = dockerClient.inspectContainer(id).name(); LOGGER.info("Stopping container: {}", name); try { dockerClient.killContainer(container.id()); } catch (final DockerRequestException e) { LOGGER.warn("Docker request error during kill!", e); } LOGGER.info("Container stopped: {}", name); } protected void removeContainer() throws DockerException, InterruptedException { final String id = container.id(); final String name = dockerClient.inspectContainer(id).name(); LOGGER.info("Removing container: {}", name); dockerClient.removeContainer(id, true); LOGGER.info("Container removed: {}", name); } /** /** * Copies a local directory to the container. * * @param localDirectory The local directory to send to the container. * @param containerDirectory The directory inside the container where the files are copied to. */ public void copyToContainer(final Path localDirectory, final Path containerDirectory) throws InterruptedException, DockerException, IOException { final String id = container.id(); dockerClient.copyToContainer(localDirectory, id, containerDirectory.toString()); } }