github.com/argoproj/argo-cd@v1.8.7/cmd/argocd-util/commands/settings.go (about)

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