feat: add ignoreResourceUpdates to reduce controller CPU usage (#13534) (#13912)

* feat: ignore watched resource update

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* add doc and CLI

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* update doc index

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* add command

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* codegen

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* revert formatting

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* do not skip on health change

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* update doc

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* update logging to use context

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* fix typos. local build broken...

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* change after review

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* manifestHash to string

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* more doc

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* example for argoproj Application

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* add unit test for ignored logs

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* codegen

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* Update docs/operator-manual/reconcile.md

Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* move hash and set log to debug

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* Update util/settings/settings.go

Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* Update util/settings/settings.go

Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* feature flag

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* fix

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* less aggressive managedFields ignore rule

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* Update docs/operator-manual/reconcile.md

Co-authored-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>

* use local settings

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* latest settings

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* safety first

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

* since it's behind a feature flag, go aggressive on overrides

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>

---------

Signed-off-by: Alexandre Gaudreault <alexandre.gaudreault@logmein.com>
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
This commit is contained in:
Alexandre Gaudreault 2023-06-24 21:32:20 -04:00 committed by GitHub
parent 771012bb65
commit 88994ea5cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 1508 additions and 676 deletions

View file

@ -101,6 +101,7 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Glovo](https://www.glovoapp.com)
1. [GMETRI](https://gmetri.com/)
1. [Gojek](https://www.gojek.io/)
1. [GoTo](https://www.goto.com/)
1. [GoTo Financial](https://gotofinancial.com/)
1. [Greenpass](https://www.greenpass.com.br/)
1. [Gridfuse](https://gridfuse.com/)

View file

@ -8007,6 +8007,9 @@
"ignoreDifferences": {
"$ref": "#/definitions/v1alpha1OverrideIgnoreDiff"
},
"ignoreResourceUpdates": {
"$ref": "#/definitions/v1alpha1OverrideIgnoreDiff"
},
"knownTypeFields": {
"type": "array",
"items": {

View file

@ -349,6 +349,7 @@ func NewResourceOverridesCommand(cmdCtx commandContext) *cobra.Command {
},
}
command.AddCommand(NewResourceIgnoreDifferencesCommand(cmdCtx))
command.AddCommand(NewResourceIgnoreResourceUpdatesCommand(cmdCtx))
command.AddCommand(NewResourceActionListCommand(cmdCtx))
command.AddCommand(NewResourceActionRunCommand(cmdCtx))
command.AddCommand(NewResourceHealthCommand(cmdCtx))
@ -380,6 +381,31 @@ func executeResourceOverrideCommand(ctx context.Context, cmdCtx commandContext,
callback(res, override, overrides)
}
func executeIgnoreResourceUpdatesOverrideCommand(ctx context.Context, cmdCtx commandContext, args []string, callback func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride)) {
data, err := os.ReadFile(args[0])
errors.CheckError(err)
res := unstructured.Unstructured{}
errors.CheckError(yaml.Unmarshal(data, &res))
settingsManager, err := cmdCtx.createSettingsManager(ctx)
errors.CheckError(err)
overrides, err := settingsManager.GetIgnoreResourceUpdatesOverrides()
errors.CheckError(err)
gvk := res.GroupVersionKind()
key := gvk.Kind
if gvk.Group != "" {
key = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
}
override, hasOverride := overrides[key]
if !hasOverride {
_, _ = fmt.Printf("No overrides configured for '%s/%s'\n", gvk.Group, gvk.Kind)
return
}
callback(res, override, overrides)
}
func NewResourceIgnoreDifferencesCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "ignore-differences RESOURCE_YAML_PATH",
@ -430,6 +456,52 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
return command
}
func NewResourceIgnoreResourceUpdatesCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "ignore-resource-updates RESOURCE_YAML_PATH",
Short: "Renders fields excluded from resource updates",
Long: "Renders ignored fields using the 'ignoreResourceUpdates' setting specified in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
Example: `
argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if len(args) < 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
executeIgnoreResourceUpdatesOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
gvk := res.GroupVersionKind()
if len(override.IgnoreResourceUpdates.JSONPointers) == 0 && len(override.IgnoreResourceUpdates.JQPathExpressions) == 0 {
_, _ = fmt.Printf("Ignore resource updates are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
return
}
normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
errors.CheckError(err)
normalizedRes := res.DeepCopy()
logs := collectLogs(func() {
errors.CheckError(normalizer.Normalize(normalizedRes))
})
if logs != "" {
_, _ = fmt.Println(logs)
}
if reflect.DeepEqual(&res, normalizedRes) {
_, _ = fmt.Printf("No fields are ignored by ignoreResourceUpdates settings: \n%s\n", override.IgnoreResourceUpdates)
return
}
_, _ = fmt.Printf("Following fields are ignored:\n\n")
_ = cli.PrintDiff(res.GetName(), &res, normalizedRes)
})
},
}
return command
}
func NewResourceHealthCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "health RESOURCE_YAML_PATH",

View file

@ -359,17 +359,18 @@ func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]b
level = CompareWithRecent
}
// Additional check for debug level so we don't need to evaluate the
// format string in case of non-debug scenarios
if log.GetLevel() >= log.DebugLevel {
var resKey string
if ref.Namespace != "" {
resKey = ref.Namespace + "/" + ref.Name
} else {
resKey = "(cluster-scoped)/" + ref.Name
}
log.Debugf("Refreshing app %s for change in cluster of object %s of type %s/%s", appKey, resKey, ref.APIVersion, ref.Kind)
namespace := ref.Namespace
if ref.Namespace == "" {
namespace = "(cluster-scoped)"
}
log.WithFields(log.Fields{
"application": appKey,
"level": level,
"namespace": namespace,
"name": ref.Name,
"api-version": ref.APIVersion,
"kind": ref.Kind,
}).Debug("Requesting app refresh caused by object update")
ctrl.requestAppRefresh(app.QualifiedName(), &level, nil)
}

View file

@ -29,6 +29,7 @@ import (
"k8s.io/client-go/tools/cache"
"github.com/argoproj/argo-cd/v2/controller/metrics"
"github.com/argoproj/argo-cd/v2/pkg/apis/application"
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/argo"
"github.com/argoproj/argo-cd/v2/util/db"
@ -149,6 +150,8 @@ type ResourceInfo struct {
PodInfo *PodInfo
// NodeInfo is available for nodes only
NodeInfo *NodeInfo
manifestHash string
}
func NewLiveStateCache(
@ -178,6 +181,11 @@ type cacheSettings struct {
clusterSettings clustercache.Settings
appInstanceLabelKey string
trackingMethod appv1.TrackingMethod
// resourceOverrides provides a list of ignored differences to ignore watched resource updates
resourceOverrides map[string]appv1.ResourceOverride
// ignoreResourceUpdates is a flag to enable resource-ignore rules.
ignoreResourceUpdatesEnabled bool
}
type liveStateCache struct {
@ -200,6 +208,14 @@ func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
if err != nil {
return nil, err
}
resourceUpdatesOverrides, err := c.settingsMgr.GetIgnoreResourceUpdatesOverrides()
if err != nil {
return nil, err
}
ignoreResourceUpdatesEnabled, err := c.settingsMgr.GetIsIgnoreResourceUpdatesEnabled()
if err != nil {
return nil, err
}
resourcesFilter, err := c.settingsMgr.GetResourcesFilter()
if err != nil {
return nil, err
@ -212,7 +228,8 @@ func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
ResourceHealthOverride: lua.ResourceHealthOverrides(resourceOverrides),
ResourcesFilter: resourcesFilter,
}
return &cacheSettings{clusterSettings, appInstanceLabelKey, argo.GetTrackingMethod(c.settingsMgr)}, nil
return &cacheSettings{clusterSettings, appInstanceLabelKey, argo.GetTrackingMethod(c.settingsMgr), resourceUpdatesOverrides, ignoreResourceUpdatesEnabled}, nil
}
func asResourceNode(r *clustercache.Resource) appv1.ResourceNode {
@ -309,6 +326,27 @@ func skipAppRequeuing(key kube.ResourceKey) bool {
return ignoredRefreshResources[key.Group+"/"+key.Kind]
}
func skipResourceUpdate(oldInfo, newInfo *ResourceInfo) bool {
if oldInfo == nil || newInfo == nil {
return false
}
isSameHealthStatus := (oldInfo.Health == nil && newInfo.Health == nil) || oldInfo.Health != nil && newInfo.Health != nil && oldInfo.Health.Status == newInfo.Health.Status
isSameManifest := oldInfo.manifestHash != "" && newInfo.manifestHash != "" && oldInfo.manifestHash == newInfo.manifestHash
return isSameHealthStatus && isSameManifest
}
// shouldHashManifest validates if the API resource needs to be hashed.
// If there's an app name from resource tracking, or if this is itself an app, we should generate a hash.
// Otherwise, the hashing should be skipped to save CPU time.
func shouldHashManifest(appName string, gvk schema.GroupVersionKind) bool {
// Only hash if the resource belongs to an app.
// Best - Only hash for resources that are part of an app or their dependencies
// (current) - Only hash for resources that are part of an app + all apps that might be from an ApplicationSet
// Orphan - If orphan is enabled, hash should be made on all resource of that namespace and a config to disable it
// Worst - Hash all resources watched by Argo
return appName != "" || (gvk.Group == application.Group && gvk.Kind == application.ApplicationKind)
}
// isRetryableError is a helper method to see whether an error
// returned from the dynamic client is potentially retryable.
func isRetryableError(err error) bool {
@ -424,14 +462,25 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
c.lock.RLock()
cacheSettings := c.cacheSettings
c.lock.RUnlock()
res.Health, _ = health.GetResourceHealth(un, cacheSettings.clusterSettings.ResourceHealthOverride)
appName := c.resourceTracking.GetAppName(un, cacheSettings.appInstanceLabelKey, cacheSettings.trackingMethod)
if isRoot && appName != "" {
res.AppName = appName
}
gvk := un.GroupVersionKind()
if cacheSettings.ignoreResourceUpdatesEnabled && shouldHashManifest(appName, gvk) {
hash, err := generateManifestHash(un, nil, cacheSettings.resourceOverrides)
if err != nil {
log.Errorf("Failed to generate manifest hash: %v", err)
} else {
res.manifestHash = hash
}
}
// edge case. we do not label CRDs, so they miss the tracking label we inject. But we still
// want the full resource to be available in our cache (to diff), so we store all CRDs
return res, res.AppName != "" || gvk.Kind == kube.CustomResourceDefinitionKind
@ -450,6 +499,30 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
} else {
ref = oldRes.Ref
}
c.lock.RLock()
cacheSettings := c.cacheSettings
c.lock.RUnlock()
if cacheSettings.ignoreResourceUpdatesEnabled && oldRes != nil && newRes != nil && skipResourceUpdate(resInfo(oldRes), resInfo(newRes)) {
// Additional check for debug level so we don't need to evaluate the
// format string in case of non-debug scenarios
if log.GetLevel() >= log.DebugLevel {
namespace := ref.Namespace
if ref.Namespace == "" {
namespace = "(cluster-scoped)"
}
log.WithFields(log.Fields{
"server": clusterCache.GetClusterInfo().Server,
"namespace": namespace,
"name": ref.Name,
"api-version": ref.APIVersion,
"kind": ref.Kind,
}).Debug("Ignoring change of object because none of the watched resource fields have changed")
}
return
}
for _, r := range []*clustercache.Resource{newRes, oldRes} {
if r == nil {
continue

View file

@ -14,6 +14,7 @@ import (
"github.com/argoproj/gitops-engine/pkg/cache"
"github.com/argoproj/gitops-engine/pkg/cache/mocks"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/stretchr/testify/mock"
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
@ -202,3 +203,126 @@ func Test_asResourceNode_owner_refs(t *testing.T) {
}
assert.Equal(t, expected, resNode)
}
func TestSkipResourceUpdate(t *testing.T) {
var (
hash1_x string = "x"
hash2_y string = "y"
hash3_x string = "x"
)
info := &ResourceInfo{
manifestHash: hash1_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "default",
},
}
t.Run("Nil", func(t *testing.T) {
assert.False(t, skipResourceUpdate(nil, nil))
})
t.Run("From Nil", func(t *testing.T) {
assert.False(t, skipResourceUpdate(nil, info))
})
t.Run("To Nil", func(t *testing.T) {
assert.False(t, skipResourceUpdate(info, nil))
})
t.Run("No hash", func(t *testing.T) {
assert.False(t, skipResourceUpdate(&ResourceInfo{}, &ResourceInfo{}))
})
t.Run("Same hash", func(t *testing.T) {
assert.True(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
}, &ResourceInfo{
manifestHash: hash1_x,
}))
})
t.Run("Same hash value", func(t *testing.T) {
assert.True(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
}, &ResourceInfo{
manifestHash: hash3_x,
}))
})
t.Run("Different hash value", func(t *testing.T) {
assert.False(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
}, &ResourceInfo{
manifestHash: hash2_y,
}))
})
t.Run("Same hash, empty health", func(t *testing.T) {
assert.True(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
Health: &health.HealthStatus{},
}, &ResourceInfo{
manifestHash: hash3_x,
Health: &health.HealthStatus{},
}))
})
t.Run("Same hash, old health", func(t *testing.T) {
assert.False(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy},
}, &ResourceInfo{
manifestHash: hash3_x,
Health: nil,
}))
})
t.Run("Same hash, new health", func(t *testing.T) {
assert.False(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
Health: &health.HealthStatus{},
}, &ResourceInfo{
manifestHash: hash3_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy,
},
}))
})
t.Run("Same hash, same health", func(t *testing.T) {
assert.True(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "same",
},
}, &ResourceInfo{
manifestHash: hash3_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "same",
},
}))
})
t.Run("Same hash, different health status", func(t *testing.T) {
assert.False(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "same",
},
}, &ResourceInfo{
manifestHash: hash3_x,
Health: &health.HealthStatus{
Status: health.HealthStatusDegraded,
Message: "same",
},
}))
})
t.Run("Same hash, different health message", func(t *testing.T) {
assert.True(t, skipResourceUpdate(&ResourceInfo{
manifestHash: hash1_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "same",
},
}, &ResourceInfo{
manifestHash: hash3_x,
Health: &health.HealthStatus{
Status: health.HealthStatusHealthy,
Message: "different",
},
}))
})
}

View file

@ -3,12 +3,14 @@ package cache
import (
"errors"
"fmt"
"strconv"
"strings"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/text"
"github.com/cespare/xxhash/v2"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -16,6 +18,7 @@ import (
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
"github.com/argoproj/argo-cd/v2/util/resource"
)
@ -386,3 +389,27 @@ func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
SystemInfo: node.Status.NodeInfo,
}
}
func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (string, error) {
normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides)
if err != nil {
return "", fmt.Errorf("error creating normalizer: %w", err)
}
resource := un.DeepCopy()
err = normalizer.Normalize(resource)
if err != nil {
return "", fmt.Errorf("error normalizing resource: %w", err)
}
data, err := resource.MarshalJSON()
if err != nil {
return "", fmt.Errorf("error marshaling resource: %w", err)
}
hash := hash(data)
return hash, nil
}
func hash(data []byte) string {
return strconv.FormatUint(xxhash.Sum64(data), 16)
}

View file

@ -694,3 +694,62 @@ func TestCustomLabel(t *testing.T) {
assert.Equal(t, "other-label", info.Info[1].Name)
assert.Equal(t, "value2", info.Info[1].Value)
}
func TestManifestHash(t *testing.T) {
manifest := strToUnstructured(`
apiVersion: v1
kind: Pod
metadata:
name: helm-guestbook-pod
namespace: default
ownerReferences:
- apiVersion: extensions/v1beta1
kind: ReplicaSet
name: helm-guestbook-rs
resourceVersion: "123"
labels:
app: guestbook
spec:
nodeName: minikube
containers:
- image: bar
resources:
requests:
memory: 128Mi
`)
ignores := []v1alpha1.ResourceIgnoreDifferences{
{
Group: "*",
Kind: "*",
JSONPointers: []string{"/metadata/resourceVersion"},
},
}
data, _ := strToUnstructured(`
apiVersion: v1
kind: Pod
metadata:
name: helm-guestbook-pod
namespace: default
ownerReferences:
- apiVersion: extensions/v1beta1
kind: ReplicaSet
name: helm-guestbook-rs
labels:
app: guestbook
spec:
nodeName: minikube
containers:
- image: bar
resources:
requests:
memory: 128Mi
`).MarshalJSON()
expected := hash(data)
hash, err := generateManifestHash(manifest, ignores, nil)
assert.Equal(t, expected, hash)
assert.Nil(t, err)
}

View file

@ -85,6 +85,7 @@ data:
# Configuration to customize resource behavior (optional) can be configured via splitted sub keys.
# Keys are in the form: resource.customizations.ignoreDifferences.<group_kind>, resource.customizations.health.<group_kind>
# resource.customizations.actions.<group_kind>, resource.customizations.knownTypeFields.<group-kind>
# resource.customizations.ignoreResourceUpdates.<group-kind>
resource.customizations.ignoreDifferences.admissionregistration.k8s.io_MutatingWebhookConfiguration: |
jsonPointers:
- /webhooks/0/clientConfig/caBundle
@ -101,6 +102,33 @@ data:
jsonPointers:
- /spec/replicas
# Enable resource.customizations.ignoreResourceUpdates rules. If "false," those rules are not applied, and all updates
# to resources are applied to the cluster cache. Default is false.
resource.ignoreResourceUpdatesEnabled: "false"
# Configuration to define customizations ignoring differences during watched resource updates to skip application reconciles.
resource.customizations.ignoreResourceUpdates.all: |
jsonPointers:
- /metadata/resourceVersion
# Configuration to define customizations ignoring differences during watched resource updates can be configured via splitted sub key.
resource.customizations.ignoreResourceUpdates.argoproj.io_Application: |
jsonPointers:
- /status
# jsonPointers and jqPathExpressions can be specified.
resource.customizations.ignoreResourceUpdates.autoscaling_HorizontalPodAutoscaler: |
jqPathExpressions:
- '.metadata.annotations."autoscaling.alpha.kubernetes.io/behavior"'
- '.metadata.annotations."autoscaling.alpha.kubernetes.io/conditions"'
- '.metadata.annotations."autoscaling.alpha.kubernetes.io/metrics"'
- '.metadata.annotations."autoscaling.alpha.kubernetes.io/current-metrics"'
jsonPointers:
- /metadata/annotations/autoscaling.alpha.kubernetes.io~1behavior
- /metadata/annotations/autoscaling.alpha.kubernetes.io~1conditions
- /metadata/annotations/autoscaling.alpha.kubernetes.io~1metrics
- /metadata/annotations/autoscaling.alpha.kubernetes.io~1current-metrics
resource.customizations.health.certmanager.k8s.io-Certificate: |
hs = {}
if obj.status ~= nil then

View file

@ -0,0 +1,64 @@
# Reconcile Optimization
By default, an Argo CD Application is refreshed everytime a resource that belongs to it changes.
Kubernetes controllers often update the resources they watch periodically, causing continuous reconcile operation on the Application
and a high CPU usage on the `argocd-application-controller`. Argo CD allows you to optionally ignore resource updates on specific fields
for [tracked resources](../user-guide/resource_tracking.md).
When a resource update is ignored, if the resource's [health status](./health.md) does not change, the Application that this resource belongs to will not be reconciled.
## System-Level Configuration
Argo CD allows ignoring resource updates at a specific JSON path, using [RFC6902 JSON patches](https://tools.ietf.org/html/rfc6902) and [JQ path expressions](https://stedolan.github.io/jq/manual/#path(path_expression)). It can be configured for a specified group and kind
in `resource.customizations` key of the `argocd-cm` ConfigMap.
The feature is behind a flag. To enable it, set `resource.ignoreResourceUpdatesEnabled` to `"true"` in the `argocd-cm` ConfigMap.
Following is an example of a customization which ignores the `refreshTime` status field of an [`ExternalSecret`](https://external-secrets.io/main/api/externalsecret/) resource:
```yaml
data:
resource.customizations.ignoreResourceUpdates.external-secrets.io_ExternalSecret: |
jsonPointers:
- /status/refreshTime
```
It is possible to configure `ignoreResourceUpdates` to be applied to all tracked resources in every Application managed by an Argo CD instance. In order to do so, resource customizations can be configured like in the example below:
```yaml
data:
resource.customizations.ignoreResourceUpdates.all: |
jsonPointers:
- /status
```
### 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,
the `ignoreDifferencesOnResourceUpdates` setting can be used to add all ignored differences as ignored resource updates:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
data:
resource.compareoptions: |
ignoreDifferencesOnResourceUpdates: true
```
## Default Configuration
By default, the metadata fields `generation`, `resourceVersion` and `managedFields` are always ignored for all resources.
## Finding Resources to Ignore
The application controller logs when a resource change triggers a refresh. You can use these logs to find
high-churn resource kinds and then inspect those resources to find which fields to ignore.
To find these logs, search for `"Requesting app refresh caused by object update"`. The logs include structured
fields for `api-version` and `kind`. Counting the number of refreshes triggered, by api-version/kind should
reveal the high-churn resource kinds.
Note that these logs are at the `debug` level. Configure the application-controller's log level to `debug`.

View file

@ -61,6 +61,7 @@ argocd admin settings resource-overrides [flags]
* [argocd admin settings](argocd_admin_settings.md) - Provides set of commands for settings validation and troubleshooting
* [argocd admin settings resource-overrides health](argocd_admin_settings_resource-overrides_health.md) - Assess resource health
* [argocd admin settings resource-overrides ignore-differences](argocd_admin_settings_resource-overrides_ignore-differences.md) - Renders fields excluded from diffing
* [argocd admin settings resource-overrides ignore-resource-updates](argocd_admin_settings_resource-overrides_ignore-resource-updates.md) - Renders fields excluded from resource updates
* [argocd admin settings resource-overrides list-actions](argocd_admin_settings_resource-overrides_list-actions.md) - List available resource actions
* [argocd admin settings resource-overrides run-action](argocd_admin_settings_resource-overrides_run-action.md) - Executes resource action

View file

@ -0,0 +1,73 @@
## argocd admin settings resource-overrides ignore-resource-updates
Renders fields excluded from resource updates
### Synopsis
Renders ignored fields using the 'ignoreResourceUpdates' setting specified in the 'resource.customizations' field of 'argocd-cm' ConfigMap
```
argocd admin settings resource-overrides ignore-resource-updates RESOURCE_YAML_PATH [flags]
```
### Examples
```
argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml
```
### Options
```
-h, --help help for ignore-resource-updates
```
### Options inherited from parent commands
```
--argocd-cm-path string Path to local argocd-cm.yaml file
--argocd-secret-path string Path to local argocd-secret.yaml file
--as string Username to impersonate for the operation
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
--as-uid string UID to impersonate for the operation
--auth-token string Authentication token
--certificate-authority string Path to a cert file for the certificate authority
--client-certificate string Path to a client certificate file for TLS
--client-crt string Client certificate file
--client-crt-key string Client certificate key file
--client-key string Path to a client key file for TLS
--cluster string The name of the kubeconfig cluster to use
--config string Path to Argo CD config (default "/home/user/.config/argocd/config")
--context string The name of the kubeconfig context to use
--core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server
--grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.
--grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.
-H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)
--http-retry-max int Maximum number of retries to establish http connection to Argo CD server
--insecure Skip server certificate and domain verification
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
--kube-context string Directs the command to the given kube-context
--kubeconfig string Path to a kube config. Only required if out-of-cluster
--load-cluster-settings Indicates that config map and secret should be loaded from cluster unless local file path is provided
--logformat string Set the logging format. One of: text|json (default "text")
--loglevel string Set the logging level. One of: debug|info|warn|error (default "info")
-n, --namespace string If present, the namespace scope for this CLI request
--password string Password for basic authentication to the API server
--plaintext Disable TLS
--port-forward Connect to a random argocd-server port using port forwarding
--port-forward-namespace string Namespace name which should be used for port forwarding
--proxy-url string If provided, this URL will be used to connect via proxy
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
--server string The address and port of the Kubernetes API server
--server-crt string Server certificate file
--tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used.
--token string Bearer token for authentication to the API server
--user string The name of the kubeconfig user to use
--username string Username for basic authentication to the API server
```
### SEE ALSO
* [argocd admin settings resource-overrides](argocd_admin_settings_resource-overrides.md) - Troubleshoot resource overrides

2
go.mod
View file

@ -125,7 +125,7 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0
github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect

View file

@ -43,8 +43,9 @@ nav:
- operator-manual/tls.md
- operator-manual/cluster-bootstrapping.md
- operator-manual/secret-management.md
- operator-manual/high_availability.md
- operator-manual/disaster_recovery.md
- operator-manual/high_availability.md
- operator-manual/reconcile.md
- operator-manual/webhook.md
- operator-manual/health.md
- operator-manual/resource_actions.md

View file

@ -116,6 +116,7 @@ API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/applicat
API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ResourceOverride,Actions
API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ResourceOverride,HealthLua
API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ResourceOverride,IgnoreDifferences
API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ResourceOverride,IgnoreResourceUpdates
API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ResourceOverride,KnownTypeFields
API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,ResourceOverride,UseOpenLibs
API rule violation: names_match,github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1,objectMeta,Name

File diff suppressed because it is too large Load diff

View file

@ -1662,6 +1662,8 @@ message ResourceOverride {
optional OverrideIgnoreDiff ignoreDifferences = 2;
optional OverrideIgnoreDiff ignoreResourceUpdates = 6;
repeated KnownTypeField knownTypeFields = 4;
}

View file

@ -5835,6 +5835,12 @@ func schema_pkg_apis_application_v1alpha1_ResourceOverride(ref common.ReferenceC
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OverrideIgnoreDiff"),
},
},
"IgnoreResourceUpdates": {
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OverrideIgnoreDiff"),
},
},
"KnownTypeFields": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
@ -5849,7 +5855,7 @@ func schema_pkg_apis_application_v1alpha1_ResourceOverride(ref common.ReferenceC
},
},
},
Required: []string{"HealthLua", "UseOpenLibs", "Actions", "IgnoreDifferences", "KnownTypeFields"},
Required: []string{"HealthLua", "UseOpenLibs", "Actions", "IgnoreDifferences", "IgnoreResourceUpdates", "KnownTypeFields"},
},
},
Dependencies: []string{
@ -7457,6 +7463,12 @@ func schema_pkg_apis_application_v1alpha1_rawResourceOverride(ref common.Referen
Format: "",
},
},
"ignoreResourceUpdates": {
SchemaProps: spec.SchemaProps{
Type: []string{"string"},
Format: "",
},
},
"knownTypeFields": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},

View file

@ -1848,21 +1848,23 @@ type OverrideIgnoreDiff struct {
}
type rawResourceOverride struct {
HealthLua string `json:"health.lua,omitempty"`
UseOpenLibs bool `json:"health.lua.useOpenLibs,omitempty"`
Actions string `json:"actions,omitempty"`
IgnoreDifferences string `json:"ignoreDifferences,omitempty"`
KnownTypeFields []KnownTypeField `json:"knownTypeFields,omitempty"`
HealthLua string `json:"health.lua,omitempty"`
UseOpenLibs bool `json:"health.lua.useOpenLibs,omitempty"`
Actions string `json:"actions,omitempty"`
IgnoreDifferences string `json:"ignoreDifferences,omitempty"`
IgnoreResourceUpdates string `json:"ignoreResourceUpdates,omitempty"`
KnownTypeFields []KnownTypeField `json:"knownTypeFields,omitempty"`
}
// ResourceOverride holds configuration to customize resource diffing and health assessment
// TODO: describe the members of this type
type ResourceOverride struct {
HealthLua string `protobuf:"bytes,1,opt,name=healthLua"`
UseOpenLibs bool `protobuf:"bytes,5,opt,name=useOpenLibs"`
Actions string `protobuf:"bytes,3,opt,name=actions"`
IgnoreDifferences OverrideIgnoreDiff `protobuf:"bytes,2,opt,name=ignoreDifferences"`
KnownTypeFields []KnownTypeField `protobuf:"bytes,4,opt,name=knownTypeFields"`
HealthLua string `protobuf:"bytes,1,opt,name=healthLua"`
UseOpenLibs bool `protobuf:"bytes,5,opt,name=useOpenLibs"`
Actions string `protobuf:"bytes,3,opt,name=actions"`
IgnoreDifferences OverrideIgnoreDiff `protobuf:"bytes,2,opt,name=ignoreDifferences"`
IgnoreResourceUpdates OverrideIgnoreDiff `protobuf:"bytes,6,opt,name=ignoreResourceUpdates"`
KnownTypeFields []KnownTypeField `protobuf:"bytes,4,opt,name=knownTypeFields"`
}
// TODO: describe this method
@ -1875,7 +1877,15 @@ func (s *ResourceOverride) UnmarshalJSON(data []byte) error {
s.HealthLua = raw.HealthLua
s.UseOpenLibs = raw.UseOpenLibs
s.Actions = raw.Actions
return yaml.Unmarshal([]byte(raw.IgnoreDifferences), &s.IgnoreDifferences)
err := yaml.Unmarshal([]byte(raw.IgnoreDifferences), &s.IgnoreDifferences)
if err != nil {
return err
}
err = yaml.Unmarshal([]byte(raw.IgnoreResourceUpdates), &s.IgnoreResourceUpdates)
if err != nil {
return err
}
return nil
}
// TODO: describe this method
@ -1884,7 +1894,11 @@ func (s ResourceOverride) MarshalJSON() ([]byte, error) {
if err != nil {
return nil, err
}
raw := &rawResourceOverride{s.HealthLua, s.UseOpenLibs, s.Actions, string(ignoreDifferencesData), s.KnownTypeFields}
ignoreResourceUpdatesData, err := yaml.Marshal(s.IgnoreResourceUpdates)
if err != nil {
return nil, err
}
raw := &rawResourceOverride{s.HealthLua, s.UseOpenLibs, s.Actions, string(ignoreDifferencesData), string(ignoreResourceUpdatesData), s.KnownTypeFields}
return json.Marshal(raw)
}

View file

@ -3233,6 +3233,7 @@ func (in *ResourceNode) DeepCopy() *ResourceNode {
func (in *ResourceOverride) DeepCopyInto(out *ResourceOverride) {
*out = *in
in.IgnoreDifferences.DeepCopyInto(&out.IgnoreDifferences)
in.IgnoreResourceUpdates.DeepCopyInto(&out.IgnoreResourceUpdates)
if in.KnownTypeFields != nil {
in, out := &in.KnownTypeFields, &out.KnownTypeFields
*out = make([]KnownTypeField, len(*in))

View file

@ -3,6 +3,7 @@ package normalizers
import (
"encoding/json"
"fmt"
"strings"
"github.com/argoproj/gitops-engine/pkg/diff"
jsonpatch "github.com/evanphx/json-patch"
@ -179,7 +180,9 @@ func (n *ignoreNormalizer) Normalize(un *unstructured.Unstructured) error {
for _, patch := range matched {
patchedDocData, err := patch.Apply(docData)
if err != nil {
log.Debugf("Failed to apply normalization: %v", err)
if shouldLogError(err) {
log.Debugf("Failed to apply normalization: %v", err)
}
continue
}
docData = patchedDocData
@ -191,3 +194,13 @@ func (n *ignoreNormalizer) Normalize(un *unstructured.Unstructured) error {
}
return nil
}
func shouldLogError(e error) bool {
if strings.Contains(e.Error(), "Unable to remove nonexistent key") {
return false
}
if strings.Contains(e.Error(), "remove operation does not apply: doc is missing path") {
return false
}
return true
}

View file

@ -1,6 +1,8 @@
package normalizers
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@ -219,3 +221,34 @@ func TestNormalizeJQPathExpressionWithError(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, originalDeployment, normalizedDeployment)
}
func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
"*/*": {
IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
JSONPointers: []string{"/invalid", "/invalid/json/path"},
},
},
})
assert.Nil(t, err)
ignoreNormalizer := normalizer.(*ignoreNormalizer)
assert.Len(t, ignoreNormalizer.patches, 2)
jsonPatch := ignoreNormalizer.patches[0]
jqPatch := ignoreNormalizer.patches[1]
deployment := test.NewDeployment()
deploymentData, err := json.Marshal(deployment)
assert.Nil(t, err)
// Error: "error in remove for path: '/invalid': Unable to remove nonexistent key: invalid: missing value"
_, err = jsonPatch.Apply(deploymentData)
assert.False(t, shouldLogError(err))
// Error: "remove operation does not apply: doc is missing path: \"/invalid/json/path\": missing value"
_, err = jqPatch.Apply(deploymentData)
assert.False(t, shouldLogError(err))
assert.True(t, shouldLogError(fmt.Errorf("An error that should not be ignored")))
}

View file

@ -421,6 +421,8 @@ const (
resourceExclusionsKey = "resource.exclusions"
// resourceInclusions is the key to the list of explicitly watched resources
resourceInclusionsKey = "resource.inclusions"
// resourceIgnoreResourceUpdatesEnabledKey is the key to a boolean determining whether the resourceIgnoreUpdates feature is enabled
resourceIgnoreResourceUpdatesEnabledKey = "resource.ignoreResourceUpdatesEnabled"
// resourceCustomLabelKey is the key to a custom label to show in node info, if present
resourceCustomLabelsKey = "resource.customLabels"
// kustomizeBuildOptionsKey is a string of kustomize build parameters
@ -528,6 +530,9 @@ type ArgoCDDiffOptions struct {
// If set to true then differences caused by status are ignored.
IgnoreResourceStatusField IgnoreStatus `json:"ignoreResourceStatusField,omitempty"`
// If set to true then ignoreDifferences are applied to ignore application refresh on resource updates.
IgnoreDifferencesOnResourceUpdates bool `json:"ignoreDifferencesOnResourceUpdates,omitempty"`
}
func (e *incompleteSettingsError) Error() string {
@ -777,6 +782,54 @@ func (mgr *SettingsManager) GetEnabledSourceTypes() (map[string]bool, error) {
return res, nil
}
func (mgr *SettingsManager) GetIgnoreResourceUpdatesOverrides() (map[string]v1alpha1.ResourceOverride, error) {
compareOptions, err := mgr.GetResourceCompareOptions()
if err != nil {
return nil, fmt.Errorf("failed to get compare options: %w", err)
}
resourceOverrides, err := mgr.GetResourceOverrides()
if err != nil {
return nil, fmt.Errorf("failed to get resource overrides: %w", err)
}
for k, v := range resourceOverrides {
resourceUpdates := v.IgnoreResourceUpdates
if compareOptions.IgnoreDifferencesOnResourceUpdates {
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
}
if compareOptions.IgnoreDifferencesOnResourceUpdates {
log.Info("Using diffing customizations to ignore resource updates")
}
addIgnoreDiffItemOverrideToGK(resourceOverrides, "*/*", "/metadata/resourceVersion")
addIgnoreDiffItemOverrideToGK(resourceOverrides, "*/*", "/metadata/generation")
addIgnoreDiffItemOverrideToGK(resourceOverrides, "*/*", "/metadata/managedFields")
return resourceOverrides, nil
}
func (mgr *SettingsManager) GetIsIgnoreResourceUpdatesEnabled() (bool, error) {
argoCDCM, err := mgr.getConfigMap()
if err != nil {
return false, err
}
if argoCDCM.Data[resourceIgnoreResourceUpdatesEnabledKey] == "" {
return false, nil
}
return strconv.ParseBool(argoCDCM.Data[resourceIgnoreResourceUpdatesEnabledKey])
}
// GetResourceOverrides loads Resource Overrides from argocd-cm ConfigMap
func (mgr *SettingsManager) GetResourceOverrides() (map[string]v1alpha1.ResourceOverride, error) {
argoCDCM, err := mgr.getConfigMap()
@ -893,6 +946,13 @@ func (mgr *SettingsManager) appendResourceOverridesFromSplitKeys(cmData map[stri
return err
}
overrideVal.IgnoreDifferences = overrideIgnoreDiff
case "ignoreResourceUpdates":
overrideIgnoreUpdate := v1alpha1.OverrideIgnoreDiff{}
err := yaml.Unmarshal([]byte(v), &overrideIgnoreUpdate)
if err != nil {
return err
}
overrideVal.IgnoreResourceUpdates = overrideIgnoreUpdate
case "knownTypeFields":
var knownTypeFields []v1alpha1.KnownTypeField
err := yaml.Unmarshal([]byte(v), &knownTypeFields)
@ -922,7 +982,7 @@ func convertToOverrideKey(groupKind string) (string, error) {
}
func GetDefaultDiffOptions() ArgoCDDiffOptions {
return ArgoCDDiffOptions{IgnoreAggregatedRoles: false}
return ArgoCDDiffOptions{IgnoreAggregatedRoles: false, IgnoreDifferencesOnResourceUpdates: false}
}
// GetResourceCompareOptions loads the resource compare options settings from the ConfigMap

View file

@ -185,6 +185,22 @@ func TestGetServerRBACLogEnforceEnableKeyDefaultFalse(t *testing.T) {
assert.Equal(t, false, serverRBACLogEnforceEnable)
}
func TestGetIsIgnoreResourceUpdatesEnabled(t *testing.T) {
_, settingsManager := fixtures(map[string]string{
"resource.ignoreResourceUpdatesEnabled": "true",
})
ignoreResourceUpdatesEnabled, err := settingsManager.GetIsIgnoreResourceUpdatesEnabled()
assert.NoError(t, err)
assert.True(t, ignoreResourceUpdatesEnabled)
}
func TestGetIsIgnoreResourceUpdatesEnabledDefaultFalse(t *testing.T) {
_, settingsManager := fixtures(nil)
ignoreResourceUpdatesEnabled, err := settingsManager.GetIsIgnoreResourceUpdatesEnabled()
assert.NoError(t, err)
assert.False(t, ignoreResourceUpdatesEnabled)
}
func TestGetServerRBACLogEnforceEnableKey(t *testing.T) {
_, settingsManager := fixtures(map[string]string{
"server.rbac.log.enforce.enable": "true",
@ -210,7 +226,12 @@ func TestGetResourceOverrides(t *testing.T) {
jsonPointers:
- /webhooks/0/clientConfig/caBundle
jqPathExpressions:
- .webhooks[0].clientConfig.caBundle`,
- .webhooks[0].clientConfig.caBundle
ignoreResourceUpdates: |
jsonPointers:
- /webhooks/1/clientConfig/caBundle
jqPathExpressions:
- .webhooks[1].clientConfig.caBundle`,
})
overrides, err := settingsManager.GetResourceOverrides()
assert.NoError(t, err)
@ -223,6 +244,10 @@ func TestGetResourceOverrides(t *testing.T) {
JSONPointers: []string{"/webhooks/0/clientConfig/caBundle"},
JQPathExpressions: []string{".webhooks[0].clientConfig.caBundle"},
},
IgnoreResourceUpdates: v1alpha1.OverrideIgnoreDiff{
JSONPointers: []string{"/webhooks/1/clientConfig/caBundle"},
JQPathExpressions: []string{".webhooks[1].clientConfig.caBundle"},
},
}, webHookOverrides)
// by default, crd status should be ignored
@ -324,6 +349,9 @@ func TestGetResourceOverrides_with_splitted_keys(t *testing.T) {
ignoreDifferences: |
jsonPointers:
- foo
ignoreResourceUpdates: |
jsonPointers:
- foo
certmanager.k8s.io/Certificate:
health.lua.useOpenLibs: true
health.lua: |
@ -346,6 +374,8 @@ func TestGetResourceOverrides_with_splitted_keys(t *testing.T) {
assert.Equal(t, 2, len(overrides[crdGK].IgnoreDifferences.JSONPointers))
assert.Equal(t, 1, len(overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreDifferences.JSONPointers))
assert.Equal(t, "foo", overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreDifferences.JSONPointers[0])
assert.Equal(t, 1, len(overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreResourceUpdates.JSONPointers))
assert.Equal(t, "foo", overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreResourceUpdates.JSONPointers[0])
assert.Equal(t, "foo\n", overrides["certmanager.k8s.io/Certificate"].HealthLua)
assert.Equal(t, true, overrides["certmanager.k8s.io/Certificate"].UseOpenLibs)
assert.Equal(t, "foo\n", overrides["cert-manager.io/Certificate"].HealthLua)
@ -357,6 +387,8 @@ func TestGetResourceOverrides_with_splitted_keys(t *testing.T) {
newData := map[string]string{
"resource.customizations.health.admissionregistration.k8s.io_MutatingWebhookConfiguration": "bar",
"resource.customizations.ignoreDifferences.admissionregistration.k8s.io_MutatingWebhookConfiguration": `jsonPointers:
- bar`,
"resource.customizations.ignoreResourceUpdates.admissionregistration.k8s.io_MutatingWebhookConfiguration": `jsonPointers:
- bar`,
"resource.customizations.knownTypeFields.admissionregistration.k8s.io_MutatingWebhookConfiguration": `
- field: foo
@ -373,9 +405,13 @@ func TestGetResourceOverrides_with_splitted_keys(t *testing.T) {
- bar`,
"resource.customizations.ignoreDifferences.apps_Deployment": `jqPathExpressions:
- bar`,
"resource.customizations.ignoreDifferences.all": `managedFieldsManagers:
"resource.customizations.ignoreDifferences.all": `managedFieldsManagers:
- kube-controller-manager
- argo-rollouts`,
"resource.customizations.ignoreResourceUpdates.iam-manager.k8s.io_Iamrole": `jsonPointers:
- bar`,
"resource.customizations.ignoreResourceUpdates.apps_Deployment": `jqPathExpressions:
- bar`,
}
crdGK := "apiextensions.k8s.io/CustomResourceDefinition"
@ -389,6 +425,8 @@ func TestGetResourceOverrides_with_splitted_keys(t *testing.T) {
assert.Equal(t, "/spec/preserveUnknownFields", overrides[crdGK].IgnoreDifferences.JSONPointers[1])
assert.Equal(t, 1, len(overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreDifferences.JSONPointers))
assert.Equal(t, "bar", overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreDifferences.JSONPointers[0])
assert.Equal(t, 1, len(overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreResourceUpdates.JSONPointers))
assert.Equal(t, "bar", overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].IgnoreResourceUpdates.JSONPointers[0])
assert.Equal(t, 1, len(overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].KnownTypeFields))
assert.Equal(t, "bar", overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].KnownTypeFields[0].Type)
assert.Equal(t, "bar", overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"].HealthLua)
@ -406,6 +444,9 @@ func TestGetResourceOverrides_with_splitted_keys(t *testing.T) {
assert.Equal(t, 2, len(overrides["*/*"].IgnoreDifferences.ManagedFieldsManagers))
assert.Equal(t, "kube-controller-manager", overrides["*/*"].IgnoreDifferences.ManagedFieldsManagers[0])
assert.Equal(t, "argo-rollouts", overrides["*/*"].IgnoreDifferences.ManagedFieldsManagers[1])
assert.Equal(t, 1, len(overrides["iam-manager.k8s.io/Iamrole"].IgnoreResourceUpdates.JSONPointers))
assert.Equal(t, 1, len(overrides["apps/Deployment"].IgnoreResourceUpdates.JQPathExpressions))
assert.Equal(t, "bar", overrides["apps/Deployment"].IgnoreResourceUpdates.JQPathExpressions[0])
})
t.Run("SplitKeysCompareOptionsAll", func(t *testing.T) {
@ -451,6 +492,64 @@ func mergemaps(mapA map[string]string, mapB map[string]string) map[string]string
return mapB
}
func TestGetIgnoreResourceUpdatesOverrides(t *testing.T) {
allDefault := v1alpha1.ResourceOverride{IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
JSONPointers: []string{"/metadata/resourceVersion", "/metadata/generation", "/metadata/managedFields"},
}}
allGK := "*/*"
testCustomizations := map[string]string{
"resource.customizations": `
admissionregistration.k8s.io/MutatingWebhookConfiguration:
ignoreDifferences: |
jsonPointers:
- /webhooks/0/clientConfig/caBundle
jqPathExpressions:
- .webhooks[0].clientConfig.caBundle
ignoreResourceUpdates: |
jsonPointers:
- /webhooks/1/clientConfig/caBundle
jqPathExpressions:
- .webhooks[1].clientConfig.caBundle`,
}
_, settingsManager := fixtures(testCustomizations)
overrides, err := settingsManager.GetIgnoreResourceUpdatesOverrides()
assert.NoError(t, err)
// default overrides should always be present
allOverrides := overrides[allGK]
assert.NotNil(t, allOverrides)
assert.Equal(t, allDefault, allOverrides)
// without ignoreDifferencesOnResourceUpdates, only ignoreResourceUpdates should be added
assert.NotNil(t, overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"])
assert.Equal(t, v1alpha1.ResourceOverride{
IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
JSONPointers: []string{"/webhooks/1/clientConfig/caBundle"},
JQPathExpressions: []string{".webhooks[1].clientConfig.caBundle"},
},
IgnoreResourceUpdates: v1alpha1.OverrideIgnoreDiff{},
}, overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"])
// with ignoreDifferencesOnResourceUpdates, ignoreDifferences should be added
_, settingsManager = fixtures(mergemaps(testCustomizations, map[string]string{
"resource.compareoptions": `
ignoreDifferencesOnResourceUpdates: true`,
}))
overrides, err = settingsManager.GetIgnoreResourceUpdatesOverrides()
assert.NoError(t, err)
assert.NotNil(t, overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"])
assert.Equal(t, v1alpha1.ResourceOverride{
IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
JSONPointers: []string{"/webhooks/1/clientConfig/caBundle", "/webhooks/0/clientConfig/caBundle"},
JQPathExpressions: []string{".webhooks[1].clientConfig.caBundle", ".webhooks[0].clientConfig.caBundle"},
},
IgnoreResourceUpdates: v1alpha1.OverrideIgnoreDiff{},
}, overrides["admissionregistration.k8s.io/MutatingWebhookConfiguration"])
}
func TestConvertToOverrideKey(t *testing.T) {
key, err := convertToOverrideKey("cert-manager.io_Certificate")
assert.NoError(t, err)
@ -488,6 +587,26 @@ func TestGetResourceCompareOptions(t *testing.T) {
assert.False(t, compareOptions.IgnoreAggregatedRoles)
}
// ignoreDifferencesOnResourceUpdates is true
{
_, settingsManager := fixtures(map[string]string{
"resource.compareoptions": "ignoreDifferencesOnResourceUpdates: true",
})
compareOptions, err := settingsManager.GetResourceCompareOptions()
assert.NoError(t, err)
assert.True(t, compareOptions.IgnoreDifferencesOnResourceUpdates)
}
// ignoreDifferencesOnResourceUpdates is false
{
_, settingsManager := fixtures(map[string]string{
"resource.compareoptions": "ignoreDifferencesOnResourceUpdates: false",
})
compareOptions, err := settingsManager.GetResourceCompareOptions()
assert.NoError(t, err)
assert.False(t, compareOptions.IgnoreDifferencesOnResourceUpdates)
}
// The empty resource.compareoptions should result in default being returned
{
_, settingsManager := fixtures(map[string]string{
@ -497,6 +616,7 @@ func TestGetResourceCompareOptions(t *testing.T) {
defaultOptions := GetDefaultDiffOptions()
assert.NoError(t, err)
assert.Equal(t, defaultOptions.IgnoreAggregatedRoles, compareOptions.IgnoreAggregatedRoles)
assert.Equal(t, defaultOptions.IgnoreDifferencesOnResourceUpdates, compareOptions.IgnoreDifferencesOnResourceUpdates)
}
// resource.compareoptions not defined - should result in default being returned
@ -506,6 +626,7 @@ func TestGetResourceCompareOptions(t *testing.T) {
defaultOptions := GetDefaultDiffOptions()
assert.NoError(t, err)
assert.Equal(t, defaultOptions.IgnoreAggregatedRoles, compareOptions.IgnoreAggregatedRoles)
assert.Equal(t, defaultOptions.IgnoreDifferencesOnResourceUpdates, compareOptions.IgnoreDifferencesOnResourceUpdates)
}
}