package org.testcontainers.utility; import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.DockerException; import com.github.dockerjava.api.exception.InternalServerErrorException; import com.github.dockerjava.api.exception.NotFoundException; import com.github.dockerjava.api.model.Container; import com.github.dockerjava.api.model.Network; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testcontainers.DockerClientFactory; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Component that responsible for container removal and automatic cleanup of dead containers at JVM shutdown. */ public final class ResourceReaper { private static final Logger LOGGER = LoggerFactory.getLogger(ResourceReaper.class); private static ResourceReaper instance; private final DockerClient dockerClient; private Map<String, String> registeredContainers = new ConcurrentHashMap<>(); private List<String> registeredNetworks = new ArrayList<>(); private ResourceReaper() { dockerClient = DockerClientFactory.instance().client(); // If the JVM stops without containers being stopped, try and stop the container. Runtime.getRuntime().addShutdownHook(new Thread(this::performCleanup)); } public synchronized static ResourceReaper instance() { if (instance == null) { instance = new ResourceReaper(); } return instance; } /** * Perform a cleanup. * */ public synchronized void performCleanup() { registeredContainers.forEach(this::stopContainer); registeredNetworks.forEach(this::removeNetwork); } /** * Register a container to be cleaned up, either on explicit call to stopAndRemoveContainer, or at JVM shutdown. * * @param containerId the ID of the container * @param imageName the image name of the container (used for logging) */ public void registerContainerForCleanup(String containerId, String imageName) { registeredContainers.put(containerId, imageName); } /** * Stop a potentially running container and remove it, including associated volumes. * * @param containerId the ID of the container */ public void stopAndRemoveContainer(String containerId) { stopContainer(containerId, registeredContainers.get(containerId)); registeredContainers.remove(containerId); } /** * Stop a potentially running container and remove it, including associated volumes. * * @param containerId the ID of the container * @param imageName the image name of the container (used for logging) */ public void stopAndRemoveContainer(String containerId, String imageName) { stopContainer(containerId, imageName); registeredContainers.remove(containerId); } private void stopContainer(String containerId, String imageName) { boolean running; try { InspectContainerResponse containerInfo = dockerClient.inspectContainerCmd(containerId).exec(); running = containerInfo.getState().getRunning(); } catch (NotFoundException e) { LOGGER.trace("Was going to stop container but it apparently no longer exists: {}"); return; } catch (DockerException e) { LOGGER.trace("Error encountered when checking container for shutdown (ID: {}) - it may not have been stopped, or may already be stopped: {}", containerId, e.getMessage()); return; } if (running) { try { LOGGER.trace("Stopping container: {}", containerId); dockerClient.killContainerCmd(containerId).exec(); LOGGER.trace("Stopped container: {}", imageName); } catch (DockerException e) { LOGGER.trace("Error encountered shutting down container (ID: {}) - it may not have been stopped, or may already be stopped: {}", containerId, e.getMessage()); } } try { dockerClient.inspectContainerCmd(containerId).exec(); } catch (NotFoundException e) { LOGGER.trace("Was going to remove container but it apparently no longer exists: {}"); return; } try { LOGGER.trace("Removing container: {}", containerId); try { dockerClient.removeContainerCmd(containerId).withRemoveVolumes(true).withForce(true).exec(); LOGGER.debug("Removed container and associated volume(s): {}", imageName); } catch (InternalServerErrorException e) { LOGGER.trace("Exception when removing container with associated volume(s): {} (due to {})", imageName, e.getMessage()); } } catch (DockerException e) { LOGGER.trace("Error encountered shutting down container (ID: {}) - it may not have been stopped, or may already be stopped: {}", containerId, e.getMessage()); } } /** * Register a network to be cleaned up at JVM shutdown. * * @param networkName the image name of the network */ public void registerNetworkForCleanup(String networkName) { registeredNetworks.add(networkName); } /** * Removes any networks that contain the identifier. * @param identifier */ public void removeNetworks(String identifier) { removeNetwork(identifier); } private void removeNetwork(String networkName) { List<Network> networks; try { networks = dockerClient.listNetworksCmd().withNameFilter(networkName).exec(); } catch (DockerException e) { LOGGER.trace("Error encountered when looking up network for removal (name: {}) - it may not have been removed", networkName); return; } for (Network network : networks) { try { dockerClient.removeNetworkCmd(network.getId()).exec(); registeredNetworks.remove(network.getId()); LOGGER.debug("Removed network: {}", networkName); } catch (DockerException e) { LOGGER.trace("Error encountered removing network (name: {}) - it may not have been removed", network.getName()); } } } }