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

org.testcontainers.containers.GenericContainer Maven / Gradle / Ivy

There is a newer version: 1.20.1
Show newest version
package org.testcontainers.containers;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.ExecCreateCmdResponse;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.command.LogContainerCmd;
import com.github.dockerjava.api.exception.DockerException;
import com.github.dockerjava.api.model.*;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NonNull;
import org.apache.commons.lang.SystemUtils;
import org.jetbrains.annotations.Nullable;
import org.junit.runner.Description;
import org.rnorth.ducttape.ratelimits.RateLimiter;
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.slf4j.Logger;
import org.slf4j.profiler.Profiler;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.output.ToStringConsumer;
import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
import org.testcontainers.containers.startupcheck.MinimumDurationRunningStartupCheckStrategy;
import org.testcontainers.containers.startupcheck.StartupCheckStrategy;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.containers.wait.Wait;
import org.testcontainers.containers.wait.WaitStrategy;
import org.testcontainers.images.RemoteDockerImage;
import org.testcontainers.utility.*;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDERR;
import static org.testcontainers.containers.output.OutputFrame.OutputType.STDOUT;
import static org.testcontainers.utility.CommandLine.runShellCommand;

/**
 * Base class for that allows a container to be launched and controlled.
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class GenericContainer>
        extends FailureDetectingExternalResource
        implements Container {

    private static final Charset UTF8 = Charset.forName("UTF-8");

    public static final int CONTAINER_RUNNING_TIMEOUT_SEC = 30;

    /*
     * Default settings
     */
    @NonNull
    private List exposedPorts = new ArrayList<>();

    @NonNull
    private List portBindings = new ArrayList<>();

    @NonNull
    private List extraHosts = new ArrayList<>();

    @NonNull
    private String networkMode;

    @NonNull
    private Future image;

    @NonNull
    private List env = new ArrayList<>();

    @NonNull
    private String[] commandParts = new String[0];

    @NonNull
    private List binds = new ArrayList<>();

    @NonNull
    private Map linkedContainers = new HashMap<>();

    private StartupCheckStrategy startupCheckStrategy = new IsRunningStartupCheckStrategy();

    private int startupAttempts = 1;

    @Nullable
    private String workingDirectory = null;

    /*
     * Unique instance of DockerClient for use by this container object.
     */
    protected DockerClient dockerClient = DockerClientFactory.instance().client();

    /*
     * Info about the Docker server; lazily fetched.
     */
    protected Info dockerDaemonInfo = null;

    /*
     * Set during container startup
     */
    protected String containerId;
    protected String containerName;

    /**
     * The approach to determine if the container is ready.
     */
    @NonNull
    protected WaitStrategy waitStrategy = Wait.defaultWaitStrategy();

    @Nullable
    private InspectContainerResponse containerInfo;

    private List> logConsumers = new ArrayList<>();

    private static final Set AVAILABLE_IMAGE_NAME_CACHE = new HashSet<>();
    private static final RateLimiter DOCKER_CLIENT_RATE_LIMITER = RateLimiterBuilder
            .newBuilder()
            .withRate(1, TimeUnit.SECONDS)
            .withConstantThroughput()
            .build();


    public GenericContainer() {
        this("alpine:3.2");
    }

    public GenericContainer(@NonNull final String dockerImageName) {
        this.setDockerImageName(dockerImageName);
    }

    public GenericContainer(@NonNull final Future image) {
        this.image = image;
    }

    /**
     * Starts the container using docker, pulling an image if necessary.
     */
    public void start() {
        Profiler profiler = new Profiler("Container startup");
        profiler.setLogger(logger());

        try {
            profiler.start("Prepare container configuration and host configuration");
            configure();

            logger().debug("Starting container: {}", getDockerImageName());
            logger().debug("Trying to start container: {}", image.get());

            AtomicInteger attempt = new AtomicInteger(0);
            Unreliables.retryUntilSuccess(startupAttempts, () -> {
                logger().debug("Trying to start container: {} (attempt {}/{})", image.get(), attempt.incrementAndGet(), startupAttempts);
                tryStart(profiler.startNested("Container startup attempt"));
                return true;
            });

        } catch (Exception e) {
            throw new ContainerLaunchException("Container startup failed", e);
        } finally {
            profiler.stop().log();
        }
    }

    private void tryStart(Profiler profiler) {
        try {
            String dockerImageName = image.get();
            logger().debug("Starting container: {}", dockerImageName);

            logger().info("Creating container for image: {}", dockerImageName);
            profiler.start("Create container");
            CreateContainerCmd createCommand = dockerClient.createContainerCmd(dockerImageName);
            applyConfiguration(createCommand);

            containerId = createCommand.exec().getId();
            ResourceReaper.instance().registerContainerForCleanup(containerId, dockerImageName);

            logger().info("Starting container with ID: {}", containerId);
            profiler.start("Start container");
            dockerClient.startContainerCmd(containerId).exec();

            // For all registered output consumers, start following as close to container startup as possible
            this.logConsumers.forEach(this::followOutput);

            logger().info("Container {} is starting: {}", dockerImageName, containerId);

            // Tell subclasses that we're starting
            profiler.start("Inspecting container");
            containerInfo = dockerClient.inspectContainerCmd(containerId).exec();
            containerName = containerInfo.getName();
            profiler.start("Call containerIsStarting on subclasses");
            containerIsStarting(containerInfo);

            // Wait until the container is running (may not be fully started)
            profiler.start("Wait until container has started properly, or there's evidence it failed to start.");

            if (!this.startupCheckStrategy.waitUntilStartupSuccessful(dockerClient, containerId)) {
                // Bail out, don't wait for the port to start listening.
                // (Exception thrown here will be caught below and wrapped)
                throw new IllegalStateException("Container did not start correctly.");
            }

            profiler.start("Wait until container started properly");
            waitUntilContainerStarted();

            logger().info("Container {} started", dockerImageName);
            containerIsStarted(containerInfo);
        } catch (Exception e) {
            logger().error("Could not start container", e);

            // Log output if startup failed, either due to a container failure or exception (including timeout)
            logger().error("Container log output (if any) will follow:");
            FrameConsumerResultCallback resultCallback = new FrameConsumerResultCallback();
            resultCallback.addConsumer(STDOUT, new Slf4jLogConsumer(logger()));
            resultCallback.addConsumer(STDERR, new Slf4jLogConsumer(logger()));
            dockerClient.logContainerCmd(containerId).withStdOut(true).withStdErr(true).exec(resultCallback);

            // Try to ensure that container log output is shown before proceeding
            try {
                resultCallback.getCompletionLatch().await(1, TimeUnit.MINUTES);
            } catch (InterruptedException ignored) {
                // Cannot do anything at this point
            }

            throw new ContainerLaunchException("Could not create/start container", e);
        } finally {
            profiler.stop();
        }
    }

    /**
     * Stops the container.
     */
    public void stop() {

        if (containerId == null) {
            return;
        }

        String imageName;

        try {
            imageName = image.get();
        } catch (Exception e) {
            imageName = "";
        }

        ResourceReaper.instance().stopAndRemoveContainer(containerId, imageName);

        try {
            dockerClient.close();
        } catch (IOException e) {
            logger().debug("Failed to close docker client");
        }
    }

    /**
     * Provide a logger that references the docker image name.
     *
     * @return a logger that references the docker image name
     */
    protected Logger logger() {
        return DockerLoggerFactory.getLogger(this.getDockerImageName());
    }

    /**
     * Creates a directory on the local filesystem which will be mounted as a volume for the container.
     *
     * @param temporary is the volume directory temporary? If true, the directory will be deleted on JVM shutdown.
     * @return path to the volume directory
     */
    protected Path createVolumeDirectory(boolean temporary) {
        Path directory = new File(".tmp-volume-" + System.currentTimeMillis()).toPath();
        PathUtils.mkdirp(directory);

        if (temporary) Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            PathUtils.recursiveDeleteDir(directory);
        }));

        return directory;
    }

    protected void configure() {

    }

    @SuppressWarnings({"EmptyMethod", "UnusedParameters"})
    protected void containerIsStarting(InspectContainerResponse containerInfo) {
    }

    @SuppressWarnings({"EmptyMethod", "UnusedParameters"})
    protected void containerIsStarted(InspectContainerResponse containerInfo) {
    }

    /**
     * @return the port on which to check if the container is ready
     */
    protected Integer getLivenessCheckPort() {
        if (exposedPorts.size() > 0) {
            return getMappedPort(exposedPorts.get(0));
        } else if (portBindings.size() > 0) {
            return Integer.valueOf(PortBinding.parse(portBindings.get(0)).getBinding().getHostPortSpec());
        } else {
            return null;
        }
    }

    private void applyConfiguration(CreateContainerCmd createCommand) {

        // Set up exposed ports (where there are no host port bindings defined)
        ExposedPort[] portArray = exposedPorts.stream()
                .map(ExposedPort::new)
                .toArray(ExposedPort[]::new);

        createCommand.withExposedPorts(portArray);

        // Set up exposed ports that need host port bindings
        PortBinding[] portBindingArray = portBindings.stream()
                .map(PortBinding::parse)
                .toArray(PortBinding[]::new);

        createCommand.withPortBindings(portBindingArray);

        if (commandParts != null) {
            createCommand.withCmd(commandParts);
        }

        String[] envArray = env.stream()
                .toArray(String[]::new);
        createCommand.withEnv(envArray);

        Bind[] bindsArray = binds.stream()
                .toArray(Bind[]::new);
        createCommand.withBinds(bindsArray);

        Set allLinks = new HashSet<>();
        Set allLinkedContainerNetworks = new HashSet<>();
        for (Map.Entry linkEntries : linkedContainers.entrySet()) {

            String alias = linkEntries.getKey();
            LinkableContainer linkableContainer = linkEntries.getValue();

            Set links = dockerClient.listContainersCmd().exec().stream()
                    .filter(container -> container.getNames()[0].endsWith(linkableContainer.getContainerName()))
                    .map(container -> new Link(container.getNames()[0], alias))
                    .collect(Collectors.toSet());
            allLinks.addAll(links);

            boolean linkableContainerIsRunning = dockerClient.listContainersCmd().exec().stream()
                    .filter(container -> container.getNames()[0].endsWith(linkableContainer.getContainerName()))
                    .map(com.github.dockerjava.api.model.Container::getId)
                    .map(id -> dockerClient.inspectContainerCmd(id).exec())
                    .anyMatch(linkableContainerInspectResponse -> linkableContainerInspectResponse.getState().getRunning());

            if (!linkableContainerIsRunning) {
                throw new ContainerLaunchException("Aborting attempt to link to container " +
                        linkableContainer.getContainerName() +
                        " as it is not running");
            }

            Set linkedContainerNetworks = dockerClient.listContainersCmd().exec().stream()
                    .filter(container -> container.getNames()[0].endsWith(linkableContainer.getContainerName()))
                    .filter(container -> container.getNetworkSettings() != null &&
                            container.getNetworkSettings().getNetworks() != null)
                    .flatMap(container -> container.getNetworkSettings().getNetworks().keySet().stream())
                    .distinct()
                    .collect(Collectors.toSet());
            allLinkedContainerNetworks.addAll(linkedContainerNetworks);
        }

        createCommand.withLinks(allLinks.toArray(new Link[allLinks.size()]));

        allLinkedContainerNetworks.remove("bridge");
        if (allLinkedContainerNetworks.size() > 1) {
            logger().warn("Container needs to be on more than one custom network to link to other " +
                            "containers - this is not currently supported. Required networks are: {}",
                    allLinkedContainerNetworks);
        }

        Optional networkForLinks = allLinkedContainerNetworks.stream().findFirst();
        if (networkForLinks.isPresent()) {
            logger().debug("Associating container with network: {}", networkForLinks.get());
            createCommand.withNetworkMode(networkForLinks.get());
        }

        createCommand.withPublishAllPorts(true);

        String[] extraHostsArray = extraHosts.stream()
                .toArray(String[]::new);
        createCommand.withExtraHosts(extraHostsArray);

        if (networkMode != null) {
            createCommand.withNetworkMode(networkMode);
        }

        if (workingDirectory != null) {
            createCommand.withWorkingDir(workingDirectory);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SELF waitingFor(@NonNull WaitStrategy waitStrategy) {
        this.waitStrategy = waitStrategy;
        return self();
    }

    /**
     * The {@link WaitStrategy} to use to determine if the container is ready.
     * Defaults to {@link Wait#defaultWaitStrategy()}.
     *
     * @return the {@link WaitStrategy} to use
     */
    protected WaitStrategy getWaitStrategy() {
        return waitStrategy;
    }

    /**
     * Wait until the container has started. The default implementation simply
     * waits for a port to start listening; other implementations are available
     * as implementations of {@link WaitStrategy}
     *
     * @see #waitingFor(WaitStrategy)
     */
    protected void waitUntilContainerStarted() {
        getWaitStrategy().waitUntilReady(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setCommand(@NonNull String command) {
        this.commandParts = command.split(" ");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setCommand(@NonNull String... commandParts) {
        this.commandParts = commandParts;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addEnv(String key, String value) {
        env.add(key + "=" + value);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addFileSystemBind(String hostPath, String containerPath, BindMode mode) {
        if (SystemUtils.IS_OS_WINDOWS) {
            hostPath = PathUtils.createMinGWPath(hostPath);
        }

        binds.add(new Bind(hostPath, new Volume(containerPath), mode.accessMode));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SELF withFileSystemBind(String hostPath, String containerPath, BindMode mode) {
        addFileSystemBind(hostPath, containerPath, mode);
        return self();
    }

    @Override
    public void addLink(LinkableContainer otherContainer, String alias) {
        this.linkedContainers.put(alias, otherContainer);
    }

    @Override
    public void addExposedPort(Integer port) {
        exposedPorts.add(port);
    }

    @Override
    public void addExposedPorts(int... ports) {
        for (int port : ports) {
            exposedPorts.add(port);
        }
    }

    @Override
    protected void starting(Description description) {
        this.start();
    }

    @Override
    protected void finished(Description description) {
        this.stop();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SELF withExposedPorts(Integer... ports) {
        this.setExposedPorts(asList(ports));
        return self();

    }

    /**
     * Add a container port that should be bound to a fixed port on the docker host.
     * 

* Note that this method is protected scope to discourage use, as clashes or instability are more likely when * using fixed port mappings. If you need to use this method from a test, please use {@link FixedHostPortGenericContainer} * instead of GenericContainer. * * @param hostPort * @param containerPort */ protected void addFixedExposedPort(int hostPort, int containerPort) { portBindings.add(String.format("%d:%d", hostPort, containerPort)); } /** * {@inheritDoc} */ @Override public SELF withEnv(String key, String value) { this.addEnv(key, value); return self(); } /** * {@inheritDoc} */ @Override public SELF withEnv(Map env) { env.forEach(this::addEnv); return self(); } /** * {@inheritDoc} */ @Override public SELF withCommand(String cmd) { this.setCommand(cmd); return self(); } /** * {@inheritDoc} */ @Override public SELF withCommand(String... commandParts) { this.setCommand(commandParts); return self(); } /** * {@inheritDoc} */ @Override public SELF withExtraHost(String hostname, String ipAddress) { this.extraHosts.add(String.format("%s:%s", hostname, ipAddress)); return self(); } @Override public SELF withNetworkMode(String networkMode) { this.networkMode = networkMode; return self(); } /** * {@inheritDoc} */ @Override public SELF withClasspathResourceMapping(String resourcePath, String containerPath, BindMode mode) { URL resource = GenericContainer.class.getClassLoader().getResource(resourcePath); if (resource == null) { throw new IllegalArgumentException("Could not find classpath resource at provided path: " + resourcePath); } String resourceFilePath = resource.getFile(); this.addFileSystemBind(resourceFilePath, containerPath, mode); return self(); } /** * {@inheritDoc} */ @Override public SELF withStartupTimeout(Duration startupTimeout) { getWaitStrategy().withStartupTimeout(startupTimeout); return self(); } /** * {@inheritDoc} */ @Override public String getContainerIpAddress() { return DockerClientFactory.instance().dockerHostIpAddress(); } /** * {@inheritDoc} */ @Override public SELF withMinimumRunningDuration(Duration minimumRunningDuration) { this.startupCheckStrategy = new MinimumDurationRunningStartupCheckStrategy(minimumRunningDuration); return self(); } /** * {@inheritDoc} */ @Override public SELF withStartupCheckStrategy(StartupCheckStrategy strategy) { this.startupCheckStrategy = strategy; return self(); } /** * {@inheritDoc} */ @Override public SELF withWorkingDirectory(String workDir) { this.setWorkingDirectory(workDir); return self(); } /** * Get the IP address that this container may be reached on (may not be the local machine). * * @return an IP address * @deprecated please use getContainerIpAddress() instead */ @Deprecated public String getIpAddress() { return getContainerIpAddress(); } /** * {@inheritDoc} */ @Override public Boolean isRunning() { try { return dockerClient.inspectContainerCmd(containerId).exec().getState().getRunning(); } catch (DockerException e) { return false; } } /** * {@inheritDoc} */ @Override public Integer getMappedPort(final int originalPort) { Preconditions.checkState(containerId != null, "Mapped port can only be obtained after the container is started"); Ports.Binding[] binding = new Ports.Binding[0]; if (containerInfo != null) { binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort)); } if (binding != null && binding.length > 0 && binding[0] != null) { return Integer.valueOf(binding[0].getHostPortSpec()); } else { throw new IllegalArgumentException("Requested port (" + originalPort + ") is not mapped"); } } /** * {@inheritDoc} */ @Override public void setDockerImageName(@NonNull String dockerImageName) { this.image = new RemoteDockerImage(dockerImageName); // Mimic old behavior where we resolve image once it's set getDockerImageName(); } /** * {@inheritDoc} */ @Override @NonNull public String getDockerImageName() { try { return image.get(); } catch (Exception e) { throw new ContainerFetchException("Can't get Docker image name from " + image, e); } } /** * {@inheritDoc} */ @Override public String getTestHostIpAddress() { if (DockerMachineClient.instance().isInstalled()) { try { Optional defaultMachine = DockerMachineClient.instance().getDefaultMachine(); if (!defaultMachine.isPresent()) { throw new IllegalStateException("Could not find a default docker-machine instance"); } String sshConnectionString = runShellCommand("docker-machine", "ssh", defaultMachine.get(), "echo $SSH_CONNECTION").trim(); if (Strings.isNullOrEmpty(sshConnectionString)) { throw new IllegalStateException("Could not obtain SSH_CONNECTION environment variable for docker machine " + defaultMachine.get()); } String[] sshConnectionParts = sshConnectionString.split("\\s"); if (sshConnectionParts.length != 4) { throw new IllegalStateException("Unexpected pattern for SSH_CONNECTION for docker machine - expected 'IP PORT IP PORT' pattern but found '" + sshConnectionString + "'"); } return sshConnectionParts[0]; } catch (Exception e) { throw new RuntimeException(e); } } else { throw new UnsupportedOperationException("getTestHostIpAddress() is only implemented for docker-machine right now"); } } /** * {@inheritDoc} */ @Override public void followOutput(Consumer consumer) { this.followOutput(consumer, OutputFrame.OutputType.STDOUT, OutputFrame.OutputType.STDERR); } /** * {@inheritDoc} */ @Override public void followOutput(Consumer consumer, OutputFrame.OutputType... types) { LogContainerCmd cmd = dockerClient.logContainerCmd(containerId) .withFollowStream(true); FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); for (OutputFrame.OutputType type : types) { callback.addConsumer(type, consumer); if (type == STDOUT) cmd.withStdOut(true); if (type == STDERR) cmd.withStdErr(true); } cmd.exec(callback); } /** * {@inheritDoc} */ @Override public SELF withLogConsumer(Consumer consumer) { this.logConsumers.add(consumer); return self(); } @Override public synchronized Info fetchDockerDaemonInfo() throws IOException { if (this.dockerDaemonInfo == null) { this.dockerDaemonInfo = this.dockerClient.infoCmd().exec(); } return this.dockerDaemonInfo; } /** * {@inheritDoc} */ @Override public ExecResult execInContainer(String... command) throws UnsupportedOperationException, IOException, InterruptedException { return execInContainer(UTF8, command); } /** * {@inheritDoc} */ @Override public ExecResult execInContainer(Charset outputCharset, String... command) throws UnsupportedOperationException, IOException, InterruptedException { if (!TestEnvironment.dockerExecutionDriverSupportsExec()) { // at time of writing, this is the expected result in CircleCI. throw new UnsupportedOperationException( "Your docker daemon is running the \"lxc\" driver, which doesn't support \"docker exec\"."); } this.dockerClient .execCreateCmd(this.containerId) .withCmd(command); logger().info("Running \"exec\" command: " + String.join(" ", command)); final ExecCreateCmdResponse execCreateCmdResponse = dockerClient.execCreateCmd(this.containerId) .withAttachStdout(true).withAttachStderr(true).withCmd(command).exec(); final ToStringConsumer stdoutConsumer = new ToStringConsumer(); final ToStringConsumer stderrConsumer = new ToStringConsumer(); FrameConsumerResultCallback callback = new FrameConsumerResultCallback(); callback.addConsumer(OutputFrame.OutputType.STDOUT, stdoutConsumer); callback.addConsumer(OutputFrame.OutputType.STDERR, stderrConsumer); dockerClient.execStartCmd(execCreateCmdResponse.getId()).exec(callback).awaitCompletion(); final ExecResult result = new ExecResult( stdoutConsumer.toString(outputCharset), stderrConsumer.toString(outputCharset)); logger().trace("stdout: " + result.getStdout()); logger().trace("stderr: " + result.getStderr()); return result; } /** * Allow container startup to be attempted more than once if an error occurs. To be if containers are * 'flaky' but this flakiness is not something that should affect test outcomes. * * @param attempts number of attempts */ public void withStartupAttempts(int attempts) { this.startupAttempts = attempts; } /** * Convenience class with access to non-public members of GenericContainer. */ public static abstract class AbstractWaitStrategy implements WaitStrategy { protected GenericContainer container; @NonNull protected Duration startupTimeout = Duration.ofSeconds(60); /** * Wait until the container has started. * * @param container the container for which to wait */ @Override public void waitUntilReady(GenericContainer container) { this.container = container; waitUntilReady(); } /** * Wait until {@link #container} has started. */ protected abstract void waitUntilReady(); /** * Set the duration of waiting time until container treated as started. * * @param startupTimeout timeout * @return this * @see WaitStrategy#waitUntilReady(GenericContainer) */ public WaitStrategy withStartupTimeout(Duration startupTimeout) { this.startupTimeout = startupTimeout; return this; } /** * @return the container's logger */ protected Logger logger() { return container.logger(); } /** * @return the port on which to check if the container is ready */ protected Integer getLivenessCheckPort() { return container.getLivenessCheckPort(); } /** * @return the rate limiter to use */ protected RateLimiter getRateLimiter() { return DOCKER_CLIENT_RATE_LIMITER; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy