diff --git a/content/file/file.go b/content/file/file.go index 57d583e8f..f742ce7d5 100644 --- a/content/file/file.go +++ b/content/file/file.go @@ -349,6 +349,29 @@ func (s *Store) Resolve(ctx context.Context, ref string) (ocispec.Descriptor, er return s.resolver.Resolve(ctx, ref) } +func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error { + if s.isClosedSet() { + return ErrStoreClosed + } + exists, err := s.Exists(ctx, target) + if err != nil { + return err + } + if !exists { + return errdef.ErrNotFound + } + path, _ := s.digestToPath.Load(target.Digest) + pathString, ok := path.(string) + if !ok { + return errdef.ErrInvalidDigest + } + err = os.RemoveAll(s.absPath(pathString)) + if err != nil { + return s.fallbackStorage.Delete(ctx, target) + } + return err +} + // Tag tags a descriptor with a reference string. func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, ref string) error { if s.isClosedSet() { diff --git a/content/file/file_test.go b/content/file/file_test.go index 66548aef8..c9c8282d7 100644 --- a/content/file/file_test.go +++ b/content/file/file_test.go @@ -2988,6 +2988,36 @@ func TestCopyGraph_FileToMemory_PartialCopy(t *testing.T) { } } +func TestStore_Delete(t *testing.T) { + tempDir := t.TempDir() + s, err := New(tempDir) + if err != nil { + t.Fatal("Store.New() error =", err) + } + defer s.Close() + ctx := context.Background() + + blob := []byte("hello world") + name := "test.txt" + mediaType := "test" + + path := filepath.Join(tempDir, name) + if err := os.WriteFile(path, blob, 0444); err != nil { + t.Fatal("error calling WriteFile(), error =", err) + } + + // test blob add + gotDesc, err := s.Add(ctx, name, mediaType, path) + if err != nil { + t.Fatal("Store.Add() error =", err) + } + + err = s.Delete(ctx, gotDesc) + if err != nil { + t.Fatal("Store.Delete() error =", err) + } +} + func equalDescriptorSet(actual []ocispec.Descriptor, expected []ocispec.Descriptor) bool { if len(actual) != len(expected) { return false diff --git a/content/memory/memory.go b/content/memory/memory.go index f4e6c1d96..7b3a2a46e 100644 --- a/content/memory/memory.go +++ b/content/memory/memory.go @@ -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 6b91431e6..5e7f58b88 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 a473e5c1e..1fe228313 100644 --- a/content/oci/oci.go +++ b/content/oci/oci.go @@ -192,6 +192,22 @@ func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descript return desc, nil } +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.Remove(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 02fb80147..47eb29523 100644 --- a/content/oci/oci_test.go +++ b/content/oci/oci_test.go @@ -1993,3 +1993,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 6b0e90a8c..cd9a68ca3 100644 --- a/content/oci/storage.go +++ b/content/oci/storage.go @@ -106,6 +106,22 @@ func (s *Storage) Push(_ context.Context, expected ocispec.Descriptor, content i return nil } +// Delete removes the descriptor blob from the storage +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) + } + fullpath := filepath.Join(s.root, path) + + // check if the target content does not exists in the blob directory. + if _, err := os.Stat(fullpath); os.IsNotExist(err) { + return err + } + + return os.Remove(fullpath) +} + // 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 7e3b1e58f..1297792c5 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 971142cbf..16b351c18 100644 --- a/content/storage.go +++ b/content/storage.go @@ -43,6 +43,7 @@ type Pusher interface { type Storage interface { ReadOnlyStorage Pusher + Deleter } // ReadOnlyStorage represents a read-only Storage. diff --git a/internal/cas/memory.go b/internal/cas/memory.go index 7e358e136..05252d327 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 } + +func (m *Memory) Delete(ctx context.Context, target ocispec.Descriptor) error { + key := descriptor.FromOCI(target) + + // check if the content exists in advance to avoid reading from the content. + if _, exists := m.content.Load(key); !exists { + return fmt.Errorf("Cannot delete %s: %s: %w", key.Digest, key.MediaType, errdef.ErrNotFound) + } + m.content.Delete(key) + return nil +} diff --git a/internal/cas/memory_test.go b/internal/cas/memory_test.go index aaacf984f..7fb964f0b 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/cas/proxy.go b/internal/cas/proxy.go index ada5f94e0..f9cde1922 100644 --- a/internal/cas/proxy.go +++ b/internal/cas/proxy.go @@ -17,6 +17,7 @@ package cas import ( "context" + "fmt" "io" "sync" @@ -123,3 +124,8 @@ func (p *Proxy) Exists(ctx context.Context, target ocispec.Descriptor) (bool, er } return p.ReadOnlyStorage.Exists(ctx, target) } + +// Delete not implemented as it uses a read only storage. +func (m *Proxy) Delete(ctx context.Context, target ocispec.Descriptor) error { + return fmt.Errorf("not implemented") +} diff --git a/internal/resolver/memory.go b/internal/resolver/memory.go index 6fac5e2d0..ac461c77c 100644 --- a/internal/resolver/memory.go +++ b/internal/resolver/memory.go @@ -48,6 +48,10 @@ func (m *Memory) Tag(_ context.Context, desc ocispec.Descriptor, reference strin return nil } +func (m *Memory) Remove(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.