forked from TrueCloudLab/distribution
0e1b1cc04e
These methods rely on an ObjectStore interface, which is meant to approximate the storage behavior of the docker engine. This is very much subject to change.
510 lines
15 KiB
Go
510 lines
15 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
|
|
"github.com/docker/docker-registry"
|
|
)
|
|
|
|
// Client implements the client interface to the registry http api
|
|
type Client interface {
|
|
// GetImageManifest returns an image manifest for the image at the given
|
|
// name, tag pair
|
|
GetImageManifest(name, tag string) (*registry.ImageManifest, error)
|
|
|
|
// PutImageManifest uploads an image manifest for the image at the given
|
|
// name, tag pair
|
|
PutImageManifest(name, tag string, imageManifest *registry.ImageManifest) error
|
|
|
|
// DeleteImage removes the image at the given name, tag pair
|
|
DeleteImage(name, tag string) error
|
|
|
|
// ListImageTags returns a list of all image tags with the given repository
|
|
// name
|
|
ListImageTags(name string) ([]string, error)
|
|
|
|
// GetImageLayer returns the image layer at the given name, tarsum pair in
|
|
// the form of an io.ReadCloser with the length of this layer
|
|
// A nonzero byteOffset can be provided to receive a partial layer beginning
|
|
// at the given offset
|
|
GetImageLayer(name, tarsum string, byteOffset int) (io.ReadCloser, int, error)
|
|
|
|
// InitiateLayerUpload starts an image upload for the given name, tarsum
|
|
// pair and returns a unique location url to use for other layer upload
|
|
// methods
|
|
// Returns a *registry.LayerAlreadyExistsError if the layer already exists
|
|
// on the registry
|
|
InitiateLayerUpload(name, tarsum string) (string, error)
|
|
|
|
// GetLayerUploadStatus returns the byte offset and length of the layer at
|
|
// the given upload location
|
|
GetLayerUploadStatus(location string) (int, int, error)
|
|
|
|
// UploadLayer uploads a full image layer to the registry
|
|
UploadLayer(location string, layer io.ReadCloser, length int, checksum *registry.Checksum) error
|
|
|
|
// UploadLayerChunk uploads a layer chunk with a given length and startByte
|
|
// to the registry
|
|
// FinishChunkedLayerUpload must be called to finalize this upload
|
|
UploadLayerChunk(location string, layerChunk io.ReadCloser, length, startByte int) error
|
|
|
|
// FinishChunkedLayerUpload completes a chunked layer upload at a given
|
|
// location
|
|
FinishChunkedLayerUpload(location string, length int, checksum *registry.Checksum) error
|
|
|
|
// CancelLayerUpload deletes all content at the unfinished layer upload
|
|
// location and invalidates any future calls to this layer upload
|
|
CancelLayerUpload(location string) error
|
|
}
|
|
|
|
// New returns a new Client which operates against a registry with the
|
|
// given base endpoint
|
|
// This endpoint should not include /v2/ or any part of the url after this
|
|
func New(endpoint string) Client {
|
|
return &clientImpl{endpoint}
|
|
}
|
|
|
|
// clientImpl is the default implementation of the Client interface
|
|
type clientImpl struct {
|
|
Endpoint string
|
|
}
|
|
|
|
// TODO(bbland): use consistent route generation between server and client
|
|
|
|
func (r *clientImpl) GetImageManifest(name, tag string) (*registry.ImageManifest, error) {
|
|
response, err := http.Get(r.imageManifestURL(name, tag))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusOK:
|
|
break
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return nil, ®istry.ImageManifestNotFoundError{Name: name, Tag: tag}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, errors
|
|
default:
|
|
return nil, ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
|
|
decoder := json.NewDecoder(response.Body)
|
|
|
|
manifest := new(registry.ImageManifest)
|
|
err = decoder.Decode(manifest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
func (r *clientImpl) PutImageManifest(name, tag string, manifest *registry.ImageManifest) error {
|
|
manifestBytes, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
putRequest, err := http.NewRequest("PUT",
|
|
r.imageManifestURL(name, tag), bytes.NewReader(manifestBytes))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
response, err := http.DefaultClient.Do(putRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusOK:
|
|
return nil
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors
|
|
default:
|
|
return ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
func (r *clientImpl) DeleteImage(name, tag string) error {
|
|
deleteRequest, err := http.NewRequest("DELETE",
|
|
r.imageManifestURL(name, tag), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
response, err := http.DefaultClient.Do(deleteRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusNoContent:
|
|
break
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return ®istry.ImageManifestNotFoundError{Name: name, Tag: tag}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors
|
|
default:
|
|
return ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *clientImpl) ListImageTags(name string) ([]string, error) {
|
|
response, err := http.Get(fmt.Sprintf("%s/v2/%s/tags/list", r.Endpoint, name))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusOK:
|
|
break
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return nil, ®istry.RepositoryNotFoundError{Name: name}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, errors
|
|
default:
|
|
return nil, ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
|
|
tags := struct {
|
|
Tags []string `json:"tags"`
|
|
}{}
|
|
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tags.Tags, nil
|
|
}
|
|
|
|
func (r *clientImpl) GetImageLayer(name, tarsum string, byteOffset int) (io.ReadCloser, int, error) {
|
|
getRequest, err := http.NewRequest("GET",
|
|
fmt.Sprintf("%s/v2/%s/layer/%s", r.Endpoint, name, tarsum), nil)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
getRequest.Header.Add("Range", fmt.Sprintf("%d-", byteOffset))
|
|
response, err := http.DefaultClient.Do(getRequest)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
if response.StatusCode == http.StatusNotFound {
|
|
return nil, 0, ®istry.LayerNotFoundError{Name: name, TarSum: tarsum}
|
|
}
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusOK:
|
|
lengthHeader := response.Header.Get("Content-Length")
|
|
length, err := strconv.ParseInt(lengthHeader, 10, 0)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return response.Body, int(length), nil
|
|
case response.StatusCode == http.StatusNotFound:
|
|
response.Body.Close()
|
|
return nil, 0, ®istry.LayerNotFoundError{Name: name, TarSum: tarsum}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return nil, 0, errors
|
|
default:
|
|
response.Body.Close()
|
|
return nil, 0, ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
func (r *clientImpl) InitiateLayerUpload(name, tarsum string) (string, error) {
|
|
postRequest, err := http.NewRequest("POST",
|
|
fmt.Sprintf("%s/v2/%s/layer/%s/upload/", r.Endpoint, name, tarsum), nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
response, err := http.DefaultClient.Do(postRequest)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusAccepted:
|
|
return response.Header.Get("Location"), nil
|
|
case response.StatusCode == http.StatusNotModified:
|
|
return "", ®istry.LayerAlreadyExistsError{Name: name, TarSum: tarsum}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "", errors
|
|
default:
|
|
return "", ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
func (r *clientImpl) GetLayerUploadStatus(location string) (int, int, error) {
|
|
response, err := http.Get(fmt.Sprintf("%s%s", r.Endpoint, location))
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusNoContent:
|
|
return parseRangeHeader(response.Header.Get("Range"))
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return 0, 0, ®istry.LayerUploadNotFoundError{Location: location}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return 0, 0, errors
|
|
default:
|
|
return 0, 0, ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
func (r *clientImpl) UploadLayer(location string, layer io.ReadCloser, length int, checksum *registry.Checksum) error {
|
|
defer layer.Close()
|
|
|
|
putRequest, err := http.NewRequest("PUT",
|
|
fmt.Sprintf("%s%s", r.Endpoint, location), layer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
queryValues := url.Values{}
|
|
queryValues.Set("length", fmt.Sprint(length))
|
|
queryValues.Set(checksum.HashAlgorithm, checksum.Sum)
|
|
putRequest.URL.RawQuery = queryValues.Encode()
|
|
|
|
putRequest.Header.Set("Content-Type", "application/octet-stream")
|
|
putRequest.Header.Set("Content-Length", fmt.Sprint(length))
|
|
|
|
response, err := http.DefaultClient.Do(putRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusCreated:
|
|
return nil
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return ®istry.LayerUploadNotFoundError{Location: location}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors
|
|
default:
|
|
return ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
func (r *clientImpl) UploadLayerChunk(location string, layerChunk io.ReadCloser, length, startByte int) error {
|
|
defer layerChunk.Close()
|
|
|
|
putRequest, err := http.NewRequest("PUT",
|
|
fmt.Sprintf("%s%s", r.Endpoint, location), layerChunk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
endByte := startByte + length
|
|
|
|
putRequest.Header.Set("Content-Type", "application/octet-stream")
|
|
putRequest.Header.Set("Content-Length", fmt.Sprint(length))
|
|
putRequest.Header.Set("Content-Range",
|
|
fmt.Sprintf("%d-%d/%d", startByte, endByte, endByte))
|
|
|
|
response, err := http.DefaultClient.Do(putRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusAccepted:
|
|
return nil
|
|
case response.StatusCode == http.StatusRequestedRangeNotSatisfiable:
|
|
lastValidRange, layerSize, err := parseRangeHeader(response.Header.Get("Range"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ®istry.LayerUploadInvalidRangeError{
|
|
Location: location,
|
|
LastValidRange: lastValidRange,
|
|
LayerSize: layerSize,
|
|
}
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return ®istry.LayerUploadNotFoundError{Location: location}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors
|
|
default:
|
|
return ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
func (r *clientImpl) FinishChunkedLayerUpload(location string, length int, checksum *registry.Checksum) error {
|
|
putRequest, err := http.NewRequest("PUT",
|
|
fmt.Sprintf("%s%s", r.Endpoint, location), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
queryValues := new(url.Values)
|
|
queryValues.Set("length", fmt.Sprint(length))
|
|
queryValues.Set(checksum.HashAlgorithm, checksum.Sum)
|
|
putRequest.URL.RawQuery = queryValues.Encode()
|
|
|
|
putRequest.Header.Set("Content-Type", "application/octet-stream")
|
|
putRequest.Header.Set("Content-Length", "0")
|
|
putRequest.Header.Set("Content-Range",
|
|
fmt.Sprintf("%d-%d/%d", length, length, length))
|
|
|
|
response, err := http.DefaultClient.Do(putRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusCreated:
|
|
return nil
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return ®istry.LayerUploadNotFoundError{Location: location}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors
|
|
default:
|
|
return ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
func (r *clientImpl) CancelLayerUpload(location string) error {
|
|
deleteRequest, err := http.NewRequest("DELETE",
|
|
fmt.Sprintf("%s%s", r.Endpoint, location), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
response, err := http.DefaultClient.Do(deleteRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
// TODO(bbland): handle other status codes, like 5xx errors
|
|
switch {
|
|
case response.StatusCode == http.StatusNoContent:
|
|
return nil
|
|
case response.StatusCode == http.StatusNotFound:
|
|
return ®istry.LayerUploadNotFoundError{Location: location}
|
|
case response.StatusCode >= 400 && response.StatusCode < 500:
|
|
errors := new(registry.Errors)
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(&errors)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return errors
|
|
default:
|
|
return ®istry.UnexpectedHTTPStatusError{Status: response.Status}
|
|
}
|
|
}
|
|
|
|
// imageManifestURL is a helper method for returning the full url to an image
|
|
// manifest
|
|
func (r *clientImpl) imageManifestURL(name, tag string) string {
|
|
return fmt.Sprintf("%s/v2/%s/image/%s", r.Endpoint, name, tag)
|
|
}
|
|
|
|
// parseRangeHeader parses out the offset and length from a returned Range
|
|
// header
|
|
func parseRangeHeader(byteRangeHeader string) (int, int, error) {
|
|
r := regexp.MustCompile("bytes=0-(\\d+)/(\\d+)")
|
|
submatches := r.FindStringSubmatch(byteRangeHeader)
|
|
offset, err := strconv.ParseInt(submatches[1], 10, 0)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
length, err := strconv.ParseInt(submatches[2], 10, 0)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return int(offset), int(length), nil
|
|
}
|