@@ -30,6 +30,7 @@ import (
3030 "sort"
3131 "strings"
3232 "sync"
33+ "time"
3334
3435 "github.com/Masterminds/semver/v3"
3536 "helm.sh/helm/v3/pkg/getter"
@@ -38,6 +39,7 @@ import (
3839
3940 "github.com/fluxcd/pkg/version"
4041
42+ "github.com/fluxcd/source-controller/internal/cache"
4143 "github.com/fluxcd/source-controller/internal/helm"
4244 "github.com/fluxcd/source-controller/internal/transport"
4345)
@@ -70,13 +72,52 @@ type ChartRepository struct {
7072 tlsConfig * tls.Config
7173
7274 * sync.RWMutex
75+
76+ cacheInfo
77+ }
78+
79+ type cacheInfo struct {
80+ // In memory cache of the index.yaml file.
81+ IndexCache * cache.Cache
82+ // IndexKey is the cache key for the index.yaml file.
83+ IndexKey string
84+ // IndexTTL is the cache TTL for the index.yaml file.
85+ IndexTTL time.Duration
86+ // RecordIndexCacheMetric records the cache hit/miss metrics for the index.yaml file.
87+ RecordIndexCacheMetric RecordMetricsFunc
88+ }
89+
90+ // ChartRepositoryOptions is a function that can be passed to NewChartRepository
91+ // to configure a ChartRepository.
92+ type ChartRepositoryOption func (* ChartRepository ) error
93+
94+ // RecordMetricsFunc is a function that records metrics.
95+ type RecordMetricsFunc func (event string )
96+
97+ // WithMemoryCache returns a ChartRepositoryOptions that will enable the
98+ // ChartRepository to cache the index.yaml file in memory.
99+ // The cache key have to be safe in multi-tenancy environments,
100+ // as otherwise it could be used as a vector to bypass the helm repository's authentication.
101+ func WithMemoryCache (key string , c * cache.Cache , ttl time.Duration , rec RecordMetricsFunc ) ChartRepositoryOption {
102+ return func (r * ChartRepository ) error {
103+ if c != nil {
104+ if key == "" {
105+ return errors .New ("cache key cannot be empty" )
106+ }
107+ }
108+ r .IndexCache = c
109+ r .IndexKey = key
110+ r .IndexTTL = ttl
111+ r .RecordIndexCacheMetric = rec
112+ return nil
113+ }
73114}
74115
75116// NewChartRepository constructs and returns a new ChartRepository with
76117// the ChartRepository.Client configured to the getter.Getter for the
77118// repository URL scheme. It returns an error on URL parsing failures,
78119// or if there is no getter available for the scheme.
79- func NewChartRepository (repositoryURL , cachePath string , providers getter.Providers , tlsConfig * tls.Config , opts []getter.Option ) (* ChartRepository , error ) {
120+ func NewChartRepository (repositoryURL , cachePath string , providers getter.Providers , tlsConfig * tls.Config , getterOpts []getter.Option , chartRepoOpts ... ChartRepositoryOption ) (* ChartRepository , error ) {
80121 u , err := url .Parse (repositoryURL )
81122 if err != nil {
82123 return nil , err
@@ -90,8 +131,15 @@ func NewChartRepository(repositoryURL, cachePath string, providers getter.Provid
90131 r .URL = repositoryURL
91132 r .CachePath = cachePath
92133 r .Client = c
93- r .Options = opts
134+ r .Options = getterOpts
94135 r .tlsConfig = tlsConfig
136+
137+ for _ , opt := range chartRepoOpts {
138+ if err := opt (r ); err != nil {
139+ return nil , err
140+ }
141+ }
142+
95143 return r , nil
96144}
97145
@@ -292,14 +340,39 @@ func (r *ChartRepository) CacheIndex() (string, error) {
292340 return hex .EncodeToString (h .Sum (nil )), nil
293341}
294342
295- // StrategicallyLoadIndex lazy-loads the Index from CachePath using
296- // LoadFromCache if it does not HasIndex.
343+ // CacheIndexInMemory attempts to cache the index in memory.
344+ // It returns an error if it fails.
345+ // The cache key have to be safe in multi-tenancy environments,
346+ // as otherwise it could be used as a vector to bypass the helm repository's authentication.
347+ func (r * ChartRepository ) CacheIndexInMemory () error {
348+ // Cache the index if it was successfully retrieved
349+ // and the chart was successfully built
350+ if r .IndexCache != nil && r .Index != nil {
351+ err := r .IndexCache .Set (r .IndexKey , r .Index , r .IndexTTL )
352+ if err != nil {
353+ return err
354+ }
355+ }
356+
357+ return nil
358+ }
359+
360+ // StrategicallyLoadIndex lazy-loads the Index
361+ // first from Indexcache,
362+ // then from CachePath using oadFromCache if it does not HasIndex.
297363// If not HasCacheFile, a cache attempt is made using CacheIndex
298364// before continuing to load.
299365func (r * ChartRepository ) StrategicallyLoadIndex () (err error ) {
300366 if r .HasIndex () {
301367 return
302368 }
369+
370+ if r .IndexCache != nil {
371+ if found := r .LoadFromMemCache (); found {
372+ return
373+ }
374+ }
375+
303376 if ! r .HasCacheFile () {
304377 if _ , err = r .CacheIndex (); err != nil {
305378 err = fmt .Errorf ("failed to strategically load index: %w" , err )
@@ -313,6 +386,28 @@ func (r *ChartRepository) StrategicallyLoadIndex() (err error) {
313386 return
314387}
315388
389+ // LoadFromMemCache attempts to load the Index from the provided cache.
390+ // It returns true if the Index was found in the cache, and false otherwise.
391+ func (r * ChartRepository ) LoadFromMemCache () bool {
392+ if index , found := r .IndexCache .Get (r .IndexKey ); found {
393+ r .Lock ()
394+ r .Index = index .(* repo.IndexFile )
395+ r .Unlock ()
396+
397+ // record the cache hit
398+ if r .RecordIndexCacheMetric != nil {
399+ r .RecordIndexCacheMetric (cache .CacheEventTypeHit )
400+ }
401+ return true
402+ }
403+
404+ // record the cache miss
405+ if r .RecordIndexCacheMetric != nil {
406+ r .RecordIndexCacheMetric (cache .CacheEventTypeMiss )
407+ }
408+ return false
409+ }
410+
316411// LoadFromCache attempts to load the Index from the configured CachePath.
317412// It returns an error if no CachePath is set, or if the load failed.
318413func (r * ChartRepository ) LoadFromCache () error {
@@ -375,6 +470,14 @@ func (r *ChartRepository) Unload() {
375470 r .Index = nil
376471}
377472
473+ // SetMemCache sets the cache to use for this repository.
474+ func (r * ChartRepository ) SetMemCache (key string , c * cache.Cache , ttl time.Duration , rec RecordMetricsFunc ) {
475+ r .IndexKey = key
476+ r .IndexCache = c
477+ r .IndexTTL = ttl
478+ r .RecordIndexCacheMetric = rec
479+ }
480+
378481// RemoveCache removes the CachePath if Cached.
379482func (r * ChartRepository ) RemoveCache () error {
380483 if r == nil {
0 commit comments