From 43f27819451a099aa6cd61506854a36b07c7573d Mon Sep 17 00:00:00 2001 From: Sargun Dhillon Date: Mon, 14 Jun 2021 12:25:06 -0700 Subject: [PATCH] Allow for "automatic content discovery" on mount As described in https://github.com/opencontainers/distribution-spec/pull/275, this adds, and allows for "automatic content discovery" on mount if the from field is ommitted. This implementation does not provide and authz functionality, so it should be only be used / implemented, if the registry has no cross-repo authz. Signed-off-by: Sargun Dhillon --- blobs.go | 57 +++++++++++++--- configuration/configuration.go | 4 ++ notifications/bridge.go | 12 ++++ notifications/event.go | 4 ++ notifications/listener.go | 8 ++- notifications/listener_test.go | 5 ++ registry/client/repository.go | 28 ++++++-- registry/client/repository_test.go | 11 ++- registry/handlers/app.go | 29 +++++--- registry/handlers/blobupload.go | 11 ++- registry/storage/blob_test.go | 60 +++++++++++++--- registry/storage/linkedblobstore.go | 87 ++++++++++++++++++------ registry/storage/linkedblobstore_test.go | 12 +++- registry/storage/registry.go | 17 +++-- 14 files changed, 272 insertions(+), 73 deletions(-) diff --git a/blobs.go b/blobs.go index 33273213b00..20fd65e97b1 100644 --- a/blobs.go +++ b/blobs.go @@ -9,6 +9,7 @@ import ( "time" "github.com/distribution/distribution/v3/reference" + "github.com/opencontainers/go-digest" v1 "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -45,15 +46,29 @@ func (err ErrBlobInvalidDigest) Error() string { // ErrBlobMounted returned when a blob is mounted from another repository // instead of initiating an upload session. type ErrBlobMounted struct { - From reference.Canonical Descriptor Descriptor } func (err ErrBlobMounted) Error() string { + return fmt.Sprintf("blob mounted to: %v", err.Descriptor) +} + +// ErrBlobMountedFrom returned when a blob is mounted from another repository +// instead of initiating an upload session.type ErrBlobMounted struct { +type ErrBlobMountedFrom struct { + ErrBlobMounted + From reference.Canonical +} + +func (err ErrBlobMountedFrom) Error() string { return fmt.Sprintf("blob mounted from: %v to: %v", err.From, err.Descriptor) } +func (err ErrBlobMountedFrom) Unwrap() error { + return err.ErrBlobMounted +} + // Descriptor describes targeted content. Used in conjunction with a blob // store, a descriptor can be used to fetch, store and target any kind of // blob. The struct also describes the wire protocol format. Fields should @@ -203,13 +218,39 @@ type BlobCreateOption interface { // CreateOptions is a collection of blob creation modifiers relevant to general // blob storage intended to be configured by the BlobCreateOption.Apply method. type CreateOptions struct { - Mount struct { - ShouldMount bool - From reference.Canonical - // Stat allows to pass precalculated descriptor to link and return. - // Blob access check will be skipped if set. - Stat *Descriptor - } + Mount Mount +} + +type StatMount struct { + // Stat allows to pass precalculated descriptor to link and return. + // Blob access check will be skipped if set. + Stat *Descriptor +} + +func (s StatMount) Digest() digest.Digest { + return s.Stat.Digest +} + +type DigestMount struct { + // BlobDigest is only checked if automatic content discovery is enabled + BlobDigest digest.Digest +} + +func (d DigestMount) Digest() digest.Digest { + return d.BlobDigest +} + +type FromMount struct { + // From is optional if automatic content discovery is enabled + From reference.Canonical +} + +func (f FromMount) Digest() digest.Digest { + return f.From.Digest() +} + +type Mount interface { + Digest() digest.Digest } // BlobWriter provides a handle for inserting data into a blob store. diff --git a/configuration/configuration.go b/configuration/configuration.go index 796fc3596e4..5a43ec17955 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -247,6 +247,10 @@ type Configuration struct { Classes []string `yaml:"classes"` } `yaml:"repository,omitempty"` } `yaml:"policy,omitempty"` + + // AutomaticContentDiscovery enables automatic content discovery options for the registry. + // It must only be used with auth disabled. + AutomaticContentDiscovery bool `yaml:"automaticcontentdiscovery,omitempty"` } // LogHook is composed of hook Level and Type. diff --git a/notifications/bridge.go b/notifications/bridge.go index 58a1d3fc345..a7f8230110f 100644 --- a/notifications/bridge.go +++ b/notifications/bridge.go @@ -107,6 +107,18 @@ func (b *bridge) BlobMounted(repo reference.Named, desc distribution.Descriptor, return b.sink.Write(*event) } +func (b *bridge) BlobMountedAutomaticContentDiscovery(repo reference.Named, desc distribution.Descriptor) error { + event, err := b.createBlobEvent(EventActionMount, repo, desc) + if err != nil { + return err + } + + t := true + event.Target.AutomaticContentDiscovery = &t + + return b.sink.Write(*event) +} + func (b *bridge) BlobDeleted(repo reference.Named, dgst digest.Digest) error { return b.createBlobDeleteEventAndWrite(EventActionDelete, repo, dgst) } diff --git a/notifications/event.go b/notifications/event.go index 2a3c4e21ac1..27ac790643e 100644 --- a/notifications/event.go +++ b/notifications/event.go @@ -67,6 +67,10 @@ type Event struct { // from if appropriate. FromRepository string `json:"fromRepository,omitempty"` + // AutomaticContentDiscovery identifies that the blob was mounted via + // automatic content discovery, and thus the "source" repo is unspecified + AutomaticContentDiscovery *bool `json:"automaticContentDiscovery,omitempty"` + // URL provides a direct link to the content. URL string `json:"url,omitempty"` diff --git a/notifications/listener.go b/notifications/listener.go index 9c0d2708598..a8dd59f08ef 100644 --- a/notifications/listener.go +++ b/notifications/listener.go @@ -23,6 +23,7 @@ type BlobListener interface { BlobPushed(repo reference.Named, desc distribution.Descriptor) error BlobPulled(repo reference.Named, desc distribution.Descriptor) error BlobMounted(repo reference.Named, desc distribution.Descriptor, fromRepo reference.Named) error + BlobMountedAutomaticContentDiscovery(repo reference.Named, desc distribution.Descriptor) error BlobDeleted(repo reference.Named, desc digest.Digest) error } @@ -191,11 +192,16 @@ func (bsl *blobServiceListener) Put(ctx context.Context, mediaType string, p []b func (bsl *blobServiceListener) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { wr, err := bsl.BlobStore.Create(ctx, options...) switch err := err.(type) { - case distribution.ErrBlobMounted: + case distribution.ErrBlobMountedFrom: if err := bsl.parent.listener.BlobMounted(bsl.parent.Repository.Named(), err.Descriptor, err.From); err != nil { dcontext.GetLogger(ctx).Errorf("error dispatching blob mount to listener: %v", err) } return nil, err + case distribution.ErrBlobMounted: + if err := bsl.parent.listener.BlobMountedAutomaticContentDiscovery(bsl.parent.Repository.Named(), err.Descriptor); err != nil { + dcontext.GetLogger(ctx).Errorf("error dispatching blob mount to listener: %v", err) + } + return nil, err } return bsl.decorateWriter(wr), err } diff --git a/notifications/listener_test.go b/notifications/listener_test.go index e34707d722b..6a01b881d5d 100644 --- a/notifications/listener_test.go +++ b/notifications/listener_test.go @@ -98,6 +98,11 @@ func (tl *testListener) BlobMounted(repo reference.Named, desc distribution.Desc return nil } +func (tl *testListener) BlobMountedAutomaticContentDiscovery(repo reference.Named, desc distribution.Descriptor) error { + tl.ops["layer:automount"]++ + return nil +} + func (tl *testListener) BlobDeleted(repo reference.Named, d digest.Digest) error { tl.ops["layer:delete"]++ return nil diff --git a/registry/client/repository.go b/registry/client/repository.go index a3379c0a06c..434b448847d 100644 --- a/registry/client/repository.go +++ b/registry/client/repository.go @@ -764,8 +764,9 @@ func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption { return fmt.Errorf("unexpected options type: %T", v) } - opts.Mount.ShouldMount = true - opts.Mount.From = ref + opts.Mount = distribution.FromMount{ + From: ref, + } return nil }) @@ -783,8 +784,14 @@ func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateO var values []url.Values - if opts.Mount.ShouldMount { - values = append(values, url.Values{"from": {opts.Mount.From.Name()}, "mount": {opts.Mount.From.Digest().String()}}) + switch v := opts.Mount.(type) { + case distribution.FromMount: + values = append(values, url.Values{"from": {v.From.Name()}, "mount": {v.Digest().String()}}) + case distribution.DigestMount: + values = append(values, url.Values{"mount": {v.Digest().String()}}) + case nil: + default: + return nil, fmt.Errorf("Unknown mount type: %T", v) } u, err := bs.ub.BuildBlobUploadURL(bs.name, values...) @@ -805,11 +812,20 @@ func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateO switch resp.StatusCode { case http.StatusCreated: - desc, err := bs.statter.Stat(ctx, opts.Mount.From.Digest()) + desc, err := bs.statter.Stat(ctx, opts.Mount.Digest()) if err != nil { return nil, err } - return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc} + + switch v := opts.Mount.(type) { + case distribution.FromMount: + return nil, distribution.ErrBlobMountedFrom{ErrBlobMounted: distribution.ErrBlobMounted{Descriptor: desc}, From: v.From} + case distribution.DigestMount: + // We shouldn't expose the from. + return nil, distribution.ErrBlobMounted{Descriptor: desc} + default: + panic("Unexpected state") + } case http.StatusAccepted: // TODO(dmcgowan): Check for invalid UUID uuid := resp.Header.Get("Docker-Upload-UUID") diff --git a/registry/client/repository_test.go b/registry/client/repository_test.go index 2e8e978a2fe..5512bf8bed7 100644 --- a/registry/client/repository_test.go +++ b/registry/client/repository_test.go @@ -908,15 +908,12 @@ func TestBlobMount(t *testing.T) { t.Fatalf("Expected blob writer to be nil, was %v", bw) } - if ebm, ok := err.(distribution.ErrBlobMounted); ok { - if ebm.From.Digest() != dgst { - t.Fatalf("Unexpected digest: %s, expected %s", ebm.From.Digest(), dgst) - } - if ebm.From.Name() != sourceRepo.Name() { - t.Fatalf("Unexpected from: %s, expected %s", ebm.From.Name(), sourceRepo) + if ebm, ok := err.(distribution.ErrBlobMountedFrom); ok { + if ebm.Descriptor.Digest != dgst { + t.Fatalf("Unexpected digest: %s, expected %s", ebm.Descriptor.Digest, dgst) } } else { - t.Fatalf("Unexpected error: %v, expected an ErrBlobMounted", err) + t.Fatalf("Unexpected error: %v, expected an ErrBlobMountedFrom", err) } } diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 212c79a709f..9aded2854bc 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -258,6 +258,24 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { } } + authType := config.Auth.Type() + + if authType != "" && !strings.EqualFold(authType, "none") { + accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters()) + if err != nil { + panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) + } + + if config.AutomaticContentDiscovery { + dcontext.GetLogger(app).Warn("Not enabling automatic content discovery because auth is enabled") + } + + app.accessController = accessController + dcontext.GetLogger(app).Debugf("configured %q access controller", authType) + } else if config.AutomaticContentDiscovery { + options = append(options, storage.EnableAutomaticContentDiscovery) + } + // configure storage caches if cc, ok := config.Storage["cache"]; ok { v, ok := cc["blobdescriptor"] @@ -306,17 +324,6 @@ func NewApp(ctx context.Context, config *configuration.Configuration) *App { panic(err) } - authType := config.Auth.Type() - - if authType != "" && !strings.EqualFold(authType, "none") { - accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters()) - if err != nil { - panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) - } - app.accessController = accessController - dcontext.GetLogger(app).Debugf("configured %q access controller", authType) - } - // configure as a pull through cache if config.Proxy.RemoteURL != "" { app.registry, err = proxy.NewRegistryPullThroughCache(ctx, app.registry, app.driver, config.Proxy) diff --git a/registry/handlers/blobupload.go b/registry/handlers/blobupload.go index d712fe009ef..0657445246f 100644 --- a/registry/handlers/blobupload.go +++ b/registry/handlers/blobupload.go @@ -1,6 +1,7 @@ package handlers import ( + "errors" "fmt" "net/http" "net/url" @@ -67,8 +68,9 @@ func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Req fromRepo := r.FormValue("from") mountDigest := r.FormValue("mount") - if mountDigest != "" && fromRepo != "" { + if mountDigest != "" { opt, err := buh.createBlobMountOption(fromRepo, mountDigest) + // We ignore errors here because this is supposed to (always) fail open according to the spec. if opt != nil && err == nil { options = append(options, opt) } @@ -78,7 +80,8 @@ func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Req upload, err := blobs.Create(buh, options...) if err != nil { - if ebm, ok := err.(distribution.ErrBlobMounted); ok { + var ebm distribution.ErrBlobMounted + if errors.As(err, &ebm) { if err := buh.writeBlobCreatedHeaders(w, ebm.Descriptor); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } @@ -364,6 +367,10 @@ func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string return nil, err } + if fromRepo == "" { + return storage.WithMount(dgst), nil + } + ref, err := reference.WithName(fromRepo) if err != nil { return nil, err diff --git a/registry/storage/blob_test.go b/registry/storage/blob_test.go index 541c07050c3..c21ef3d5a62 100644 --- a/registry/storage/blob_test.go +++ b/registry/storage/blob_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "errors" "fmt" "io" "io/ioutil" @@ -428,13 +429,28 @@ func createSource(ctx context.Context, t *testing.T, registry distribution.Names } } -// TestBlobMount covers the blob mount process, exercising common +func TestBlobMountAutomaticContentDiscovery(t *testing.T) { + t.Run("testBlobMountAutomaticContent", func(t *testing.T) { + testBlobMountAutomaticContentDiscovery(t, false) + }) + t.Run("testBlobMountAutomaticContentDiscoveryEnabled", func(t *testing.T) { + testBlobMountAutomaticContentDiscovery(t, true) + }) +} + +// testBlobMountAutomaticContentDiscovery covers the blob mount process, exercising common // error paths that might be seen during a mount. -func TestBlobMount(t *testing.T) { +func testBlobMountAutomaticContentDiscovery(t *testing.T, enableAutomaticContentDiscovery bool) { ctx := context.Background() driver := testdriver.New() - registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) + var registry distribution.Namespace + var err error + if enableAutomaticContentDiscovery { + registry, err = NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect, EnableAutomaticContentDiscovery) + } else { + registry, err = NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) + } if err != nil { t.Fatalf("error creating registry: %v", err) } @@ -453,22 +469,48 @@ func TestBlobMount(t *testing.T) { } bs := repository.Blobs(ctx) - bw, err := bs.Create(ctx, WithMountFrom(canonicalRef)) + var bw distribution.BlobWriter + if enableAutomaticContentDiscovery { + bw, err = bs.Create(ctx, WithMount(source.digest)) + } else { + // Make sure without automatic content discovery this fails gracefully + bw, err = bs.Create(ctx, WithMount(source.digest)) + if err != nil { + t.Fatalf("Received unexpected non-error, when cross-mounting without canonical reference and automatic content discovery disabled") + } + if bw == nil { + t.Fatalf("Did not start upload upon failed cross mount with automatic content discovery disabled") + } + + bw, err = bs.Create(ctx, WithMountFrom(canonicalRef)) + } if bw != nil { t.Fatal("unexpected blobwriter returned from Create call, should mount instead") } - ebm, ok := err.(distribution.ErrBlobMounted) - if !ok { - t.Fatalf("unexpected error mounting layer: %v", err) + var mountedDescriptor distribution.Descriptor + if enableAutomaticContentDiscovery { + ebm := distribution.ErrBlobMounted{} + if !errors.As(err, &ebm) { + t.Fatalf("unexpected error mounting layer: %v", err) + } + mountedDescriptor = ebm.Descriptor + } else { + ebmf := distribution.ErrBlobMountedFrom{} + if !errors.As(err, &ebmf) { + t.Fatalf("unexpected error mounting layer: %v", err) + } + mountedDescriptor = ebmf.Descriptor } - if !reflect.DeepEqual(ebm.Descriptor, source.desc) { - t.Fatalf("descriptors not equal: %v != %v", ebm.Descriptor, source.desc) + if !reflect.DeepEqual(mountedDescriptor, source.desc) { + t.Fatalf("descriptors not equal: %v != %v", mountedDescriptor, source.desc) } // Test the negative case // This uses the source repo from the first repo, but the digest from the second + // Even in automatic content discovery, it is stated that the from *MAY* be ignored, but + // is not required to be ignored. wrongCanonicalRef, err := reference.WithDigest(source.repository.Named(), source2.desc.Digest) if err != nil { t.Fatal(err) diff --git a/registry/storage/linkedblobstore.go b/registry/storage/linkedblobstore.go index 89573ddc79d..53432b4683b 100644 --- a/registry/storage/linkedblobstore.go +++ b/registry/storage/linkedblobstore.go @@ -2,6 +2,7 @@ package storage import ( "context" + "errors" "fmt" "net/http" "path" @@ -15,6 +16,8 @@ import ( "github.com/opencontainers/go-digest" ) +var errNoDiscoveryMechanism = errors.New("No discovery mechanism for blob cross mount available") + // linkPathFunc describes a function that can resolve a link based on the // repository name and digest. type linkPathFunc func(name string, dgst digest.Digest) (string, error) @@ -24,13 +27,14 @@ type linkPathFunc func(name string, dgst digest.Digest) (string, error) // that grant access to the global blob store. type linkedBlobStore struct { *blobStore - registry *registry - blobServer distribution.BlobServer - blobAccessController distribution.BlobDescriptorService - repository distribution.Repository - ctx context.Context // only to be used where context can't come through method args - deleteEnabled bool - resumableDigestEnabled bool + registry *registry + blobServer distribution.BlobServer + blobAccessController distribution.BlobDescriptorService + repository distribution.Repository + ctx context.Context // only to be used where context can't come through method args + deleteEnabled bool + resumableDigestEnabled bool + automaticContentDiscovery bool // linkPathFns specifies one or more path functions allowing one to // control the repository blob link set to which the blob store @@ -117,8 +121,26 @@ func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption { return fmt.Errorf("unexpected options type: %T", v) } - opts.Mount.ShouldMount = true - opts.Mount.From = ref + opts.Mount = distribution.FromMount{ + From: ref, + } + + return nil + }) +} + +// WithMount returns a BlobCreateOption which designates that the blob should be +// mounted, and omits the from repository. +func WithMount(digest digest.Digest) distribution.BlobCreateOption { + return optionFunc(func(v interface{}) error { + opts, ok := v.(*distribution.CreateOptions) + if !ok { + return fmt.Errorf("unexpected options type: %T", v) + } + + opts.Mount = distribution.DigestMount{ + BlobDigest: digest, + } return nil }) @@ -137,11 +159,14 @@ func (lbs *linkedBlobStore) Create(ctx context.Context, options ...distribution. } } - if opts.Mount.ShouldMount { - desc, err := lbs.mount(ctx, opts.Mount.From, opts.Mount.From.Digest(), opts.Mount.Stat) + if opts.Mount != nil { + desc, ref, err := lbs.mount(ctx, opts.Mount) if err == nil { // Mount successful, no need to initiate an upload session - return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc} + if ref != nil { + return nil, distribution.ErrBlobMountedFrom{ErrBlobMounted: distribution.ErrBlobMounted{Descriptor: desc}, From: ref} + } + return nil, distribution.ErrBlobMounted{Descriptor: desc} } } @@ -275,21 +300,39 @@ func (lbs *linkedBlobStore) Enumerate(ctx context.Context, ingestor func(digest. }) } -func (lbs *linkedBlobStore) mount(ctx context.Context, sourceRepo reference.Named, dgst digest.Digest, sourceStat *distribution.Descriptor) (distribution.Descriptor, error) { +// mount attempts to perform a cross-repo mount. If the mount is successful, the blob descriptor will be returned. If the +// mount was a "FromMount", then the reference returned should be non-nil. +func (lbs *linkedBlobStore) mount(ctx context.Context, mount distribution.Mount) (distribution.Descriptor, reference.Canonical, error) { var stat distribution.Descriptor - if sourceStat == nil { + var from reference.Canonical + switch v := mount.(type) { + case distribution.FromMount: // look up the blob info from the sourceRepo if not already provided - repo, err := lbs.registry.Repository(ctx, sourceRepo) + repo, err := lbs.registry.Repository(ctx, v.From) if err != nil { - return distribution.Descriptor{}, err + return distribution.Descriptor{}, nil, err } - stat, err = repo.Blobs(ctx).Stat(ctx, dgst) + stat, err = repo.Blobs(ctx).Stat(ctx, v.Digest()) if err != nil { - return distribution.Descriptor{}, err + return distribution.Descriptor{}, nil, err } - } else { + from = v.From + case distribution.DigestMount: + if lbs.automaticContentDiscovery { + // We need to perform this enumeration because internally, there's a bunch of stuff that requires + // "from". This could be optimized, specifically if the bloblistener did not need to have the + // from information. + var err error + stat, err = lbs.registry.statter.Stat(ctx, v.Digest()) + if err != nil { + return distribution.Descriptor{}, nil, err + } + } else { + return distribution.Descriptor{}, nil, errNoDiscoveryMechanism + } + case distribution.StatMount: // use the provided blob info - stat = *sourceStat + stat = *v.Stat } desc := distribution.Descriptor{ @@ -299,9 +342,9 @@ func (lbs *linkedBlobStore) mount(ctx context.Context, sourceRepo reference.Name // other users. The caller should look this up and override the value // for the specific repository. MediaType: "application/octet-stream", - Digest: dgst, + Digest: mount.Digest(), } - return desc, lbs.linkBlob(ctx, desc) + return desc, from, lbs.linkBlob(ctx, desc) } // newBlobUpload allocates a new upload controller with the given state. diff --git a/registry/storage/linkedblobstore_test.go b/registry/storage/linkedblobstore_test.go index b08d4dcedc9..92b69dc137b 100644 --- a/registry/storage/linkedblobstore_test.go +++ b/registry/storage/linkedblobstore_test.go @@ -116,7 +116,11 @@ func TestLinkedBlobStoreCreateWithMountFrom(t *testing.T) { if err := option.Apply(&createOpts); err != nil { t.Fatalf("failed to apply MountFrom option: %v", err) } - if !createOpts.Mount.ShouldMount || createOpts.Mount.From.String() != fooCanonical.String() { + mount, ok := createOpts.Mount.(distribution.FromMount) + if !ok { + t.Fatalf("Expected mount to be FromMount") + } + if mount.From.String() != fooCanonical.String() { t.Fatalf("unexpected create options: %#+v", createOpts.Mount) } @@ -254,11 +258,13 @@ func (f statCrossMountCreateOption) Apply(v interface{}) error { return fmt.Errorf("Unexpected create options: %#v", v) } - if !opts.Mount.ShouldMount { + if opts.Mount != nil { return nil } - opts.Mount.Stat = &f.desc + opts.Mount = distribution.StatMount{ + Stat: &f.desc, + } return nil } diff --git a/registry/storage/registry.go b/registry/storage/registry.go index 45939083f7a..159b9cfcb99 100644 --- a/registry/storage/registry.go +++ b/registry/storage/registry.go @@ -21,6 +21,7 @@ type registry struct { deleteEnabled bool schema1Enabled bool resumableDigestEnabled bool + automaticContentDiscovery bool schema1SigningKey libtrust.PrivateKey blobDescriptorServiceFactory distribution.BlobDescriptorServiceFactory manifestURLs manifestURLs @@ -64,6 +65,13 @@ func DisableDigestResumption(registry *registry) error { return nil } +// EnableAutomaticContentDiscovery is a functional option for NewRegistry. It should be +// used if the user is not expected to specify the from parameter on a mount. +func EnableAutomaticContentDiscovery(registry *registry) error { + registry.automaticContentDiscovery = true + return nil +} + // ManifestURLsAllowRegexp is a functional option for NewRegistry. func ManifestURLsAllowRegexp(r *regexp.Regexp) RegistryOption { return func(registry *registry) error { @@ -329,9 +337,10 @@ func (repo *repository) Blobs(ctx context.Context) distribution.BlobStore { // TODO(stevvooe): linkPath limits this blob store to only layers. // This instance cannot be used for manifest checks. - linkPathFns: []linkPathFunc{blobLinkPath}, - linkDirectoryPathSpec: layersPathSpec{name: repo.name.Name()}, - deleteEnabled: repo.registry.deleteEnabled, - resumableDigestEnabled: repo.resumableDigestEnabled, + linkPathFns: []linkPathFunc{blobLinkPath}, + linkDirectoryPathSpec: layersPathSpec{name: repo.name.Name()}, + deleteEnabled: repo.registry.deleteEnabled, + automaticContentDiscovery: repo.registry.automaticContentDiscovery, + resumableDigestEnabled: repo.resumableDigestEnabled, } }