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

package au.net.causal.maven.plugins.boxdb.db;

import au.net.causal.maven.plugins.boxdb.DependencyUtils;
import au.net.causal.maven.plugins.boxdb.db.DockerHacks.ImageDetails;
import au.net.causal.maven.plugins.boxdb.db.DockerRegistry.ReadManifestResult;
import au.net.causal.maven.plugins.boxdb.db.DockerRegistry.ReadManifestResult.Type;
import au.net.causal.maven.plugins.boxdb.db.ImageComponent.ImageStatus;
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.ExecException;
import io.fabric8.maven.docker.access.PortMapping;
import io.fabric8.maven.docker.access.log.LogCallback;
import io.fabric8.maven.docker.config.ImageConfiguration;
import io.fabric8.maven.docker.config.RunImageConfiguration;
import io.fabric8.maven.docker.config.WaitConfiguration;
import io.fabric8.maven.docker.model.Container;
import io.fabric8.maven.docker.service.QueryService;
import io.fabric8.maven.docker.service.RunService;
import io.fabric8.maven.docker.util.ImageName;
import io.fabric8.maven.docker.util.GavLabel;
import io.fabric8.maven.docker.util.Timestamp;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.maven.plugin.MojoExecutionException;
import org.codehaus.plexus.util.IOUtil;
import org.eclipse.aether.resolution.DependencyResolutionException;

import java.io.BufferedWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
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.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;

public abstract class DockerDatabase implements BoxDatabase
{
    private final BoxContext context;
    private final BoxConfiguration boxConfiguration;
    private final ProjectConfiguration projectConfiguration;
    private final DockerRegistry dockerRegistry;

    public DockerDatabase(BoxConfiguration boxConfiguration, ProjectConfiguration projectConfiguration,
                          BoxContext context, DockerRegistry dockerRegistry)
    {
        this.boxConfiguration = boxConfiguration;
        this.projectConfiguration = projectConfiguration;
        this.context = context;
        this.dockerRegistry = dockerRegistry;
    }

    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
            //There's what looks like a bug in RunService.shutdown() that does a substring for a log message and assumes
            //it's an  ID instead of a name and has a particular length
            Container container = context.getDockerServiceHub().getDockerAccess().getContainer(containerName());

            //No need to stop a container that doesn't exist
            if (container == null)
                return;

            String containerId = container.getId();
            context.getDockerServiceHub().getRunService().stopContainer(containerId, imageConfig, true, false);
        }
        catch (DockerAccessException e)
        {
            throw createBoxDatabaseException(e);
        }
        catch (ExecException e)
        {
            throw createBoxDatabaseException(e);
        }
    }

    @Override
    public void createAndStart() throws BoxDatabaseException
    {
        RunService runService = context.getDockerServiceHub().getRunService();
        ImageConfiguration imageConfig = imageConfiguration();
        Properties projProperties = projectConfiguration.getProjectProperties();
        GavLabel pomLabel = projectConfiguration.getPomLabel();
        PortMapping portMapping = runService.createPortMapping(imageConfig.getRunConfiguration(), projProperties);
        try
        {
            String imageName = imageConfig.getName();
            DockerPuller.pullImage(imageName, getBoxConfiguration(), getProjectConfiguration(), getContext());
            Date buildTimestamp = new Date();
            String containerId = runService.createAndStartContainer(imageConfig, portMapping, pomLabel, projProperties, getProjectConfiguration().getBaseDirectory(), null, buildTimestamp);
            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
        {
            return queryService.getContainer(containerName());
        }
        catch (DockerAccessException e)
        {
            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.containerNamePattern("%a")
               .ports(ImmutableList.of(boxConfiguration.getDatabasePort() + ":" + containerDatabasePort()));
    }

    /**
     * Customize the docker image configuration for the database.
     * To augment default settings, call super.configureImage() first.
     */
    protected void configureImage(ImageConfiguration.Builder builder)
    {
        builder.name(dockerImageName())
                .alias(containerName());
    }

    protected ImageConfiguration imageConfiguration()
    {
        RunImageConfiguration.Builder runBuilder = new RunImageConfiguration.Builder();

        if (getProjectConfiguration().getKillTimeout() != null)
        {
            WaitConfiguration.Builder waitBuilder = new WaitConfiguration.Builder();
            waitBuilder.kill(Math.toIntExact(getProjectConfiguration().getKillTimeout().toMillis()));
            runBuilder.wait(waitBuilder.build());
        }
        
        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);
    }

    protected static BoxDatabaseException createBoxDatabaseException(ExecException ex)
    {
        return new BoxDatabaseException(ex);
    }

    @Override
    public String getName()
    {
        return containerName();
    }

    protected AuthConfig prepareAuthConfig(ImageName image, String configuredRegistry, boolean isPush, boolean skipExtendedAuth) 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, skipExtendedAuth, 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
    {
        Integer exitCode = container.getExitCode();
        if (exitCode == null)
            return -1;
        else
            return exitCode;
    }

    @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.getContainer(containerId);
                if (container == null)
                    throw new BoxDatabaseException("Container " + containerId + " does not exist.");

                Thread.sleep(getProjectConfiguration().getPollTime().toMillis());
                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");
        }
    }
    
    @Override
    public List logFiles() throws BoxDatabaseException, IOException 
    {
        //If the container does not exist, there are no logs
        if (findDockerContainer() == null)
            return Collections.emptyList();
        else
            return Collections.singletonList(new MainDockerDatabaseLog());
    }

    /**
     * Reads log files from the docker container.
     * 
     * @param includeMain true to include the main docker container log as first item in the list, false to just 
     *                    use the files.
     * @param logFileEncoding the encoding of the log files in the container.
     * @param containerPath the path to read log files from in the container.  Ends with '/' character.
     * @param fileNames a list of file names relative to containerPath.  Do not prefix with '/'.
     *                  
     * @return a list of log files.
     * 
     * @throws BoxDatabaseException if an error occurs with Docker.
     * @throws IOException if an I/O error occurs.
     */
    protected List logFilesInContainer(boolean includeMain, Charset logFileEncoding, 
                                                              String containerPath, String... fileNames)
    throws BoxDatabaseException, IOException
    {
        List results = new ArrayList<>();
        if (includeMain)
            results.add(new MainDockerDatabaseLog());
        
        DockerAccess docker = getContext().getDockerServiceHub().getDockerAccess();

        Container container = findDockerContainer();
        
        //Might not find container if it doesn't exist yet or has terminated unexpectedly
        if (container == null)
            return Collections.emptyList();
        
        String containerId = container.getId();

        //If the container is running, then we can use exec to display log
        Path archive = Files.createTempFile(getContext().getTempDirectory(), "logarchive", ".tar");
            
        DockerHacks dockerHacks = new DockerHacks();
        dockerHacks.copyArchiveFromDocker(docker, containerId, archive, containerPath);
        
        for (String fileName : fileNames)
        {
            results.add(new ContainerFileDatabaseLog(archive, fileName, logFileEncoding));
        }
        
        return results;
    }
    
    public class ContainerFileDatabaseLog implements DatabaseLog
    {
        private final Path tarFile;
        private final String entryName;
        private final Charset encoding;
        
        public ContainerFileDatabaseLog(Path tarFile, String entryName, Charset encoding)
        {
            this.tarFile = tarFile;
            this.entryName = entryName;
            this.encoding = encoding;
        }
        
        @Override
        public String getName() throws BoxDatabaseException 
        {
            return entryName;
        }

        @Override
        public void save(Writer w) throws BoxDatabaseException, IOException 
        {
            try (TarArchiveInputStream tis = new TarArchiveInputStream(Files.newInputStream(tarFile)))
            {
                TarArchiveEntry entry;
                do
                {
                    entry = tis.getNextTarEntry();
                    if (entry != null && entry.getName().endsWith("/" + entryName)) 
                    {
                        //Intentionally not closed because I don't want to close the tar input stream
                        InputStreamReader isr = new InputStreamReader(tis, encoding);
                        IOUtil.copy(isr, w);
                    }
                } 
                while (entry != null);
            }
        }
    }
    
    public class MainDockerDatabaseLog implements DatabaseLog
    {
        @Override
        public String getName() throws BoxDatabaseException 
        {
            return "docker.log";
        }

        @Override
        public void save(Writer w) throws BoxDatabaseException, IOException 
        {
            DockerAccess docker = getContext().getDockerServiceHub().getDockerAccess();
            Container container = findDockerContainer();

            //Might not find container if it doesn't exist yet or has terminated unexpectedly
            if (container == null)
                return;
            
            String containerId = container.getId();

            final AtomicReference exceptionHolder = new AtomicReference<>();

            docker.getLogSync(containerId, new LogCallback() 
            {
                @Override
                public void log(int type, Timestamp timestamp, String txt) 
                {
                    try 
                    {
                        w.write(txt);
                        w.write('\n');
                    } 
                    catch (IOException e) 
                    {
                        exceptionHolder.set(e);
                    }
                }

                @Override
                public void error(String s) 
                {
                    getContext().getLog().error(s);
                }

                @Override
                public void open() throws FileNotFoundException
                {
                }

                @Override
                public void close()
                {
                }
            });
            
            if (exceptionHolder.get() != null)
                throw exceptionHolder.get();
        }
    }

    @Override
    public void prepareImage()
    throws BoxDatabaseException
    {
        //JDBC drivers
        try
        {
            DependencyUtils.resolveDependencies(jdbcDriverInfo().getDependencies(),
                                                getContext().getRepositorySystem(),
                                                getContext().getRepositorySystemSession(),
                                                getContext().getRemoteRepositories());
        }
        catch (DependencyResolutionException e)
        {
            throw new BoxDatabaseException("Failed to download dependencies for database: " + e.getMessage(), e);
        }

        //Docker image
        try
        {
            DockerPuller.pullImage(dockerImageName(), getBoxConfiguration(), getProjectConfiguration(), getContext());
        }
        catch (MojoExecutionException e)
        {
            throw new BoxDatabaseException(e);
        }
        catch (DockerAccessException e)
        {
            throw createBoxDatabaseException(e);
        }
    }

    protected ImageComponent checkDockerDatabaseImage(String remoteDockerRepositoryName)
    throws BoxDatabaseException
    {
        return checkDockerDatabaseImage(remoteDockerRepositoryName, getBoxConfiguration().getDatabaseVersion());
    }

    protected ImageComponent checkDockerDatabaseImage(String remoteDockerRepositoryName, String imageVersion)
    throws BoxDatabaseException
    {
        String imageName = dockerImageName();
        ImageStatus dockerImageStatus;
        try
        {
            DockerHacks dockerHacks = new DockerHacks();
            ImageDetails localImageDetails = dockerHacks.inspectImage(getContext().getDockerServiceHub().getDockerAccess(), imageName);
            boolean hasLocalImage = (localImageDetails != null);

            ReadManifestResult repoImageDetails = dockerRegistry.readManifest(remoteDockerRepositoryName, imageVersion);
            boolean hasRemoteImage = (repoImageDetails.getType() == Type.FOUND || repoImageDetails.getType() == Type.OLD_MANIFEST);

            if (!hasRemoteImage)
            {
                if (!hasLocalImage)
                    dockerImageStatus = ImageStatus.NOT_FOUND;
                else
                    dockerImageStatus = ImageStatus.LOCAL_ONLY;
            }
            else
            {
                if (!hasLocalImage)
                    dockerImageStatus = ImageStatus.NOT_DOWNLOADED;
                else
                {
                    //If we get here, we have both local and remote images
                    //Check if remote/local are different, which would indicate the remote has been updated

                    //If the remote has an image ID, compare those
                    boolean remoteDifferentFromLocal;
                    if (repoImageDetails.getImageId() != null)
                        remoteDifferentFromLocal = !repoImageDetails.getImageId().equals(localImageDetails.getId());
                    else if (repoImageDetails.getDigest() != null)
                    {
                        //Might be an old manifest version where the remote doesn't have image ID, but we still have
                        //repo hash
                        remoteDifferentFromLocal = localImageDetails.getUnprefixedRepoDigests().contains(repoImageDetails.getDigest());
                    }
                    else //Remote has no image ID or digest, could be really old registry?  Just assume no changes then.
                        remoteDifferentFromLocal = false;

                    if (remoteDifferentFromLocal)
                        dockerImageStatus = ImageStatus.REMOTE_UPDATE_AVAILABLE;
                    else
                        dockerImageStatus = ImageStatus.DOWNLOADED;
                }
            }
        }
        catch (DockerAccessException e)
        {
            throw createBoxDatabaseException(e);
        }
        catch (IOException e)
        {
            throw new BoxDatabaseException("Error reading manifest from Docker registry: " + e.getMessage(), e);
        }

        return new ImageComponent("Docker image", imageName, dockerImageStatus);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy