github.com/mika/distribution@v2.2.2-0.20160108133430-a75790e3d8e0+incompatible/registry/client/repository.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"strconv"
    13  	"time"
    14  
    15  	"github.com/docker/distribution"
    16  	"github.com/docker/distribution/context"
    17  	"github.com/docker/distribution/digest"
    18  	"github.com/docker/distribution/reference"
    19  	"github.com/docker/distribution/registry/api/v2"
    20  	"github.com/docker/distribution/registry/client/transport"
    21  	"github.com/docker/distribution/registry/storage/cache"
    22  	"github.com/docker/distribution/registry/storage/cache/memory"
    23  )
    24  
    25  // Registry provides an interface for calling Repositories, which returns a catalog of repositories.
    26  type Registry interface {
    27  	Repositories(ctx context.Context, repos []string, last string) (n int, err error)
    28  }
    29  
    30  // NewRegistry creates a registry namespace which can be used to get a listing of repositories
    31  func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTripper) (Registry, error) {
    32  	ub, err := v2.NewURLBuilderFromString(baseURL)
    33  	if err != nil {
    34  		return nil, err
    35  	}
    36  
    37  	client := &http.Client{
    38  		Transport: transport,
    39  		Timeout:   1 * time.Minute,
    40  	}
    41  
    42  	return &registry{
    43  		client:  client,
    44  		ub:      ub,
    45  		context: ctx,
    46  	}, nil
    47  }
    48  
    49  type registry struct {
    50  	client  *http.Client
    51  	ub      *v2.URLBuilder
    52  	context context.Context
    53  }
    54  
    55  // Repositories returns a lexigraphically sorted catalog given a base URL.  The 'entries' slice will be filled up to the size
    56  // of the slice, starting at the value provided in 'last'.  The number of entries will be returned along with io.EOF if there
    57  // are no more entries
    58  func (r *registry) Repositories(ctx context.Context, entries []string, last string) (int, error) {
    59  	var numFilled int
    60  	var returnErr error
    61  
    62  	values := buildCatalogValues(len(entries), last)
    63  	u, err := r.ub.BuildCatalogURL(values)
    64  	if err != nil {
    65  		return 0, err
    66  	}
    67  
    68  	resp, err := r.client.Get(u)
    69  	if err != nil {
    70  		return 0, err
    71  	}
    72  	defer resp.Body.Close()
    73  
    74  	if SuccessStatus(resp.StatusCode) {
    75  		var ctlg struct {
    76  			Repositories []string `json:"repositories"`
    77  		}
    78  		decoder := json.NewDecoder(resp.Body)
    79  
    80  		if err := decoder.Decode(&ctlg); err != nil {
    81  			return 0, err
    82  		}
    83  
    84  		for cnt := range ctlg.Repositories {
    85  			entries[cnt] = ctlg.Repositories[cnt]
    86  		}
    87  		numFilled = len(ctlg.Repositories)
    88  
    89  		link := resp.Header.Get("Link")
    90  		if link == "" {
    91  			returnErr = io.EOF
    92  		}
    93  	} else {
    94  		return 0, HandleErrorResponse(resp)
    95  	}
    96  
    97  	return numFilled, returnErr
    98  }
    99  
   100  // NewRepository creates a new Repository for the given repository name and base URL.
   101  func NewRepository(ctx context.Context, name, baseURL string, transport http.RoundTripper) (distribution.Repository, error) {
   102  	if _, err := reference.ParseNamed(name); err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	ub, err := v2.NewURLBuilderFromString(baseURL)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	client := &http.Client{
   112  		Transport: transport,
   113  		// TODO(dmcgowan): create cookie jar
   114  	}
   115  
   116  	return &repository{
   117  		client:  client,
   118  		ub:      ub,
   119  		name:    name,
   120  		context: ctx,
   121  	}, nil
   122  }
   123  
   124  type repository struct {
   125  	client  *http.Client
   126  	ub      *v2.URLBuilder
   127  	context context.Context
   128  	name    string
   129  }
   130  
   131  func (r *repository) Name() string {
   132  	return r.name
   133  }
   134  
   135  func (r *repository) Blobs(ctx context.Context) distribution.BlobStore {
   136  	statter := &blobStatter{
   137  		name:   r.Name(),
   138  		ub:     r.ub,
   139  		client: r.client,
   140  	}
   141  	return &blobs{
   142  		name:    r.Name(),
   143  		ub:      r.ub,
   144  		client:  r.client,
   145  		statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter),
   146  	}
   147  }
   148  
   149  func (r *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) {
   150  	// todo(richardscothern): options should be sent over the wire
   151  	return &manifests{
   152  		name:   r.Name(),
   153  		ub:     r.ub,
   154  		client: r.client,
   155  		etags:  make(map[string]string),
   156  	}, nil
   157  }
   158  
   159  func (r *repository) Tags(ctx context.Context) distribution.TagService {
   160  	return &tags{
   161  		client:  r.client,
   162  		ub:      r.ub,
   163  		context: r.context,
   164  		name:    r.Name(),
   165  	}
   166  }
   167  
   168  // tags implements remote tagging operations.
   169  type tags struct {
   170  	client  *http.Client
   171  	ub      *v2.URLBuilder
   172  	context context.Context
   173  	name    string
   174  }
   175  
   176  // All returns all tags
   177  func (t *tags) All(ctx context.Context) ([]string, error) {
   178  	var tags []string
   179  
   180  	u, err := t.ub.BuildTagsURL(t.name)
   181  	if err != nil {
   182  		return tags, err
   183  	}
   184  
   185  	resp, err := t.client.Get(u)
   186  	if err != nil {
   187  		return tags, err
   188  	}
   189  	defer resp.Body.Close()
   190  
   191  	if SuccessStatus(resp.StatusCode) {
   192  		b, err := ioutil.ReadAll(resp.Body)
   193  		if err != nil {
   194  			return tags, err
   195  		}
   196  
   197  		tagsResponse := struct {
   198  			Tags []string `json:"tags"`
   199  		}{}
   200  		if err := json.Unmarshal(b, &tagsResponse); err != nil {
   201  			return tags, err
   202  		}
   203  		tags = tagsResponse.Tags
   204  		return tags, nil
   205  	}
   206  	return tags, HandleErrorResponse(resp)
   207  }
   208  
   209  func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) {
   210  	desc := distribution.Descriptor{}
   211  	headers := response.Header
   212  
   213  	ctHeader := headers.Get("Content-Type")
   214  	if ctHeader == "" {
   215  		return distribution.Descriptor{}, errors.New("missing or empty Content-Type header")
   216  	}
   217  	desc.MediaType = ctHeader
   218  
   219  	digestHeader := headers.Get("Docker-Content-Digest")
   220  	if digestHeader == "" {
   221  		bytes, err := ioutil.ReadAll(response.Body)
   222  		if err != nil {
   223  			return distribution.Descriptor{}, err
   224  		}
   225  		_, desc, err := distribution.UnmarshalManifest(ctHeader, bytes)
   226  		if err != nil {
   227  			return distribution.Descriptor{}, err
   228  		}
   229  		return desc, nil
   230  	}
   231  
   232  	dgst, err := digest.ParseDigest(digestHeader)
   233  	if err != nil {
   234  		return distribution.Descriptor{}, err
   235  	}
   236  	desc.Digest = dgst
   237  
   238  	lengthHeader := headers.Get("Content-Length")
   239  	if lengthHeader == "" {
   240  		return distribution.Descriptor{}, errors.New("missing or empty Content-Length header")
   241  	}
   242  	length, err := strconv.ParseInt(lengthHeader, 10, 64)
   243  	if err != nil {
   244  		return distribution.Descriptor{}, err
   245  	}
   246  	desc.Size = length
   247  
   248  	return desc, nil
   249  
   250  }
   251  
   252  // Get issues a HEAD request for a Manifest against its named endpoint in order
   253  // to construct a descriptor for the tag.  If the registry doesn't support HEADing
   254  // a manifest, fallback to GET.
   255  func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) {
   256  	u, err := t.ub.BuildManifestURL(t.name, tag)
   257  	if err != nil {
   258  		return distribution.Descriptor{}, err
   259  	}
   260  	var attempts int
   261  	resp, err := t.client.Head(u)
   262  
   263  check:
   264  	if err != nil {
   265  		return distribution.Descriptor{}, err
   266  	}
   267  
   268  	switch {
   269  	case resp.StatusCode >= 200 && resp.StatusCode < 400:
   270  		return descriptorFromResponse(resp)
   271  	case resp.StatusCode == http.StatusMethodNotAllowed:
   272  		resp, err = t.client.Get(u)
   273  		attempts++
   274  		if attempts > 1 {
   275  			return distribution.Descriptor{}, err
   276  		}
   277  		goto check
   278  	default:
   279  		return distribution.Descriptor{}, HandleErrorResponse(resp)
   280  	}
   281  }
   282  
   283  func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) {
   284  	panic("not implemented")
   285  }
   286  
   287  func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error {
   288  	panic("not implemented")
   289  }
   290  
   291  func (t *tags) Untag(ctx context.Context, tag string) error {
   292  	panic("not implemented")
   293  }
   294  
   295  type manifests struct {
   296  	name   string
   297  	ub     *v2.URLBuilder
   298  	client *http.Client
   299  	etags  map[string]string
   300  }
   301  
   302  func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) {
   303  	u, err := ms.ub.BuildManifestURL(ms.name, dgst.String())
   304  	if err != nil {
   305  		return false, err
   306  	}
   307  
   308  	resp, err := ms.client.Head(u)
   309  	if err != nil {
   310  		return false, err
   311  	}
   312  
   313  	if SuccessStatus(resp.StatusCode) {
   314  		return true, nil
   315  	} else if resp.StatusCode == http.StatusNotFound {
   316  		return false, nil
   317  	}
   318  	return false, HandleErrorResponse(resp)
   319  }
   320  
   321  // AddEtagToTag allows a client to supply an eTag to Get which will be
   322  // used for a conditional HTTP request.  If the eTag matches, a nil manifest
   323  // and ErrManifestNotModified error will be returned. etag is automatically
   324  // quoted when added to this map.
   325  func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption {
   326  	return etagOption{tag, etag}
   327  }
   328  
   329  type etagOption struct{ tag, etag string }
   330  
   331  func (o etagOption) Apply(ms distribution.ManifestService) error {
   332  	if ms, ok := ms.(*manifests); ok {
   333  		ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag)
   334  		return nil
   335  	}
   336  	return fmt.Errorf("etag options is a client-only option")
   337  }
   338  
   339  func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) {
   340  
   341  	var tag string
   342  	for _, option := range options {
   343  		if opt, ok := option.(withTagOption); ok {
   344  			tag = opt.tag
   345  		} else {
   346  			err := option.Apply(ms)
   347  			if err != nil {
   348  				return nil, err
   349  			}
   350  		}
   351  	}
   352  
   353  	var ref string
   354  	if tag != "" {
   355  		ref = tag
   356  	} else {
   357  		ref = dgst.String()
   358  	}
   359  
   360  	u, err := ms.ub.BuildManifestURL(ms.name, ref)
   361  	if err != nil {
   362  		return nil, err
   363  	}
   364  
   365  	req, err := http.NewRequest("GET", u, nil)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  
   370  	for _, t := range distribution.ManifestMediaTypes() {
   371  		req.Header.Add("Accept", t)
   372  	}
   373  
   374  	if _, ok := ms.etags[ref]; ok {
   375  		req.Header.Set("If-None-Match", ms.etags[ref])
   376  	}
   377  
   378  	resp, err := ms.client.Do(req)
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  	defer resp.Body.Close()
   383  	if resp.StatusCode == http.StatusNotModified {
   384  		return nil, distribution.ErrManifestNotModified
   385  	} else if SuccessStatus(resp.StatusCode) {
   386  		mt := resp.Header.Get("Content-Type")
   387  		body, err := ioutil.ReadAll(resp.Body)
   388  
   389  		if err != nil {
   390  			return nil, err
   391  		}
   392  		m, _, err := distribution.UnmarshalManifest(mt, body)
   393  		if err != nil {
   394  			return nil, err
   395  		}
   396  		return m, nil
   397  	}
   398  	return nil, HandleErrorResponse(resp)
   399  }
   400  
   401  // WithTag allows a tag to be passed into Put which enables the client
   402  // to build a correct URL.
   403  func WithTag(tag string) distribution.ManifestServiceOption {
   404  	return withTagOption{tag}
   405  }
   406  
   407  type withTagOption struct{ tag string }
   408  
   409  func (o withTagOption) Apply(m distribution.ManifestService) error {
   410  	if _, ok := m.(*manifests); ok {
   411  		return nil
   412  	}
   413  	return fmt.Errorf("withTagOption is a client-only option")
   414  }
   415  
   416  // Put puts a manifest.  A tag can be specified using an options parameter which uses some shared state to hold the
   417  // tag name in order to build the correct upload URL.  This state is written and read under a lock.
   418  func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) {
   419  	var tag string
   420  
   421  	for _, option := range options {
   422  		if opt, ok := option.(withTagOption); ok {
   423  			tag = opt.tag
   424  		} else {
   425  			err := option.Apply(ms)
   426  			if err != nil {
   427  				return "", err
   428  			}
   429  		}
   430  	}
   431  
   432  	manifestURL, err := ms.ub.BuildManifestURL(ms.name, tag)
   433  	if err != nil {
   434  		return "", err
   435  	}
   436  
   437  	mediaType, p, err := m.Payload()
   438  	if err != nil {
   439  		return "", err
   440  	}
   441  
   442  	putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p))
   443  	if err != nil {
   444  		return "", err
   445  	}
   446  
   447  	putRequest.Header.Set("Content-Type", mediaType)
   448  
   449  	resp, err := ms.client.Do(putRequest)
   450  	if err != nil {
   451  		return "", err
   452  	}
   453  	defer resp.Body.Close()
   454  
   455  	if SuccessStatus(resp.StatusCode) {
   456  		dgstHeader := resp.Header.Get("Docker-Content-Digest")
   457  		dgst, err := digest.ParseDigest(dgstHeader)
   458  		if err != nil {
   459  			return "", err
   460  		}
   461  
   462  		return dgst, nil
   463  	}
   464  
   465  	return "", HandleErrorResponse(resp)
   466  }
   467  
   468  func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error {
   469  	u, err := ms.ub.BuildManifestURL(ms.name, dgst.String())
   470  	if err != nil {
   471  		return err
   472  	}
   473  	req, err := http.NewRequest("DELETE", u, nil)
   474  	if err != nil {
   475  		return err
   476  	}
   477  
   478  	resp, err := ms.client.Do(req)
   479  	if err != nil {
   480  		return err
   481  	}
   482  	defer resp.Body.Close()
   483  
   484  	if SuccessStatus(resp.StatusCode) {
   485  		return nil
   486  	}
   487  	return HandleErrorResponse(resp)
   488  }
   489  
   490  // todo(richardscothern): Restore interface and implementation with merge of #1050
   491  /*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) {
   492  	panic("not supported")
   493  }*/
   494  
   495  type blobs struct {
   496  	name   string
   497  	ub     *v2.URLBuilder
   498  	client *http.Client
   499  
   500  	statter distribution.BlobDescriptorService
   501  	distribution.BlobDeleter
   502  }
   503  
   504  func sanitizeLocation(location, base string) (string, error) {
   505  	baseURL, err := url.Parse(base)
   506  	if err != nil {
   507  		return "", err
   508  	}
   509  
   510  	locationURL, err := url.Parse(location)
   511  	if err != nil {
   512  		return "", err
   513  	}
   514  
   515  	return baseURL.ResolveReference(locationURL).String(), nil
   516  }
   517  
   518  func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
   519  	return bs.statter.Stat(ctx, dgst)
   520  
   521  }
   522  
   523  func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
   524  	reader, err := bs.Open(ctx, dgst)
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  	defer reader.Close()
   529  
   530  	return ioutil.ReadAll(reader)
   531  }
   532  
   533  func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
   534  	blobURL, err := bs.ub.BuildBlobURL(bs.name, dgst)
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  
   539  	return transport.NewHTTPReadSeeker(bs.client, blobURL,
   540  		func(resp *http.Response) error {
   541  			if resp.StatusCode == http.StatusNotFound {
   542  				return distribution.ErrBlobUnknown
   543  			}
   544  			return HandleErrorResponse(resp)
   545  		}), nil
   546  }
   547  
   548  func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
   549  	panic("not implemented")
   550  }
   551  
   552  func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
   553  	writer, err := bs.Create(ctx)
   554  	if err != nil {
   555  		return distribution.Descriptor{}, err
   556  	}
   557  	dgstr := digest.Canonical.New()
   558  	n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash()))
   559  	if err != nil {
   560  		return distribution.Descriptor{}, err
   561  	}
   562  	if n < int64(len(p)) {
   563  		return distribution.Descriptor{}, fmt.Errorf("short copy: wrote %d of %d", n, len(p))
   564  	}
   565  
   566  	desc := distribution.Descriptor{
   567  		MediaType: mediaType,
   568  		Size:      int64(len(p)),
   569  		Digest:    dgstr.Digest(),
   570  	}
   571  
   572  	return writer.Commit(ctx, desc)
   573  }
   574  
   575  func (bs *blobs) Create(ctx context.Context) (distribution.BlobWriter, error) {
   576  	u, err := bs.ub.BuildBlobUploadURL(bs.name)
   577  
   578  	resp, err := bs.client.Post(u, "", nil)
   579  	if err != nil {
   580  		return nil, err
   581  	}
   582  	defer resp.Body.Close()
   583  
   584  	if SuccessStatus(resp.StatusCode) {
   585  		// TODO(dmcgowan): Check for invalid UUID
   586  		uuid := resp.Header.Get("Docker-Upload-UUID")
   587  		location, err := sanitizeLocation(resp.Header.Get("Location"), u)
   588  		if err != nil {
   589  			return nil, err
   590  		}
   591  
   592  		return &httpBlobUpload{
   593  			statter:   bs.statter,
   594  			client:    bs.client,
   595  			uuid:      uuid,
   596  			startedAt: time.Now(),
   597  			location:  location,
   598  		}, nil
   599  	}
   600  	return nil, HandleErrorResponse(resp)
   601  }
   602  
   603  func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
   604  	panic("not implemented")
   605  }
   606  
   607  func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error {
   608  	return bs.statter.Clear(ctx, dgst)
   609  }
   610  
   611  type blobStatter struct {
   612  	name   string
   613  	ub     *v2.URLBuilder
   614  	client *http.Client
   615  }
   616  
   617  func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
   618  	u, err := bs.ub.BuildBlobURL(bs.name, dgst)
   619  	if err != nil {
   620  		return distribution.Descriptor{}, err
   621  	}
   622  
   623  	resp, err := bs.client.Head(u)
   624  	if err != nil {
   625  		return distribution.Descriptor{}, err
   626  	}
   627  	defer resp.Body.Close()
   628  
   629  	if SuccessStatus(resp.StatusCode) {
   630  		lengthHeader := resp.Header.Get("Content-Length")
   631  		if lengthHeader == "" {
   632  			return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u)
   633  		}
   634  
   635  		length, err := strconv.ParseInt(lengthHeader, 10, 64)
   636  		if err != nil {
   637  			return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err)
   638  		}
   639  
   640  		return distribution.Descriptor{
   641  			MediaType: resp.Header.Get("Content-Type"),
   642  			Size:      length,
   643  			Digest:    dgst,
   644  		}, nil
   645  	} else if resp.StatusCode == http.StatusNotFound {
   646  		return distribution.Descriptor{}, distribution.ErrBlobUnknown
   647  	}
   648  	return distribution.Descriptor{}, HandleErrorResponse(resp)
   649  }
   650  
   651  func buildCatalogValues(maxEntries int, last string) url.Values {
   652  	values := url.Values{}
   653  
   654  	if maxEntries > 0 {
   655  		values.Add("n", strconv.Itoa(maxEntries))
   656  	}
   657  
   658  	if last != "" {
   659  		values.Add("last", last)
   660  	}
   661  
   662  	return values
   663  }
   664  
   665  func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error {
   666  	blobURL, err := bs.ub.BuildBlobURL(bs.name, dgst)
   667  	if err != nil {
   668  		return err
   669  	}
   670  
   671  	req, err := http.NewRequest("DELETE", blobURL, nil)
   672  	if err != nil {
   673  		return err
   674  	}
   675  
   676  	resp, err := bs.client.Do(req)
   677  	if err != nil {
   678  		return err
   679  	}
   680  	defer resp.Body.Close()
   681  
   682  	if SuccessStatus(resp.StatusCode) {
   683  		return nil
   684  	}
   685  	return HandleErrorResponse(resp)
   686  }
   687  
   688  func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
   689  	return nil
   690  }