github.com/npaton/distribution@v2.3.1-rc.0+incompatible/registry/storage/linkedblobstore.go (about)

     1  package storage
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"time"
     7  
     8  	"github.com/docker/distribution"
     9  	"github.com/docker/distribution/context"
    10  	"github.com/docker/distribution/digest"
    11  	"github.com/docker/distribution/reference"
    12  	"github.com/docker/distribution/registry/storage/driver"
    13  	"github.com/docker/distribution/uuid"
    14  )
    15  
    16  // linkPathFunc describes a function that can resolve a link based on the
    17  // repository name and digest.
    18  type linkPathFunc func(name string, dgst digest.Digest) (string, error)
    19  
    20  // linkedBlobStore provides a full BlobService that namespaces the blobs to a
    21  // given repository. Effectively, it manages the links in a given repository
    22  // that grant access to the global blob store.
    23  type linkedBlobStore struct {
    24  	*blobStore
    25  	registry               *registry
    26  	blobServer             distribution.BlobServer
    27  	blobAccessController   distribution.BlobDescriptorService
    28  	repository             distribution.Repository
    29  	ctx                    context.Context // only to be used where context can't come through method args
    30  	deleteEnabled          bool
    31  	resumableDigestEnabled bool
    32  
    33  	// linkPathFns specifies one or more path functions allowing one to
    34  	// control the repository blob link set to which the blob store
    35  	// dispatches. This is required because manifest and layer blobs have not
    36  	// yet been fully merged. At some point, this functionality should be
    37  	// removed an the blob links folder should be merged. The first entry is
    38  	// treated as the "canonical" link location and will be used for writes.
    39  	linkPathFns []linkPathFunc
    40  }
    41  
    42  var _ distribution.BlobStore = &linkedBlobStore{}
    43  
    44  func (lbs *linkedBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
    45  	return lbs.blobAccessController.Stat(ctx, dgst)
    46  }
    47  
    48  func (lbs *linkedBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) {
    49  	canonical, err := lbs.Stat(ctx, dgst) // access check
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	return lbs.blobStore.Get(ctx, canonical.Digest)
    55  }
    56  
    57  func (lbs *linkedBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) {
    58  	canonical, err := lbs.Stat(ctx, dgst) // access check
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  
    63  	return lbs.blobStore.Open(ctx, canonical.Digest)
    64  }
    65  
    66  func (lbs *linkedBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error {
    67  	canonical, err := lbs.Stat(ctx, dgst) // access check
    68  	if err != nil {
    69  		return err
    70  	}
    71  
    72  	if canonical.MediaType != "" {
    73  		// Set the repository local content type.
    74  		w.Header().Set("Content-Type", canonical.MediaType)
    75  	}
    76  
    77  	return lbs.blobServer.ServeBlob(ctx, w, r, canonical.Digest)
    78  }
    79  
    80  func (lbs *linkedBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) {
    81  	dgst := digest.FromBytes(p)
    82  	// Place the data in the blob store first.
    83  	desc, err := lbs.blobStore.Put(ctx, mediaType, p)
    84  	if err != nil {
    85  		context.GetLogger(ctx).Errorf("error putting into main store: %v", err)
    86  		return distribution.Descriptor{}, err
    87  	}
    88  
    89  	if err := lbs.blobAccessController.SetDescriptor(ctx, dgst, desc); err != nil {
    90  		return distribution.Descriptor{}, err
    91  	}
    92  
    93  	// TODO(stevvooe): Write out mediatype if incoming differs from what is
    94  	// returned by Put above. Note that we should allow updates for a given
    95  	// repository.
    96  
    97  	return desc, lbs.linkBlob(ctx, desc)
    98  }
    99  
   100  // createOptions is a collection of blob creation modifiers relevant to general
   101  // blob storage intended to be configured by the BlobCreateOption.Apply method.
   102  type createOptions struct {
   103  	Mount struct {
   104  		ShouldMount bool
   105  		From        reference.Canonical
   106  	}
   107  }
   108  
   109  type optionFunc func(interface{}) error
   110  
   111  func (f optionFunc) Apply(v interface{}) error {
   112  	return f(v)
   113  }
   114  
   115  // WithMountFrom returns a BlobCreateOption which designates that the blob should be
   116  // mounted from the given canonical reference.
   117  func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption {
   118  	return optionFunc(func(v interface{}) error {
   119  		opts, ok := v.(*createOptions)
   120  		if !ok {
   121  			return fmt.Errorf("unexpected options type: %T", v)
   122  		}
   123  
   124  		opts.Mount.ShouldMount = true
   125  		opts.Mount.From = ref
   126  
   127  		return nil
   128  	})
   129  }
   130  
   131  // Writer begins a blob write session, returning a handle.
   132  func (lbs *linkedBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) {
   133  	context.GetLogger(ctx).Debug("(*linkedBlobStore).Writer")
   134  
   135  	var opts createOptions
   136  
   137  	for _, option := range options {
   138  		err := option.Apply(&opts)
   139  		if err != nil {
   140  			return nil, err
   141  		}
   142  	}
   143  
   144  	if opts.Mount.ShouldMount {
   145  		desc, err := lbs.mount(ctx, opts.Mount.From, opts.Mount.From.Digest())
   146  		if err == nil {
   147  			// Mount successful, no need to initiate an upload session
   148  			return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc}
   149  		}
   150  	}
   151  
   152  	uuid := uuid.Generate().String()
   153  	startedAt := time.Now().UTC()
   154  
   155  	path, err := pathFor(uploadDataPathSpec{
   156  		name: lbs.repository.Name().Name(),
   157  		id:   uuid,
   158  	})
   159  
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	startedAtPath, err := pathFor(uploadStartedAtPathSpec{
   165  		name: lbs.repository.Name().Name(),
   166  		id:   uuid,
   167  	})
   168  
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	// Write a startedat file for this upload
   174  	if err := lbs.blobStore.driver.PutContent(ctx, startedAtPath, []byte(startedAt.Format(time.RFC3339))); err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	return lbs.newBlobUpload(ctx, uuid, path, startedAt)
   179  }
   180  
   181  func (lbs *linkedBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) {
   182  	context.GetLogger(ctx).Debug("(*linkedBlobStore).Resume")
   183  
   184  	startedAtPath, err := pathFor(uploadStartedAtPathSpec{
   185  		name: lbs.repository.Name().Name(),
   186  		id:   id,
   187  	})
   188  
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	startedAtBytes, err := lbs.blobStore.driver.GetContent(ctx, startedAtPath)
   194  	if err != nil {
   195  		switch err := err.(type) {
   196  		case driver.PathNotFoundError:
   197  			return nil, distribution.ErrBlobUploadUnknown
   198  		default:
   199  			return nil, err
   200  		}
   201  	}
   202  
   203  	startedAt, err := time.Parse(time.RFC3339, string(startedAtBytes))
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	path, err := pathFor(uploadDataPathSpec{
   209  		name: lbs.repository.Name().Name(),
   210  		id:   id,
   211  	})
   212  
   213  	if err != nil {
   214  		return nil, err
   215  	}
   216  
   217  	return lbs.newBlobUpload(ctx, id, path, startedAt)
   218  }
   219  
   220  func (lbs *linkedBlobStore) Delete(ctx context.Context, dgst digest.Digest) error {
   221  	if !lbs.deleteEnabled {
   222  		return distribution.ErrUnsupported
   223  	}
   224  
   225  	// Ensure the blob is available for deletion
   226  	_, err := lbs.blobAccessController.Stat(ctx, dgst)
   227  	if err != nil {
   228  		return err
   229  	}
   230  
   231  	err = lbs.blobAccessController.Clear(ctx, dgst)
   232  	if err != nil {
   233  		return err
   234  	}
   235  
   236  	return nil
   237  }
   238  
   239  func (lbs *linkedBlobStore) mount(ctx context.Context, sourceRepo reference.Named, dgst digest.Digest) (distribution.Descriptor, error) {
   240  	repo, err := lbs.registry.Repository(ctx, sourceRepo)
   241  	if err != nil {
   242  		return distribution.Descriptor{}, err
   243  	}
   244  	stat, err := repo.Blobs(ctx).Stat(ctx, dgst)
   245  	if err != nil {
   246  		return distribution.Descriptor{}, err
   247  	}
   248  
   249  	desc := distribution.Descriptor{
   250  		Size: stat.Size,
   251  
   252  		// NOTE(stevvooe): The central blob store firewalls media types from
   253  		// other users. The caller should look this up and override the value
   254  		// for the specific repository.
   255  		MediaType: "application/octet-stream",
   256  		Digest:    dgst,
   257  	}
   258  	return desc, lbs.linkBlob(ctx, desc)
   259  }
   260  
   261  // newBlobUpload allocates a new upload controller with the given state.
   262  func (lbs *linkedBlobStore) newBlobUpload(ctx context.Context, uuid, path string, startedAt time.Time) (distribution.BlobWriter, error) {
   263  	fw, err := newFileWriter(ctx, lbs.driver, path)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	bw := &blobWriter{
   269  		blobStore:              lbs,
   270  		id:                     uuid,
   271  		startedAt:              startedAt,
   272  		digester:               digest.Canonical.New(),
   273  		bufferedFileWriter:     *fw,
   274  		resumableDigestEnabled: lbs.resumableDigestEnabled,
   275  	}
   276  
   277  	return bw, nil
   278  }
   279  
   280  // linkBlob links a valid, written blob into the registry under the named
   281  // repository for the upload controller.
   282  func (lbs *linkedBlobStore) linkBlob(ctx context.Context, canonical distribution.Descriptor, aliases ...digest.Digest) error {
   283  	dgsts := append([]digest.Digest{canonical.Digest}, aliases...)
   284  
   285  	// TODO(stevvooe): Need to write out mediatype for only canonical hash
   286  	// since we don't care about the aliases. They are generally unused except
   287  	// for tarsum but those versions don't care about mediatype.
   288  
   289  	// Don't make duplicate links.
   290  	seenDigests := make(map[digest.Digest]struct{}, len(dgsts))
   291  
   292  	// only use the first link
   293  	linkPathFn := lbs.linkPathFns[0]
   294  
   295  	for _, dgst := range dgsts {
   296  		if _, seen := seenDigests[dgst]; seen {
   297  			continue
   298  		}
   299  		seenDigests[dgst] = struct{}{}
   300  
   301  		blobLinkPath, err := linkPathFn(lbs.repository.Name().Name(), dgst)
   302  		if err != nil {
   303  			return err
   304  		}
   305  
   306  		if err := lbs.blobStore.link(ctx, blobLinkPath, canonical.Digest); err != nil {
   307  			return err
   308  		}
   309  	}
   310  
   311  	return nil
   312  }
   313  
   314  type linkedBlobStatter struct {
   315  	*blobStore
   316  	repository distribution.Repository
   317  
   318  	// linkPathFns specifies one or more path functions allowing one to
   319  	// control the repository blob link set to which the blob store
   320  	// dispatches. This is required because manifest and layer blobs have not
   321  	// yet been fully merged. At some point, this functionality should be
   322  	// removed an the blob links folder should be merged. The first entry is
   323  	// treated as the "canonical" link location and will be used for writes.
   324  	linkPathFns []linkPathFunc
   325  }
   326  
   327  var _ distribution.BlobDescriptorService = &linkedBlobStatter{}
   328  
   329  func (lbs *linkedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) {
   330  	var (
   331  		resolveErr error
   332  		target     digest.Digest
   333  	)
   334  
   335  	// try the many link path functions until we get success or an error that
   336  	// is not PathNotFoundError.
   337  	for _, linkPathFn := range lbs.linkPathFns {
   338  		var err error
   339  		target, err = lbs.resolveWithLinkFunc(ctx, dgst, linkPathFn)
   340  
   341  		if err == nil {
   342  			break // success!
   343  		}
   344  
   345  		switch err := err.(type) {
   346  		case driver.PathNotFoundError:
   347  			resolveErr = distribution.ErrBlobUnknown // move to the next linkPathFn, saving the error
   348  		default:
   349  			return distribution.Descriptor{}, err
   350  		}
   351  	}
   352  
   353  	if resolveErr != nil {
   354  		return distribution.Descriptor{}, resolveErr
   355  	}
   356  
   357  	if target != dgst {
   358  		// Track when we are doing cross-digest domain lookups. ie, sha512 to sha256.
   359  		context.GetLogger(ctx).Warnf("looking up blob with canonical target: %v -> %v", dgst, target)
   360  	}
   361  
   362  	// TODO(stevvooe): Look up repository local mediatype and replace that on
   363  	// the returned descriptor.
   364  
   365  	return lbs.blobStore.statter.Stat(ctx, target)
   366  }
   367  
   368  func (lbs *linkedBlobStatter) Clear(ctx context.Context, dgst digest.Digest) (err error) {
   369  	// clear any possible existence of a link described in linkPathFns
   370  	for _, linkPathFn := range lbs.linkPathFns {
   371  		blobLinkPath, err := linkPathFn(lbs.repository.Name().Name(), dgst)
   372  		if err != nil {
   373  			return err
   374  		}
   375  
   376  		err = lbs.blobStore.driver.Delete(ctx, blobLinkPath)
   377  		if err != nil {
   378  			switch err := err.(type) {
   379  			case driver.PathNotFoundError:
   380  				continue // just ignore this error and continue
   381  			default:
   382  				return err
   383  			}
   384  		}
   385  	}
   386  
   387  	return nil
   388  }
   389  
   390  // resolveTargetWithFunc allows us to read a link to a resource with different
   391  // linkPathFuncs to let us try a few different paths before returning not
   392  // found.
   393  func (lbs *linkedBlobStatter) resolveWithLinkFunc(ctx context.Context, dgst digest.Digest, linkPathFn linkPathFunc) (digest.Digest, error) {
   394  	blobLinkPath, err := linkPathFn(lbs.repository.Name().Name(), dgst)
   395  	if err != nil {
   396  		return "", err
   397  	}
   398  
   399  	return lbs.blobStore.readlink(ctx, blobLinkPath)
   400  }
   401  
   402  func (lbs *linkedBlobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error {
   403  	// The canonical descriptor for a blob is set at the commit phase of upload
   404  	return nil
   405  }
   406  
   407  // blobLinkPath provides the path to the blob link, also known as layers.
   408  func blobLinkPath(name string, dgst digest.Digest) (string, error) {
   409  	return pathFor(layerLinkPathSpec{name: name, digest: dgst})
   410  }
   411  
   412  // manifestRevisionLinkPath provides the path to the manifest revision link.
   413  func manifestRevisionLinkPath(name string, dgst digest.Digest) (string, error) {
   414  	return pathFor(manifestRevisionLinkPathSpec{name: name, revision: dgst})
   415  }