github.com/argoproj/argo-cd@v1.8.7/reposerver/cache/cache.go (about)

     1  package cache
     2  
     3  import (
     4  	"encoding/base64"
     5  	"encoding/json"
     6  	"fmt"
     7  	"hash/fnv"
     8  	"time"
     9  
    10  	"github.com/go-redis/redis/v8"
    11  	"github.com/spf13/cobra"
    12  
    13  	appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    14  	"github.com/argoproj/argo-cd/reposerver/apiclient"
    15  	cacheutil "github.com/argoproj/argo-cd/util/cache"
    16  	"github.com/argoproj/argo-cd/util/hash"
    17  
    18  	log "github.com/sirupsen/logrus"
    19  )
    20  
    21  var ErrCacheMiss = cacheutil.ErrCacheMiss
    22  
    23  type Cache struct {
    24  	cache               *cacheutil.Cache
    25  	repoCacheExpiration time.Duration
    26  }
    27  
    28  func NewCache(cache *cacheutil.Cache, repoCacheExpiration time.Duration) *Cache {
    29  	return &Cache{cache, repoCacheExpiration}
    30  }
    31  
    32  func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) func() (*Cache, error) {
    33  	var repoCacheExpiration time.Duration
    34  
    35  	cmd.Flags().DurationVar(&repoCacheExpiration, "repo-cache-expiration", 24*time.Hour, "Cache expiration for repo state, incl. app lists, app details, manifest generation, revision meta-data")
    36  
    37  	repoFactory := cacheutil.AddCacheFlagsToCmd(cmd, opts...)
    38  
    39  	return func() (*Cache, error) {
    40  		cache, err := repoFactory()
    41  		if err != nil {
    42  			return nil, err
    43  		}
    44  		return NewCache(cache, repoCacheExpiration), nil
    45  	}
    46  }
    47  
    48  func appSourceKey(appSrc *appv1.ApplicationSource) uint32 {
    49  	appSrc = appSrc.DeepCopy()
    50  	if !appSrc.IsHelm() {
    51  		appSrc.RepoURL = ""        // superceded by commitSHA
    52  		appSrc.TargetRevision = "" // superceded by commitSHA
    53  	}
    54  	appSrcStr, _ := json.Marshal(appSrc)
    55  	return hash.FNVa(string(appSrcStr))
    56  }
    57  
    58  func listApps(repoURL, revision string) string {
    59  	return fmt.Sprintf("ldir|%s|%s", repoURL, revision)
    60  }
    61  
    62  func (c *Cache) ListApps(repoUrl, revision string) (map[string]string, error) {
    63  	res := make(map[string]string)
    64  	err := c.cache.GetItem(listApps(repoUrl, revision), &res)
    65  	return res, err
    66  }
    67  
    68  func (c *Cache) SetApps(repoUrl, revision string, apps map[string]string) error {
    69  	return c.cache.SetItem(listApps(repoUrl, revision), apps, c.repoCacheExpiration, apps == nil)
    70  }
    71  
    72  func manifestCacheKey(revision string, appSrc *appv1.ApplicationSource, namespace string, appLabelKey string, appLabelValue string) string {
    73  	return fmt.Sprintf("mfst|%s|%s|%s|%s|%d", appLabelKey, appLabelValue, revision, namespace, appSourceKey(appSrc))
    74  }
    75  
    76  func (c *Cache) GetManifests(revision string, appSrc *appv1.ApplicationSource, namespace string, appLabelKey string, appLabelValue string, res *CachedManifestResponse) error {
    77  	err := c.cache.GetItem(manifestCacheKey(revision, appSrc, namespace, appLabelKey, appLabelValue), res)
    78  
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	hash, err := res.generateCacheEntryHash()
    84  	if err != nil {
    85  		return fmt.Errorf("Unable to generate hash value: %s", err)
    86  	}
    87  
    88  	// If the expected hash of the cache entry does not match the actual hash value...
    89  	if hash != res.CacheEntryHash {
    90  		log.Warnf("Manifest hash did not match expected value, treating as a cache miss: %s", appLabelValue)
    91  
    92  		err = c.DeleteManifests(revision, appSrc, namespace, appLabelKey, appLabelValue)
    93  		if err != nil {
    94  			return fmt.Errorf("Unable to delete manifest after hash mismatch, %v", err)
    95  		}
    96  
    97  		// Treat hash mismatches as cache misses, so that the underlying resource is reacquired
    98  		return ErrCacheMiss
    99  	}
   100  
   101  	// The expected hash matches the actual hash, so remove the hash from the returned value
   102  	res.CacheEntryHash = ""
   103  
   104  	return nil
   105  }
   106  
   107  func (c *Cache) SetManifests(revision string, appSrc *appv1.ApplicationSource, namespace string, appLabelKey string, appLabelValue string, res *CachedManifestResponse) error {
   108  
   109  	// Generate and apply the cache entry hash, before writing
   110  	if res != nil {
   111  		res = res.shallowCopy()
   112  		hash, err := res.generateCacheEntryHash()
   113  		if err != nil {
   114  			return fmt.Errorf("Unable to generate hash value: %s", err)
   115  		}
   116  		res.CacheEntryHash = hash
   117  	}
   118  
   119  	return c.cache.SetItem(manifestCacheKey(revision, appSrc, namespace, appLabelKey, appLabelValue), res, c.repoCacheExpiration, res == nil)
   120  }
   121  
   122  func (c *Cache) DeleteManifests(revision string, appSrc *appv1.ApplicationSource, namespace string, appLabelKey string, appLabelValue string) error {
   123  	return c.cache.SetItem(manifestCacheKey(revision, appSrc, namespace, appLabelKey, appLabelValue), "", c.repoCacheExpiration, true)
   124  }
   125  
   126  func appDetailsCacheKey(revision string, appSrc *appv1.ApplicationSource) string {
   127  	return fmt.Sprintf("appdetails|%s|%d", revision, appSourceKey(appSrc))
   128  }
   129  
   130  func (c *Cache) GetAppDetails(revision string, appSrc *appv1.ApplicationSource, res *apiclient.RepoAppDetailsResponse) error {
   131  	return c.cache.GetItem(appDetailsCacheKey(revision, appSrc), res)
   132  }
   133  
   134  func (c *Cache) SetAppDetails(revision string, appSrc *appv1.ApplicationSource, res *apiclient.RepoAppDetailsResponse) error {
   135  	return c.cache.SetItem(appDetailsCacheKey(revision, appSrc), res, c.repoCacheExpiration, res == nil)
   136  }
   137  
   138  func revisionMetadataKey(repoURL, revision string) string {
   139  	return fmt.Sprintf("revisionmetadata|%s|%s", repoURL, revision)
   140  }
   141  
   142  func (c *Cache) GetRevisionMetadata(repoURL, revision string) (*appv1.RevisionMetadata, error) {
   143  	item := &appv1.RevisionMetadata{}
   144  	return item, c.cache.GetItem(revisionMetadataKey(repoURL, revision), item)
   145  }
   146  
   147  func (c *Cache) SetRevisionMetadata(repoURL, revision string, item *appv1.RevisionMetadata) error {
   148  	return c.cache.SetItem(revisionMetadataKey(repoURL, revision), item, c.repoCacheExpiration, false)
   149  }
   150  
   151  func (cmr *CachedManifestResponse) shallowCopy() *CachedManifestResponse {
   152  	if cmr == nil {
   153  		return nil
   154  	}
   155  
   156  	return &CachedManifestResponse{
   157  		CacheEntryHash:                  cmr.CacheEntryHash,
   158  		FirstFailureTimestamp:           cmr.FirstFailureTimestamp,
   159  		ManifestResponse:                cmr.ManifestResponse,
   160  		MostRecentError:                 cmr.MostRecentError,
   161  		NumberOfCachedResponsesReturned: cmr.NumberOfCachedResponsesReturned,
   162  		NumberOfConsecutiveFailures:     cmr.NumberOfConsecutiveFailures,
   163  	}
   164  }
   165  
   166  func (cmr *CachedManifestResponse) generateCacheEntryHash() (string, error) {
   167  
   168  	// Copy, then remove the old hash
   169  	copy := cmr.shallowCopy()
   170  	copy.CacheEntryHash = ""
   171  
   172  	// 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)
   173  	bytes, err := json.Marshal(copy)
   174  	if err != nil {
   175  		return "", err
   176  	}
   177  	h := fnv.New64a()
   178  	_, err = h.Write(bytes)
   179  	if err != nil {
   180  		return "", err
   181  	}
   182  	fnvHash := h.Sum(nil)
   183  	return base64.URLEncoding.EncodeToString(fnvHash), nil
   184  
   185  }
   186  
   187  // CachedManifestResponse represents a cached result of a previous manifest generation operation, including the caching
   188  // of a manifest generation error, plus additional information on previous failures
   189  type CachedManifestResponse struct {
   190  
   191  	// NOTE: When adding fields to this struct, you MUST also update shallowCopy()
   192  
   193  	CacheEntryHash                  string                      `json:"cacheEntryHash"`
   194  	ManifestResponse                *apiclient.ManifestResponse `json:"manifestResponse"`
   195  	MostRecentError                 string                      `json:"mostRecentError"`
   196  	FirstFailureTimestamp           int64                       `json:"firstFailureTimestamp"`
   197  	NumberOfConsecutiveFailures     int                         `json:"numberOfConsecutiveFailures"`
   198  	NumberOfCachedResponsesReturned int                         `json:"numberOfCachedResponsesReturned"`
   199  }