au.net.causal.maven.plugins.boxdb.db.DockerDatabase Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of boxdb-maven-plugin Show documentation
Show all versions of boxdb-maven-plugin Show documentation
Maven plugin to start databases using Docker and VMs
package au.net.causal.maven.plugins.boxdb.db;
import com.google.common.collect.ImmutableList;
import io.fabric8.maven.docker.access.AuthConfig;
import io.fabric8.maven.docker.access.DockerAccess;
import io.fabric8.maven.docker.access.DockerAccessException;
import io.fabric8.maven.docker.access.PortMapping;
import io.fabric8.maven.docker.access.hc.http.HttpRequestException;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.config.RunImageConfiguration;
import io.fabric8.maven.docker.model.Container;
import io.fabric8.maven.docker.model.ContainerDetails;
import io.fabric8.maven.docker.service.QueryService;
import io.fabric8.maven.docker.service.RunService;
import io.fabric8.maven.docker.util.EnvUtil;
import io.fabric8.maven.docker.util.ImageName;
import io.fabric8.maven.docker.util.ImagePullCache;
import io.fabric8.maven.docker.util.PomLabel;
import org.apache.maven.plugin.MojoExecutionException;
import org.codehaus.plexus.util.IOUtil;
import org.json.JSONObject;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.reflect.Field;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeoutException;
public abstract class DockerDatabase implements BoxDatabase
{
private final BoxContext context;
private final BoxConfiguration boxConfiguration;
private final ProjectConfiguration projectConfiguration;
public DockerDatabase(BoxConfiguration boxConfiguration, ProjectConfiguration projectConfiguration, BoxContext context)
{
this.boxConfiguration = boxConfiguration;
this.projectConfiguration = projectConfiguration;
this.context = context;
}
protected BoxConfiguration getBoxConfiguration()
{
return boxConfiguration;
}
protected ProjectConfiguration getProjectConfiguration()
{
return projectConfiguration;
}
protected BoxContext getContext()
{
return context;
}
@Override
public void start() throws BoxDatabaseException
{
DockerAccess dockerAccess = context.getDockerServiceHub().getDockerAccess();
try
{
dockerAccess.startContainer(containerName());
}
catch (DockerAccessException e)
{
throw createBoxDatabaseException(e);
}
}
@Override
public void stop() throws BoxDatabaseException
{
ImageConfiguration imageConfig = imageConfiguration();
try
{
//Resolve container name to ID first
String containerId = context.getDockerServiceHub().getDockerAccess().inspectContainer(containerName()).getId();
context.getDockerServiceHub().getRunService().stopContainer(containerId, imageConfig, true, false);
}
catch (DockerAccessException e)
{
throw createBoxDatabaseException(e);
}
}
@Override
public void createAndStart() throws BoxDatabaseException
{
QueryService queryService = context.getDockerServiceHub().getQueryService();
RunService runService = context.getDockerServiceHub().getRunService();
DockerAccess docker = context.getDockerServiceHub().getDockerAccess();
ImageConfiguration imageConfig = imageConfiguration();
Properties projProperties = projectConfiguration.getProjectProperties();
PomLabel pomLabel = projectConfiguration.getPomLabel();
PortMapping portMapping = runService.getPortMapping(imageConfig.getRunConfiguration(), projProperties);
String autoPullMode = context.getImageUpdateMode().name().toLowerCase(Locale.ENGLISH);
try
{
boolean pullImage = queryService.imageRequiresAutoPull(autoPullMode, imageConfig.getName(), true, new ImagePullCache());
if (pullImage)
{
String registry = getConfiguredRegistry(imageConfig, null); //TODO should we override this to allow specific registry?
ImageName imageName = new ImageName(imageConfig.getName());
AuthConfig authConfig = prepareAuthConfig(imageName, registry, false);
docker.pullImage(imageConfig.getName(), authConfig, registry);
}
String containerId = runService.createAndStartContainer(imageConfig, portMapping, pomLabel, projProperties);
context.getLog().info("Created docker container: " + containerId);
}
catch (MojoExecutionException e)
{
throw new BoxDatabaseException(e);
}
catch (DockerAccessException e)
{
throw createBoxDatabaseException(e);
}
}
protected Container findDockerContainer()
throws BoxDatabaseException
{
QueryService queryService = context.getDockerServiceHub().getQueryService();
try
{
//TODO Workaround for bug in Docker plugin - should return null but actually throws exception when not found
return queryService.getContainer(containerName());
}
catch (DockerAccessException e)
{
//Special case for 404 - bug in Docker plugin
if (e.getCause() instanceof HttpRequestException && e.getCause().getMessage().contains("No such container"))
return null;
throw createBoxDatabaseException(e);
}
}
@Override
public boolean exists() throws BoxDatabaseException
{
return findDockerContainer() != null;
}
@Override
public boolean isRunning() throws BoxDatabaseException
{
Container container = findDockerContainer();
if (container == null)
return false;
return container.isRunning();
}
@Override
public void waitUntilStarted(Duration maxTimeToWait)
throws TimeoutException, BoxDatabaseException
{
DatabaseUtils.waitUntilTcpPortResponding(maxTimeToWait, this, context);
}
@Override
public void delete() throws BoxDatabaseException
{
DockerAccess dockerAccess = context.getDockerServiceHub().getDockerAccess();
try
{
boolean removeVolumes = true;
dockerAccess.removeContainer(containerName(), removeVolumes);
}
catch (DockerAccessException e)
{
throw createBoxDatabaseException(e);
}
}
@Override
public void deleteImage() throws BoxDatabaseException
{
DockerAccess dockerAccess = context.getDockerServiceHub().getDockerAccess();
try
{
boolean force = false;
dockerAccess.removeImage(dockerImageName(), force);
}
catch (DockerAccessException e)
{
throw createBoxDatabaseException(e);
}
}
/**
* @return the name of the docker container. Defaults to the name given in the configuration.
*/
protected String containerName()
{
return boxConfiguration.getContainerName();
}
/**
* @return the port the container runs the database on. This is not necessarily the port that is mapped on
* the host system.
*
* @see BoxConfiguration#getDatabasePort()
*/
protected abstract int containerDatabasePort();
/**
* @return the name of the docker image to build the container with. Defaults to
* databaseType:databaseVersion
*/
protected String dockerImageName()
{
return boxConfiguration.getDatabaseType() + ":" + boxConfiguration.getDatabaseVersion();
}
/**
* Customize the docker run image for the database.
* To augment default settings, call super.configureRunImage()
first.
*
* @param builder the builder to configure.
*/
protected void configureRunImage(RunImageConfiguration.Builder builder)
{
builder.namingStrategy("alias")
.ports(ImmutableList.of(boxConfiguration.getDatabasePort() + ":" + containerDatabasePort()));
}
/**
* Customize the docker image configuration for the database.
* To augment default settings, call super.configureImage()
first.
* @param builder
*/
protected void configureImage(ImageConfiguration.Builder builder)
{
builder.name(dockerImageName())
.alias(containerName());
}
protected ImageConfiguration imageConfiguration()
{
RunImageConfiguration.Builder runBuilder = new RunImageConfiguration.Builder();
configureRunImage(runBuilder);
RunImageConfiguration runConfig = runBuilder.build();
ImageConfiguration.Builder imageBuilder = new ImageConfiguration.Builder();
imageBuilder.runConfig(runConfig);
configureImage(imageBuilder);
return imageBuilder.build();
}
protected static BoxDatabaseException createBoxDatabaseException(DockerAccessException ex)
{
return new BoxDatabaseException(ex);
}
@Override
public String getName()
{
return containerName();
}
protected String getConfiguredRegistry(ImageConfiguration imageConfig, String specificRegistry)
{
return EnvUtil.findRegistry(imageConfig.getRegistry(), specificRegistry);
}
protected AuthConfig prepareAuthConfig(ImageName image, String configuredRegistry, boolean isPush) throws MojoExecutionException
{
String user = isPush?image.getUser():null;
String registry = image.getRegistry() != null?image.getRegistry():configuredRegistry;
Map authConfigMap = new HashMap<>();
return getContext().getAuthConfigFactory().createAuthConfig(isPush, authConfigMap, getProjectConfiguration().getSettings(), user, registry);
}
/**
* Attempt to read the exit code from the process. Not publicly accessible, but we can use reflection to get it
* from the JSON response.
*
* @param container the docker container information.
*
* @return the exit code of the process the container was initialized with, or -1 if not available.
*
* @throws BoxDatabaseException if in error occurs.
*/
protected int readExitCodeFromContainer(Container container)
throws BoxDatabaseException
{
if (!(container instanceof ContainerDetails))
return -1;
try
{
Field jsonField = ContainerDetails.class.getDeclaredField("json");
jsonField.setAccessible(true);
JSONObject json = (JSONObject)jsonField.get(container);
if (json == null)
return -1;
JSONObject state = json.optJSONObject("State");
if (state == null)
return -1;
return state.optInt("ExitCode", -1);
}
catch (NoSuchFieldException | IllegalAccessException e)
{
throw new BoxDatabaseException("Error reading container details: " + e, e);
}
}
@Override
public void executeScript(Reader scriptReader, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SQLException, BoxDatabaseException
{
//Save script to file
Path tempDir = getContext().getTempDirectory();
Path file = Files.createTempFile(tempDir, getBoxConfiguration().getContainerName() + "-", ".sql");
try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8))
{
IOUtil.copy(scriptReader, writer);
}
executeScriptFile(file, targetDatabase, timeout);
}
@Override
public void executeScript(URL script, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SQLException, BoxDatabaseException
{
//Save script to file if it's not already a file
Path file;
try
{
file = Paths.get(script.toURI());
}
catch (URISyntaxException e)
{
throw new IOException("Invalid URL: " + script.toExternalForm() + ": " + e, e);
}
catch (FileSystemNotFoundException e)
{
//If we get here, then we need to save to temporary file first
Path tempDir = getContext().getTempDirectory();
file = Files.createTempFile(tempDir, getBoxConfiguration().getContainerName() + "-", ".sql");
try (InputStream is = script.openStream())
{
Files.copy(is, file);
}
}
executeScriptFile(file, targetDatabase, timeout);
}
protected abstract void executeScriptFile(Path file, DatabaseTarget targetDatabase, Duration timeout)
throws IOException, SQLException, BoxDatabaseException;
@Override
public void restore(URL backupResource) throws BoxDatabaseException, IOException, SQLException
{
//Save backup to file if it's not already a file
Path file;
try
{
file = Paths.get(backupResource.toURI());
}
catch (URISyntaxException e)
{
throw new IOException("Invalid URL: " + backupResource.toExternalForm() + ": " + e, e);
}
catch (FileSystemNotFoundException e)
{
//If we get here, then we need to save to temporary file first
Path tempDir = getContext().getTempDirectory();
file = Files.createTempFile(tempDir, getBoxConfiguration().getContainerName() + "-", ".dbbackup");
try (InputStream is = backupResource.openStream())
{
Files.copy(is, file);
}
}
restore(file);
}
protected Container waitForContainerToFinish(String containerId, int timeoutInSeconds)
throws DockerAccessException, BoxDatabaseException
{
DockerAccess docker = getContext().getDockerServiceHub().getDockerAccess();
try
{
long startTime = System.currentTimeMillis();
long maxTime = startTime + timeoutInSeconds * 1000L;
Container container;
do
{
container = docker.inspectContainer(containerId);
Thread.sleep(100L);
if (System.currentTimeMillis() > maxTime)
{
docker.stopContainer(containerId, timeoutInSeconds);
throw new BoxDatabaseException("Timeout waiting for container to execute.");
}
}
while (container.isRunning());
return container;
}
catch (InterruptedException e)
{
docker.stopContainer(containerId, timeoutInSeconds);
throw new BoxDatabaseException("Interrupted waiting for container to finish");
}
}
}