diff --git a/content/oci/deletable.go b/content/oci/deletable.go deleted file mode 100644 index 1e1dd7a6..00000000 --- a/content/oci/deletable.go +++ /dev/null @@ -1,163 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package oci provides access to an OCI content store. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md -package oci - -import ( - "context" - "io" - "sync" - - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "oras.land/oras-go/v2/content" -) - -// DeletableStore implements `oras.Target`, and represents a content store -// extended with the delete operation. -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md -type DeletableStore struct { - *Store - lock sync.RWMutex -} - -// NewDeletableStore returns a new DeletableStore with context.Background(). -func NewDeletableStore(root string) (*DeletableStore, error) { - return NewDeletableStoreWithContext(context.Background(), root) -} - -// NewDeletableStoreWithContext returns a new DeletableStore. -func NewDeletableStoreWithContext(ctx context.Context, root string) (*DeletableStore, error) { - store, err := NewWithContext(ctx, root) - if err != nil { - return nil, err - } - return &DeletableStore{ - Store: store, - }, nil -} - -// Fetch fetches the content identified by the descriptor. It returns an io.ReadCloser. -// It's recommended to close the io.ReadCloser before a DeletableStore.Delete -// operation, otherwise Delete may fail on some systems. -func (ds *DeletableStore) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { - ds.lock.RLock() - defer ds.lock.RUnlock() - - return ds.Store.Fetch(ctx, target) -} - -// Push pushes the content, matching the expected descriptor. This implementation -// does not allow concurrent Push. To use concurrent Push, consider Store.Push. -func (ds *DeletableStore) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error { - ds.lock.Lock() - defer ds.lock.Unlock() - - return ds.Store.Push(ctx, expected, reader) -} - -// Delete removes the content matching the descriptor from the store. Delete may -// fail on some systems (i.e. Windows), if there is a process (i.e. an unclosed -// Reader) using `target`. -func (ds *DeletableStore) Delete(ctx context.Context, target ocispec.Descriptor) error { - ds.lock.Lock() - defer ds.lock.Unlock() - - resolvers := ds.tagResolver.Map() - untagged := false - for reference, desc := range resolvers { - if content.Equal(desc, target) { - ds.tagResolver.Untag(reference) - untagged = true - } - } - if err := ds.graph.Remove(ctx, target); err != nil { - return err - } - if untagged && ds.AutoSaveIndex { - err := ds.saveIndex() - if err != nil { - return err - } - } - return ds.storage.Delete(ctx, target) -} - -// Exists returns true if the described content exists. -func (ds *DeletableStore) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { - ds.lock.RLock() - defer ds.lock.RUnlock() - - return ds.Store.Exists(ctx, target) -} - -// Tag tags a descriptor with a reference string. -// reference should be a valid tag (e.g. "latest"). -// Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md#indexjson-file -func (ds *DeletableStore) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { - ds.lock.Lock() - defer ds.lock.Unlock() - - return ds.Store.Tag(ctx, desc, reference) -} - -// Resolve resolves a reference to a descriptor. If the reference to be resolved -// is a tag, the returned descriptor will be a full descriptor declared by -// github.com/opencontainers/image-spec/specs-go/v1. If the reference is a -// digest the returned descriptor will be a plain descriptor (containing only -// the digest, media type and size). -func (ds *DeletableStore) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { - ds.lock.RLock() - defer ds.lock.RUnlock() - - return ds.Store.Resolve(ctx, reference) -} - -// Predecessors returns the nodes directly pointing to the current node. -// Predecessors returns nil without error if the node does not exists in the -// store. -func (ds *DeletableStore) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { - ds.lock.RLock() - defer ds.lock.RUnlock() - - return ds.Store.Predecessors(ctx, node) -} - -// Tags lists the tags presented in the `index.json` file of the OCI layout, -// returned in ascending order. -// If `last` is NOT empty, the entries in the response start after the tag -// specified by `last`. Otherwise, the response starts from the top of the tags -// list. -// -// See also `Tags()` in the package `registry`. -func (ds *DeletableStore) Tags(ctx context.Context, last string, fn func(tags []string) error) error { - ds.lock.RLock() - defer ds.lock.RUnlock() - - return ds.Store.Tags(ctx, last, fn) -} - -// SaveIndex writes the `index.json` file to the file system. -// - If AutoSaveIndex is set to true (default value), -// the OCI store will automatically save the index on each Tag() call. -// - If AutoSaveIndex is set to false, it's the caller's responsibility -// to manually call this method when needed. -func (ds *DeletableStore) SaveIndex() error { - ds.lock.Lock() - defer ds.lock.Unlock() - - return ds.Store.saveIndex() -} diff --git a/content/oci/deletable_test.go b/content/oci/deletable_test.go deleted file mode 100644 index e3e24dfd..00000000 --- a/content/oci/deletable_test.go +++ /dev/null @@ -1,545 +0,0 @@ -/* -Copyright The ORAS Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package oci - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "reflect" - "strings" - "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" - "oras.land/oras-go/v2/errdef" - "oras.land/oras-go/v2/internal/spec" - "oras.land/oras-go/v2/registry" -) - -func TestDeletableStore_basic(t *testing.T) { - content := []byte("test delete") - desc := ocispec.Descriptor{ - MediaType: "test-delete", - Digest: digest.FromBytes(content), - Size: int64(len(content)), - } - ref := "latest" - - tempDir := t.TempDir() - s, err := NewDeletableStore(tempDir) - if err != nil { - t.Fatal("NewDeletableStore() 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) - } - - 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) - } - - 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, desc) - if err != nil { - t.Errorf("Store.Delete() = %v, wantErr %v", err, nil) - } - - 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, false) - } -} - -func TestDeletableStore_FetchAndDelete(t *testing.T) { - // create a store - tempDir := t.TempDir() - s, err := NewDeletableStoreWithContext(context.Background(), tempDir) - if err != nil { - t.Fatal("error =", err) - } - - // push a content - content := []byte("test delete") - desc := ocispec.Descriptor{ - MediaType: "test-delete", - Digest: digest.FromBytes(content), - Size: int64(len(content)), - } - err = s.Push(context.Background(), desc, bytes.NewReader(content)) - if err != nil { - t.Fatal("error =", err) - } - - // fetch a content - rc, err := s.Fetch(context.Background(), desc) - if err != nil { - t.Fatal("error =", err) - } - - // read and verify the content - got, err := io.ReadAll(rc) - if err != nil { - t.Fatal("error =", err) - } - if !bytes.Equal(got, content) { - fmt.Println(got) - t.Fatal("wrong content") - } - rc.Close() - - // delete. If rc is not closed, Delete would fail on some systems. - err = s.Delete(context.Background(), desc) - if err != nil { - t.Fatal("error =", err) - } -} - -func TestDeletableSore_Interface(t *testing.T) { - var ds interface{} = &DeletableStore{} - if _, ok := ds.(oras.GraphTarget); !ok { - t.Error("&DeletableStore{} does not conform oras.Target") - } - if _, ok := ds.(registry.TagLister); !ok { - t.Error("&DeletableStore{} does not conform registry.TagLister") - } -} - -func TestDeletableStore_Predecessors(t *testing.T) { - tempDir := t.TempDir() - s, err := NewDeletableStore(tempDir) - if err != nil { - t.Fatal("NewDeletableStore() 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, layers ...ocispec.Descriptor) { - manifest := ocispec.Manifest{ - Config: config, - Layers: layers, - } - manifestJSON, err := json.Marshal(manifest) - if err != nil { - t.Fatal(err) - } - appendBlob(ocispec.MediaTypeImageManifest, manifestJSON) - } - generateIndex := func(manifests ...ocispec.Descriptor) { - index := ocispec.Index{ - Manifests: manifests, - } - indexJSON, err := json.Marshal(index) - if err != nil { - t.Fatal(err) - } - appendBlob(ocispec.MediaTypeImageIndex, indexJSON) - } - generateArtifactManifest := func(subject ocispec.Descriptor, blobs ...ocispec.Descriptor) { - var manifest spec.Artifact - manifest.Subject = &subject - manifest.Blobs = append(manifest.Blobs, blobs...) - manifestJSON, err := json.Marshal(manifest) - if err != nil { - t.Fatal(err) - } - appendBlob(spec.MediaTypeArtifactManifest, manifestJSON) - } - - appendBlob(ocispec.MediaTypeImageConfig, []byte("config")) // Blob 0 - appendBlob(ocispec.MediaTypeImageLayer, []byte("foo")) // Blob 1 - appendBlob(ocispec.MediaTypeImageLayer, []byte("bar")) // Blob 2 - appendBlob(ocispec.MediaTypeImageLayer, []byte("hello")) // Blob 3 - generateManifest(descs[0], descs[1:3]...) // Blob 4 - generateManifest(descs[0], descs[3]) // Blob 5 - generateManifest(descs[0], descs[1:4]...) // Blob 6 - generateIndex(descs[4:6]...) // Blob 7 - generateIndex(descs[6]) // Blob 8 - generateIndex() // Blob 9 - generateIndex(descs[7:10]...) // Blob 10 - appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_1")) // Blob 11 - generateArtifactManifest(descs[6], descs[11]) // Blob 12 - appendBlob(ocispec.MediaTypeImageLayer, []byte("sig_2")) // Blob 13 - generateArtifactManifest(descs[10], descs[13]) // Blob 14 - - 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) - } - - // verify predecessors - wants := [][]ocispec.Descriptor{ - descs[4:7], // Blob 0 - {descs[4], descs[6]}, // Blob 1 - {descs[4], descs[6]}, // Blob 2 - {descs[5], descs[6]}, // Blob 3 - {descs[7]}, // Blob 4 - {descs[7]}, // Blob 5 - {descs[8], descs[12]}, // Blob 6 - {descs[10]}, // Blob 7 - {descs[10]}, // Blob 8 - {descs[10]}, // Blob 9 - {descs[14]}, // Blob 10 - {descs[12]}, // Blob 11 - nil, // Blob 12 - {descs[14]}, // Blob 13 - nil, // Blob 14 - } - for i, want := range wants { - predecessors, err := s.Predecessors(ctx, descs[i]) - if err != nil { - t.Errorf("Store.Predecessors(%d) error = %v", i, err) - } - if !equalDescriptorSet(predecessors, want) { - t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want) - } - } - - // delete one node - s.Delete(ctx, descs[6]) - - // verify predecessors - wants = [][]ocispec.Descriptor{ - descs[4:6], // Blob 0 - {descs[4]}, // Blob 1 - {descs[4]}, // Blob 2 - {descs[5]}, // Blob 3 - {descs[7]}, // Blob 4 - {descs[7]}, // Blob 5 - {descs[8], descs[12]}, // Blob 6 - {descs[10]}, // Blob 7 - {descs[10]}, // Blob 8 - {descs[10]}, // Blob 9 - {descs[14]}, // Blob 10 - {descs[12]}, // Blob 11 - nil, // Blob 12 - {descs[14]}, // Blob 13 - nil, // Blob 14 - } - for i, want := range wants { - predecessors, err := s.Predecessors(ctx, descs[i]) - if err != nil { - t.Errorf("Store.Predecessors(%d) error = %v", i, err) - } - if !equalDescriptorSet(predecessors, want) { - t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want) - } - } - - // delete more nodes - s.Delete(ctx, descs[12]) - s.Delete(ctx, descs[7]) - - // verify predecessors - wants = [][]ocispec.Descriptor{ - descs[4:6], // Blob 0 - {descs[4]}, // Blob 1 - {descs[4]}, // Blob 2 - {descs[5]}, // Blob 3 - nil, // Blob 4 - nil, // Blob 5 - {descs[8]}, // Blob 6 - {descs[10]}, // Blob 7 - {descs[10]}, // Blob 8 - {descs[10]}, // Blob 9 - {descs[14]}, // Blob 10 - nil, // Blob 11 - nil, // Blob 12 - {descs[14]}, // Blob 13 - nil, // Blob 14 - } - for i, want := range wants { - predecessors, err := s.Predecessors(ctx, descs[i]) - if err != nil { - t.Errorf("Store.Predecessors(%d) error = %v", i, err) - } - if !equalDescriptorSet(predecessors, want) { - t.Errorf("Store.Predecessors(%d) = %v, want %v", i, predecessors, want) - } - } -} - -func TestDeletableSore_TagNotFound(t *testing.T) { - ref := "foobar" - - tempDir := t.TempDir() - s, err := NewDeletableStore(tempDir) - if err != nil { - t.Fatal("NewDeletable() error =", err) - } - ctx := context.Background() - - _, err = s.Resolve(ctx, ref) - if !errors.Is(err, errdef.ErrNotFound) { - t.Errorf("Store.Resolve() error = %v, want %v", err, errdef.ErrNotFound) - } -} - -func TestDeletableStore_NotExistingRoot(t *testing.T) { - tempDir := t.TempDir() - root := filepath.Join(tempDir, "rootDir") - _, err := NewDeletableStore(root) - if err != nil { - t.Fatal("NewDeletable() error =", err) - } - - // validate layout - layoutFilePath := filepath.Join(root, ocispec.ImageLayoutFile) - layoutFile, err := os.Open(layoutFilePath) - if err != nil { - t.Errorf("error opening layout file, error = %v", err) - } - defer layoutFile.Close() - - var layout *ocispec.ImageLayout - err = json.NewDecoder(layoutFile).Decode(&layout) - if err != nil { - t.Fatal("error decoding layout, error =", err) - } - if want := ocispec.ImageLayoutVersion; layout.Version != want { - t.Errorf("layout.Version = %s, want %s", layout.Version, want) - } - - // validate index.json - indexFilePath := filepath.Join(root, "index.json") - indexFile, err := os.Open(indexFilePath) - if err != nil { - t.Errorf("error opening layout file, error = %v", err) - } - defer indexFile.Close() - - var index *ocispec.Index - err = json.NewDecoder(indexFile).Decode(&index) - if err != nil { - t.Fatal("error decoding index.json, error =", err) - } - if want := 2; index.SchemaVersion != want { - t.Errorf("index.SchemaVersion = %v, want %v", index.SchemaVersion, want) - } -} - -func TestDeletableStore_ContentNotFound(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 := NewDeletableStore(tempDir) - if err != nil { - t.Fatal("NewDeletable() error =", err) - } - ctx := context.Background() - - exists, err := s.Exists(ctx, desc) - if err != nil { - t.Error("Store.Exists() error =", err) - } - if exists { - t.Errorf("Store.Exists() = %v, want %v", exists, false) - } - - _, err = s.Fetch(ctx, desc) - if !errors.Is(err, errdef.ErrNotFound) { - t.Errorf("Store.Fetch() error = %v, want %v", err, errdef.ErrNotFound) - } -} - -func TestDeletableStore_ContentAlreadyExists(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 := NewDeletableStore(tempDir) - if err != nil { - t.Fatal("NewDeletable() error =", err) - } - ctx := context.Background() - - err = s.Push(ctx, desc, bytes.NewReader(content)) - if err != nil { - t.Fatal("Store.Push() error =", err) - } - - err = s.Push(ctx, desc, bytes.NewReader(content)) - if !errors.Is(err, errdef.ErrAlreadyExists) { - t.Errorf("Store.Push() error = %v, want %v", err, errdef.ErrAlreadyExists) - } -} - -func TestDeletableStore_ContentBadPush(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 := NewDeletableStore(tempDir) - if err != nil { - t.Fatal("NewDeletable() error =", err) - } - ctx := context.Background() - - err = s.Push(ctx, desc, strings.NewReader("foobar")) - if err == nil { - t.Errorf("Store.Push() error = %v, wantErr %v", err, true) - } -} - -func TestDeletableStore_DisableAutoSaveIndex(t *testing.T) { - content := []byte(`{"layers":[]}`) - desc := ocispec.Descriptor{ - MediaType: ocispec.MediaTypeImageManifest, - Digest: digest.FromBytes(content), - Size: int64(len(content)), - } - ref := "foobar" - - tempDir := t.TempDir() - s, err := NewDeletableStore(tempDir) - if err != nil { - t.Fatal("NewDeletable() error =", err) - } - // disable auto save - s.AutoSaveIndex = false - ctx := context.Background() - - // validate layout - layoutFilePath := filepath.Join(tempDir, ocispec.ImageLayoutFile) - layoutFile, err := os.Open(layoutFilePath) - if err != nil { - t.Errorf("error opening layout file, error = %v", err) - } - defer layoutFile.Close() - - var layout *ocispec.ImageLayout - err = json.NewDecoder(layoutFile).Decode(&layout) - if err != nil { - t.Fatal("error decoding layout, error =", err) - } - if want := ocispec.ImageLayoutVersion; layout.Version != want { - t.Errorf("layout.Version = %s, want %s", layout.Version, want) - } - - // test push - err = s.Push(ctx, desc, bytes.NewReader(content)) - if err != nil { - t.Fatal("Store.Push() error =", err) - } - internalResolver := s.tagResolver - if got, want := len(internalResolver.Map()), 1; got != want { - t.Errorf("resolver.Map() = %v, want %v", got, want) - } - - // test resolving by digest - gotDesc, err := s.Resolve(ctx, desc.Digest.String()) - if err != nil { - t.Fatal("Store.Resolve() error =", err) - } - if !reflect.DeepEqual(gotDesc, desc) { - t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc) - } - - // test tag - err = s.Tag(ctx, desc, ref) - if err != nil { - t.Fatal("Store.Tag() error =", err) - } - if got, want := len(internalResolver.Map()), 2; got != want { - t.Errorf("resolver.Map() = %v, want %v", got, want) - } - - // test resolving by digest - gotDesc, err = s.Resolve(ctx, ref) - if err != nil { - t.Fatal("Store.Resolve() error =", err) - } - if !reflect.DeepEqual(gotDesc, desc) { - t.Errorf("Store.Resolve() = %v, want %v", gotDesc, desc) - } - - // test index file - if got, want := len(s.index.Manifests), 0; got != want { - t.Errorf("len(index.Manifests) = %v, want %v", got, want) - } - if err := s.SaveIndex(); err != nil { - t.Fatal("Store.SaveIndex() error =", err) - } - // test index file again - if got, want := len(s.index.Manifests), 1; got != want { - t.Errorf("len(index.Manifests) = %v, want %v", got, want) - } - if _, err := os.Stat(s.indexPath); err != nil { - t.Errorf("error: %s does not exist", s.indexPath) - } -} diff --git a/content/oci/oci.go b/content/oci/oci.go index 470aef19..1182dad9 100644 --- a/content/oci/oci.go +++ b/content/oci/oci.go @@ -30,6 +30,7 @@ import ( "github.com/opencontainers/go-digest" specs "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/errdef" "oras.land/oras-go/v2/internal/container/set" "oras.land/oras-go/v2/internal/descriptor" @@ -53,6 +54,7 @@ type Store struct { indexPath string index *ocispec.Index indexLock sync.Mutex + sync sync.RWMutex storage *Storage tagResolver *resolver.Memory @@ -97,13 +99,21 @@ func NewWithContext(ctx context.Context, root string) (*Store, error) { return store, nil } -// Fetch fetches the content identified by the descriptor. +// Fetch fetches the content identified by the descriptor. It returns an io.ReadCloser. +// It's recommended to close the io.ReadCloser before a DeletableStore.Delete +// operation, otherwise Delete may fail (for example on NTFS file systems). func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + s.sync.RLock() + defer s.sync.RUnlock() + return s.storage.Fetch(ctx, target) } // Push pushes the content, matching the expected descriptor. func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error { + s.sync.RLock() + defer s.sync.RUnlock() + if err := s.storage.Push(ctx, expected, reader); err != nil { return err } @@ -119,13 +129,46 @@ func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, reader io // Exists returns true if the described content exists. func (s *Store) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + s.sync.RLock() + defer s.sync.RUnlock() + return s.storage.Exists(ctx, target) } +// Delete removes the content matching the descriptor from the store. Delete may +// fail on some systems (i.e. Windows), if there is a process (i.e. an unclosed +// Reader) using `target`. +func (s *Store) Delete(ctx context.Context, target ocispec.Descriptor) error { + s.sync.Lock() + defer s.sync.Unlock() + + resolvers := s.tagResolver.Map() + untagged := false + for reference, desc := range resolvers { + if content.Equal(desc, target) { + s.tagResolver.Untag(reference) + untagged = true + } + } + if err := s.graph.Remove(ctx, target); err != nil { + return err + } + if untagged && s.AutoSaveIndex { + err := s.SaveIndex() + if err != nil { + return err + } + } + return s.storage.Delete(ctx, target) +} + // Tag tags a descriptor with a reference string. // reference should be a valid tag (e.g. "latest"). // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc4/image-layout.md#indexjson-file func (s *Store) Tag(ctx context.Context, desc ocispec.Descriptor, reference string) error { + s.sync.RLock() + defer s.sync.RUnlock() + if err := validateReference(reference); err != nil { return err } @@ -165,6 +208,9 @@ func (s *Store) tag(ctx context.Context, desc ocispec.Descriptor, reference stri // digest the returned descriptor will be a plain descriptor (containing only // the digest, media type and size). func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) { + s.sync.RLock() + defer s.sync.RUnlock() + if reference == "" { return ocispec.Descriptor{}, errdef.ErrMissingReference } @@ -190,6 +236,9 @@ func (s *Store) Resolve(ctx context.Context, reference string) (ocispec.Descript // Predecessors returns nil without error if the node does not exists in the // store. func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) { + s.sync.RLock() + defer s.sync.RUnlock() + return s.graph.Predecessors(ctx, node) } @@ -201,6 +250,9 @@ func (s *Store) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]oc // // See also `Tags()` in the package `registry`. func (s *Store) Tags(ctx context.Context, last string, fn func(tags []string) error) error { + s.sync.RLock() + defer s.sync.RUnlock() + return listTags(ctx, s.tagResolver, last, fn) } @@ -266,13 +318,12 @@ func (s *Store) loadIndexFile(ctx context.Context) error { // - If AutoSaveIndex is set to false, it's the caller's responsibility // to manually call this method when needed. func (s *Store) SaveIndex() error { + s.sync.RLock() + defer s.sync.RUnlock() + s.indexLock.Lock() defer s.indexLock.Unlock() - return s.saveIndex() -} - -func (s *Store) saveIndex() error { var manifests []ocispec.Descriptor tagged := set.New[digest.Digest]() refMap := s.tagResolver.Map() diff --git a/content/oci/oci_test.go b/content/oci/oci_test.go index 43e57f0a..d7dec330 100644 --- a/content/oci/oci_test.go +++ b/content/oci/oci_test.go @@ -1975,6 +1975,107 @@ func TestStore_Tags(t *testing.T) { } } +func TestStore_BasicDelete(t *testing.T) { + content := []byte("test delete") + desc := ocispec.Descriptor{ + MediaType: "test-delete", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + ref := "latest" + + tempDir := t.TempDir() + s, err := New(tempDir) + if err != nil { + t.Fatal("NewDeletableStore() 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) + } + + 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) + } + + 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, desc) + if err != nil { + t.Errorf("Store.Delete() = %v, wantErr %v", err, nil) + } + + 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, false) + } +} + +func TestStore_FetchAndDelete(t *testing.T) { + // create a store + tempDir := t.TempDir() + s, err := New(tempDir) + if err != nil { + t.Fatal("error =", err) + } + + // push a content + content := []byte("test delete") + desc := ocispec.Descriptor{ + MediaType: "test-delete", + Digest: digest.FromBytes(content), + Size: int64(len(content)), + } + err = s.Push(context.Background(), desc, bytes.NewReader(content)) + if err != nil { + t.Fatal("error =", err) + } + + // fetch a content + rc, err := s.Fetch(context.Background(), desc) + if err != nil { + t.Fatal("error =", err) + } + + // read and verify the content + got, err := io.ReadAll(rc) + if err != nil { + t.Fatal("error =", err) + } + if !bytes.Equal(got, content) { + fmt.Println(got) + t.Fatal("wrong content") + } + rc.Close() + + // delete. If rc is not closed, Delete would fail on some systems. + err = s.Delete(context.Background(), desc) + if err != nil { + t.Fatal("error =", err) + } +} + func equalDescriptorSet(actual []ocispec.Descriptor, expected []ocispec.Descriptor) bool { if len(actual) != len(expected) { return false