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  }