software.xdev.tci.network.LazyNetwork Maven / Gradle / Ivy
The newest version!
/*
* Copyright © 2024 XDEV Software (https://xdev.software)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package software.xdev.tci.network;
import java.time.Duration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Function;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.Network;
import org.testcontainers.utility.ResourceReaper;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateNetworkCmd;
/**
* A better implementation of {@link Network} in relation to {@link org.testcontainers.containers.Network.NetworkImpl}.
*
* Improvements:
*
* - Allows creation of the network in the background
* - Doesn't create the network inside {@link NetworkImpl#getId()}
* - Doesn't check for duplicate network names when using a random {@link UUID} as name (see
* {@link #checkDuplicate below})
* - Tries to delete the network when it's closed
*
*
*/
public class LazyNetwork implements Network
{
private static final Logger LOG = LoggerFactory.getLogger(LazyNetwork.class);
/**
* Behavior if null
: random UUID will be chosen
*/
protected String name;
protected Boolean enableIpv6;
protected String driver;
protected Set> createNetworkCmdModifiers = new HashSet<>();
/**
* Behavior if null
:
* true
when a {@link #name} was specified, otherwise false
because
*
* - When using a random UUIDv4 as name the chances of collision are extremely small (
* 1 : 2.17 x 10^18
* - you're 155 billion times more likely to win the lottery)
*
* - According to the Docker docs
* this is "best effort" and not guaranteed to catch all name collisions.
*
*
*/
protected Boolean checkDuplicate;
protected boolean deleteNetworkOnClose = true;
protected int deleteNetworkOnCloseTries = 20;
protected CompletableFuture startCF;
protected String id; // null -> not created
public LazyNetwork create()
{
return this.create(CompletableFuture::runAsync);
}
public LazyNetwork create(final Function> executor)
{
if(this.startCF != null)
{
throw new IllegalStateException("Creation was already started");
}
this.startCF = executor.apply(this::startInternal);
return this;
}
@SuppressWarnings({"deprecation", "java:S1874", "java:S2095"})
protected synchronized void startInternal()
{
if(this.id != null)
{
throw new IllegalStateException("Id was already set");
}
final CreateNetworkCmd createNetworkCmd = this.getClient().createNetworkCmd();
if(this.checkDuplicate == null)
{
this.checkDuplicate = this.name != null;
}
if(this.name == null)
{
this.name = UUID.randomUUID().toString();
}
createNetworkCmd.withName(this.name);
createNetworkCmd.withCheckDuplicate(this.checkDuplicate);
if(this.enableIpv6 != null)
{
createNetworkCmd.withEnableIpv6(this.enableIpv6);
}
if(this.driver != null)
{
createNetworkCmd.withDriver(this.driver);
}
for(final Consumer consumer : this.createNetworkCmdModifiers)
{
consumer.accept(createNetworkCmd);
}
final Map labels = Optional.ofNullable(createNetworkCmd.getLabels())
.map(HashMap::new)
.orElseGet(HashMap::new);
labels.putAll(DockerClientFactory.DEFAULT_LABELS);
labels.putAll(ResourceReaper.instance().getLabels());
createNetworkCmd.withLabels(labels);
this.id = createNetworkCmd.exec().getId();
// Free up
this.startCF = null;
}
public void waitForCreation(final Duration timeout)
{
if(this.id == null && this.startCF != null)
{
try
{
this.startCF.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
}
catch(final InterruptedException iex)
{
Thread.currentThread().interrupt();
}
catch(final Exception ex)
{
throw new IllegalStateException("Unable to start", ex);
}
}
}
public String getIdWithoutCheck()
{
return this.id;
}
@Override
public String getId()
{
this.waitForCreation(Duration.ofMinutes(10));
return this.id;
}
@Override
public void close()
{
if(this.id != null)
{
this.closeInternal();
}
}
@SuppressWarnings("resource") // AutoClosable is implemented but does nothing?
protected synchronized void closeInternal()
{
if(this.id != null)
{
if(this.deleteNetworkOnClose)
{
try
{
final AtomicInteger retryCounter = new AtomicInteger(1);
// Docker needs a few moments until all endpoints are removed from the network
Unreliables.retryUntilSuccess(
this.deleteNetworkOnCloseTries,
() -> {
if(retryCounter.getAndIncrement() > 1)
{
Thread.sleep(1000);
}
return this.getClient().removeNetworkCmd(this.id).exec();
}
);
}
catch(final RuntimeException rex)
{
LOG.warn("Failed to delete network; May cause network leak", rex);
}
}
this.id = null;
}
}
protected DockerClient getClient()
{
return DockerClientFactory.instance().client();
}
// region Get/Set
public LazyNetwork withName(final String name)
{
this.name = name;
return this;
}
public LazyNetwork withEnableIpv6(final Boolean enableIpv6)
{
this.enableIpv6 = enableIpv6;
return this;
}
public LazyNetwork withDriver(final String driver)
{
this.driver = driver;
return this;
}
public LazyNetwork withCreateNetworkCmdModifier(final Consumer createNetworkCmdModifier)
{
this.createNetworkCmdModifiers.add(createNetworkCmdModifier);
return this;
}
public LazyNetwork withCheckDuplicate(final Boolean checkDuplicate)
{
this.checkDuplicate = checkDuplicate;
return this;
}
public LazyNetwork withDeleteNetworkOnClose(final boolean deleteNetworkOnClose)
{
this.deleteNetworkOnClose = deleteNetworkOnClose;
return this;
}
public LazyNetwork withDeleteNetworkOnCloseTries(final int deleteNetworkOnCloseTries)
{
this.deleteNetworkOnCloseTries = deleteNetworkOnCloseTries;
return this;
}
public String getName()
{
return this.name;
}
public Boolean getEnableIpv6()
{
return this.enableIpv6;
}
public String getDriver()
{
return this.driver;
}
public Set> getCreateNetworkCmdModifiers()
{
return this.createNetworkCmdModifiers;
}
public Boolean getCheckDuplicate()
{
return this.checkDuplicate;
}
public boolean isDeleteNetworkOnClose()
{
return this.deleteNetworkOnClose;
}
public int getDeleteNetworkOnCloseTries()
{
return this.deleteNetworkOnCloseTries;
}
// endregion
// region Legacy
/**
* @deprecated JUNit4 is effectively dead
*/
@Deprecated(forRemoval = true)
@Override
public Statement apply(final Statement base, final Description description)
{
return null;
}
// endregion
}