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

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

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

import au.net.causal.maven.plugins.boxdb.db.DockerRegistry.ReadManifestResult.Type;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;

public class DockerRegistry
{
    private final URI registryUri;
    private final CloseableHttpClient http;

    private final JsonParser jsonParser = new JsonParser();

    public DockerRegistry(URI registryUri)
    {
        this(registryUri, HttpClientBuilder.create().build());
    }

    public DockerRegistry(URI registryUri, CloseableHttpClient http)
    {
        Objects.requireNonNull(registryUri, "registryUri == null");
        Objects.requireNonNull(http, "http == null");

        this.registryUri = registryUri;
        this.http = http;
    }

    /**
     * Finds an image in the Docker registry and returns manifest information.
     *
     * @param imageName        the name of the image in the repository.
     * @param imageTagOrDigest the tag or digest of the image to find.
     *
     * @return the result, which may indicate the image was not found or details about the image if it was found.
     *
     * @throws IOException if an error occurs reading the manifest.
     */
    public ReadManifestResult readManifest(String imageName, String imageTagOrDigest)
    throws IOException
    {
        return readManifest(imageName, imageTagOrDigest, null);
    }

    private ReadManifestResult readManifest(String imageName, String imageTagOrDigest, String authToken)
    throws IOException
    {
        Objects.requireNonNull(imageName, "repositoryName == null");
        Objects.requireNonNull(imageTagOrDigest, "imageTagOrDigest == null");

        HttpGet get = new HttpGet(registryUri.resolve(imageName + "/manifests/" + imageTagOrDigest));
        get.setHeader(HttpHeaders.ACCEPT, "application/vnd.docker.distribution.manifest.v2+json");
        if (authToken != null)
            get.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + authToken);

        try (CloseableHttpResponse response = http.execute(get))
        {
            if (authToken == null && response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED)
            {
                try
                {
                    //Make a new request now with auth
                    authToken = attemptAuthorization(response, http);
                    return readManifest(imageName, imageTagOrDigest, authToken);
                }
                catch (URISyntaxException e)
                {
                    throw new IOException("Error getting auth token for Docker registry readManifest: " + e, e);
                }
            }

            if (response.getStatusLine().getStatusCode() == HttpStatus.SC_NOT_FOUND)
                return new ReadManifestResult(Type.NOT_FOUND);

            if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine()
                                                                          .getStatusCode() >= 300) //Non-200 response
                throw new IOException("Bad response reading Docker manifest: " + response.getStatusLine());

            //System.err.println(Arrays.toString(response.getAllHeaders()));
            String imageId = parseImageIdFromManifestResponse(EntityUtils.toString(response.getEntity()));
            Header dockerContentDigestHeader = response.getFirstHeader("Docker-Content-Digest");
            String digest = (dockerContentDigestHeader == null ? null : dockerContentDigestHeader.getValue());

            if (imageId == null)
                return new ReadManifestResult(Type.OLD_MANIFEST, digest);
            else
                return new ReadManifestResult(Type.FOUND, digest, imageId);
        }
    }

    public List readTags(String repositoryName)
    throws IOException
    {
        return readTags(repositoryName, null);
    }

    private List readTags(String repositoryName, String authToken)
    throws IOException
    {
        Objects.requireNonNull(repositoryName, "repositoryName == null");

        HttpGet get = new HttpGet(registryUri.resolve(repositoryName + "/tags/list"));
        if (authToken != null)
            get.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + authToken);

        try (CloseableHttpResponse response = http.execute(get))
        {
            if (authToken == null && response.getStatusLine().getStatusCode() == HttpStatus.SC_UNAUTHORIZED)
            {
                try
                {
                    //Make a new request now with auth
                    authToken = attemptAuthorization(response, http);
                    return readTags(repositoryName, authToken);
                }
                catch (URISyntaxException e)
                {
                    throw new IOException("Error getting auth token for Docker registry readTags: " + e, e);
                }
            }

            if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine()
                                                                          .getStatusCode() >= 300) //Non-200 response
                throw new IOException("Bad response reading Docker tags: " + response.getStatusLine());

            return parseTagsFromResponse(EntityUtils.toString(response.getEntity()));
        }
    }

    protected List parseTagsFromResponse(String responseString)
    {
        JsonElement responseJson = jsonParser.parse(responseString);
        JsonArray tagsArray = responseJson.getAsJsonObject().getAsJsonArray("tags");
        ImmutableList.Builder tagsBuilder = ImmutableList.builder();
        for (JsonElement tagsElement : tagsArray)
        {
            tagsBuilder.add(tagsElement.getAsString());
        }

        return tagsBuilder.build();
    }

    protected String parseImageIdFromManifestResponse(String responseString)
    throws IOException
    {
        JsonElement responseJson = jsonParser.parse(responseString);
        JsonObject config = responseJson.getAsJsonObject().getAsJsonObject("config");
        if (config == null)
            return null;

        JsonPrimitive digestPrim = config.getAsJsonPrimitive("digest");
        if (digestPrim == null)
            return null;

        return digestPrim.getAsString();
    }

    private String attemptAuthorization(HttpResponse response, CloseableHttpClient http)
    throws IOException, URISyntaxException
    {
        for (Header authHeader : response.getHeaders(HttpHeaders.WWW_AUTHENTICATE))
        {
            //Look for something like this:
            //Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="registry:catalog:*"

            String authHeaderValue = authHeader.getValue();
            if (authHeaderValue.startsWith("Bearer "))
            {
                URI realmUri = null;
                List authParams = new ArrayList<>();

                for (HeaderElement bearerParam : authHeader.getElements())
                {
                    String paramName = bearerParam.getName();
                    if (paramName.startsWith("Bearer ")) //The first value has this prefixed so get rid of it
                        paramName = paramName.substring("Bearer ".length());

                    String paramValue = bearerParam.getValue();

                    if ("realm".equals(paramName))
                        realmUri = new URI(paramValue);
                    else
                        authParams.add(new BasicNameValuePair(bearerParam.getName(), bearerParam.getValue()));
                }

                if (realmUri == null)
                    throw new IOException("Failed to parse realm from bearer: " + authHeader);

                URIBuilder authUriBuilder = new URIBuilder(realmUri);
                authUriBuilder.addParameters(authParams);

                HttpGet authRequest = new HttpGet();
                authRequest.setURI(authUriBuilder.build());

                try (CloseableHttpResponse authResponse = http.execute(authRequest))
                {
                    if (authResponse.getStatusLine().getStatusCode() != HttpStatus.SC_OK)
                        throw new IOException(
                                "HTTP error " + authResponse.getStatusLine() + " getting auth for Docker registry");

                    //Parse the JSON response
                    JsonElement responseJson = jsonParser.parse(EntityUtils.toString(authResponse.getEntity()));

                    return responseJson.getAsJsonObject().getAsJsonPrimitive("token").getAsString();
                }
            }
        }

        //If we get here we couldn't understand the auth request so bail out
        throw new IOException("Found no understandable auth requests for Docker registry");
    }

    public static class ReadManifestResult
    {
        private final Type type;
        private final String digest;
        private final String imageId;

        public ReadManifestResult(Type type, String digest, String imageId)
        {
            Objects.requireNonNull(type, "type == null");
            this.type = type;
            this.digest = digest;
            this.imageId = imageId;
        }

        public ReadManifestResult(Type type, String digest)
        {
            this(type, digest, null);
        }

        public ReadManifestResult(Type type)
        {
            this(type, null, null);
        }

        /**
         * @return type of response, indicating whether the manifest was found and what type of manifest it is.  The
         * manifest type indicates whether an image ID is available for comparison with the local registry.
         */
        public Type getType()
        {
            return type;
        }

        /**
         * @return the digest of the manifest.  Not the same as the image hash but may be used for comparison with the
         * local repository.  Digest should be available both for old and new manifests.  Prefixed by the algorithm,
         * e.g. 'sha256:'.
         */
        public String getDigest()
        {
            return digest;
        }

        /**
         * @return the image hash, or null if none exists.  This may be compared to the image ID of the image in the
         * local repository.  Prefixed by the algorithm, e.g. 'sha256:'.
         */
        public String getImageId()
        {
            return imageId;
        }

        @Override
        public String toString()
        {
            return new StringJoiner(", ", ReadManifestResult.class.getSimpleName() + "[", "]")
                    .add("type=" + type)
                    .add("digest=" + digest)
                    .add("imageId=" + imageId)
                    .toString();
        }

        public static enum Type
        {
            /**
             * Manifest was found and was of an appropriate version, image hash will be available.
             */
            FOUND,

            /**
             * Manifest not found, indicating no image with the specified name exists remotely.
             */
            NOT_FOUND,

            /**
             * Manifest was found but was an old version so the image hash is unavailable.  Old manifests are typically
             * served for very old Docker images.
             */
            OLD_MANIFEST;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy