diff --git a/applicationset/controllers/applicationset_controller.go b/applicationset/controllers/applicationset_controller.go index f30ea8e2231c5..757356dc555de 100644 --- a/applicationset/controllers/applicationset_controller.go +++ b/applicationset/controllers/applicationset_controller.go @@ -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 diff --git a/applicationset/utils/createOrUpdate.go b/applicationset/utils/createOrUpdate.go index 1f2a8a9c4a54c..301d477bab2db 100644 --- a/applicationset/utils/createOrUpdate.go +++ b/applicationset/utils/createOrUpdate.go @@ -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 { diff --git a/applicationset/utils/createOrUpdate_test.go b/applicationset/utils/createOrUpdate_test.go index a294e89281974..2dc5945d2d2cc 100644 --- a/applicationset/utils/createOrUpdate_test.go +++ b/applicationset/utils/createOrUpdate_test.go @@ -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) diff --git a/cmd/argocd-application-controller/commands/argocd_application_controller.go b/cmd/argocd-application-controller/commands/argocd_application_controller.go index a43174633b02a..9a2c884051d3c 100644 --- a/cmd/argocd-application-controller/commands/argocd_application_controller.go +++ b/cmd/argocd-application-controller/commands/argocd_application_controller.go @@ -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 }) diff --git a/cmd/argocd/commands/admin/app.go b/cmd/argocd/commands/admin/app.go index fbceb436f8609..e6b8d094b007d 100644 --- a/cmd/argocd/commands/admin/app.go +++ b/cmd/argocd/commands/admin/app.go @@ -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 { diff --git a/cmd/argocd/commands/admin/app_test.go b/cmd/argocd/commands/admin/app_test.go index 0cad2485e6696..2e92910727a88 100644 --- a/cmd/argocd/commands/admin/app_test.go +++ b/cmd/argocd/commands/admin/app_test.go @@ -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) { diff --git a/cmd/argocd/commands/admin/settings.go b/cmd/argocd/commands/admin/settings.go index 281d9875691c4..a449900f759d9 100644 --- a/cmd/argocd/commands/admin/settings.go +++ b/cmd/argocd/commands/admin/settings.go @@ -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 } diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index a356e168530db..2580dfa6c7f45 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -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 } diff --git a/controller/appcontroller.go b/controller/appcontroller.go index a6859b9984c69..33592cf3bcd52 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -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())). diff --git a/controller/appcontroller_test.go b/controller/appcontroller_test.go index cfb2141664348..c347e2d759dd9 100644 --- a/controller/appcontroller_test.go +++ b/controller/appcontroller_test.go @@ -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) diff --git a/controller/cache/cache.go b/controller/cache/cache.go index 9eac161714089..cfea42f527857 100644 --- a/controller/cache/cache.go +++ b/controller/cache/cache.go @@ -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 { diff --git a/controller/cache/info.go b/controller/cache/info.go index cf0d12318a447..8f90d4d18c635 100644 --- a/controller/cache/info.go +++ b/controller/cache/info.go @@ -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) } diff --git a/controller/cache/info_test.go b/controller/cache/info_test.go index 8a06d3745e13b..2d4dbb7dc5735 100644 --- a/controller/cache/info_test.go +++ b/controller/cache/info_test.go @@ -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) } diff --git a/controller/state.go b/controller/state.go index 40e2f9a5fa76b..0389c33d2e899 100644 --- a/controller/state.go +++ b/controller/state.go @@ -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, } } diff --git a/controller/sync_test.go b/controller/sync_test.go index 96a5a98cb5b0b..c3054ec35558a 100644 --- a/controller/sync_test.go +++ b/controller/sync_test.go @@ -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) diff --git a/docs/user-guide/diffing.md b/docs/user-guide/diffing.md index 5a056b9c3769b..f02ed57cd8753 100644 --- a/docs/user-guide/diffing.md +++ b/docs/user-guide/diffing.md @@ -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" diff --git a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml index 33f02100a947a..a15350e941c2f 100644 --- a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml +++ b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml @@ -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 diff --git a/util/argo/diff/diff.go b/util/argo/diff/diff.go index 9b104719c5616..28fe2ee59c67a 100644 --- a/util/argo/diff/diff.go +++ b/util/argo/diff/diff.go @@ -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) } diff --git a/util/argo/diff/diff_test.go b/util/argo/diff/diff_test.go index 2c95d7404d299..151f369c28e9d 100644 --- a/util/argo/diff/diff_test.go +++ b/util/argo/diff/diff_test.go @@ -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() diff --git a/util/argo/diff/normalize.go b/util/argo/diff/normalize.go index 95a9e70a81276..88238fdb88cfd 100644 --- a/util/argo/diff/normalize.go +++ b/util/argo/diff/normalize.go @@ -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 } diff --git a/util/argo/diff/normalize_test.go b/util/argo/diff/normalize_test.go index 2464a2e91ee6b..246f6697355b6 100644 --- a/util/argo/diff/normalize_test.go +++ b/util/argo/diff/normalize_test.go @@ -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) diff --git a/util/argo/normalizers/diff_normalizer.go b/util/argo/normalizers/diff_normalizer.go index af2d69fb2488a..5c9bef2a119b7 100644 --- a/util/argo/normalizers/diff_normalizer.go +++ b/util/argo/normalizers/diff_normalizer.go @@ -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(), }) } } diff --git a/util/argo/normalizers/diff_normalizer_test.go b/util/argo/normalizers/diff_normalizer_test.go index 1b8c2bcdcebca..fc6de6bc40d53 100644 --- a/util/argo/normalizers/diff_normalizer_test.go +++ b/util/argo/normalizers/diff_normalizer_test.go @@ -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)