diff --git a/CHANGELOG.md b/CHANGELOG.md index a95fa857274..27aa66bb4f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file. ### Fixed ### Changed +- Added support for Docker networks (#372) ## [1.3.1] - 2017-06-22 ### Fixed diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java index 35abe058168..2f2d9fa0917 100644 --- a/core/src/main/java/org/testcontainers/containers/Container.java +++ b/core/src/main/java/org/testcontainers/containers/Container.java @@ -209,6 +209,24 @@ default void addFileSystemBind(final String hostPath, final String containerPath */ SELF withNetworkMode(String networkMode); + /** + * Set the network for this container, similar to the --network <name> + * option on the docker CLI. + * + * @param network the instance of {@link Network} + * @return this + */ + SELF withNetwork(Network network); + + /** + * Set the network aliases for this container, similar to the --network-alias <my-service> + * option on the docker CLI. + * + * @param aliases the list of aliases + * @return this + */ + SELF withNetworkAliases(String... aliases); + /** * Map a resource (file or directory) on the classpath to a path inside the container. * This will only work if you are running your tests outside a Docker container. diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 09494c78610..6b055860256 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -75,6 +75,12 @@ public class GenericContainer> @NonNull private String networkMode; + @NonNull + private Network network; + + @NonNull + private List networkAliases = new ArrayList<>(); + @NonNull private Future image; @@ -412,7 +418,10 @@ private void applyConfiguration(CreateContainerCmd createCommand) { .toArray(String[]::new); createCommand.withExtraHosts(extraHostsArray); - if (networkMode != null) { + if (network != null) { + createCommand.withNetworkMode(network.getId()); + createCommand.withAliases(this.networkAliases); + } else if (networkMode != null) { createCommand.withNetworkMode(networkMode); } @@ -615,6 +624,18 @@ public SELF withNetworkMode(String networkMode) { return self(); } + @Override + public SELF withNetwork(Network network) { + this.network = network; + return self(); + } + + @Override + public SELF withNetworkAliases(String... aliases) { + Collections.addAll(this.networkAliases, aliases); + return self(); + } + /** * {@inheritDoc} */ diff --git a/core/src/main/java/org/testcontainers/containers/Network.java b/core/src/main/java/org/testcontainers/containers/Network.java new file mode 100644 index 00000000000..4d1da15cb69 --- /dev/null +++ b/core/src/main/java/org/testcontainers/containers/Network.java @@ -0,0 +1,78 @@ +package org.testcontainers.containers; + +import com.github.dockerjava.api.command.CreateNetworkCmd; +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; +import org.junit.rules.ExternalResource; +import org.junit.rules.TestRule; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.utility.ResourceReaper; + +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; + +public interface Network extends AutoCloseable, TestRule { + + String getId(); + + @Override + default void close() { + ResourceReaper.instance().removeNetworks(getId()); + } + + static Network newNetwork() { + return builder().build(); + } + + static NetworkImpl.NetworkImplBuilder builder() { + return NetworkImpl.builder(); + } + + @Builder + @Getter + class NetworkImpl extends ExternalResource implements Network { + + private final String name = UUID.randomUUID().toString(); + + private Boolean enableIpv6; + + private String driver; + + @Singular + private Set> createNetworkCmdModifiers = new LinkedHashSet<>(); + + @Getter(lazy = true) + private final String id = create(); + + private String create() { + ResourceReaper.instance().registerNetworkForCleanup(name); + + CreateNetworkCmd createNetworkCmd = DockerClientFactory.instance().client().createNetworkCmd(); + + createNetworkCmd.withName(name); + createNetworkCmd.withCheckDuplicate(true); + + if (enableIpv6 != null) { + createNetworkCmd.withEnableIpv6(enableIpv6); + } + + if (driver != null) { + createNetworkCmd.withDriver(driver); + } + + for (Consumer consumer : createNetworkCmdModifiers) { + consumer.accept(createNetworkCmd); + } + + return createNetworkCmd.exec().getId(); + } + + @Override + protected void after() { + close(); + } + } +} diff --git a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java index e9f10b76b2c..a9ed8f1ba45 100644 --- a/core/src/main/java/org/testcontainers/utility/ResourceReaper.java +++ b/core/src/main/java/org/testcontainers/utility/ResourceReaper.java @@ -5,15 +5,12 @@ 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.*; import java.util.concurrent.ConcurrentHashMap; /** @@ -25,7 +22,7 @@ public final class ResourceReaper { private static ResourceReaper instance; private final DockerClient dockerClient; private Map registeredContainers = new ConcurrentHashMap<>(); - private List registeredNetworks = new ArrayList<>(); + private Set registeredNetworks = Collections.newSetFromMap(new ConcurrentHashMap<>()); private ResourceReaper() { dockerClient = DockerClientFactory.instance().client(); @@ -145,22 +142,34 @@ public void removeNetworks(String identifier) { } private void removeNetwork(String networkName) { - List 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; - } + try { + // First try to remove by name + dockerClient.removeNetworkCmd(networkName).exec(); + } catch (Exception e) { + LOGGER.trace("Error encountered removing network by name ({}) - it may not have been removed", networkName); + } - for (Network network : networks) { + List 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()); + // Then try to list all networks with the same name + networks = dockerClient.listNetworksCmd().withNameFilter(networkName).exec(); + } catch (Exception 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 (Exception e) { + LOGGER.trace("Error encountered removing network (name: {}) - it may not have been removed", network.getName()); + } } + } finally { + registeredNetworks.remove(networkName); } } } diff --git a/core/src/test/java/org/testcontainers/containers/NetworkTest.java b/core/src/test/java/org/testcontainers/containers/NetworkTest.java new file mode 100644 index 00000000000..70b55c09928 --- /dev/null +++ b/core/src/test/java/org/testcontainers/containers/NetworkTest.java @@ -0,0 +1,94 @@ +package org.testcontainers.containers; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.testcontainers.DockerClientFactory; + +import static org.rnorth.visibleassertions.VisibleAssertions.*; +import static org.testcontainers.containers.Network.newNetwork; + +@RunWith(Enclosed.class) +public class NetworkTest { + + public static class WithRules { + + @Rule + public Network network = newNetwork(); + + @Rule + public GenericContainer foo = new GenericContainer() + .withNetwork(network) + .withNetworkAliases("foo") + .withCommand("/bin/sh", "-c", "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"); + + @Rule + public GenericContainer bar = new GenericContainer() + .withNetwork(network) + .withCommand("top"); + + @Test + public void testNetworkSupport() throws Exception { + String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout(); + assertEquals("received response", "yay", response); + } + } + + public static class WithoutRules { + + @Test + public void testNetworkSupport() throws Exception { + try ( + Network network = newNetwork(); + + GenericContainer foo = new GenericContainer() + .withNetwork(network) + .withNetworkAliases("foo") + .withCommand("/bin/sh", "-c", "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"); + + GenericContainer bar = new GenericContainer() + .withNetwork(network) + .withCommand("top") + ) { + foo.start(); + bar.start(); + + String response = bar.execInContainer("wget", "-O", "-", "http://foo:8080").getStdout(); + assertEquals("received response", "yay", response); + } + } + + @Test + public void testBuilder() throws Exception { + try ( + Network network = Network.builder() + .driver("macvlan") + .build(); + ) { + String id = network.getId(); + assertEquals( + "Flag is set", + "macvlan", + DockerClientFactory.instance().client().inspectNetworkCmd().withNetworkId(id).exec().getDriver() + ); + } + } + + @Test + public void testModifiers() throws Exception { + try ( + Network network = Network.builder() + .createNetworkCmdModifier(cmd -> cmd.withDriver("macvlan")) + .build(); + ) { + String id = network.getId(); + assertEquals( + "Flag is set", + "macvlan", + DockerClientFactory.instance().client().inspectNetworkCmd().withNetworkId(id).exec().getDriver() + ); + } + } + } +} diff --git a/modules/selenium/src/test/java/org/testcontainers/junit/LinkedContainerTest.java b/modules/selenium/src/test/java/org/testcontainers/junit/LinkedContainerTest.java index b135e92a754..862a985ad8f 100644 --- a/modules/selenium/src/test/java/org/testcontainers/junit/LinkedContainerTest.java +++ b/modules/selenium/src/test/java/org/testcontainers/junit/LinkedContainerTest.java @@ -8,6 +8,7 @@ import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import org.testcontainers.containers.BrowserWebDriverContainer; +import org.testcontainers.containers.Network; import org.testcontainers.containers.NginxContainer; import java.io.*; @@ -23,13 +24,18 @@ public class LinkedContainerTest { private static File contentFolder = new File(System.getProperty("user.home") + "/.tmp-test-container"); @Rule - public NginxContainer nginx = new NginxContainer() + public Network network = Network.newNetwork(); + + @Rule + public NginxContainer nginx = new NginxContainer<>() + .withNetwork(network) + .withNetworkAliases("nginx") .withCustomContent(contentFolder.toString()); @Rule - public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer() - .withDesiredCapabilities(DesiredCapabilities.chrome()) - .withLinkToContainer(nginx, "nginx"); + public BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() + .withNetwork(network) + .withDesiredCapabilities(DesiredCapabilities.chrome()); @BeforeClass public static void setupContent() throws FileNotFoundException {