diff --git a/content/memory/memory.go b/content/memory/memory.go index f4e6c1d9..8c3ef361 100644 --- a/content/memory/memory.go +++ b/content/memory/memory.go @@ -31,7 +31,7 @@ import ( // Store represents a memory based store, which implements `oras.Target`. type Store struct { - storage content.Storage + storage content.DeleteStorage resolver content.TagResolver graph *graph.Memory } @@ -72,6 +72,18 @@ func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descript return s.resolver.Resolve(ctx, reference) } +// Delete a target descriptor for storage. +func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error { + exists, err := s.storage.Exists(ctx, target) + if err != nil { + return err + } + if !exists { + return errdef.ErrNotFound + } + return s.storage.Delete(ctx, target) +} + // Tag tags a descriptor with a reference string. // Returns ErrNotFound if the tagged content does not exist. func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { diff --git a/content/memory/memory_test.go b/content/memory/memory_test.go index 6b91431e..5e7f58b8 100644 --- a/content/memory/memory_test.go +++ b/content/memory/memory_test.go @@ -401,6 +401,51 @@ func TestStorePredecessors(t *testing.T) { } } +func TestStoreDelete(t *testing.T) { + content := []byte("hello world") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + s := New() + ctx := context.Background() + + err := s.Push(ctx, desc, bytes.NewReader(content)) + if err != nil { + t.Fatal("Store.Push() error =", err) + } + ref := "foobar" + err = s.Tag(ctx, desc, ref) + if err != nil { + t.Fatal("Store.Tag() error =", err) + } + + internalResolver := s.resolver.(*resolver.Memory) + if got := len(internalResolver.Map()); got != 1 { + t.Errorf("resolver.Map() = %v, want %v", got, 1) + } + + exists, err := s.Exists(ctx, desc) + if err != nil { + t.Fatal("Store.Exists() error =", err) + } + if !exists { + t.Errorf("Store.Exists() = %v, want %v", exists, true) + } + + err = s.Delete(ctx, desc) + if err != nil { + t.Fatal("Store.Delete() error =", err) + } + + internalStorage := s.storage.(*cas.Memory) + if got := len(internalStorage.Map()); got != 0 { + t.Errorf("storage.Map() = %v, want %v", got, 0) + } +} + func equalDescriptorSet(actual []ocispec.Descriptor, expected []ocispec.Descriptor) bool { if len(actual) != len(expected) { return false diff --git a/content/oci/oci.go b/content/oci/oci.go index a473e5c1..44d551f7 100644 --- a/content/oci/oci.go +++ b/content/oci/oci.go @@ -60,7 +60,7 @@ type Store struct { index *ocispec.Index indexLock sync.Mutex - storage content.Storage + storage content.DeleteStorage tagResolver *resolver.Memory graph *graph.Memory } @@ -76,6 +76,7 @@ func NewWithContext(ctx context.Context, root string) (*Store, error) { if err != nil { return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", root, err) } + storage, err := NewStorage(rootAbs) if err != nil { return nil, fmt.Errorf("failed to create storage: %w", err) @@ -192,6 +193,44 @@ func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descript return desc, nil } +// Untag removes a reference string from index. +// reference should be a valid tag (e.g. "latest"). +// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/image-layout.md#indexjson-file +func (s *Store) Untag(ctx context.Context, descr ocispec.Descriptor, reference string) error { + if err := validateReference(reference); err != nil { + return err + } + + s.tagResolver.Delete((reference)) + s.tagResolver.Delete(descr.Digest.String()) + + if s.AutoSaveIndex { + err := s.SaveIndex() + if err != nil { + return err + } + } + + return nil +} + +// Delete removed a target descriptor from index and storage. +func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error { + resolvers := s.tagResolver.Map() + for reference, desc := range resolvers { + if content.Equal(desc, target) { + s.tagResolver.Delete(reference) + } + } + if s.AutoSaveIndex { + err := s.SaveIndex() + if err != nil { + return err + } + } + return s.storage.Delete(ctx, target) +} + // Predecessors returns the nodes directly pointing to the current node. // Predecessors returns nil without error if the node does not exists in the // store. diff --git a/content/oci/oci_test.go b/content/oci/oci_test.go index 02fb8014..cd17197c 100644 --- a/content/oci/oci_test.go +++ b/content/oci/oci_test.go @@ -926,6 +926,55 @@ func TestStore_TagByDigest(t *testing.T) { } } +func TestStore_Untag(t *testing.T) { + content := []byte("hello world") + ref := "hello-world:0.0.1" + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + tempDir := t.TempDir() + s, err := New(tempDir) + if err != nil { + t.Fatal("New() error =", err) + } + ctx := context.Background() + + err = s.Push(ctx, desc, bytes.NewReader(content)) + if err != nil { + t.Errorf("Store.Push() error = %v, wantErr %v", err, false) + } + + err = s.Tag(ctx, desc, ref) + if err != nil { + t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false) + } + + if len(s.tagResolver.Map()) == 0 { + t.Error("tagresolver map should not be empty") + } + + resolvedDescr, err := s.Resolve(ctx, string(desc.Digest)) + if err != nil { + t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false) + } + + if !reflect.DeepEqual(resolvedDescr, desc) { + t.Errorf("Store.Resolve() = %v, want %v", resolvedDescr, desc) + } + + err = s.Untag(ctx, resolvedDescr, ref) + if err != nil { + t.Errorf("error untagging descriptor error = %v, wantErr %v", err, false) + } + + if len(s.tagResolver.Map()) > 0 { + t.Error("tagresolver map should be empty") + } +} + func TestStore_BadIndex(t *testing.T) { tempDir := t.TempDir() content := []byte("whatever") @@ -1993,3 +2042,114 @@ func equalDescriptorSet(actual []ocispec.Descriptor, expected []ocispec.Descript } return true } + +func TestStore_Delete(t *testing.T) { + content := []byte("hello world") + ref := "hello-world:0.0.1" + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + tempDir := t.TempDir() + s, err := New(tempDir) + if err != nil { + t.Fatal("New() error =", err) + } + ctx := context.Background() + + err = s.Push(ctx, desc, bytes.NewReader(content)) + if err != nil { + t.Errorf("Store.Push() error = %v, wantErr %v", err, false) + } + + err = s.Tag(ctx, desc, ref) + if err != nil { + t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false) + } + + resolvedDescr, err := s.Resolve(ctx, ref) + if err != nil { + t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false) + } + + if !reflect.DeepEqual(resolvedDescr, desc) { + t.Errorf("Store.Resolve() = %v, want %v", resolvedDescr, desc) + } + + err = s.Delete(ctx, resolvedDescr) + if err != nil { + t.Errorf("Store.Delete() = %v, wantErr %v", err, true) + } + + _, err = s.Resolve(ctx, ref) + if !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("descriptor should no longer exist in store = %v, wantErr %v", err, errdef.ErrNotFound) + } +} + +func TestStore_DeleteDescriptoMultipleRefs(t *testing.T) { + content := []byte("hello world") + ref1 := "hello-world:0.0.1" + ref2 := "hello-world:0.0.2" + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + tempDir := t.TempDir() + s, err := New(tempDir) + s.AutoSaveIndex = true + if err != nil { + t.Fatal("New() error =", err) + } + ctx := context.Background() + + err = s.Push(ctx, desc, bytes.NewReader(content)) + if err != nil { + t.Errorf("Store.Push() error = %v, wantErr %v", err, false) + } + + if len(s.index.Manifests) != 0 { + t.Errorf("manifest should be empty but has %d elements", len(s.index.Manifests)) + } + + err = s.Tag(ctx, desc, ref1) + if err != nil { + t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false) + } + + err = s.Tag(ctx, desc, ref2) + if err != nil { + t.Errorf("error tagging descriptor error = %v, wantErr %v", err, false) + } + + if len(s.index.Manifests) != 2 { + t.Errorf("manifest should have %d, but has %d", len(s.index.Manifests), 0) + } + + resolvedDescr, err := s.Resolve(ctx, ref1) + if err != nil { + t.Errorf("error resolving descriptor error = %v, wantErr %v", err, false) + } + + if !reflect.DeepEqual(resolvedDescr, desc) { + t.Errorf("Store.Resolve() = %v, want %v", resolvedDescr, desc) + } + + err = s.Delete(ctx, resolvedDescr) + if err != nil { + t.Errorf("Store.Delete() = %v, wantErr %v", err, true) + } + + if len(s.index.Manifests) != 0 { + t.Errorf("manifest should be empty after delete but has %d", len(s.index.Manifests)) + } + + _, err = s.Resolve(ctx, ref2) + if !errors.Is(err, errdef.ErrNotFound) { + t.Errorf("descriptor should no longer exist in store = %v, wantErr %v", err, errdef.ErrNotFound) + } +} diff --git a/content/oci/storage.go b/content/oci/storage.go index 6b0e90a8..6b8739c6 100644 --- a/content/oci/storage.go +++ b/content/oci/storage.go @@ -106,6 +106,17 @@ func (s *Storage) Push(_ context.Context, expected ocispec.Descriptor, content i return nil } +// Delete removes the target blob. +func (s *Storage) Delete(ctx context.Context, target ocispec.Descriptor) error { + path, err := blobPath(target.Digest) + if err != nil { + return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest) + } + targetpath := filepath.Join(s.root, path) + + return os.Remove(targetpath) +} + // ingest write the content into a temporary ingest file. func (s *Storage) ingest(expected ocispec.Descriptor, content io.Reader) (path string, ingestErr error) { if err := ensureDir(s.ingestRoot); err != nil { diff --git a/content/oci/storage_test.go b/content/oci/storage_test.go index 7e3b1e58..1297792c 100644 --- a/content/oci/storage_test.go +++ b/content/oci/storage_test.go @@ -377,3 +377,27 @@ func TestStorage_Fetch_Concurrent(t *testing.T) { t.Fatal(err) } } + +func TestStorage_Delete(t *testing.T) { + content := []byte("hello world") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + tempDir := t.TempDir() + s, err := NewStorage(tempDir) + if err != nil { + t.Fatal("New() error =", err) + } + ctx := context.Background() + + if err := s.Push(ctx, desc, bytes.NewReader(content)); err != nil { + t.Fatal("Storage.Push() error =", err) + } + err = s.Delete(ctx, desc) + if err != nil { + t.Fatal("Storage.Delete() error =", err) + } +} diff --git a/content/storage.go b/content/storage.go index 971142cb..644467c3 100644 --- a/content/storage.go +++ b/content/storage.go @@ -45,6 +45,12 @@ type Storage interface { Pusher } +// DeleteStorage represents an extension of the Storage interface that includes the Deleter. +type DeleteStorage interface { + Storage + Deleter +} + // ReadOnlyStorage represents a read-only Storage. type ReadOnlyStorage interface { Fetcher diff --git a/internal/cas/memory.go b/internal/cas/memory.go index 7e358e13..5190c63c 100644 --- a/internal/cas/memory.go +++ b/internal/cas/memory.go @@ -86,3 +86,14 @@ func (m *Memory) Map() map[descriptor.Descriptor][]byte { }) return res } + +// Delete removes a target descriptor from content map. +func (m *Memory) Delete(ctx context.Context, target ocispec.Descriptor) error { + key := descriptor.FromOCI(target) + + _, deleted := m.content.LoadAndDelete(key) + if !deleted { + return errdef.ErrNotFound + } + return nil +} diff --git a/internal/cas/memory_test.go b/internal/cas/memory_test.go index aaacf984..7fb964f0 100644 --- a/internal/cas/memory_test.go +++ b/internal/cas/memory_test.go @@ -136,3 +136,56 @@ func TestMemoryBadPush(t *testing.T) { t.Errorf("Memory.Push() error = %v, wantErr %v", err, true) } } + +func TestMemoryDelete(t *testing.T) { + content := []byte("hello world") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + s := NewMemory() + ctx := context.Background() + + err := s.Push(ctx, desc, bytes.NewReader(content)) + if err != nil { + t.Fatal("Memory.Push() error =", err) + } + + exists, err := s.Exists(ctx, desc) + if err != nil { + t.Fatal("Memory.Exists() error =", err) + } + if !exists { + t.Errorf("Memory.Exists() = %v, want %v", exists, true) + } + if got := len(s.Map()); got != 1 { + t.Errorf("Memory.Map() = %v, want %v", got, 1) + } + + err = s.Delete(ctx, desc) + if err != nil { + t.Fatal("Memory.Delete() error =", err) + } + if got := len(s.Map()); got != 0 { + t.Errorf("Memory.Map() = %v, want %v", got, 0) + } +} + +func TestMemoryBadDelete(t *testing.T) { + content := []byte("hello world") + desc := ocispec.Descriptor{ + MediaType: "test", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + + s := NewMemory() + ctx := context.Background() + + err := s.Delete(ctx, desc) + if err == nil { + t.Errorf("Memory.Delete() error = %v, wantErr %v", err, true) + } +} diff --git a/internal/resolver/memory.go b/internal/resolver/memory.go index 6fac5e2d..27d6a59d 100644 --- a/internal/resolver/memory.go +++ b/internal/resolver/memory.go @@ -48,6 +48,11 @@ func (m *Memory) Tag(_ context.Context, desc ocispec.Descriptor, reference strin return nil } +// Delete removes a reference from index map. +func (m *Memory) Delete(reference string) { + m.index.Delete(reference) +} + // 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.