All Downloads are FREE. Search and download functionalities are using the official Maven repository.

au.net.causal.maven.plugins.boxdb.db.DockerDatabase Maven / Gradle / Ivy

There is a newer version: 3.3
Show newest version
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");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy