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

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

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

import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.fabric8.maven.docker.access.DockerAccess;
import io.fabric8.maven.docker.access.DockerAccessException;
import io.fabric8.maven.docker.access.UrlBuilder;
import io.fabric8.maven.docker.access.hc.ApacheHttpClientDelegate;
import io.fabric8.maven.docker.access.hc.ApacheHttpClientDelegate.HttpBodyAndStatus;
import io.fabric8.maven.docker.access.hc.DockerAccessWithHcClient;
import io.fabric8.maven.docker.util.JsonFactory;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import static java.net.HttpURLConnection.HTTP_NOT_FOUND;

/**
 * Terrible hacks to get additional functionality with the existing docker client from the docker maven plugin.
 */
public class DockerHacks 
{
    /**
     * Retrieves details about a local Docker image.
     *
     * @param docker the docker client.
     * @param imageName the name of the image to get details of.
     *
     * @return details of the image, or null if the image does not exist.
     *
     * @throws DockerAccessException if an error occurs.
     */
    public ImageDetails inspectImage(DockerAccess docker, String imageName)
    throws DockerAccessException
    {
        DockerAccessWithHcClient client = (DockerAccessWithHcClient)docker;

        try
        {
            Method inspectImageMethod = DockerAccessWithHcClient.class.getDeclaredMethod("inspectImage", String.class);
            inspectImageMethod.setAccessible(true);
            HttpBodyAndStatus response = (HttpBodyAndStatus)inspectImageMethod.invoke(client, imageName);

            if (response.getStatusCode() == HTTP_NOT_FOUND)
                return null;

            JsonObject imageDetails = JsonFactory.newJsonObject(response.getBody());
            JsonPrimitive fullImageIdPrim = imageDetails.getAsJsonPrimitive("Id");
            if (fullImageIdPrim == null)
                throw new DockerAccessException("Id property not found in image");

            String fullImageId = fullImageIdPrim.getAsString();

            //Also save the digests, might not have any though
            JsonArray repoDigestsJson = imageDetails.getAsJsonArray("RepoDigests");
            Set repoDigests;
            if (repoDigestsJson == null)
                repoDigests = ImmutableSet.of();
            else
            {
                ImmutableSet.Builder rdb = ImmutableSet.builder();
                for (JsonElement repoDigest : repoDigestsJson)
                {
                    rdb.add(repoDigest.getAsString());
                }
                repoDigests = rdb.build();
            }

            return new ImageDetails(fullImageId, repoDigests);

        }
        catch (ReflectiveOperationException e)
        {
            throw new DockerAccessException(e, e.getMessage());
        }
    }

    /**
     * The opposite of {@link DockerAccess#copyArchive(String, File, String)}, this method creates an archive of all
     * files in a directory in the docker container.  File names in the generated TAR archive will be relative to the
     * target path.
     * 
     * @param docker the docker client.
     * @param containerId the ID of the container to access.  Can be running or stopped.
     * @param archive the TAR archive file to save to.  
     * @param targetPath the absolute path in the container to read from.  All files under this directory are archived.
     *                   
     * @throws DockerAccessException if an error occurs.
     */
    public void copyArchiveFromDocker(DockerAccess docker, String containerId, Path archive, String targetPath) 
    throws DockerAccessException 
    {
        DockerAccessWithHcClient client = (DockerAccessWithHcClient)docker;

        try 
        {
            //We can reuse the URLBuilder for the other copyArchive because the only difference
            //is the HTTP method (GET vs POST)
            Field urlBuilderField = DockerAccessWithHcClient.class.getDeclaredField("urlBuilder");
            urlBuilderField.setAccessible(true);
            UrlBuilder urlBuilder = (UrlBuilder)urlBuilderField.get(client);

            Field delegateField = DockerAccessWithHcClient.class.getDeclaredField("delegate");
            delegateField.setAccessible(true);
            ApacheHttpClientDelegate delegate = (ApacheHttpClientDelegate)delegateField.get(client);

            String url = urlBuilder.copyArchive(containerId, targetPath);
            delegate.get(url, new ResponseHandler() 
            {
                @Override
                public Object handleResponse(HttpResponse response) throws ClientProtocolException, IOException 
                {
                    try (OutputStream os = Files.newOutputStream(archive)) 
                    {
                        response.getEntity().writeTo(os);
                    }
                    return null;
                }
            }, HttpURLConnection.HTTP_OK);
        } 
        catch (ReflectiveOperationException | IOException e) 
        {
            throw new DockerAccessException(e, e.getMessage());
        }
    }

    public static class ImageDetails
    {
        private final String id;
        private final Collection repoDigests;

        public ImageDetails(String id, Collection repoDigests)
        {
            this.id = id;
            this.repoDigests = ImmutableSet.copyOf(repoDigests);
        }

        /**
         * @return the image ID, including the digest algorithm.  e.g. sha256:123456.
         */
        public String getId()
        {
            return id;
        }

        /**
         * @return full repo digests, including the reponame@ prefix.  e.g. 'postgres@sha256:123456'.
         */
        public Collection getRepoDigests()
        {
            return repoDigests;
        }

        /**
         * @return repo digests without the 'reponame@' prefix.  Returned strings should just contain algorithm:hash,
         * e.g. 'sha256:123456'.
         */
        public Collection getUnprefixedRepoDigests()
        {
            return getRepoDigests().stream()
                                    .map(ImageDetails::removeRepoDigestRepositoryPrefix)
                                    .collect(Collectors.toSet());
        }

        private static String removeRepoDigestRepositoryPrefix(String fullRepoDigest)
        {
            int separatorIndex = fullRepoDigest.indexOf('@');
            if (separatorIndex < 0)
                return fullRepoDigest;

            return fullRepoDigest.substring(separatorIndex + 1);
        }

        @Override
        public String toString()
        {
            return new StringJoiner(", ", ImageDetails.class.getSimpleName() + "[", "]")
                    .add("id=" + id)
                    .add("repoDigests=" + repoDigests)
                    .toString();
        }
    }
}