github.com/argoproj/argo-cd/v2@v2.10.9/reposerver/cache/cache.go (about) 1 package cache 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "fmt" 7 "hash/fnv" 8 "math" 9 "sort" 10 "strings" 11 "time" 12 13 "github.com/argoproj/gitops-engine/pkg/utils/text" 14 "github.com/go-git/go-git/v5/plumbing" 15 "github.com/redis/go-redis/v9" 16 log "github.com/sirupsen/logrus" 17 "github.com/spf13/cobra" 18 19 appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 20 "github.com/argoproj/argo-cd/v2/reposerver/apiclient" 21 "github.com/argoproj/argo-cd/v2/util/argo" 22 cacheutil "github.com/argoproj/argo-cd/v2/util/cache" 23 "github.com/argoproj/argo-cd/v2/util/env" 24 "github.com/argoproj/argo-cd/v2/util/hash" 25 ) 26 27 var ErrCacheMiss = cacheutil.ErrCacheMiss 28 29 type Cache struct { 30 cache *cacheutil.Cache 31 repoCacheExpiration time.Duration 32 revisionCacheExpiration time.Duration 33 } 34 35 // ClusterRuntimeInfo holds cluster runtime information 36 type ClusterRuntimeInfo interface { 37 // GetApiVersions returns supported api versions 38 GetApiVersions() []string 39 // GetKubeVersion returns cluster API version 40 GetKubeVersion() string 41 } 42 43 func NewCache(cache *cacheutil.Cache, repoCacheExpiration time.Duration, revisionCacheExpiration time.Duration) *Cache { 44 return &Cache{cache, repoCacheExpiration, revisionCacheExpiration} 45 } 46 47 func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) func() (*Cache, error) { 48 var repoCacheExpiration time.Duration 49 var revisionCacheExpiration time.Duration 50 51 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") 52 cmd.Flags().DurationVar(&revisionCacheExpiration, "revision-cache-expiration", env.ParseDurationFromEnv("ARGOCD_RECONCILIATION_TIMEOUT", 3*time.Minute, 0, math.MaxInt64), "Cache expiration for cached revision") 53 54 repoFactory := cacheutil.AddCacheFlagsToCmd(cmd, opts...) 55 56 return func() (*Cache, error) { 57 cache, err := repoFactory() 58 if err != nil { 59 return nil, fmt.Errorf("error adding cache flags to cmd: %w", err) 60 } 61 return NewCache(cache, repoCacheExpiration, revisionCacheExpiration), nil 62 } 63 } 64 65 type refTargetForCacheKey struct { 66 RepoURL string `json:"repoURL"` 67 Project string `json:"project"` 68 TargetRevision string `json:"targetRevision"` 69 Chart string `json:"chart"` 70 } 71 72 func refTargetForCacheKeyFromRefTarget(refTarget *appv1.RefTarget) refTargetForCacheKey { 73 return refTargetForCacheKey{ 74 RepoURL: refTarget.Repo.Repo, 75 Project: refTarget.Repo.Project, 76 TargetRevision: refTarget.TargetRevision, 77 Chart: refTarget.Chart, 78 } 79 } 80 81 type refTargetRevisionMappingForCacheKey map[string]refTargetForCacheKey 82 83 func getRefTargetRevisionMappingForCacheKey(refTargetRevisionMapping appv1.RefTargetRevisionMapping) refTargetRevisionMappingForCacheKey { 84 res := make(refTargetRevisionMappingForCacheKey) 85 for k, v := range refTargetRevisionMapping { 86 res[k] = refTargetForCacheKeyFromRefTarget(v) 87 } 88 return res 89 } 90 91 func appSourceKey(appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, refSourceCommitSHAs ResolvedRevisions) uint32 { 92 return hash.FNVa(appSourceKeyJSON(appSrc, srcRefs, refSourceCommitSHAs)) 93 } 94 95 // ResolvedRevisions is a map of "normalized git URL" -> "git commit SHA". When one source references another source, 96 // the referenced source revision may change, for example, when someone pushes a commit to the referenced branch. This 97 // map lets us keep track of the current revision for each referenced source. 98 type ResolvedRevisions map[string]string 99 100 type appSourceKeyStruct struct { 101 AppSrc *appv1.ApplicationSource `json:"appSrc"` 102 SrcRefs refTargetRevisionMappingForCacheKey `json:"srcRefs"` 103 ResolvedRevisions ResolvedRevisions `json:"resolvedRevisions,omitempty"` 104 } 105 106 func appSourceKeyJSON(appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, refSourceCommitSHAs ResolvedRevisions) string { 107 appSrc = appSrc.DeepCopy() 108 if !appSrc.IsHelm() { 109 appSrc.RepoURL = "" // superseded by commitSHA 110 appSrc.TargetRevision = "" // superseded by commitSHA 111 } 112 appSrcStr, _ := json.Marshal(appSourceKeyStruct{ 113 AppSrc: appSrc, 114 SrcRefs: getRefTargetRevisionMappingForCacheKey(srcRefs), 115 ResolvedRevisions: refSourceCommitSHAs, 116 }) 117 return string(appSrcStr) 118 } 119 120 func clusterRuntimeInfoKey(info ClusterRuntimeInfo) uint32 { 121 if info == nil { 122 return 0 123 } 124 key := clusterRuntimeInfoKeyUnhashed(info) 125 return hash.FNVa(key) 126 } 127 128 // clusterRuntimeInfoKeyUnhashed gets the cluster runtime info for a cache key, but does not hash the info. Does not 129 // check if info is nil, the caller must do that. 130 func clusterRuntimeInfoKeyUnhashed(info ClusterRuntimeInfo) string { 131 apiVersions := info.GetApiVersions() 132 sort.Slice(apiVersions, func(i, j int) bool { 133 return apiVersions[i] < apiVersions[j] 134 }) 135 return info.GetKubeVersion() + "|" + strings.Join(apiVersions, ",") 136 } 137 138 func listApps(repoURL, revision string) string { 139 return fmt.Sprintf("ldir|%s|%s", repoURL, revision) 140 } 141 142 func (c *Cache) ListApps(repoUrl, revision string) (map[string]string, error) { 143 res := make(map[string]string) 144 err := c.cache.GetItem(listApps(repoUrl, revision), &res) 145 return res, err 146 } 147 148 func (c *Cache) SetApps(repoUrl, revision string, apps map[string]string) error { 149 return c.cache.SetItem(listApps(repoUrl, revision), apps, c.repoCacheExpiration, apps == nil) 150 } 151 152 func helmIndexRefsKey(repo string) string { 153 return fmt.Sprintf("helm-index|%s", repo) 154 } 155 156 // SetHelmIndex stores helm repository index.yaml content to cache 157 func (c *Cache) SetHelmIndex(repo string, indexData []byte) error { 158 return c.cache.SetItem(helmIndexRefsKey(repo), indexData, c.revisionCacheExpiration, false) 159 } 160 161 // GetHelmIndex retrieves helm repository index.yaml content from cache 162 func (c *Cache) GetHelmIndex(repo string, indexData *[]byte) error { 163 return c.cache.GetItem(helmIndexRefsKey(repo), indexData) 164 } 165 166 func gitRefsKey(repo string) string { 167 return fmt.Sprintf("git-refs|%s", repo) 168 } 169 170 // SetGitReferences saves resolved Git repository references to cache 171 func (c *Cache) SetGitReferences(repo string, references []*plumbing.Reference) error { 172 var input [][2]string 173 for i := range references { 174 input = append(input, references[i].Strings()) 175 } 176 return c.cache.SetItem(gitRefsKey(repo), input, c.revisionCacheExpiration, false) 177 } 178 179 // GetGitReferences retrieves resolved Git repository references from cache 180 func (c *Cache) GetGitReferences(repo string, references *[]*plumbing.Reference) error { 181 var input [][2]string 182 if err := c.cache.GetItem(gitRefsKey(repo), &input); err != nil { 183 return err 184 } 185 var res []*plumbing.Reference 186 for i := range input { 187 res = append(res, plumbing.NewReferenceFromStrings(input[i][0], input[i][1])) 188 } 189 *references = res 190 return nil 191 } 192 193 // refSourceCommitSHAs is a list of resolved revisions for each ref source. This allows us to invalidate the cache 194 // when someone pushes a commit to a source which is referenced from the main source (the one referred to by `revision`). 195 func manifestCacheKey(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, namespace string, trackingMethod string, appLabelKey string, appName string, info ClusterRuntimeInfo, refSourceCommitSHAs ResolvedRevisions) string { 196 // TODO: this function is getting unwieldy. We should probably consolidate some of this stuff into a struct. For 197 // example, revision could be part of ResolvedRevisions. And srcRefs is probably redundant now that 198 // refSourceCommitSHAs has been added. We don't need to know the _target_ revisions of the referenced sources 199 // when the _resolved_ revisions are already part of the key. 200 trackingKey := trackingKey(appLabelKey, trackingMethod) 201 return fmt.Sprintf("mfst|%s|%s|%s|%s|%d", trackingKey, appName, revision, namespace, appSourceKey(appSrc, srcRefs, refSourceCommitSHAs)+clusterRuntimeInfoKey(info)) 202 } 203 204 func trackingKey(appLabelKey string, trackingMethod string) string { 205 trackingKey := appLabelKey 206 if text.FirstNonEmpty(trackingMethod, string(argo.TrackingMethodLabel)) != string(argo.TrackingMethodLabel) { 207 trackingKey = trackingMethod + ":" + trackingKey 208 } 209 return trackingKey 210 } 211 212 // LogDebugManifestCacheKeyFields logs all the information included in a manifest cache key. It's intended to be run 213 // before every manifest cache operation to help debug cache misses. 214 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) { 215 if log.IsLevelEnabled(log.DebugLevel) { 216 log.WithFields(log.Fields{ 217 "revision": revision, 218 "appSrc": appSourceKeyJSON(appSrc, srcRefs, refSourceCommitSHAs), 219 "namespace": namespace, 220 "trackingKey": trackingKey(appLabelKey, trackingMethod), 221 "appName": appName, 222 "clusterInfo": clusterRuntimeInfoKeyUnhashed(clusterInfo), 223 "reason": reason, 224 }).Debug(message) 225 } 226 } 227 228 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) error { 229 err := c.cache.GetItem(manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs), res) 230 231 if err != nil { 232 return err 233 } 234 235 hash, err := res.generateCacheEntryHash() 236 if err != nil { 237 return fmt.Errorf("Unable to generate hash value: %s", err) 238 } 239 240 // If cached result does not have manifests or the expected hash of the cache entry does not match the actual hash value... 241 if hash != res.CacheEntryHash || res.ManifestResponse == nil && res.MostRecentError == "" { 242 log.Warnf("Manifest hash did not match expected value or cached manifests response is empty, treating as a cache miss: %s", appName) 243 244 LogDebugManifestCacheKeyFields("deleting manifests cache", "manifest hash did not match or cached response is empty", revision, appSrc, srcRefs, clusterInfo, namespace, trackingMethod, appLabelKey, appName, refSourceCommitSHAs) 245 246 err = c.DeleteManifests(revision, appSrc, srcRefs, clusterInfo, namespace, trackingMethod, appLabelKey, appName, refSourceCommitSHAs) 247 if err != nil { 248 return fmt.Errorf("Unable to delete manifest after hash mismatch, %v", err) 249 } 250 251 // Treat hash mismatches as cache misses, so that the underlying resource is reacquired 252 return ErrCacheMiss 253 } 254 255 // The expected hash matches the actual hash, so remove the hash from the returned value 256 res.CacheEntryHash = "" 257 258 return nil 259 } 260 261 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) error { 262 // Generate and apply the cache entry hash, before writing 263 if res != nil { 264 res = res.shallowCopy() 265 hash, err := res.generateCacheEntryHash() 266 if err != nil { 267 return fmt.Errorf("Unable to generate hash value: %s", err) 268 } 269 res.CacheEntryHash = hash 270 } 271 272 return c.cache.SetItem(manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs), res, c.repoCacheExpiration, res == nil) 273 } 274 275 func (c *Cache) DeleteManifests(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace, trackingMethod, appLabelKey, appName string, refSourceCommitSHAs ResolvedRevisions) error { 276 return c.cache.SetItem(manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs), "", c.repoCacheExpiration, true) 277 } 278 279 func appDetailsCacheKey(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, trackingMethod appv1.TrackingMethod, refSourceCommitSHAs ResolvedRevisions) string { 280 if trackingMethod == "" { 281 trackingMethod = argo.TrackingMethodLabel 282 } 283 return fmt.Sprintf("appdetails|%s|%d|%s", revision, appSourceKey(appSrc, srcRefs, refSourceCommitSHAs), trackingMethod) 284 } 285 286 func (c *Cache) GetAppDetails(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, res *apiclient.RepoAppDetailsResponse, trackingMethod appv1.TrackingMethod, refSourceCommitSHAs ResolvedRevisions) error { 287 return c.cache.GetItem(appDetailsCacheKey(revision, appSrc, srcRefs, trackingMethod, refSourceCommitSHAs), res) 288 } 289 290 func (c *Cache) SetAppDetails(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, res *apiclient.RepoAppDetailsResponse, trackingMethod appv1.TrackingMethod, refSourceCommitSHAs ResolvedRevisions) error { 291 return c.cache.SetItem(appDetailsCacheKey(revision, appSrc, srcRefs, trackingMethod, refSourceCommitSHAs), res, c.repoCacheExpiration, res == nil) 292 } 293 294 func revisionMetadataKey(repoURL, revision string) string { 295 return fmt.Sprintf("revisionmetadata|%s|%s", repoURL, revision) 296 } 297 298 func (c *Cache) GetRevisionMetadata(repoURL, revision string) (*appv1.RevisionMetadata, error) { 299 item := &appv1.RevisionMetadata{} 300 return item, c.cache.GetItem(revisionMetadataKey(repoURL, revision), item) 301 } 302 303 func (c *Cache) SetRevisionMetadata(repoURL, revision string, item *appv1.RevisionMetadata) error { 304 return c.cache.SetItem(revisionMetadataKey(repoURL, revision), item, c.repoCacheExpiration, false) 305 } 306 307 func revisionChartDetailsKey(repoURL, chart, revision string) string { 308 return fmt.Sprintf("chartdetails|%s|%s|%s", repoURL, chart, revision) 309 } 310 311 func (c *Cache) GetRevisionChartDetails(repoURL, chart, revision string) (*appv1.ChartDetails, error) { 312 item := &appv1.ChartDetails{} 313 return item, c.cache.GetItem(revisionChartDetailsKey(repoURL, chart, revision), item) 314 } 315 316 func (c *Cache) SetRevisionChartDetails(repoURL, chart, revision string, item *appv1.ChartDetails) error { 317 return c.cache.SetItem(revisionChartDetailsKey(repoURL, chart, revision), item, c.repoCacheExpiration, false) 318 } 319 320 func gitFilesKey(repoURL, revision, pattern string) string { 321 return fmt.Sprintf("gitfiles|%s|%s|%s", repoURL, revision, pattern) 322 } 323 324 func (c *Cache) SetGitFiles(repoURL, revision, pattern string, files map[string][]byte) error { 325 return c.cache.SetItem(gitFilesKey(repoURL, revision, pattern), &files, c.repoCacheExpiration, false) 326 } 327 328 func (c *Cache) GetGitFiles(repoURL, revision, pattern string) (map[string][]byte, error) { 329 var item map[string][]byte 330 return item, c.cache.GetItem(gitFilesKey(repoURL, revision, pattern), &item) 331 } 332 333 func gitDirectoriesKey(repoURL, revision string) string { 334 return fmt.Sprintf("gitdirs|%s|%s", repoURL, revision) 335 } 336 337 func (c *Cache) SetGitDirectories(repoURL, revision string, directories []string) error { 338 return c.cache.SetItem(gitDirectoriesKey(repoURL, revision), &directories, c.repoCacheExpiration, false) 339 } 340 341 func (c *Cache) GetGitDirectories(repoURL, revision string) ([]string, error) { 342 var item []string 343 return item, c.cache.GetItem(gitDirectoriesKey(repoURL, revision), &item) 344 } 345 346 func (cmr *CachedManifestResponse) shallowCopy() *CachedManifestResponse { 347 if cmr == nil { 348 return nil 349 } 350 351 return &CachedManifestResponse{ 352 CacheEntryHash: cmr.CacheEntryHash, 353 FirstFailureTimestamp: cmr.FirstFailureTimestamp, 354 ManifestResponse: cmr.ManifestResponse, 355 MostRecentError: cmr.MostRecentError, 356 NumberOfCachedResponsesReturned: cmr.NumberOfCachedResponsesReturned, 357 NumberOfConsecutiveFailures: cmr.NumberOfConsecutiveFailures, 358 } 359 } 360 361 func (cmr *CachedManifestResponse) generateCacheEntryHash() (string, error) { 362 363 // Copy, then remove the old hash 364 copy := cmr.shallowCopy() 365 copy.CacheEntryHash = "" 366 367 // 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) 368 bytes, err := json.Marshal(copy) 369 if err != nil { 370 return "", err 371 } 372 h := fnv.New64a() 373 _, err = h.Write(bytes) 374 if err != nil { 375 return "", err 376 } 377 fnvHash := h.Sum(nil) 378 return base64.URLEncoding.EncodeToString(fnvHash), nil 379 380 } 381 382 // CachedManifestResponse represents a cached result of a previous manifest generation operation, including the caching 383 // of a manifest generation error, plus additional information on previous failures 384 type CachedManifestResponse struct { 385 386 // NOTE: When adding fields to this struct, you MUST also update shallowCopy() 387 388 CacheEntryHash string `json:"cacheEntryHash"` 389 ManifestResponse *apiclient.ManifestResponse `json:"manifestResponse"` 390 MostRecentError string `json:"mostRecentError"` 391 FirstFailureTimestamp int64 `json:"firstFailureTimestamp"` 392 NumberOfConsecutiveFailures int `json:"numberOfConsecutiveFailures"` 393 NumberOfCachedResponsesReturned int `json:"numberOfCachedResponsesReturned"` 394 }