diff --git a/registry/storage/tagstore.go b/registry/storage/tagstore.go index a3c766b4..3bc3f5ac 100644 --- a/registry/storage/tagstore.go +++ b/registry/storage/tagstore.go @@ -177,3 +177,37 @@ func (ts *tagStore) Lookup(ctx context.Context, desc distribution.Descriptor) ([ return tags, nil } + +func (ts *tagStore) ManifestDigests(ctx context.Context, tag string) ([]digest.Digest, error) { + var tagLinkPath = func(name string, dgst digest.Digest) (string, error) { + return pathFor(manifestTagIndexEntryLinkPathSpec{ + name: name, + tag: tag, + revision: dgst, + }) + } + lbs := &linkedBlobStore{ + blobStore: ts.blobStore, + blobAccessController: &linkedBlobStatter{ + blobStore: ts.blobStore, + repository: ts.repository, + linkPathFns: []linkPathFunc{manifestRevisionLinkPath}, + }, + repository: ts.repository, + ctx: ctx, + linkPathFns: []linkPathFunc{tagLinkPath}, + linkDirectoryPathSpec: manifestTagIndexPathSpec{ + name: ts.repository.Named().Name(), + tag: tag, + }, + } + var dgsts []digest.Digest + err := lbs.Enumerate(ctx, func(dgst digest.Digest) error { + dgsts = append(dgsts, dgst) + return nil + }) + if err != nil { + return nil, err + } + return dgsts, nil +} diff --git a/registry/storage/tagstore_test.go b/registry/storage/tagstore_test.go index 314fa433..6f4c6c91 100644 --- a/registry/storage/tagstore_test.go +++ b/registry/storage/tagstore_test.go @@ -2,15 +2,22 @@ package storage import ( "context" + "reflect" "testing" "github.com/docker/distribution" + "github.com/docker/distribution/manifest" + "github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/driver/inmemory" + digest "github.com/opencontainers/go-digest" ) type tagsTestEnv struct { ts distribution.TagService + bs distribution.BlobStore + ms distribution.ManifestService + gbs distribution.BlobStatter ctx context.Context } @@ -27,10 +34,17 @@ func testTagStore(t *testing.T) *tagsTestEnv { if err != nil { t.Fatal(err) } + ms, err := repo.Manifests(ctx) + if err != nil { + t.Fatal(err) + } return &tagsTestEnv{ ctx: ctx, ts: repo.Tags(ctx), + bs: repo.Blobs(ctx), + gbs: reg.BlobStatter(), + ms: ms, } } @@ -205,5 +219,98 @@ func TestTagLookup(t *testing.T) { if len(tags) != 2 { t.Errorf("Lookup of descB returned %d tags, expected 2", len(tags)) } - +} + +func TestTagIndexes(t *testing.T) { + env := testTagStore(t) + tagStore := env.ts + ctx := env.ctx + + md, ok := tagStore.(distribution.TagManifestsProvider) + if !ok { + t.Fatal("tagStore does not implement TagManifestDigests interface") + } + + conf, err := env.bs.Put(ctx, "application/octet-stream", []byte{0}) + if err != nil { + t.Fatal(err) + } + + t1Dgsts := make(map[digest.Digest]struct{}) + t2Dgsts := make(map[digest.Digest]struct{}) + for i := 0; i < 5; i++ { + layer, err := env.bs.Put(ctx, "application/octet-stream", []byte{byte(i + 1)}) + if err != nil { + t.Fatal(err) + } + m := schema2.Manifest{ + Versioned: manifest.Versioned{ + SchemaVersion: 2, + MediaType: schema2.MediaTypeManifest, + }, + Config: distribution.Descriptor{ + Digest: conf.Digest, + Size: 1, + MediaType: schema2.MediaTypeImageConfig, + }, + Layers: []distribution.Descriptor{ + { + Digest: layer.Digest, + Size: 1, + MediaType: schema2.MediaTypeLayer, + }, + }, + } + dm, err := schema2.FromStruct(m) + if err != nil { + t.Fatal(err) + } + dgst, err := env.ms.Put(ctx, dm) + if err != nil { + t.Fatal(err) + } + desc, err := env.gbs.Stat(ctx, dgst) + if err != nil { + t.Fatal(err) + } + if i < 3 { + // tag first 3 manifests as "t1" + err = tagStore.Tag(ctx, "t1", desc) + if err != nil { + t.Fatal(err) + } + t1Dgsts[dgst] = struct{}{} + } else { + // the last two under "t2" + err = tagStore.Tag(ctx, "t2", desc) + if err != nil { + t.Fatal(err) + } + t2Dgsts[dgst] = struct{}{} + } + } + + gotT1Dgsts, err := md.ManifestDigests(ctx, "t1") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(t1Dgsts, digestMap(gotT1Dgsts)) { + t.Fatalf("Expected digests: %v but got digests: %v", t1Dgsts, digestMap(gotT1Dgsts)) + } + + gotT2Dgsts, err := md.ManifestDigests(ctx, "t2") + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(t2Dgsts, digestMap(gotT2Dgsts)) { + t.Fatalf("Expected digests: %v but got digests: %v", t2Dgsts, digestMap(gotT2Dgsts)) + } +} + +func digestMap(dgsts []digest.Digest) map[digest.Digest]struct{} { + set := make(map[digest.Digest]struct{}) + for _, dgst := range dgsts { + set[dgst] = struct{}{} + } + return set } diff --git a/tags.go b/tags.go index f22df2b8..f0e0bea3 100644 --- a/tags.go +++ b/tags.go @@ -2,6 +2,8 @@ package distribution import ( "context" + + digest "github.com/opencontainers/go-digest" ) // TagService provides access to information about tagged objects. @@ -25,3 +27,11 @@ type TagService interface { // Lookup returns the set of tags referencing the given digest. Lookup(ctx context.Context, digest Descriptor) ([]string, error) } + +// TagManifestsProvider provides method to retreive the digests of manifests that a tag historically +// pointed to +type TagManifestsProvider interface { + // ManifestDigests returns set of digests that this tag historically pointed to. This also + // includes currently linked digest. There is no ordering guaranteed + ManifestDigests(ctx context.Context, tag string) ([]digest.Digest, error) +}