testcontainers-java
                                
                                 testcontainers-java copied to clipboard
                                
                                    testcontainers-java copied to clipboard
                            
                            
                            
                        Make (containers with) networks reusable
I am loving the reusable containers. They can speed up development so much when repeating individual tests locally or when tests are run in several JVMs successively in the CI.
Currently, I am working with containers that are additionally connected through a network. These cannot be reused since the name of the network will be a random UUID each time. The hash of the containers to be reused is based on the create container command. This includes the network. Hence the hash chances each time and a reusable container cannot be found if a network is used.
I guess an easy solution would be to make the name of the network customizable. I can imagine weird effects when a user doesn't know the exact consequences, however. A more robust solution would be copy the reuse mechanism from containers using a name prefix for the network to make sure that one can have multiple networks which are created with identical settings.
What do you think? I would like to work on this, if it matches the direction of the project.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. If you believe this is a mistake, please reply to this comment to keep it open. If there isn't one already, a PR to fix or at least reproduce the problem in a test case will always help us get back on track to tackle this.
Thanks stale bot. There is already a PR which at least prevents erroneously reusing containers with Networks.
I was hoping for some discussion on the matter itself :)
Allowing reuse of connected containers would be really great. As a workaround I now use the Docker client to check if the network already exists and then use Java reflection to modify the NetworkImpl object. https://github.com/testcontainers/testcontainers-java/pull/3084#issuecomment-776607920 mentions that "there are options to preserve the network IDs, which makes the reuse work". Can someone elaborate on those options? I assume they are better than my hack.
@knutwannheden there is no need to use Reflection btw, just implement Network yourself.
We plan to work on improving the reusable mode eventually, but it requires some major changes, as compared to the current "alpha" version.
Thanks for the tip. I guess I have grown too used to always declaring everything final 😄
I was a bit surprised that the containers declared for reuse always get stopped through the JUnit 5 lifecycle hooks of the @Container annotation. Is there also some workaround for that?
I was a bit surprised that the containers declared for reuse always get stopped through the JUnit 5 lifecycle hooks of the
@Containerannotation. Is there also some workaround for that?
Would it possibly make sense with something like @Container(reuse = true) to indicate that a container should not be stopped at the end of the test scope and thus be available for reuse by another matching @Container annotation in another test?
@knutwannheden the problem with this approach is that the behaviour will differ between reuse and regular mode (as it is per-environment)
It is recommended to use the manual lifecycle control: https://www.testcontainers.org/test_framework_integration/manual_lifecycle_control/
You should really think about the reusable containers as a next step in the "Container per test (@Rule) -> per class (@ClassRule) -> per JVM (singleton)" chain
Makes sense. Thanks for the explanation.
Hi @knutwannheden, if you could share your hack or at least what you are planning to do, it would be great !! I am also really interested by the ability to reuse containers that share the same network.
@loic-seguin What I did was to use Java reflection to create a Network object and then set its name, id, and initialized fields. I set the name field, since I also use the name to find any corresponding network to reuse, if it already exists. But that could of course also be done using the id or some label:
    private Network createNetwork(String networkName) {
        Network network = Network.newNetwork();
        try {
            Field nameField = Network.NetworkImpl.class.getDeclaredField("name");
            nameField.setAccessible(true);
            nameField.set(network, networkName);
            List<com.github.dockerjava.api.model.Network> networks =
                    DockerClientFactory.instance().client().listNetworksCmd().withNameFilter(networkName).exec();
            if (!networks.isEmpty()) {
                Field idField = Network.NetworkImpl.class.getDeclaredField("id");
                idField.setAccessible(true);
                idField.set(network, networks.get(0).getId());
                Field initializedField = Network.NetworkImpl.class.getDeclaredField("initialized");
                initializedField.setAccessible(true);
                ((AtomicBoolean) initializedField.get(network)).set(true);
            }
            return network;
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
Also, as discussed, creating a custom Network subclass would probably be more appropriate.
@knutwannheden : I have subclass Network instead and did the job you did but it is not enough apparently. I had a look in the code and when there is a network inside of the command, the code is setting Random Networks aliases in the method applyConfiguration that is called in the method 'tryStart'.... Then the hash that is calculated is always different either you are reusing the same network or not.
Then a new container is recreated on the same network of course but this is not why I wanted. I wanted that no new container is recreated even if we are using withNetwork(myNetwork).withReuse(true).
Maybe I misunderstood the point.
+1 For allowing a reuse of connected containers
Please refrain from spamming issues with "+1" comments. this won't expedite the implementation and it just adds noise for the maintainers.
I had a look in the code and when there is a network inside of the command, the code is setting Random Networks aliases in the method applyConfiguration that is called in the method 'tryStart'.... Then the hash that is calculated is always different either you are reusing the same network or not.
@loic-seguin I believe this is the code you are referring to?
public class GenericContainer<SELF extends GenericContainer<SELF>>
...
    private List<String> networkAliases = new ArrayList<>(Arrays.asList("tc-" + Base58.randomString(8)));
Could this have been used to avoid what you're seeing?
    // For the container to be reused
    container.withCreateContainerCmdModifier(cmd -> cmd.withAliases(alias));
since:
    public CreateContainerCmd withAliases(List<String> aliases) {
        Preconditions.checkNotNull(aliases, "aliases was not specified");
        this.aliases = aliases; // Note that this is not an append, unlike `container.withAliases`
        return this;
    }
For me, the following works (use the same name everytime, obviously):
public static Network createReusableNetwork(String name) {
	String id = DockerClientFactory.instance().client().listNetworksCmd().exec().stream()
		.filter(network -> network.getName().equals(name)
				&& network.getLabels().equals(DockerClientFactory.DEFAULT_LABELS))
		.map(com.github.dockerjava.api.model.Network::getId)
		.findFirst()
		.orElseGet(() -> DockerClientFactory.instance().client().createNetworkCmd()
			.withName(name)
			.withCheckDuplicate(true)
			.withLabels(DockerClientFactory.DEFAULT_LABELS)
			.exec().getId());
	return new Network() {
		@Override
		public Statement apply(Statement base, Description description) {
			return base;
		}
		@Override
		public String getId() {
			return id;
		}
		@Override
		public void close() {
			// never close
		}
	};
}
@Tillerino Thanks for sharing your solution.
I'll add my 2 cents: I had to upgrade testcontainers dependency to 1.18.2 as in the version I used previously (1.16.3) the DockerClientFactory.DEFAULT_LABELS included a randomly generated UUID which made the filter "network.getLabels().equals(DockerClientFactory.DEFAULT_LABELS)" fail