diff --git a/cmd/oras/internal/display/console/console.go b/cmd/oras/internal/display/console/console.go index 6a932d68a..deda149e0 100644 --- a/cmd/oras/internal/display/console/console.go +++ b/cmd/oras/internal/display/console/console.go @@ -81,7 +81,7 @@ func (c *Console) NewRow() { func (c *Console) OutputTo(upCnt uint, str string) { _, _ = c.Write([]byte(Restore)) _, _ = c.Write([]byte(aec.PreviousLine(upCnt).Apply(str))) - _, _ = c.Write([]byte(" ")) + _, _ = c.Write([]byte("\n")) _, _ = c.Write([]byte(aec.EraseLine(aec.EraseModes.Tail).String())) } diff --git a/cmd/oras/internal/display/progress/status.go b/cmd/oras/internal/display/progress/status.go index d8a63ecfb..5c67c2053 100644 --- a/cmd/oras/internal/display/progress/status.go +++ b/cmd/oras/internal/display/progress/status.go @@ -55,6 +55,7 @@ type status struct { func newStatus() *status { return &status{ offset: -1, + total: humanize.ToBytes(0), lastRenderTime: time.Now(), } } @@ -99,9 +100,6 @@ func (s *status) String(width int) (string, string) { // todo: doesn't support multiline prompt total := uint64(s.descriptor.Size) var percent float64 - if s.offset >= 0 { - percent = float64(s.offset) / float64(total) - } name := s.descriptor.Annotations["org.opencontainers.image.title"] if name == "" { @@ -112,10 +110,15 @@ func (s *status) String(width int) (string, string) { // mark(1) bar(22) speed(8) action(<=11) name(<=126) size_per_size(<=13) percent(8) time(>=6) // └─ digest(72) var offset string - switch percent { - case 1: // 100%, show exact size + switch s.done { + case true: // 100%, show exact size offset = fmt.Sprint(s.total.Size) + percent = 1 default: // 0% ~ 99%, show 2-digit precision + if total != 0 && s.offset >= 0 { + // percentage calculatable + percent = float64(s.offset) / float64(total) + } offset = fmt.Sprintf("%.2f", humanize.RoundTo(s.total.Size*percent)) } right := fmt.Sprintf(" %s/%s %6.2f%% %6s", offset, s.total, percent*100, s.durationString()) @@ -131,7 +134,7 @@ func (s *status) String(width int) (string, string) { // bar + wrapper(2) + space(1) + speed + "/s"(2) + wrapper(2) = len(bar) + len(speed) + 7 lenLeft = barLength + speedLength + 7 } else { - left = fmt.Sprintf("√ %s %s", s.prompt, name) + left = fmt.Sprintf("✓ %s %s", s.prompt, name) } // mark(1) + space(1) + prompt + space(1) + name = len(prompt) + len(name) + 3 lenLeft += utf8.RuneCountInString(s.prompt) + utf8.RuneCountInString(name) + 3 diff --git a/cmd/oras/internal/display/progress/status_test.go b/cmd/oras/internal/display/progress/status_test.go index 10c38f9ad..4ce98bace 100644 --- a/cmd/oras/internal/display/progress/status_test.go +++ b/cmd/oras/internal/display/progress/status_test.go @@ -54,7 +54,6 @@ func Test_status_String(t *testing.T) { if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, "application/v.", "0.00/2 B", "0.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } - // done s.Update(&status{ endTime: time.Now(), @@ -62,11 +61,46 @@ func Test_status_String(t *testing.T) { descriptor: s.descriptor, }) statusStr, digestStr = s.String(120) - if err := testutils.OrderedMatch(statusStr+digestStr, "√", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { + if err := testutils.OrderedMatch(statusStr+digestStr, "✓", s.prompt, s.descriptor.MediaType, "2/2 B", "100.00%", s.descriptor.Digest.String()); err != nil { t.Error(err) } } +func Test_status_String_zeroWitdth(t *testing.T) { + // zero status and progress + s := newStatus() + if status, digest := s.String(console.MinWidth); status != zeroStatus || digest != zeroDigest { + t.Errorf("status.String() = %v, %v, want %v, %v", status, digest, zeroStatus, zeroDigest) + } + + // not done + s.Update(&status{ + prompt: "test", + descriptor: ocispec.Descriptor{ + MediaType: "application/vnd.oci.empty.oras.test.v1+json", + Size: 0, + Digest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + startTime: time.Now().Add(-time.Minute), + offset: 0, + total: humanize.ToBytes(0), + }) + // not done + statusStr, digestStr := s.String(120) + if err := testutils.OrderedMatch(statusStr+digestStr, " [\x1b[7m\x1b[0m....................]", s.prompt, s.descriptor.MediaType, "0.00/0 B", "0.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) + } + // done + s.Update(&status{ + endTime: time.Now(), + offset: s.descriptor.Size, + descriptor: s.descriptor, + }) + statusStr, digestStr = s.String(120) + if err := testutils.OrderedMatch(statusStr+digestStr, "✓", s.prompt, s.descriptor.MediaType, "0/0 B", "100.00%", s.descriptor.Digest.String()); err != nil { + t.Error(err) + } +} func Test_status_durationString(t *testing.T) { // zero duration s := newStatus() diff --git a/cmd/oras/internal/option/common.go b/cmd/oras/internal/option/common.go index 0530ed056..7cb14a2c0 100644 --- a/cmd/oras/internal/option/common.go +++ b/cmd/oras/internal/option/common.go @@ -17,7 +17,6 @@ package option import ( "context" - "errors" "os" "github.com/sirupsen/logrus" @@ -37,7 +36,7 @@ type Common struct { // ApplyFlags applies flags to a command flag set. func (opts *Common) ApplyFlags(fs *pflag.FlagSet) { - fs.BoolVarP(&opts.Debug, "debug", "d", false, "debug mode") + fs.BoolVarP(&opts.Debug, "debug", "d", false, "output debug logs (implies --no-tty)") fs.BoolVarP(&opts.Verbose, "verbose", "v", false, "verbose output") fs.BoolVarP(&opts.noTTY, "no-tty", "", false, "[Preview] do not show progress output") } @@ -55,11 +54,12 @@ func (opts *Common) Parse() error { // parseTTY gets target options from user input. func (opts *Common) parseTTY(f *os.File) error { - if !opts.noTTY && term.IsTerminal(int(f.Fd())) { + if !opts.noTTY { if opts.Debug { - return errors.New("cannot use --debug, add --no-tty to suppress terminal output") + opts.noTTY = true + } else if term.IsTerminal(int(f.Fd())) { + opts.TTY = f } - opts.TTY = f } return nil } diff --git a/cmd/oras/internal/option/common_unix_test.go b/cmd/oras/internal/option/common_unix_test.go index 2d4388385..2ad72620f 100644 --- a/cmd/oras/internal/option/common_unix_test.go +++ b/cmd/oras/internal/option/common_unix_test.go @@ -38,7 +38,10 @@ func TestCommon_parseTTY(t *testing.T) { // --debug opts.Debug = true - if err := opts.parseTTY(device); err == nil { - t.Error("expected error when debug is set with TTY output") + if err := opts.parseTTY(device); err != nil { + t.Errorf("unexpected error with --debug: %v", err) + } + if !opts.noTTY { + t.Errorf("expected --no-tty to be true with --debug") } } diff --git a/cmd/oras/internal/option/target.go b/cmd/oras/internal/option/target.go index 849a739e1..b3c9d5d6b 100644 --- a/cmd/oras/internal/option/target.go +++ b/cmd/oras/internal/option/target.go @@ -19,6 +19,8 @@ import ( "context" "errors" "fmt" + "io" + "io/fs" "os" "strings" "sync" @@ -154,12 +156,22 @@ func (opts *Target) NewReadonlyTarget(ctx context.Context, common Common, logger } info, err := os.Stat(opts.Path) if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil, fmt.Errorf("invalid argument %q: failed to find path %q: %w", opts.RawReference, opts.Path, err) + } return nil, err } if info.IsDir() { return oci.NewFromFS(ctx, os.DirFS(opts.Path)) } - return oci.NewFromTar(ctx, opts.Path) + store, err := oci.NewFromTar(ctx, opts.Path) + if err != nil { + if errors.Is(err, io.ErrUnexpectedEOF) { + return nil, fmt.Errorf("%q does not look like a tar archive: %w", opts.Path, err) + } + return nil, err + } + return store, nil case TargetTypeRemote: repo, err := opts.NewRepository(opts.RawReference, common, logger) if err != nil { diff --git a/cmd/oras/root/cp.go b/cmd/oras/root/cp.go index 8df14084f..2bd463b8c 100644 --- a/cmd/oras/root/cp.go +++ b/cmd/oras/root/cp.go @@ -154,25 +154,32 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar return graph.Referrers(ctx, src, desc, "") } + const ( + promptExists = "Exists " + promptCopying = "Copying" + promptCopied = "Copied " + promptSkipped = "Skipped" + ) + if opts.TTY == nil { // none TTY output extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintStatus(desc, "Exists ", opts.Verbose) + return display.PrintStatus(desc, promptExists, opts.Verbose) } extendedCopyOptions.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - return display.PrintStatus(desc, "Copying", opts.Verbose) + return display.PrintStatus(desc, promptCopying, opts.Verbose) } extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - if err := display.PrintSuccessorStatus(ctx, desc, dst, committed, display.StatusPrinter("Skipped", opts.Verbose)); err != nil { + if err := display.PrintSuccessorStatus(ctx, desc, dst, committed, display.StatusPrinter(promptSkipped, opts.Verbose)); err != nil { return err } - return display.PrintStatus(desc, "Copied ", opts.Verbose) + return display.PrintStatus(desc, promptCopied, opts.Verbose) } } else { // TTY output - tracked, err := track.NewTarget(dst, "Copying ", "Copied ", opts.TTY) + tracked, err := track.NewTarget(dst, promptCopying, promptCopied, opts.TTY) if err != nil { return ocispec.Descriptor{}, err } @@ -180,12 +187,12 @@ func doCopy(ctx context.Context, src oras.ReadOnlyGraphTarget, dst oras.GraphTar dst = tracked extendedCopyOptions.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return tracked.Prompt(desc, "Exists ") + return tracked.Prompt(desc, promptExists) } extendedCopyOptions.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return display.PrintSuccessorStatus(ctx, desc, tracked, committed, func(desc ocispec.Descriptor) error { - return tracked.Prompt(desc, "Skipped") + return tracked.Prompt(desc, promptSkipped) }) } } diff --git a/cmd/oras/root/pull.go b/cmd/oras/root/pull.go index 41a1ad675..e98b03691 100644 --- a/cmd/oras/root/pull.go +++ b/cmd/oras/root/pull.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "sync" + "sync/atomic" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/spf13/cobra" @@ -130,18 +131,22 @@ func runPull(ctx context.Context, opts pullOptions) error { dst.AllowPathTraversalOnWrite = opts.PathTraversal dst.DisableOverwrite = opts.KeepOldFiles - desc, pulledEmpty, err := doPull(ctx, src, dst, copyOptions, &opts) + desc, layerSkipped, err := doPull(ctx, src, dst, copyOptions, &opts) if err != nil { if errors.Is(err, file.ErrPathTraversalDisallowed) { err = fmt.Errorf("%s: %w", "use flag --allow-path-traversal to allow insecurely pulling files outside of working directory", err) } return err } - if pulledEmpty { - fmt.Println("Downloaded empty artifact") + + // suggest oras copy for pulling layers without annotation + if layerSkipped { + fmt.Printf("Skipped pulling layers without file name in %q\n", ocispec.AnnotationTitle) + fmt.Printf("Use 'oras copy %s --to-oci-layout ' to pull all layers.\n", opts.RawReference) + } else { + fmt.Println("Pulled", opts.AnnotatedReference()) + fmt.Println("Digest:", desc.Digest) } - fmt.Println("Pulled", opts.AnnotatedReference()) - fmt.Println("Digest:", desc.Digest) return nil } @@ -155,15 +160,24 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, } } + const ( + promptDownloading = "Downloading" + promptPulled = "Pulled " + promptProcessing = "Processing " + promptSkipped = "Skipped " + promptRestored = "Restored " + promptDownloaded = "Downloaded " + ) + var tracked track.GraphTarget - dst, tracked, err = getTrackedTarget(dst, po.TTY, "Downloading", "Downloaded ") + dst, tracked, err = getTrackedTarget(dst, po.TTY, "Downloading", "Pulled ") if err != nil { return ocispec.Descriptor{}, false, err } if tracked != nil { defer tracked.Close() } - + var layerSkipped atomic.Bool var printed sync.Map var getConfigOnce sync.Once opts.FindSuccessors = func(ctx context.Context, fetcher content.Fetcher, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { @@ -173,7 +187,7 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, } if po.TTY == nil { // none TTY, print status log for first-time fetching - if err := display.PrintStatus(target, "Downloading", po.Verbose); err != nil { + if err := display.PrintStatus(target, promptDownloading, po.Verbose); err != nil { return nil, err } } @@ -188,7 +202,7 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, }() if po.TTY == nil { // none TTY, add logs for processing manifest - return rc, display.PrintStatus(target, "Processing ", po.Verbose) + return rc, display.PrintStatus(target, promptProcessing, po.Verbose) } return rc, nil }) @@ -209,19 +223,29 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, config.Annotations[ocispec.AnnotationTitle] = configPath } }) - nodes = append(nodes, *config) + if config.Size != ocispec.DescriptorEmptyJSON.Size || config.Digest != ocispec.DescriptorEmptyJSON.Digest || config.Annotations[ocispec.AnnotationTitle] != "" { + nodes = append(nodes, *config) + } } var ret []ocispec.Descriptor for _, s := range nodes { if s.Annotations[ocispec.AnnotationTitle] == "" { + if content.Equal(s, ocispec.DescriptorEmptyJSON) { + // empty layer + continue + } + if s.Annotations[ocispec.AnnotationTitle] == "" { + // unnamed layers are skipped + layerSkipped.Store(true) + } ss, err := content.Successors(ctx, fetcher, s) if err != nil { return nil, err } if len(ss) == 0 { // skip s if it is unnamed AND has no successors. - if err := printOnce(&printed, s, "Skipped ", po.Verbose, tracked); err != nil { + if err := printOnce(&printed, s, promptSkipped, po.Verbose, tracked); err != nil { return nil, err } continue @@ -233,14 +257,13 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, return ret, nil } - pulledEmpty := true opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { if _, ok := printed.LoadOrStore(generateContentKey(desc), true); ok { return nil } if po.TTY == nil { // none TTY, print status log for downloading - return display.PrintStatus(desc, "Downloading", po.Verbose) + return display.PrintStatus(desc, promptDownloading, po.Verbose) } // TTY return nil @@ -253,7 +276,7 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, } for _, s := range successors { if _, ok := s.Annotations[ocispec.AnnotationTitle]; ok { - if err := printOnce(&printed, s, "Restored ", po.Verbose, tracked); err != nil { + if err := printOnce(&printed, s, promptRestored, po.Verbose, tracked); err != nil { return err } } @@ -264,17 +287,14 @@ func doPull(ctx context.Context, src oras.ReadOnlyTarget, dst oras.GraphTarget, return nil } name = desc.MediaType - } else { - // named content downloaded - pulledEmpty = false } printed.Store(generateContentKey(desc), true) - return display.Print("Downloaded ", display.ShortDigest(desc), name) + return display.Print(promptDownloaded, display.ShortDigest(desc), name) } // Copy desc, err := oras.Copy(ctx, src, po.Reference, dst, po.Reference, opts) - return desc, pulledEmpty, err + return desc, layerSkipped.Load(), err } // generateContentKey generates a unique key for each content descriptor, using diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 740ad29b2..c9dc0a7f7 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -242,33 +242,40 @@ func doPush(dst oras.Target, pack packFunc, copy copyFunc) (ocispec.Descriptor, func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool, tracked track.GraphTarget) { committed := &sync.Map{} + const ( + promptSkipped = "Skipped " + promptUploaded = "Uploaded " + promptExists = "Exists " + promptUploading = "Uploading" + ) + if tracked == nil { // non TTY opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return display.PrintStatus(desc, "Exists ", verbose) + return display.PrintStatus(desc, promptExists, verbose) } opts.PreCopy = func(ctx context.Context, desc ocispec.Descriptor) error { - return display.PrintStatus(desc, "Uploading", verbose) + return display.PrintStatus(desc, promptUploading, verbose) } opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - if err := display.PrintSuccessorStatus(ctx, desc, fetcher, committed, display.StatusPrinter("Skipped ", verbose)); err != nil { + if err := display.PrintSuccessorStatus(ctx, desc, fetcher, committed, display.StatusPrinter(promptSkipped, verbose)); err != nil { return err } - return display.PrintStatus(desc, "Uploaded ", verbose) + return display.PrintStatus(desc, promptUploaded, verbose) } return } // TTY opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) - return tracked.Prompt(desc, "Exists ") + return tracked.Prompt(desc, promptExists) } opts.PostCopy = func(ctx context.Context, desc ocispec.Descriptor) error { committed.Store(desc.Digest.String(), desc.Annotations[ocispec.AnnotationTitle]) return display.PrintSuccessorStatus(ctx, desc, fetcher, committed, func(d ocispec.Descriptor) error { - return tracked.Prompt(d, "Skipped ") + return tracked.Prompt(d, promptSkipped) }) } } diff --git a/cmd/oras/root/repo/ls.go b/cmd/oras/root/repo/ls.go index cea8dd334..0d6c3a7c5 100644 --- a/cmd/oras/root/repo/ls.go +++ b/cmd/oras/root/repo/ls.go @@ -17,6 +17,7 @@ package repo import ( "context" + "errors" "fmt" "strings" @@ -74,7 +75,7 @@ func listRepository(ctx context.Context, opts repositoryOptions) error { if err != nil { return err } - return reg.Repositories(ctx, opts.last, func(repos []string) error { + err = reg.Repositories(ctx, opts.last, func(repos []string) error { for _, repo := range repos { if subRepo, found := strings.CutPrefix(repo, opts.namespace); found { fmt.Println(subRepo) @@ -82,4 +83,16 @@ func listRepository(ctx context.Context, opts repositoryOptions) error { } return nil }) + + if err != nil { + var repoErr error + if opts.namespace != "" { + repoErr = fmt.Errorf("could not list repositories for %q with prefix %q", reg.Reference.Host(), opts.namespace) + } else { + repoErr = fmt.Errorf("could not list repositories for %q", reg.Reference.Host()) + } + return errors.Join(repoErr, err) + } + + return nil } diff --git a/go.mod b/go.mod index 2d02b2f20..6c4422691 100644 --- a/go.mod +++ b/go.mod @@ -9,15 +9,15 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc5 github.com/oras-project/oras-credentials-go v0.3.1 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - golang.org/x/sync v0.4.0 - golang.org/x/term v0.13.0 + golang.org/x/sync v0.5.0 + golang.org/x/term v0.14.0 gopkg.in/yaml.v3 v3.0.1 oras.land/oras-go/v2 v2.3.1-0.20231026053328-062ed0e058f8 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index e8020248d..437327250 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,21 +19,21 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/e2e/README.md b/test/e2e/README.md index 2bd607f2c..2306b256c 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -189,6 +189,9 @@ graph TD; G1["referrer.index(image)"] -- subject --> F1 G2["referrer.image(image)"] -- subject --> F2 G3["index"] -- subject --> F1 + + H0>tag: unnamed]-..->H1["artifact contains unnamed layer"] + I0>tag: empty]-..->I1["artifact contains only one empty layer"] end ``` diff --git a/test/e2e/go.mod b/test/e2e/go.mod index 996ae4647..b62ed08f2 100644 --- a/test/e2e/go.mod +++ b/test/e2e/go.mod @@ -3,8 +3,8 @@ module oras.land/oras/test/e2e go 1.21 require ( - github.com/onsi/ginkgo/v2 v2.13.0 - github.com/onsi/gomega v1.29.0 + github.com/onsi/ginkgo/v2 v2.13.1 + github.com/onsi/gomega v1.30.0 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.0-rc5 gopkg.in/yaml.v2 v2.4.0 @@ -12,14 +12,14 @@ require ( ) require ( - github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.14.0 // indirect golang.org/x/text v0.13.0 // indirect - golang.org/x/tools v0.12.0 // indirect + golang.org/x/tools v0.14.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/e2e/go.sum b/test/e2e/go.sum index 513c61bc1..fb70bfb60 100644 --- a/test/e2e/go.sum +++ b/test/e2e/go.sum @@ -4,8 +4,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= @@ -15,10 +15,10 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= -github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= -github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU= +github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= @@ -29,19 +29,19 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/test/e2e/internal/testdata/artifact/empty/const.go b/test/e2e/internal/testdata/artifact/empty/const.go new file mode 100644 index 000000000..7b5170ef5 --- /dev/null +++ b/test/e2e/internal/testdata/artifact/empty/const.go @@ -0,0 +1,22 @@ +/* +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 empty + +const ( + Tag = "empty" + Digest = "sha256:7156dd4030da2375969c4da97e051d907f54ff8e9d28cc82c57289bbd22e3f9f" + ArtifactType = "test/empty.artifact" +) diff --git a/test/e2e/internal/testdata/artifact/unnamed/const.go b/test/e2e/internal/testdata/artifact/unnamed/const.go new file mode 100644 index 000000000..ffe21730f --- /dev/null +++ b/test/e2e/internal/testdata/artifact/unnamed/const.go @@ -0,0 +1,22 @@ +/* +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 unnamed + +const ( + Tag = "unnamed" + Digest = "sha256:977c6cf8e8aeaa35a5b5d6127e5008775d66d65985ac77634f79e1d7501bba83" + ArtifactType = "application/vnd.unknown.config.v1+json" +) diff --git a/test/e2e/suite/command/blob.go b/test/e2e/suite/command/blob.go index 249eec99a..770a0a78c 100644 --- a/test/e2e/suite/command/blob.go +++ b/test/e2e/suite/command/blob.go @@ -110,8 +110,8 @@ var _ = Describe("ORAS beginners:", func() { ExpectFailure().Exec() }) - It("should fail if provided digest doesn't existed", func() { - ORAS("blob", "fetch", RegistryRef(ZOTHost, ImageRepo, "sha256:2aaa2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")). + It("should fail if provided digest doesn't exist", func() { + ORAS("blob", "fetch", RegistryRef(ZOTHost, ImageRepo, "sha256:2aaa2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a"), "-o", "/dev/null"). ExpectFailure().Exec() }) diff --git a/test/e2e/suite/command/pull.go b/test/e2e/suite/command/pull.go index 65d5e6eea..06accb879 100644 --- a/test/e2e/suite/command/pull.go +++ b/test/e2e/suite/command/pull.go @@ -25,9 +25,10 @@ import ( "github.com/onsi/gomega" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" - "oras.land/oras-go/v2" "oras.land/oras/test/e2e/internal/testdata/artifact/blob" "oras.land/oras/test/e2e/internal/testdata/artifact/config" + "oras.land/oras/test/e2e/internal/testdata/artifact/empty" + "oras.land/oras/test/e2e/internal/testdata/artifact/unnamed" "oras.land/oras/test/e2e/internal/testdata/feature" "oras.land/oras/test/e2e/internal/testdata/foobar" "oras.land/oras/test/e2e/internal/testdata/multi_arch" @@ -40,6 +41,31 @@ var _ = Describe("ORAS beginners:", func() { out := ORAS("pull", "--help").MatchKeyWords(ExampleDesc).Exec().Out gomega.Expect(out).Should(gbytes.Say("--include-subject\\s+%s", regexp.QuoteMeta(feature.Preview.Mark))) }) + + hintMsg := func(reference string) string { + return fmt.Sprintf("Skipped pulling layers without file name in \"org.opencontainers.image.title\"\nUse 'oras copy %s --to-oci-layout ' to pull all layers.\n", reference) + } + It("should show hint for unnamed layer", func() { + tempDir := PrepareTempFiles() + ref := RegistryRef(ZOTHost, ArtifactRepo, unnamed.Tag) + ORAS("pull", ref). + WithWorkDir(tempDir). + MatchContent(hintMsg(ref)).Exec() + }) + + It("should not show hint for unnamed config blob", func() { + tempDir := PrepareTempFiles() + ref := RegistryRef(ZOTHost, ImageRepo, foobar.Tag) + out := ORAS("pull", ref).WithWorkDir(tempDir).Exec().Out + gomega.Expect(out).ShouldNot(gbytes.Say(hintMsg(ref))) + }) + + It("should not show hint for empty layer", func() { + tempDir := PrepareTempFiles() + ref := RegistryRef(ZOTHost, ArtifactRepo, empty.Tag) + out := ORAS("pull", ref).WithWorkDir(tempDir).Exec().Out + gomega.Expect(out).ShouldNot(gbytes.Say(hintMsg(ref))) + }) }) }) @@ -77,7 +103,7 @@ var _ = Describe("OCI spec 1.1 registry users:", func() { It("should skip config if media type not matching", func() { pullRoot := "pulled" tempDir := PrepareTempFiles() - stateKeys := append(foobar.ImageLayerStateKeys, foobar.ManifestStateKey, foobar.ImageConfigStateKey(oras.MediaTypeUnknownConfig)) + stateKeys := append(foobar.ImageLayerStateKeys, foobar.ManifestStateKey) ORAS("pull", RegistryRef(ZOTHost, ImageRepo, foobar.Tag), "-v", "--config", fmt.Sprintf("%s:%s", configName, "???"), "-o", pullRoot). MatchStatus(stateKeys, true, len(stateKeys)). WithWorkDir(tempDir).Exec() @@ -206,7 +232,7 @@ var _ = Describe("OCI image layout users:", func() { It("should skip config if media type does not match", func() { pullRoot := "pulled" root := PrepareTempOCI(ImageRepo) - stateKeys := append(foobar.ImageLayerStateKeys, foobar.ManifestStateKey, foobar.ImageConfigStateKey(oras.MediaTypeUnknownConfig)) + stateKeys := append(foobar.ImageLayerStateKeys, foobar.ManifestStateKey) ORAS("pull", Flags.Layout, LayoutRef(root, foobar.Tag), "-v", "--config", fmt.Sprintf("%s:%s", configName, "???"), "-o", pullRoot). MatchStatus(stateKeys, true, len(stateKeys)). WithWorkDir(root).Exec() diff --git a/test/e2e/testdata/zot/command/artifacts/blobs/sha256/7156dd4030da2375969c4da97e051d907f54ff8e9d28cc82c57289bbd22e3f9f b/test/e2e/testdata/zot/command/artifacts/blobs/sha256/7156dd4030da2375969c4da97e051d907f54ff8e9d28cc82c57289bbd22e3f9f new file mode 100644 index 000000000..feab712f9 --- /dev/null +++ b/test/e2e/testdata/zot/command/artifacts/blobs/sha256/7156dd4030da2375969c4da97e051d907f54ff8e9d28cc82c57289bbd22e3f9f @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","artifactType":"test/empty.artifact","config":{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2,"data":"e30="},"layers":[{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2,"data":"e30="}],"annotations":{"org.opencontainers.image.created":"2023-11-06T08:07:54Z"}} \ No newline at end of file diff --git a/test/e2e/testdata/zot/command/artifacts/blobs/sha256/977c6cf8e8aeaa35a5b5d6127e5008775d66d65985ac77634f79e1d7501bba83 b/test/e2e/testdata/zot/command/artifacts/blobs/sha256/977c6cf8e8aeaa35a5b5d6127e5008775d66d65985ac77634f79e1d7501bba83 new file mode 100644 index 000000000..9822b4869 --- /dev/null +++ b/test/e2e/testdata/zot/command/artifacts/blobs/sha256/977c6cf8e8aeaa35a5b5d6127e5008775d66d65985ac77634f79e1d7501bba83 @@ -0,0 +1 @@ +{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.unknown.config.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar","digest":"sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae","size":3}]} \ No newline at end of file diff --git a/test/e2e/testdata/zot/command/artifacts/index.json b/test/e2e/testdata/zot/command/artifacts/index.json index 58f824e70..83bff2e72 100644 --- a/test/e2e/testdata/zot/command/artifacts/index.json +++ b/test/e2e/testdata/zot/command/artifacts/index.json @@ -77,6 +77,22 @@ "mediaType": "application/vnd.oci.image.index.v1+json", "digest": "sha256:7679bc22c33b87aa345c6950a993db98a6df7a6cc77a35c388908a3a50be6bad", "size": 867 + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:7156dd4030da2375969c4da97e051d907f54ff8e9d28cc82c57289bbd22e3f9f", + "size": 519, + "annotations": { + "org.opencontainers.image.ref.name": "empty" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:977c6cf8e8aeaa35a5b5d6127e5008775d66d65985ac77634f79e1d7501bba83", + "size": 390, + "annotations": { + "org.opencontainers.image.ref.name": "unnamed" + } } ] } \ No newline at end of file