Skip to content

Commit a984a73

Browse files
committed
add oci cache
Signed-off-by: Austin Abro <[email protected]>
1 parent a8fca42 commit a984a73

File tree

4 files changed

+185
-2
lines changed

4 files changed

+185
-2
lines changed

oci/cache/cache.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2024-Present Defense Unicorns
3+
4+
// Originally taken from - https://github.com/oras-project/oras/blob/main/internal/cache/target.go
5+
// Ideally we can merge this behavior into the oras-go library - https://github.com/oras-project/oras-go/issues/881
6+
/*
7+
Copyright The ORAS Authors.
8+
Licensed under the Apache License, Version 2.0 (the "License");
9+
you may not use this file except in compliance with the License.
10+
You may obtain a copy of the License at
11+
12+
http://www.apache.org/licenses/LICENSE-2.0
13+
14+
Unless required by applicable law or agreed to in writing, software
15+
distributed under the License is distributed on an "AS IS" BASIS,
16+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
See the License for the specific language governing permissions and
18+
limitations under the License.
19+
*/
20+
21+
// Package cache is used to create a ReadOnlyTarget from an existing target and an oci.Store
22+
package cache
23+
24+
import (
25+
"context"
26+
"io"
27+
"sync"
28+
29+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
30+
"oras.land/oras-go/v2"
31+
"oras.land/oras-go/v2/content"
32+
"oras.land/oras-go/v2/registry"
33+
)
34+
35+
type closer func() error
36+
37+
func (fn closer) Close() error {
38+
return fn()
39+
}
40+
41+
// Cache target struct.
42+
type target struct {
43+
oras.ReadOnlyTarget
44+
cache content.Storage
45+
}
46+
47+
// New generates a new target storage with caching.
48+
func New(source oras.ReadOnlyTarget, cache content.Storage) oras.ReadOnlyTarget {
49+
t := &target{
50+
ReadOnlyTarget: source,
51+
cache: cache,
52+
}
53+
if refFetcher, ok := source.(registry.ReferenceFetcher); ok {
54+
return &referenceTarget{
55+
target: t,
56+
ReferenceFetcher: refFetcher,
57+
}
58+
}
59+
return t
60+
}
61+
62+
// Fetch fetches the content identified by the descriptor.
63+
func (t *target) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
64+
rc, err := t.cache.Fetch(ctx, target)
65+
if err == nil {
66+
// Fetch from cache
67+
return rc, nil
68+
}
69+
70+
if rc, err = t.ReadOnlyTarget.Fetch(ctx, target); err != nil {
71+
return nil, err
72+
}
73+
74+
// Fetch from origin with caching
75+
return t.cacheReadCloser(ctx, rc, target), nil
76+
}
77+
78+
func (t *target) cacheReadCloser(ctx context.Context, rc io.ReadCloser, target ocispec.Descriptor) io.ReadCloser {
79+
pr, pw := io.Pipe()
80+
var wg sync.WaitGroup
81+
82+
wg.Add(1)
83+
var pushErr error
84+
go func() {
85+
defer wg.Done()
86+
pushErr = t.cache.Push(ctx, target, pr)
87+
if pushErr != nil {
88+
pr.CloseWithError(pushErr)
89+
}
90+
}()
91+
92+
return struct {
93+
io.Reader
94+
io.Closer
95+
}{
96+
Reader: io.TeeReader(rc, pw),
97+
Closer: closer(func() error {
98+
rcErr := rc.Close()
99+
if err := pw.Close(); err != nil {
100+
return err
101+
}
102+
wg.Wait()
103+
if pushErr != nil {
104+
return pushErr
105+
}
106+
return rcErr
107+
}),
108+
}
109+
}
110+
111+
// Exists returns true if the described content exists.
112+
func (t *target) Exists(ctx context.Context, desc ocispec.Descriptor) (bool, error) {
113+
exists, err := t.cache.Exists(ctx, desc)
114+
if err == nil && exists {
115+
return true, nil
116+
}
117+
return t.ReadOnlyTarget.Exists(ctx, desc)
118+
}
119+
120+
// Cache referenceTarget struct.
121+
type referenceTarget struct {
122+
*target
123+
registry.ReferenceFetcher
124+
}
125+
126+
// FetchReference fetches the content identified by the reference from the
127+
// remote and cache the fetched content.
128+
// Cached content will only be read via Fetch, FetchReference will always fetch
129+
// From origin.
130+
func (t *referenceTarget) FetchReference(ctx context.Context, reference string) (ocispec.Descriptor, io.ReadCloser, error) {
131+
target, rc, err := t.ReferenceFetcher.FetchReference(ctx, reference)
132+
if err != nil {
133+
return ocispec.Descriptor{}, nil, err
134+
}
135+
136+
// skip caching if the content already exists in cache
137+
exists, err := t.cache.Exists(ctx, target)
138+
if err != nil {
139+
return ocispec.Descriptor{}, nil, err
140+
}
141+
if exists {
142+
err = rc.Close()
143+
if err != nil {
144+
return ocispec.Descriptor{}, nil, err
145+
}
146+
147+
// get rc from the cache
148+
rc, err = t.cache.Fetch(ctx, target)
149+
if err != nil {
150+
return ocispec.Descriptor{}, nil, err
151+
}
152+
153+
// no need to do tee'd push
154+
return target, rc, nil
155+
}
156+
157+
// Fetch from origin with caching
158+
return target, t.cacheReadCloser(ctx, rc, target), nil
159+
}

oci/common.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212

1313
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
14+
"oras.land/oras-go/v2/content/oci"
1415
"oras.land/oras-go/v2/registry"
1516
"oras.land/oras-go/v2/registry/remote"
1617
"oras.land/oras-go/v2/registry/remote/auth"
@@ -28,6 +29,7 @@ const (
2829
// OrasRemote is a wrapper around the Oras remote repository that includes a progress bar for interactive feedback.
2930
type OrasRemote struct {
3031
repo *remote.Repository
32+
cache *oci.Store
3133
root *Manifest
3234
progTransport *helpers.Transport
3335
targetPlatform *ocispec.Platform
@@ -87,6 +89,13 @@ func WithLogger(logger *slog.Logger) Modifier {
8789
}
8890
}
8991

92+
// WithCache sets the cache for the remote
93+
func WithCache(cache *oci.Store) Modifier {
94+
return func(o *OrasRemote) {
95+
o.cache = cache
96+
}
97+
}
98+
9099
// NewOrasRemote returns an oras remote repository client and context for the given url.
91100
//
92101
// Registry auth is handled by the Docker CLI's credential store and checked before returning the client

oci/fetch.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"oras.land/oras-go/v2"
1313
"oras.land/oras-go/v2/content"
1414

15+
orasCache "github.com/defenseunicorns/pkg/oci/cache"
16+
1517
goyaml "github.com/goccy/go-yaml"
1618
)
1719

@@ -66,7 +68,12 @@ func (o *OrasRemote) FetchManifest(ctx context.Context, desc ocispec.Descriptor)
6668

6769
// FetchLayer fetches the layer with the given descriptor from the remote repository.
6870
func (o *OrasRemote) FetchLayer(ctx context.Context, desc ocispec.Descriptor) (bytes []byte, err error) {
69-
return content.FetchAll(ctx, o.repo, desc)
71+
var src oras.ReadOnlyTarget
72+
src = o.repo
73+
if o.cache != nil {
74+
src = orasCache.New(o.repo, o.cache)
75+
}
76+
return content.FetchAll(ctx, src, desc)
7077
}
7178

7279
// FetchJSONFile fetches the given JSON file from the remote repository.

oci/pull.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1414
"oras.land/oras-go/v2"
1515

16+
orasCache "github.com/defenseunicorns/pkg/oci/cache"
17+
1618
"github.com/defenseunicorns/pkg/helpers/v2"
1719
)
1820

@@ -68,8 +70,14 @@ func (o *OrasRemote) CopyToTarget(ctx context.Context, layers []ocispec.Descript
6870

6971
return oras.SkipNode
7072
}
73+
var src oras.ReadOnlyTarget
74+
src = o.repo
75+
76+
if o.cache != nil {
77+
src = orasCache.New(o.repo, o.cache)
78+
}
7179

72-
_, err := oras.Copy(ctx, o.repo, o.repo.Reference.String(), target, o.repo.Reference.String(), copyOpts)
80+
_, err := oras.Copy(ctx, src, o.repo.Reference.String(), target, o.repo.Reference.String(), copyOpts)
7381
if err != nil {
7482
return err
7583
}

0 commit comments

Comments
 (0)