VYPR
Moderate severityNVD Advisory· Published Apr 26, 2024· Updated Aug 2, 2024

Denial of Service via malicious jqPathExpressions in ignoreDifferences

CVE-2024-32476

Description

Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes. There is a Denial of Service (DoS) vulnerability via OOM using jq in ignoreDifferences. This vulnerability has been patched in version(s) 2.10.7, 2.9.12 and 2.8.16.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/argoproj/argo-cd/v2Go
>= 2.10.0, < 2.10.82.10.8
github.com/argoproj/argo-cd/v2Go
>= 2.9.0, < 2.9.132.9.13
github.com/argoproj/argo-cd/v2Go
< 2.8.172.8.17

Affected products

1

Patches

3
9e5cc5a26ff0

Merge pull request from GHSA-9m6p-x4h2-6frq

https://github.com/argoproj/argo-cdpasha-codefreshApr 26, 2024via ghsa
23 files changed · +184 73
  • applicationset/controllers/applicationset_controller.go+2 1 modified
    @@ -50,6 +50,7 @@ import (
     	argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
     	argoutil "github.com/argoproj/argo-cd/v2/util/argo"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application"
     )
    @@ -666,7 +667,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
     			},
     		}
     
    -		action, err := utils.CreateOrUpdate(ctx, appLog, r.Client, applicationSet.Spec.IgnoreApplicationDifferences, found, func() error {
    +		action, err := utils.CreateOrUpdate(ctx, appLog, r.Client, applicationSet.Spec.IgnoreApplicationDifferences, normalizers.IgnoreNormalizerOpts{}, found, func() error {
     			// Copy only the Application/ObjectMeta fields that are significant, from the generatedApp
     			found.Spec = generatedApp.Spec
     
    
  • applicationset/utils/createOrUpdate.go+5 4 modified
    @@ -20,6 +20,7 @@ import (
     	argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     // CreateOrUpdate overrides "sigs.k8s.io/controller-runtime" function
    @@ -35,7 +36,7 @@ import (
     // The MutateFn is called regardless of creating or updating an object.
     //
     // It returns the executed operation and an error.
    -func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ignoreAppDifferences argov1alpha1.ApplicationSetIgnoreDifferences, obj *argov1alpha1.Application, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
    +func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ignoreAppDifferences argov1alpha1.ApplicationSetIgnoreDifferences, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, obj *argov1alpha1.Application, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
     
     	key := client.ObjectKeyFromObject(obj)
     	if err := c.Get(ctx, key, obj); err != nil {
    @@ -60,7 +61,7 @@ func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ign
     
     	// Apply ignoreApplicationDifferences rules to remove ignored fields from both the live and the desired state. This
     	// prevents those differences from appearing in the diff and therefore in the patch.
    -	err := applyIgnoreDifferences(ignoreAppDifferences, normalizedLive, obj)
    +	err := applyIgnoreDifferences(ignoreAppDifferences, normalizedLive, obj, ignoreNormalizerOpts)
     	if err != nil {
     		return controllerutil.OperationResultNone, fmt.Errorf("failed to apply ignore differences: %w", err)
     	}
    @@ -134,14 +135,14 @@ func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object)
     }
     
     // applyIgnoreDifferences applies the ignore differences rules to the found application. It modifies the applications in place.
    -func applyIgnoreDifferences(applicationSetIgnoreDifferences argov1alpha1.ApplicationSetIgnoreDifferences, found *argov1alpha1.Application, generatedApp *argov1alpha1.Application) error {
    +func applyIgnoreDifferences(applicationSetIgnoreDifferences argov1alpha1.ApplicationSetIgnoreDifferences, found *argov1alpha1.Application, generatedApp *argov1alpha1.Application, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) error {
     	if len(applicationSetIgnoreDifferences) == 0 {
     		return nil
     	}
     
     	generatedAppCopy := generatedApp.DeepCopy()
     	diffConfig, err := argodiff.NewDiffConfigBuilder().
    -		WithDiffSettings(applicationSetIgnoreDifferences.ToApplicationIgnoreDifferences(), nil, false).
    +		WithDiffSettings(applicationSetIgnoreDifferences.ToApplicationIgnoreDifferences(), nil, false, ignoreNormalizerOpts).
     		WithNoCache().
     		Build()
     	if err != nil {
    
  • applicationset/utils/createOrUpdate_test.go+2 1 modified
    @@ -9,6 +9,7 @@ import (
     	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func Test_applyIgnoreDifferences(t *testing.T) {
    @@ -222,7 +223,7 @@ spec:
     			generatedApp := v1alpha1.Application{TypeMeta: appMeta}
     			err = yaml.Unmarshal([]byte(tc.generatedApp), &generatedApp)
     			require.NoError(t, err, tc.generatedApp)
    -			err = applyIgnoreDifferences(tc.ignoreDifferences, &foundApp, &generatedApp)
    +			err = applyIgnoreDifferences(tc.ignoreDifferences, &foundApp, &generatedApp, normalizers.IgnoreNormalizerOpts{})
     			require.NoError(t, err)
     			yamlFound, err := yaml.Marshal(tc.foundApp)
     			require.NoError(t, err)
    
  • cmd/argocd-application-controller/commands/argocd_application_controller.go+5 1 modified
    @@ -6,7 +6,6 @@ import (
     	"math"
     	"time"
     
    -	"github.com/argoproj/argo-cd/v2/pkg/ratelimiter"
     	"github.com/argoproj/pkg/stats"
     	"github.com/redis/go-redis/v9"
     	log "github.com/sirupsen/logrus"
    @@ -20,7 +19,9 @@ import (
     	"github.com/argoproj/argo-cd/v2/controller/sharding"
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
    +	"github.com/argoproj/argo-cd/v2/pkg/ratelimiter"
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/cli"
    @@ -72,6 +73,7 @@ func NewCommand() *cobra.Command {
     		shardingAlgorithm                string
     		enableDynamicClusterDistribution bool
     		serverSideDiff                   bool
    +		ignoreNormalizerOpts             normalizers.IgnoreNormalizerOpts
     	)
     	var command = cobra.Command{
     		Use:               cliName,
    @@ -169,6 +171,7 @@ func NewCommand() *cobra.Command {
     				&workqueueRateLimit,
     				serverSideDiff,
     				enableDynamicClusterDistribution,
    +				ignoreNormalizerOpts,
     			)
     			errors.CheckError(err)
     			cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
    @@ -229,6 +232,7 @@ func NewCommand() *cobra.Command {
     	command.Flags().Float64Var(&workqueueRateLimit.BackoffFactor, "wq-backoff-factor", env.ParseFloat64FromEnv("WORKQUEUE_BACKOFF_FACTOR", 1.5, 0, math.MaxFloat64), "Set Workqueue Per Item Rate Limiter Backoff Factor, default is 1.5")
     	command.Flags().BoolVar(&enableDynamicClusterDistribution, "dynamic-cluster-distribution-enabled", env.ParseBoolFromEnv(common.EnvEnableDynamicClusterDistribution, false), "Enables dynamic cluster distribution.")
     	command.Flags().BoolVar(&serverSideDiff, "server-side-diff-enabled", env.ParseBoolFromEnv(common.EnvServerSideDiff, false), "Feature flag to enable ServerSide diff. Default (\"false\")")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "", env.ParseDurationFromEnv("ARGOCD_IGNORE_NORMALIZER_JQ_TIMEOUT", 0*time.Second, 0, math.MaxInt64), "Set ignore normalizer JQ execution timeout")
     	cacheSource = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
     		redisClient = client
     	})
    
  • cmd/argocd/commands/admin/app.go+12 9 modified
    @@ -30,6 +30,7 @@ import (
     	appinformers "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions"
     	reposerverclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/cli"
    @@ -238,12 +239,13 @@ func diffReconcileResults(res1 reconcileResults, res2 reconcileResults) error {
     
     func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
     	var (
    -		clientConfig      clientcmd.ClientConfig
    -		selector          string
    -		repoServerAddress string
    -		outputFormat      string
    -		refresh           bool
    -		serverSideDiff    bool
    +		clientConfig         clientcmd.ClientConfig
    +		selector             string
    +		repoServerAddress    string
    +		outputFormat         string
    +		refresh              bool
    +		serverSideDiff       bool
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     	)
     
     	var command = &cobra.Command{
    @@ -281,7 +283,7 @@ func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
     
     				appClientset := appclientset.NewForConfigOrDie(cfg)
     				kubeClientset := kubernetes.NewForConfigOrDie(cfg)
    -				result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache, serverSideDiff)
    +				result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache, serverSideDiff, ignoreNormalizerOpts)
     				errors.CheckError(err)
     			} else {
     				appClientset := appclientset.NewForConfigOrDie(cfg)
    @@ -297,7 +299,7 @@ func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
     	command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)")
     	command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation")
     	command.Flags().BoolVar(&serverSideDiff, "server-side-diff", false, "If set to \"true\" will use server-side diff while comparing resources. Default (\"false\")")
    -
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    @@ -347,6 +349,7 @@ func reconcileApplications(
     	selector string,
     	createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache,
     	serverSideDiff bool,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) ([]appReconcileResult, error) {
     	settingsMgr := settings.NewSettingsManager(ctx, kubeClientset, namespace)
     	argoDB := db.NewDB(namespace, settingsMgr, kubeClientset)
    @@ -387,7 +390,7 @@ func reconcileApplications(
     	)
     
     	appStateManager := controller.NewAppStateManager(
    -		argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false, 0, serverSideDiff)
    +		argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false, 0, serverSideDiff, ignoreNormalizerOpts)
     
     	appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, v1.ListOptions{LabelSelector: selector})
     	if err != nil {
    
  • cmd/argocd/commands/admin/app_test.go+2 0 modified
    @@ -23,6 +23,7 @@ import (
     	argocdclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
     	"github.com/argoproj/argo-cd/v2/test"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/settings"
     )
    @@ -114,6 +115,7 @@ func TestGetReconcileResults_Refresh(t *testing.T) {
     			return &liveStateCache
     		},
     		false,
    +		normalizers.IgnoreNormalizerOpts{},
     	)
     
     	if !assert.NoError(t, err) {
    
  • cmd/argocd/commands/admin/settings.go+6 2 modified
    @@ -428,7 +428,7 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
     				// configurations. This requires access to live resources which is not the
     				// purpose of this command. This will just apply jsonPointers and
     				// jqPathExpressions configurations.
    -				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
    +				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, normalizers.IgnoreNormalizerOpts{})
     				errors.CheckError(err)
     
     				normalizedRes := res.DeepCopy()
    @@ -453,6 +453,9 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
     }
     
     func NewResourceIgnoreResourceUpdatesCommand(cmdCtx commandContext) *cobra.Command {
    +	var (
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
    +	)
     	var command = &cobra.Command{
     		Use:   "ignore-resource-updates RESOURCE_YAML_PATH",
     		Short: "Renders fields excluded from resource updates",
    @@ -474,7 +477,7 @@ argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml -
     					return
     				}
     
    -				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
    +				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, ignoreNormalizerOpts)
     				errors.CheckError(err)
     
     				normalizedRes := res.DeepCopy()
    @@ -495,6 +498,7 @@ argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml -
     			})
     		},
     	}
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    
  • cmd/argocd/commands/app.go+17 12 modified
    @@ -44,6 +44,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/repository"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/cli"
     	"github.com/argoproj/argo-cd/v2/util/errors"
     	"github.com/argoproj/argo-cd/v2/util/git"
    @@ -1049,14 +1050,15 @@ type objKeyLiveTarget struct {
     // NewApplicationDiffCommand returns a new instance of an `argocd app diff` command
     func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
     	var (
    -		refresh            bool
    -		hardRefresh        bool
    -		exitCode           bool
    -		local              string
    -		revision           string
    -		localRepoRoot      string
    -		serverSideGenerate bool
    -		localIncludes      []string
    +		refresh              bool
    +		hardRefresh          bool
    +		exitCode             bool
    +		local                string
    +		revision             string
    +		localRepoRoot        string
    +		serverSideGenerate   bool
    +		localIncludes        []string
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     	)
     	shortDesc := "Perform a diff against the target and live state."
     	var command = &cobra.Command{
    @@ -1123,7 +1125,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     				}
     			}
     			proj := getProject(c, clientOpts, ctx, app.Spec.Project)
    -			foundDiffs := findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption)
    +			foundDiffs := findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
     			if foundDiffs && exitCode {
     				os.Exit(1)
     			}
    @@ -1137,6 +1139,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     	command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
     	command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
     	command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    @@ -1151,7 +1154,7 @@ type DifferenceOption struct {
     }
     
     // findandPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
    -func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) bool {
    +func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) bool {
     	var foundDiffs bool
     	liveObjs, err := cmdutil.LiveObjects(resources.Items)
     	errors.CheckError(err)
    @@ -1206,7 +1209,7 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
     		// compareOptions in the protobuf
     		ignoreAggregatedRoles := false
     		diffConfig, err := argodiff.NewDiffConfigBuilder().
    -			WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles).
    +			WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles, ignoreNormalizerOpts).
     			WithTracking(argoSettings.AppLabelKey, argoSettings.TrackingMethod).
     			WithNoCache().
     			Build()
    @@ -1699,6 +1702,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     		diffChangesConfirm      bool
     		projects                []string
     		output                  string
    +		ignoreNormalizerOpts    normalizers.IgnoreNormalizerOpts
     	)
     	var command = &cobra.Command{
     		Use:   "sync [APPNAME... | -l selector | --project project-name]",
    @@ -1923,7 +1927,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     					fmt.Printf("====== Previewing differences between live and desired state of application %s ======\n", appQualifiedName)
     
     					proj := getProject(c, clientOpts, ctx, app.Spec.Project)
    -					foundDiffs = findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption)
    +					foundDiffs = findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
     					if foundDiffs {
     						if !diffChangesConfirm {
     							yesno := cli.AskToProceed(fmt.Sprintf("Please review changes to application %s shown above. Do you want to continue the sync process? (y/n): ", appQualifiedName))
    @@ -1981,6 +1985,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     	command.Flags().BoolVar(&diffChanges, "preview-changes", false, "Preview difference against the target and live state before syncing app and wait for user confirmation")
     	command.Flags().StringArrayVar(&projects, "project", []string{}, "Sync apps that belong to the specified projects. This option may be specified repeatedly.")
     	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    
  • controller/appcontroller.go+6 2 modified
    @@ -55,6 +55,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/env"
     
     	kubeerrors "k8s.io/apimachinery/pkg/api/errors"
    @@ -130,6 +131,7 @@ type ApplicationController struct {
     	clusterSharding               sharding.ClusterShardingCache
     	projByNameCache               sync.Map
     	applicationNamespaces         []string
    +	ignoreNormalizerOpts          normalizers.IgnoreNormalizerOpts
     
     	// dynamicClusterDistributionEnabled if disabled deploymentInformer is never initialized
     	dynamicClusterDistributionEnabled bool
    @@ -160,6 +162,7 @@ func NewApplicationController(
     	rateLimiterConfig *ratelimiter.AppControllerRateLimiterConfig,
     	serverSideDiff bool,
     	dynamicClusterDistributionEnabled bool,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) (*ApplicationController, error) {
     	log.Infof("appResyncPeriod=%v, appHardResyncPeriod=%v, appResyncJitter=%v", appResyncPeriod, appHardResyncPeriod, appResyncJitter)
     	db := db.NewDB(namespace, settingsMgr, kubeClientset)
    @@ -191,6 +194,7 @@ func NewApplicationController(
     		projByNameCache:                   sync.Map{},
     		applicationNamespaces:             applicationNamespaces,
     		dynamicClusterDistributionEnabled: dynamicClusterDistributionEnabled,
    +		ignoreNormalizerOpts:              ignoreNormalizerOpts,
     	}
     	if kubectlParallelismLimit > 0 {
     		ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
    @@ -278,7 +282,7 @@ func NewApplicationController(
     		}
     	}
     	stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated, clusterSharding, argo.NewResourceTracking())
    -	appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth, repoErrorGracePeriod, serverSideDiff)
    +	appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth, repoErrorGracePeriod, serverSideDiff, ignoreNormalizerOpts)
     	ctrl.appInformer = appInformer
     	ctrl.appLister = appLister
     	ctrl.projInformer = projInformer
    @@ -729,7 +733,7 @@ func (ctrl *ApplicationController) hideSecretData(app *appv1.Application, compar
     				return nil, fmt.Errorf("error getting cluster cache: %s", err)
     			}
     			diffConfig, err := argodiff.NewDiffConfigBuilder().
    -				WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
    +				WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles, ctrl.ignoreNormalizerOpts).
     				WithTracking(appLabelKey, trackingMethod).
     				WithNoCache().
     				WithLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig())).
    
  • controller/appcontroller_test.go+2 1 modified
    @@ -42,6 +42,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	mockrepoclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
     	"github.com/argoproj/argo-cd/v2/test"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/settings"
    @@ -155,9 +156,9 @@ func newFakeController(data *fakeData, repoErr error) *ApplicationController {
     		nil,
     		data.applicationNamespaces,
     		nil,
    -
     		false,
     		false,
    +		normalizers.IgnoreNormalizerOpts{},
     	)
     	db := &dbmocks.ArgoDB{}
     	db.On("GetApplicationControllerReplicas").Return(1)
    
  • controller/cache/cache.go+11 9 modified
    @@ -33,6 +33,7 @@ import (
     	"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/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/env"
     	logutils "github.com/argoproj/argo-cd/v2/util/log"
    @@ -197,14 +198,15 @@ type cacheSettings struct {
     }
     
     type liveStateCache struct {
    -	db               db.ArgoDB
    -	appInformer      cache.SharedIndexInformer
    -	onObjectUpdated  ObjectUpdatedHandler
    -	kubectl          kube.Kubectl
    -	settingsMgr      *settings.SettingsManager
    -	metricsServer    *metrics.MetricsServer
    -	clusterSharding  sharding.ClusterShardingCache
    -	resourceTracking argo.ResourceTracking
    +	db                   db.ArgoDB
    +	appInformer          cache.SharedIndexInformer
    +	onObjectUpdated      ObjectUpdatedHandler
    +	kubectl              kube.Kubectl
    +	settingsMgr          *settings.SettingsManager
    +	metricsServer        *metrics.MetricsServer
    +	clusterSharding      sharding.ClusterShardingCache
    +	resourceTracking     argo.ResourceTracking
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     
     	clusters      map[string]clustercache.ClusterCache
     	cacheSettings cacheSettings
    @@ -487,7 +489,7 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
     			gvk := un.GroupVersionKind()
     
     			if cacheSettings.ignoreResourceUpdatesEnabled && shouldHashManifest(appName, gvk) {
    -				hash, err := generateManifestHash(un, nil, cacheSettings.resourceOverrides)
    +				hash, err := generateManifestHash(un, nil, cacheSettings.resourceOverrides, c.ignoreNormalizerOpts)
     				if err != nil {
     					log.Errorf("Failed to generate manifest hash: %v", err)
     				} else {
    
  • controller/cache/info.go+2 2 modified
    @@ -408,8 +408,8 @@ func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
     	}
     }
     
    -func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (string, error) {
    -	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides)
    +func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (string, error) {
    +	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides, opts)
     	if err != nil {
     		return "", fmt.Errorf("error creating normalizer: %w", err)
     	}
    
  • controller/cache/info_test.go+2 1 modified
    @@ -16,6 +16,7 @@ import (
     	"sigs.k8s.io/yaml"
     
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func strToUnstructured(jsonStr string) *unstructured.Unstructured {
    @@ -749,7 +750,7 @@ func TestManifestHash(t *testing.T) {
     
     	expected := hash(data)
     
    -	hash, err := generateManifestHash(manifest, ignores, nil)
    +	hash, err := generateManifestHash(manifest, ignores, nil, normalizers.IgnoreNormalizerOpts{})
     	assert.Equal(t, expected, hash)
     	assert.Nil(t, err)
     }
    
  • controller/state.go+5 1 modified
    @@ -35,6 +35,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/gpg"
    @@ -117,6 +118,7 @@ type appStateManager struct {
     	repoErrorCache        goSync.Map
     	repoErrorGracePeriod  time.Duration
     	serverSideDiff        bool
    +	ignoreNormalizerOpts  normalizers.IgnoreNormalizerOpts
     }
     
     // GetRepoObjs will generate the manifests for the given application delegating the
    @@ -605,7 +607,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
     	useDiffCache := useDiffCache(noCache, manifestInfos, sources, app, manifestRevisions, m.statusRefreshTimeout, serverSideDiff, logCtx)
     
     	diffConfigBuilder := argodiff.NewDiffConfigBuilder().
    -		WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
    +		WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles, m.ignoreNormalizerOpts).
     		WithTracking(appLabelKey, string(trackingMethod))
     
     	if useDiffCache {
    @@ -935,6 +937,7 @@ func NewAppStateManager(
     	persistResourceHealth bool,
     	repoErrorGracePeriod time.Duration,
     	serverSideDiff bool,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) AppStateManager {
     	return &appStateManager{
     		liveStateCache:        liveStateCache,
    @@ -952,6 +955,7 @@ func NewAppStateManager(
     		persistResourceHealth: persistResourceHealth,
     		repoErrorGracePeriod:  repoErrorGracePeriod,
     		serverSideDiff:        serverSideDiff,
    +		ignoreNormalizerOpts:  ignoreNormalizerOpts,
     	}
     }
     
    
  • controller/sync_test.go+3 2 modified
    @@ -18,6 +18,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/test"
     	"github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func TestPersistRevisionHistory(t *testing.T) {
    @@ -261,7 +262,7 @@ func TestNormalizeTargetResources(t *testing.T) {
     	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    @@ -394,7 +395,7 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
     	setupHttpProxy := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    
  • docs/user-guide/diffing.md+13 0 modified
    @@ -185,3 +185,16 @@ The list of supported Kubernetes types is available in [diffing_known_types.txt]
     
     * `core/Quantity`
     * `meta/v1/duration`
    +
    +
    +### JQ Path expression timeout
    +
    +By default, the evaluation of a JQPathExpression is limited to one second. If you encounter a "JQ patch execution timed out" error message due to a complex JQPathExpression that requires more time to evaluate, you can extend the timeout period by configuring the `ignore.normalizer.jq.timeout` setting within the `argocd-cmd-params-cm` ConfigMap.
    +
    +```yaml
    +apiVersion: v1
    +kind: ConfigMap
    +metadata:
    +  name: argocd-cmd-params-cm
    +data:
    +  ignore.normalizer.jq.timeout: "5s"
    
  • manifests/base/application-controller/argocd-application-controller-statefulset.yaml+6 0 modified
    @@ -197,6 +197,12 @@ spec:
                   name: argocd-cmd-params-cm
                   key: controller.diff.server.side
                   optional: true
    +        - name: ARGOCD_IGNORE_NORMALIZER_JQ_TIMEOUT
    +          valueFrom:
    +            configMapKeyRef:
    +              name: argocd-cmd-params-cm
    +              key: controller.ignore.normalizer.jq.timeout
    +              optional: true
             image: quay.io/argoproj/argocd:latest
             imagePullPolicy: Always
             name: argocd-application-controller
    
  • util/argo/diff/diff.go+10 2 modified
    @@ -11,6 +11,7 @@ import (
     	"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/argo/managedfields"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     
     	"github.com/argoproj/gitops-engine/pkg/diff"
    @@ -34,7 +35,7 @@ func NewDiffConfigBuilder() *DiffConfigBuilder {
     }
     
     // WithDiffSettings will set the diff settings in the builder.
    -func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDifferences, o map[string]v1alpha1.ResourceOverride, ignoreAggregatedRoles bool) *DiffConfigBuilder {
    +func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDifferences, o map[string]v1alpha1.ResourceOverride, ignoreAggregatedRoles bool, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) *DiffConfigBuilder {
     	ignores := id
     	if ignores == nil {
     		ignores = []v1alpha1.ResourceIgnoreDifferences{}
    @@ -47,6 +48,7 @@ func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDiffere
     	}
     	b.diffConfig.overrides = overrides
     	b.diffConfig.ignoreAggregatedRoles = ignoreAggregatedRoles
    +	b.diffConfig.ignoreNormalizerOpts = ignoreNormalizerOpts
     	return b
     }
     
    @@ -161,6 +163,8 @@ type DiffConfig interface {
     	ServerSideDiff() bool
     	ServerSideDryRunner() diff.ServerSideDryRunner
     	IgnoreMutationWebhook() bool
    +
    +	IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts
     }
     
     // diffConfig defines the configurations used while applying diffs.
    @@ -180,6 +184,7 @@ type diffConfig struct {
     	serverSideDiff        bool
     	serverSideDryRunner   diff.ServerSideDryRunner
     	ignoreMutationWebhook bool
    +	ignoreNormalizerOpts  normalizers.IgnoreNormalizerOpts
     }
     
     func (c *diffConfig) Ignores() []v1alpha1.ResourceIgnoreDifferences {
    @@ -227,6 +232,9 @@ func (c *diffConfig) ServerSideDiff() bool {
     func (c *diffConfig) IgnoreMutationWebhook() bool {
     	return c.ignoreMutationWebhook
     }
    +func (c *diffConfig) IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts {
    +	return c.ignoreNormalizerOpts
    +}
     
     // Validate will check the current state of this diffConfig and return
     // error if it finds any required configuration missing.
    @@ -279,7 +287,7 @@ func StateDiffs(lives, configs []*unstructured.Unstructured, diffConfig DiffConf
     		return nil, fmt.Errorf("failed to perform pre-diff normalization: %w", err)
     	}
     
    -	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides())
    +	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides(), diffConfig.IgnoreNormalizerOpts())
     	if err != nil {
     		return nil, fmt.Errorf("failed to create diff normalizer: %w", err)
     	}
    
  • util/argo/diff/diff_test.go+6 5 modified
    @@ -10,6 +10,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	testutil "github.com/argoproj/argo-cd/v2/test"
     	argo "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/argo/testdata"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     )
    @@ -40,7 +41,7 @@ func TestStateDiff(t *testing.T) {
     	diffConfig := func(t *testing.T, params *diffConfigParams) argo.DiffConfig {
     		t.Helper()
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(params.ignores, params.overrides, params.ignoreRoles).
    +			WithDiffSettings(params.ignores, params.overrides, params.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(params.label, params.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -185,7 +186,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -209,7 +210,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(nil, nil, f.ignoreRoles).
    +			WithDiffSettings(nil, nil, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -231,7 +232,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithCache(&appstatecache.Cache{}, "").
     			Build()
    @@ -246,7 +247,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithCache(nil, f.appName).
     			Build()
    
  • util/argo/diff/normalize.go+3 3 modified
    @@ -15,7 +15,7 @@ func Normalize(lives, configs []*unstructured.Unstructured, diffConfig DiffConfi
     	if err != nil {
     		return nil, err
     	}
    -	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides())
    +	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides(), diffConfig.IgnoreNormalizerOpts())
     	if err != nil {
     		return nil, err
     	}
    @@ -40,8 +40,8 @@ func Normalize(lives, configs []*unstructured.Unstructured, diffConfig DiffConfi
     }
     
     // newDiffNormalizer creates normalizer that uses Argo CD and application settings to normalize the resource prior to diffing.
    -func newDiffNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (diff.Normalizer, error) {
    -	ignoreNormalizer, err := normalizers.NewIgnoreNormalizer(ignore, overrides)
    +func newDiffNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (diff.Normalizer, error) {
    +	ignoreNormalizer, err := normalizers.NewIgnoreNormalizer(ignore, overrides, opts)
     	if err != nil {
     		return nil, err
     	}
    
  • util/argo/diff/normalize_test.go+2 1 modified
    @@ -10,6 +10,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	"github.com/argoproj/argo-cd/v2/test"
     	"github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/argo/testdata"
     )
     
    @@ -22,7 +23,7 @@ func TestNormalize(t *testing.T) {
     	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    
  • util/argo/normalizers/diff_normalizer.go+30 4 modified
    @@ -1,9 +1,11 @@
     package normalizers
     
     import (
    +	"context"
     	"encoding/json"
     	"fmt"
     	"strings"
    +	"time"
     
     	"github.com/argoproj/gitops-engine/pkg/diff"
     	jsonpatch "github.com/evanphx/json-patch"
    @@ -16,6 +18,11 @@ import (
     	"github.com/argoproj/argo-cd/v2/util/glob"
     )
     
    +const (
    +	// DefaultJQExecutionTimeout is the maximum time allowed for a JQ patch to execute
    +	DefaultJQExecutionTimeout = 1 * time.Second
    +)
    +
     type normalizerPatch interface {
     	GetGroupKind() schema.GroupKind
     	GetNamespace() string
    @@ -57,7 +64,8 @@ func (np *jsonPatchNormalizerPatch) Apply(data []byte) ([]byte, error) {
     
     type jqNormalizerPatch struct {
     	baseNormalizerPatch
    -	code *gojq.Code
    +	code               *gojq.Code
    +	jqExecutionTimeout time.Duration
     }
     
     func (np *jqNormalizerPatch) Apply(data []byte) ([]byte, error) {
    @@ -67,12 +75,18 @@ func (np *jqNormalizerPatch) Apply(data []byte) ([]byte, error) {
     		return nil, err
     	}
     
    -	iter := np.code.Run(dataJson)
    +	ctx, cancel := context.WithTimeout(context.Background(), np.jqExecutionTimeout)
    +	defer cancel()
    +
    +	iter := np.code.RunWithContext(ctx, dataJson)
     	first, ok := iter.Next()
     	if !ok {
     		return nil, fmt.Errorf("JQ patch did not return any data")
     	}
     	if err, ok = first.(error); ok {
    +		if err == context.DeadlineExceeded {
    +			return nil, fmt.Errorf("JQ patch execution timed out (%v)", np.jqExecutionTimeout.String())
    +		}
     		return nil, fmt.Errorf("JQ patch returned error: %w", err)
     	}
     	_, ok = iter.Next()
    @@ -91,8 +105,19 @@ type ignoreNormalizer struct {
     	patches []normalizerPatch
     }
     
    +type IgnoreNormalizerOpts struct {
    +	JQExecutionTimeout time.Duration
    +}
    +
    +func (opts *IgnoreNormalizerOpts) getJQExecutionTimeout() time.Duration {
    +	if opts == nil || opts.JQExecutionTimeout == 0 {
    +		return DefaultJQExecutionTimeout
    +	}
    +	return opts.JQExecutionTimeout
    +}
    +
     // NewIgnoreNormalizer creates diff normalizer which removes ignored fields according to given application spec and resource overrides
    -func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (diff.Normalizer, error) {
    +func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts IgnoreNormalizerOpts) (diff.Normalizer, error) {
     	for key, override := range overrides {
     		group, kind, err := getGroupKindForOverrideKey(key)
     		if err != nil {
    @@ -147,7 +172,8 @@ func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides
     					name:      ignore[i].Name,
     					namespace: ignore[i].Namespace,
     				},
    -				code: jqDeletionCode,
    +				code:               jqDeletionCode,
    +				jqExecutionTimeout: opts.getJQExecutionTimeout(),
     			})
     		}
     	}
    
  • util/argo/normalizers/diff_normalizer_test.go+32 10 modified
    @@ -19,7 +19,7 @@ func TestNormalizeObjectWithMatchedGroupKind(t *testing.T) {
     		Group:        "apps",
     		Kind:         "Deployment",
     		JSONPointers: []string{"/not-matching-path", "/spec/template/spec/containers"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -44,7 +44,7 @@ func TestNormalizeNoMatchedGroupKinds(t *testing.T) {
     		Group:        "",
     		Kind:         "Service",
     		JSONPointers: []string{"/spec"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -63,7 +63,7 @@ func TestNormalizeMatchedResourceOverrides(t *testing.T) {
     		"apps/Deployment": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -118,7 +118,7 @@ func TestNormalizeMissingJsonPointer(t *testing.T) {
     		"apiextensions.k8s.io/CustomResourceDefinition": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/additionalPrinterColumns/0/priority"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     	assert.NoError(t, err)
     
     	deployment := test.NewDeployment()
    @@ -139,7 +139,7 @@ func TestNormalizeGlobMatch(t *testing.T) {
     		"*/*": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -161,7 +161,7 @@ func TestNormalizeJQPathExpression(t *testing.T) {
     		Group:             "apps",
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.template.spec.initContainers[] | select(.name == \"init-container-0\")"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -197,7 +197,7 @@ func TestNormalizeIllegalJQPathExpression(t *testing.T) {
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"missing-quote)"},
     		// JSONPointers: []string{"no-starting-slash"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Error(t, err)
     }
    @@ -207,7 +207,7 @@ func TestNormalizeJQPathExpressionWithError(t *testing.T) {
     		Group:             "apps",
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.fakeField.foo[]"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -230,7 +230,7 @@ func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
     				JSONPointers: []string{"/invalid", "/invalid/json/path"},
     			},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     	assert.Nil(t, err)
     
     	ignoreNormalizer := normalizer.(*ignoreNormalizer)
    @@ -254,12 +254,34 @@ func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
     
     }
     
    +func TestJqPathExpressionFailWithTimeout(t *testing.T) {
    +	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
    +		"*/*": {
    +			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
    +				JQPathExpressions: []string{"until(true==false; [.] + [1])"},
    +			},
    +		},
    +	}, IgnoreNormalizerOpts{})
    +	assert.Nil(t, err)
    +
    +	ignoreNormalizer := normalizer.(*ignoreNormalizer)
    +	assert.Len(t, ignoreNormalizer.patches, 1)
    +	jqPatch := ignoreNormalizer.patches[0]
    +
    +	deployment := test.NewDeployment()
    +	deploymentData, err := json.Marshal(deployment)
    +	assert.Nil(t, err)
    +
    +	_, err = jqPatch.Apply(deploymentData)
    +	assert.ErrorContains(t, err, "JQ patch execution timed out")
    +}
    +
     func TestJQPathExpressionReturnsHelpfulError(t *testing.T) {
     	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
     		Kind: "ConfigMap",
     		// This is a really wild expression, but it does trigger the desired error.
     		JQPathExpressions: []string{`.nothing) | .data["config.yaml"] |= (fromjson | del(.auth) | tojson`},
    -	}}, nil)
    +	}}, nil, IgnoreNormalizerOpts{})
     
     	assert.NoError(t, err)
     
    
7893979a1e78

Merge pull request from GHSA-9m6p-x4h2-6frq

https://github.com/argoproj/argo-cdpasha-codefreshApr 26, 2024via ghsa
22 files changed · +161 46
  • applicationset/controllers/applicationset_controller.go+2 1 modified
    @@ -42,6 +42,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/applicationset/generators"
     	"github.com/argoproj/argo-cd/v2/applicationset/utils"
     	"github.com/argoproj/argo-cd/v2/common"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/glob"
     
    @@ -609,7 +610,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
     			},
     		}
     
    -		action, err := utils.CreateOrUpdate(ctx, r.Client, found, func() error {
    +		action, err := utils.CreateOrUpdate(ctx, r.Client, found, normalizers.IgnoreNormalizerOpts{}, func() error {
     			// Copy only the Application/ObjectMeta fields that are significant, from the generatedApp
     			found.Spec = generatedApp.Spec
     
    
  • applicationset/utils/createOrUpdate.go+3 2 modified
    @@ -14,6 +14,7 @@ import (
     	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
     
     	argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     // CreateOrUpdate overrides "sigs.k8s.io/controller-runtime" function
    @@ -29,7 +30,7 @@ import (
     // The MutateFn is called regardless of creating or updating an object.
     //
     // It returns the executed operation and an error.
    -func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
    +func CreateOrUpdate(ctx context.Context, c client.Client, obj client.Object, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
     
     	key := client.ObjectKeyFromObject(obj)
     	if err := c.Get(ctx, key, obj); err != nil {
    @@ -94,4 +95,4 @@ func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object)
     		return fmt.Errorf("MutateFn cannot mutate object name and/or object namespace")
     	}
     	return nil
    -}
    +}
    \ No newline at end of file
    
  • cmd/argocd-application-controller/commands/argocd_application_controller.go+4 0 modified
    @@ -20,6 +20,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/cli"
    @@ -64,6 +65,7 @@ func NewCommand() *cobra.Command {
     		applicationNamespaces    []string
     		persistResourceHealth    bool
     		shardingAlgorithm        string
    +		ignoreNormalizerOpts             normalizers.IgnoreNormalizerOpts
     	)
     	var command = cobra.Command{
     		Use:               cliName,
    @@ -155,6 +157,7 @@ func NewCommand() *cobra.Command {
     				persistResourceHealth,
     				clusterFilter,
     				applicationNamespaces,
    +				ignoreNormalizerOpts,
     			)
     			errors.CheckError(err)
     			cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
    @@ -199,6 +202,7 @@ func NewCommand() *cobra.Command {
     	command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("ARGOCD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces that applications are allowed to be reconciled from")
     	command.Flags().BoolVar(&persistResourceHealth, "persist-resource-health", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_PERSIST_RESOURCE_HEALTH", true), "Enables storing the managed resources health in the Application CRD")
     	command.Flags().StringVar(&shardingAlgorithm, "sharding-method", env.StringFromEnv(common.EnvControllerShardingAlgorithm, common.DefaultShardingAlgorithm), "Enables choice of sharding method. Supported sharding methods are : [legacy, round-robin] ")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "", env.ParseDurationFromEnv("ARGOCD_IGNORE_NORMALIZER_JQ_TIMEOUT", 0*time.Second, 0, math.MaxInt64), "Set ignore normalizer JQ execution timeout")
     	cacheSrc = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
     		redisClient = client
     	})
    
  • cmd/argocd/commands/admin/app.go+6 3 modified
    @@ -28,6 +28,7 @@ import (
     	appinformers "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions"
     	argocdclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/cli"
    @@ -231,6 +232,7 @@ func NewReconcileCommand() *cobra.Command {
     		repoServerAddress string
     		outputFormat      string
     		refresh           bool
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     	)
     
     	var command = &cobra.Command{
    @@ -267,7 +269,7 @@ func NewReconcileCommand() *cobra.Command {
     
     				appClientset := appclientset.NewForConfigOrDie(cfg)
     				kubeClientset := kubernetes.NewForConfigOrDie(cfg)
    -				result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache)
    +				result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache, ignoreNormalizerOpts)
     				errors.CheckError(err)
     			} else {
     				appClientset := appclientset.NewForConfigOrDie(cfg)
    @@ -282,7 +284,7 @@ func NewReconcileCommand() *cobra.Command {
     	command.Flags().StringVar(&selector, "l", "", "Label selector")
     	command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)")
     	command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation")
    -
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    @@ -331,6 +333,7 @@ func reconcileApplications(
     	repoServerClient argocdclient.Clientset,
     	selector string,
     	createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) ([]appReconcileResult, error) {
     	settingsMgr := settings.NewSettingsManager(ctx, kubeClientset, namespace)
     	argoDB := db.NewDB(namespace, settingsMgr, kubeClientset)
    @@ -371,7 +374,7 @@ func reconcileApplications(
     	)
     
     	appStateManager := controller.NewAppStateManager(
    -		argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false)
    +		argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false, ignoreNormalizerOpts)
     
     	appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, v1.ListOptions{LabelSelector: selector})
     	if err != nil {
    
  • cmd/argocd/commands/admin/app_test.go+2 0 modified
    @@ -23,6 +23,7 @@ import (
     	argocdclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
     	"github.com/argoproj/argo-cd/v2/test"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/settings"
     )
    @@ -113,6 +114,7 @@ func TestGetReconcileResults_Refresh(t *testing.T) {
     		func(argoDB db.ArgoDB, appInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) statecache.LiveStateCache {
     			return &liveStateCache
     		},
    +		normalizers.IgnoreNormalizerOpts{},
     	)
     
     	if !assert.NoError(t, err) {
    
  • cmd/argocd/commands/admin/settings.go+6 2 modified
    @@ -432,7 +432,7 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
     				// configurations. This requires access to live resources which is not the
     				// purpose of this command. This will just apply jsonPointers and
     				// jqPathExpressions configurations.
    -				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
    +				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, normalizers.IgnoreNormalizerOpts{})
     				errors.CheckError(err)
     
     				normalizedRes := res.DeepCopy()
    @@ -457,6 +457,9 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
     }
     
     func NewResourceIgnoreResourceUpdatesCommand(cmdCtx commandContext) *cobra.Command {
    +	var (
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
    +	)
     	var command = &cobra.Command{
     		Use:   "ignore-resource-updates RESOURCE_YAML_PATH",
     		Short: "Renders fields excluded from resource updates",
    @@ -478,7 +481,7 @@ argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml -
     					return
     				}
     
    -				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
    +				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, ignoreNormalizerOpts)
     				errors.CheckError(err)
     
     				normalizedRes := res.DeepCopy()
    @@ -499,6 +502,7 @@ argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml -
     			})
     		},
     	}
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    
  • cmd/argocd/commands/app.go+9 4 modified
    @@ -44,6 +44,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/repository"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/cli"
     	"github.com/argoproj/argo-cd/v2/util/errors"
     	"github.com/argoproj/argo-cd/v2/util/git"
    @@ -925,6 +926,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     		localRepoRoot      string
     		serverSideGenerate bool
     		localIncludes      []string
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     	)
     	shortDesc := "Perform a diff against the target and live state."
     	var command = &cobra.Command{
    @@ -989,7 +991,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     					diffOption.cluster = cluster
     				}
     			}
    -			foundDiffs := findandPrintDiff(ctx, app, resources, argoSettings, diffOption)
    +			foundDiffs := findandPrintDiff(ctx, app, resources, argoSettings, diffOption, ignoreNormalizerOpts)
     			if foundDiffs && exitCode {
     				os.Exit(1)
     			}
    @@ -1003,6 +1005,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     	command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
     	command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
     	command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    @@ -1017,7 +1020,7 @@ type DifferenceOption struct {
     }
     
     // findandPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
    -func findandPrintDiff(ctx context.Context, app *argoappv1.Application, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) bool {
    +func findandPrintDiff(ctx context.Context, app *argoappv1.Application, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) bool {
     	var foundDiffs bool
     	liveObjs, err := cmdutil.LiveObjects(resources.Items)
     	errors.CheckError(err)
    @@ -1072,7 +1075,7 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, resources
     		// compareOptions in the protobuf
     		ignoreAggregatedRoles := false
     		diffConfig, err := argodiff.NewDiffConfigBuilder().
    -			WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles).
    +			WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles, ignoreNormalizerOpts).
     			WithTracking(argoSettings.AppLabelKey, argoSettings.TrackingMethod).
     			WithNoCache().
     			Build()
    @@ -1543,6 +1546,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     		diffChanges             bool
     		diffChangesConfirm      bool
     		projects                []string
    +		ignoreNormalizerOpts    normalizers.IgnoreNormalizerOpts
     	)
     	var command = &cobra.Command{
     		Use:   "sync [APPNAME... | -l selector | --project project-name]",
    @@ -1764,7 +1768,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     					foundDiffs := false
     					fmt.Printf("====== Previewing differences between live and desired state of application %s ======\n", appQualifiedName)
     
    -					foundDiffs = findandPrintDiff(ctx, app, resources, argoSettings, diffOption)
    +					foundDiffs = findandPrintDiff(ctx, app, resources, argoSettings, diffOption, ignoreNormalizerOpts)
     					if foundDiffs {
     						if !diffChangesConfirm {
     							yesno := cli.AskToProceed(fmt.Sprintf("Please review changes to application %s shown above. Do you want to continue the sync process? (y/n): ", appQualifiedName))
    @@ -1820,6 +1824,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     	command.Flags().BoolVar(&diffChangesConfirm, "assumeYes", false, "Assume yes as answer for all user queries or prompts")
     	command.Flags().BoolVar(&diffChanges, "preview-changes", false, "Preview difference against the target and live state before syncing app and wait for user confirmation")
     	command.Flags().StringArrayVar(&projects, "project", []string{}, "Sync apps that belong to the specified projects. This option may be specified repeatedly.")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    
  • controller/appcontroller.go+6 2 modified
    @@ -51,6 +51,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/db"
    @@ -120,6 +121,7 @@ type ApplicationController struct {
     	clusterFilter                 func(cluster *appv1.Cluster) bool
     	projByNameCache               sync.Map
     	applicationNamespaces         []string
    +	ignoreNormalizerOpts          normalizers.IgnoreNormalizerOpts
     }
     
     // NewApplicationController creates new instance of ApplicationController.
    @@ -141,6 +143,7 @@ func NewApplicationController(
     	persistResourceHealth bool,
     	clusterFilter func(cluster *appv1.Cluster) bool,
     	applicationNamespaces []string,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) (*ApplicationController, error) {
     	log.Infof("appResyncPeriod=%v, appHardResyncPeriod=%v", appResyncPeriod, appHardResyncPeriod)
     	db := db.NewDB(namespace, settingsMgr, kubeClientset)
    @@ -166,6 +169,7 @@ func NewApplicationController(
     		clusterFilter:                 clusterFilter,
     		projByNameCache:               sync.Map{},
     		applicationNamespaces:         applicationNamespaces,
    +		ignoreNormalizerOpts:              ignoreNormalizerOpts,
     	}
     	if kubectlParallelismLimit > 0 {
     		ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
    @@ -216,7 +220,7 @@ func NewApplicationController(
     		}
     	}
     	stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated, clusterFilter, argo.NewResourceTracking())
    -	appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth)
    +	appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth, ignoreNormalizerOpts)
     	ctrl.appInformer = appInformer
     	ctrl.appLister = appLister
     	ctrl.projInformer = projInformer
    @@ -666,7 +670,7 @@ func (ctrl *ApplicationController) hideSecretData(app *appv1.Application, compar
     				return nil, fmt.Errorf("error getting cluster cache: %s", err)
     			}
     			diffConfig, err := argodiff.NewDiffConfigBuilder().
    -				WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
    +				WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles, ctrl.ignoreNormalizerOpts).
     				WithTracking(appLabelKey, trackingMethod).
     				WithNoCache().
     				WithLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig())).
    
  • controller/appcontroller_test.go+2 0 modified
    @@ -38,6 +38,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	mockrepoclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
     	"github.com/argoproj/argo-cd/v2/test"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/settings"
    @@ -123,6 +124,7 @@ func newFakeController(data *fakeData) *ApplicationController {
     		true,
     		nil,
     		data.applicationNamespaces,
    +		normalizers.IgnoreNormalizerOpts{},
     	)
     	if err != nil {
     		panic(err)
    
  • controller/cache/cache.go+3 1 modified
    @@ -32,6 +32,7 @@ import (
     	"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/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/env"
     	logutils "github.com/argoproj/argo-cd/v2/util/log"
    @@ -197,6 +198,7 @@ type liveStateCache struct {
     	metricsServer    *metrics.MetricsServer
     	clusterFilter    func(cluster *appv1.Cluster) bool
     	resourceTracking argo.ResourceTracking
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     
     	clusters      map[string]clustercache.ClusterCache
     	cacheSettings cacheSettings
    @@ -473,7 +475,7 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
     			gvk := un.GroupVersionKind()
     
     			if cacheSettings.ignoreResourceUpdatesEnabled && shouldHashManifest(appName, gvk) {
    -				hash, err := generateManifestHash(un, nil, cacheSettings.resourceOverrides)
    +				hash, err := generateManifestHash(un, nil, cacheSettings.resourceOverrides, c.ignoreNormalizerOpts)
     				if err != nil {
     					log.Errorf("Failed to generate manifest hash: %v", err)
     				} else {
    
  • controller/cache/info.go+2 2 modified
    @@ -390,8 +390,8 @@ func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
     	}
     }
     
    -func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (string, error) {
    -	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides)
    +func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (string, error) {
    +	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides, opts)
     	if err != nil {
     		return "", fmt.Errorf("error creating normalizer: %w", err)
     	}
    
  • controller/cache/info_test.go+2 1 modified
    @@ -16,6 +16,7 @@ import (
     	"sigs.k8s.io/yaml"
     
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func strToUnstructured(jsonStr string) *unstructured.Unstructured {
    @@ -749,7 +750,7 @@ func TestManifestHash(t *testing.T) {
     
     	expected := hash(data)
     
    -	hash, err := generateManifestHash(manifest, ignores, nil)
    +	hash, err := generateManifestHash(manifest, ignores, nil, normalizers.IgnoreNormalizerOpts{})
     	assert.Equal(t, expected, hash)
     	assert.Nil(t, err)
     }
    
  • controller/state.go+7 2 modified
    @@ -4,11 +4,12 @@ import (
     	"context"
     	"encoding/json"
     	"fmt"
    -	v1 "k8s.io/api/core/v1"
     	"reflect"
     	"strings"
     	"time"
     
    +	v1 "k8s.io/api/core/v1"
    +	
     	"github.com/argoproj/gitops-engine/pkg/diff"
     	"github.com/argoproj/gitops-engine/pkg/health"
     	"github.com/argoproj/gitops-engine/pkg/sync"
    @@ -32,6 +33,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/gpg"
    @@ -105,6 +107,7 @@ type appStateManager struct {
     	statusRefreshTimeout  time.Duration
     	resourceTracking      argo.ResourceTracking
     	persistResourceHealth bool
    +	ignoreNormalizerOpts  normalizers.IgnoreNormalizerOpts
     }
     
     func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) {
    @@ -564,7 +567,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
     	noCache = noCache || refreshRequested || app.Status.Expired(m.statusRefreshTimeout) || specChanged || revisionChanged
     
     	diffConfigBuilder := argodiff.NewDiffConfigBuilder().
    -		WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
    +		WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles, m.ignoreNormalizerOpts).
     		WithTracking(appLabelKey, string(trackingMethod))
     
     	if noCache {
    @@ -830,6 +833,7 @@ func NewAppStateManager(
     	statusRefreshTimeout time.Duration,
     	resourceTracking argo.ResourceTracking,
     	persistResourceHealth bool,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) AppStateManager {
     	return &appStateManager{
     		liveStateCache:        liveStateCache,
    @@ -845,6 +849,7 @@ func NewAppStateManager(
     		statusRefreshTimeout:  statusRefreshTimeout,
     		resourceTracking:      resourceTracking,
     		persistResourceHealth: persistResourceHealth,
    +		ignoreNormalizerOpts:  ignoreNormalizerOpts,
     	}
     }
     
    
  • controller/sync_test.go+3 2 modified
    @@ -19,6 +19,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/test"
     	"github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func TestPersistRevisionHistory(t *testing.T) {
    @@ -263,7 +264,7 @@ func TestNormalizeTargetResources(t *testing.T) {
     	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    @@ -396,7 +397,7 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
     	setupHttpProxy := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    
  • docs/user-guide/diffing.md+13 0 modified
    @@ -182,3 +182,16 @@ data:
     ```
     
     The list of supported Kubernetes types is available in [diffing_known_types.txt](https://raw.githubusercontent.com/argoproj/argo-cd/master/util/argo/normalizers/diffing_known_types.txt)
    +
    +
    +### JQ Path expression timeout
    +
    +By default, the evaluation of a JQPathExpression is limited to one second. If you encounter a "JQ patch execution timed out" error message due to a complex JQPathExpression that requires more time to evaluate, you can extend the timeout period by configuring the `ignore.normalizer.jq.timeout` setting within the `argocd-cmd-params-cm` ConfigMap.
    +
    +```yaml
    +apiVersion: v1
    +kind: ConfigMap
    +metadata:
    +  name: argocd-cmd-params-cm
    +data:
    +  ignore.normalizer.jq.timeout: "5s"
    
  • manifests/base/application-controller/argocd-application-controller-statefulset.yaml+6 0 modified
    @@ -155,6 +155,12 @@ spec:
                     name: argocd-cmd-params-cm
                     key: controller.kubectl.parallelism.limit
                     optional: true
    +        - name: ARGOCD_IGNORE_NORMALIZER_JQ_TIMEOUT
    +          valueFrom:
    +              configMapKeyRef:
    +                name: argocd-cmd-params-cm
    +                key: controller.ignore.normalizer.jq.timeout
    +                optional: true
             image: quay.io/argoproj/argocd:latest
             imagePullPolicy: Always
             name: argocd-application-controller
    
  • util/argo/diff/diff.go+10 2 modified
    @@ -10,6 +10,7 @@ import (
     	"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/argo/managedfields"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     
     	"github.com/argoproj/gitops-engine/pkg/diff"
    @@ -31,7 +32,7 @@ func NewDiffConfigBuilder() *DiffConfigBuilder {
     }
     
     // WithDiffSettings will set the diff settings in the builder.
    -func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDifferences, o map[string]v1alpha1.ResourceOverride, ignoreAggregatedRoles bool) *DiffConfigBuilder {
    +func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDifferences, o map[string]v1alpha1.ResourceOverride, ignoreAggregatedRoles bool, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) *DiffConfigBuilder {
     	ignores := id
     	if ignores == nil {
     		ignores = []v1alpha1.ResourceIgnoreDifferences{}
    @@ -44,6 +45,7 @@ func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDiffere
     	}
     	b.diffConfig.overrides = overrides
     	b.diffConfig.ignoreAggregatedRoles = ignoreAggregatedRoles
    +	b.diffConfig.ignoreNormalizerOpts = ignoreNormalizerOpts
     	return b
     }
     
    @@ -140,6 +142,8 @@ type DiffConfig interface {
     	// Manager returns the manager that should be used by the diff while
     	// calculating the structured merge diff.
     	Manager() string
    +
    +	IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts
     }
     
     // diffConfig defines the configurations used while applying diffs.
    @@ -156,6 +160,7 @@ type diffConfig struct {
     	gvkParser             *k8smanagedfields.GvkParser
     	structuredMergeDiff   bool
     	manager               string
    +	ignoreNormalizerOpts  normalizers.IgnoreNormalizerOpts
     }
     
     func (c *diffConfig) Ignores() []v1alpha1.ResourceIgnoreDifferences {
    @@ -194,6 +199,9 @@ func (c *diffConfig) StructuredMergeDiff() bool {
     func (c *diffConfig) Manager() string {
     	return c.manager
     }
    +func (c *diffConfig) IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts {
    +	return c.ignoreNormalizerOpts
    +}
     
     // Validate will check the current state of this diffConfig and return
     // error if it finds any required configuration missing.
    @@ -243,7 +251,7 @@ func StateDiffs(lives, configs []*unstructured.Unstructured, diffConfig DiffConf
     		return nil, fmt.Errorf("failed to perform pre-diff normalization: %w", err)
     	}
     
    -	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides())
    +	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides(), diffConfig.IgnoreNormalizerOpts())
     	if err != nil {
     		return nil, fmt.Errorf("failed to create diff normalizer: %w", err)
     	}
    
  • util/argo/diff/diff_test.go+6 5 modified
    @@ -10,6 +10,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	testutil "github.com/argoproj/argo-cd/v2/test"
     	argo "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/argo/testdata"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     )
    @@ -40,7 +41,7 @@ func TestStateDiff(t *testing.T) {
     	diffConfig := func(t *testing.T, params *diffConfigParams) argo.DiffConfig {
     		t.Helper()
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(params.ignores, params.overrides, params.ignoreRoles).
    +			WithDiffSettings(params.ignores, params.overrides, params.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(params.label, params.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -185,7 +186,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -209,7 +210,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(nil, nil, f.ignoreRoles).
    +			WithDiffSettings(nil, nil, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -231,7 +232,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithCache(&appstatecache.Cache{}, "").
     			Build()
    @@ -246,7 +247,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithCache(nil, f.appName).
     			Build()
    
  • util/argo/diff/normalize.go+3 3 modified
    @@ -15,7 +15,7 @@ func Normalize(lives, configs []*unstructured.Unstructured, diffConfig DiffConfi
     	if err != nil {
     		return nil, err
     	}
    -	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides())
    +	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides(), diffConfig.IgnoreNormalizerOpts())
     	if err != nil {
     		return nil, err
     	}
    @@ -40,8 +40,8 @@ func Normalize(lives, configs []*unstructured.Unstructured, diffConfig DiffConfi
     }
     
     // newDiffNormalizer creates normalizer that uses Argo CD and application settings to normalize the resource prior to diffing.
    -func newDiffNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (diff.Normalizer, error) {
    -	ignoreNormalizer, err := normalizers.NewIgnoreNormalizer(ignore, overrides)
    +func newDiffNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (diff.Normalizer, error) {
    +	ignoreNormalizer, err := normalizers.NewIgnoreNormalizer(ignore, overrides, opts)
     	if err != nil {
     		return nil, err
     	}
    
  • util/argo/diff/normalize_test.go+2 1 modified
    @@ -10,6 +10,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	"github.com/argoproj/argo-cd/v2/test"
     	"github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/argo/testdata"
     )
     
    @@ -22,7 +23,7 @@ func TestNormalize(t *testing.T) {
     	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    
  • util/argo/normalizers/diff_normalizer.go+33 4 modified
    @@ -1,9 +1,11 @@
     package normalizers
     
     import (
    +	"context"
     	"encoding/json"
     	"fmt"
     	"strings"
    +	"time"
     
     	"github.com/argoproj/gitops-engine/pkg/diff"
     	jsonpatch "github.com/evanphx/json-patch"
    @@ -16,6 +18,11 @@ import (
     	"github.com/argoproj/argo-cd/v2/util/glob"
     )
     
    +const (
    +	// DefaultJQExecutionTimeout is the maximum time allowed for a JQ patch to execute
    +	DefaultJQExecutionTimeout = 1 * time.Second
    +)
    +
     type normalizerPatch interface {
     	GetGroupKind() schema.GroupKind
     	GetNamespace() string
    @@ -57,7 +64,8 @@ func (np *jsonPatchNormalizerPatch) Apply(data []byte) ([]byte, error) {
     
     type jqNormalizerPatch struct {
     	baseNormalizerPatch
    -	code *gojq.Code
    +	code               *gojq.Code
    +	jqExecutionTimeout time.Duration
     }
     
     func (np *jqNormalizerPatch) Apply(data []byte) ([]byte, error) {
    @@ -67,11 +75,20 @@ func (np *jqNormalizerPatch) Apply(data []byte) ([]byte, error) {
     		return nil, err
     	}
     
    -	iter := np.code.Run(dataJson)
    +	ctx, cancel := context.WithTimeout(context.Background(), np.jqExecutionTimeout)
    +	defer cancel()
    +
    +	iter := np.code.RunWithContext(ctx, dataJson)
     	first, ok := iter.Next()
     	if !ok {
     		return nil, fmt.Errorf("JQ patch did not return any data")
     	}
    +	if err, ok = first.(error); ok {
    +		if err == context.DeadlineExceeded {
    +			return nil, fmt.Errorf("JQ patch execution timed out (%v)", np.jqExecutionTimeout.String())
    +		}
    +		return nil, fmt.Errorf("JQ patch returned error: %w", err)
    +	}
     	_, ok = iter.Next()
     	if ok {
     		return nil, fmt.Errorf("JQ patch returned multiple objects")
    @@ -88,8 +105,19 @@ type ignoreNormalizer struct {
     	patches []normalizerPatch
     }
     
    +type IgnoreNormalizerOpts struct {
    +	JQExecutionTimeout time.Duration
    +}
    +
    +func (opts *IgnoreNormalizerOpts) getJQExecutionTimeout() time.Duration {
    +	if opts == nil || opts.JQExecutionTimeout == 0 {
    +		return DefaultJQExecutionTimeout
    +	}
    +	return opts.JQExecutionTimeout
    +}
    +
     // NewIgnoreNormalizer creates diff normalizer which removes ignored fields according to given application spec and resource overrides
    -func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (diff.Normalizer, error) {
    +func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts IgnoreNormalizerOpts) (diff.Normalizer, error) {
     	for key, override := range overrides {
     		group, kind, err := getGroupKindForOverrideKey(key)
     		if err != nil {
    @@ -144,7 +172,8 @@ func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides
     					name:      ignore[i].Name,
     					namespace: ignore[i].Namespace,
     				},
    -				code: jqDeletionCode,
    +				code:               jqDeletionCode,
    +				jqExecutionTimeout: opts.getJQExecutionTimeout(),
     			})
     		}
     	}
    
  • util/argo/normalizers/diff_normalizer_test.go+31 9 modified
    @@ -18,7 +18,7 @@ func TestNormalizeObjectWithMatchedGroupKind(t *testing.T) {
     		Group:        "apps",
     		Kind:         "Deployment",
     		JSONPointers: []string{"/not-matching-path", "/spec/template/spec/containers"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -43,7 +43,7 @@ func TestNormalizeNoMatchedGroupKinds(t *testing.T) {
     		Group:        "",
     		Kind:         "Service",
     		JSONPointers: []string{"/spec"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -62,7 +62,7 @@ func TestNormalizeMatchedResourceOverrides(t *testing.T) {
     		"apps/Deployment": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -117,7 +117,7 @@ func TestNormalizeMissingJsonPointer(t *testing.T) {
     		"apiextensions.k8s.io/CustomResourceDefinition": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/additionalPrinterColumns/0/priority"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     	assert.NoError(t, err)
     
     	deployment := test.NewDeployment()
    @@ -138,7 +138,7 @@ func TestNormalizeGlobMatch(t *testing.T) {
     		"*/*": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -160,7 +160,7 @@ func TestNormalizeJQPathExpression(t *testing.T) {
     		Group:             "apps",
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.template.spec.initContainers[] | select(.name == \"init-container-0\")"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -196,7 +196,7 @@ func TestNormalizeIllegalJQPathExpression(t *testing.T) {
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"missing-quote)"},
     		// JSONPointers: []string{"no-starting-slash"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Error(t, err)
     }
    @@ -206,7 +206,7 @@ func TestNormalizeJQPathExpressionWithError(t *testing.T) {
     		Group:             "apps",
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.fakeField.foo[]"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -229,7 +229,7 @@ func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
     				JSONPointers: []string{"/invalid", "/invalid/json/path"},
     			},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     	assert.Nil(t, err)
     
     	ignoreNormalizer := normalizer.(*ignoreNormalizer)
    @@ -252,3 +252,25 @@ func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
     	assert.True(t, shouldLogError(fmt.Errorf("An error that should not be ignored")))
     
     }
    +
    +func TestJqPathExpressionFailWithTimeout(t *testing.T) {
    +	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
    +		"*/*": {
    +			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
    +				JQPathExpressions: []string{"until(true==false; [.] + [1])"},
    +			},
    +		},
    +	}, IgnoreNormalizerOpts{})
    +	assert.Nil(t, err)
    +
    +	ignoreNormalizer := normalizer.(*ignoreNormalizer)
    +	assert.Len(t, ignoreNormalizer.patches, 1)
    +	jqPatch := ignoreNormalizer.patches[0]
    +
    +	deployment := test.NewDeployment()
    +	deploymentData, err := json.Marshal(deployment)
    +	assert.Nil(t, err)
    +
    +	_, err = jqPatch.Apply(deploymentData)
    +	assert.ErrorContains(t, err, "JQ patch execution timed out")
    +}
    \ No newline at end of file
    
e2df7315fb7d

Merge pull request from GHSA-9m6p-x4h2-6frq

https://github.com/argoproj/argo-cdpasha-codefreshApr 26, 2024via ghsa
23 files changed · +186 70
  • applicationset/controllers/applicationset_controller.go+2 1 modified
    @@ -50,6 +50,7 @@ import (
     	argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
     	argoutil "github.com/argoproj/argo-cd/v2/util/argo"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application"
     )
    @@ -623,7 +624,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
     			},
     		}
     
    -		action, err := utils.CreateOrUpdate(ctx, appLog, r.Client, applicationSet.Spec.IgnoreApplicationDifferences, found, func() error {
    +		action, err := utils.CreateOrUpdate(ctx, appLog, r.Client, applicationSet.Spec.IgnoreApplicationDifferences, normalizers.IgnoreNormalizerOpts{}, found, func() error {
     			// Copy only the Application/ObjectMeta fields that are significant, from the generatedApp
     			found.Spec = generatedApp.Spec
     
    
  • applicationset/utils/createOrUpdate.go+5 4 modified
    @@ -20,6 +20,7 @@ import (
     	argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     // CreateOrUpdate overrides "sigs.k8s.io/controller-runtime" function
    @@ -35,7 +36,7 @@ import (
     // The MutateFn is called regardless of creating or updating an object.
     //
     // It returns the executed operation and an error.
    -func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ignoreAppDifferences argov1alpha1.ApplicationSetIgnoreDifferences, obj *argov1alpha1.Application, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
    +func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ignoreAppDifferences argov1alpha1.ApplicationSetIgnoreDifferences, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, obj *argov1alpha1.Application, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
     
     	key := client.ObjectKeyFromObject(obj)
     	if err := c.Get(ctx, key, obj); err != nil {
    @@ -60,7 +61,7 @@ func CreateOrUpdate(ctx context.Context, logCtx *log.Entry, c client.Client, ign
     
     	// Apply ignoreApplicationDifferences rules to remove ignored fields from both the live and the desired state. This
     	// prevents those differences from appearing in the diff and therefore in the patch.
    -	err := applyIgnoreDifferences(ignoreAppDifferences, normalizedLive, obj)
    +	err := applyIgnoreDifferences(ignoreAppDifferences, normalizedLive, obj, ignoreNormalizerOpts)
     	if err != nil {
     		return controllerutil.OperationResultNone, fmt.Errorf("failed to apply ignore differences: %w", err)
     	}
    @@ -134,14 +135,14 @@ func mutate(f controllerutil.MutateFn, key client.ObjectKey, obj client.Object)
     }
     
     // applyIgnoreDifferences applies the ignore differences rules to the found application. It modifies the applications in place.
    -func applyIgnoreDifferences(applicationSetIgnoreDifferences argov1alpha1.ApplicationSetIgnoreDifferences, found *argov1alpha1.Application, generatedApp *argov1alpha1.Application) error {
    +func applyIgnoreDifferences(applicationSetIgnoreDifferences argov1alpha1.ApplicationSetIgnoreDifferences, found *argov1alpha1.Application, generatedApp *argov1alpha1.Application, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) error {
     	if len(applicationSetIgnoreDifferences) == 0 {
     		return nil
     	}
     
     	generatedAppCopy := generatedApp.DeepCopy()
     	diffConfig, err := argodiff.NewDiffConfigBuilder().
    -		WithDiffSettings(applicationSetIgnoreDifferences.ToApplicationIgnoreDifferences(), nil, false).
    +		WithDiffSettings(applicationSetIgnoreDifferences.ToApplicationIgnoreDifferences(), nil, false, ignoreNormalizerOpts).
     		WithNoCache().
     		Build()
     	if err != nil {
    
  • applicationset/utils/createOrUpdate_test.go+2 1 modified
    @@ -9,6 +9,7 @@ import (
     	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
     
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func Test_applyIgnoreDifferences(t *testing.T) {
    @@ -222,7 +223,7 @@ spec:
     			generatedApp := v1alpha1.Application{TypeMeta: appMeta}
     			err = yaml.Unmarshal([]byte(tc.generatedApp), &generatedApp)
     			require.NoError(t, err, tc.generatedApp)
    -			err = applyIgnoreDifferences(tc.ignoreDifferences, &foundApp, &generatedApp)
    +			err = applyIgnoreDifferences(tc.ignoreDifferences, &foundApp, &generatedApp, normalizers.IgnoreNormalizerOpts{})
     			require.NoError(t, err)
     			yamlFound, err := yaml.Marshal(tc.foundApp)
     			require.NoError(t, err)
    
  • cmd/argocd-application-controller/commands/argocd_application_controller.go+4 0 modified
    @@ -20,6 +20,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/cli"
    @@ -68,6 +69,7 @@ func NewCommand() *cobra.Command {
     		persistResourceHealth            bool
     		shardingAlgorithm                string
     		enableDynamicClusterDistribution bool
    +		ignoreNormalizerOpts             normalizers.IgnoreNormalizerOpts
     	)
     	var command = cobra.Command{
     		Use:               cliName,
    @@ -160,6 +162,7 @@ func NewCommand() *cobra.Command {
     				persistResourceHealth,
     				clusterFilter,
     				applicationNamespaces,
    +				ignoreNormalizerOpts,
     			)
     			errors.CheckError(err)
     			cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
    @@ -206,6 +209,7 @@ func NewCommand() *cobra.Command {
     	command.Flags().BoolVar(&persistResourceHealth, "persist-resource-health", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_PERSIST_RESOURCE_HEALTH", true), "Enables storing the managed resources health in the Application CRD")
     	command.Flags().StringVar(&shardingAlgorithm, "sharding-method", env.StringFromEnv(common.EnvControllerShardingAlgorithm, common.DefaultShardingAlgorithm), "Enables choice of sharding method. Supported sharding methods are : [legacy, round-robin] ")
     	command.Flags().BoolVar(&enableDynamicClusterDistribution, "dynamic-cluster-distribution-enabled", env.ParseBoolFromEnv(common.EnvEnableDynamicClusterDistribution, false), "Enables dynamic cluster distribution.")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "", env.ParseDurationFromEnv("ARGOCD_IGNORE_NORMALIZER_JQ_TIMEOUT", 0*time.Second, 0, math.MaxInt64), "Set ignore normalizer JQ execution timeout")
     	cacheSource = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
     		redisClient = client
     	})
    
  • cmd/argocd/commands/admin/app.go+11 7 modified
    @@ -30,6 +30,7 @@ import (
     	appinformers "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions"
     	reposerverclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/cli"
    @@ -228,11 +229,12 @@ func diffReconcileResults(res1 reconcileResults, res2 reconcileResults) error {
     
     func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
     	var (
    -		clientConfig      clientcmd.ClientConfig
    -		selector          string
    -		repoServerAddress string
    -		outputFormat      string
    -		refresh           bool
    +		clientConfig         clientcmd.ClientConfig
    +		selector             string
    +		repoServerAddress    string
    +		outputFormat         string
    +		refresh              bool
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     	)
     
     	var command = &cobra.Command{
    @@ -270,7 +272,7 @@ func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
     
     				appClientset := appclientset.NewForConfigOrDie(cfg)
     				kubeClientset := kubernetes.NewForConfigOrDie(cfg)
    -				result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache)
    +				result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache, ignoreNormalizerOpts)
     				errors.CheckError(err)
     			} else {
     				appClientset := appclientset.NewForConfigOrDie(cfg)
    @@ -285,6 +287,7 @@ func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
     	command.Flags().StringVar(&selector, "l", "", "Label selector")
     	command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)")
     	command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     
     	return command
     }
    @@ -334,6 +337,7 @@ func reconcileApplications(
     	repoServerClient reposerverclient.Clientset,
     	selector string,
     	createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) ([]appReconcileResult, error) {
     	settingsMgr := settings.NewSettingsManager(ctx, kubeClientset, namespace)
     	argoDB := db.NewDB(namespace, settingsMgr, kubeClientset)
    @@ -374,7 +378,7 @@ func reconcileApplications(
     	)
     
     	appStateManager := controller.NewAppStateManager(
    -		argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false)
    +		argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false, ignoreNormalizerOpts)
     
     	appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, v1.ListOptions{LabelSelector: selector})
     	if err != nil {
    
  • cmd/argocd/commands/admin/app_test.go+2 0 modified
    @@ -23,6 +23,7 @@ import (
     	argocdclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
     	"github.com/argoproj/argo-cd/v2/test"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/settings"
     )
    @@ -113,6 +114,7 @@ func TestGetReconcileResults_Refresh(t *testing.T) {
     		func(argoDB db.ArgoDB, appInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) statecache.LiveStateCache {
     			return &liveStateCache
     		},
    +		normalizers.IgnoreNormalizerOpts{},
     	)
     
     	if !assert.NoError(t, err) {
    
  • cmd/argocd/commands/admin/settings.go+6 2 modified
    @@ -432,7 +432,7 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
     				// configurations. This requires access to live resources which is not the
     				// purpose of this command. This will just apply jsonPointers and
     				// jqPathExpressions configurations.
    -				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
    +				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, normalizers.IgnoreNormalizerOpts{})
     				errors.CheckError(err)
     
     				normalizedRes := res.DeepCopy()
    @@ -457,6 +457,9 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
     }
     
     func NewResourceIgnoreResourceUpdatesCommand(cmdCtx commandContext) *cobra.Command {
    +	var (
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
    +	)
     	var command = &cobra.Command{
     		Use:   "ignore-resource-updates RESOURCE_YAML_PATH",
     		Short: "Renders fields excluded from resource updates",
    @@ -478,7 +481,7 @@ argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml -
     					return
     				}
     
    -				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
    +				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, ignoreNormalizerOpts)
     				errors.CheckError(err)
     
     				normalizedRes := res.DeepCopy()
    @@ -499,6 +502,7 @@ argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml -
     			})
     		},
     	}
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    
  • cmd/argocd/commands/app.go+19 12 modified
    @@ -44,6 +44,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/repository"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/cli"
     	"github.com/argoproj/argo-cd/v2/util/errors"
     	"github.com/argoproj/argo-cd/v2/util/git"
    @@ -964,14 +965,15 @@ type objKeyLiveTarget struct {
     // NewApplicationDiffCommand returns a new instance of an `argocd app diff` command
     func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
     	var (
    -		refresh            bool
    -		hardRefresh        bool
    -		exitCode           bool
    -		local              string
    -		revision           string
    -		localRepoRoot      string
    -		serverSideGenerate bool
    -		localIncludes      []string
    +		refresh              bool
    +		hardRefresh          bool
    +		exitCode             bool
    +		local                string
    +		revision             string
    +		localRepoRoot        string
    +		serverSideGenerate   bool
    +		localIncludes        []string
    +		ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     	)
     	shortDesc := "Perform a diff against the target and live state."
     	var command = &cobra.Command{
    @@ -1038,7 +1040,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     				}
     			}
     			proj := getProject(c, clientOpts, ctx, app.Spec.Project)
    -			foundDiffs := findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption)
    +			foundDiffs := findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
     			if foundDiffs && exitCode {
     				os.Exit(1)
     			}
    @@ -1052,6 +1054,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     	command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
     	command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
     	command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    @@ -1066,7 +1069,7 @@ type DifferenceOption struct {
     }
     
     // findandPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
    -func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) bool {
    +func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) bool {
     	var foundDiffs bool
     	liveObjs, err := cmdutil.LiveObjects(resources.Items)
     	errors.CheckError(err)
    @@ -1121,7 +1124,7 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
     		// compareOptions in the protobuf
     		ignoreAggregatedRoles := false
     		diffConfig, err := argodiff.NewDiffConfigBuilder().
    -			WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles).
    +			WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles, ignoreNormalizerOpts).
     			WithTracking(argoSettings.AppLabelKey, argoSettings.TrackingMethod).
     			WithNoCache().
     			Build()
    @@ -1614,6 +1617,8 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     		diffChangesConfirm      bool
     		projects                []string
     		output                  string
    +		appNamespace            string
    +		ignoreNormalizerOpts    normalizers.IgnoreNormalizerOpts
     	)
     	var command = &cobra.Command{
     		Use:   "sync [APPNAME... | -l selector | --project project-name]",
    @@ -1838,7 +1843,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     					fmt.Printf("====== Previewing differences between live and desired state of application %s ======\n", appQualifiedName)
     
     					proj := getProject(c, clientOpts, ctx, app.Spec.Project)
    -					foundDiffs = findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption)
    +					foundDiffs = findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
     					if foundDiffs {
     						if !diffChangesConfirm {
     							yesno := cli.AskToProceed(fmt.Sprintf("Please review changes to application %s shown above. Do you want to continue the sync process? (y/n): ", appQualifiedName))
    @@ -1896,6 +1901,8 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
     	command.Flags().BoolVar(&diffChanges, "preview-changes", false, "Preview difference against the target and live state before syncing app and wait for user confirmation")
     	command.Flags().StringArrayVar(&projects, "project", []string{}, "Sync apps that belong to the specified projects. This option may be specified repeatedly.")
     	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed")
    +	command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only sync an application in namespace")
    +	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
     	return command
     }
     
    
  • controller/appcontroller.go+6 2 modified
    @@ -54,6 +54,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/env"
     
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
    @@ -127,6 +128,7 @@ type ApplicationController struct {
     	clusterFilter                 func(cluster *appv1.Cluster) bool
     	projByNameCache               sync.Map
     	applicationNamespaces         []string
    +	ignoreNormalizerOpts          normalizers.IgnoreNormalizerOpts
     }
     
     // NewApplicationController creates new instance of ApplicationController.
    @@ -148,6 +150,7 @@ func NewApplicationController(
     	persistResourceHealth bool,
     	clusterFilter func(cluster *appv1.Cluster) bool,
     	applicationNamespaces []string,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) (*ApplicationController, error) {
     	log.Infof("appResyncPeriod=%v, appHardResyncPeriod=%v", appResyncPeriod, appHardResyncPeriod)
     	db := db.NewDB(namespace, settingsMgr, kubeClientset)
    @@ -173,6 +176,7 @@ func NewApplicationController(
     		clusterFilter:                 clusterFilter,
     		projByNameCache:               sync.Map{},
     		applicationNamespaces:         applicationNamespaces,
    +		ignoreNormalizerOpts:          ignoreNormalizerOpts,
     	}
     	if kubectlParallelismLimit > 0 {
     		ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
    @@ -247,7 +251,7 @@ func NewApplicationController(
     		}
     	}
     	stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated, clusterFilter, argo.NewResourceTracking())
    -	appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth)
    +	appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth, ignoreNormalizerOpts)
     	ctrl.appInformer = appInformer
     	ctrl.appLister = appLister
     	ctrl.projInformer = projInformer
    @@ -698,7 +702,7 @@ func (ctrl *ApplicationController) hideSecretData(app *appv1.Application, compar
     				return nil, fmt.Errorf("error getting cluster cache: %s", err)
     			}
     			diffConfig, err := argodiff.NewDiffConfigBuilder().
    -				WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
    +				WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles, ctrl.ignoreNormalizerOpts).
     				WithTracking(appLabelKey, trackingMethod).
     				WithNoCache().
     				WithLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig())).
    
  • controller/appcontroller_test.go+2 0 modified
    @@ -38,6 +38,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	mockrepoclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks"
     	"github.com/argoproj/argo-cd/v2/test"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	cacheutil "github.com/argoproj/argo-cd/v2/util/cache"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/settings"
    @@ -123,6 +124,7 @@ func newFakeController(data *fakeData) *ApplicationController {
     		true,
     		nil,
     		data.applicationNamespaces,
    +		normalizers.IgnoreNormalizerOpts{},
     	)
     	if err != nil {
     		panic(err)
    
  • controller/cache/cache.go+11 9 modified
    @@ -32,6 +32,7 @@ import (
     	"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/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/env"
     	logutils "github.com/argoproj/argo-cd/v2/util/log"
    @@ -196,14 +197,15 @@ type cacheSettings struct {
     }
     
     type liveStateCache struct {
    -	db               db.ArgoDB
    -	appInformer      cache.SharedIndexInformer
    -	onObjectUpdated  ObjectUpdatedHandler
    -	kubectl          kube.Kubectl
    -	settingsMgr      *settings.SettingsManager
    -	metricsServer    *metrics.MetricsServer
    -	clusterFilter    func(cluster *appv1.Cluster) bool
    -	resourceTracking argo.ResourceTracking
    +	db                   db.ArgoDB
    +	appInformer          cache.SharedIndexInformer
    +	onObjectUpdated      ObjectUpdatedHandler
    +	kubectl              kube.Kubectl
    +	settingsMgr          *settings.SettingsManager
    +	metricsServer        *metrics.MetricsServer
    +	clusterFilter        func(cluster *appv1.Cluster) bool
    +	resourceTracking     argo.ResourceTracking
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
     
     	clusters      map[string]clustercache.ClusterCache
     	cacheSettings cacheSettings
    @@ -486,7 +488,7 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
     			gvk := un.GroupVersionKind()
     
     			if cacheSettings.ignoreResourceUpdatesEnabled && shouldHashManifest(appName, gvk) {
    -				hash, err := generateManifestHash(un, nil, cacheSettings.resourceOverrides)
    +				hash, err := generateManifestHash(un, nil, cacheSettings.resourceOverrides, c.ignoreNormalizerOpts)
     				if err != nil {
     					log.Errorf("Failed to generate manifest hash: %v", err)
     				} else {
    
  • controller/cache/info.go+2 2 modified
    @@ -390,8 +390,8 @@ func populateHostNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
     	}
     }
     
    -func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (string, error) {
    -	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides)
    +func generateManifestHash(un *unstructured.Unstructured, ignores []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (string, error) {
    +	normalizer, err := normalizers.NewIgnoreNormalizer(ignores, overrides, opts)
     	if err != nil {
     		return "", fmt.Errorf("error creating normalizer: %w", err)
     	}
    
  • controller/cache/info_test.go+2 1 modified
    @@ -16,6 +16,7 @@ import (
     	"sigs.k8s.io/yaml"
     
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func strToUnstructured(jsonStr string) *unstructured.Unstructured {
    @@ -749,7 +750,7 @@ func TestManifestHash(t *testing.T) {
     
     	expected := hash(data)
     
    -	hash, err := generateManifestHash(manifest, ignores, nil)
    +	hash, err := generateManifestHash(manifest, ignores, nil, normalizers.IgnoreNormalizerOpts{})
     	assert.Equal(t, expected, hash)
     	assert.Nil(t, err)
     }
    
  • controller/state.go+7 2 modified
    @@ -4,11 +4,12 @@ import (
     	"context"
     	"encoding/json"
     	"fmt"
    -	v1 "k8s.io/api/core/v1"
     	"reflect"
     	"strings"
     	"time"
     
    +	v1 "k8s.io/api/core/v1"
    +
     	"github.com/argoproj/gitops-engine/pkg/diff"
     	"github.com/argoproj/gitops-engine/pkg/health"
     	"github.com/argoproj/gitops-engine/pkg/sync"
    @@ -32,6 +33,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/util/argo"
     	argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     	"github.com/argoproj/argo-cd/v2/util/db"
     	"github.com/argoproj/argo-cd/v2/util/gpg"
    @@ -105,6 +107,7 @@ type appStateManager struct {
     	statusRefreshTimeout  time.Duration
     	resourceTracking      argo.ResourceTracking
     	persistResourceHealth bool
    +	ignoreNormalizerOpts  normalizers.IgnoreNormalizerOpts
     }
     
     // getRepoObjs will generate the manifests for the given application delegating the
    @@ -565,7 +568,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
     	useDiffCache := useDiffCache(noCache, manifestInfos, sources, app, manifestRevisions, m.statusRefreshTimeout, logCtx)
     
     	diffConfigBuilder := argodiff.NewDiffConfigBuilder().
    -		WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
    +		WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles, m.ignoreNormalizerOpts).
     		WithTracking(appLabelKey, string(trackingMethod))
     
     	if useDiffCache {
    @@ -871,6 +874,7 @@ func NewAppStateManager(
     	statusRefreshTimeout time.Duration,
     	resourceTracking argo.ResourceTracking,
     	persistResourceHealth bool,
    +	ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts,
     ) AppStateManager {
     	return &appStateManager{
     		liveStateCache:        liveStateCache,
    @@ -886,6 +890,7 @@ func NewAppStateManager(
     		statusRefreshTimeout:  statusRefreshTimeout,
     		resourceTracking:      resourceTracking,
     		persistResourceHealth: persistResourceHealth,
    +		ignoreNormalizerOpts:  ignoreNormalizerOpts,
     	}
     }
     
    
  • controller/sync_test.go+3 2 modified
    @@ -18,6 +18,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
     	"github.com/argoproj/argo-cd/v2/test"
     	"github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     )
     
     func TestPersistRevisionHistory(t *testing.T) {
    @@ -261,7 +262,7 @@ func TestNormalizeTargetResources(t *testing.T) {
     	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    @@ -394,7 +395,7 @@ func TestNormalizeTargetResourcesWithList(t *testing.T) {
     	setupHttpProxy := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    
  • docs/user-guide/diffing.md+13 0 modified
    @@ -182,3 +182,16 @@ data:
     ```
     
     The list of supported Kubernetes types is available in [diffing_known_types.txt](https://raw.githubusercontent.com/argoproj/argo-cd/master/util/argo/normalizers/diffing_known_types.txt)
    +
    +
    +### JQ Path expression timeout
    +
    +By default, the evaluation of a JQPathExpression is limited to one second. If you encounter a "JQ patch execution timed out" error message due to a complex JQPathExpression that requires more time to evaluate, you can extend the timeout period by configuring the `ignore.normalizer.jq.timeout` setting within the `argocd-cmd-params-cm` ConfigMap.
    +
    +```yaml
    +apiVersion: v1
    +kind: ConfigMap
    +metadata:
    +  name: argocd-cmd-params-cm
    +data:
    +  ignore.normalizer.jq.timeout: "5s"
    
  • manifests/base/application-controller/argocd-application-controller-statefulset.yaml+6 0 modified
    @@ -155,6 +155,12 @@ spec:
                     name: argocd-cmd-params-cm
                     key: controller.kubectl.parallelism.limit
                     optional: true
    +        - name: ARGOCD_IGNORE_NORMALIZER_JQ_TIMEOUT
    +          valueFrom:
    +            configMapKeyRef:
    +              name: argocd-cmd-params-cm
    +              key: controller.ignore.normalizer.jq.timeout
    +              optional: true
             image: quay.io/argoproj/argocd:latest
             imagePullPolicy: Always
             name: argocd-application-controller
    
  • util/argo/diff/diff.go+10 2 modified
    @@ -10,6 +10,7 @@ import (
     	"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/argo/managedfields"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     
     	"github.com/argoproj/gitops-engine/pkg/diff"
    @@ -31,7 +32,7 @@ func NewDiffConfigBuilder() *DiffConfigBuilder {
     }
     
     // WithDiffSettings will set the diff settings in the builder.
    -func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDifferences, o map[string]v1alpha1.ResourceOverride, ignoreAggregatedRoles bool) *DiffConfigBuilder {
    +func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDifferences, o map[string]v1alpha1.ResourceOverride, ignoreAggregatedRoles bool, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) *DiffConfigBuilder {
     	ignores := id
     	if ignores == nil {
     		ignores = []v1alpha1.ResourceIgnoreDifferences{}
    @@ -44,6 +45,7 @@ func (b *DiffConfigBuilder) WithDiffSettings(id []v1alpha1.ResourceIgnoreDiffere
     	}
     	b.diffConfig.overrides = overrides
     	b.diffConfig.ignoreAggregatedRoles = ignoreAggregatedRoles
    +	b.diffConfig.ignoreNormalizerOpts = ignoreNormalizerOpts
     	return b
     }
     
    @@ -140,6 +142,8 @@ type DiffConfig interface {
     	// Manager returns the manager that should be used by the diff while
     	// calculating the structured merge diff.
     	Manager() string
    +
    +	IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts
     }
     
     // diffConfig defines the configurations used while applying diffs.
    @@ -156,6 +160,7 @@ type diffConfig struct {
     	gvkParser             *k8smanagedfields.GvkParser
     	structuredMergeDiff   bool
     	manager               string
    +	ignoreNormalizerOpts  normalizers.IgnoreNormalizerOpts
     }
     
     func (c *diffConfig) Ignores() []v1alpha1.ResourceIgnoreDifferences {
    @@ -194,6 +199,9 @@ func (c *diffConfig) StructuredMergeDiff() bool {
     func (c *diffConfig) Manager() string {
     	return c.manager
     }
    +func (c *diffConfig) IgnoreNormalizerOpts() normalizers.IgnoreNormalizerOpts {
    +	return c.ignoreNormalizerOpts
    +}
     
     // Validate will check the current state of this diffConfig and return
     // error if it finds any required configuration missing.
    @@ -243,7 +251,7 @@ func StateDiffs(lives, configs []*unstructured.Unstructured, diffConfig DiffConf
     		return nil, fmt.Errorf("failed to perform pre-diff normalization: %w", err)
     	}
     
    -	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides())
    +	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides(), diffConfig.IgnoreNormalizerOpts())
     	if err != nil {
     		return nil, fmt.Errorf("failed to create diff normalizer: %w", err)
     	}
    
  • util/argo/diff/diff_test.go+6 5 modified
    @@ -10,6 +10,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	testutil "github.com/argoproj/argo-cd/v2/test"
     	argo "github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/argo/testdata"
     	appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
     )
    @@ -40,7 +41,7 @@ func TestStateDiff(t *testing.T) {
     	diffConfig := func(t *testing.T, params *diffConfigParams) argo.DiffConfig {
     		t.Helper()
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(params.ignores, params.overrides, params.ignoreRoles).
    +			WithDiffSettings(params.ignores, params.overrides, params.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(params.label, params.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -185,7 +186,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -209,7 +210,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(nil, nil, f.ignoreRoles).
    +			WithDiffSettings(nil, nil, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithNoCache().
     			Build()
    @@ -231,7 +232,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithCache(&appstatecache.Cache{}, "").
     			Build()
    @@ -246,7 +247,7 @@ func TestDiffConfigBuilder(t *testing.T) {
     
     		// when
     		diffConfig, err := argo.NewDiffConfigBuilder().
    -			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles).
    +			WithDiffSettings(f.ignores, f.overrides, f.ignoreRoles, normalizers.IgnoreNormalizerOpts{}).
     			WithTracking(f.label, f.trackingMethod).
     			WithCache(nil, f.appName).
     			Build()
    
  • util/argo/diff/normalize.go+3 3 modified
    @@ -15,7 +15,7 @@ func Normalize(lives, configs []*unstructured.Unstructured, diffConfig DiffConfi
     	if err != nil {
     		return nil, err
     	}
    -	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides())
    +	diffNormalizer, err := newDiffNormalizer(diffConfig.Ignores(), diffConfig.Overrides(), diffConfig.IgnoreNormalizerOpts())
     	if err != nil {
     		return nil, err
     	}
    @@ -40,8 +40,8 @@ func Normalize(lives, configs []*unstructured.Unstructured, diffConfig DiffConfi
     }
     
     // newDiffNormalizer creates normalizer that uses Argo CD and application settings to normalize the resource prior to diffing.
    -func newDiffNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (diff.Normalizer, error) {
    -	ignoreNormalizer, err := normalizers.NewIgnoreNormalizer(ignore, overrides)
    +func newDiffNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts normalizers.IgnoreNormalizerOpts) (diff.Normalizer, error) {
    +	ignoreNormalizer, err := normalizers.NewIgnoreNormalizer(ignore, overrides, opts)
     	if err != nil {
     		return nil, err
     	}
    
  • util/argo/diff/normalize_test.go+2 1 modified
    @@ -10,6 +10,7 @@ import (
     	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
     	"github.com/argoproj/argo-cd/v2/test"
     	"github.com/argoproj/argo-cd/v2/util/argo/diff"
    +	"github.com/argoproj/argo-cd/v2/util/argo/normalizers"
     	"github.com/argoproj/argo-cd/v2/util/argo/testdata"
     )
     
    @@ -22,7 +23,7 @@ func TestNormalize(t *testing.T) {
     	setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
     		t.Helper()
     		dc, err := diff.NewDiffConfigBuilder().
    -			WithDiffSettings(ignores, nil, true).
    +			WithDiffSettings(ignores, nil, true, normalizers.IgnoreNormalizerOpts{}).
     			WithNoCache().
     			Build()
     		require.NoError(t, err)
    
  • util/argo/normalizers/diff_normalizer.go+30 4 modified
    @@ -1,9 +1,11 @@
     package normalizers
     
     import (
    +	"context"
     	"encoding/json"
     	"fmt"
     	"strings"
    +	"time"
     
     	"github.com/argoproj/gitops-engine/pkg/diff"
     	jsonpatch "github.com/evanphx/json-patch"
    @@ -16,6 +18,11 @@ import (
     	"github.com/argoproj/argo-cd/v2/util/glob"
     )
     
    +const (
    +	// DefaultJQExecutionTimeout is the maximum time allowed for a JQ patch to execute
    +	DefaultJQExecutionTimeout = 1 * time.Second
    +)
    +
     type normalizerPatch interface {
     	GetGroupKind() schema.GroupKind
     	GetNamespace() string
    @@ -57,7 +64,8 @@ func (np *jsonPatchNormalizerPatch) Apply(data []byte) ([]byte, error) {
     
     type jqNormalizerPatch struct {
     	baseNormalizerPatch
    -	code *gojq.Code
    +	code               *gojq.Code
    +	jqExecutionTimeout time.Duration
     }
     
     func (np *jqNormalizerPatch) Apply(data []byte) ([]byte, error) {
    @@ -67,12 +75,18 @@ func (np *jqNormalizerPatch) Apply(data []byte) ([]byte, error) {
     		return nil, err
     	}
     
    -	iter := np.code.Run(dataJson)
    +	ctx, cancel := context.WithTimeout(context.Background(), np.jqExecutionTimeout)
    +	defer cancel()
    +
    +	iter := np.code.RunWithContext(ctx, dataJson)
     	first, ok := iter.Next()
     	if !ok {
     		return nil, fmt.Errorf("JQ patch did not return any data")
     	}
     	if err, ok = first.(error); ok {
    +		if err == context.DeadlineExceeded {
    +			return nil, fmt.Errorf("JQ patch execution timed out (%v)", np.jqExecutionTimeout.String())
    +		}
     		return nil, fmt.Errorf("JQ patch returned error: %w", err)
     	}
     	_, ok = iter.Next()
    @@ -91,8 +105,19 @@ type ignoreNormalizer struct {
     	patches []normalizerPatch
     }
     
    +type IgnoreNormalizerOpts struct {
    +	JQExecutionTimeout time.Duration
    +}
    +
    +func (opts *IgnoreNormalizerOpts) getJQExecutionTimeout() time.Duration {
    +	if opts == nil || opts.JQExecutionTimeout == 0 {
    +		return DefaultJQExecutionTimeout
    +	}
    +	return opts.JQExecutionTimeout
    +}
    +
     // NewIgnoreNormalizer creates diff normalizer which removes ignored fields according to given application spec and resource overrides
    -func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride) (diff.Normalizer, error) {
    +func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides map[string]v1alpha1.ResourceOverride, opts IgnoreNormalizerOpts) (diff.Normalizer, error) {
     	for key, override := range overrides {
     		group, kind, err := getGroupKindForOverrideKey(key)
     		if err != nil {
    @@ -147,7 +172,8 @@ func NewIgnoreNormalizer(ignore []v1alpha1.ResourceIgnoreDifferences, overrides
     					name:      ignore[i].Name,
     					namespace: ignore[i].Namespace,
     				},
    -				code: jqDeletionCode,
    +				code:               jqDeletionCode,
    +				jqExecutionTimeout: opts.getJQExecutionTimeout(),
     			})
     		}
     	}
    
  • util/argo/normalizers/diff_normalizer_test.go+32 10 modified
    @@ -19,7 +19,7 @@ func TestNormalizeObjectWithMatchedGroupKind(t *testing.T) {
     		Group:        "apps",
     		Kind:         "Deployment",
     		JSONPointers: []string{"/not-matching-path", "/spec/template/spec/containers"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -44,7 +44,7 @@ func TestNormalizeNoMatchedGroupKinds(t *testing.T) {
     		Group:        "",
     		Kind:         "Service",
     		JSONPointers: []string{"/spec"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -63,7 +63,7 @@ func TestNormalizeMatchedResourceOverrides(t *testing.T) {
     		"apps/Deployment": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -118,7 +118,7 @@ func TestNormalizeMissingJsonPointer(t *testing.T) {
     		"apiextensions.k8s.io/CustomResourceDefinition": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/additionalPrinterColumns/0/priority"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     	assert.NoError(t, err)
     
     	deployment := test.NewDeployment()
    @@ -139,7 +139,7 @@ func TestNormalizeGlobMatch(t *testing.T) {
     		"*/*": {
     			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{JSONPointers: []string{"/spec/template/spec/containers"}},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -161,7 +161,7 @@ func TestNormalizeJQPathExpression(t *testing.T) {
     		Group:             "apps",
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.template.spec.initContainers[] | select(.name == \"init-container-0\")"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -197,7 +197,7 @@ func TestNormalizeIllegalJQPathExpression(t *testing.T) {
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"missing-quote)"},
     		// JSONPointers: []string{"no-starting-slash"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Error(t, err)
     }
    @@ -207,7 +207,7 @@ func TestNormalizeJQPathExpressionWithError(t *testing.T) {
     		Group:             "apps",
     		Kind:              "Deployment",
     		JQPathExpressions: []string{".spec.fakeField.foo[]"},
    -	}}, make(map[string]v1alpha1.ResourceOverride))
    +	}}, make(map[string]v1alpha1.ResourceOverride), IgnoreNormalizerOpts{})
     
     	assert.Nil(t, err)
     
    @@ -230,7 +230,7 @@ func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
     				JSONPointers: []string{"/invalid", "/invalid/json/path"},
     			},
     		},
    -	})
    +	}, IgnoreNormalizerOpts{})
     	assert.Nil(t, err)
     
     	ignoreNormalizer := normalizer.(*ignoreNormalizer)
    @@ -254,12 +254,34 @@ func TestNormalizeExpectedErrorAreSilenced(t *testing.T) {
     
     }
     
    +func TestJqPathExpressionFailWithTimeout(t *testing.T) {
    +	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{}, map[string]v1alpha1.ResourceOverride{
    +		"*/*": {
    +			IgnoreDifferences: v1alpha1.OverrideIgnoreDiff{
    +				JQPathExpressions: []string{"until(true==false; [.] + [1])"},
    +			},
    +		},
    +	}, IgnoreNormalizerOpts{})
    +	assert.Nil(t, err)
    +
    +	ignoreNormalizer := normalizer.(*ignoreNormalizer)
    +	assert.Len(t, ignoreNormalizer.patches, 1)
    +	jqPatch := ignoreNormalizer.patches[0]
    +
    +	deployment := test.NewDeployment()
    +	deploymentData, err := json.Marshal(deployment)
    +	assert.Nil(t, err)
    +
    +	_, err = jqPatch.Apply(deploymentData)
    +	assert.ErrorContains(t, err, "JQ patch execution timed out")
    +}
    +
     func TestJQPathExpressionReturnsHelpfulError(t *testing.T) {
     	normalizer, err := NewIgnoreNormalizer([]v1alpha1.ResourceIgnoreDifferences{{
     		Kind: "ConfigMap",
     		// This is a really wild expression, but it does trigger the desired error.
     		JQPathExpressions: []string{`.nothing) | .data["config.yaml"] |= (fromjson | del(.auth) | tojson`},
    -	}}, nil)
    +	}}, nil, IgnoreNormalizerOpts{})
     
     	assert.NoError(t, err)
     
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.