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

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"text/tabwriter"
    10  
    11  	"google.golang.org/grpc/codes"
    12  	"google.golang.org/grpc/status"
    13  
    14  	"github.com/mattn/go-isatty"
    15  	log "github.com/sirupsen/logrus"
    16  	"github.com/spf13/cobra"
    17  	"k8s.io/client-go/kubernetes"
    18  	"k8s.io/client-go/rest"
    19  	"k8s.io/client-go/tools/clientcmd"
    20  
    21  	"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
    22  	cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
    23  	"github.com/argoproj/argo-cd/v3/common"
    24  	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
    25  	clusterpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
    26  	argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
    27  	"github.com/argoproj/argo-cd/v3/util/cli"
    28  	"github.com/argoproj/argo-cd/v3/util/clusterauth"
    29  	"github.com/argoproj/argo-cd/v3/util/errors"
    30  	utilio "github.com/argoproj/argo-cd/v3/util/io"
    31  	"github.com/argoproj/argo-cd/v3/util/text/label"
    32  )
    33  
    34  const (
    35  	// type of the cluster ID is 'name'
    36  	clusterIdTypeName = "name"
    37  	// cluster field is 'name'
    38  	clusterFieldName = "name"
    39  	// cluster field is 'namespaces'
    40  	clusterFieldNamespaces = "namespaces"
    41  	// cluster field is 'labels'
    42  	clusterFieldLabel = "labels"
    43  	// cluster field is 'annotations'
    44  	clusterFieldAnnotation = "annotations"
    45  	// indicates managing all namespaces
    46  	allNamespaces = "*"
    47  )
    48  
    49  // NewClusterCommand returns a new instance of an `argocd cluster` command
    50  func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
    51  	command := &cobra.Command{
    52  		Use:   "cluster",
    53  		Short: "Manage cluster credentials",
    54  		Run: func(c *cobra.Command, args []string) {
    55  			c.HelpFunc()(c, args)
    56  			os.Exit(1)
    57  		},
    58  		Example: `  # List all known clusters in JSON format:
    59    argocd cluster list -o json
    60  
    61    # Add a target cluster configuration to ArgoCD. The context must exist in your kubectl config:
    62    argocd cluster add example-cluster
    63  
    64    # Get specific details about a cluster in plain text (wide) format:
    65    argocd cluster get example-cluster -o wide
    66  
    67    # Remove a target cluster context from ArgoCD
    68    argocd cluster rm example-cluster
    69  
    70    # Set a target cluster context from ArgoCD
    71    argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace '*'
    72    argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace namespace-one --namespace namespace-two`,
    73  	}
    74  
    75  	command.AddCommand(NewClusterAddCommand(clientOpts, pathOpts))
    76  	command.AddCommand(NewClusterGetCommand(clientOpts))
    77  	command.AddCommand(NewClusterListCommand(clientOpts))
    78  	command.AddCommand(NewClusterRemoveCommand(clientOpts, pathOpts))
    79  	command.AddCommand(NewClusterRotateAuthCommand(clientOpts))
    80  	command.AddCommand(NewClusterSetCommand(clientOpts))
    81  	return command
    82  }
    83  
    84  // NewClusterAddCommand returns a new instance of an `argocd cluster add` command
    85  func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
    86  	var (
    87  		clusterOpts      cmdutil.ClusterOptions
    88  		skipConfirmation bool
    89  		labels           []string
    90  		annotations      []string
    91  	)
    92  	command := &cobra.Command{
    93  		Use:   "add CONTEXT",
    94  		Short: cliName + " cluster add CONTEXT",
    95  		Run: func(c *cobra.Command, args []string) {
    96  			ctx := c.Context()
    97  
    98  			var configAccess clientcmd.ConfigAccess = pathOpts
    99  			if len(args) == 0 {
   100  				log.Error("Choose a context name from:")
   101  				cmdutil.PrintKubeContexts(configAccess)
   102  				os.Exit(1)
   103  			}
   104  
   105  			if clusterOpts.InCluster && clusterOpts.ClusterEndpoint != "" {
   106  				log.Fatal("Can only use one of --in-cluster or --cluster-endpoint")
   107  				return
   108  			}
   109  
   110  			contextName := args[0]
   111  			conf, err := getRestConfig(pathOpts, contextName)
   112  			errors.CheckError(err)
   113  			if clusterOpts.ProxyUrl != "" {
   114  				u, err := argoappv1.ParseProxyUrl(clusterOpts.ProxyUrl)
   115  				errors.CheckError(err)
   116  				conf.Proxy = http.ProxyURL(u)
   117  			}
   118  			clientset, err := kubernetes.NewForConfig(conf)
   119  			errors.CheckError(err)
   120  			managerBearerToken := ""
   121  			var awsAuthConf *argoappv1.AWSAuthConfig
   122  			var execProviderConf *argoappv1.ExecProviderConfig
   123  			switch {
   124  			case clusterOpts.AwsClusterName != "":
   125  				awsAuthConf = &argoappv1.AWSAuthConfig{
   126  					ClusterName: clusterOpts.AwsClusterName,
   127  					RoleARN:     clusterOpts.AwsRoleArn,
   128  					Profile:     clusterOpts.AwsProfile,
   129  				}
   130  			case clusterOpts.ExecProviderCommand != "":
   131  				execProviderConf = &argoappv1.ExecProviderConfig{
   132  					Command:     clusterOpts.ExecProviderCommand,
   133  					Args:        clusterOpts.ExecProviderArgs,
   134  					Env:         clusterOpts.ExecProviderEnv,
   135  					APIVersion:  clusterOpts.ExecProviderAPIVersion,
   136  					InstallHint: clusterOpts.ExecProviderInstallHint,
   137  				}
   138  			default:
   139  				// Install RBAC resources for managing the cluster
   140  				if clusterOpts.ServiceAccount != "" {
   141  					managerBearerToken, err = clusterauth.GetServiceAccountBearerToken(clientset, clusterOpts.SystemNamespace, clusterOpts.ServiceAccount, common.BearerTokenTimeout)
   142  				} else {
   143  					isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
   144  					if isTerminal && !skipConfirmation {
   145  						accessLevel := "cluster"
   146  						if len(clusterOpts.Namespaces) > 0 {
   147  							accessLevel = "namespace"
   148  						}
   149  						message := fmt.Sprintf("WARNING: This will create a service account `argocd-manager` on the cluster referenced by context `%s` with full %s level privileges. Do you want to continue [y/N]? ", contextName, accessLevel)
   150  						if !cli.AskToProceed(message) {
   151  							os.Exit(1)
   152  						}
   153  					}
   154  					managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, clusterOpts.SystemNamespace, clusterOpts.Namespaces, common.BearerTokenTimeout)
   155  				}
   156  				errors.CheckError(err)
   157  			}
   158  
   159  			labelsMap, err := label.Parse(labels)
   160  			errors.CheckError(err)
   161  			annotationsMap, err := label.Parse(annotations)
   162  			errors.CheckError(err)
   163  
   164  			conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
   165  			defer utilio.Close(conn)
   166  			if clusterOpts.Name != "" {
   167  				contextName = clusterOpts.Name
   168  			}
   169  			clst := cmdutil.NewCluster(contextName, clusterOpts.Namespaces, clusterOpts.ClusterResources, conf, managerBearerToken, awsAuthConf, execProviderConf, labelsMap, annotationsMap)
   170  			if clusterOpts.InClusterEndpoint() {
   171  				clst.Server = argoappv1.KubernetesInternalAPIServerAddr
   172  			} else if clusterOpts.ClusterEndpoint == string(cmdutil.KubePublicEndpoint) {
   173  				endpoint, caData, err := cmdutil.GetKubePublicEndpoint(clientset)
   174  				if err != nil || endpoint == "" {
   175  					log.Warnf("Failed to find the cluster endpoint from kube-public data: %v", err)
   176  					log.Infof("Falling back to the endpoint '%s' as listed in the kubeconfig context", clst.Server)
   177  					endpoint = clst.Server
   178  				}
   179  				clst.Server = endpoint
   180  				clst.Config.CAData = caData
   181  			}
   182  
   183  			if clusterOpts.Shard >= 0 {
   184  				clst.Shard = &clusterOpts.Shard
   185  			}
   186  			if clusterOpts.Project != "" {
   187  				clst.Project = clusterOpts.Project
   188  			}
   189  			clstCreateReq := clusterpkg.ClusterCreateRequest{
   190  				Cluster: clst,
   191  				Upsert:  clusterOpts.Upsert,
   192  			}
   193  			_, err = clusterIf.Create(ctx, &clstCreateReq)
   194  			errors.CheckError(err)
   195  			fmt.Printf("Cluster '%s' added\n", clst.Server)
   196  		},
   197  	}
   198  	command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
   199  	command.Flags().BoolVar(&clusterOpts.Upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
   200  	command.Flags().StringVar(&clusterOpts.ServiceAccount, "service-account", "", fmt.Sprintf("System namespace service account to use for kubernetes resource management. If not set then default %q SA will be created", clusterauth.ArgoCDManagerServiceAccount))
   201  	command.Flags().StringVar(&clusterOpts.SystemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
   202  	command.Flags().BoolVarP(&skipConfirmation, "yes", "y", false, "Skip explicit confirmation")
   203  	command.Flags().StringArrayVar(&labels, "label", nil, "Set metadata labels (e.g. --label key=value)")
   204  	command.Flags().StringArrayVar(&annotations, "annotation", nil, "Set metadata annotations (e.g. --annotation key=value)")
   205  	command.Flags().StringVar(&clusterOpts.ProxyUrl, "proxy-url", "", "use proxy to connect cluster")
   206  	cmdutil.AddClusterFlags(command, &clusterOpts)
   207  	return command
   208  }
   209  
   210  func getRestConfig(pathOpts *clientcmd.PathOptions, ctxName string) (*rest.Config, error) {
   211  	config, err := pathOpts.GetStartingConfig()
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	clstContext := config.Contexts[ctxName]
   217  	if clstContext == nil {
   218  		return nil, fmt.Errorf("context %s does not exist in kubeconfig", ctxName)
   219  	}
   220  
   221  	overrides := clientcmd.ConfigOverrides{
   222  		Context: *clstContext,
   223  	}
   224  
   225  	clientConfig := clientcmd.NewDefaultClientConfig(*config, &overrides)
   226  	conf, err := clientConfig.ClientConfig()
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	return conf, nil
   232  }
   233  
   234  // NewClusterSetCommand returns a new instance of an `argocd cluster set` command
   235  func NewClusterSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   236  	var (
   237  		clusterOptions cmdutil.ClusterOptions
   238  		clusterName    string
   239  		labels         []string
   240  		annotations    []string
   241  	)
   242  	command := &cobra.Command{
   243  		Use:   "set NAME",
   244  		Short: "Set cluster information",
   245  		Example: `  # Set cluster information
   246    argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace '*'
   247    argocd cluster set CLUSTER_NAME --name new-cluster-name --namespace namespace-one --namespace namespace-two`,
   248  		Run: func(c *cobra.Command, args []string) {
   249  			ctx := c.Context()
   250  			if len(args) != 1 {
   251  				c.HelpFunc()(c, args)
   252  				os.Exit(1)
   253  			}
   254  			// name of the cluster whose fields have to be updated.
   255  			clusterName = args[0]
   256  			conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
   257  			defer utilio.Close(conn)
   258  			// checks the fields that needs to be updated
   259  			updatedFields := checkFieldsToUpdate(clusterOptions, labels, annotations)
   260  			namespaces := clusterOptions.Namespaces
   261  			// check if all namespaces have to be considered
   262  			if len(namespaces) == 1 && strings.EqualFold(namespaces[0], allNamespaces) {
   263  				namespaces[0] = ""
   264  			}
   265  			// parse the labels you're receiving from the label flag
   266  			labelsMap, err := label.Parse(labels)
   267  			errors.CheckError(err)
   268  			// parse the annotations you're receiving from the annotation flag
   269  			annotationsMap, err := label.Parse(annotations)
   270  			errors.CheckError(err)
   271  			if updatedFields != nil {
   272  				clusterUpdateRequest := clusterpkg.ClusterUpdateRequest{
   273  					Cluster: &argoappv1.Cluster{
   274  						Name:        clusterOptions.Name,
   275  						Namespaces:  namespaces,
   276  						Labels:      labelsMap,
   277  						Annotations: annotationsMap,
   278  					},
   279  					UpdatedFields: updatedFields,
   280  					Id: &clusterpkg.ClusterID{
   281  						Type:  clusterIdTypeName,
   282  						Value: clusterName,
   283  					},
   284  				}
   285  				_, err := clusterIf.Update(ctx, &clusterUpdateRequest)
   286  				if err != nil {
   287  					if status.Code(err) == codes.PermissionDenied {
   288  						log.Error("Ensure that the cluster is present and you have the necessary permissions to update the cluster")
   289  					}
   290  					errors.CheckError(err)
   291  				}
   292  				fmt.Printf("Cluster '%s' updated.\n", clusterName)
   293  			} else {
   294  				fmt.Print("Specify the cluster field to be updated.\n")
   295  			}
   296  		},
   297  	}
   298  	command.Flags().StringVar(&clusterOptions.Name, "name", "", "Overwrite the cluster name")
   299  	command.Flags().StringArrayVar(&clusterOptions.Namespaces, "namespace", nil, "List of namespaces which are allowed to manage. Specify '*' to manage all namespaces")
   300  	command.Flags().StringArrayVar(&labels, "label", nil, "Set metadata labels (e.g. --label key=value)")
   301  	command.Flags().StringArrayVar(&annotations, "annotation", nil, "Set metadata annotations (e.g. --annotation key=value)")
   302  	return command
   303  }
   304  
   305  // checkFieldsToUpdate returns the fields that needs to be updated
   306  func checkFieldsToUpdate(clusterOptions cmdutil.ClusterOptions, labels []string, annotations []string) []string {
   307  	var updatedFields []string
   308  	if clusterOptions.Name != "" {
   309  		updatedFields = append(updatedFields, clusterFieldName)
   310  	}
   311  	if clusterOptions.Namespaces != nil {
   312  		updatedFields = append(updatedFields, clusterFieldNamespaces)
   313  	}
   314  	if labels != nil {
   315  		updatedFields = append(updatedFields, clusterFieldLabel)
   316  	}
   317  	if annotations != nil {
   318  		updatedFields = append(updatedFields, clusterFieldAnnotation)
   319  	}
   320  	return updatedFields
   321  }
   322  
   323  // NewClusterGetCommand returns a new instance of an `argocd cluster get` command
   324  func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   325  	var output string
   326  	command := &cobra.Command{
   327  		Use:   "get SERVER/NAME",
   328  		Short: "Get cluster information",
   329  		Example: `argocd cluster get https://12.34.567.89
   330  argocd cluster get in-cluster`,
   331  		Run: func(c *cobra.Command, args []string) {
   332  			ctx := c.Context()
   333  
   334  			if len(args) == 0 {
   335  				c.HelpFunc()(c, args)
   336  				os.Exit(1)
   337  			}
   338  			conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
   339  			defer utilio.Close(conn)
   340  			clusters := make([]argoappv1.Cluster, 0)
   341  			for _, clusterSelector := range args {
   342  				clst, err := clusterIf.Get(ctx, getQueryBySelector(clusterSelector))
   343  				errors.CheckError(err)
   344  				clusters = append(clusters, *clst)
   345  			}
   346  			switch output {
   347  			case "yaml", "json":
   348  				err := PrintResourceList(clusters, output, true)
   349  				errors.CheckError(err)
   350  			case "wide", "":
   351  				printClusterDetails(clusters)
   352  			case "server":
   353  				printClusterServers(clusters)
   354  			default:
   355  				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
   356  			}
   357  		},
   358  	}
   359  	// we have yaml as default to not break backwards-compatibility
   360  	command.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: json|yaml|wide|server")
   361  	return command
   362  }
   363  
   364  func strWithDefault(value string, def string) string {
   365  	if value == "" {
   366  		return def
   367  	}
   368  	return value
   369  }
   370  
   371  func formatNamespaces(cluster argoappv1.Cluster) string {
   372  	if len(cluster.Namespaces) == 0 {
   373  		return "all namespaces"
   374  	}
   375  	return strings.Join(cluster.Namespaces, ", ")
   376  }
   377  
   378  func printClusterDetails(clusters []argoappv1.Cluster) {
   379  	for _, cluster := range clusters {
   380  		fmt.Printf("Cluster information\n\n")
   381  		fmt.Printf("  Server URL:            %s\n", cluster.Server)
   382  		fmt.Printf("  Server Name:           %s\n", strWithDefault(cluster.Name, "-"))
   383  		//nolint:staticcheck
   384  		fmt.Printf("  Server Version:        %s\n", cluster.ServerVersion)
   385  		fmt.Printf("  Namespaces:        	 %s\n", formatNamespaces(cluster))
   386  		fmt.Printf("\nTLS configuration\n\n")
   387  		fmt.Printf("  Client cert:           %v\n", len(cluster.Config.CertData) != 0)
   388  		fmt.Printf("  Cert validation:       %v\n", !cluster.Config.Insecure)
   389  		fmt.Printf("\nAuthentication\n\n")
   390  		fmt.Printf("  Basic authentication:  %v\n", cluster.Config.Username != "")
   391  		fmt.Printf("  oAuth authentication:  %v\n", cluster.Config.BearerToken != "")
   392  		fmt.Printf("  AWS authentication:    %v\n", cluster.Config.AWSAuthConfig != nil)
   393  		fmt.Printf("\nDisable compression: %v\n", cluster.Config.DisableCompression)
   394  		fmt.Printf("\nUse proxy: %v\n", cluster.Config.ProxyUrl != "")
   395  		fmt.Println()
   396  	}
   397  }
   398  
   399  // NewClusterRemoveCommand returns a new instance of an `argocd cluster rm` command
   400  func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
   401  	var noPrompt bool
   402  	command := &cobra.Command{
   403  		Use:   "rm SERVER/NAME",
   404  		Short: "Remove cluster credentials",
   405  		Example: `argocd cluster rm https://12.34.567.89
   406  argocd cluster rm cluster-name`,
   407  		Run: func(c *cobra.Command, args []string) {
   408  			ctx := c.Context()
   409  
   410  			if len(args) == 0 {
   411  				c.HelpFunc()(c, args)
   412  				os.Exit(1)
   413  			}
   414  			conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
   415  			defer utilio.Close(conn)
   416  			numOfClusters := len(args)
   417  			var isConfirmAll bool
   418  
   419  			for _, clusterSelector := range args {
   420  				clusterQuery := getQueryBySelector(clusterSelector)
   421  				var lowercaseAnswer string
   422  				if !noPrompt {
   423  					if numOfClusters == 1 {
   424  						lowercaseAnswer = cli.AskToProceedS("Are you sure you want to remove '" + clusterSelector + "'? Any Apps deploying to this cluster will go to health status Unknown.[y/n] ")
   425  					} else {
   426  						if !isConfirmAll {
   427  							lowercaseAnswer = cli.AskToProceedS("Are you sure you want to remove '" + clusterSelector + "'? Any Apps deploying to this cluster will go to health status Unknown.[y/n/A] where 'A' is to remove all specified clusters without prompting. Any Apps deploying to these clusters will go to health status Unknown. ")
   428  							if lowercaseAnswer == "a" {
   429  								lowercaseAnswer = "y"
   430  								isConfirmAll = true
   431  							}
   432  						} else {
   433  							lowercaseAnswer = "y"
   434  						}
   435  					}
   436  				} else {
   437  					lowercaseAnswer = "y"
   438  				}
   439  
   440  				if lowercaseAnswer == "y" {
   441  					// get the cluster name to use as context to delete RBAC on cluster
   442  					clst, err := clusterIf.Get(ctx, clusterQuery)
   443  					errors.CheckError(err)
   444  
   445  					// remove cluster
   446  					_, err = clusterIf.Delete(ctx, clusterQuery)
   447  					errors.CheckError(err)
   448  					fmt.Printf("Cluster '%s' removed\n", clusterSelector)
   449  
   450  					// remove RBAC from cluster
   451  					conf, err := getRestConfig(pathOpts, clst.Name)
   452  					errors.CheckError(err)
   453  
   454  					clientset, err := kubernetes.NewForConfig(conf)
   455  					errors.CheckError(err)
   456  
   457  					err = clusterauth.UninstallClusterManagerRBAC(clientset)
   458  					errors.CheckError(err)
   459  				} else {
   460  					fmt.Println("The command to remove '" + clusterSelector + "' was cancelled.")
   461  				}
   462  			}
   463  		},
   464  	}
   465  	command.Flags().BoolVarP(&noPrompt, "yes", "y", false, "Turn off prompting to confirm remove of cluster resources")
   466  	return command
   467  }
   468  
   469  // Print table of cluster information
   470  func printClusterTable(clusters []argoappv1.Cluster) {
   471  	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
   472  	_, _ = fmt.Fprintf(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\tPROJECT\n")
   473  	for _, c := range clusters {
   474  		server := c.Server
   475  		if len(c.Namespaces) > 0 {
   476  			server = fmt.Sprintf("%s (%d namespaces)", c.Server, len(c.Namespaces))
   477  		}
   478  		//nolint:staticcheck
   479  		_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", server, c.Name, c.ServerVersion, c.ConnectionState.Status, c.ConnectionState.Message, c.Project)
   480  	}
   481  	_ = w.Flush()
   482  }
   483  
   484  // Returns cluster query for getting cluster depending on the cluster selector
   485  func getQueryBySelector(clusterSelector string) *clusterpkg.ClusterQuery {
   486  	var query clusterpkg.ClusterQuery
   487  	isServer, err := regexp.MatchString(`^https?://`, clusterSelector)
   488  	if isServer || err != nil {
   489  		query.Server = clusterSelector
   490  	} else {
   491  		query.Name = clusterSelector
   492  	}
   493  	return &query
   494  }
   495  
   496  // Print list of cluster servers
   497  func printClusterServers(clusters []argoappv1.Cluster) {
   498  	for _, c := range clusters {
   499  		fmt.Println(c.Server)
   500  	}
   501  }
   502  
   503  // NewClusterListCommand returns a new instance of an `argocd cluster rm` command
   504  func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   505  	var output string
   506  	command := &cobra.Command{
   507  		Use:   "list",
   508  		Short: "List configured clusters",
   509  		Run: func(c *cobra.Command, _ []string) {
   510  			ctx := c.Context()
   511  
   512  			conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
   513  			defer utilio.Close(conn)
   514  			clusters, err := clusterIf.List(ctx, &clusterpkg.ClusterQuery{})
   515  			errors.CheckError(err)
   516  			switch output {
   517  			case "yaml", "json":
   518  				err := PrintResourceList(clusters.Items, output, false)
   519  				errors.CheckError(err)
   520  			case "server":
   521  				printClusterServers(clusters.Items)
   522  			case "wide", "":
   523  				printClusterTable(clusters.Items)
   524  			default:
   525  				errors.CheckError(fmt.Errorf("unknown output format: %s", output))
   526  			}
   527  		},
   528  		Example: `
   529  # List Clusters in Default "Wide" Format
   530  argocd cluster list
   531  
   532  # List Cluster via specifying the server
   533  argocd cluster list --server <ARGOCD_SERVER_ADDRESS>
   534  
   535  # List Clusters in JSON Format
   536  argocd cluster list -o json --server <ARGOCD_SERVER_ADDRESS>
   537  
   538  # List Clusters in YAML Format
   539  argocd cluster list -o yaml --server <ARGOCD_SERVER_ADDRESS>
   540  
   541  # List Clusters that have been added to your Argo CD 
   542  argocd cluster list -o server <ARGOCD_SERVER_ADDRESS>
   543  
   544  `,
   545  	}
   546  	command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|server")
   547  	return command
   548  }
   549  
   550  // NewClusterRotateAuthCommand returns a new instance of an `argocd cluster rotate-auth` command
   551  func NewClusterRotateAuthCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
   552  	command := &cobra.Command{
   553  		Use:   "rotate-auth SERVER/NAME",
   554  		Short: cliName + " cluster rotate-auth SERVER/NAME",
   555  		Example: `argocd cluster rotate-auth https://12.34.567.89
   556  argocd cluster rotate-auth cluster-name`,
   557  		Run: func(c *cobra.Command, args []string) {
   558  			ctx := c.Context()
   559  
   560  			if len(args) != 1 {
   561  				c.HelpFunc()(c, args)
   562  				os.Exit(1)
   563  			}
   564  			conn, clusterIf := headless.NewClientOrDie(clientOpts, c).NewClusterClientOrDie()
   565  			defer utilio.Close(conn)
   566  
   567  			cluster := args[0]
   568  			clusterQuery := getQueryBySelector(cluster)
   569  			_, err := clusterIf.RotateAuth(ctx, clusterQuery)
   570  			errors.CheckError(err)
   571  
   572  			fmt.Printf("Cluster '%s' rotated auth\n", cluster)
   573  		},
   574  	}
   575  	return command
   576  }