diff --git a/copy.go b/copy.go index 522775421..46f403a96 100644 --- a/copy.go +++ b/copy.go @@ -38,6 +38,7 @@ import ( const defaultConcurrency int = 3 // This value is consistent with dockerd and containerd. // ErrSkipDesc signal to stop copying a descriptor. When returned from PreCopy the blob must exist in the target. +// This can be used to signal that a blob has been "Mounted" into the target repository. var ErrSkipDesc = errors.New("skip descriptor") // DefaultCopyOptions provides the default CopyOptions. @@ -111,6 +112,50 @@ type CopyGraphOptions struct { FindSuccessors func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) } +// WithMount enabled cross repository blob mounting. +// sourceReference is the repository to use for mounting (the mount point). +// mounter is the destination for the mount (a well-known implementation of this is *registry.Repository representing the target). +// onMounted is called (if provided) when the blob is mounted. +// The original PreCopy hook is called only on copy, and there fore not when the blob is mounted. +func (opts *CopyGraphOptions) WithMount(sourceRepository string, mounter registry.Mounter, onMounted func(context.Context, ocispec.Descriptor)) { + preCopy := opts.PreCopy + opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { + // Only care to mount blobs + if descriptor.IsManifest(desc) { + return preCopy(ctx, desc) + } + + var mountFailed bool + getContent := func() (io.ReadCloser, error) { + // call the original PreCopy function if it exists + if preCopy != nil { + if err := preCopy(ctx, desc); err != nil { + return nil, err + } + } + // the invocation of getContent indicates that mounting has failed + mountFailed = true + + // To avoid needing a content.Fetcher as an input argument we simply fall back to the default behavior + // as if getContent was nil + return nil, errdef.ErrUnsupported + } + + // Mount or copy + if err := mounter.Mount(ctx, desc, sourceRepository, getContent); err != nil { + return err + } + + if !mountFailed && onMounted != nil { + onMounted(ctx, desc) + } + + // Mount succeeded or we copied it + // either way we return ErrSkipDesc to signal that the descriptor now exists + return ErrSkipDesc + } +} + // Copy copies a rooted directed acyclic graph (DAG) with the tagged root node // in the source Target to the destination Target. // The destination reference will be the same as the source reference if the diff --git a/registry/remote/repository.go b/registry/remote/repository.go index b91054fca..abd2808e1 100644 --- a/registry/remote/repository.go +++ b/registry/remote/repository.go @@ -797,6 +797,10 @@ func (s *blobStore) Mount(ctx context.Context, desc ocispec.Descriptor, fromRepo var r io.ReadCloser if getContent != nil { r, err = getContent() + if errors.Is(err, errdef.ErrUnsupported) { + // getContent can return a ErrUnsupported to fallback to the default copy operation + r, err = s.sibling(fromRepo).Fetch(ctx, desc) + } } else { r, err = s.sibling(fromRepo).Fetch(ctx, desc) }