diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index 721b57bbe..0423df717 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -27,9 +27,11 @@ import ( "oras.land/oras-go/v2" "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/file" + "oras.land/oras-go/v2/content/memory" "oras.land/oras/cmd/oras/internal/display" "oras.land/oras/cmd/oras/internal/fileref" "oras.land/oras/cmd/oras/internal/option" + "oras.land/oras/internal/contentutil" ) type pushOptions struct { @@ -159,12 +161,13 @@ func runPush(ctx context.Context, opts pushOptions) error { if err != nil { return err } + memoryStore := memory.New() pack := func() (ocispec.Descriptor, error) { - root, err := oras.Pack(ctx, store, opts.artifactType, descs, packOpts) + root, err := oras.Pack(ctx, memoryStore, opts.artifactType, descs, packOpts) if err != nil { return ocispec.Descriptor{}, err } - if err = store.Tag(ctx, root, root.Digest.String()); err != nil { + if err = memoryStore.Tag(ctx, root, root.Digest.String()); err != nil { return ocispec.Descriptor{}, err } return root, nil @@ -177,12 +180,13 @@ func runPush(ctx context.Context, opts pushOptions) error { } copyOptions := oras.DefaultCopyOptions copyOptions.Concurrency = opts.concurrency - updateDisplayOption(©Options.CopyGraphOptions, store, opts.Verbose) + union := contentutil.MultiReadOnlyTarget(memoryStore, store) + updateDisplayOption(©Options.CopyGraphOptions, union, opts.Verbose) copy := func(root ocispec.Descriptor) error { if tag := opts.Reference; tag == "" { - err = oras.CopyGraph(ctx, store, dst, root, copyOptions.CopyGraphOptions) + err = oras.CopyGraph(ctx, union, dst, root, copyOptions.CopyGraphOptions) } else { - _, err = oras.Copy(ctx, store, root.Digest.String(), dst, tag, copyOptions) + _, err = oras.Copy(ctx, union, root.Digest.String(), dst, tag, copyOptions) } return err } @@ -195,7 +199,7 @@ func runPush(ctx context.Context, opts pushOptions) error { fmt.Println("Pushed", opts.AnnotatedReference()) if len(opts.extraRefs) != 0 { - contentBytes, err := content.FetchAll(ctx, store, root) + contentBytes, err := content.FetchAll(ctx, memoryStore, root) if err != nil { return err } @@ -209,10 +213,10 @@ func runPush(ctx context.Context, opts pushOptions) error { fmt.Println("Digest:", root.Digest) // Export manifest - return opts.ExportManifest(ctx, store, root) + return opts.ExportManifest(ctx, memoryStore, root) } -func updateDisplayOption(opts *oras.CopyGraphOptions, store content.Fetcher, verbose bool) { +func updateDisplayOption(opts *oras.CopyGraphOptions, fetcher content.Fetcher, verbose bool) { committed := &sync.Map{} opts.PreCopy = display.StatusPrinter("Uploading", verbose) opts.OnCopySkipped = func(ctx context.Context, desc ocispec.Descriptor) error { @@ -221,7 +225,7 @@ func updateDisplayOption(opts *oras.CopyGraphOptions, store content.Fetcher, ver } 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, "Skipped ", store, committed, verbose); err != nil { + if err := display.PrintSuccessorStatus(ctx, desc, "Skipped ", fetcher, committed, verbose); err != nil { return err } return display.PrintStatus(desc, "Uploaded ", verbose) diff --git a/internal/contentutil/target.go b/internal/contentutil/target.go new file mode 100644 index 000000000..5ecf9d6f7 --- /dev/null +++ b/internal/contentutil/target.go @@ -0,0 +1,78 @@ +/* +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 contentutil + +import ( + "context" + "errors" + "io" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/errdef" +) + +type multiReadOnlyTarget struct { + targets []oras.ReadOnlyTarget +} + +// MultiReadOnlyTarget returns a ReadOnlyTarget that combines multiple targets. +func MultiReadOnlyTarget(targets ...oras.ReadOnlyTarget) oras.ReadOnlyTarget { + return &multiReadOnlyTarget{ + targets: targets, + } +} + +// Fetch fetches the content from the targets in order and return first found +// content. If no content is found, it returns ErrNotFound. +func (m *multiReadOnlyTarget) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) { + lastErr := errdef.ErrNotFound + for _, c := range m.targets { + rc, err := c.Fetch(ctx, target) + if err == nil { + return rc, nil + } + if !errors.Is(err, errdef.ErrNotFound) { + return nil, err + } + lastErr = err + } + return nil, lastErr +} + +// Exists returns true if the content exists in any of the targets. +// multiReadOnlyTarget does not implement Exists() because it's read-only. +func (m *multiReadOnlyTarget) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) { + return false, errors.New("MultiReadOnlyTarget.Exists() is not implemented") +} + +// Resolve resolves the reference to a descriptor from the targets in order and +// return first found descriptor. If no descriptor is found, it returns +// ErrNotFound. +func (m *multiReadOnlyTarget) Resolve(ctx context.Context, ref string) (ocispec.Descriptor, error) { + lastErr := errdef.ErrNotFound + for _, c := range m.targets { + desc, err := c.Resolve(ctx, ref) + if err == nil { + return desc, nil + } + if !errors.Is(err, errdef.ErrNotFound) { + return ocispec.Descriptor{}, err + } + lastErr = err + } + return ocispec.Descriptor{}, lastErr +}