testcontainers-java icon indicating copy to clipboard operation
testcontainers-java copied to clipboard

Make (containers with) networks reusable

Open Tillerino opened this issue 5 years ago • 14 comments

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.

Tillerino avatar Aug 07 '20 09:08 Tillerino

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.

stale[bot] avatar Nov 07 '20 21:11 stale[bot]

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 :)

Tillerino avatar Nov 08 '20 13:11 Tillerino

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 avatar Apr 16 '21 07:04 knutwannheden

@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.

bsideup avatar Apr 16 '21 08:04 bsideup

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?

knutwannheden avatar Apr 16 '21 09:04 knutwannheden

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?

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 avatar Apr 29 '21 07:04 knutwannheden

@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

bsideup avatar Apr 29 '21 08:04 bsideup

Makes sense. Thanks for the explanation.

knutwannheden avatar Apr 29 '21 08:04 knutwannheden

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 avatar May 02 '21 09:05 loic-seguin

@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 avatar May 03 '21 08:05 knutwannheden

@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.

loic-seguin avatar May 03 '21 10:05 loic-seguin

+1 For allowing a reuse of connected containers

yoniw avatar Nov 23 '21 08:11 yoniw

Please refrain from spamming issues with "+1" comments. this won't expedite the implementation and it just adds noise for the maintainers.

kiview avatar Nov 23 '21 08:11 kiview

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;
    }

btiernay avatar Dec 11 '22 18:12 btiernay

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 avatar May 18 '23 13:05 Tillerino

@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

ngmip avatar Jun 01 '23 07:06 ngmip