Skip to content

Commit ccd61e9

Browse files
authored
feat!: support oras.Fetch and oras.FetchBytes (#282)
BREAKING CHANGE: Move `content.ErrSizeExceedLimit` to `errdef.ErrSizeExceedsLimit` Signed-off-by: Sylvia Lei <lixlei@microsoft.com>
1 parent 59befb3 commit ccd61e9

File tree

11 files changed

+1292
-95
lines changed

11 files changed

+1292
-95
lines changed

content.go

+105-12
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ package oras
1818
import (
1919
"context"
2020
"fmt"
21+
"io"
2122

2223
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24+
"oras.land/oras-go/v2/content"
2325
"oras.land/oras-go/v2/errdef"
2426
"oras.land/oras-go/v2/internal/cas"
2527
"oras.land/oras-go/v2/internal/docker"
28+
"oras.land/oras-go/v2/internal/platform"
2629
"oras.land/oras-go/v2/registry"
2730
)
2831

@@ -60,14 +63,19 @@ type ResolveOptions struct {
6063
}
6164

6265
// Resolve resolves a descriptor with provided reference from the target.
63-
func Resolve(ctx context.Context, target ReadOnlyTarget, ref string, opts ResolveOptions) (ocispec.Descriptor, error) {
66+
func Resolve(ctx context.Context, target ReadOnlyTarget, reference string, opts ResolveOptions) (ocispec.Descriptor, error) {
6467
if opts.TargetPlatform == nil {
65-
return target.Resolve(ctx, ref)
68+
return target.Resolve(ctx, reference)
6669
}
70+
return resolve(ctx, target, nil, reference, opts)
71+
}
6772

73+
// resolve resolves a descriptor with provided reference from the target, with
74+
// specified caching.
75+
func resolve(ctx context.Context, target ReadOnlyTarget, proxy *cas.Proxy, reference string, opts ResolveOptions) (ocispec.Descriptor, error) {
6876
if refFetcher, ok := target.(registry.ReferenceFetcher); ok {
6977
// optimize performance for ReferenceFetcher targets
70-
desc, rc, err := refFetcher.FetchReference(ctx, ref)
78+
desc, rc, err := refFetcher.FetchReference(ctx, reference)
7179
if err != nil {
7280
return ocispec.Descriptor{}, err
7381
}
@@ -76,25 +84,110 @@ func Resolve(ctx context.Context, target ReadOnlyTarget, ref string, opts Resolv
7684
switch desc.MediaType {
7785
case docker.MediaTypeManifestList, ocispec.MediaTypeImageIndex,
7886
docker.MediaTypeManifest, ocispec.MediaTypeImageManifest:
79-
// create a proxy to cache the fetched descriptor
80-
store := cas.NewMemory()
81-
err = store.Push(ctx, desc, rc)
82-
if err != nil {
87+
// cache the fetched content
88+
if proxy == nil {
89+
proxy = cas.NewProxy(target, cas.NewMemory())
90+
}
91+
if err := proxy.Cache.Push(ctx, desc, rc); err != nil {
8392
return ocispec.Descriptor{}, err
8493
}
85-
86-
proxy := cas.NewProxy(target, store)
94+
// stop caching as SelectManifest may fetch a config blob
8795
proxy.StopCaching = true
88-
return selectPlatform(ctx, proxy, desc, opts.TargetPlatform)
96+
return platform.SelectManifest(ctx, proxy, desc, opts.TargetPlatform)
8997
default:
9098
return ocispec.Descriptor{}, fmt.Errorf("%s: %s: %w", desc.Digest, desc.MediaType, errdef.ErrUnsupported)
9199
}
92100
}
93101

94-
desc, err := target.Resolve(ctx, ref)
102+
desc, err := target.Resolve(ctx, reference)
95103
if err != nil {
96104
return ocispec.Descriptor{}, err
97105
}
106+
return platform.SelectManifest(ctx, target, desc, opts.TargetPlatform)
107+
}
108+
109+
// DefaultFetchOptions provides the default FetchOptions.
110+
var DefaultFetchOptions FetchOptions
111+
112+
// FetchOptions contains parameters for oras.Fetch.
113+
type FetchOptions struct {
114+
// ResolveOptions contains parameters for resolving reference.
115+
ResolveOptions
116+
}
117+
118+
// Fetch fetches the content identified by the reference.
119+
func Fetch(ctx context.Context, target ReadOnlyTarget, reference string, opts FetchOptions) (ocispec.Descriptor, io.ReadCloser, error) {
120+
if opts.TargetPlatform == nil {
121+
if refFetcher, ok := target.(registry.ReferenceFetcher); ok {
122+
return refFetcher.FetchReference(ctx, reference)
123+
}
124+
125+
desc, err := target.Resolve(ctx, reference)
126+
if err != nil {
127+
return ocispec.Descriptor{}, nil, err
128+
}
129+
rc, err := target.Fetch(ctx, desc)
130+
if err != nil {
131+
return ocispec.Descriptor{}, nil, err
132+
}
133+
return desc, rc, nil
134+
}
135+
136+
proxy := cas.NewProxy(target, cas.NewMemory())
137+
desc, err := resolve(ctx, target, proxy, reference, opts.ResolveOptions)
138+
if err != nil {
139+
return ocispec.Descriptor{}, nil, err
140+
}
141+
// if the content exists in cache, fetch it from cache
142+
// otherwise fetch without caching
143+
proxy.StopCaching = true
144+
rc, err := proxy.Fetch(ctx, desc)
145+
if err != nil {
146+
return ocispec.Descriptor{}, nil, err
147+
}
148+
return desc, rc, nil
149+
}
150+
151+
// DefaultFetchBytesOptions provides the default FetchBytesOptions.
152+
var DefaultFetchBytesOptions = FetchBytesOptions{
153+
MaxBytes: defaultMaxBytes,
154+
}
155+
156+
// defaultMaxBytes is the default value of MaxBytes.
157+
const defaultMaxBytes int64 = 4 * 1024 * 1024 // 4 MiB
158+
159+
// FetchBytesOptions contains parameters for oras.FetchBytes.
160+
type FetchBytesOptions struct {
161+
// FetchOptions contains parameters for fetching content.
162+
FetchOptions
163+
// MaxBytes limits the maximum size of the fetched content bytes.
164+
// If less than or equal to 0, a default (currently 4 MiB) is used.
165+
MaxBytes int64
166+
}
167+
168+
// FetchBytes fetches the content bytes identified by the reference.
169+
func FetchBytes(ctx context.Context, target ReadOnlyTarget, reference string, opts FetchBytesOptions) (ocispec.Descriptor, []byte, error) {
170+
if opts.MaxBytes <= 0 {
171+
opts.MaxBytes = defaultMaxBytes
172+
}
173+
174+
desc, rc, err := Fetch(ctx, target, reference, opts.FetchOptions)
175+
if err != nil {
176+
return ocispec.Descriptor{}, nil, err
177+
}
178+
defer rc.Close()
179+
180+
if desc.Size > opts.MaxBytes {
181+
return ocispec.Descriptor{}, nil, fmt.Errorf(
182+
"content size %v exceeds MaxBytes %v: %w",
183+
desc.Size,
184+
opts.MaxBytes,
185+
errdef.ErrSizeExceedsLimit)
186+
}
187+
bytes, err := content.ReadAll(rc, desc)
188+
if err != nil {
189+
return ocispec.Descriptor{}, nil, err
190+
}
98191

99-
return selectPlatform(ctx, target, desc, opts.TargetPlatform)
192+
return desc, bytes, nil
100193
}

content/file/file_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -529,8 +529,8 @@ func TestStore_Push_NoName_ExceedLimit(t *testing.T) {
529529

530530
// test push
531531
err := s.Push(ctx, desc, bytes.NewReader(blob))
532-
if !errors.Is(err, content.ErrSizeExceedLimit) {
533-
t.Errorf("Store.Push() error = %v, want %v", err, content.ErrSizeExceedLimit)
532+
if !errors.Is(err, errdef.ErrSizeExceedsLimit) {
533+
t.Errorf("Store.Push() error = %v, want %v", err, errdef.ErrSizeExceedsLimit)
534534
}
535535
}
536536

content/limitedstorage.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@ package content
1717

1818
import (
1919
"context"
20-
"errors"
20+
"fmt"
2121
"io"
2222

2323
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
24+
"oras.land/oras-go/v2/errdef"
2425
)
2526

26-
var ErrSizeExceedLimit = errors.New("size exceed limit")
27-
2827
// LimitedStorage represents a CAS with a push size limit.
2928
type LimitedStorage struct {
3029
Storage // underlying storage
@@ -35,7 +34,11 @@ type LimitedStorage struct {
3534
// The size of the content cannot exceed the push size limit.
3635
func (ls *LimitedStorage) Push(ctx context.Context, expected ocispec.Descriptor, content io.Reader) error {
3736
if expected.Size > ls.PushLimit {
38-
return ErrSizeExceedLimit
37+
return fmt.Errorf(
38+
"content size %v exceeds push size limit %v: %w",
39+
expected.Size,
40+
ls.PushLimit,
41+
errdef.ErrSizeExceedsLimit)
3942
}
4043

4144
return ls.Storage.Push(ctx, expected, io.LimitReader(content, expected.Size))

0 commit comments

Comments
 (0)