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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/argoproj/argo-cd/v2Go | >= 2.10.0, < 2.10.8 | 2.10.8 |
github.com/argoproj/argo-cd/v2Go | >= 2.9.0, < 2.9.13 | 2.9.13 |
github.com/argoproj/argo-cd/v2Go | < 2.8.17 | 2.8.17 |
Affected products
1Patches
39e5cc5a26ff0Merge pull request from GHSA-9m6p-x4h2-6frq
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)
7893979a1e78Merge pull request from GHSA-9m6p-x4h2-6frq
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
e2df7315fb7dMerge pull request from GHSA-9m6p-x4h2-6frq
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- github.com/advisories/GHSA-9m6p-x4h2-6frqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-32476ghsaADVISORY
- github.com/argoproj/argo-cd/commit/7893979a1e78d59cedd0ba790ded24e30bb40657ghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/commit/9e5cc5a26ff0920a01816231d59fdb5eae032b5aghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/commit/e2df7315fb7d96652186bf7435773a27be330cacghsax_refsource_MISCWEB
- github.com/argoproj/argo-cd/security/advisories/GHSA-9m6p-x4h2-6frqghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.