diff --git a/cmd/oras/root/attach.go b/cmd/oras/root/attach.go index b67ca0c99..f60c9f390 100644 --- a/cmd/oras/root/attach.go +++ b/cmd/oras/root/attach.go @@ -26,8 +26,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/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" "oras.land/oras/cmd/oras/internal/option" "oras.land/oras/internal/graph" + "oras.land/oras/internal/registryutil" ) type attachOptions struct { @@ -116,6 +119,11 @@ func runAttach(ctx context.Context, opts attachOptions) error { if err := opts.EnsureReferenceNotEmpty(); err != nil { return err } + if repo, ok := dst.(*remote.Repository); ok { + // add both pull and push scope hints for dst repository + // to save potential push-scope token requests during copy + ctx = registryutil.WithScopeHint(ctx, repo.Reference, auth.ActionPull, auth.ActionPush) + } subject, err := dst.Resolve(ctx, opts.Reference) if err != nil { return err diff --git a/cmd/oras/root/push.go b/cmd/oras/root/push.go index c8e10a86c..601ad3a55 100644 --- a/cmd/oras/root/push.go +++ b/cmd/oras/root/push.go @@ -28,10 +28,13 @@ import ( "oras.land/oras-go/v2/content" "oras.land/oras-go/v2/content/file" "oras.land/oras-go/v2/content/memory" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" "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" + "oras.land/oras/internal/registryutil" ) type pushOptions struct { @@ -181,6 +184,12 @@ func runPush(ctx context.Context, opts pushOptions) error { union := contentutil.MultiReadOnlyTarget(memoryStore, store) updateDisplayOption(©Options.CopyGraphOptions, union, opts.Verbose) copy := func(root ocispec.Descriptor) error { + if repo, ok := dst.(*remote.Repository); ok { + // add both pull and push scope hints for dst repository + // to save potential push-scope token requests during copy + ctx = registryutil.WithScopeHint(ctx, repo.Reference, auth.ActionPull, auth.ActionPush) + } + if tag := opts.Reference; tag == "" { err = oras.CopyGraph(ctx, union, dst, root, copyOptions.CopyGraphOptions) } else { diff --git a/internal/registryutil/auth.go b/internal/registryutil/auth.go new file mode 100644 index 000000000..4a601f0c6 --- /dev/null +++ b/internal/registryutil/auth.go @@ -0,0 +1,29 @@ +/* +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 registryutil + +import ( + "context" + + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote/auth" +) + +// WithScopeHint adds a hinted scope to the context. +func WithScopeHint(ctx context.Context, ref registry.Reference, actions ...string) context.Context { + scope := auth.ScopeRepository(ref.Repository, actions...) + return auth.AppendScopes(ctx, scope) +}