diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 9f3e32ba0b6e6..2077a023a9ba4 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -1441,9 +1441,9 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo } now := metav1.Now() - compareResult := ctrl.appStateManager.CompareAppState(app, project, revisions, sources, + compareResult := ctrl.appStateManager.CompareAppStateWithComparisonLevel(app, project, revisions, sources, refreshType == appv1.RefreshTypeHard, - comparisonLevel == CompareWithLatestForceResolve, localManifests, hasMultipleSources) + comparisonLevel == CompareWithLatestForceResolve, localManifests, hasMultipleSources, comparisonLevel == CompareWithRecent) for k, v := range compareResult.timings { logCtx = logCtx.WithField(k, v.Milliseconds()) diff --git a/controller/state.go b/controller/state.go index 8b9842d91b249..58648bddf62f0 100644 --- a/controller/state.go +++ b/controller/state.go @@ -62,6 +62,7 @@ type managedResource struct { // AppStateManager defines methods which allow to compare application spec and actual application state. type AppStateManager interface { CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool) *comparisonResult + CompareAppStateWithComparisonLevel(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool, isCompareWithRecent bool) *comparisonResult SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState) } @@ -282,20 +283,24 @@ func DeduplicateTargetObjects( // getComparisonSettings will return the system level settings related to the // diff/normalization process. -func (m *appStateManager) getComparisonSettings() (string, map[string]v1alpha1.ResourceOverride, *settings.ResourcesFilter, error) { +func (m *appStateManager) getComparisonSettings() (string, map[string]v1alpha1.ResourceOverride, *settings.ResourcesFilter, bool, error) { resourceOverrides, err := m.settingsMgr.GetResourceOverrides() if err != nil { - return "", nil, nil, err + return "", nil, nil, false, err } appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey() if err != nil { - return "", nil, nil, err + return "", nil, nil, false, err } resFilter, err := m.settingsMgr.GetResourcesFilter() if err != nil { - return "", nil, nil, err + return "", nil, nil, false, err } - return appLabelKey, resourceOverrides, resFilter, nil + ignoreResourceUpdatedEnabled, err := m.settingsMgr.GetIsIgnoreResourceUpdatesEnabled() + if err != nil { + return "", nil, nil, false, err + } + return appLabelKey, resourceOverrides, resFilter, ignoreResourceUpdatedEnabled, nil } // verifyGnuPGSignature verifies the result of a GnuPG operation for a given git @@ -341,8 +346,12 @@ func verifyGnuPGSignature(revision string, project *v1alpha1.AppProject, manifes // revision and supplied source. If revision or overrides are empty, then compares against // revision and overrides in the app spec. func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool) *comparisonResult { + return m.CompareAppStateWithComparisonLevel(app, project, revisions, sources, noCache, noRevisionCache, localManifests, hasMultipleSources, false) +} + +func (m *appStateManager) CompareAppStateWithComparisonLevel(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localManifests []string, hasMultipleSources bool, isCompareWithRecent bool) *comparisonResult { ts := stats.NewTimingStats() - appLabelKey, resourceOverrides, resFilter, err := m.getComparisonSettings() + appLabelKey, resourceOverrides, resFilter, ignoreResourceUpdatedEnabled, err := m.getComparisonSettings() ts.AddCheckpoint("settings_ms") @@ -520,6 +529,22 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 _, refreshRequested := app.IsRefreshRequested() noCache = noCache || refreshRequested || app.Status.Expired(m.statusRefreshTimeout) || specChanged || revisionChanged + if isCompareWithRecent && ignoreResourceUpdatedEnabled { + // Although we modify resourceOverrides here, it is not persisted + // since m.getComparisonSettings() is reading from the configMap every time its called + for k, v := range resourceOverrides { + resourceUpdates := v.IgnoreResourceUpdates + resourceUpdates.JQPathExpressions = append(resourceUpdates.JQPathExpressions, v.IgnoreDifferences.JQPathExpressions...) + resourceUpdates.JSONPointers = append(resourceUpdates.JSONPointers, v.IgnoreDifferences.JSONPointers...) + resourceUpdates.ManagedFieldsManagers = append(resourceUpdates.ManagedFieldsManagers, v.IgnoreDifferences.ManagedFieldsManagers...) + // Set the IgnoreDifferences because these are the overrides used by Normalizers + v.IgnoreDifferences = resourceUpdates + v.IgnoreResourceUpdates = v1alpha1.OverrideIgnoreDiff{} + resourceOverrides[k] = v + } + log.Info("Using ignore resource updates as ignoreDifferences on comparing with recent") + } + diffConfigBuilder := argodiff.NewDiffConfigBuilder(). WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles). WithTracking(appLabelKey, string(trackingMethod)) diff --git a/controller/state_test.go b/controller/state_test.go index ab004af591807..055c0ea7bb74f 100644 --- a/controller/state_test.go +++ b/controller/state_test.go @@ -1,7 +1,9 @@ package controller import ( + "bytes" "encoding/json" + log "github.com/sirupsen/logrus" "os" "testing" "time" @@ -281,8 +283,139 @@ func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) { assert.NotNil(t, compRes) assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status) assert.Len(t, compRes.resources, 0) - assert.Len(t, compRes.managedResources, 0) - assert.Len(t, app.Status.Conditions, 0) +} + +func TestCompareAppStateIgnoreResourceUpdatesWhenComparingWithRecent(t *testing.T) { + log.SetLevel(log.DebugLevel) + + t.Run("Modified app should be OutOfSync", func(t *testing.T) { + // given + //t.Parallel() + obj1 := NewPod() + obj1.SetNamespace(test.FakeDestNamespace) + app := newFakeApp() + data := fakeData{ + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{toJSON(t, obj1)}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(obj1): obj1, + }, + } + + // Modify the live object to have a different cpu request + obj1.Object["spec"].(map[string]interface{})["containers"].([]interface{})[0].(map[string]interface{})["resources"].(map[string]interface{})["requests"].(map[string]interface{})["cpu"] = 0.5 + + sources := make([]argoappv1.ApplicationSource, 0) + sources = append(sources, app.Spec.GetSource()) + revisions := make([]string, 0) + revisions = append(revisions, "") + ctrl := newFakeController(&data) + compRes := ctrl.appStateManager.CompareAppStateWithComparisonLevel(app, &defaultProj, revisions, sources, false, false, nil, false, true) + + // then + assert.NotNil(t, compRes) + assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status) + assert.Len(t, compRes.resources, 1) + assert.Len(t, compRes.managedResources, 1) + assert.Len(t, app.Status.Conditions, 0) + }) + + t.Run("Modified app ignored by ignoreDifferences should be Synced", func(t *testing.T) { + var logBuffer bytes.Buffer + log.SetOutput(&logBuffer) + defer func() { + log.SetOutput(log.StandardLogger().Out) // Restore the original logger output + }() + + // given + obj1 := NewPod() + obj1.SetNamespace(test.FakeDestNamespace) + app := newFakeApp() + data := fakeData{ + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{toJSON(t, obj1)}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(obj1): obj1, + }, + } + + // Modify the live object to have a different cpu request + obj1.Object["spec"].(map[string]interface{})["containers"].([]interface{})[0].(map[string]interface{})["resources"].(map[string]interface{})["requests"].(map[string]interface{})["cpu"] = 0.5 + + sources := make([]argoappv1.ApplicationSource, 0) + sources = append(sources, app.Spec.GetSource()) + revisions := make([]string, 0) + revisions = append(revisions, "") + data.configMapData = map[string]string{ + "resource.ignoreResourceUpdatesEnabled": "true", // test our modification doesn't affect ignoreDifferences + "resource.customizations.ignoreDifferences.all": `jsonPointers: +- /spec/containers/0/resources/requests/cpu`, + } + ctrl := newFakeController(&data) + compRes := ctrl.appStateManager.CompareAppStateWithComparisonLevel(app, &defaultProj, revisions, sources, false, false, nil, false, true) + + // then + assert.NotNil(t, compRes) + assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status) + assert.Len(t, compRes.resources, 1) + assert.Len(t, compRes.managedResources, 1) + assert.Len(t, app.Status.Conditions, 0) + assert.Contains(t, logBuffer.String(), "Using ignore resource updates as ignoreDifferences on comparing with recent") + }) + + t.Run("Modified app ignored by ignoreResourceUpdates should be Synced", func(t *testing.T) { + var logBuffer bytes.Buffer + log.SetOutput(&logBuffer) + defer func() { + log.SetOutput(log.StandardLogger().Out) // Restore the original logger output + }() + // given + obj1 := NewPod() + obj1.SetNamespace(test.FakeDestNamespace) + app := newFakeApp() + data := fakeData{ + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{toJSON(t, obj1)}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(obj1): obj1, + }, + } + + // Modify the live object to have a different cpu request + obj1.Object["spec"].(map[string]interface{})["containers"].([]interface{})[0].(map[string]interface{})["resources"].(map[string]interface{})["requests"].(map[string]interface{})["cpu"] = 0.5 + + sources := make([]argoappv1.ApplicationSource, 0) + sources = append(sources, app.Spec.GetSource()) + revisions := make([]string, 0) + revisions = append(revisions, "") + data.configMapData = map[string]string{ + "resource.ignoreResourceUpdatesEnabled": "true", + "resource.customizations.ignoreResourceUpdates.all": `jsonPointers: +- /spec/containers/0/resources/requests/cpu`, + } + ctrl := newFakeController(&data) + compRes := ctrl.appStateManager.CompareAppStateWithComparisonLevel(app, &defaultProj, revisions, sources, false, false, nil, false, true) + + // then + assert.NotNil(t, compRes) + assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status) + assert.Len(t, compRes.resources, 1) + assert.Len(t, compRes.managedResources, 1) + assert.Len(t, app.Status.Conditions, 0) + assert.Contains(t, logBuffer.String(), "Using ignore resource updates as ignoreDifferences on comparing with recent") + }) } // TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git diff --git a/docs/operator-manual/reconcile.md b/docs/operator-manual/reconcile.md index a956cd9cf7b28..99f529be56972 100644 --- a/docs/operator-manual/reconcile.md +++ b/docs/operator-manual/reconcile.md @@ -37,6 +37,17 @@ data: - /status ``` +### Using ignoreResourceUpdates with managedFieldsManagers +It is possible to use `ignoreResourceUpdates` to ignore resource updates from fields owned by specific managers defined in `metadata.managedFields` in live resources. +The following example will ignore resource updates on all fields owned by `kube-controller-manager` for all resources: + +```yaml +data: + resource.customizations.ignoreResourceUpdates.all: | + managedFieldsManagers: + - kube-controller-manager +``` + ### Using ignoreDifferences to ignore reconcile It is possible to use existing system-level `ignoreDifferences` customizations to ignore resource updates as well. Instead of copying all configurations,