From e135557babfadb6b40fdaabb0cbfa148aa144e23 Mon Sep 17 00:00:00 2001 From: Wei Meng Date: Thu, 4 Aug 2022 13:36:49 +0800 Subject: [PATCH] Allow skipping unnamed blobs when pushing to file store (#254) Signed-off-by: Wei Meng --- content/file/errors.go | 2 ++ content/file/file.go | 24 +++++++++++++++--- content/file/file_test.go | 51 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/content/file/errors.go b/content/file/errors.go index d140bf8a..36c35d4d 100644 --- a/content/file/errors.go +++ b/content/file/errors.go @@ -24,3 +24,5 @@ var ( ErrOverwriteDisallowed = errors.New("overwrite disallowed") ErrStoreClosed = errors.New("store already closed") ) + +var errSkipUnnamed = errors.New("unnamed descriptor skipped") diff --git a/content/file/file.go b/content/file/file.go index 4f5d964a..d72ef64b 100644 --- a/content/file/file.go +++ b/content/file/file.go @@ -94,6 +94,14 @@ type Store struct { // ForceCAS can be specified to enforce CAS style dedup. // Default value: false. ForceCAS bool + // IgnoreNoName controls if push operations should ignore descriptors + // without a name. When specified, corresponding content will be discarded. + // Otherwise, content will be saved to a fallback storage. + // A typical scenario is pulling an arbitrary artifact masqueraded as OCI + // image to file store. This option can be specified to discard unnamed + // manifest and config file, while leaving only named layer files. + // Default value: false. + IgnoreNoName bool workingDir string // the working directory of the file store closed int32 // if the store is closed - 0: false, 1: true. @@ -199,14 +207,18 @@ func (s *Store) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCl } // Push pushes the content, matching the expected descriptor. -// If name is not specified in the descriptor, -// the content will be pushed to the fallback storage. +// If name is not specified in the descriptor, the content will be pushed to +// the fallback storage by default, or will be discarded when +// Store.IgnoreNoName is true. func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { if s.isClosedSet() { return ErrStoreClosed } if err := s.push(ctx, expected, content); err != nil { + if errors.Is(err, errSkipUnnamed) { + return nil + } return err } @@ -220,11 +232,15 @@ func (s *Store) Push(ctx context.Context, expected ocispec.Descriptor, content i } // push pushes the content, matching the expected descriptor. -// If name is not specified in the descriptor, -// the content will be pushed to the fallback storage. +// If name is not specified in the descriptor, the content will be pushed to +// the fallback storage by default, or will be discarded when +// Store.IgnoreNoName is true. func (s *Store) push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error { name := expected.Annotations[ocispec.AnnotationTitle] if name == "" { + if s.IgnoreNoName { + return errSkipUnnamed + } return s.fallbackStorage.Push(ctx, expected, content) } diff --git a/content/file/file_test.go b/content/file/file_test.go index 5eab0304..b1e98ad6 100644 --- a/content/file/file_test.go +++ b/content/file/file_test.go @@ -1670,6 +1670,57 @@ func TestStore_File_Push_DisableOverwrite(t *testing.T) { } } +func TestStore_File_Push_IgnoreNoName(t *testing.T) { + config := []byte("{}") + configDesc := ocispec.Descriptor{ + MediaType: "config", + Digest: digest.FromBytes(config), + Size: int64(len(config)), + } + manifest := ocispec.Manifest{ + MediaType: ocispec.MediaTypeImageManifest, + Config: configDesc, + Layers: []ocispec.Descriptor{}, + } + manifestJSON, err := json.Marshal(manifest) + if err != nil { + t.Fatal("json.Marshal() error =", err) + } + manifestDesc := ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageManifest, + Digest: digest.FromBytes(manifestJSON), + Size: int64(len(manifestJSON)), + } + tempDir := t.TempDir() + s := New(tempDir) + defer s.Close() + s.IgnoreNoName = true + + // push an OCI manifest + ctx := context.Background() + err = s.Push(ctx, manifestDesc, bytes.NewReader(manifestJSON)) + if err != nil { + t.Fatal("Store.Push() error = ", err) + } + + // verify the manifest is not saved + exists, err := s.Exists(ctx, manifestDesc) + if err != nil { + t.Fatal("Store.Exists() error =", err) + } + if exists { + t.Errorf("Unnamed manifest is saved in file store") + } + // verify the manifest is not indexed + predecessors, err := s.Predecessors(ctx, configDesc) + if err != nil { + t.Fatal("Store.Predecessors() error = ", err) + } + if len(predecessors) != 0 { + t.Errorf("Unnamed manifest is indexed in file store") + } +} + func TestStore_File_Push_DisallowPathTraversal(t *testing.T) { content := []byte("hello world") name := "../test.txt"