github.com/opcr-io/oras-go/v2@v2.0.0-20231122155130-eb4260d8a0ae/registry/remote/repository.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package remote
    17  
    18  import (
    19  	"bytes"
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"mime"
    26  	"net/http"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"sync/atomic"
    31  
    32  	"github.com/opcr-io/oras-go/v2/content"
    33  	"github.com/opcr-io/oras-go/v2/errdef"
    34  	"github.com/opcr-io/oras-go/v2/internal/cas"
    35  	"github.com/opcr-io/oras-go/v2/internal/httputil"
    36  	"github.com/opcr-io/oras-go/v2/internal/ioutil"
    37  	"github.com/opcr-io/oras-go/v2/internal/registryutil"
    38  	"github.com/opcr-io/oras-go/v2/internal/slices"
    39  	"github.com/opcr-io/oras-go/v2/internal/syncutil"
    40  	"github.com/opcr-io/oras-go/v2/registry"
    41  	"github.com/opcr-io/oras-go/v2/registry/remote/auth"
    42  	"github.com/opcr-io/oras-go/v2/registry/remote/errcode"
    43  	"github.com/opcr-io/oras-go/v2/registry/remote/internal/errutil"
    44  	"github.com/opencontainers/go-digest"
    45  	specs "github.com/opencontainers/image-spec/specs-go"
    46  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    47  )
    48  
    49  // dockerContentDigestHeader - The Docker-Content-Digest header, if present
    50  // on the response, returns the canonical digest of the uploaded blob.
    51  // See https://docs.docker.com/registry/spec/api/#digest-header
    52  // See https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pull
    53  const dockerContentDigestHeader = "Docker-Content-Digest"
    54  
    55  // Client is an interface for a HTTP client.
    56  type Client interface {
    57  	// Do sends an HTTP request and returns an HTTP response.
    58  	//
    59  	// Unlike http.RoundTripper, Client can attempt to interpret the response
    60  	// and handle higher-level protocol details such as redirects and
    61  	// authentication.
    62  	//
    63  	// Like http.RoundTripper, Client should not modify the request, and must
    64  	// always close the request body.
    65  	Do(*http.Request) (*http.Response, error)
    66  }
    67  
    68  // Repository is an HTTP client to a remote repository.
    69  type Repository struct {
    70  	// Client is the underlying HTTP client used to access the remote registry.
    71  	// If nil, auth.DefaultClient is used.
    72  	Client Client
    73  
    74  	// Reference references the remote repository.
    75  	Reference registry.Reference
    76  
    77  	// PlainHTTP signals the transport to access the remote repository via HTTP
    78  	// instead of HTTPS.
    79  	PlainHTTP bool
    80  
    81  	// ManifestMediaTypes is used in `Accept` header for resolving manifests
    82  	// from references. It is also used in identifying manifests and blobs from
    83  	// descriptors. If an empty list is present, default manifest media types
    84  	// are used.
    85  	ManifestMediaTypes []string
    86  
    87  	// TagListPageSize specifies the page size when invoking the tag list API.
    88  	// If zero, the page size is determined by the remote registry.
    89  	// Reference: https://docs.docker.com/registry/spec/api/#tags
    90  	TagListPageSize int
    91  
    92  	// ReferrerListPageSize specifies the page size when invoking the Referrers
    93  	// API.
    94  	// If zero, the page size is determined by the remote registry.
    95  	// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
    96  	ReferrerListPageSize int
    97  
    98  	// MaxMetadataBytes specifies a limit on how many response bytes are allowed
    99  	// in the server's response to the metadata APIs, such as catalog list, tag
   100  	// list, and referrers list.
   101  	// If less than or equal to zero, a default (currently 4MiB) is used.
   102  	MaxMetadataBytes int64
   103  
   104  	// NOTE: Must keep fields in sync with newRepositoryWithOptions function.
   105  
   106  	// referrersState represents that if the repository supports Referrers API.
   107  	// default: referrersStateUnknown
   108  	referrersState referrersState
   109  
   110  	// referrersPingLock locks the pingReferrers() method and allows only
   111  	// one go-routine to send the request.
   112  	referrersPingLock sync.Mutex
   113  
   114  	// referrersMergePool provides a way to manage concurrent updates to a
   115  	// referrers index tagged by referrers tag schema.
   116  	referrersMergePool syncutil.Pool[syncutil.Merge[referrerChange]]
   117  }
   118  
   119  // NewRepository creates a client to the remote repository identified by a
   120  // reference.
   121  // Example: localhost:5000/hello-world
   122  func NewRepository(reference string) (*Repository, error) {
   123  	ref, err := registry.ParseReference(reference)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	return &Repository{
   128  		Reference: ref,
   129  	}, nil
   130  }
   131  
   132  // newRepositoryWithOptions returns a Repository with the given Reference and
   133  // RepositoryOptions.
   134  //
   135  // RepositoryOptions are part of the Registry struct and set its defaults.
   136  // RepositoryOptions shares the same struct definition as Repository, which
   137  // contains unexported state that must not be copied to multiple Repositories.
   138  // To handle this we explicitly copy only the fields that we want to reproduce.
   139  func newRepositoryWithOptions(ref registry.Reference, opts *RepositoryOptions) (*Repository, error) {
   140  	if err := ref.ValidateRepository(); err != nil {
   141  		return nil, err
   142  	}
   143  	return &Repository{
   144  		Client:               opts.Client,
   145  		Reference:            ref,
   146  		PlainHTTP:            opts.PlainHTTP,
   147  		ManifestMediaTypes:   slices.Clone(opts.ManifestMediaTypes),
   148  		TagListPageSize:      opts.TagListPageSize,
   149  		ReferrerListPageSize: opts.ReferrerListPageSize,
   150  		MaxMetadataBytes:     opts.MaxMetadataBytes,
   151  	}, nil
   152  }
   153  
   154  // SetReferrersCapability indicates the Referrers API capability of the remote
   155  // repository. true: capable; false: not capable.
   156  //
   157  // SetReferrersCapability is valid only when it is called for the first time.
   158  // SetReferrersCapability returns ErrReferrersCapabilityAlreadySet if the
   159  // Referrers API capability has been already set.
   160  //   - When the capability is set to true, the Referrers() function will always
   161  //     request the Referrers API. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
   162  //   - When the capability is set to false, the Referrers() function will always
   163  //     request the Referrers Tag. Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema
   164  //   - When the capability is not set, the Referrers() function will automatically
   165  //     determine which API to use.
   166  func (r *Repository) SetReferrersCapability(capable bool) error {
   167  	var state referrersState
   168  	if capable {
   169  		state = referrersStateSupported
   170  	} else {
   171  		state = referrersStateUnsupported
   172  	}
   173  	if swapped := atomic.CompareAndSwapInt32(&r.referrersState, referrersStateUnknown, state); !swapped {
   174  		if fact := r.loadReferrersState(); fact != state {
   175  			return fmt.Errorf("%w: current capability = %v, new capability = %v",
   176  				ErrReferrersCapabilityAlreadySet,
   177  				fact == referrersStateSupported,
   178  				capable)
   179  		}
   180  	}
   181  	return nil
   182  }
   183  
   184  // setReferrersState atomically loads r.referrersState.
   185  func (r *Repository) loadReferrersState() referrersState {
   186  	return atomic.LoadInt32(&r.referrersState)
   187  }
   188  
   189  // client returns an HTTP client used to access the remote repository.
   190  // A default HTTP client is return if the client is not configured.
   191  func (r *Repository) client() Client {
   192  	if r.Client == nil {
   193  		return auth.DefaultClient
   194  	}
   195  	return r.Client
   196  }
   197  
   198  // blobStore detects the blob store for the given descriptor.
   199  func (r *Repository) blobStore(desc ocispec.Descriptor) registry.BlobStore {
   200  	if isManifest(r.ManifestMediaTypes, desc) {
   201  		return r.Manifests()
   202  	}
   203  	return r.Blobs()
   204  }
   205  
   206  // Fetch fetches the content identified by the descriptor.
   207  func (r *Repository) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
   208  	return r.blobStore(target).Fetch(ctx, target)
   209  }
   210  
   211  // Push pushes the content, matching the expected descriptor.
   212  func (r *Repository) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
   213  	return r.blobStore(expected).Push(ctx, expected, content)
   214  }
   215  
   216  // Exists returns true if the described content exists.
   217  func (r *Repository) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
   218  	return r.blobStore(target).Exists(ctx, target)
   219  }
   220  
   221  // Delete removes the content identified by the descriptor.
   222  func (r *Repository) Delete(ctx context.Context, target ocispec.Descriptor) error {
   223  	return r.blobStore(target).Delete(ctx, target)
   224  }
   225  
   226  // Blobs provides access to the blob CAS only, which contains config blobs,
   227  // layers, and other generic blobs.
   228  func (r *Repository) Blobs() registry.BlobStore {
   229  	return &blobStore{repo: r}
   230  }
   231  
   232  // Manifests provides access to the manifest CAS only.
   233  func (r *Repository) Manifests() registry.ManifestStore {
   234  	return &manifestStore{repo: r}
   235  }
   236  
   237  // Resolve resolves a reference to a manifest descriptor.
   238  // See also `ManifestMediaTypes`.
   239  func (r *Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
   240  	return r.Manifests().Resolve(ctx, reference)
   241  }
   242  
   243  // Tag tags a manifest descriptor with a reference string.
   244  func (r *Repository) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
   245  	return r.Manifests().Tag(ctx, desc, reference)
   246  }
   247  
   248  // PushReference pushes the manifest with a reference tag.
   249  func (r *Repository) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
   250  	return r.Manifests().PushReference(ctx, expected, content, reference)
   251  }
   252  
   253  // FetchReference fetches the manifest identified by the reference.
   254  // The reference can be a tag or digest.
   255  func (r *Repository) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) {
   256  	return r.Manifests().FetchReference(ctx, reference)
   257  }
   258  
   259  // ParseReference resolves a tag or a digest reference to a fully qualified
   260  // reference from a base reference r.Reference.
   261  // Tag, digest, or fully qualified references are accepted as input.
   262  //
   263  // If reference is a fully qualified reference, then ParseReference parses it
   264  // and returns the parsed reference. If the parsed reference does not share
   265  // the same base reference with the Repository r, ParseReference returns a
   266  // wrapped error ErrInvalidReference.
   267  func (r *Repository) ParseReference(reference string) (registry.Reference, error) {
   268  	ref, err := registry.ParseReference(reference)
   269  	if err != nil {
   270  		ref = registry.Reference{
   271  			Registry:   r.Reference.Registry,
   272  			Repository: r.Reference.Repository,
   273  			Reference:  reference,
   274  		}
   275  
   276  		// reference is not a FQDN
   277  		if index := strings.IndexByte(reference, '@'); index != -1 {
   278  			// `@` implies *digest*, so drop the *tag* (irrespective of what it is).
   279  			ref.Reference = reference[index+1:]
   280  			err = ref.ValidateReferenceAsDigest()
   281  		} else {
   282  			err = ref.ValidateReference()
   283  		}
   284  
   285  		if err != nil {
   286  			return registry.Reference{}, err
   287  		}
   288  	} else if ref.Registry != r.Reference.Registry || ref.Repository != r.Reference.Repository {
   289  		return registry.Reference{}, fmt.Errorf(
   290  			"%w: mismatch between received %q and expected %q",
   291  			errdef.ErrInvalidReference, ref, r.Reference,
   292  		)
   293  	}
   294  
   295  	if len(ref.Reference) == 0 {
   296  		return registry.Reference{}, errdef.ErrInvalidReference
   297  	}
   298  
   299  	return ref, nil
   300  }
   301  
   302  // Tags lists the tags available in the repository.
   303  // See also `TagListPageSize`.
   304  // If `last` is NOT empty, the entries in the response start after the
   305  // tag specified by `last`. Otherwise, the response starts from the top
   306  // of the Tags list.
   307  //
   308  // References:
   309  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#content-discovery
   310  //   - https://docs.docker.com/registry/spec/api/#tags
   311  func (r *Repository) Tags(ctx context.Context, last string, fn func(tags []string) error) error {
   312  	ctx = registryutil.WithScopeHint(ctx, r.Reference, auth.ActionPull)
   313  	url := buildRepositoryTagListURL(r.PlainHTTP, r.Reference)
   314  	var err error
   315  	for err == nil {
   316  		url, err = r.tags(ctx, last, fn, url)
   317  		// clear `last` for subsequent pages
   318  		last = ""
   319  	}
   320  	if err != errNoLink {
   321  		return err
   322  	}
   323  	return nil
   324  }
   325  
   326  // tags returns a single page of tag list with the next link.
   327  func (r *Repository) tags(ctx context.Context, last string, fn func(tags []string) error, url string) (string, error) {
   328  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   329  	if err != nil {
   330  		return "", err
   331  	}
   332  	if r.TagListPageSize > 0 || last != "" {
   333  		q := req.URL.Query()
   334  		if r.TagListPageSize > 0 {
   335  			q.Set("n", strconv.Itoa(r.TagListPageSize))
   336  		}
   337  		if last != "" {
   338  			q.Set("last", last)
   339  		}
   340  		req.URL.RawQuery = q.Encode()
   341  	}
   342  	resp, err := r.client().Do(req)
   343  	if err != nil {
   344  		return "", err
   345  	}
   346  	defer resp.Body.Close()
   347  
   348  	if resp.StatusCode != http.StatusOK {
   349  		return "", errutil.ParseErrorResponse(resp)
   350  	}
   351  	var page struct {
   352  		Tags []string `json:"tags"`
   353  	}
   354  	lr := limitReader(resp.Body, r.MaxMetadataBytes)
   355  	if err := json.NewDecoder(lr).Decode(&page); err != nil {
   356  		return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
   357  	}
   358  	if err := fn(page.Tags); err != nil {
   359  		return "", err
   360  	}
   361  
   362  	return parseLink(resp)
   363  }
   364  
   365  // Predecessors returns the descriptors of image or artifact manifests directly
   366  // referencing the given manifest descriptor.
   367  // Predecessors internally leverages Referrers.
   368  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
   369  func (r *Repository) Predecessors(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
   370  	var res []ocispec.Descriptor
   371  	if err := r.Referrers(ctx, desc, "", func(referrers []ocispec.Descriptor) error {
   372  		res = append(res, referrers...)
   373  		return nil
   374  	}); err != nil {
   375  		return nil, err
   376  	}
   377  	return res, nil
   378  }
   379  
   380  // Referrers lists the descriptors of image or artifact manifests directly
   381  // referencing the given manifest descriptor.
   382  //
   383  // fn is called for each page of the referrers result.
   384  // If artifactType is not empty, only referrers of the same artifact type are
   385  // fed to fn.
   386  //
   387  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
   388  func (r *Repository) Referrers(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
   389  	state := r.loadReferrersState()
   390  	if state == referrersStateUnsupported {
   391  		// The repository is known to not support Referrers API, fallback to
   392  		// referrers tag schema.
   393  		return r.referrersByTagSchema(ctx, desc, artifactType, fn)
   394  	}
   395  
   396  	err := r.referrersByAPI(ctx, desc, artifactType, fn)
   397  	if state == referrersStateSupported {
   398  		// The repository is known to support Referrers API, no fallback.
   399  		return err
   400  	}
   401  
   402  	// The referrers state is unknown.
   403  	if err != nil {
   404  		var errResp *errcode.ErrorResponse
   405  		if !errors.As(err, &errResp) || errResp.StatusCode != http.StatusNotFound {
   406  			return err
   407  		}
   408  		if errutil.IsErrorCode(errResp, errcode.ErrorCodeNameUnknown) {
   409  			// The repository is not found, no fallback.
   410  			return err
   411  		}
   412  		// A 404 returned by Referrers API indicates that Referrers API is
   413  		// not supported. Fallback to referrers tag schema.
   414  		r.SetReferrersCapability(false)
   415  		return r.referrersByTagSchema(ctx, desc, artifactType, fn)
   416  	}
   417  
   418  	r.SetReferrersCapability(true)
   419  	return nil
   420  }
   421  
   422  // referrersByAPI lists the descriptors of manifests directly referencing
   423  // the given manifest descriptor by requesting Referrers API.
   424  // fn is called for the referrers result. If artifactType is not empty,
   425  // only referrers of the same artifact type are fed to fn.
   426  func (r *Repository) referrersByAPI(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
   427  	ref := r.Reference
   428  	ref.Reference = desc.Digest.String()
   429  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
   430  
   431  	url := buildReferrersURL(r.PlainHTTP, ref, artifactType)
   432  	var err error
   433  	for err == nil {
   434  		url, err = r.referrersPageByAPI(ctx, artifactType, fn, url)
   435  	}
   436  	if err == errNoLink {
   437  		return nil
   438  	}
   439  	return err
   440  }
   441  
   442  // referrersPageByAPI lists a single page of the descriptors of manifests
   443  // directly referencing the given manifest descriptor. fn is called for
   444  // a page of referrersPageByAPI result.
   445  // If artifactType is not empty, only referrersPageByAPI of the same
   446  // artifact type are fed to fn.
   447  // referrersPageByAPI returns the link url for the next page.
   448  func (r *Repository) referrersPageByAPI(ctx context.Context, artifactType string, fn func(referrers []ocispec.Descriptor) error, url string) (string, error) {
   449  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   450  	if err != nil {
   451  		return "", err
   452  	}
   453  	if r.ReferrerListPageSize > 0 {
   454  		q := req.URL.Query()
   455  		q.Set("n", strconv.Itoa(r.ReferrerListPageSize))
   456  		req.URL.RawQuery = q.Encode()
   457  	}
   458  
   459  	resp, err := r.client().Do(req)
   460  	if err != nil {
   461  		return "", err
   462  	}
   463  	defer resp.Body.Close()
   464  
   465  	if resp.StatusCode != http.StatusOK {
   466  		return "", errutil.ParseErrorResponse(resp)
   467  	}
   468  
   469  	var index ocispec.Index
   470  	lr := limitReader(resp.Body, r.MaxMetadataBytes)
   471  	if err := json.NewDecoder(lr).Decode(&index); err != nil {
   472  		return "", fmt.Errorf("%s %q: failed to decode response: %w", resp.Request.Method, resp.Request.URL, err)
   473  	}
   474  	referrers := index.Manifests
   475  	if artifactType != "" && !isReferrersFilterApplied(index.Annotations, "artifactType") {
   476  		// perform client side filtering if the filter is not applied on the server side
   477  		referrers = filterReferrers(referrers, artifactType)
   478  	}
   479  	if len(referrers) > 0 {
   480  		if err := fn(referrers); err != nil {
   481  			return "", err
   482  		}
   483  	}
   484  	return parseLink(resp)
   485  }
   486  
   487  // referrersByTagSchema lists the descriptors of manifests directly
   488  // referencing the given manifest descriptor by requesting referrers tag.
   489  // fn is called for the referrers result. If artifactType is not empty,
   490  // only referrers of the same artifact type are fed to fn.
   491  // reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#backwards-compatibility
   492  func (r *Repository) referrersByTagSchema(ctx context.Context, desc ocispec.Descriptor, artifactType string, fn func(referrers []ocispec.Descriptor) error) error {
   493  	referrersTag := buildReferrersTag(desc)
   494  	_, referrers, err := r.referrersFromIndex(ctx, referrersTag)
   495  	if err != nil {
   496  		if errors.Is(err, errdef.ErrNotFound) {
   497  			// no referrers to the manifest
   498  			return nil
   499  		}
   500  		return err
   501  	}
   502  
   503  	filtered := filterReferrers(referrers, artifactType)
   504  	if len(filtered) == 0 {
   505  		return nil
   506  	}
   507  	return fn(filtered)
   508  }
   509  
   510  // referrersFromIndex queries the referrers index using the the given referrers
   511  // tag. If Succeeded, returns the descriptor of referrers index and the
   512  // referrers list.
   513  func (r *Repository) referrersFromIndex(ctx context.Context, referrersTag string) (ocispec.Descriptor, []ocispec.Descriptor, error) {
   514  	desc, rc, err := r.FetchReference(ctx, referrersTag)
   515  	if err != nil {
   516  		return ocispec.Descriptor{}, nil, err
   517  	}
   518  	defer rc.Close()
   519  
   520  	if err := limitSize(desc, r.MaxMetadataBytes); err != nil {
   521  		return ocispec.Descriptor{}, nil, fmt.Errorf("failed to read referrers index from referrers tag %s: %w", referrersTag, err)
   522  	}
   523  	var index ocispec.Index
   524  	if err := decodeJSON(rc, desc, &index); err != nil {
   525  		return ocispec.Descriptor{}, nil, fmt.Errorf("failed to decode referrers index from referrers tag %s: %w", referrersTag, err)
   526  	}
   527  
   528  	return desc, index.Manifests, nil
   529  }
   530  
   531  // pingReferrers returns true if the Referrers API is available for r.
   532  func (r *Repository) pingReferrers(ctx context.Context) (bool, error) {
   533  	switch r.loadReferrersState() {
   534  	case referrersStateSupported:
   535  		return true, nil
   536  	case referrersStateUnsupported:
   537  		return false, nil
   538  	}
   539  
   540  	// referrers state is unknown
   541  	// limit the rate of pinging referrers API
   542  	r.referrersPingLock.Lock()
   543  	defer r.referrersPingLock.Unlock()
   544  
   545  	switch r.loadReferrersState() {
   546  	case referrersStateSupported:
   547  		return true, nil
   548  	case referrersStateUnsupported:
   549  		return false, nil
   550  	}
   551  
   552  	ref := r.Reference
   553  	ref.Reference = zeroDigest
   554  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
   555  
   556  	url := buildReferrersURL(r.PlainHTTP, ref, "")
   557  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   558  	if err != nil {
   559  		return false, err
   560  	}
   561  	resp, err := r.client().Do(req)
   562  	if err != nil {
   563  		return false, err
   564  	}
   565  	defer resp.Body.Close()
   566  
   567  	switch resp.StatusCode {
   568  	case http.StatusOK:
   569  		r.SetReferrersCapability(true)
   570  		return true, nil
   571  	case http.StatusNotFound:
   572  		if err := errutil.ParseErrorResponse(resp); errutil.IsErrorCode(err, errcode.ErrorCodeNameUnknown) {
   573  			// repository not found
   574  			return false, err
   575  		}
   576  		r.SetReferrersCapability(false)
   577  		return false, nil
   578  	default:
   579  		return false, errutil.ParseErrorResponse(resp)
   580  	}
   581  }
   582  
   583  // delete removes the content identified by the descriptor in the entity "blobs"
   584  // or "manifests".
   585  func (r *Repository) delete(ctx context.Context, target ocispec.Descriptor, isManifest bool) error {
   586  	ref := r.Reference
   587  	ref.Reference = target.Digest.String()
   588  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionDelete)
   589  	buildURL := buildRepositoryBlobURL
   590  	if isManifest {
   591  		buildURL = buildRepositoryManifestURL
   592  	}
   593  	url := buildURL(r.PlainHTTP, ref)
   594  	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
   595  	if err != nil {
   596  		return err
   597  	}
   598  
   599  	resp, err := r.client().Do(req)
   600  	if err != nil {
   601  		return err
   602  	}
   603  	defer resp.Body.Close()
   604  
   605  	switch resp.StatusCode {
   606  	case http.StatusAccepted:
   607  		return verifyContentDigest(resp, target.Digest)
   608  	case http.StatusNotFound:
   609  		return fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
   610  	default:
   611  		return errutil.ParseErrorResponse(resp)
   612  	}
   613  }
   614  
   615  // blobStore accesses the blob part of the repository.
   616  type blobStore struct {
   617  	repo *Repository
   618  }
   619  
   620  // Fetch fetches the content identified by the descriptor.
   621  func (s *blobStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) {
   622  	ref := s.repo.Reference
   623  	ref.Reference = target.Digest.String()
   624  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
   625  	url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
   626  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   627  	if err != nil {
   628  		return nil, err
   629  	}
   630  
   631  	resp, err := s.repo.client().Do(req)
   632  	if err != nil {
   633  		return nil, err
   634  	}
   635  	defer func() {
   636  		if err != nil {
   637  			resp.Body.Close()
   638  		}
   639  	}()
   640  
   641  	switch resp.StatusCode {
   642  	case http.StatusOK: // server does not support seek as `Range` was ignored.
   643  		if size := resp.ContentLength; size != -1 && size != target.Size {
   644  			return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
   645  		}
   646  
   647  		// check server range request capability.
   648  		// Docker spec allows range header form of "Range: bytes=<start>-<end>".
   649  		// However, the remote server may still not RFC 7233 compliant.
   650  		// Reference: https://docs.docker.com/registry/spec/api/#blob
   651  		if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" {
   652  			return httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, target.Size), nil
   653  		}
   654  		return resp.Body, nil
   655  	case http.StatusNotFound:
   656  		return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
   657  	default:
   658  		return nil, errutil.ParseErrorResponse(resp)
   659  	}
   660  }
   661  
   662  // Push pushes the content, matching the expected descriptor.
   663  // Existing content is not checked by Push() to minimize the number of out-going
   664  // requests.
   665  // Push is done by conventional 2-step monolithic upload instead of a single
   666  // `POST` request for better overall performance. It also allows early fail on
   667  // authentication errors.
   668  // References:
   669  // - https://docs.docker.com/registry/spec/api/#pushing-an-image
   670  // - https://docs.docker.com/registry/spec/api/#initiate-blob-upload
   671  // - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-a-blob-monolithically
   672  func (s *blobStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
   673  	// start an upload
   674  	// pushing usually requires both pull and push actions.
   675  	// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
   676  	ctx = registryutil.WithScopeHint(ctx, s.repo.Reference, auth.ActionPull, auth.ActionPush)
   677  	url := buildRepositoryBlobUploadURL(s.repo.PlainHTTP, s.repo.Reference)
   678  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, nil)
   679  	if err != nil {
   680  		return err
   681  	}
   682  	reqHostname := req.URL.Hostname()
   683  	reqPort := req.URL.Port()
   684  
   685  	client := s.repo.client()
   686  	resp, err := client.Do(req)
   687  	if err != nil {
   688  		return err
   689  	}
   690  
   691  	if resp.StatusCode != http.StatusAccepted {
   692  		defer resp.Body.Close()
   693  		return errutil.ParseErrorResponse(resp)
   694  	}
   695  	resp.Body.Close()
   696  
   697  	// monolithic upload
   698  	location, err := resp.Location()
   699  	if err != nil {
   700  		return err
   701  	}
   702  	// work-around solution for https://github.com/oras-project/oras-go/issues/177
   703  	// For some registries, if the port 443 is explicitly set to the hostname
   704  	// like registry.wabbit-networks.io:443/myrepo, blob push will fail since
   705  	// the hostname of the Location header in the response is set to
   706  	// registry.wabbit-networks.io instead of registry.wabbit-networks.io:443.
   707  	locationHostname := location.Hostname()
   708  	locationPort := location.Port()
   709  	// if location port 443 is missing, add it back
   710  	if reqPort == "443" && locationHostname == reqHostname && locationPort == "" {
   711  		location.Host = locationHostname + ":" + reqPort
   712  	}
   713  	url = location.String()
   714  	req, err = http.NewRequestWithContext(ctx, http.MethodPut, url, content)
   715  	if err != nil {
   716  		return err
   717  	}
   718  	if req.GetBody != nil && req.ContentLength != expected.Size {
   719  		// short circuit a size mismatch for built-in types.
   720  		return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size)
   721  	}
   722  	req.ContentLength = expected.Size
   723  	// the expected media type is ignored as in the API doc.
   724  	req.Header.Set("Content-Type", "application/octet-stream")
   725  	q := req.URL.Query()
   726  	q.Set("digest", expected.Digest.String())
   727  	req.URL.RawQuery = q.Encode()
   728  
   729  	// reuse credential from previous POST request
   730  	if auth := resp.Request.Header.Get("Authorization"); auth != "" {
   731  		req.Header.Set("Authorization", auth)
   732  	}
   733  	resp, err = client.Do(req)
   734  	if err != nil {
   735  		return err
   736  	}
   737  	defer resp.Body.Close()
   738  
   739  	if resp.StatusCode != http.StatusCreated {
   740  		return errutil.ParseErrorResponse(resp)
   741  	}
   742  	return nil
   743  }
   744  
   745  // Exists returns true if the described content exists.
   746  func (s *blobStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
   747  	_, err := s.Resolve(ctx, target.Digest.String())
   748  	if err == nil {
   749  		return true, nil
   750  	}
   751  	if errors.Is(err, errdef.ErrNotFound) {
   752  		return false, nil
   753  	}
   754  	return false, err
   755  }
   756  
   757  // Delete removes the content identified by the descriptor.
   758  func (s *blobStore) Delete(ctx context.Context, target ocispec.Descriptor) error {
   759  	return s.repo.delete(ctx, target, false)
   760  }
   761  
   762  // Resolve resolves a reference to a descriptor.
   763  func (s *blobStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
   764  	ref, err := s.repo.ParseReference(reference)
   765  	if err != nil {
   766  		return ocispec.Descriptor{}, err
   767  	}
   768  	refDigest, err := ref.Digest()
   769  	if err != nil {
   770  		return ocispec.Descriptor{}, err
   771  	}
   772  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
   773  	url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
   774  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
   775  	if err != nil {
   776  		return ocispec.Descriptor{}, err
   777  	}
   778  
   779  	resp, err := s.repo.client().Do(req)
   780  	if err != nil {
   781  		return ocispec.Descriptor{}, err
   782  	}
   783  	defer resp.Body.Close()
   784  
   785  	switch resp.StatusCode {
   786  	case http.StatusOK:
   787  		return generateBlobDescriptor(resp, refDigest)
   788  	case http.StatusNotFound:
   789  		return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
   790  	default:
   791  		return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp)
   792  	}
   793  }
   794  
   795  // FetchReference fetches the blob identified by the reference.
   796  // The reference must be a digest.
   797  func (s *blobStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) {
   798  	ref, err := s.repo.ParseReference(reference)
   799  	if err != nil {
   800  		return ocispec.Descriptor{}, nil, err
   801  	}
   802  	refDigest, err := ref.Digest()
   803  	if err != nil {
   804  		return ocispec.Descriptor{}, nil, err
   805  	}
   806  
   807  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
   808  	url := buildRepositoryBlobURL(s.repo.PlainHTTP, ref)
   809  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   810  	if err != nil {
   811  		return ocispec.Descriptor{}, nil, err
   812  	}
   813  
   814  	resp, err := s.repo.client().Do(req)
   815  	if err != nil {
   816  		return ocispec.Descriptor{}, nil, err
   817  	}
   818  	defer func() {
   819  		if err != nil {
   820  			resp.Body.Close()
   821  		}
   822  	}()
   823  
   824  	switch resp.StatusCode {
   825  	case http.StatusOK: // server does not support seek as `Range` was ignored.
   826  		if resp.ContentLength == -1 {
   827  			desc, err = s.Resolve(ctx, reference)
   828  		} else {
   829  			desc, err = generateBlobDescriptor(resp, refDigest)
   830  		}
   831  		if err != nil {
   832  			return ocispec.Descriptor{}, nil, err
   833  		}
   834  
   835  		// check server range request capability.
   836  		// Docker spec allows range header form of "Range: bytes=<start>-<end>".
   837  		// However, the remote server may still not RFC 7233 compliant.
   838  		// Reference: https://docs.docker.com/registry/spec/api/#blob
   839  		if rangeUnit := resp.Header.Get("Accept-Ranges"); rangeUnit == "bytes" {
   840  			return desc, httputil.NewReadSeekCloser(s.repo.client(), req, resp.Body, desc.Size), nil
   841  		}
   842  		return desc, resp.Body, nil
   843  	case http.StatusNotFound:
   844  		return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
   845  	default:
   846  		return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp)
   847  	}
   848  }
   849  
   850  // generateBlobDescriptor returns a descriptor generated from the response.
   851  func generateBlobDescriptor(resp *http.Response, refDigest digest.Digest) (ocispec.Descriptor, error) {
   852  	mediaType, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
   853  	if mediaType == "" {
   854  		mediaType = "application/octet-stream"
   855  	}
   856  
   857  	size := resp.ContentLength
   858  	if size == -1 {
   859  		return ocispec.Descriptor{}, fmt.Errorf("%s %q: unknown response Content-Length", resp.Request.Method, resp.Request.URL)
   860  	}
   861  
   862  	if err := verifyContentDigest(resp, refDigest); err != nil {
   863  		return ocispec.Descriptor{}, err
   864  	}
   865  
   866  	return ocispec.Descriptor{
   867  		MediaType: mediaType,
   868  		Digest:    refDigest,
   869  		Size:      size,
   870  	}, nil
   871  }
   872  
   873  // manifestStore accesses the manifest part of the repository.
   874  type manifestStore struct {
   875  	repo *Repository
   876  }
   877  
   878  // Fetch fetches the content identified by the descriptor.
   879  func (s *manifestStore) Fetch(ctx context.Context, target ocispec.Descriptor) (rc io.ReadCloser, err error) {
   880  	ref := s.repo.Reference
   881  	ref.Reference = target.Digest.String()
   882  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
   883  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
   884  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   885  	if err != nil {
   886  		return nil, err
   887  	}
   888  	req.Header.Set("Accept", target.MediaType)
   889  
   890  	resp, err := s.repo.client().Do(req)
   891  	if err != nil {
   892  		return nil, err
   893  	}
   894  	defer func() {
   895  		if err != nil {
   896  			resp.Body.Close()
   897  		}
   898  	}()
   899  
   900  	switch resp.StatusCode {
   901  	case http.StatusOK:
   902  		// no-op
   903  	case http.StatusNotFound:
   904  		return nil, fmt.Errorf("%s: %w", target.Digest, errdef.ErrNotFound)
   905  	default:
   906  		return nil, errutil.ParseErrorResponse(resp)
   907  	}
   908  	mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
   909  	if err != nil {
   910  		return nil, fmt.Errorf("%s %q: invalid response Content-Type: %w", resp.Request.Method, resp.Request.URL, err)
   911  	}
   912  	if mediaType != target.MediaType {
   913  		return nil, fmt.Errorf("%s %q: mismatch response Content-Type %q: expect %q", resp.Request.Method, resp.Request.URL, mediaType, target.MediaType)
   914  	}
   915  	if size := resp.ContentLength; size != -1 && size != target.Size {
   916  		return nil, fmt.Errorf("%s %q: mismatch Content-Length", resp.Request.Method, resp.Request.URL)
   917  	}
   918  	if err := verifyContentDigest(resp, target.Digest); err != nil {
   919  		return nil, err
   920  	}
   921  	return resp.Body, nil
   922  }
   923  
   924  // Push pushes the content, matching the expected descriptor.
   925  func (s *manifestStore) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
   926  	return s.pushWithIndexing(ctx, expected, content, expected.Digest.String())
   927  }
   928  
   929  // Exists returns true if the described content exists.
   930  func (s *manifestStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
   931  	_, err := s.Resolve(ctx, target.Digest.String())
   932  	if err == nil {
   933  		return true, nil
   934  	}
   935  	if errors.Is(err, errdef.ErrNotFound) {
   936  		return false, nil
   937  	}
   938  	return false, err
   939  }
   940  
   941  // Delete removes the manifest content identified by the descriptor.
   942  func (s *manifestStore) Delete(ctx context.Context, target ocispec.Descriptor) error {
   943  	return s.deleteWithIndexing(ctx, target)
   944  }
   945  
   946  // deleteWithIndexing removes the manifest content identified by the descriptor,
   947  // and indexes referrers for the manifest when needed.
   948  func (s *manifestStore) deleteWithIndexing(ctx context.Context, target ocispec.Descriptor) error {
   949  	if target.MediaType == ocispec.MediaTypeImageManifest {
   950  		if state := s.repo.loadReferrersState(); state == referrersStateSupported {
   951  			// referrers API is available, no client-side indexing needed
   952  			return s.repo.delete(ctx, target, true)
   953  		}
   954  
   955  		if err := limitSize(target, s.repo.MaxMetadataBytes); err != nil {
   956  			return err
   957  		}
   958  		manifestJSON, err := content.FetchAll(ctx, s, target)
   959  		if err != nil {
   960  			return err
   961  		}
   962  		if err := s.indexReferrersForDelete(ctx, target, manifestJSON); err != nil {
   963  			return err
   964  		}
   965  	}
   966  
   967  	return s.repo.delete(ctx, target, true)
   968  }
   969  
   970  // indexReferrersForDelete indexes referrers for image or artifact manifest with
   971  // the subject field on manifest delete.
   972  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests
   973  func (s *manifestStore) indexReferrersForDelete(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error {
   974  	var manifest struct {
   975  		Subject *ocispec.Descriptor `json:"subject"`
   976  	}
   977  	if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
   978  		return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
   979  	}
   980  	if manifest.Subject == nil {
   981  		// no subject, no indexing needed
   982  		return nil
   983  	}
   984  
   985  	subject := *manifest.Subject
   986  	ok, err := s.repo.pingReferrers(ctx)
   987  	if err != nil {
   988  		return err
   989  	}
   990  	if ok {
   991  		// referrers API is available, no client-side indexing needed
   992  		return nil
   993  	}
   994  	return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationRemove})
   995  }
   996  
   997  // Resolve resolves a reference to a descriptor.
   998  // See also `ManifestMediaTypes`.
   999  func (s *manifestStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
  1000  	ref, err := s.repo.ParseReference(reference)
  1001  	if err != nil {
  1002  		return ocispec.Descriptor{}, err
  1003  	}
  1004  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
  1005  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
  1006  	req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
  1007  	if err != nil {
  1008  		return ocispec.Descriptor{}, err
  1009  	}
  1010  	req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))
  1011  
  1012  	resp, err := s.repo.client().Do(req)
  1013  	if err != nil {
  1014  		return ocispec.Descriptor{}, err
  1015  	}
  1016  	defer resp.Body.Close()
  1017  
  1018  	switch resp.StatusCode {
  1019  	case http.StatusOK:
  1020  		return s.generateDescriptor(resp, ref, req.Method)
  1021  	case http.StatusNotFound:
  1022  		return ocispec.Descriptor{}, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
  1023  	default:
  1024  		return ocispec.Descriptor{}, errutil.ParseErrorResponse(resp)
  1025  	}
  1026  }
  1027  
  1028  // FetchReference fetches the manifest identified by the reference.
  1029  // The reference can be a tag or digest.
  1030  func (s *manifestStore) FetchReference(ctx context.Context, reference string) (desc ocispec.Descriptor, rc io.ReadCloser, err error) {
  1031  	ref, err := s.repo.ParseReference(reference)
  1032  	if err != nil {
  1033  		return ocispec.Descriptor{}, nil, err
  1034  	}
  1035  
  1036  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull)
  1037  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
  1038  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
  1039  	if err != nil {
  1040  		return ocispec.Descriptor{}, nil, err
  1041  	}
  1042  	req.Header.Set("Accept", manifestAcceptHeader(s.repo.ManifestMediaTypes))
  1043  
  1044  	resp, err := s.repo.client().Do(req)
  1045  	if err != nil {
  1046  		return ocispec.Descriptor{}, nil, err
  1047  	}
  1048  	defer func() {
  1049  		if err != nil {
  1050  			resp.Body.Close()
  1051  		}
  1052  	}()
  1053  
  1054  	switch resp.StatusCode {
  1055  	case http.StatusOK:
  1056  		if resp.ContentLength == -1 {
  1057  			desc, err = s.Resolve(ctx, reference)
  1058  		} else {
  1059  			desc, err = s.generateDescriptor(resp, ref, req.Method)
  1060  		}
  1061  		if err != nil {
  1062  			return ocispec.Descriptor{}, nil, err
  1063  		}
  1064  		return desc, resp.Body, nil
  1065  	case http.StatusNotFound:
  1066  		return ocispec.Descriptor{}, nil, fmt.Errorf("%s: %w", ref, errdef.ErrNotFound)
  1067  	default:
  1068  		return ocispec.Descriptor{}, nil, errutil.ParseErrorResponse(resp)
  1069  	}
  1070  }
  1071  
  1072  // Tag tags a manifest descriptor with a reference string.
  1073  func (s *manifestStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error {
  1074  	ref, err := s.repo.ParseReference(reference)
  1075  	if err != nil {
  1076  		return err
  1077  	}
  1078  
  1079  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull, auth.ActionPush)
  1080  	rc, err := s.Fetch(ctx, desc)
  1081  	if err != nil {
  1082  		return err
  1083  	}
  1084  	defer rc.Close()
  1085  
  1086  	return s.push(ctx, desc, rc, ref.Reference)
  1087  }
  1088  
  1089  // PushReference pushes the manifest with a reference tag.
  1090  func (s *manifestStore) PushReference(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
  1091  	ref, err := s.repo.ParseReference(reference)
  1092  	if err != nil {
  1093  		return err
  1094  	}
  1095  	return s.pushWithIndexing(ctx, expected, content, ref.Reference)
  1096  }
  1097  
  1098  // push pushes the manifest content, matching the expected descriptor.
  1099  func (s *manifestStore) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader, reference string) error {
  1100  	ref := s.repo.Reference
  1101  	ref.Reference = reference
  1102  	// pushing usually requires both pull and push actions.
  1103  	// Reference: https://github.com/distribution/distribution/blob/v2.7.1/registry/handlers/app.go#L921-L930
  1104  	ctx = registryutil.WithScopeHint(ctx, ref, auth.ActionPull, auth.ActionPush)
  1105  	url := buildRepositoryManifestURL(s.repo.PlainHTTP, ref)
  1106  	// unwrap the content for optimizations of built-in types.
  1107  	body := ioutil.UnwrapNopCloser(content)
  1108  	if _, ok := body.(io.ReadCloser); ok {
  1109  		// undo unwrap if the nopCloser is intended.
  1110  		body = content
  1111  	}
  1112  	req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body)
  1113  	if err != nil {
  1114  		return err
  1115  	}
  1116  	if req.GetBody != nil && req.ContentLength != expected.Size {
  1117  		// short circuit a size mismatch for built-in types.
  1118  		return fmt.Errorf("mismatch content length %d: expect %d", req.ContentLength, expected.Size)
  1119  	}
  1120  	req.ContentLength = expected.Size
  1121  	req.Header.Set("Content-Type", expected.MediaType)
  1122  
  1123  	// if the underlying client is an auth client, the content might be read
  1124  	// more than once for obtaining the auth challenge and the actual request.
  1125  	// To prevent double reading, the manifest is read and stored in the memory,
  1126  	// and serve from the memory.
  1127  	client := s.repo.client()
  1128  	if _, ok := client.(*auth.Client); ok && req.GetBody == nil {
  1129  		store := cas.NewMemory()
  1130  		err := store.Push(ctx, expected, content)
  1131  		if err != nil {
  1132  			return err
  1133  		}
  1134  		req.GetBody = func() (io.ReadCloser, error) {
  1135  			return store.Fetch(ctx, expected)
  1136  		}
  1137  		req.Body, err = req.GetBody()
  1138  		if err != nil {
  1139  			return err
  1140  		}
  1141  	}
  1142  	resp, err := client.Do(req)
  1143  	if err != nil {
  1144  		return err
  1145  	}
  1146  	defer resp.Body.Close()
  1147  
  1148  	if resp.StatusCode != http.StatusCreated {
  1149  		return errutil.ParseErrorResponse(resp)
  1150  	}
  1151  	return verifyContentDigest(resp, expected.Digest)
  1152  }
  1153  
  1154  // pushWithIndexing pushes the manifest content matching the expected descriptor,
  1155  // and indexes referrers for the manifest when needed.
  1156  func (s *manifestStore) pushWithIndexing(ctx context.Context, expected ocispec.Descriptor, r io.Reader, reference string) error {
  1157  	switch expected.MediaType {
  1158  	case ocispec.MediaTypeImageManifest:
  1159  		if state := s.repo.loadReferrersState(); state == referrersStateSupported {
  1160  			// referrers API is available, no client-side indexing needed
  1161  			return s.push(ctx, expected, r, reference)
  1162  		}
  1163  
  1164  		if err := limitSize(expected, s.repo.MaxMetadataBytes); err != nil {
  1165  			return err
  1166  		}
  1167  		manifestJSON, err := content.ReadAll(r, expected)
  1168  		if err != nil {
  1169  			return err
  1170  		}
  1171  		if err := s.push(ctx, expected, bytes.NewReader(manifestJSON), reference); err != nil {
  1172  			return err
  1173  		}
  1174  		return s.indexReferrersForPush(ctx, expected, manifestJSON)
  1175  	default:
  1176  		return s.push(ctx, expected, r, reference)
  1177  	}
  1178  }
  1179  
  1180  // indexReferrersForPush indexes referrers for image or artifact manifest with
  1181  // the subject field on manifest push.
  1182  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject
  1183  func (s *manifestStore) indexReferrersForPush(ctx context.Context, desc ocispec.Descriptor, manifestJSON []byte) error {
  1184  	var subject ocispec.Descriptor
  1185  	switch desc.MediaType {
  1186  	case ocispec.MediaTypeImageManifest:
  1187  		var manifest ocispec.Manifest
  1188  		if err := json.Unmarshal(manifestJSON, &manifest); err != nil {
  1189  			return fmt.Errorf("failed to decode manifest: %s: %s: %w", desc.Digest, desc.MediaType, err)
  1190  		}
  1191  		if manifest.Subject == nil {
  1192  			// no subject, no indexing needed
  1193  			return nil
  1194  		}
  1195  		subject = *manifest.Subject
  1196  		desc.ArtifactType = manifest.Config.MediaType
  1197  		desc.Annotations = manifest.Annotations
  1198  	default:
  1199  		return nil
  1200  	}
  1201  
  1202  	ok, err := s.repo.pingReferrers(ctx)
  1203  	if err != nil {
  1204  		return err
  1205  	}
  1206  	if ok {
  1207  		// referrers API is available, no client-side indexing needed
  1208  		return nil
  1209  	}
  1210  	return s.updateReferrersIndex(ctx, subject, referrerChange{desc, referrerOperationAdd})
  1211  }
  1212  
  1213  // updateReferrersIndex updates the referrers index for desc referencing subject
  1214  // on manifest push and manifest delete.
  1215  // References:
  1216  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#pushing-manifests-with-subject
  1217  //   - https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#deleting-manifests
  1218  func (s *manifestStore) updateReferrersIndex(ctx context.Context, subject ocispec.Descriptor, change referrerChange) (err error) {
  1219  	referrersTag := buildReferrersTag(subject)
  1220  
  1221  	var skipDelete bool
  1222  	var oldIndexDesc ocispec.Descriptor
  1223  	var referrers []ocispec.Descriptor
  1224  	prepare := func() error {
  1225  		// 1. pull the original referrers list using the referrers tag schema
  1226  		var err error
  1227  		oldIndexDesc, referrers, err = s.repo.referrersFromIndex(ctx, referrersTag)
  1228  		if err != nil {
  1229  			if errors.Is(err, errdef.ErrNotFound) {
  1230  				// no old index found, skip delete
  1231  				skipDelete = true
  1232  				return nil
  1233  			}
  1234  			return err
  1235  		}
  1236  		return nil
  1237  	}
  1238  	update := func(referrerChanges []referrerChange) error {
  1239  		// 2. apply the referrer changes on the referrers list
  1240  		updatedReferrers, err := applyReferrerChanges(referrers, referrerChanges)
  1241  		if err != nil {
  1242  			if err == errNoReferrerUpdate {
  1243  				return nil
  1244  			}
  1245  			return err
  1246  		}
  1247  
  1248  		// 3. push the updated referrers list using referrers tag schema
  1249  		if len(updatedReferrers) > 0 {
  1250  			newIndexDesc, newIndex, err := generateIndex(updatedReferrers)
  1251  			if err != nil {
  1252  				return fmt.Errorf("failed to generate referrers index for referrers tag %s: %w", referrersTag, err)
  1253  			}
  1254  			if err := s.push(ctx, newIndexDesc, bytes.NewReader(newIndex), referrersTag); err != nil {
  1255  				return fmt.Errorf("failed to push referrers index tagged by %s: %w", referrersTag, err)
  1256  			}
  1257  		}
  1258  
  1259  		// 4. delete the dangling original referrers index
  1260  		if !skipDelete {
  1261  			if err := s.repo.delete(ctx, oldIndexDesc, true); err != nil {
  1262  				return fmt.Errorf("failed to delete dangling referrers index %s for referrers tag %s: %w", oldIndexDesc.Digest.String(), referrersTag, err)
  1263  			}
  1264  		}
  1265  		return nil
  1266  	}
  1267  
  1268  	merge, done := s.repo.referrersMergePool.Get(referrersTag)
  1269  	defer done()
  1270  	return merge.Do(change, prepare, update)
  1271  }
  1272  
  1273  // ParseReference parses a reference to a fully qualified reference.
  1274  func (s *manifestStore) ParseReference(reference string) (registry.Reference, error) {
  1275  	return s.repo.ParseReference(reference)
  1276  }
  1277  
  1278  // generateDescriptor returns a descriptor generated from the response.
  1279  // See the truth table at the top of `repository_test.go`
  1280  func (s *manifestStore) generateDescriptor(resp *http.Response, ref registry.Reference, httpMethod string) (ocispec.Descriptor, error) {
  1281  	// 1. Validate Content-Type
  1282  	mediaType, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
  1283  	if err != nil {
  1284  		return ocispec.Descriptor{}, fmt.Errorf(
  1285  			"%s %q: invalid response `Content-Type` header; %w",
  1286  			resp.Request.Method,
  1287  			resp.Request.URL,
  1288  			err,
  1289  		)
  1290  	}
  1291  
  1292  	// 2. Validate Size
  1293  	if resp.ContentLength == -1 {
  1294  		return ocispec.Descriptor{}, fmt.Errorf(
  1295  			"%s %q: unknown response Content-Length",
  1296  			resp.Request.Method,
  1297  			resp.Request.URL,
  1298  		)
  1299  	}
  1300  
  1301  	// 3. Validate Client Reference
  1302  	var refDigest digest.Digest
  1303  	if d, err := ref.Digest(); err == nil {
  1304  		refDigest = d
  1305  	}
  1306  
  1307  	// 4. Validate Server Digest (if present)
  1308  	var serverHeaderDigest digest.Digest
  1309  	if serverHeaderDigestStr := resp.Header.Get(dockerContentDigestHeader); serverHeaderDigestStr != "" {
  1310  		if serverHeaderDigest, err = digest.Parse(serverHeaderDigestStr); err != nil {
  1311  			return ocispec.Descriptor{}, fmt.Errorf(
  1312  				"%s %q: invalid response header value: `%s: %s`; %w",
  1313  				resp.Request.Method,
  1314  				resp.Request.URL,
  1315  				dockerContentDigestHeader,
  1316  				serverHeaderDigestStr,
  1317  				err,
  1318  			)
  1319  		}
  1320  	}
  1321  
  1322  	/* 5. Now, look for specific error conditions; see truth table in method docstring */
  1323  	var contentDigest digest.Digest
  1324  
  1325  	if len(serverHeaderDigest) == 0 {
  1326  		if httpMethod == http.MethodHead {
  1327  			if len(refDigest) == 0 {
  1328  				// HEAD without server `Docker-Content-Digest` header is an
  1329  				// immediate fail
  1330  				return ocispec.Descriptor{}, fmt.Errorf(
  1331  					"HTTP %s request missing required header %q",
  1332  					httpMethod, dockerContentDigestHeader,
  1333  				)
  1334  			}
  1335  			// Otherwise, just trust the client-supplied digest
  1336  			contentDigest = refDigest
  1337  		} else {
  1338  			// GET without server `Docker-Content-Digest` header forces the
  1339  			// expensive calculation
  1340  			var calculatedDigest digest.Digest
  1341  			if calculatedDigest, err = calculateDigestFromResponse(resp, s.repo.MaxMetadataBytes); err != nil {
  1342  				return ocispec.Descriptor{}, fmt.Errorf("failed to calculate digest on response body; %w", err)
  1343  			}
  1344  			contentDigest = calculatedDigest
  1345  		}
  1346  	} else {
  1347  		contentDigest = serverHeaderDigest
  1348  	}
  1349  
  1350  	if len(refDigest) > 0 && refDigest != contentDigest {
  1351  		return ocispec.Descriptor{}, fmt.Errorf(
  1352  			"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
  1353  			resp.Request.Method, resp.Request.URL,
  1354  			dockerContentDigestHeader, contentDigest,
  1355  			refDigest,
  1356  		)
  1357  	}
  1358  
  1359  	// 6. Finally, if we made it this far, then all is good; return.
  1360  	return ocispec.Descriptor{
  1361  		MediaType: mediaType,
  1362  		Digest:    contentDigest,
  1363  		Size:      resp.ContentLength,
  1364  	}, nil
  1365  }
  1366  
  1367  // calculateDigestFromResponse calculates the actual digest of the response body
  1368  // taking care not to destroy it in the process.
  1369  func calculateDigestFromResponse(resp *http.Response, maxMetadataBytes int64) (digest.Digest, error) {
  1370  	defer resp.Body.Close()
  1371  
  1372  	body := limitReader(resp.Body, maxMetadataBytes)
  1373  	content, err := io.ReadAll(body)
  1374  	if err != nil {
  1375  		return "", fmt.Errorf("%s %q: failed to read response body: %w", resp.Request.Method, resp.Request.URL, err)
  1376  	}
  1377  	resp.Body = io.NopCloser(bytes.NewReader(content))
  1378  
  1379  	return digest.FromBytes(content), nil
  1380  }
  1381  
  1382  // verifyContentDigest verifies "Docker-Content-Digest" header if present.
  1383  // OCI distribution-spec states the Docker-Content-Digest header is optional.
  1384  // Reference: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#legacy-docker-support-http-headers
  1385  func verifyContentDigest(resp *http.Response, expected digest.Digest) error {
  1386  	digestStr := resp.Header.Get(dockerContentDigestHeader)
  1387  
  1388  	if len(digestStr) == 0 {
  1389  		return nil
  1390  	}
  1391  
  1392  	contentDigest, err := digest.Parse(digestStr)
  1393  	if err != nil {
  1394  		return fmt.Errorf(
  1395  			"%s %q: invalid response header: `%s: %s`",
  1396  			resp.Request.Method, resp.Request.URL,
  1397  			dockerContentDigestHeader, digestStr,
  1398  		)
  1399  	}
  1400  
  1401  	if contentDigest != expected {
  1402  		return fmt.Errorf(
  1403  			"%s %q: invalid response; digest mismatch in %s: received %q when expecting %q",
  1404  			resp.Request.Method, resp.Request.URL,
  1405  			dockerContentDigestHeader, contentDigest,
  1406  			expected,
  1407  		)
  1408  	}
  1409  
  1410  	return nil
  1411  }
  1412  
  1413  // generateIndex generates an image index containing the given manifests list.
  1414  func generateIndex(manifests []ocispec.Descriptor) (ocispec.Descriptor, []byte, error) {
  1415  	if manifests == nil {
  1416  		manifests = []ocispec.Descriptor{} // make it an empty array to prevent potential server-side bugs
  1417  	}
  1418  	index := ocispec.Index{
  1419  		Versioned: specs.Versioned{
  1420  			SchemaVersion: 2, // historical value. does not pertain to OCI or docker version
  1421  		},
  1422  		MediaType: ocispec.MediaTypeImageIndex,
  1423  		Manifests: manifests,
  1424  	}
  1425  	indexJSON, err := json.Marshal(index)
  1426  	if err != nil {
  1427  		return ocispec.Descriptor{}, nil, err
  1428  	}
  1429  	indexDesc := content.NewDescriptorFromBytes(index.MediaType, indexJSON)
  1430  	return indexDesc, indexJSON, nil
  1431  }