github.com/argoproj/argo-cd/v3@v3.2.1/util/argo/diff/diff.go (about)

     1  package diff
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/go-logr/logr"
     8  	log "github.com/sirupsen/logrus"
     9  
    10  	k8smanagedfields "k8s.io/apimachinery/pkg/util/managedfields"
    11  
    12  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    13  	"github.com/argoproj/argo-cd/v3/util/argo"
    14  	"github.com/argoproj/argo-cd/v3/util/argo/managedfields"
    15  	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
    16  	appstatecache "github.com/argoproj/argo-cd/v3/util/cache/appstate"
    17  
    18  	"github.com/argoproj/gitops-engine/pkg/diff"
    19  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    20  	"github.com/argoproj/gitops-engine/pkg/utils/kube/scheme"
    21  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    22  )
    23  
    24  // DiffConfigBuilder is used as a safe way to create valid DiffConfigs.
    25  type DiffConfigBuilder struct {
    26  	diffConfig *diffConfig
    27  }
    28  
    29  // NewDiffConfigBuilder create a new DiffConfigBuilder instance.
    30  func NewDiffConfigBuilder() *DiffConfigBuilder {
    31  	return &DiffConfigBuilder{
    32  		diffConfig: &diffConfig{
    33  			ignoreMutationWebhook: true,
    34  		},
    35  	}
    36  }
    37  
    38  // WithDiffSettings will set the diff settings in the builder.
    39  func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDifferences, o map[string]v1alpha1.ResourceOverride, ignoreAggregatedRoles bool, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) *DiffConfigBuilder {
    40  	ignores := id
    41  	if ignores == nil {
    42  		ignores = []v1alpha1.ResourceIgnoreDifferences{}
    43  	}
    44  	b.diffConfig.ignores = ignores
    45  
    46  	overrides := o
    47  	if overrides == nil {
    48  		overrides = make(map[string]v1alpha1.ResourceOverride)
    49  	}
    50  	b.diffConfig.overrides = overrides
    51  	b.diffConfig.ignoreAggregatedRoles = ignoreAggregatedRoles
    52  	b.diffConfig.ignoreNormalizerOpts = ignoreNormalizerOpts
    53  	return b
    54  }
    55  
    56  // WithTrackingMethod sets the tracking in the diff config.
    57  func (b *DiffConfigBuilder) WithTracking(appLabelKey, trackingMethod string) *DiffConfigBuilder {
    58  	b.diffConfig.appLabelKey = appLabelKey
    59  	b.diffConfig.trackingMethod = trackingMethod
    60  	return b
    61  }
    62  
    63  // WithNoCache sets the nocache in the diff config.
    64  func (b *DiffConfigBuilder) WithNoCache() *DiffConfigBuilder {
    65  	b.diffConfig.noCache = true
    66  	return b
    67  }
    68  
    69  // WithCache sets the appstatecache.Cache and the appName in the diff config. Those the
    70  // are two objects necessary to retrieve a cached diff.
    71  func (b *DiffConfigBuilder) WithCache(s *appstatecache.Cache, appName string) *DiffConfigBuilder {
    72  	b.diffConfig.stateCache = s
    73  	b.diffConfig.appName = appName
    74  	return b
    75  }
    76  
    77  // WithLogger sets the logger in the diff config.
    78  func (b *DiffConfigBuilder) WithLogger(l logr.Logger) *DiffConfigBuilder {
    79  	b.diffConfig.logger = &l
    80  	return b
    81  }
    82  
    83  // WithGVKParser sets the gvkParser in the diff config.
    84  func (b *DiffConfigBuilder) WithGVKParser(parser *k8smanagedfields.GvkParser) *DiffConfigBuilder {
    85  	b.diffConfig.gvkParser = parser
    86  	return b
    87  }
    88  
    89  // WithStructuredMergeDiff defines if the diff should be calculated using structured
    90  // merge.
    91  func (b *DiffConfigBuilder) WithStructuredMergeDiff(smd bool) *DiffConfigBuilder {
    92  	b.diffConfig.structuredMergeDiff = smd
    93  	return b
    94  }
    95  
    96  // WithManager defines the manager that should be using during structured
    97  // merge diffs.
    98  func (b *DiffConfigBuilder) WithManager(manager string) *DiffConfigBuilder {
    99  	b.diffConfig.manager = manager
   100  	return b
   101  }
   102  
   103  func (b *DiffConfigBuilder) WithServerSideDryRunner(ssdr diff.ServerSideDryRunner) *DiffConfigBuilder {
   104  	b.diffConfig.serverSideDryRunner = ssdr
   105  	return b
   106  }
   107  
   108  func (b *DiffConfigBuilder) WithServerSideDiff(ssd bool) *DiffConfigBuilder {
   109  	b.diffConfig.serverSideDiff = ssd
   110  	return b
   111  }
   112  
   113  func (b *DiffConfigBuilder) WithIgnoreMutationWebhook(m bool) *DiffConfigBuilder {
   114  	b.diffConfig.ignoreMutationWebhook = m
   115  	return b
   116  }
   117  
   118  // Build will first validate the current state of the diff config and return the
   119  // DiffConfig implementation if no errors are found. Will return nil and the error
   120  // details otherwise.
   121  func (b *DiffConfigBuilder) Build() (DiffConfig, error) {
   122  	err := b.diffConfig.Validate()
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  	return b.diffConfig, nil
   127  }
   128  
   129  // DiffConfig defines methods to retrieve the configurations used while applying diffs
   130  // and normalizing resources.
   131  type DiffConfig interface {
   132  	// Validate will check if the current configurations are set properly.
   133  	Validate() error
   134  	// DiffFromCache will verify if it should retrieve the cached ResourceDiff based on this
   135  	// DiffConfig.
   136  	DiffFromCache(appName string) (bool, []*v1alpha1.ResourceDiff)
   137  	// Ignores Application level ignore difference configurations.
   138  	Ignores() []v1alpha1.ResourceIgnoreDifferences
   139  	// Overrides is map of system configurations to override the Application ones.
   140  	// The key should follow the "group/kind" format.
   141  	Overrides() map[string]v1alpha1.ResourceOverride
   142  	AppLabelKey() string
   143  	TrackingMethod() string
   144  	// AppName the Application name. Used to retrieve the cached diff.
   145  	AppName() string
   146  	// NoCache defines if should retrieve the diff from cache.
   147  	NoCache() bool
   148  	// StateCache is used when retrieving the diff from the cache.
   149  	StateCache() *appstatecache.Cache
   150  	IgnoreAggregatedRoles() bool
   151  	// Logger used during the diff.
   152  	Logger() *logr.Logger
   153  	// GVKParser returns a parser able to build a TypedValue used in
   154  	// structured merge diffs.
   155  	GVKParser() *k8smanagedfields.GvkParser
   156  	// StructuredMergeDiff defines if the diff should be calculated using
   157  	// structured merge diffs. Will use standard 3-way merge diffs if
   158  	// returns false.
   159  	StructuredMergeDiff() bool
   160  	// Manager returns the manager that should be used by the diff while
   161  	// calculating the structured merge diff.
   162  	Manager() string
   163  
   164  	ServerSideDiff() bool
   165  	ServerSideDryRunner() diff.ServerSideDryRunner
   166  	IgnoreMutationWebhook() bool
   167  
   168  	IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts
   169  }
   170  
   171  // diffConfig defines the configurations used while applying diffs.
   172  type diffConfig struct {
   173  	ignores               []v1alpha1.ResourceIgnoreDifferences
   174  	overrides             map[string]v1alpha1.ResourceOverride
   175  	appLabelKey           string
   176  	trackingMethod        string
   177  	appName               string
   178  	noCache               bool
   179  	stateCache            *appstatecache.Cache
   180  	ignoreAggregatedRoles bool
   181  	logger                *logr.Logger
   182  	gvkParser             *k8smanagedfields.GvkParser
   183  	structuredMergeDiff   bool
   184  	manager               string
   185  	serverSideDiff        bool
   186  	serverSideDryRunner   diff.ServerSideDryRunner
   187  	ignoreMutationWebhook bool
   188  	ignoreNormalizerOpts  normalizers.IgnoreNormalizerOpts
   189  }
   190  
   191  func (c *diffConfig) Ignores() []v1alpha1.ResourceIgnoreDifferences {
   192  	return c.ignores
   193  }
   194  
   195  func (c *diffConfig) Overrides() map[string]v1alpha1.ResourceOverride {
   196  	return c.overrides
   197  }
   198  
   199  func (c *diffConfig) AppLabelKey() string {
   200  	return c.appLabelKey
   201  }
   202  
   203  func (c *diffConfig) TrackingMethod() string {
   204  	return c.trackingMethod
   205  }
   206  
   207  func (c *diffConfig) AppName() string {
   208  	return c.appName
   209  }
   210  
   211  func (c *diffConfig) NoCache() bool {
   212  	return c.noCache
   213  }
   214  
   215  func (c *diffConfig) StateCache() *appstatecache.Cache {
   216  	return c.stateCache
   217  }
   218  
   219  func (c *diffConfig) IgnoreAggregatedRoles() bool {
   220  	return c.ignoreAggregatedRoles
   221  }
   222  
   223  func (c *diffConfig) Logger() *logr.Logger {
   224  	return c.logger
   225  }
   226  
   227  func (c *diffConfig) GVKParser() *k8smanagedfields.GvkParser {
   228  	return c.gvkParser
   229  }
   230  
   231  func (c *diffConfig) StructuredMergeDiff() bool {
   232  	return c.structuredMergeDiff
   233  }
   234  
   235  func (c *diffConfig) Manager() string {
   236  	return c.manager
   237  }
   238  
   239  func (c *diffConfig) ServerSideDryRunner() diff.ServerSideDryRunner {
   240  	return c.serverSideDryRunner
   241  }
   242  
   243  func (c *diffConfig) ServerSideDiff() bool {
   244  	return c.serverSideDiff
   245  }
   246  
   247  func (c *diffConfig) IgnoreMutationWebhook() bool {
   248  	return c.ignoreMutationWebhook
   249  }
   250  
   251  func (c *diffConfig) IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts {
   252  	return c.ignoreNormalizerOpts
   253  }
   254  
   255  // Validate will check the current state of this diffConfig and return
   256  // error if it finds any required configuration missing.
   257  func (c *diffConfig) Validate() error {
   258  	msg := "diffConfig validation error"
   259  	if c.ignores == nil {
   260  		return fmt.Errorf("%s: ResourceIgnoreDifferences can not be nil", msg)
   261  	}
   262  	if c.overrides == nil {
   263  		return fmt.Errorf("%s: ResourceOverride can not be nil", msg)
   264  	}
   265  	if !c.noCache {
   266  		if c.appName == "" {
   267  			return fmt.Errorf("%s: AppName must be set when retrieving from cache", msg)
   268  		}
   269  		if c.stateCache == nil {
   270  			return fmt.Errorf("%s: StateCache must be set when retrieving from cache", msg)
   271  		}
   272  	}
   273  	if c.serverSideDiff && c.serverSideDryRunner == nil {
   274  		return fmt.Errorf("%s: serverSideDryRunner must be set when using server side diff", msg)
   275  	}
   276  	return nil
   277  }
   278  
   279  // NormalizationResult holds the normalized lives and target resources.
   280  type NormalizationResult struct {
   281  	Lives   []*unstructured.Unstructured
   282  	Targets []*unstructured.Unstructured
   283  }
   284  
   285  // StateDiff will apply all required normalizations and calculate the diffs between
   286  // the live and the config/desired states.
   287  func StateDiff(live, config *unstructured.Unstructured, diffConfig DiffConfig) (diff.DiffResult, error) {
   288  	results, err := StateDiffs([]*unstructured.Unstructured{live}, []*unstructured.Unstructured{config}, diffConfig)
   289  	if err != nil {
   290  		return diff.DiffResult{}, err
   291  	}
   292  	if len(results.Diffs) != 1 {
   293  		return diff.DiffResult{}, fmt.Errorf("StateDiff error: unexpected diff results: expected 1 got %d", len(results.Diffs))
   294  	}
   295  	return results.Diffs[0], nil
   296  }
   297  
   298  // StateDiffs will apply all required normalizations and calculate the diffs between
   299  // the live and the config/desired states.
   300  func StateDiffs(lives, configs []*unstructured.Unstructured, diffConfig DiffConfig) (*diff.DiffResultList, error) {
   301  	normResults, err := preDiffNormalize(lives, configs, diffConfig)
   302  	if err != nil {
   303  		return nil, fmt.Errorf("failed to perform pre-diff normalization: %w", err)
   304  	}
   305  
   306  	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides(), diffConfig.IgnoreNormalizerOpts())
   307  	if err != nil {
   308  		return nil, fmt.Errorf("failed to create diff normalizer: %w", err)
   309  	}
   310  
   311  	diffOpts := []diff.Option{
   312  		diff.WithNormalizer(diffNormalizer),
   313  		diff.IgnoreAggregatedRoles(diffConfig.IgnoreAggregatedRoles()),
   314  		diff.WithStructuredMergeDiff(diffConfig.StructuredMergeDiff()),
   315  		diff.WithGVKParser(diffConfig.GVKParser()),
   316  		diff.WithManager(diffConfig.Manager()),
   317  		diff.WithServerSideDiff(diffConfig.ServerSideDiff()),
   318  		diff.WithServerSideDryRunner(diffConfig.ServerSideDryRunner()),
   319  		diff.WithIgnoreMutationWebhook(diffConfig.IgnoreMutationWebhook()),
   320  	}
   321  
   322  	if diffConfig.Logger() != nil {
   323  		diffOpts = append(diffOpts, diff.WithLogr(*diffConfig.Logger()))
   324  	}
   325  
   326  	useCache, cachedDiff := diffConfig.DiffFromCache(diffConfig.AppName())
   327  	if useCache && cachedDiff != nil {
   328  		cached, err := diffArrayCached(normResults.Targets, normResults.Lives, cachedDiff, diffOpts...)
   329  		if err != nil {
   330  			return nil, fmt.Errorf("failed to calculate diff from cache: %w", err)
   331  		}
   332  		return cached, nil
   333  	}
   334  	array, err := diff.DiffArray(normResults.Targets, normResults.Lives, diffOpts...)
   335  	if err != nil {
   336  		return nil, fmt.Errorf("failed to calculate diff: %w", err)
   337  	}
   338  	return array, nil
   339  }
   340  
   341  func diffArrayCached(configArray []*unstructured.Unstructured, liveArray []*unstructured.Unstructured, cachedDiff []*v1alpha1.ResourceDiff, opts ...diff.Option) (*diff.DiffResultList, error) {
   342  	numItems := len(configArray)
   343  	if len(liveArray) != numItems {
   344  		return nil, errors.New("left and right arrays have mismatched lengths")
   345  	}
   346  
   347  	diffByKey := map[kube.ResourceKey]*v1alpha1.ResourceDiff{}
   348  	for _, res := range cachedDiff {
   349  		diffByKey[kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)] = res
   350  	}
   351  
   352  	diffResultList := diff.DiffResultList{
   353  		Diffs: make([]diff.DiffResult, numItems),
   354  	}
   355  
   356  	for i := 0; i < numItems; i++ {
   357  		config := configArray[i]
   358  		live := liveArray[i]
   359  		resourceVersion := ""
   360  		var key kube.ResourceKey
   361  		if live != nil {
   362  			key = kube.GetResourceKey(live)
   363  			resourceVersion = live.GetResourceVersion()
   364  		} else {
   365  			key = kube.GetResourceKey(config)
   366  		}
   367  		var dr *diff.DiffResult
   368  		if cachedDiff, ok := diffByKey[key]; ok && cachedDiff.ResourceVersion == resourceVersion {
   369  			dr = &diff.DiffResult{
   370  				NormalizedLive: []byte(cachedDiff.NormalizedLiveState),
   371  				PredictedLive:  []byte(cachedDiff.PredictedLiveState),
   372  				Modified:       cachedDiff.Modified,
   373  			}
   374  		} else {
   375  			res, err := diff.Diff(configArray[i], liveArray[i], opts...)
   376  			if err != nil {
   377  				return nil, err
   378  			}
   379  			dr = res
   380  		}
   381  		if dr != nil {
   382  			diffResultList.Diffs[i] = *dr
   383  			if dr.Modified {
   384  				diffResultList.Modified = true
   385  			}
   386  		}
   387  	}
   388  
   389  	return &diffResultList, nil
   390  }
   391  
   392  // DiffFromCache will verify if it should retrieve the cached ResourceDiff based on this
   393  // DiffConfig. Returns true and the cached ResourceDiff if configured to use the cache.
   394  // Returns false and nil otherwise.
   395  func (c *diffConfig) DiffFromCache(appName string) (bool, []*v1alpha1.ResourceDiff) {
   396  	if c.noCache || c.stateCache == nil || appName == "" {
   397  		return false, nil
   398  	}
   399  	cachedDiff := make([]*v1alpha1.ResourceDiff, 0)
   400  	if c.stateCache != nil {
   401  		err := c.stateCache.GetAppManagedResources(appName, &cachedDiff)
   402  		if err != nil {
   403  			log.Errorf("DiffFromCache error: error getting managed resources for app %s: %s", appName, err)
   404  			return false, nil
   405  		}
   406  		return true, cachedDiff
   407  	}
   408  	return false, nil
   409  }
   410  
   411  // preDiffNormalize applies the normalization of live and target resources before invoking
   412  // the diff. None of the attributes in the lives and targets params will be modified.
   413  func preDiffNormalize(lives, targets []*unstructured.Unstructured, diffConfig DiffConfig) (*NormalizationResult, error) {
   414  	if diffConfig == nil {
   415  		return nil, errors.New("preDiffNormalize error: diffConfig can not be nil")
   416  	}
   417  	err := diffConfig.Validate()
   418  	if err != nil {
   419  		return nil, fmt.Errorf("preDiffNormalize error: %w", err)
   420  	}
   421  
   422  	results := &NormalizationResult{}
   423  	for i := range targets {
   424  		target := safeDeepCopy(targets[i])
   425  		live := safeDeepCopy(lives[i])
   426  		resourceTracking := argo.NewResourceTracking()
   427  		_ = resourceTracking.Normalize(target, live, diffConfig.AppLabelKey(), diffConfig.TrackingMethod())
   428  		// just normalize on managed fields if live and target aren't nil as we just care
   429  		// about conflicting fields
   430  		if live != nil && target != nil {
   431  			gvk := target.GetObjectKind().GroupVersionKind()
   432  			idc := NewIgnoreDiffConfig(diffConfig.Ignores(), diffConfig.Overrides())
   433  			ok, ignoreDiff := idc.HasIgnoreDifference(gvk.Group, gvk.Kind, target.GetName(), target.GetNamespace())
   434  			if ok && len(ignoreDiff.ManagedFieldsManagers) > 0 {
   435  				pt := scheme.ResolveParseableType(gvk, diffConfig.GVKParser())
   436  				var err error
   437  				live, target, err = managedfields.Normalize(live, target, ignoreDiff.ManagedFieldsManagers, pt)
   438  				if err != nil {
   439  					return nil, err
   440  				}
   441  			}
   442  		}
   443  		results.Lives = append(results.Lives, live)
   444  		results.Targets = append(results.Targets, target)
   445  	}
   446  	return results, nil
   447  }
   448  
   449  // safeDeepCopy will return nil if given obj is nil.
   450  func safeDeepCopy(obj *unstructured.Unstructured) *unstructured.Unstructured {
   451  	if obj == nil {
   452  		return nil
   453  	}
   454  	return obj.DeepCopy()
   455  }