Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for WaitStrategy on DockerComposeContainer - alternative approach #600

Merged
merged 10 commits into from
Mar 19, 2018
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ All notable changes to this project will be documented in this file.
- Abstracted and changed database init script functionality to support use of SQL-like scripts with non-JDBC connections. ([\#551](https://github.com/testcontainers/testcontainers-java/pull/551))
- Added `JdbcDatabaseContainer(Future)` constructor. ([\#543](https://github.com/testcontainers/testcontainers-java/issues/543))
- Mark DockerMachineClientProviderStrategy as not persistable ([\#593](https://github.com/testcontainers/testcontainers-java/pull/593))
- Added `waitingFor(String serviceName, WaitStrategy waitStrategy)` and overloaded `withExposedService()` methods to `DockerComposeContainer` to allow user to define `WaitStrategy` for compose containers. ([\#174](https://github.com/testcontainers/testcontainers-java/issues/174) and [\#515](https://github.com/testcontainers/testcontainers-java/issues/515))
- Deprecated `WaitStrategy` and implementations in favour of classes with same names in `org.testcontainers.containers.strategy`
- Added `ContainerState` interface representing the state of a started container
- Added `WaitStrategyTarget` interface which is the target of the new `WaitStrategy`

## [1.6.0] - 2018-01-28

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.model.Container;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
* Class to provide a wait strategy target for services started through docker-compose
*/
@EqualsAndHashCode
class ComposeServiceWaitStrategyTarget implements WaitStrategyTarget {

private final Container container;
private final GenericContainer proxyContainer;
@NonNull
private Map<Integer, Integer> mappedPorts;
@Getter(lazy=true)
private final InspectContainerResponse containerInfo = DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec();

ComposeServiceWaitStrategyTarget(Container container, GenericContainer proxyContainer,
@NonNull Map<Integer, Integer> mappedPorts) {
this.container = container;
this.proxyContainer = proxyContainer;
this.mappedPorts = new HashMap<>(mappedPorts);
}

/**
* {@inheritDoc}
*/
@Override
public List<Integer> getExposedPorts() {
return new ArrayList<>(this.mappedPorts.keySet());
}

/**
* {@inheritDoc}
*/
@Override
public Integer getMappedPort(int originalPort) {
return this.proxyContainer.getMappedPort(this.mappedPorts.get(originalPort));
}

/**
* {@inheritDoc}
*/
@Override
public String getContainerIpAddress() {
return proxyContainer.getContainerIpAddress();
}

/**
* {@inheritDoc}
*/
@Override
public String getContainerId() {
return this.container.getId();
}
}
78 changes: 14 additions & 64 deletions core/src/main/java/org/testcontainers/containers/Container.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.model.Bind;
import com.github.dockerjava.api.model.Info;
import lombok.NonNull;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.output.OutputFrame;
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.containers.wait.strategy.WaitStrategy;
import org.testcontainers.utility.LogUtils;
import org.testcontainers.utility.MountableFile;

import java.io.IOException;
Expand All @@ -22,7 +22,7 @@
import java.util.function.Consumer;
import java.util.function.Function;

public interface Container<SELF extends Container<SELF>> extends LinkableContainer {
public interface Container<SELF extends Container<SELF>> extends LinkableContainer, ContainerState {

/**
* @return a reference to this container instance, cast to the expected generic type.
Expand Down Expand Up @@ -131,7 +131,7 @@ default void addFileSystemBind(final String hostPath, final String containerPath
/**
* Specify the {@link WaitStrategy} to use to determine if the container is ready.
*
* @see Wait#defaultWaitStrategy()
* @see org.testcontainers.containers.wait.strategy.Wait#defaultWaitStrategy()
* @param waitStrategy the WaitStrategy to use
* @return this
*/
Expand Down Expand Up @@ -283,7 +283,7 @@ default SELF withClasspathResourceMapping(final String resourcePath, final Strin

/**
* Set the duration of waiting time until container treated as started.
* @see WaitStrategy#waitUntilReady(GenericContainer)
* @see WaitStrategy#waitUntilReady(org.testcontainers.containers.wait.strategy.WaitStrategyTarget)
*
* @param startupTimeout timeout
* @return this
Expand All @@ -297,13 +297,6 @@ default SELF withClasspathResourceMapping(final String resourcePath, final Strin
*/
SELF withPrivilegedMode(boolean mode);

/**
* Get the IP address that this container may be reached on (may not be the local machine).
*
* @return an IP address
*/
String getContainerIpAddress();

/**
* Only consider a container to have successfully started if it has been running for this duration. The default
* value is null; if that's the value, ignore this check.
Expand All @@ -327,33 +320,6 @@ default SELF withClasspathResourceMapping(final String resourcePath, final Strin
*/
SELF withWorkingDirectory(String workDir);

/**
* @return is the container currently running?
*/
Boolean isRunning();

/**
* Get the actual mapped port for a first port exposed by the container.
*
* @return the port that the exposed port is mapped to
* @throws IllegalStateException if there are no exposed ports
*/
default Integer getFirstMappedPort() {
return getExposedPorts()
.stream()
.findFirst()
.map(this::getMappedPort)
.orElseThrow(() -> new IllegalStateException("Container doesn't expose any ports"));
}

/**
* Get the actual mapped port for a given port exposed by the container.
*
* @param originalPort the original TCP port that is exposed
* @return the port that the exposed port is mapped to, or null if it is not exposed
*/
Integer getMappedPort(int originalPort);

/**
* <b>Resolve</b> Docker image and set it.
*
Expand Down Expand Up @@ -386,7 +352,9 @@ default Integer getFirstMappedPort() {
*
* @param consumer consumer that the frames should be sent to
*/
void followOutput(Consumer<OutputFrame> consumer);
default void followOutput(Consumer<OutputFrame> consumer) {
LogUtils.followOutput(DockerClientFactory.instance().client(), getContainerId(), consumer);
}

/**
* Follow container output, sending each frame (usually, line) to a consumer. This method allows Stdout and/or stderr
Expand All @@ -395,7 +363,9 @@ default Integer getFirstMappedPort() {
* @param consumer consumer that the frames should be sent to
* @param types types that should be followed (one or both of STDOUT, STDERR)
*/
void followOutput(Consumer<OutputFrame> consumer, OutputFrame.OutputType... types);
default void followOutput(Consumer<OutputFrame> consumer, OutputFrame.OutputType... types) {
LogUtils.followOutput(DockerClientFactory.instance().client(), getContainerId(), consumer, types);
}


/**
Expand All @@ -419,22 +389,15 @@ default Integer getFirstMappedPort() {
* Run a command inside a running container, as though using "docker exec", and interpreting
* the output as UTF8.
* <p>
* @see #execInContainer(Charset, String...)
* @see ExecInContainerPattern#execInContainer(com.github.dockerjava.api.command.InspectContainerResponse, String...)
*/
ExecResult execInContainer(String... command)
throws UnsupportedOperationException, IOException, InterruptedException;

/**
* Run a command inside a running container, as though using "docker exec".
* <p>
* This functionality is not available on a docker daemon running the older "lxc" execution driver. At
* the time of writing, CircleCI was using this driver.
* @param outputCharset the character set used to interpret the output.
* @param command the parts of the command to run
* @return the result of execution
* @throws IOException if there's an issue communicating with Docker
* @throws InterruptedException if the thread waiting for the response is interrupted
* @throws UnsupportedOperationException if the docker daemon you're connecting to doesn't support "exec".
* @see ExecInContainerPattern#execInContainer(com.github.dockerjava.api.command.InspectContainerResponse, Charset, String...)
*/
ExecResult execInContainer(Charset outputCharset, String... command)
throws UnsupportedOperationException, IOException, InterruptedException;
Expand All @@ -460,8 +423,6 @@ ExecResult execInContainer(Charset outputCharset, String... command)
*/
void copyFileFromContainer(String containerPath, String destinationPath) throws IOException, InterruptedException;

List<Integer> getExposedPorts();

List<String> getPortBindings();

List<String> getExtraHosts();
Expand Down Expand Up @@ -496,17 +457,6 @@ ExecResult execInContainer(Charset outputCharset, String... command)
@Deprecated
Info getDockerDaemonInfo();

String getContainerId();

String getContainerName();

/**
*
* @deprecated please use {@code org.testcontainers.DockerClientFactory.instance().client().inspectContainerCmd(container.getContainerId()).exec()}
*/
@Deprecated
InspectContainerResponse getContainerInfo();

void setExposedPorts(List<Integer> exposedPorts);

void setPortBindings(List<String> portBindings);
Expand Down
114 changes: 114 additions & 0 deletions core/src/main/java/org/testcontainers/containers/ContainerState.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package org.testcontainers.containers;

import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.exception.DockerException;
import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.PortBinding;
import com.github.dockerjava.api.model.Ports;
import com.google.common.base.Preconditions;
import org.testcontainers.DockerClientFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public interface ContainerState {

/**
* Get the IP address that this container may be reached on (may not be the local machine).
*
* @return an IP address
*/
default String getContainerIpAddress() {
return DockerClientFactory.instance().dockerHostIpAddress();
}

/**
* @return is the container currently running?
*/
default Boolean isRunning() {
try {
return getContainerId() != null && DockerClientFactory.instance().client().inspectContainerCmd(getContainerId()).exec().getState().getRunning();
} catch (DockerException e) {
return false;
}
}

/**
* Get the actual mapped port for a first port exposed by the container.
*
* @return the port that the exposed port is mapped to
* @throws IllegalStateException if there are no exposed ports
*/
default Integer getFirstMappedPort() {
return getExposedPorts()
.stream()
.findFirst()
.map(this::getMappedPort)
.orElseThrow(() -> new IllegalStateException("Container doesn't expose any ports"));
}

/**
* Get the actual mapped port for a given port exposed by the container.
*
* @param originalPort the original TCP port that is exposed
* @return the port that the exposed port is mapped to, or null if it is not exposed
*/
default Integer getMappedPort(int originalPort) {
Preconditions.checkState(this.getContainerId() != null, "Mapped port can only be obtained after the container is started");

Ports.Binding[] binding = new Ports.Binding[0];
final InspectContainerResponse containerInfo = this.getContainerInfo();
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");
}
}

/**
* @return the exposed ports
*/
List<Integer> getExposedPorts();

/**
* @return the port bindings
*/
default List<String> getPortBindings() {
List<String> portBindings = new ArrayList<>();
final Ports hostPortBindings = this.getContainerInfo().getHostConfig().getPortBindings();
for (Map.Entry<ExposedPort, Ports.Binding[]> binding : hostPortBindings.getBindings().entrySet()) {
for (Ports.Binding portBinding : binding.getValue()) {
portBindings.add(String.format("%s:%s", portBinding.toString(), binding.getKey()));
}
}
return portBindings;
}

/**
* @return the bound port numbers
*/
default List<Integer> getBoundPortNumbers() {
return getPortBindings().stream()
.map(PortBinding::parse)
.map(PortBinding::getBinding)
.map(Ports.Binding::getHostPortSpec)
.map(Integer::valueOf)
.collect(Collectors.toList());
}

/**
* @return the id of the container
*/
String getContainerId();

/**
* @return the container info
*/
InspectContainerResponse getContainerInfo();
}
Loading