github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/admin/settings.go (about)

     1  package admin
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	stderrors "errors"
     7  	"fmt"
     8  	"os"
     9  	"reflect"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  	"text/tabwriter"
    14  
    15  	healthutil "github.com/argoproj/gitops-engine/pkg/health"
    16  	log "github.com/sirupsen/logrus"
    17  	"github.com/spf13/cobra"
    18  	corev1 "k8s.io/api/core/v1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    21  	"k8s.io/client-go/kubernetes"
    22  	"k8s.io/client-go/kubernetes/fake"
    23  	"k8s.io/client-go/tools/clientcmd"
    24  	"sigs.k8s.io/yaml"
    25  
    26  	"github.com/argoproj/argo-cd/v3/common"
    27  	applicationpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
    28  	"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    29  	"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
    30  	"github.com/argoproj/argo-cd/v3/util/cli"
    31  	"github.com/argoproj/argo-cd/v3/util/errors"
    32  	"github.com/argoproj/argo-cd/v3/util/lua"
    33  	"github.com/argoproj/argo-cd/v3/util/settings"
    34  )
    35  
    36  type settingsOpts struct {
    37  	argocdCMPath        string
    38  	argocdSecretPath    string
    39  	loadClusterSettings bool
    40  	clientConfig        clientcmd.ClientConfig
    41  }
    42  
    43  type commandContext interface {
    44  	createSettingsManager(context.Context) (*settings.SettingsManager, error)
    45  }
    46  
    47  func collectLogs(callback func()) string {
    48  	log.SetLevel(log.DebugLevel)
    49  	out := bytes.Buffer{}
    50  	log.SetOutput(&out)
    51  	defer log.SetLevel(log.FatalLevel)
    52  	callback()
    53  	return out.String()
    54  }
    55  
    56  func setSettingsMeta(obj metav1.Object) {
    57  	obj.SetNamespace("default")
    58  	labels := obj.GetLabels()
    59  	if labels == nil {
    60  		labels = make(map[string]string)
    61  	}
    62  	labels["app.kubernetes.io/part-of"] = "argocd"
    63  	obj.SetLabels(labels)
    64  }
    65  
    66  func (opts *settingsOpts) createSettingsManager(ctx context.Context) (*settings.SettingsManager, error) {
    67  	var argocdCM *corev1.ConfigMap
    68  	switch {
    69  	case opts.argocdCMPath == "" && !opts.loadClusterSettings:
    70  		return nil, stderrors.New("either --argocd-cm-path must be provided or --load-cluster-settings must be set to true")
    71  	case opts.argocdCMPath == "":
    72  		realClientset, ns, err := opts.getK8sClient()
    73  		if err != nil {
    74  			return nil, err
    75  		}
    76  
    77  		argocdCM, err = realClientset.CoreV1().ConfigMaps(ns).Get(ctx, common.ArgoCDConfigMapName, metav1.GetOptions{})
    78  		if err != nil {
    79  			return nil, err
    80  		}
    81  	default:
    82  		data, err := os.ReadFile(opts.argocdCMPath)
    83  		if err != nil {
    84  			return nil, err
    85  		}
    86  		err = yaml.Unmarshal(data, &argocdCM)
    87  		if err != nil {
    88  			return nil, err
    89  		}
    90  	}
    91  	setSettingsMeta(argocdCM)
    92  
    93  	var argocdSecret *corev1.Secret
    94  	switch {
    95  	case opts.argocdSecretPath != "":
    96  		data, err := os.ReadFile(opts.argocdSecretPath)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  		err = yaml.Unmarshal(data, &argocdSecret)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		setSettingsMeta(argocdSecret)
   105  	case opts.loadClusterSettings:
   106  		realClientset, ns, err := opts.getK8sClient()
   107  		if err != nil {
   108  			return nil, err
   109  		}
   110  		argocdSecret, err = realClientset.CoreV1().Secrets(ns).Get(ctx, common.ArgoCDSecretName, metav1.GetOptions{})
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  	default:
   115  		argocdSecret = &corev1.Secret{
   116  			ObjectMeta: metav1.ObjectMeta{
   117  				Name: common.ArgoCDSecretName,
   118  			},
   119  			Data: map[string][]byte{
   120  				"admin.password":   []byte("test"),
   121  				"server.secretkey": []byte("test"),
   122  			},
   123  		}
   124  	}
   125  	setSettingsMeta(argocdSecret)
   126  	clientset := fake.NewClientset(argocdSecret, argocdCM)
   127  
   128  	manager := settings.NewSettingsManager(ctx, clientset, "default")
   129  	errors.CheckError(manager.ResyncInformers())
   130  
   131  	return manager, nil
   132  }
   133  
   134  func (opts *settingsOpts) getK8sClient() (*kubernetes.Clientset, string, error) {
   135  	namespace, _, err := opts.clientConfig.Namespace()
   136  	if err != nil {
   137  		return nil, "", err
   138  	}
   139  
   140  	restConfig, err := opts.clientConfig.ClientConfig()
   141  	if err != nil {
   142  		return nil, "", err
   143  	}
   144  
   145  	realClientset, err := kubernetes.NewForConfig(restConfig)
   146  	if err != nil {
   147  		return nil, "", err
   148  	}
   149  	return realClientset, namespace, nil
   150  }
   151  
   152  func NewSettingsCommand() *cobra.Command {
   153  	var opts settingsOpts
   154  
   155  	command := &cobra.Command{
   156  		Use:   "settings",
   157  		Short: "Provides set of commands for settings validation and troubleshooting",
   158  		Run: func(c *cobra.Command, args []string) {
   159  			c.HelpFunc()(c, args)
   160  		},
   161  	}
   162  	log.SetLevel(log.FatalLevel)
   163  
   164  	command.AddCommand(NewValidateSettingsCommand(&opts))
   165  	command.AddCommand(NewResourceOverridesCommand(&opts))
   166  	command.AddCommand(NewRBACCommand())
   167  
   168  	opts.clientConfig = cli.AddKubectlFlagsToCmd(command)
   169  	command.PersistentFlags().StringVar(&opts.argocdCMPath, "argocd-cm-path", "", "Path to local argocd-cm.yaml file")
   170  	command.PersistentFlags().StringVar(&opts.argocdSecretPath, "argocd-secret-path", "", "Path to local argocd-secret.yaml file")
   171  	command.PersistentFlags().BoolVar(&opts.loadClusterSettings, "load-cluster-settings", false,
   172  		"Indicates that config map and secret should be loaded from cluster unless local file path is provided")
   173  	return command
   174  }
   175  
   176  type settingValidator func(manager *settings.SettingsManager) (string, error)
   177  
   178  func joinValidators(validators ...settingValidator) settingValidator {
   179  	return func(manager *settings.SettingsManager) (string, error) {
   180  		var errorStrs []string
   181  		var summaries []string
   182  		for i := range validators {
   183  			summary, err := validators[i](manager)
   184  			if err != nil {
   185  				errorStrs = append(errorStrs, err.Error())
   186  			}
   187  			if summary != "" {
   188  				summaries = append(summaries, summary)
   189  			}
   190  		}
   191  		if len(errorStrs) > 0 {
   192  			return "", fmt.Errorf("%s", strings.Join(errorStrs, "\n"))
   193  		}
   194  		return strings.Join(summaries, "\n"), nil
   195  	}
   196  }
   197  
   198  var validatorsByGroup = map[string]settingValidator{
   199  	"general": joinValidators(func(manager *settings.SettingsManager) (string, error) {
   200  		general, err := manager.GetSettings()
   201  		if err != nil {
   202  			return "", err
   203  		}
   204  		ssoProvider := ""
   205  		if general.DexConfig != "" {
   206  			if _, err := settings.UnmarshalDexConfig(general.DexConfig); err != nil {
   207  				return "", fmt.Errorf("invalid dex.config: %w", err)
   208  			}
   209  			ssoProvider = "Dex"
   210  		} else if general.OIDCConfigRAW != "" {
   211  			if err := settings.ValidateOIDCConfig(general.OIDCConfigRAW); err != nil {
   212  				return "", fmt.Errorf("invalid oidc.config: %w", err)
   213  			}
   214  			ssoProvider = "OIDC"
   215  		}
   216  		var summary string
   217  		if ssoProvider != "" {
   218  			summary = ssoProvider + " is configured"
   219  			if general.URL == "" {
   220  				summary = summary + " ('url' field is missing)"
   221  			}
   222  		} else if ssoProvider != "" && general.URL != "" {
   223  		} else {
   224  			summary = "SSO is not configured"
   225  		}
   226  		return summary, nil
   227  	}, func(manager *settings.SettingsManager) (string, error) {
   228  		_, err := manager.GetAppInstanceLabelKey()
   229  		return "", err
   230  	}, func(manager *settings.SettingsManager) (string, error) {
   231  		_, err := manager.GetHelp()
   232  		return "", err
   233  	}, func(manager *settings.SettingsManager) (string, error) {
   234  		_, err := manager.GetGoogleAnalytics()
   235  		return "", err
   236  	}),
   237  	"kustomize": func(manager *settings.SettingsManager) (string, error) {
   238  		opts, err := manager.GetKustomizeSettings()
   239  		if err != nil {
   240  			return "", err
   241  		}
   242  		summary := "default options"
   243  		if opts.BuildOptions != "" {
   244  			summary = opts.BuildOptions
   245  		}
   246  		if len(opts.Versions) > 0 {
   247  			summary = fmt.Sprintf("%s (%d versions)", summary, len(opts.Versions))
   248  		}
   249  		return summary, err
   250  	},
   251  	"accounts": func(manager *settings.SettingsManager) (string, error) {
   252  		accounts, err := manager.GetAccounts()
   253  		if err != nil {
   254  			return "", err
   255  		}
   256  		return fmt.Sprintf("%d accounts", len(accounts)), nil
   257  	},
   258  	"resource-overrides": func(manager *settings.SettingsManager) (string, error) {
   259  		overrides, err := manager.GetResourceOverrides()
   260  		if err != nil {
   261  			return "", err
   262  		}
   263  		return fmt.Sprintf("%d resource overrides", len(overrides)), nil
   264  	},
   265  }
   266  
   267  func NewValidateSettingsCommand(cmdCtx commandContext) *cobra.Command {
   268  	var groups []string
   269  
   270  	var allGroups []string
   271  	for k := range validatorsByGroup {
   272  		allGroups = append(allGroups, k)
   273  	}
   274  	sort.Slice(allGroups, func(i, j int) bool {
   275  		return allGroups[i] < allGroups[j]
   276  	})
   277  
   278  	command := &cobra.Command{
   279  		Use:   "validate",
   280  		Short: "Validate settings",
   281  		Long:  "Validates settings specified in 'argocd-cm' ConfigMap and 'argocd-secret' Secret",
   282  		Example: `
   283  #Validates all settings in the specified YAML file
   284  argocd admin settings validate --argocd-cm-path ./argocd-cm.yaml
   285  
   286  #Validates accounts and plugins settings in Kubernetes cluster of current kubeconfig context
   287  argocd admin settings validate --group accounts --group plugins --load-cluster-settings`,
   288  		Run: func(c *cobra.Command, _ []string) {
   289  			ctx := c.Context()
   290  
   291  			settingsManager, err := cmdCtx.createSettingsManager(ctx)
   292  			errors.CheckError(err)
   293  
   294  			if len(groups) == 0 {
   295  				groups = allGroups
   296  			}
   297  			for i, group := range groups {
   298  				validator := validatorsByGroup[group]
   299  
   300  				logs := collectLogs(func() {
   301  					summary, err := validator(settingsManager)
   302  
   303  					if err != nil {
   304  						_, _ = fmt.Fprintf(os.Stdout, "❌ %s\n", group)
   305  						_, _ = fmt.Fprintf(os.Stdout, "%s\n", err.Error())
   306  					} else {
   307  						_, _ = fmt.Fprintf(os.Stdout, "✅ %s\n", group)
   308  						if summary != "" {
   309  							_, _ = fmt.Fprintf(os.Stdout, "%s\n", summary)
   310  						}
   311  					}
   312  				})
   313  				if logs != "" {
   314  					_, _ = fmt.Fprintf(os.Stdout, "%s\n", logs)
   315  				}
   316  				if i != len(groups)-1 {
   317  					_, _ = fmt.Fprintf(os.Stdout, "\n")
   318  				}
   319  			}
   320  		},
   321  	}
   322  
   323  	command.Flags().StringArrayVar(&groups, "group", nil, fmt.Sprintf(
   324  		"Optional list of setting groups that have to be validated ( one of: %s)", strings.Join(allGroups, ", ")))
   325  
   326  	return command
   327  }
   328  
   329  func NewResourceOverridesCommand(cmdCtx commandContext) *cobra.Command {
   330  	command := &cobra.Command{
   331  		Use:   "resource-overrides",
   332  		Short: "Troubleshoot resource overrides",
   333  		Run: func(c *cobra.Command, args []string) {
   334  			c.HelpFunc()(c, args)
   335  		},
   336  	}
   337  	command.AddCommand(NewResourceIgnoreDifferencesCommand(cmdCtx))
   338  	command.AddCommand(NewResourceIgnoreResourceUpdatesCommand(cmdCtx))
   339  	command.AddCommand(NewResourceActionListCommand(cmdCtx))
   340  	command.AddCommand(NewResourceActionRunCommand(cmdCtx))
   341  	command.AddCommand(NewResourceHealthCommand(cmdCtx))
   342  	return command
   343  }
   344  
   345  func executeResourceOverrideCommand(ctx context.Context, cmdCtx commandContext, args []string, callback func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride)) {
   346  	data, err := os.ReadFile(args[0])
   347  	errors.CheckError(err)
   348  
   349  	res := unstructured.Unstructured{}
   350  	errors.CheckError(yaml.Unmarshal(data, &res))
   351  
   352  	settingsManager, err := cmdCtx.createSettingsManager(ctx)
   353  	errors.CheckError(err)
   354  
   355  	overrides, err := settingsManager.GetResourceOverrides()
   356  	errors.CheckError(err)
   357  	gvk := res.GroupVersionKind()
   358  	key := gvk.Kind
   359  	if gvk.Group != "" {
   360  		key = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
   361  	}
   362  	override := overrides[key]
   363  	callback(res, override, overrides)
   364  }
   365  
   366  func executeIgnoreResourceUpdatesOverrideCommand(ctx context.Context, cmdCtx commandContext, args []string, callback func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride)) {
   367  	data, err := os.ReadFile(args[0])
   368  	errors.CheckError(err)
   369  
   370  	res := unstructured.Unstructured{}
   371  	errors.CheckError(yaml.Unmarshal(data, &res))
   372  
   373  	settingsManager, err := cmdCtx.createSettingsManager(ctx)
   374  	errors.CheckError(err)
   375  
   376  	overrides, err := settingsManager.GetIgnoreResourceUpdatesOverrides()
   377  	errors.CheckError(err)
   378  	gvk := res.GroupVersionKind()
   379  	key := gvk.Kind
   380  	if gvk.Group != "" {
   381  		key = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
   382  	}
   383  	override, hasOverride := overrides[key]
   384  	if !hasOverride {
   385  		_, _ = fmt.Printf("No overrides configured for '%s/%s'\n", gvk.Group, gvk.Kind)
   386  		return
   387  	}
   388  	callback(res, override, overrides)
   389  }
   390  
   391  func NewResourceIgnoreDifferencesCommand(cmdCtx commandContext) *cobra.Command {
   392  	command := &cobra.Command{
   393  		Use:   "ignore-differences RESOURCE_YAML_PATH",
   394  		Short: "Renders fields excluded from diffing",
   395  		Long:  "Renders ignored fields using the 'ignoreDifferences' setting specified in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
   396  		Example: `
   397  argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
   398  		Run: func(c *cobra.Command, args []string) {
   399  			ctx := c.Context()
   400  
   401  			if len(args) < 1 {
   402  				c.HelpFunc()(c, args)
   403  				os.Exit(1)
   404  			}
   405  
   406  			executeResourceOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
   407  				gvk := res.GroupVersionKind()
   408  				if len(override.IgnoreDifferences.JSONPointers) == 0 && len(override.IgnoreDifferences.JQPathExpressions) == 0 {
   409  					_, _ = fmt.Printf("Ignore differences are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
   410  					return
   411  				}
   412  
   413  				// This normalizer won't verify 'managedFieldsManagers' ignore difference
   414  				// configurations. This requires access to live resources which is not the
   415  				// purpose of this command. This will just apply jsonPointers and
   416  				// jqPathExpressions configurations.
   417  				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, normalizers.IgnoreNormalizerOpts{})
   418  				errors.CheckError(err)
   419  
   420  				normalizedRes := res.DeepCopy()
   421  				logs := collectLogs(func() {
   422  					errors.CheckError(normalizer.Normalize(normalizedRes))
   423  				})
   424  				if logs != "" {
   425  					_, _ = fmt.Println(logs)
   426  				}
   427  
   428  				if reflect.DeepEqual(&res, normalizedRes) {
   429  					_, _ = fmt.Printf("No fields are ignored by ignoreDifferences settings: \n%s\n", override.IgnoreDifferences)
   430  					return
   431  				}
   432  
   433  				_, _ = fmt.Printf("Following fields are ignored:\n\n")
   434  				_ = cli.PrintDiff(res.GetName(), &res, normalizedRes)
   435  			})
   436  		},
   437  	}
   438  	return command
   439  }
   440  
   441  func NewResourceIgnoreResourceUpdatesCommand(cmdCtx commandContext) *cobra.Command {
   442  	var ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
   443  	command := &cobra.Command{
   444  		Use:   "ignore-resource-updates RESOURCE_YAML_PATH",
   445  		Short: "Renders fields excluded from resource updates",
   446  		Long:  "Renders ignored fields using the 'ignoreResourceUpdates' setting specified in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
   447  		Example: `
   448  argocd admin settings resource-overrides ignore-resource-updates ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
   449  		Run: func(c *cobra.Command, args []string) {
   450  			ctx := c.Context()
   451  
   452  			if len(args) < 1 {
   453  				c.HelpFunc()(c, args)
   454  				os.Exit(1)
   455  			}
   456  
   457  			executeIgnoreResourceUpdatesOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
   458  				gvk := res.GroupVersionKind()
   459  				if len(override.IgnoreResourceUpdates.JSONPointers) == 0 && len(override.IgnoreResourceUpdates.JQPathExpressions) == 0 {
   460  					_, _ = fmt.Printf("Ignore resource updates are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
   461  					return
   462  				}
   463  
   464  				normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides, ignoreNormalizerOpts)
   465  				errors.CheckError(err)
   466  
   467  				normalizedRes := res.DeepCopy()
   468  				logs := collectLogs(func() {
   469  					errors.CheckError(normalizer.Normalize(normalizedRes))
   470  				})
   471  				if logs != "" {
   472  					_, _ = fmt.Println(logs)
   473  				}
   474  
   475  				if reflect.DeepEqual(&res, normalizedRes) {
   476  					_, _ = fmt.Printf("No fields are ignored by ignoreResourceUpdates settings: \n%s\n", override.IgnoreResourceUpdates)
   477  					return
   478  				}
   479  
   480  				_, _ = fmt.Printf("Following fields are ignored:\n\n")
   481  				_ = cli.PrintDiff(res.GetName(), &res, normalizedRes)
   482  			})
   483  		},
   484  	}
   485  	command.Flags().DurationVar(&ignoreNormalizerOpts.JQExecutionTimeout, "ignore-normalizer-jq-execution-timeout", normalizers.DefaultJQExecutionTimeout, "Set ignore normalizer JQ execution timeout")
   486  	return command
   487  }
   488  
   489  func NewResourceHealthCommand(cmdCtx commandContext) *cobra.Command {
   490  	command := &cobra.Command{
   491  		Use:   "health RESOURCE_YAML_PATH",
   492  		Short: "Assess resource health",
   493  		Long:  "Assess resource health using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
   494  		Example: `
   495  argocd admin settings resource-overrides health ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
   496  		Run: func(c *cobra.Command, args []string) {
   497  			ctx := c.Context()
   498  
   499  			if len(args) < 1 {
   500  				c.HelpFunc()(c, args)
   501  				os.Exit(1)
   502  			}
   503  
   504  			executeResourceOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, _ v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
   505  				gvk := res.GroupVersionKind()
   506  				resHealth, err := healthutil.GetResourceHealth(&res, lua.ResourceHealthOverrides(overrides))
   507  				switch {
   508  				case err != nil:
   509  					errors.CheckError(err)
   510  				case resHealth == nil:
   511  					fmt.Printf("Health script is not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
   512  				default:
   513  					_, _ = fmt.Printf("STATUS: %s\n", resHealth.Status)
   514  					_, _ = fmt.Printf("MESSAGE: %s\n", resHealth.Message)
   515  				}
   516  			})
   517  		},
   518  	}
   519  	return command
   520  }
   521  
   522  func NewResourceActionListCommand(cmdCtx commandContext) *cobra.Command {
   523  	command := &cobra.Command{
   524  		Use:   "list-actions RESOURCE_YAML_PATH",
   525  		Short: "List available resource actions",
   526  		Long:  "List actions available for given resource action using the lua scripts configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap and outputs updated fields",
   527  		Example: `
   528  argocd admin settings resource-overrides action list /tmp/deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
   529  		Run: func(c *cobra.Command, args []string) {
   530  			ctx := c.Context()
   531  
   532  			if len(args) < 1 {
   533  				c.HelpFunc()(c, args)
   534  				os.Exit(1)
   535  			}
   536  
   537  			executeResourceOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
   538  				gvk := res.GroupVersionKind()
   539  				if override.Actions == "" {
   540  					_, _ = fmt.Printf("Actions are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
   541  					return
   542  				}
   543  
   544  				luaVM := lua.VM{ResourceOverrides: overrides}
   545  				discoveryScript, err := luaVM.GetResourceActionDiscovery(&res)
   546  				errors.CheckError(err)
   547  
   548  				availableActions, err := luaVM.ExecuteResourceActionDiscovery(&res, discoveryScript)
   549  				errors.CheckError(err)
   550  				sort.Slice(availableActions, func(i, j int) bool {
   551  					return availableActions[i].Name < availableActions[j].Name
   552  				})
   553  
   554  				w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   555  				_, _ = fmt.Fprintf(w, "NAME\tDISABLED\n")
   556  				for _, action := range availableActions {
   557  					_, _ = fmt.Fprintf(w, "%s\t%s\n", action.Name, strconv.FormatBool(action.Disabled))
   558  				}
   559  				_ = w.Flush()
   560  			})
   561  		},
   562  	}
   563  	return command
   564  }
   565  
   566  func NewResourceActionRunCommand(cmdCtx commandContext) *cobra.Command {
   567  	var resourceActionParameters []string
   568  
   569  	command := &cobra.Command{
   570  		Use:     "run-action RESOURCE_YAML_PATH ACTION",
   571  		Aliases: []string{"action"},
   572  		Short:   "Executes resource action",
   573  		Long:    "Executes resource action using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap and outputs updated fields",
   574  		Example: `
   575  argocd admin settings resource-overrides action /tmp/deploy.yaml restart --argocd-cm-path ./argocd-cm.yaml`,
   576  		Run: func(c *cobra.Command, args []string) {
   577  			ctx := c.Context()
   578  
   579  			if len(args) < 2 {
   580  				c.HelpFunc()(c, args)
   581  				os.Exit(1)
   582  			}
   583  			action := args[1]
   584  
   585  			// Parse resource action parameters
   586  			parsedParams := make([]*applicationpkg.ResourceActionParameters, 0)
   587  			if len(resourceActionParameters) > 0 {
   588  				for _, param := range resourceActionParameters {
   589  					parts := strings.SplitN(param, "=", 2)
   590  					if len(parts) != 2 {
   591  						log.Fatalf("Invalid parameter format: %s", param)
   592  					}
   593  					name := parts[0]
   594  					value := parts[1]
   595  					parsedParams = append(parsedParams, &applicationpkg.ResourceActionParameters{
   596  						Name:  &name,
   597  						Value: &value,
   598  					})
   599  				}
   600  			}
   601  
   602  			executeResourceOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
   603  				gvk := res.GroupVersionKind()
   604  				if override.Actions == "" {
   605  					_, _ = fmt.Printf("Actions are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
   606  					return
   607  				}
   608  
   609  				luaVM := lua.VM{ResourceOverrides: overrides}
   610  				action, err := luaVM.GetResourceAction(&res, action)
   611  				errors.CheckError(err)
   612  
   613  				modifiedRes, err := luaVM.ExecuteResourceAction(&res, action.ActionLua, parsedParams)
   614  				errors.CheckError(err)
   615  
   616  				for _, impactedResource := range modifiedRes {
   617  					result := impactedResource.UnstructuredObj
   618  					switch impactedResource.K8SOperation {
   619  					// No default case since a not supported operation would have failed upon unmarshaling earlier
   620  					case lua.PatchOperation:
   621  						if reflect.DeepEqual(&res, modifiedRes) {
   622  							_, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name)
   623  							return
   624  						}
   625  
   626  						_, _ = fmt.Printf("Following fields have been changed:\n\n")
   627  						_ = cli.PrintDiff(res.GetName(), &res, result)
   628  					case lua.CreateOperation:
   629  						yamlBytes, err := yaml.Marshal(impactedResource.UnstructuredObj)
   630  						errors.CheckError(err)
   631  						fmt.Println("Following resource was created:")
   632  						fmt.Println(bytes.NewBuffer(yamlBytes).String())
   633  					}
   634  				}
   635  			})
   636  		},
   637  	}
   638  
   639  	command.Flags().StringArrayVar(&resourceActionParameters, "param", []string{}, "Action parameters (e.g. --param key1=value1)")
   640  	return command
   641  }