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 }