Skip to content

Commit 0d8d277

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

File tree

15 files changed

+817
-159
lines changed

15 files changed

+817
-159
lines changed

pkg/remoteresolution/cache/cache.go

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy