github.com/argoproj/argo-cd/v3@v3.2.1/reposerver/cache/cache.go (about)

     1  package cache
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"hash/fnv"
     9  	"math"
    10  	"sort"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/argoproj/gitops-engine/pkg/utils/text"
    15  	"github.com/go-git/go-git/v5/plumbing"
    16  	log "github.com/sirupsen/logrus"
    17  	"github.com/spf13/cobra"
    18  
    19  	appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    20  	"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
    21  	cacheutil "github.com/argoproj/argo-cd/v3/util/cache"
    22  	"github.com/argoproj/argo-cd/v3/util/env"
    23  	"github.com/argoproj/argo-cd/v3/util/hash"
    24  )
    25  
    26  var (
    27  	ErrCacheMiss      = cacheutil.ErrCacheMiss
    28  	ErrCacheKeyLocked = cacheutil.ErrCacheKeyLocked
    29  )
    30  
    31  type Cache struct {
    32  	cache                    *cacheutil.Cache
    33  	repoCacheExpiration      time.Duration
    34  	revisionCacheExpiration  time.Duration
    35  	revisionCacheLockTimeout time.Duration
    36  }
    37  
    38  // ClusterRuntimeInfo holds cluster runtime information
    39  type ClusterRuntimeInfo interface {
    40  	// GetApiVersions returns supported api versions
    41  	GetApiVersions() []string
    42  	// GetKubeVersion returns cluster API version
    43  	GetKubeVersion() string
    44  }
    45  
    46  // CachedManifestResponse represents a cached result of a previous manifest generation operation, including the caching
    47  // of a manifest generation error, plus additional information on previous failures
    48  type CachedManifestResponse struct {
    49  	// NOTE: When adding fields to this struct, you MUST also update shallowCopy()
    50  
    51  	CacheEntryHash                  string                      `json:"cacheEntryHash"`
    52  	ManifestResponse                *apiclient.ManifestResponse `json:"manifestResponse"`
    53  	MostRecentError                 string                      `json:"mostRecentError"`
    54  	FirstFailureTimestamp           int64                       `json:"firstFailureTimestamp"`
    55  	NumberOfConsecutiveFailures     int                         `json:"numberOfConsecutiveFailures"`
    56  	NumberOfCachedResponsesReturned int                         `json:"numberOfCachedResponsesReturned"`
    57  }
    58  
    59  func NewCache(cache *cacheutil.Cache, repoCacheExpiration time.Duration, revisionCacheExpiration time.Duration, revisionCacheLockTimeout time.Duration) *Cache {
    60  	return &Cache{cache, repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout}
    61  }
    62  
    63  func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...cacheutil.Options) func() (*Cache, error) {
    64  	var repoCacheExpiration time.Duration
    65  	var revisionCacheExpiration time.Duration
    66  	var revisionCacheLockTimeout time.Duration
    67  
    68  	cmd.Flags().DurationVar(&repoCacheExpiration, "repo-cache-expiration", env.ParseDurationFromEnv("ARGOCD_REPO_CACHE_EXPIRATION", 24*time.Hour, 0, math.MaxInt64), "Cache expiration for repo state, incl. app lists, app details, manifest generation, revision meta-data")
    69  	cmd.Flags().DurationVar(&revisionCacheExpiration, "revision-cache-expiration", env.ParseDurationFromEnv("ARGOCD_RECONCILIATION_TIMEOUT", 3*time.Minute, 0, math.MaxInt64), "Cache expiration for cached revision")
    70  	cmd.Flags().DurationVar(&revisionCacheLockTimeout, "revision-cache-lock-timeout", env.ParseDurationFromEnv("ARGOCD_REVISION_CACHE_LOCK_TIMEOUT", 10*time.Second, 0, math.MaxInt64), "Cache TTL for locks to prevent duplicate requests on revisions, set to 0 to disable")
    71  
    72  	repoFactory := cacheutil.AddCacheFlagsToCmd(cmd, opts...)
    73  
    74  	return func() (*Cache, error) {
    75  		cache, err := repoFactory()
    76  		if err != nil {
    77  			return nil, fmt.Errorf("error adding cache flags to cmd: %w", err)
    78  		}
    79  		return NewCache(cache, repoCacheExpiration, revisionCacheExpiration, revisionCacheLockTimeout), nil
    80  	}
    81  }
    82  
    83  type refTargetForCacheKey struct {
    84  	RepoURL        string `json:"repoURL"`
    85  	Project        string `json:"project"`
    86  	TargetRevision string `json:"targetRevision"`
    87  	Chart          string `json:"chart"`
    88  }
    89  
    90  func refTargetForCacheKeyFromRefTarget(refTarget *appv1.RefTarget) refTargetForCacheKey {
    91  	return refTargetForCacheKey{
    92  		RepoURL:        refTarget.Repo.Repo,
    93  		Project:        refTarget.Repo.Project,
    94  		TargetRevision: refTarget.TargetRevision,
    95  		Chart:          refTarget.Chart,
    96  	}
    97  }
    98  
    99  type refTargetRevisionMappingForCacheKey map[string]refTargetForCacheKey
   100  
   101  func getRefTargetRevisionMappingForCacheKey(refTargetRevisionMapping appv1.RefTargetRevisionMapping) refTargetRevisionMappingForCacheKey {
   102  	res := make(refTargetRevisionMappingForCacheKey)
   103  	for k, v := range refTargetRevisionMapping {
   104  		res[k] = refTargetForCacheKeyFromRefTarget(v)
   105  	}
   106  	return res
   107  }
   108  
   109  func appSourceKey(appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, refSourceCommitSHAs ResolvedRevisions) uint32 {
   110  	return hash.FNVa(appSourceKeyJSON(appSrc, srcRefs, refSourceCommitSHAs))
   111  }
   112  
   113  // ResolvedRevisions is a map of "normalized git URL" -> "git commit SHA". When one source references another source,
   114  // the referenced source revision may change, for example, when someone pushes a commit to the referenced branch. This
   115  // map lets us keep track of the current revision for each referenced source.
   116  type ResolvedRevisions map[string]string
   117  
   118  type appSourceKeyStruct struct {
   119  	AppSrc            *appv1.ApplicationSource            `json:"appSrc"`
   120  	SrcRefs           refTargetRevisionMappingForCacheKey `json:"srcRefs"`
   121  	ResolvedRevisions ResolvedRevisions                   `json:"resolvedRevisions,omitempty"`
   122  }
   123  
   124  func appSourceKeyJSON(appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, refSourceCommitSHAs ResolvedRevisions) string {
   125  	appSrc = appSrc.DeepCopy()
   126  	if !appSrc.IsHelm() {
   127  		appSrc.RepoURL = ""        // superseded by commitSHA
   128  		appSrc.TargetRevision = "" // superseded by commitSHA
   129  	}
   130  	appSrcStr, _ := json.Marshal(appSourceKeyStruct{
   131  		AppSrc:            appSrc,
   132  		SrcRefs:           getRefTargetRevisionMappingForCacheKey(srcRefs),
   133  		ResolvedRevisions: refSourceCommitSHAs,
   134  	})
   135  	return string(appSrcStr)
   136  }
   137  
   138  func clusterRuntimeInfoKey(info ClusterRuntimeInfo) uint32 {
   139  	if info == nil {
   140  		return 0
   141  	}
   142  	key := clusterRuntimeInfoKeyUnhashed(info)
   143  	return hash.FNVa(key)
   144  }
   145  
   146  // clusterRuntimeInfoKeyUnhashed gets the cluster runtime info for a cache key, but does not hash the info. Does not
   147  // check if info is nil, the caller must do that.
   148  func clusterRuntimeInfoKeyUnhashed(info ClusterRuntimeInfo) string {
   149  	apiVersions := info.GetApiVersions()
   150  	sort.Slice(apiVersions, func(i, j int) bool {
   151  		return apiVersions[i] < apiVersions[j]
   152  	})
   153  	return info.GetKubeVersion() + "|" + strings.Join(apiVersions, ",")
   154  }
   155  
   156  func listApps(repoURL, revision string) string {
   157  	return fmt.Sprintf("ldir|%s|%s", repoURL, revision)
   158  }
   159  
   160  func (c *Cache) ListApps(repoURL, revision string) (map[string]string, error) {
   161  	res := make(map[string]string)
   162  	err := c.cache.GetItem(listApps(repoURL, revision), &res)
   163  	return res, err
   164  }
   165  
   166  func (c *Cache) SetApps(repoURL, revision string, apps map[string]string) error {
   167  	return c.cache.SetItem(
   168  		listApps(repoURL, revision),
   169  		apps,
   170  		&cacheutil.CacheActionOpts{
   171  			Expiration: c.repoCacheExpiration,
   172  			Delete:     apps == nil,
   173  		})
   174  }
   175  
   176  func helmIndexRefsKey(repo string) string {
   177  	return "helm-index|" + repo
   178  }
   179  
   180  func ociTagsKey(repo string) string {
   181  	return "oci-tags|" + repo
   182  }
   183  
   184  // SetHelmIndex stores helm repository index.yaml content to cache
   185  func (c *Cache) SetHelmIndex(repo string, indexData []byte) error {
   186  	if indexData == nil {
   187  		// Logged as warning upstream
   188  		return errors.New("helm index data is nil, skipping cache")
   189  	}
   190  	return c.cache.SetItem(
   191  		helmIndexRefsKey(repo),
   192  		indexData,
   193  		&cacheutil.CacheActionOpts{Expiration: c.revisionCacheExpiration})
   194  }
   195  
   196  // GetHelmIndex retrieves helm repository index.yaml content from cache
   197  func (c *Cache) GetHelmIndex(repo string, indexData *[]byte) error {
   198  	return c.cache.GetItem(helmIndexRefsKey(repo), indexData)
   199  }
   200  
   201  // SetOCITags stores oci image tags to cache
   202  func (c *Cache) SetOCITags(repo string, indexData []byte) error {
   203  	if indexData == nil {
   204  		// Logged as warning upstream
   205  		return errors.New("oci index data is nil, skipping cache")
   206  	}
   207  	return c.cache.SetItem(
   208  		ociTagsKey(repo),
   209  		indexData,
   210  		&cacheutil.CacheActionOpts{Expiration: c.revisionCacheExpiration})
   211  }
   212  
   213  // GetOCITags retrieves oci image tags from cache
   214  func (c *Cache) GetOCITags(repo string, indexData *[]byte) error {
   215  	return c.cache.GetItem(ociTagsKey(repo), indexData)
   216  }
   217  
   218  func gitRefsKey(repo string) string {
   219  	return "git-refs|" + repo
   220  }
   221  
   222  // SetGitReferences saves resolved Git repository references to cache
   223  func (c *Cache) SetGitReferences(repo string, references []*plumbing.Reference) error {
   224  	var input [][2]string
   225  	for i := range references {
   226  		input = append(input, references[i].Strings())
   227  	}
   228  	return c.cache.SetItem(gitRefsKey(repo), input, &cacheutil.CacheActionOpts{Expiration: c.revisionCacheExpiration})
   229  }
   230  
   231  // Converts raw cache items to plumbing.Reference objects
   232  func GitRefCacheItemToReferences(cacheItem [][2]string) *[]*plumbing.Reference {
   233  	var res []*plumbing.Reference
   234  	for i := range cacheItem {
   235  		// Skip empty data
   236  		if cacheItem[i][0] != "" || cacheItem[i][1] != "" {
   237  			res = append(res, plumbing.NewReferenceFromStrings(cacheItem[i][0], cacheItem[i][1]))
   238  		}
   239  	}
   240  	return &res
   241  }
   242  
   243  // TryLockGitRefCache attempts to lock the key for the Git repository references if the key doesn't exist, returns the value of
   244  // GetGitReferences after calling the SET
   245  func (c *Cache) TryLockGitRefCache(repo string, lockId string, references *[]*plumbing.Reference) (string, error) {
   246  	// This try set with DisableOverwrite is important for making sure that only one process is able to claim ownership
   247  	// A normal get + set, or just set would cause ownership to go to whoever the last writer was, and during race conditions
   248  	// leads to duplicate requests
   249  	err := c.cache.SetItem(gitRefsKey(repo), [][2]string{{cacheutil.CacheLockedValue, lockId}}, &cacheutil.CacheActionOpts{
   250  		Expiration:       c.revisionCacheLockTimeout,
   251  		DisableOverwrite: true,
   252  	})
   253  	if err != nil {
   254  		// Log but ignore this error since we'll want to retry, failing to obtain the lock should not throw an error
   255  		log.Errorf("Error attempting to acquire git references cache lock: %v", err)
   256  	}
   257  	return c.GetGitReferences(repo, references)
   258  }
   259  
   260  // Retrieves the cache item for git repo references. Returns foundLockId, error
   261  func (c *Cache) GetGitReferences(repo string, references *[]*plumbing.Reference) (string, error) {
   262  	var input [][2]string
   263  	err := c.cache.GetItem(gitRefsKey(repo), &input)
   264  	valueExists := len(input) > 0 && len(input[0]) > 0
   265  	switch {
   266  	// Unexpected Error
   267  	case err != nil && !errors.Is(err, ErrCacheMiss):
   268  		log.Errorf("Error attempting to retrieve git references from cache: %v", err)
   269  		return "", err
   270  	// Value is set
   271  	case valueExists && input[0][0] != cacheutil.CacheLockedValue:
   272  		*references = *GitRefCacheItemToReferences(input)
   273  		return "", nil
   274  	// Key is locked
   275  	case valueExists:
   276  		return input[0][1], nil
   277  	// No key or empty key
   278  	default:
   279  		return "", nil
   280  	}
   281  }
   282  
   283  // GetOrLockGitReferences retrieves the git references if they exist, otherwise creates a lock and returns so the caller can populate the cache
   284  // Returns isLockOwner, localLockId, error
   285  func (c *Cache) GetOrLockGitReferences(repo string, lockId string, references *[]*plumbing.Reference) (string, error) {
   286  	// Value matches the ttl on the lock in TryLockGitRefCache
   287  	waitUntil := time.Now().Add(c.revisionCacheLockTimeout)
   288  	// Wait only the maximum amount of time configured for the lock
   289  	// if the configured time is zero then the for loop will never run and instead act as the owner immediately
   290  	for time.Now().Before(waitUntil) {
   291  		// Get current cache state
   292  		if foundLockId, err := c.GetGitReferences(repo, references); foundLockId == lockId || err != nil || (references != nil && len(*references) > 0) {
   293  			return foundLockId, err
   294  		}
   295  		if foundLockId, err := c.TryLockGitRefCache(repo, lockId, references); foundLockId == lockId || err != nil || (references != nil && len(*references) > 0) {
   296  			return foundLockId, err
   297  		}
   298  		time.Sleep(1 * time.Second)
   299  	}
   300  	// If configured time is 0 then this is expected
   301  	if c.revisionCacheLockTimeout > 0 {
   302  		log.Debug("Repository cache was unable to acquire lock or valid data within timeout")
   303  	}
   304  	// Timeout waiting for lock
   305  	return lockId, nil
   306  }
   307  
   308  // UnlockGitReferences unlocks the key for the Git repository references if needed
   309  func (c *Cache) UnlockGitReferences(repo string, lockId string) error {
   310  	var input [][2]string
   311  	var err error
   312  	if err = c.cache.GetItem(gitRefsKey(repo), &input); err == nil &&
   313  		input != nil &&
   314  		len(input) > 0 &&
   315  		len(input[0]) > 1 &&
   316  		input[0][0] == cacheutil.CacheLockedValue &&
   317  		input[0][1] == lockId {
   318  		// We have the lock, so remove it
   319  		return c.cache.SetItem(gitRefsKey(repo), input, &cacheutil.CacheActionOpts{Delete: true})
   320  	}
   321  	return err
   322  }
   323  
   324  // refSourceCommitSHAs is a list of resolved revisions for each ref source. This allows us to invalidate the cache
   325  // when someone pushes a commit to a source which is referenced from the main source (the one referred to by `revision`).
   326  func manifestCacheKey(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, namespace string, trackingMethod string, appLabelKey string, appName string, info ClusterRuntimeInfo, refSourceCommitSHAs ResolvedRevisions, installationID string) string {
   327  	// TODO: this function is getting unwieldy. We should probably consolidate some of this stuff into a struct. For
   328  	//       example, revision could be part of ResolvedRevisions. And srcRefs is probably redundant now that
   329  	//       refSourceCommitSHAs has been added. We don't need to know the _target_ revisions of the referenced sources
   330  	//       when the _resolved_ revisions are already part of the key.
   331  	trackingKey := trackingKey(appLabelKey, trackingMethod)
   332  	key := fmt.Sprintf("mfst|%s|%s|%s|%s|%d", trackingKey, appName, revision, namespace, appSourceKey(appSrc, srcRefs, refSourceCommitSHAs)+clusterRuntimeInfoKey(info))
   333  	if installationID != "" {
   334  		key = fmt.Sprintf("%s|%s", key, installationID)
   335  	}
   336  	return key
   337  }
   338  
   339  func trackingKey(appLabelKey string, trackingMethod string) string {
   340  	trackingKey := appLabelKey
   341  	if text.FirstNonEmpty(trackingMethod, string(appv1.TrackingMethodLabel)) != string(appv1.TrackingMethodLabel) {
   342  		trackingKey = trackingMethod + ":" + trackingKey
   343  	}
   344  	return trackingKey
   345  }
   346  
   347  // LogDebugManifestCacheKeyFields logs all the information included in a manifest cache key. It's intended to be run
   348  // before every manifest cache operation to help debug cache misses.
   349  func LogDebugManifestCacheKeyFields(message string, reason string, revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace string, trackingMethod string, appLabelKey string, appName string, refSourceCommitSHAs ResolvedRevisions) {
   350  	if log.IsLevelEnabled(log.DebugLevel) {
   351  		log.WithFields(log.Fields{
   352  			"revision":    revision,
   353  			"appSrc":      appSourceKeyJSON(appSrc, srcRefs, refSourceCommitSHAs),
   354  			"namespace":   namespace,
   355  			"trackingKey": trackingKey(appLabelKey, trackingMethod),
   356  			"appName":     appName,
   357  			"clusterInfo": clusterRuntimeInfoKeyUnhashed(clusterInfo),
   358  			"reason":      reason,
   359  		}).Debug(message)
   360  	}
   361  }
   362  
   363  func (c *Cache) SetNewRevisionManifests(newRevision string, revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace string, trackingMethod string, appLabelKey string, appName string, refSourceCommitSHAs ResolvedRevisions, installationID string) error {
   364  	oldKey := manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs, installationID)
   365  	newKey := manifestCacheKey(newRevision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs, installationID)
   366  	return c.cache.RenameItem(oldKey, newKey, c.repoCacheExpiration)
   367  }
   368  
   369  func (c *Cache) GetManifests(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace string, trackingMethod string, appLabelKey string, appName string, res *CachedManifestResponse, refSourceCommitSHAs ResolvedRevisions, installationID string) error {
   370  	err := c.cache.GetItem(manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs, installationID), res)
   371  	if err != nil {
   372  		return err
   373  	}
   374  
   375  	hash, err := res.generateCacheEntryHash()
   376  	if err != nil {
   377  		return fmt.Errorf("unable to generate hash value: %w", err)
   378  	}
   379  
   380  	// If cached result does not have manifests or the expected hash of the cache entry does not match the actual hash value...
   381  	if hash != res.CacheEntryHash || res.ManifestResponse == nil && res.MostRecentError == "" {
   382  		log.Warnf("Manifest hash did not match expected value or cached manifests response is empty, treating as a cache miss: %s", appName)
   383  
   384  		LogDebugManifestCacheKeyFields("deleting manifests cache", "manifest hash did not match or cached response is empty", revision, appSrc, srcRefs, clusterInfo, namespace, trackingMethod, appLabelKey, appName, refSourceCommitSHAs)
   385  
   386  		err = c.DeleteManifests(revision, appSrc, srcRefs, clusterInfo, namespace, trackingMethod, appLabelKey, appName, refSourceCommitSHAs, installationID)
   387  		if err != nil {
   388  			return fmt.Errorf("unable to delete manifest after hash mismatch: %w", err)
   389  		}
   390  
   391  		// Treat hash mismatches as cache misses, so that the underlying resource is reacquired
   392  		return ErrCacheMiss
   393  	}
   394  
   395  	// The expected hash matches the actual hash, so remove the hash from the returned value
   396  	res.CacheEntryHash = ""
   397  
   398  	if res.ManifestResponse != nil {
   399  		// cached manifest response might be reused across different revisions, so we need to assume that the revision is the one we are looking for
   400  		res.ManifestResponse.Revision = revision
   401  	}
   402  
   403  	return nil
   404  }
   405  
   406  func (c *Cache) SetManifests(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace string, trackingMethod string, appLabelKey string, appName string, res *CachedManifestResponse, refSourceCommitSHAs ResolvedRevisions, installationID string) error {
   407  	// Generate and apply the cache entry hash, before writing
   408  	if res != nil {
   409  		res = res.shallowCopy()
   410  		hash, err := res.generateCacheEntryHash()
   411  		if err != nil {
   412  			return fmt.Errorf("unable to generate hash value: %w", err)
   413  		}
   414  		res.CacheEntryHash = hash
   415  	}
   416  
   417  	return c.cache.SetItem(
   418  		manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs, installationID),
   419  		res,
   420  		&cacheutil.CacheActionOpts{
   421  			Expiration: c.repoCacheExpiration,
   422  			Delete:     res == nil,
   423  		})
   424  }
   425  
   426  func (c *Cache) DeleteManifests(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace, trackingMethod, appLabelKey, appName string, refSourceCommitSHAs ResolvedRevisions, installationID string) error {
   427  	return c.cache.SetItem(
   428  		manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs, installationID),
   429  		"",
   430  		&cacheutil.CacheActionOpts{Delete: true})
   431  }
   432  
   433  func appDetailsCacheKey(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, trackingMethod appv1.TrackingMethod, refSourceCommitSHAs ResolvedRevisions) string {
   434  	if trackingMethod == "" {
   435  		trackingMethod = appv1.TrackingMethodLabel
   436  	}
   437  	return fmt.Sprintf("appdetails|%s|%d|%s", revision, appSourceKey(appSrc, srcRefs, refSourceCommitSHAs), trackingMethod)
   438  }
   439  
   440  func (c *Cache) GetAppDetails(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, res *apiclient.RepoAppDetailsResponse, trackingMethod appv1.TrackingMethod, refSourceCommitSHAs ResolvedRevisions) error {
   441  	return c.cache.GetItem(appDetailsCacheKey(revision, appSrc, srcRefs, trackingMethod, refSourceCommitSHAs), res)
   442  }
   443  
   444  func (c *Cache) SetAppDetails(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, res *apiclient.RepoAppDetailsResponse, trackingMethod appv1.TrackingMethod, refSourceCommitSHAs ResolvedRevisions) error {
   445  	return c.cache.SetItem(
   446  		appDetailsCacheKey(revision, appSrc, srcRefs, trackingMethod, refSourceCommitSHAs),
   447  		res,
   448  		&cacheutil.CacheActionOpts{
   449  			Expiration: c.repoCacheExpiration,
   450  			Delete:     res == nil,
   451  		})
   452  }
   453  
   454  func revisionMetadataKey(repoURL, revision string) string {
   455  	return fmt.Sprintf("revisionmetadata|%s|%s", repoURL, revision)
   456  }
   457  
   458  func (c *Cache) GetRevisionMetadata(repoURL, revision string) (*appv1.RevisionMetadata, error) {
   459  	item := &appv1.RevisionMetadata{}
   460  	return item, c.cache.GetItem(revisionMetadataKey(repoURL, revision), item)
   461  }
   462  
   463  func (c *Cache) SetRevisionMetadata(repoURL, revision string, item *appv1.RevisionMetadata) error {
   464  	return c.cache.SetItem(
   465  		revisionMetadataKey(repoURL, revision),
   466  		item,
   467  		&cacheutil.CacheActionOpts{Expiration: c.repoCacheExpiration})
   468  }
   469  
   470  func revisionChartDetailsKey(repoURL, chart, revision string) string {
   471  	return fmt.Sprintf("chartdetails|%s|%s|%s", repoURL, chart, revision)
   472  }
   473  
   474  func (c *Cache) GetRevisionChartDetails(repoURL, chart, revision string) (*appv1.ChartDetails, error) {
   475  	item := &appv1.ChartDetails{}
   476  	return item, c.cache.GetItem(revisionChartDetailsKey(repoURL, chart, revision), item)
   477  }
   478  
   479  func (c *Cache) SetRevisionChartDetails(repoURL, chart, revision string, item *appv1.ChartDetails) error {
   480  	return c.cache.SetItem(
   481  		revisionChartDetailsKey(repoURL, chart, revision),
   482  		item,
   483  		&cacheutil.CacheActionOpts{Expiration: c.repoCacheExpiration})
   484  }
   485  
   486  func gitFilesKey(repoURL, revision, pattern string) string {
   487  	return fmt.Sprintf("gitfiles|%s|%s|%s", repoURL, revision, pattern)
   488  }
   489  
   490  func (c *Cache) SetGitFiles(repoURL, revision, pattern string, files map[string][]byte) error {
   491  	return c.cache.SetItem(
   492  		gitFilesKey(repoURL, revision, pattern),
   493  		&files,
   494  		&cacheutil.CacheActionOpts{Expiration: c.repoCacheExpiration})
   495  }
   496  
   497  func (c *Cache) GetGitFiles(repoURL, revision, pattern string) (map[string][]byte, error) {
   498  	var item map[string][]byte
   499  	err := c.cache.GetItem(gitFilesKey(repoURL, revision, pattern), &item)
   500  	return item, err
   501  }
   502  
   503  func gitDirectoriesKey(repoURL, revision string) string {
   504  	return fmt.Sprintf("gitdirs|%s|%s", repoURL, revision)
   505  }
   506  
   507  func (c *Cache) SetGitDirectories(repoURL, revision string, directories []string) error {
   508  	return c.cache.SetItem(
   509  		gitDirectoriesKey(repoURL, revision),
   510  		&directories,
   511  		&cacheutil.CacheActionOpts{Expiration: c.repoCacheExpiration})
   512  }
   513  
   514  func (c *Cache) GetGitDirectories(repoURL, revision string) ([]string, error) {
   515  	var item []string
   516  	err := c.cache.GetItem(gitDirectoriesKey(repoURL, revision), &item)
   517  	return item, err
   518  }
   519  
   520  func (cmr *CachedManifestResponse) shallowCopy() *CachedManifestResponse {
   521  	if cmr == nil {
   522  		return nil
   523  	}
   524  
   525  	return &CachedManifestResponse{
   526  		CacheEntryHash:                  cmr.CacheEntryHash,
   527  		FirstFailureTimestamp:           cmr.FirstFailureTimestamp,
   528  		ManifestResponse:                cmr.ManifestResponse,
   529  		MostRecentError:                 cmr.MostRecentError,
   530  		NumberOfCachedResponsesReturned: cmr.NumberOfCachedResponsesReturned,
   531  		NumberOfConsecutiveFailures:     cmr.NumberOfConsecutiveFailures,
   532  	}
   533  }
   534  
   535  func (cmr *CachedManifestResponse) generateCacheEntryHash() (string, error) {
   536  	// Copy, then remove the old hash
   537  	shallowCopy := cmr.shallowCopy()
   538  	shallowCopy.CacheEntryHash = ""
   539  
   540  	// Hash the JSON representation into a base-64-encoded FNV 64a (we don't need a cryptographic hash algorithm, since this is only for detecting data corruption)
   541  	bytes, err := json.Marshal(shallowCopy)
   542  	if err != nil {
   543  		return "", err
   544  	}
   545  	h := fnv.New64a()
   546  	_, err = h.Write(bytes)
   547  	if err != nil {
   548  		return "", err
   549  	}
   550  	fnvHash := h.Sum(nil)
   551  	return base64.URLEncoding.EncodeToString(fnvHash), nil
   552  }