Skip to content

Commit a512086

Browse files
committed
Add shared cache for resolvers
1 parent b86ab22 commit a512086

File tree

16 files changed

+938
-598
lines changed

16 files changed

+938
-598
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
Copyright 2024 The Tekton Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cache
18+
19+
import (
20+
"crypto/sha256"
21+
"encoding/hex"
22+
"fmt"
23+
"time"
24+
25+
"context"
26+
27+
v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
28+
utilcache "k8s.io/apimachinery/pkg/util/cache"
29+
"knative.dev/pkg/logging"
30+
)
31+
32+
const (
33+
// DefaultExpiration is the default expiration time for cache entries
34+
DefaultExpiration = 5 * time.Minute
35+
36+
// DefaultMaxSize is the default size for the cache
37+
DefaultMaxSize = 1000
38+
)
39+
40+
// ResolverCache is a wrapper around utilcache.LRUExpireCache that provides
41+
// type-safe methods for caching resolver results.
42+
type ResolverCache struct {
43+
cache *utilcache.LRUExpireCache
44+
}
45+
46+
// NewResolverCache creates a new ResolverCache with the given expiration time and max size
47+
func NewResolverCache(maxSize int) *ResolverCache {
48+
return &ResolverCache{
49+
cache: utilcache.NewLRUExpireCache(maxSize),
50+
}
51+
}
52+
53+
// Get retrieves a value from the cache.
54+
func (c *ResolverCache) Get(key string) (interface{}, bool) {
55+
value, found := c.cache.Get(key)
56+
logger := logging.FromContext(context.Background())
57+
if found {
58+
logger.Debugw("Cache hit", "key", key)
59+
} else {
60+
logger.Debugw("Cache miss", "key", key)
61+
}
62+
return value, found
63+
}
64+
65+
// Add adds a value to the cache with the default expiration time.
66+
func (c *ResolverCache) Add(key string, value interface{}) {
67+
logger := logging.FromContext(context.Background())
68+
logger.Debugw("Adding to cache", "key", key, "expiration", DefaultExpiration)
69+
c.cache.Add(key, value, DefaultExpiration)
70+
}
71+
72+
// Remove removes a value from the cache.
73+
func (c *ResolverCache) Remove(key string) {
74+
logger := logging.FromContext(context.Background())
75+
logger.Debugw("Removing from cache", "key", key)
76+
c.cache.Remove(key)
77+
}
78+
79+
// AddWithExpiration adds a value to the cache with a custom expiration time
80+
func (c *ResolverCache) AddWithExpiration(key string, value interface{}, expiration time.Duration) {
81+
logger := logging.FromContext(context.Background())
82+
logger.Debugw("Adding to cache with custom expiration", "key", key, "expiration", expiration)
83+
c.cache.Add(key, value, expiration)
84+
}
85+
86+
// globalCache is the global instance of ResolverCache
87+
var globalCache = NewResolverCache(DefaultMaxSize)
88+
89+
// GetGlobalCache returns the global cache instance.
90+
func GetGlobalCache() *ResolverCache {
91+
return globalCache
92+
}
93+
94+
// GenerateCacheKey generates a cache key for the given resolver type and parameters.
95+
func GenerateCacheKey(resolverType string, params []v1.Param) (string, error) {
96+
// Create a deterministic string representation of the parameters
97+
paramStr := fmt.Sprintf("%s:", resolverType)
98+
for _, p := range params {
99+
paramStr += fmt.Sprintf("%s=%s;", p.Name, p.Value.StringVal)
100+
}
101+
102+
// Generate a SHA-256 hash of the parameter string
103+
hash := sha256.Sum256([]byte(paramStr))
104+
return hex.EncodeToString(hash[:]), nil
105+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
Copyright 2024 The Tekton Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cache
18+
19+
import (
20+
"testing"
21+
"time"
22+
23+
pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
24+
)
25+
26+
func TestGenerateCacheKey(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
resolverType string
30+
params []pipelinev1.Param
31+
wantErr bool
32+
}{
33+
{
34+
name: "empty params",
35+
resolverType: "http",
36+
params: []pipelinev1.Param{},
37+
wantErr: false,
38+
},
39+
{
40+
name: "single param",
41+
resolverType: "http",
42+
params: []pipelinev1.Param{
43+
{
44+
Name: "url",
45+
Value: pipelinev1.ParamValue{
46+
Type: pipelinev1.ParamTypeString,
47+
StringVal: "https://example.com",
48+
},
49+
},
50+
},
51+
wantErr: false,
52+
},
53+
{
54+
name: "multiple params",
55+
resolverType: "git",
56+
params: []pipelinev1.Param{
57+
{
58+
Name: "url",
59+
Value: pipelinev1.ParamValue{
60+
Type: pipelinev1.ParamTypeString,
61+
StringVal: "https://github.com/tektoncd/pipeline",
62+
},
63+
},
64+
{
65+
Name: "revision",
66+
Value: pipelinev1.ParamValue{
67+
Type: pipelinev1.ParamTypeString,
68+
StringVal: "main",
69+
},
70+
},
71+
},
72+
wantErr: false,
73+
},
74+
}
75+
76+
for _, tt := range tests {
77+
t.Run(tt.name, func(t *testing.T) {
78+
key, err := GenerateCacheKey(tt.resolverType, tt.params)
79+
if (err != nil) != tt.wantErr {
80+
t.Errorf("GenerateCacheKey() error = %v, wantErr %v", err, tt.wantErr)
81+
return
82+
}
83+
if !tt.wantErr && key == "" {
84+
t.Error("GenerateCacheKey() returned empty key")
85+
}
86+
})
87+
}
88+
}
89+
90+
func TestResolverCache(t *testing.T) {
91+
cache := NewResolverCache(DefaultMaxSize)
92+
93+
// Test adding and getting a value
94+
key := "test-key"
95+
value := "test-value"
96+
cache.Add(key, value)
97+
98+
if got, ok := cache.Get(key); !ok || got != value {
99+
t.Errorf("Get() = %v, %v, want %v, true", got, ok, value)
100+
}
101+
102+
// Test expiration
103+
shortExpiration := 100 * time.Millisecond
104+
cache.AddWithExpiration("expiring-key", "expiring-value", shortExpiration)
105+
time.Sleep(shortExpiration + 50*time.Millisecond)
106+
107+
if _, ok := cache.Get("expiring-key"); ok {
108+
t.Error("Get() returned true for expired key")
109+
}
110+
111+
// Test global cache
112+
globalCache1 := GetGlobalCache()
113+
globalCache2 := GetGlobalCache()
114+
if globalCache1 != globalCache2 {
115+
t.Error("GetGlobalCache() returned different instances")
116+
}
117+
}

pkg/remoteresolution/resolver/bundle/resolver.go

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ package bundle
1919
import (
2020
"context"
2121
"errors"
22+
"strings"
2223

2324
"github.com/tektoncd/pipeline/pkg/apis/resolution/v1beta1"
25+
"github.com/tektoncd/pipeline/pkg/remoteresolution/cache"
2426
"github.com/tektoncd/pipeline/pkg/remoteresolution/resolver/framework"
2527
"github.com/tektoncd/pipeline/pkg/resolution/common"
2628
"github.com/tektoncd/pipeline/pkg/resolution/resolver/bundle"
@@ -36,6 +38,16 @@ const (
3638

3739
// BundleResolverName is the name that the bundle resolver should be associated with.
3840
BundleResolverName = "bundleresolver"
41+
42+
// ConfigMapName is the bundle resolver's config map
43+
ConfigMapName = "bundleresolver-config"
44+
45+
// CacheModeAlways means always use cache regardless of bundle reference
46+
CacheModeAlways = "always"
47+
// CacheModeNever means never use cache regardless of bundle reference
48+
CacheModeNever = "never"
49+
// CacheModeAuto means use cache only when bundle reference has a digest
50+
CacheModeAuto = "auto"
3951
)
4052

4153
var _ framework.Resolver = &Resolver{}
@@ -58,17 +70,18 @@ func (r *Resolver) GetName(context.Context) string {
5870

5971
// GetConfigName returns the name of the bundle resolver's configmap.
6072
func (r *Resolver) GetConfigName(context.Context) string {
61-
return bundle.ConfigMapName
73+
return ConfigMapName
6274
}
6375

64-
// GetSelector returns a map of labels to match requests to this Resolver.
76+
// GetSelector returns the labels that resource requests are required to have for
77+
// the bundle resolver to process them.
6578
func (r *Resolver) GetSelector(context.Context) map[string]string {
6679
return map[string]string{
6780
common.LabelKeyResolverType: LabelValueBundleResolverType,
6881
}
6982
}
7083

71-
// Validate ensures reqolution request spec from a request are as expected.
84+
// Validate ensures parameters from a request are as expected.
7285
func (r *Resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestSpec) error {
7386
if len(req.Params) > 0 {
7487
return bundle.ValidateParams(ctx, req.Params)
@@ -77,11 +90,79 @@ func (r *Resolver) Validate(ctx context.Context, req *v1beta1.ResolutionRequestS
7790
return errors.New("cannot validate request. the Validate method has not been implemented.")
7891
}
7992

80-
// Resolve uses the given request spec resolve the requested file or resource.
93+
// ShouldUseCache determines if caching should be used based on the cache mode and bundle reference.
94+
func ShouldUseCache(cacheMode string, bundleRef string) bool {
95+
switch cacheMode {
96+
case CacheModeAlways:
97+
return true
98+
case CacheModeNever:
99+
return false
100+
case CacheModeAuto, "": // default to auto if not specified
101+
return IsOCIPullSpecByDigest(bundleRef)
102+
default: // invalid cache mode defaults to auto
103+
return IsOCIPullSpecByDigest(bundleRef)
104+
}
105+
}
106+
107+
// Resolve uses the given params to resolve the requested file or resource.
81108
func (r *Resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSpec) (resolutionframework.ResolvedResource, error) {
82109
if len(req.Params) > 0 {
83-
return bundle.ResolveRequest(ctx, r.kubeClientSet, req)
110+
if bundle.IsDisabled(ctx) {
111+
return nil, errors.New(bundle.DisabledError)
112+
}
113+
114+
origParams := req.Params
115+
116+
params, err := bundle.OptionsFromParams(ctx, origParams)
117+
if err != nil {
118+
return nil, err
119+
}
120+
121+
// Determine if caching should be used based on cache mode
122+
useCache := ShouldUseCache(params.Cache, params.Bundle)
123+
124+
if useCache {
125+
// Generate cache key
126+
cacheKey, err := cache.GenerateCacheKey(LabelValueBundleResolverType, origParams)
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
// Check cache first
132+
if cached, ok := cache.GetGlobalCache().Get(cacheKey); ok {
133+
if resource, ok := cached.(resolutionframework.ResolvedResource); ok {
134+
return resource, nil
135+
}
136+
}
137+
}
138+
139+
resource, err := bundle.ResolveRequest(ctx, r.kubeClientSet, req)
140+
if err != nil {
141+
return nil, err
142+
}
143+
144+
// Cache the result if caching is enabled
145+
if useCache {
146+
cacheKey, _ := cache.GenerateCacheKey(LabelValueBundleResolverType, origParams)
147+
cache.GetGlobalCache().Add(cacheKey, resource)
148+
}
149+
150+
return resource, nil
84151
}
85152
// Remove this error once resolution of url has been implemented.
86153
return nil, errors.New("the Resolve method has not been implemented.")
87154
}
155+
156+
// IsOCIPullSpecByDigest checks if the given OCI pull spec contains a digest.
157+
// A digest is typically in the format of @sha256:<hash> or :<tag>@sha256:<hash>
158+
func IsOCIPullSpecByDigest(pullSpec string) bool {
159+
// Check for @sha256: pattern
160+
if strings.Contains(pullSpec, "@sha256:") {
161+
return true
162+
}
163+
// Check for :<tag>@sha256: pattern
164+
if strings.Contains(pullSpec, ":") && strings.Contains(pullSpec, "@sha256:") {
165+
return true
166+
}
167+
return false
168+
}

0 commit comments

Comments
 (0)