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

com.distelli.gcr.GcrClient Maven / Gradle / Ivy

There is a newer version: 2.1.3
Show newest version
/*
  $Id: $
  @file GcrClient.java
  @brief Contains the GcrClient.java class

  @author Rahul Singh [rsingh]
  Copyright (c) 2013, Distelli Inc., All Rights Reserved.
*/
package com.distelli.gcr;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import com.distelli.gcr.models.*;
import com.distelli.gcr.auth.*;
import com.distelli.gcr.serializers.*;
import com.distelli.gcr.exceptions.*;
import java.net.URI;
import java.io.InputStream;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.HttpUrl;
import okhttp3.Response;
import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okhttp3.RequestBody;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.annotation.JsonInclude;
import java.util.Map;
import java.util.HashMap;
import java.util.Collections;
import java.io.ByteArrayInputStream;

public class GcrClient
{
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private static final Logger log = LoggerFactory.getLogger(GcrClient.class);
    private static Pattern RANGE_PATTERN = Pattern.compile("bytes=0-([0-9]+)");

    static {
        OBJECT_MAPPER.configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, true);
        OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    private OkHttpClient _httpClient;
    private URI _endpoint;

    public static class Builder {
        private OkHttpClient.Builder _httpClientBuilder = new OkHttpClient.Builder();
        private URI _endpoint;

        // If you have other OkHttpClient's that you want to
        // share a connection pool with:
        public Builder connectionPool(ConnectionPool pool) {
            _httpClientBuilder.connectionPool(pool);
            return this;
        }

        public Builder gcrCredentials(GcrCredentials creds) {
            if ( null == creds ) return this;
            _httpClientBuilder.addInterceptor((chain) -> {
                    String authHeader = creds.getHttpBasicAuthHeader();
                    Request req = chain.request();
                    if ( null != authHeader ) {
                        req = req.newBuilder()
                            .header("Authorization", creds.getHttpBasicAuthHeader())
                            .build();
                    }
                    return chain.proceed(req);
                });
            return this;
        }

        public Builder gcrRegion(GcrRegion gcrRegion) {
            _endpoint = URI.create(((null == gcrRegion)?GcrRegion.DEFAULT:gcrRegion).getHttpsEndpoint());
            return this;
        }

        public GcrClient build() {
            return new GcrClient(_httpClientBuilder.build(), _endpoint);
        }
    }

    private GcrClient(OkHttpClient client, URI endpoint)
    {
        _httpClient = client;
        _endpoint = endpoint;
    }

    public GcrClient(GcrCredentials gcrCredentials, GcrRegion gcrRegion)
    {
        this(new Builder()
             .gcrCredentials(gcrCredentials)
             ._httpClientBuilder.build(),
             URI.create(((null == gcrRegion)?GcrRegion.DEFAULT:gcrRegion).getHttpsEndpoint()));
    }

    public ConnectionPool getConnectionPool() {
        return _httpClient.connectionPool();
    }

    public List listRepositories(GcrIterator iterator)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .get()
            .url(addIterator(HttpUrl(), iterator)
                 .addPathSegment("_catalog")
                 .build())
            .build();

        try ( Response httpResponse = _httpClient.newCall(request).execute() ) {
            if(iterator != null)
            {
                String linkHeader = httpResponse.header("Link");
                iterator.updateMarker(linkHeader);
            }
            int httpStatusCode = httpResponse.code();
            JsonNode responseJson = readTree(httpResponse.body(), httpResponse.code());
            if(httpStatusCode / 100 != 2)
            {
                List errors = GcrErrorSerializer.deserialize(responseJson);
                throw(new GcrException(errors));
            }

            return GcrRepositorySerializer.deserializeList(responseJson);
        }
    }

    public List listImageTags(GcrRepository repository,
                                           GcrIterator iterator)
        throws IOException, GcrException
    {
        return listImageTags(repository.getFullName(),
                             iterator);
    }

    public List listImageTags(String repository, GcrIterator iterator)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .get()
            .url(addIterator(HttpUrl(), iterator)
                 .addPathSegments(repository)
                 .addPathSegment("tags")
                 .addPathSegment("list")
                 .build())
            .build();

        try ( Response httpResponse = _httpClient.newCall(request).execute() ) {
            if(iterator != null)
            {
                String linkHeader = httpResponse.header("Link");
                iterator.updateMarker(linkHeader);
            }
            int httpStatusCode = httpResponse.code();
            JsonNode responseJson = readTree(httpResponse.body(), httpResponse.code());
            if(httpStatusCode / 100 != 2)
            {
                List errors = GcrErrorSerializer.deserialize(responseJson);
                throw(new GcrException(errors));
            }

            return GcrImageTagSerializer.deserializeList(responseJson);
        }
    }

    public GcrManifest getManifest(String repository, String reference)
        throws IOException, GcrException
    {
        return getManifest(repository, reference, "*/*");
    }

    // GET /v2//manifests/
    public GcrManifest getManifest(String repository, String reference, String acceptHeader)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .get()
            .header("Accept", acceptHeader)
            .url(HttpUrl()
                 .addPathSegments(repository)
                 .addPathSegment("manifests")
                 .addPathSegment(reference)
                 .build())
            .build();

        try ( Response response = _httpClient.newCall(request).execute() ) {
            if ( 404 == response.code() ) {
                return null;
            }
            if ( response.code() / 100 == 2 ) {
                String manifestStr = response.body().string();
                String mediaType = response.header("Content-Type");
                return new GcrManifest() {
                    @Override
                    public String toString() {
                        return manifestStr;
                    }
                    @Override
                    public String getMediaType() {
                        return mediaType;
                    }
                };
            }
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(response.body(), response.code())));
        }
    }

    // PUT /v2//manifests/
    // reference may be a tag or GcrManifestMeta.digest
    public GcrManifestMeta putManifest(String repository, String reference, GcrManifest manifest)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .put(RequestBody.create(MediaType.parse(manifest.getMediaType()),
                                    manifest.toString()))
            .url(HttpUrl()
                 .addPathSegments(repository)
                 .addPathSegment("manifests")
                 .addPathSegment(reference)
                 .build())
            .build();
        try ( Response response = _httpClient.newCall(request).execute() ) {
            if ( response.code() / 100 == 2 ) {
                return GcrManifestMeta.builder()
                    .digest(response.header("Docker-Content-Digest"))
                    .location(response.header("Location"))
                    .mediaType(manifest.getMediaType())
                    .build();
            }
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(response.body(), response.code())));
        }
    }

    // GET /v2//blobs/
    public  T getBlob(String repository, String digest, GcrBlobReader reader)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .get()
            .url(HttpUrl()
                 .addPathSegments(repository)
                 .addPathSegment("blobs")
                 .addPathSegment(digest)
                 .build())
            .build();
        try ( Response response = _httpClient.newCall(request).execute() ) {
            if ( 404 == response.code() ) {
                return reader.read(new ByteArrayInputStream(new byte[0]), null);
            }
            if ( response.code() / 100 != 2 ) {
                throw new GcrException(
                    GcrErrorSerializer.deserialize(
                        readTree(response.body(), response.code())));
            }
            return reader.read(response.body().byteStream(),
                               GcrBlobMeta.builder()
                               .digest(digest)
                               .length(response.body().contentLength())
                               .build());
        }        
    }

    // HEAD /v2//blobs/
    public GcrBlobMeta getBlobMeta(String repository, String digest)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .head()
            .url(HttpUrl()
                 .addPathSegments(repository)
                 .addPathSegment("blobs")
                 .addPathSegment(digest)
                 .build())
            .build();
        try ( Response response = _httpClient.newCall(request).execute() ) {
            if ( 404 == response.code() ) return null;
            if(response.code() / 100 != 2) {
                throw new GcrException(
                    GcrErrorSerializer.deserialize(
                        readTree(response.body(), response.code())));
            }
            return GcrBlobMeta.builder()
                .digest(digest)
                .length(response.body().contentLength())
                .build();
        }
    }

    // POST /v2//blobs/uploads/
    public GcrBlobUpload createBlobUpload(String repository)
        throws IOException, GcrException
    {
        return createBlobUpload(repository, null, null);
    }

    // cross repository blob mounting:
    // POST /v2//blobs/uploads/?mount=&from=
    public GcrBlobUpload createBlobUpload(String repository, String digest, String fromRepository)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .post(RequestBody.create(null, ""))
            .url(addCrossMount(HttpUrl(), digest, fromRepository)
                 .addPathSegments(repository)
                 .addPathSegment("blobs")
                 .addPathSegments("uploads/")
                 .build())
            .build();
        try ( Response httpResponse = _httpClient.newCall(request).execute() ) {
            switch ( httpResponse.code() ) {
            case 404: return null;
            case 201: return GcrBlobUpload.builder()
                    .complete(true)
                    .blobLocation(httpResponse.header("Location"))
                    .digest(httpResponse.header("Docker-Content-Digest"))
                    .build();
            case 202: return GcrBlobUpload.builder()
                    .uploadLocation(httpResponse.header("Location"))
                    .uploadId(httpResponse.header("Docker-Upload-UUID"))
                    .rangeBegin(0)
                    .complete(false)
                    .build();
            }
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(httpResponse.body(), httpResponse.code())));
        }
    }

    // GET /v2//blobs/uploads/
    public GcrBlobUpload getBlobUploadProgress(GcrBlobUpload blobUpload)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .get()
            .url(getUploadLocation(blobUpload))
            .build();
        try ( Response response = _httpClient.newCall(request).execute() ) {
            if ( 204 == response.code() ) {
                String range = response.header("Range");
                Matcher matcher = ( null == range ) ? null : RANGE_PATTERN.matcher(range);
                if ( null == matcher || ! matcher.find() ) {
                    throw new IllegalStateException("Missing Range header got headers="+response.headers());
                }
                return blobUpload.toBuilder()
                    .rangeBegin(parseLong(matcher.group(1)))
                    .build();
            }
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(response.body(), response.code())));
        }
    }

    // PATCH /v2//blobs/uploads/
    // Returns a new GcrBlobUpload with startRange upadated.
    public GcrBlobUpload blobUploadChunk(GcrBlobUpload blobUpload, InputStream chunk, Long chunkLength)
        throws IOException, GcrException
    {
        AtomicLong actualChunkLength = new AtomicLong();
        Request.Builder request = new Request.Builder()
            .patch(toRequestBody(chunk, chunkLength, actualChunkLength))
            .header("Expect", "100-continue")
            .header("Range", getRangeHeader(blobUpload.getRangeBegin(), chunkLength))
            .url(getUploadLocation(blobUpload, null));

        try ( Response response = _httpClient.newCall(request.build()).execute() ) {
            if ( 202 == response.code() || 204 == response.code() ) {
                String range = response.header("Range");
                Matcher matcher = ( null == range ) ? null : RANGE_PATTERN.matcher(range);
                if ( null == matcher || ! matcher.find() ) {
                    return blobUpload.toBuilder()
                        .rangeBegin(blobUpload.getRangeBegin() + actualChunkLength.get())
                        .build();
                }
                return blobUpload.toBuilder()
                    .rangeBegin(parseLong(matcher.group(1)))
                    .build();
            }
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(response.body(), response.code())));
        }
    }

    // PUT /v2//blob/uploads/?digest=
    public GcrBlobMeta blobUploadChunk(GcrBlobUpload blobUpload, InputStream chunk, Long chunkLength, String digest)
        throws IOException, GcrException
    {
        AtomicLong actualChunkLength = new AtomicLong();
        Request.Builder request = new Request.Builder()
            .put(toRequestBody(chunk, chunkLength, actualChunkLength))
            .header("Expect", "100-continue")
            .header("Range", getRangeHeader(blobUpload.getRangeBegin(), chunkLength))
            .url(getUploadLocation(blobUpload, digest));

        try ( Response response = _httpClient.newCall(request.build()).execute() ) {
            if ( 201 == response.code() ) {
                return GcrBlobMeta.builder()
                    .digest(response.header("Docker-Content-Digest"))
                    .length(blobUpload.getRangeBegin() + actualChunkLength.get())
                    .build();
            }
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(response.body(), response.code())));
        }
    }

    // DELETE /v2//blobs/uploads/
    public void cancelUploadChunk(GcrBlobUpload blobUpload)
        throws IOException, GcrException
    {
        Request request = new Request.Builder()
            .delete(RequestBody.create(null, ""))
            .url(getUploadLocation(blobUpload))
            .build();
        try ( Response httpResponse = _httpClient.newCall(request).execute() ) {
            if ( httpResponse.code() / 100 == 2 ) return;
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(httpResponse.body(), httpResponse.code())));
        }
    }

    // DELETE /v2//blobs/
    public boolean deleteBlob(String repository, String digest)
        throws IOException, GcrException
    {

        Request request = new Request.Builder()
            .delete(RequestBody.create(null, ""))
            .url(HttpUrl()
                 .addPathSegments(repository)
                 .addPathSegment("blobs")
                 .addPathSegment(digest)
                 .build())
            .build();
        try ( Response response = _httpClient.newCall(request).execute() ) {
            if ( response.code() / 100 == 2 ) return true;
            if ( 404 == response.code() ) return false;
            throw new GcrException(
                GcrErrorSerializer.deserialize(
                    readTree(response.body(), response.code())));
        }
    }

    private static Long parseLong(String str) {
        try {
            return Long.parseLong(str);
        } catch ( NumberFormatException ex ) {
            return null;
        }
    }

    private String getRangeHeader(long rangeBegin, Long chunkLength) {
        if ( null == chunkLength ) {
            return rangeBegin + "-";
        }
        return rangeBegin + "-" + (rangeBegin+chunkLength-1L);
    }

    private RequestBody toRequestBody(InputStream in, Long length, AtomicLong actualLength) {
        return new RequestBody() {
            @Override
            public long contentLength() {
                return ( null == length ) ? -1 : length;
            }
            @Override
            public MediaType contentType() {
                return MediaType.parse("application/octet-stream");
            }
            @Override
            public void writeTo(okio.BufferedSink sink) throws IOException {
                okio.Source source = okio.Okio.source(in);
                if ( null != length ) {
                    actualLength.addAndGet(length);
                    sink.write(source, length);
                } else {
                    actualLength.addAndGet(sink.writeAll(source));
                }
            }
        };
    }

    private HttpUrl getUploadLocation(GcrBlobUpload blobUpload) {
        return getUploadLocation(blobUpload, null);
    }

    private HttpUrl getUploadLocation(GcrBlobUpload blobUpload, String digest) {
        HttpUrl url = HttpUrl.parse(blobUpload.getUploadLocation());
        if ( null == url ) {
            throw new IllegalArgumentException(
                "GcrBlobUpload.uploadLocation="+blobUpload.getUploadLocation()+" is invalid");
        }
        if ( null != digest ) {
            url = url.newBuilder()
                .addQueryParameter("digest", digest)
                .build();
        }
        return url;
    }

    private HttpUrl.Builder addCrossMount(HttpUrl.Builder urlBuilder, String digest, String fromRepository) {
        if ( null == digest ) return urlBuilder;
        urlBuilder.addQueryParameter("mount", digest);
        if ( null == fromRepository ) return urlBuilder;
        return urlBuilder.addQueryParameter("from", fromRepository);
    }

    private HttpUrl.Builder HttpUrl() {
        return HttpUrl.get(_endpoint).newBuilder()
            .addPathSegment("v2");
    }

    private boolean isJson(MediaType mediaType) {
        return null != mediaType &&
            "application".equals(mediaType.type()) &&
            "json".equals(mediaType.subtype());
    }

    private JsonNode readTree(ResponseBody body, int code) throws IOException {
        if ( ! isJson(body.contentType()) ) {
            String bodyStr = new String(body.bytes(), ISO_8859_1);
            JsonNodeFactory jnf = OBJECT_MAPPER.getNodeFactory();
            return jnf.objectNode()
                .set("errors",
                     jnf.arrayNode()
                     .add(jnf.objectNode()
                          .put("code", ""+code)
                          .put("message", "")));
        }
        JsonParser parser = OBJECT_MAPPER.getFactory().createJsonParser(body.byteStream());
        if ( null == parser.nextToken() ) {
            JsonNodeFactory jnf = OBJECT_MAPPER.getNodeFactory();
            return jnf.objectNode()
                .set("errors",
                     jnf.arrayNode()
                     .add(jnf.objectNode()
                          .put("code", ""+code)
                          .put("message", "")));
        }
        return OBJECT_MAPPER.readValue(parser, JsonNode.class);
    }

    private HttpUrl.Builder addIterator(HttpUrl.Builder builder, GcrIterator iterator) {
        if ( null == iterator ) return builder;
        if ( null != iterator.getMarker() ) {
            builder.addQueryParameter("last", iterator.getMarker());
        }
        return builder.addQueryParameter("n", ""+iterator.getPageSize());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy