diff --git a/content/oci/oci.go b/content/oci/oci.go index ccefc0d9..b7494b4e 100644 --- a/content/oci/oci.go +++ b/content/oci/oci.go @@ -36,6 +36,7 @@ import ( "oras.land/oras-go/v2/internal/container/set" "oras.land/oras-go/v2/internal/descriptor" "oras.land/oras-go/v2/internal/graph" + "oras.land/oras-go/v2/internal/manifestutil" "oras.land/oras-go/v2/internal/resolver" "oras.land/oras-go/v2/registry" ) @@ -57,8 +58,8 @@ type Store struct { // AutoGC controls if the OCI store will automatically clean newly produced // dangling (unreferenced) blobs during Delete() operation. For example the - // blobs whose manifests have been deleted. Manifests in index.json will not - // be deleted. + // blobs whose manifests have been deleted. Tagged manifests will not be + // deleted. // - Default value: true. AutoGC bool @@ -76,8 +77,9 @@ type Store struct { graph *graph.Memory // sync ensures that most operations can be done concurrently, while Delete - // has the exclusive access to Store if a delete operation is underway. Operations - // such as Fetch, Push use sync.RLock(), while Delete uses sync.Lock(). + // has the exclusive access to Store if a delete operation is underway. + // Operations such as Fetch, Push use sync.RLock(), while Delete uses + // sync.Lock(). sync sync.RWMutex // indexLock ensures that only one go-routine is writing to the index. indexLock sync.Mutex @@ -190,9 +192,8 @@ func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error { } if s.AutoGC { for _, d := range danglings { - // do not delete existing manifests in tagResolver - _, err = s.tagResolver.Resolve(ctx, string(d.Digest)) - if errors.Is(err, errdef.ErrNotFound) { + // do not delete existing tagged manifests + if !s.isTagged(d) { deleteQueue = append(deleteQueue, d) } } @@ -455,19 +456,6 @@ func (s *Store) writeIndexFile() error { return os.WriteFile(s.indexPath, indexJSON, 0666) } -// reloadIndex reloads the index and updates metadata by creating a new store. -func (s *Store) reloadIndex(ctx context.Context) error { - newStore, err := NewWithContext(ctx, s.root) - if err != nil { - return err - } - s.index = newStore.index - s.storage = newStore.storage - s.tagResolver = newStore.tagResolver - s.graph = newStore.graph - return nil -} - // GC removes garbage from Store. Unsaved index will be lost. To prevent unexpected // loss, call SaveIndex() before GC or set AutoSaveIndex to true. // The garbage to be cleaned are: @@ -478,7 +466,7 @@ func (s *Store) GC(ctx context.Context) error { defer s.sync.Unlock() // get reachable nodes by reloading the index - err := s.reloadIndex(ctx) + err := s.gcIndex(ctx) if err != nil { return fmt.Errorf("unable to reload index: %w", err) } @@ -526,6 +514,73 @@ func (s *Store) GC(ctx context.Context) error { return nil } +// gcIndex reloads the index and updates metadata. Information of untagged blobs +// are cleaned and only tagged blobs remain. +func (s *Store) gcIndex(ctx context.Context) error { + tagResolver := resolver.NewMemory() + graph := graph.NewMemory() + tagged := set.New[digest.Digest]() + + // index tagged manifests + refMap := s.tagResolver.Map() + for ref, desc := range refMap { + if ref == desc.Digest.String() { + continue + } + if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil { + return err + } + if err := tagResolver.Tag(ctx, desc, ref); err != nil { + return err + } + plain := descriptor.Plain(desc) + if err := graph.IndexAll(ctx, s.storage, plain); err != nil { + return err + } + tagged.Add(desc.Digest) + } + + // index referrer manifests + for ref, desc := range refMap { + if ref != desc.Digest.String() || tagged.Contains(desc.Digest) { + continue + } + // check if the referrers manifest can traverse to the existing graph + subject := &desc + for { + subject, err := manifestutil.Subject(ctx, s.storage, *subject) + if err != nil { + return err + } + if subject == nil { + break + } + if graph.Exists(*subject) { + if err := tagResolver.Tag(ctx, deleteAnnotationRefName(desc), desc.Digest.String()); err != nil { + return err + } + plain := descriptor.Plain(desc) + if err := graph.IndexAll(ctx, s.storage, plain); err != nil { + return err + } + break + } + } + } + s.tagResolver = tagResolver + s.graph = graph + return nil +} + +// isTagged checks if the blob given by the descriptor is tagged. +func (s *Store) isTagged(desc ocispec.Descriptor) bool { + tagSet := s.tagResolver.TagSet(desc) + if tagSet.Contains(string(desc.Digest)) { + return len(tagSet) > 1 + } + return len(tagSet) > 0 +} + // unsafeStore is used to bypass lock restrictions in Delete. type unsafeStore struct { *Store diff --git a/content/oci/oci_test.go b/content/oci/oci_test.go index 42702a6d..aaf7f45f 100644 --- a/content/oci/oci_test.go +++ b/content/oci/oci_test.go @@ -2900,7 +2900,7 @@ func TestStore_GC(t *testing.T) { appendBlob(ocispec.MediaTypeImageLayer, []byte("blob")) // Blob 1 appendBlob(ocispec.MediaTypeImageLayer, []byte("dangling layer")) // Blob 2, dangling layer generateManifest(descs[0], nil, descs[1]) // Blob 3, valid manifest - generateManifest(descs[0], &descs[3], descs[1]) // Blob 4, referrer of a valid manifest, not in index.json, should be cleaned with current implementation + generateManifest(descs[0], &descs[3], descs[1]) // Blob 4, referrer of a valid manifest appendBlob(ocispec.MediaTypeImageLayer, []byte("dangling layer 2")) // Blob 5, dangling layer generateArtifactManifest(descs[4]) // blob 6, dangling artifact generateManifest(descs[0], &descs[5], descs[1]) // Blob 7, referrer of a dangling manifest @@ -2913,6 +2913,8 @@ func TestStore_GC(t *testing.T) { appendBlob(ocispec.MediaTypeImageLayer, []byte("garbage layer 2")) // Blob 14, garbage layer 2 generateManifest(descs[6], nil, descs[7]) // Blob 15, garbage manifest 2 generateManifest(descs[0], &descs[13], descs[1]) // Blob 16, referrer of a garbage manifest + appendBlob(ocispec.MediaTypeImageLayer, []byte("another layer")) // Blob 17, untagged manifest + generateManifest(descs[0], nil, descs[17]) // Blob 18, valid untagged manifest // push blobs 0 - blobs 10 into s for i := 0; i <= 10; i++ { @@ -2922,15 +2924,23 @@ func TestStore_GC(t *testing.T) { } } - // remove blobs 4 - blobs 10 from index.json - for i := 4; i <= 10; i++ { + // push blobs 17 - blobs 18 into s + for i := 17; i <= 18; i++ { + err := s.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Errorf("failed to push test content to src: %d: %v", i, err) + } + } + + // remove blobs 5 - blobs 10 from index.json + for i := 5; i <= 10; i++ { s.tagResolver.Untag(string(descs[i].Digest)) } s.SaveIndex() // push blobs 11 - blobs 16 into s.storage, making them garbage as their metadata // doesn't exist in s - for i := 11; i < len(blobs); i++ { + for i := 11; i < 17; i++ { err := s.storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) if err != nil { t.Errorf("failed to push test content to src: %d: %v", i, err) @@ -2938,7 +2948,7 @@ func TestStore_GC(t *testing.T) { } // confirm that all the blobs are in the storage - for i := 11; i < len(blobs); i++ { + for i := 0; i < len(blobs); i++ { exists, err := s.Exists(ctx, descs[i]) if err != nil { t.Fatal(err) @@ -2948,13 +2958,113 @@ func TestStore_GC(t *testing.T) { } } - // perform GC + // tag manifest blob 3 + s.Tag(ctx, descs[3], "latest") + + // perform double GC + if err = s.GC(ctx); err != nil { + t.Fatal(err) + } if err = s.GC(ctx); err != nil { t.Fatal(err) } // verify existence - wantExistence := []bool{true, true, false, true, false, false, false, false, false, false, false, false, false, false, false, false, false} + wantExistence := []bool{true, true, false, true, true, false, false, false, + false, false, false, false, false, false, false, false, false, false, false} + for i, wantValue := range wantExistence { + exists, err := s.Exists(ctx, descs[i]) + if err != nil { + t.Fatal(err) + } + if exists != wantValue { + t.Fatalf("want existence %d to be %v, got %v", i, wantValue, exists) + } + } +} + +func TestStore_GCAndDeleteOnIndex(t *testing.T) { + tempDir := t.TempDir() + s, err := New(tempDir) + if err != nil { + t.Fatal("New() error =", err) + } + ctx := context.Background() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Subject: subject, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + generateImageIndex := func(manifests ...ocispec.Descriptor) { + index := ocispec.Index{ + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageIndex, indexJSON) + } + + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("blob1")) // Blob 1 + generateManifest(descs[0], nil, descs[1]) // Blob 2, manifest + appendBlob(ocispec.MediaTypeImageLayer, []byte("blob2")) // Blob 3 + generateManifest(descs[0], nil, descs[3]) // Blob 4, manifest + appendBlob(ocispec.MediaTypeImageLayer, []byte("blob3")) // Blob 5 + generateManifest(descs[0], nil, descs[5]) // Blob 6, manifest + appendBlob(ocispec.MediaTypeImageLayer, []byte("blob4")) // Blob 7 + generateManifest(descs[0], nil, descs[7]) // Blob 8, manifest + generateImageIndex(descs[2], descs[4], descs[6], descs[8]) // blob 9, image index + + // push all blobs into the store + for i := 0; i < len(blobs); i++ { + err := s.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Errorf("failed to push test content to src: %d: %v", i, err) + } + } + + // confirm that all the blobs are in the storage + for i := 0; i < len(blobs); i++ { + exists, err := s.Exists(ctx, descs[i]) + if err != nil { + t.Fatal(err) + } + if !exists { + t.Fatalf("descs[%d] should exist", i) + } + } + + // tag manifest blob 8 + s.Tag(ctx, descs[8], "latest") + + // delete the image index + if err := s.Delete(ctx, descs[9]); err != nil { + t.Fatal(err) + } + + // verify existence + wantExistence := []bool{true, false, false, false, false, false, false, true, true, false} for i, wantValue := range wantExistence { exists, err := s.Exists(ctx, descs[i]) if err != nil { diff --git a/internal/graph/memory.go b/internal/graph/memory.go index aa735552..016e5f96 100644 --- a/internal/graph/memory.go +++ b/internal/graph/memory.go @@ -150,6 +150,9 @@ func (m *Memory) Remove(node ocispec.Descriptor) []ocispec.Descriptor { // DigestSet returns the set of node digest in memory. func (m *Memory) DigestSet() set.Set[digest.Digest] { + m.lock.RLock() + defer m.lock.RUnlock() + s := set.New[digest.Digest]() for desc := range m.nodes { s.Add(desc.Digest) @@ -186,3 +189,13 @@ func (m *Memory) index(ctx context.Context, fetcher content.Fetcher, node ocispe } return successors, nil } + +// Exists checks if the node exists in the graph +func (m *Memory) Exists(node ocispec.Descriptor) bool { + m.lock.RLock() + defer m.lock.RUnlock() + + nodeKey := descriptor.FromOCI(node) + _, exists := m.nodes[nodeKey] + return exists +} diff --git a/internal/graph/memory_test.go b/internal/graph/memory_test.go index 89ef4446..bdb0eb5f 100644 --- a/internal/graph/memory_test.go +++ b/internal/graph/memory_test.go @@ -688,3 +688,80 @@ func TestMemory_DigestSet(t *testing.T) { } } } + +func TestMemory_Exists(t *testing.T) { + testFetcher := cas.NewMemory() + testMemory := NewMemory() + ctx := context.Background() + + // generate test content + var blobs [][]byte + var descriptors []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) ocispec.Descriptor { + blobs = append(blobs, blob) + descriptors = append(descriptors, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + return descriptors[len(descriptors)-1] + } + generateManifest := func(layers ...ocispec.Descriptor) ocispec.Descriptor { + manifest := ocispec.Manifest{ + Config: ocispec.Descriptor{MediaType: "test config"}, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + return appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + generateIndex := func(manifests ...ocispec.Descriptor) ocispec.Descriptor { + index := ocispec.Index{ + Manifests: manifests, + } + indexJSON, err := json.Marshal(index) + if err != nil { + t.Fatal(err) + } + return appendBlob(ocispec.MediaTypeImageIndex, indexJSON) + } + descE := appendBlob("layer node E", []byte("Node E is a layer")) // blobs[0], layer "E" + descF := appendBlob("layer node F", []byte("Node F is a layer")) // blobs[1], layer "F" + descB := generateManifest(descriptors[0:1]...) // blobs[2], manifest "B" + descC := generateManifest(descriptors[0:2]...) // blobs[3], manifest "C" + descD := generateManifest(descriptors[1:2]...) // blobs[4], manifest "D" + descA := generateIndex(descriptors[2:5]...) // blobs[5], index "A" + + // prepare the content in the fetcher, so that it can be used to test IndexAll + testContents := []ocispec.Descriptor{descE, descF, descB, descC, descD, descA} + for i := 0; i < len(blobs); i++ { + testFetcher.Push(ctx, testContents[i], bytes.NewReader(blobs[i])) + } + + // make sure that testFetcher works + rc, err := testFetcher.Fetch(ctx, descA) + if err != nil { + t.Errorf("testFetcher.Fetch() error = %v", err) + } + got, err := io.ReadAll(rc) + if err != nil { + t.Errorf("testFetcher.Fetch().Read() error = %v", err) + } + err = rc.Close() + if err != nil { + t.Errorf("testFetcher.Fetch().Close() error = %v", err) + } + if !bytes.Equal(got, blobs[5]) { + t.Errorf("testFetcher.Fetch() = %v, want %v", got, blobs[4]) + } + + // index node A into testMemory using IndexAll + testMemory.IndexAll(ctx, testFetcher, descA) + for i := 0; i < len(blobs); i++ { + if exists := testMemory.Exists(descriptors[i]); exists != true { + t.Errorf("digest of blob[%d] should exist in digestSet", i) + } + } +} diff --git a/internal/manifestutil/parser.go b/internal/manifestutil/parser.go index c904dc69..89d556b8 100644 --- a/internal/manifestutil/parser.go +++ b/internal/manifestutil/parser.go @@ -22,6 +22,7 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/internal/docker" + "oras.land/oras-go/v2/internal/spec" ) // Config returns the config of desc, if present. @@ -61,3 +62,23 @@ func Manifests(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descri return nil, nil } } + +// Subject returns the subject of desc, if present. +func Subject(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) (*ocispec.Descriptor, error) { + switch desc.MediaType { + case ocispec.MediaTypeImageManifest, ocispec.MediaTypeImageIndex, spec.MediaTypeArtifactManifest: + content, err := content.FetchAll(ctx, fetcher, desc) + if err != nil { + return nil, err + } + var manifest struct { + Subject *ocispec.Descriptor `json:"subject,omitempty"` + } + if err := json.Unmarshal(content, &manifest); err != nil { + return nil, err + } + return manifest.Subject, nil + default: + return nil, nil + } +} diff --git a/internal/manifestutil/parser_test.go b/internal/manifestutil/parser_test.go index 44c5e43e..487e11dc 100644 --- a/internal/manifestutil/parser_test.go +++ b/internal/manifestutil/parser_test.go @@ -19,15 +19,48 @@ import ( "bytes" "context" "encoding/json" + "errors" + "fmt" + "io" "reflect" "testing" "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/sync/errgroup" + "oras.land/oras-go/v2/content/memory" "oras.land/oras-go/v2/internal/cas" + "oras.land/oras-go/v2/internal/container/set" "oras.land/oras-go/v2/internal/docker" ) +var ErrBadFetch = errors.New("bad fetch error") + +// testStorage implements Fetcher +type testStorage struct { + store *memory.Store + badFetch set.Set[digest.Digest] +} + +func (s *testStorage) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error { + return s.store.Push(ctx, expected, reader) +} + +func (s *testStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + if s.badFetch.Contains(target.Digest) { + return nil, ErrBadFetch + } + return s.store.Fetch(ctx, target) +} + +// func (s *testStorage) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { +// return s.store.Exists(ctx, target) +// } + +// func (s *testStorage) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { +// return s.store.Predecessors(ctx, node) +// } + func TestConfig(t *testing.T) { storage := cas.NewMemory() @@ -200,3 +233,121 @@ func TestManifests(t *testing.T) { }) } } + +func TestSubject(t *testing.T) { + storage := cas.NewMemory() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + Config: config, + Subject: subject, + Layers: layers, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) + } + appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, []byte("blob")) // Blob 1 + generateManifest(descs[0], nil, descs[1]) // Blob 2, manifest + generateManifest(descs[0], &descs[2], descs[1]) // Blob 3, referrer of blob 2 + + ctx := context.Background() + for i := range blobs { + err := storage.Push(ctx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + t.Fatalf("failed to push test content to src: %d: %v", i, err) + } + } + got, err := Subject(ctx, storage, descs[3]) + if err != nil { + t.Fatalf("error when getting subject: %v", err) + } + if !reflect.DeepEqual(*got, descs[2]) { + t.Errorf("Subject() = %v, want %v", got, descs[2]) + } + got, err = Subject(ctx, storage, descs[0]) + if err != nil { + t.Fatalf("error when getting subject: %v", err) + } + if got != nil { + t.Errorf("Subject() = %v, want %v", got, nil) + } +} + +func TestSubject_ErrorPath(t *testing.T) { + s := testStorage{ + store: memory.New(), + badFetch: set.New[digest.Digest](), + } + ctx := context.Background() + + // generate test content + var blobs [][]byte + var descs []ocispec.Descriptor + appendBlob := func(mediaType string, artifactType string, blob []byte) { + blobs = append(blobs, blob) + descs = append(descs, ocispec.Descriptor{ + MediaType: mediaType, + ArtifactType: artifactType, + Annotations: map[string]string{"test": "content"}, + Digest: digest.FromBytes(blob), + Size: int64(len(blob)), + }) + } + generateImageManifest := func(config ocispec.Descriptor, subject *ocispec.Descriptor, layers ...ocispec.Descriptor) { + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Config: config, + Subject: subject, + Layers: layers, + Annotations: map[string]string{"test": "content"}, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal(err) + } + appendBlob(ocispec.MediaTypeImageManifest, manifest.Config.MediaType, manifestJSON) + } + appendBlob("image manifest", "image config", []byte("config")) // Blob 0 + appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("foo")) // Blob 1 + appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("bar")) // Blob 2 + appendBlob(ocispec.MediaTypeImageLayer, "layer", []byte("hello")) // Blob 3 + generateImageManifest(descs[0], nil, descs[1]) // Blob 4 + generateImageManifest(descs[0], &descs[4], descs[2]) // Blob 5 + s.badFetch.Add(descs[5].Digest) + + eg, egCtx := errgroup.WithContext(ctx) + for i := range blobs { + eg.Go(func(i int) func() error { + return func() error { + err := s.Push(egCtx, descs[i], bytes.NewReader(blobs[i])) + if err != nil { + return fmt.Errorf("failed to push test content to src: %d: %v", i, err) + } + return nil + } + }(i)) + } + if err := eg.Wait(); err != nil { + t.Fatal(err) + } + + _, err := Subject(ctx, &s, descs[5]) + if !errors.Is(err, ErrBadFetch) { + t.Errorf("Store.Referrers() error = %v, want %v", err, ErrBadFetch) + } +} diff --git a/internal/resolver/memory.go b/internal/resolver/memory.go index 2111d504..80843e43 100644 --- a/internal/resolver/memory.go +++ b/internal/resolver/memory.go @@ -19,48 +19,93 @@ import ( "context" "sync" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "oras.land/oras-go/v2/errdef" + "oras.land/oras-go/v2/internal/container/set" ) // Memory is a memory based resolver. type Memory struct { - index sync.Map // map[string]ocispec.Descriptor + lock sync.RWMutex + index map[string]ocispec.Descriptor + tags map[digest.Digest]set.Set[string] } // NewMemory creates a new Memory resolver. func NewMemory() *Memory { - return &Memory{} + return &Memory{ + index: make(map[string]ocispec.Descriptor), + tags: make(map[digest.Digest]set.Set[string]), + } } // Resolve resolves a reference to a descriptor. func (m *Memory) Resolve(_ context.Context, reference string) (ocispec.Descriptor, error) { - desc, ok := m.index.Load(reference) + m.lock.RLock() + defer m.lock.RUnlock() + + desc, ok := m.index[reference] if !ok { return ocispec.Descriptor{}, errdef.ErrNotFound } - return desc.(ocispec.Descriptor), nil + return desc, nil } // Tag tags a descriptor with a reference string. func (m *Memory) Tag(_ context.Context, desc ocispec.Descriptor, reference string) error { - m.index.Store(reference, desc) + m.lock.Lock() + defer m.lock.Unlock() + + m.index[reference] = desc + tagSet, ok := m.tags[desc.Digest] + if !ok { + tagSet = set.New[string]() + m.tags[desc.Digest] = tagSet + } + tagSet.Add(reference) return nil } // Untag removes a reference from index map. func (m *Memory) Untag(reference string) { - m.index.Delete(reference) + m.lock.Lock() + defer m.lock.Unlock() + + desc, ok := m.index[reference] + if !ok { + return + } + delete(m.index, reference) + tagSet := m.tags[desc.Digest] + tagSet.Delete(reference) + if len(tagSet) == 0 { + delete(m.tags, desc.Digest) + } } // Map dumps the memory into a built-in map structure. -// Like other operations, calling Map() is go-routine safe. However, it does not -// necessarily correspond to any consistent snapshot of the storage contents. +// Like other operations, calling Map() is go-routine safe. func (m *Memory) Map() map[string]ocispec.Descriptor { - res := make(map[string]ocispec.Descriptor) - m.index.Range(func(key, value interface{}) bool { - res[key.(string)] = value.(ocispec.Descriptor) - return true - }) + m.lock.RLock() + defer m.lock.RUnlock() + + res := make(map[string]ocispec.Descriptor, len(m.index)) + for key, value := range m.index { + res[key] = value + } + return res +} + +// TagSet returns the set of tags of the descriptor. +func (m *Memory) TagSet(desc ocispec.Descriptor) set.Set[string] { + m.lock.RLock() + defer m.lock.RUnlock() + + tagSet := m.tags[desc.Digest] + res := make(set.Set[string], len(tagSet)) + for tag := range tagSet { + res.Add(tag) + } return res } diff --git a/internal/resolver/memory_test.go b/internal/resolver/memory_test.go index f6f04b06..5e16fd9f 100644 --- a/internal/resolver/memory_test.go +++ b/internal/resolver/memory_test.go @@ -76,3 +76,33 @@ func TestMemoryNotFound(t *testing.T) { t.Errorf("Memory.Resolve() error = %v, want %v", err, errdef.ErrNotFound) } } + +func TestTagSet(t *testing.T) { + refFoo := "foo" + refBar := "bar" + + s := NewMemory() + ctx := context.Background() + + content := []byte("hello world") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + s.Tag(ctx, desc, refFoo) + s.Tag(ctx, desc, refBar) + + tagSet := s.TagSet(desc) + + if !tagSet.Contains(refFoo) { + t.Fatalf("tagSet should contain %s", refFoo) + } + if !tagSet.Contains(refBar) { + t.Fatalf("tagSet should contain %s", refFoo) + } + if len(tagSet) != 2 { + t.Fatalf("expect size = %d, got %d", 2, len(tagSet)) + } +}