github.com/lusis/distribution@v2.0.1+incompatible/registry/client/client.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"regexp"
    10  	"strconv"
    11  
    12  	"github.com/docker/distribution/digest"
    13  	"github.com/docker/distribution/manifest"
    14  	"github.com/docker/distribution/registry/api/v2"
    15  )
    16  
    17  // Client implements the client interface to the registry http api
    18  type Client interface {
    19  	// GetImageManifest returns an image manifest for the image at the given
    20  	// name, tag pair.
    21  	GetImageManifest(name, tag string) (*manifest.SignedManifest, error)
    22  
    23  	// PutImageManifest uploads an image manifest for the image at the given
    24  	// name, tag pair.
    25  	PutImageManifest(name, tag string, imageManifest *manifest.SignedManifest) error
    26  
    27  	// DeleteImage removes the image at the given name, tag pair.
    28  	DeleteImage(name, tag string) error
    29  
    30  	// ListImageTags returns a list of all image tags with the given repository
    31  	// name.
    32  	ListImageTags(name string) ([]string, error)
    33  
    34  	// BlobLength returns the length of the blob stored at the given name,
    35  	// digest pair.
    36  	// Returns a length value of -1 on error or if the blob does not exist.
    37  	BlobLength(name string, dgst digest.Digest) (int, error)
    38  
    39  	// GetBlob returns the blob stored at the given name, digest pair in the
    40  	// form of an io.ReadCloser with the length of this blob.
    41  	// A nonzero byteOffset can be provided to receive a partial blob beginning
    42  	// at the given offset.
    43  	GetBlob(name string, dgst digest.Digest, byteOffset int) (io.ReadCloser, int, error)
    44  
    45  	// InitiateBlobUpload starts a blob upload in the given repository namespace
    46  	// and returns a unique location url to use for other blob upload methods.
    47  	InitiateBlobUpload(name string) (string, error)
    48  
    49  	// GetBlobUploadStatus returns the byte offset and length of the blob at the
    50  	// given upload location.
    51  	GetBlobUploadStatus(location string) (int, int, error)
    52  
    53  	// UploadBlob uploads a full blob to the registry.
    54  	UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error
    55  
    56  	// UploadBlobChunk uploads a blob chunk with a given length and startByte to
    57  	// the registry.
    58  	// FinishChunkedBlobUpload must be called to finalize this upload.
    59  	UploadBlobChunk(location string, blobChunk io.ReadCloser, length, startByte int) error
    60  
    61  	// FinishChunkedBlobUpload completes a chunked blob upload at a given
    62  	// location.
    63  	FinishChunkedBlobUpload(location string, length int, dgst digest.Digest) error
    64  
    65  	// CancelBlobUpload deletes all content at the unfinished blob upload
    66  	// location and invalidates any future calls to this blob upload.
    67  	CancelBlobUpload(location string) error
    68  }
    69  
    70  var (
    71  	patternRangeHeader = regexp.MustCompile("bytes=0-(\\d+)/(\\d+)")
    72  )
    73  
    74  // New returns a new Client which operates against a registry with the
    75  // given base endpoint
    76  // This endpoint should not include /v2/ or any part of the url after this.
    77  func New(endpoint string) (Client, error) {
    78  	ub, err := v2.NewURLBuilderFromString(endpoint)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	return &clientImpl{
    84  		endpoint: endpoint,
    85  		ub:       ub,
    86  	}, nil
    87  }
    88  
    89  // clientImpl is the default implementation of the Client interface
    90  type clientImpl struct {
    91  	endpoint string
    92  	ub       *v2.URLBuilder
    93  }
    94  
    95  // TODO(bbland): use consistent route generation between server and client
    96  
    97  func (r *clientImpl) GetImageManifest(name, tag string) (*manifest.SignedManifest, error) {
    98  	manifestURL, err := r.ub.BuildManifestURL(name, tag)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	response, err := http.Get(manifestURL)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	defer response.Body.Close()
   108  
   109  	// TODO(bbland): handle other status codes, like 5xx errors
   110  	switch {
   111  	case response.StatusCode == http.StatusOK:
   112  		break
   113  	case response.StatusCode == http.StatusNotFound:
   114  		return nil, &ImageManifestNotFoundError{Name: name, Tag: tag}
   115  	case response.StatusCode >= 400 && response.StatusCode < 500:
   116  		var errs v2.Errors
   117  
   118  		decoder := json.NewDecoder(response.Body)
   119  		err = decoder.Decode(&errs)
   120  		if err != nil {
   121  			return nil, err
   122  		}
   123  		return nil, &errs
   124  	default:
   125  		return nil, &UnexpectedHTTPStatusError{Status: response.Status}
   126  	}
   127  
   128  	decoder := json.NewDecoder(response.Body)
   129  
   130  	manifest := new(manifest.SignedManifest)
   131  	err = decoder.Decode(manifest)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	return manifest, nil
   136  }
   137  
   138  func (r *clientImpl) PutImageManifest(name, tag string, manifest *manifest.SignedManifest) error {
   139  	manifestURL, err := r.ub.BuildManifestURL(name, tag)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(manifest.Raw))
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	response, err := http.DefaultClient.Do(putRequest)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	defer response.Body.Close()
   154  
   155  	// TODO(bbland): handle other status codes, like 5xx errors
   156  	switch {
   157  	case response.StatusCode == http.StatusOK || response.StatusCode == http.StatusAccepted:
   158  		return nil
   159  	case response.StatusCode >= 400 && response.StatusCode < 500:
   160  		var errors v2.Errors
   161  		decoder := json.NewDecoder(response.Body)
   162  		err = decoder.Decode(&errors)
   163  		if err != nil {
   164  			return err
   165  		}
   166  
   167  		return &errors
   168  	default:
   169  		return &UnexpectedHTTPStatusError{Status: response.Status}
   170  	}
   171  }
   172  
   173  func (r *clientImpl) DeleteImage(name, tag string) error {
   174  	manifestURL, err := r.ub.BuildManifestURL(name, tag)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	deleteRequest, err := http.NewRequest("DELETE", manifestURL, nil)
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	response, err := http.DefaultClient.Do(deleteRequest)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	defer response.Body.Close()
   189  
   190  	// TODO(bbland): handle other status codes, like 5xx errors
   191  	switch {
   192  	case response.StatusCode == http.StatusNoContent:
   193  		break
   194  	case response.StatusCode == http.StatusNotFound:
   195  		return &ImageManifestNotFoundError{Name: name, Tag: tag}
   196  	case response.StatusCode >= 400 && response.StatusCode < 500:
   197  		var errs v2.Errors
   198  		decoder := json.NewDecoder(response.Body)
   199  		err = decoder.Decode(&errs)
   200  		if err != nil {
   201  			return err
   202  		}
   203  		return &errs
   204  	default:
   205  		return &UnexpectedHTTPStatusError{Status: response.Status}
   206  	}
   207  
   208  	return nil
   209  }
   210  
   211  func (r *clientImpl) ListImageTags(name string) ([]string, error) {
   212  	tagsURL, err := r.ub.BuildTagsURL(name)
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	response, err := http.Get(tagsURL)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  	defer response.Body.Close()
   222  
   223  	// TODO(bbland): handle other status codes, like 5xx errors
   224  	switch {
   225  	case response.StatusCode == http.StatusOK:
   226  		break
   227  	case response.StatusCode == http.StatusNotFound:
   228  		return nil, &RepositoryNotFoundError{Name: name}
   229  	case response.StatusCode >= 400 && response.StatusCode < 500:
   230  		var errs v2.Errors
   231  		decoder := json.NewDecoder(response.Body)
   232  		err = decoder.Decode(&errs)
   233  		if err != nil {
   234  			return nil, err
   235  		}
   236  		return nil, &errs
   237  	default:
   238  		return nil, &UnexpectedHTTPStatusError{Status: response.Status}
   239  	}
   240  
   241  	tags := struct {
   242  		Tags []string `json:"tags"`
   243  	}{}
   244  
   245  	decoder := json.NewDecoder(response.Body)
   246  	err = decoder.Decode(&tags)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  
   251  	return tags.Tags, nil
   252  }
   253  
   254  func (r *clientImpl) BlobLength(name string, dgst digest.Digest) (int, error) {
   255  	blobURL, err := r.ub.BuildBlobURL(name, dgst)
   256  	if err != nil {
   257  		return -1, err
   258  	}
   259  
   260  	response, err := http.Head(blobURL)
   261  	if err != nil {
   262  		return -1, err
   263  	}
   264  	defer response.Body.Close()
   265  
   266  	// TODO(bbland): handle other status codes, like 5xx errors
   267  	switch {
   268  	case response.StatusCode == http.StatusOK:
   269  		lengthHeader := response.Header.Get("Content-Length")
   270  		length, err := strconv.ParseInt(lengthHeader, 10, 64)
   271  		if err != nil {
   272  			return -1, err
   273  		}
   274  		return int(length), nil
   275  	case response.StatusCode == http.StatusNotFound:
   276  		return -1, nil
   277  	case response.StatusCode >= 400 && response.StatusCode < 500:
   278  		var errs v2.Errors
   279  		decoder := json.NewDecoder(response.Body)
   280  		err = decoder.Decode(&errs)
   281  		if err != nil {
   282  			return -1, err
   283  		}
   284  		return -1, &errs
   285  	default:
   286  		return -1, &UnexpectedHTTPStatusError{Status: response.Status}
   287  	}
   288  }
   289  
   290  func (r *clientImpl) GetBlob(name string, dgst digest.Digest, byteOffset int) (io.ReadCloser, int, error) {
   291  	blobURL, err := r.ub.BuildBlobURL(name, dgst)
   292  	if err != nil {
   293  		return nil, 0, err
   294  	}
   295  
   296  	getRequest, err := http.NewRequest("GET", blobURL, nil)
   297  	if err != nil {
   298  		return nil, 0, err
   299  	}
   300  
   301  	getRequest.Header.Add("Range", fmt.Sprintf("%d-", byteOffset))
   302  	response, err := http.DefaultClient.Do(getRequest)
   303  	if err != nil {
   304  		return nil, 0, err
   305  	}
   306  
   307  	// TODO(bbland): handle other status codes, like 5xx errors
   308  	switch {
   309  	case response.StatusCode == http.StatusOK:
   310  		lengthHeader := response.Header.Get("Content-Length")
   311  		length, err := strconv.ParseInt(lengthHeader, 10, 0)
   312  		if err != nil {
   313  			return nil, 0, err
   314  		}
   315  		return response.Body, int(length), nil
   316  	case response.StatusCode == http.StatusNotFound:
   317  		response.Body.Close()
   318  		return nil, 0, &BlobNotFoundError{Name: name, Digest: dgst}
   319  	case response.StatusCode >= 400 && response.StatusCode < 500:
   320  		var errs v2.Errors
   321  		decoder := json.NewDecoder(response.Body)
   322  		err = decoder.Decode(&errs)
   323  		if err != nil {
   324  			return nil, 0, err
   325  		}
   326  		return nil, 0, &errs
   327  	default:
   328  		response.Body.Close()
   329  		return nil, 0, &UnexpectedHTTPStatusError{Status: response.Status}
   330  	}
   331  }
   332  
   333  func (r *clientImpl) InitiateBlobUpload(name string) (string, error) {
   334  	uploadURL, err := r.ub.BuildBlobUploadURL(name)
   335  	if err != nil {
   336  		return "", err
   337  	}
   338  
   339  	postRequest, err := http.NewRequest("POST", uploadURL, nil)
   340  	if err != nil {
   341  		return "", err
   342  	}
   343  
   344  	response, err := http.DefaultClient.Do(postRequest)
   345  	if err != nil {
   346  		return "", err
   347  	}
   348  	defer response.Body.Close()
   349  
   350  	// TODO(bbland): handle other status codes, like 5xx errors
   351  	switch {
   352  	case response.StatusCode == http.StatusAccepted:
   353  		return response.Header.Get("Location"), nil
   354  	// case response.StatusCode == http.StatusNotFound:
   355  	// return
   356  	case response.StatusCode >= 400 && response.StatusCode < 500:
   357  		var errs v2.Errors
   358  		decoder := json.NewDecoder(response.Body)
   359  		err = decoder.Decode(&errs)
   360  		if err != nil {
   361  			return "", err
   362  		}
   363  		return "", &errs
   364  	default:
   365  		return "", &UnexpectedHTTPStatusError{Status: response.Status}
   366  	}
   367  }
   368  
   369  func (r *clientImpl) GetBlobUploadStatus(location string) (int, int, error) {
   370  	response, err := http.Get(location)
   371  	if err != nil {
   372  		return 0, 0, err
   373  	}
   374  	defer response.Body.Close()
   375  
   376  	// TODO(bbland): handle other status codes, like 5xx errors
   377  	switch {
   378  	case response.StatusCode == http.StatusNoContent:
   379  		return parseRangeHeader(response.Header.Get("Range"))
   380  	case response.StatusCode == http.StatusNotFound:
   381  		return 0, 0, &BlobUploadNotFoundError{Location: location}
   382  	case response.StatusCode >= 400 && response.StatusCode < 500:
   383  		var errs v2.Errors
   384  		decoder := json.NewDecoder(response.Body)
   385  		err = decoder.Decode(&errs)
   386  		if err != nil {
   387  			return 0, 0, err
   388  		}
   389  		return 0, 0, &errs
   390  	default:
   391  		return 0, 0, &UnexpectedHTTPStatusError{Status: response.Status}
   392  	}
   393  }
   394  
   395  func (r *clientImpl) UploadBlob(location string, blob io.ReadCloser, length int, dgst digest.Digest) error {
   396  	defer blob.Close()
   397  
   398  	putRequest, err := http.NewRequest("PUT", location, blob)
   399  	if err != nil {
   400  		return err
   401  	}
   402  
   403  	values := putRequest.URL.Query()
   404  	values.Set("digest", dgst.String())
   405  	putRequest.URL.RawQuery = values.Encode()
   406  
   407  	putRequest.Header.Set("Content-Type", "application/octet-stream")
   408  	putRequest.Header.Set("Content-Length", fmt.Sprint(length))
   409  
   410  	response, err := http.DefaultClient.Do(putRequest)
   411  	if err != nil {
   412  		return err
   413  	}
   414  	defer response.Body.Close()
   415  
   416  	// TODO(bbland): handle other status codes, like 5xx errors
   417  	switch {
   418  	case response.StatusCode == http.StatusCreated:
   419  		return nil
   420  	case response.StatusCode == http.StatusNotFound:
   421  		return &BlobUploadNotFoundError{Location: location}
   422  	case response.StatusCode >= 400 && response.StatusCode < 500:
   423  		var errs v2.Errors
   424  		decoder := json.NewDecoder(response.Body)
   425  		err = decoder.Decode(&errs)
   426  		if err != nil {
   427  			return err
   428  		}
   429  		return &errs
   430  	default:
   431  		return &UnexpectedHTTPStatusError{Status: response.Status}
   432  	}
   433  }
   434  
   435  func (r *clientImpl) UploadBlobChunk(location string, blobChunk io.ReadCloser, length, startByte int) error {
   436  	defer blobChunk.Close()
   437  
   438  	putRequest, err := http.NewRequest("PUT", location, blobChunk)
   439  	if err != nil {
   440  		return err
   441  	}
   442  
   443  	endByte := startByte + length
   444  
   445  	putRequest.Header.Set("Content-Type", "application/octet-stream")
   446  	putRequest.Header.Set("Content-Length", fmt.Sprint(length))
   447  	putRequest.Header.Set("Content-Range",
   448  		fmt.Sprintf("%d-%d/%d", startByte, endByte, endByte))
   449  
   450  	response, err := http.DefaultClient.Do(putRequest)
   451  	if err != nil {
   452  		return err
   453  	}
   454  	defer response.Body.Close()
   455  
   456  	// TODO(bbland): handle other status codes, like 5xx errors
   457  	switch {
   458  	case response.StatusCode == http.StatusAccepted:
   459  		return nil
   460  	case response.StatusCode == http.StatusRequestedRangeNotSatisfiable:
   461  		lastValidRange, blobSize, err := parseRangeHeader(response.Header.Get("Range"))
   462  		if err != nil {
   463  			return err
   464  		}
   465  		return &BlobUploadInvalidRangeError{
   466  			Location:       location,
   467  			LastValidRange: lastValidRange,
   468  			BlobSize:       blobSize,
   469  		}
   470  	case response.StatusCode == http.StatusNotFound:
   471  		return &BlobUploadNotFoundError{Location: location}
   472  	case response.StatusCode >= 400 && response.StatusCode < 500:
   473  		var errs v2.Errors
   474  		decoder := json.NewDecoder(response.Body)
   475  		err = decoder.Decode(&errs)
   476  		if err != nil {
   477  			return err
   478  		}
   479  		return &errs
   480  	default:
   481  		return &UnexpectedHTTPStatusError{Status: response.Status}
   482  	}
   483  }
   484  
   485  func (r *clientImpl) FinishChunkedBlobUpload(location string, length int, dgst digest.Digest) error {
   486  	putRequest, err := http.NewRequest("PUT", location, nil)
   487  	if err != nil {
   488  		return err
   489  	}
   490  
   491  	values := putRequest.URL.Query()
   492  	values.Set("digest", dgst.String())
   493  	putRequest.URL.RawQuery = values.Encode()
   494  
   495  	putRequest.Header.Set("Content-Type", "application/octet-stream")
   496  	putRequest.Header.Set("Content-Length", "0")
   497  	putRequest.Header.Set("Content-Range",
   498  		fmt.Sprintf("%d-%d/%d", length, length, length))
   499  
   500  	response, err := http.DefaultClient.Do(putRequest)
   501  	if err != nil {
   502  		return err
   503  	}
   504  	defer response.Body.Close()
   505  
   506  	// TODO(bbland): handle other status codes, like 5xx errors
   507  	switch {
   508  	case response.StatusCode == http.StatusCreated:
   509  		return nil
   510  	case response.StatusCode == http.StatusNotFound:
   511  		return &BlobUploadNotFoundError{Location: location}
   512  	case response.StatusCode >= 400 && response.StatusCode < 500:
   513  		var errs v2.Errors
   514  		decoder := json.NewDecoder(response.Body)
   515  		err = decoder.Decode(&errs)
   516  		if err != nil {
   517  			return err
   518  		}
   519  		return &errs
   520  	default:
   521  		return &UnexpectedHTTPStatusError{Status: response.Status}
   522  	}
   523  }
   524  
   525  func (r *clientImpl) CancelBlobUpload(location string) error {
   526  	deleteRequest, err := http.NewRequest("DELETE", location, nil)
   527  	if err != nil {
   528  		return err
   529  	}
   530  
   531  	response, err := http.DefaultClient.Do(deleteRequest)
   532  	if err != nil {
   533  		return err
   534  	}
   535  	defer response.Body.Close()
   536  
   537  	// TODO(bbland): handle other status codes, like 5xx errors
   538  	switch {
   539  	case response.StatusCode == http.StatusNoContent:
   540  		return nil
   541  	case response.StatusCode == http.StatusNotFound:
   542  		return &BlobUploadNotFoundError{Location: location}
   543  	case response.StatusCode >= 400 && response.StatusCode < 500:
   544  		var errs v2.Errors
   545  		decoder := json.NewDecoder(response.Body)
   546  		err = decoder.Decode(&errs)
   547  		if err != nil {
   548  			return err
   549  		}
   550  		return &errs
   551  	default:
   552  		return &UnexpectedHTTPStatusError{Status: response.Status}
   553  	}
   554  }
   555  
   556  // parseRangeHeader parses out the offset and length from a returned Range
   557  // header
   558  func parseRangeHeader(byteRangeHeader string) (int, int, error) {
   559  	submatches := patternRangeHeader.FindStringSubmatch(byteRangeHeader)
   560  	if submatches == nil || len(submatches) < 3 {
   561  		return 0, 0, fmt.Errorf("Malformed Range header")
   562  	}
   563  
   564  	offset, err := strconv.Atoi(submatches[1])
   565  	if err != nil {
   566  		return 0, 0, err
   567  	}
   568  	length, err := strconv.Atoi(submatches[2])
   569  	if err != nil {
   570  		return 0, 0, err
   571  	}
   572  	return offset, length, nil
   573  }