software.xdev.tci.safestart.SafeNamedContainerStarter 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.safestart;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
/**
* It's not possible to start a container with e.g. the same name, port bindings, etc. with startupAttempts >
* 1
, which is the default in a few containers (e.g. MariaDB or Selenium-Browsers).
*
* {@link SafeNamedContainerStarter} addresses these problems by
*
* - ensuring that after every started container has a unique name (addresses the name problem)
* - removing previously failed started containers (addresses the port and other problems)
*
*/
public class SafeNamedContainerStarter> implements Runnable
{
protected static final Logger LOG = LoggerFactory.getLogger(SafeNamedContainerStarter.class);
protected final String baseContainerName;
protected final List containerNames = Collections.synchronizedList(new ArrayList<>());
protected final C container;
protected final Consumer starter;
protected boolean attachRandomUUID = true;
public SafeNamedContainerStarter(
final String baseContainerName,
final C container)
{
this(baseContainerName, container, C::start);
}
public SafeNamedContainerStarter(
final String baseContainerName,
final C container,
final Consumer starter)
{
this.baseContainerName = Objects.requireNonNull(baseContainerName);
this.container = Objects.requireNonNull(container);
this.starter = Objects.requireNonNull(starter);
this.prepareContainer();
}
void prepareContainer()
{
final AtomicInteger startAttemptCounter = new AtomicInteger(1);
this.container.withCreateContainerCmdModifier(cmd ->
{
// We are here again -> Prev start (if any) must have failed!
// Clear now, otherwise e.g. fixed port bindings will fail again and forever
this.tryCleanupContainerAfterStartFail(this.containerNames);
this.containerNames.clear();
// Generate new name on each start attempt, so that we can clearly identify the container
String generatedName = this.baseContainerName + "-" + startAttemptCounter.getAndIncrement();
if(this.attachRandomUUID)
{
generatedName += "-" + UUID.randomUUID();
}
// Add first so that we can skip the stream later (there is no skipLast Method)
this.containerNames.add(0, generatedName);
cmd.withName(generatedName);
});
}
public SafeNamedContainerStarter withAttachRandomUUID(final boolean attachRandomUUID)
{
this.attachRandomUUID = attachRandomUUID;
return this;
}
@Override
public void run()
{
this.start();
}
public void start()
{
try
{
this.starter.accept(this.container);
this.tryCleanupContainerAfterStartFail(this.containerNames.stream()
.skip(1) // First one is successfully started one
.toList());
}
catch(final RuntimeException rex)
{
this.tryCleanupContainerAfterStartFail(this.containerNames);
throw rex;
}
}
@SuppressWarnings("resource")
protected void tryCleanupContainerAfterStartFail(final List containerNames)
{
for(final String containerName : containerNames)
{
LOG.info("Start of container[name='{}'] failed; Trying to remove container...", containerName);
try
{
DockerClientFactory.lazyClient()
.removeContainerCmd(containerName)
.withForce(true)
.exec();
LOG.info("Removed failed container[name='{}']", containerName);
}
catch(final Exception ex)
{
LOG.debug("Unable to cleanup container[name='{}']", containerName, ex);
}
}
}
}