github.com/oam-dev/kubevela@v1.9.11/references/cli/cluster.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  
    25  	"github.com/crossplane/crossplane-runtime/pkg/meta"
    26  	"github.com/fatih/color"
    27  	"github.com/kubevela/pkg/util/runtime"
    28  	"github.com/kubevela/pkg/util/slices"
    29  	clustergatewayapi "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
    30  	"github.com/oam-dev/cluster-gateway/pkg/config"
    31  	"github.com/pkg/errors"
    32  	"github.com/spf13/cobra"
    33  	"k8s.io/apimachinery/pkg/labels"
    34  	"k8s.io/client-go/tools/clientcmd"
    35  	"k8s.io/kubectl/pkg/util/i18n"
    36  	"k8s.io/kubectl/pkg/util/templates"
    37  	"k8s.io/utils/pointer"
    38  	"sigs.k8s.io/controller-runtime/pkg/client"
    39  
    40  	"github.com/oam-dev/kubevela/apis/types"
    41  	velacmd "github.com/oam-dev/kubevela/pkg/cmd"
    42  	"github.com/oam-dev/kubevela/pkg/multicluster"
    43  	"github.com/oam-dev/kubevela/pkg/utils/common"
    44  	cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
    45  )
    46  
    47  const (
    48  	// FlagClusterName specifies the cluster name
    49  	FlagClusterName = "name"
    50  	// FlagClusterManagementEngine specifies the cluster management type, eg: ocm
    51  	FlagClusterManagementEngine = "engine"
    52  	// FlagKubeConfigPath specifies the kubeconfig path
    53  	FlagKubeConfigPath = "kubeconfig-path"
    54  	// FlagInClusterBootstrap prescribes the cluster registration to use the internal
    55  	// IP from the kube-public/cluster-info configmap, otherwise the endpoint in the
    56  	// hub kubeconfig will be used for registration.
    57  	FlagInClusterBootstrap = "in-cluster-boostrap"
    58  
    59  	// CreateNamespace specifies the namespace need to create in managedCluster
    60  	CreateNamespace = "create-namespace"
    61  
    62  	// CreateLabel specifies the labels need to create in managedCluster
    63  	CreateLabel = "labels"
    64  )
    65  
    66  // ClusterCommandGroup create a group of cluster command
    67  func ClusterCommandGroup(f velacmd.Factory, order string, c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
    68  	cmd := &cobra.Command{
    69  		Use:   "cluster",
    70  		Short: "Manage Kubernetes clusters.",
    71  		Long:  "Manage Kubernetes clusters for continuous delivery.",
    72  		Annotations: map[string]string{
    73  			types.TagCommandType:  types.TypePlatform,
    74  			types.TagCommandOrder: order,
    75  		},
    76  		// check if cluster-gateway is ready
    77  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
    78  			k8sClient, err := c.GetClient()
    79  			if err != nil {
    80  				return errors.Wrapf(err, "failed to get k8s client")
    81  			}
    82  			svc, err := multicluster.GetClusterGatewayService(context.Background(), k8sClient)
    83  			if err != nil {
    84  				return errors.Wrapf(err, "failed to get cluster secret namespace, please ensure cluster gateway is correctly deployed")
    85  			}
    86  			multicluster.ClusterGatewaySecretNamespace = svc.Namespace
    87  			return nil
    88  		},
    89  	}
    90  	cmd.SetOut(ioStreams.Out)
    91  	cmd.AddCommand(
    92  		NewClusterListCommand(&c),
    93  		NewClusterJoinCommand(&c, ioStreams),
    94  		NewClusterRenameCommand(&c),
    95  		NewClusterDetachCommand(&c),
    96  		NewClusterProbeCommand(&c),
    97  		NewClusterLabelCommandGroup(&c),
    98  		NewClusterAliasCommand(&c),
    99  		NewClusterExportConfigCommand(f, ioStreams),
   100  	)
   101  	return cmd
   102  }
   103  
   104  // NewClusterListCommand create cluster list command
   105  func NewClusterListCommand(c *common.Args) *cobra.Command {
   106  	cmd := &cobra.Command{
   107  		Use:     "list",
   108  		Aliases: []string{"ls"},
   109  		Short:   "list managed clusters.",
   110  		Long:    "list worker clusters managed by KubeVela.",
   111  		Args:    cobra.ExactArgs(0),
   112  		RunE: func(cmd *cobra.Command, args []string) error {
   113  			table := newUITable().AddRow("CLUSTER", "ALIAS", "TYPE", "ENDPOINT", "ACCEPTED", "LABELS")
   114  			clsClient, err := c.GetClient()
   115  			if err != nil {
   116  				return err
   117  			}
   118  			clusters, err := multicluster.NewClusterClient(clsClient).List(context.Background())
   119  			if err != nil {
   120  				return errors.Wrap(err, "fail to get registered cluster")
   121  			}
   122  			for _, cluster := range clusters.Items {
   123  				var labels []string
   124  				for k, v := range cluster.Labels {
   125  					if !strings.HasPrefix(k, config.MetaApiGroupName) {
   126  						labels = append(labels, color.CyanString(k)+"="+color.GreenString(v))
   127  					}
   128  				}
   129  				sort.Strings(labels)
   130  				if len(labels) == 0 {
   131  					labels = append(labels, "")
   132  				}
   133  				for i, l := range labels {
   134  					if i == 0 {
   135  						table.AddRow(cluster.Name, cluster.Spec.Alias, cluster.Spec.CredentialType, cluster.Spec.Endpoint, fmt.Sprintf("%v", cluster.Spec.Accepted), l)
   136  					} else {
   137  						table.AddRow("", "", "", "", "", l)
   138  					}
   139  				}
   140  			}
   141  			if len(table.Rows) == 1 {
   142  				cmd.Println("No cluster found.")
   143  			} else {
   144  				cmd.Println(table.String())
   145  			}
   146  			return nil
   147  		},
   148  	}
   149  	return cmd
   150  }
   151  
   152  // NewClusterJoinCommand create command to help user join cluster to multicluster management
   153  func NewClusterJoinCommand(c *common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
   154  	cmd := &cobra.Command{
   155  		Use:   "join [KUBECONFIG]",
   156  		Short: "join managed cluster.",
   157  		Long:  "join managed cluster by kubeconfig.",
   158  		Example: "# Join cluster declared in my-child-cluster.kubeconfig\n" +
   159  			"> vela cluster join my-child-cluster.kubeconfig --name example-cluster\n" +
   160  			"> vela cluster join my-child-cluster.kubeconfig --name example-cluster --labels project=kubevela,owner=oam-dev",
   161  		Args: cobra.ExactArgs(1),
   162  		RunE: func(cmd *cobra.Command, args []string) error {
   163  			// get ClusterName from flag or config
   164  			clusterName, err := cmd.Flags().GetString(FlagClusterName)
   165  			if err != nil {
   166  				return errors.Wrapf(err, "failed to get cluster name flag")
   167  			}
   168  			clusterManagementType, err := cmd.Flags().GetString(FlagClusterManagementEngine)
   169  			if err != nil {
   170  				return errors.Wrapf(err, "failed to get cluster management type flag")
   171  			}
   172  			// get need created namespace in managed cluster
   173  			createNamespace, err := cmd.Flags().GetString(CreateNamespace)
   174  			if err != nil {
   175  				return errors.Wrapf(err, "failed to get create namespace")
   176  			}
   177  			labels, err := cmd.Flags().GetString(CreateLabel)
   178  			if err != nil {
   179  				return errors.Wrapf(err, "failed to get label")
   180  			}
   181  			client, err := c.GetClient()
   182  			if err != nil {
   183  				return err
   184  			}
   185  			restConfig, err := c.GetConfig()
   186  			if err != nil {
   187  				return err
   188  			}
   189  
   190  			var inClusterBootstrap *bool
   191  			if _inClusterBootstrap, err := cmd.Flags().GetBool(FlagInClusterBootstrap); err == nil {
   192  				inClusterBootstrap = pointer.Bool(_inClusterBootstrap)
   193  			}
   194  
   195  			managedClusterKubeConfig := args[0]
   196  			ctx := context.WithValue(context.Background(), multicluster.KubeConfigContext, restConfig)
   197  			clusterConfig, err := multicluster.JoinClusterByKubeConfig(ctx, client, managedClusterKubeConfig, clusterName,
   198  				multicluster.JoinClusterCreateNamespaceOption(createNamespace),
   199  				multicluster.JoinClusterEngineOption(clusterManagementType),
   200  				multicluster.JoinClusterOCMOptions{
   201  					InClusterBootstrap:     inClusterBootstrap,
   202  					IoStreams:              ioStreams,
   203  					HubConfig:              restConfig,
   204  					TrackingSpinnerFactory: newTrackingSpinner,
   205  				},
   206  				multicluster.JoinClusterAlreadyExistCallback(func(name string) bool {
   207  					if !NewUserInput().AskBool(fmt.Sprintf("Cluster %s already exists, do you want to overwrite it?", name), &UserInputOptions{AssumeYes: assumeYes}) {
   208  						_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Terminated.\n")
   209  						return false
   210  					}
   211  					return true
   212  				}))
   213  			if err != nil {
   214  				return err
   215  			}
   216  			cmd.Printf("Successfully add cluster %s, endpoint: %s.\n", clusterName, clusterConfig.Cluster.Server)
   217  
   218  			if len(labels) > 0 {
   219  				return addClusterLabels(cmd, c, clusterName, labels)
   220  			}
   221  			return nil
   222  		},
   223  	}
   224  	cmd.Flags().StringP(FlagClusterName, "n", "", "Specify the cluster name. If empty, it will use the cluster name in config file. Default to be empty.")
   225  	cmd.Flags().StringP(FlagClusterManagementEngine, "t", multicluster.ClusterGateWayEngine, "Specify the cluster management engine. If empty, it will use cluster-gateway cluster management solution. Default to be empty.")
   226  	cmd.Flags().StringP(CreateNamespace, "", types.DefaultKubeVelaNS, "Specifies the namespace need to create in managedCluster")
   227  	cmd.Flags().BoolP(FlagInClusterBootstrap, "", true, "If true, the registering managed cluster "+
   228  		`will use the internal endpoint prescribed in the hub cluster's configmap "kube-public/cluster-info to register "`+
   229  		"itself to the hub cluster. Otherwise use the original endpoint from the hub kubeconfig.")
   230  	cmd.Flags().StringP(CreateLabel, "", "", "Specifies the labels need to create in managedCluster")
   231  
   232  	return cmd
   233  }
   234  
   235  // NewClusterRenameCommand create command to help user rename cluster
   236  func NewClusterRenameCommand(c *common.Args) *cobra.Command {
   237  	cmd := &cobra.Command{
   238  		Use:   "rename [OLD_NAME] [NEW_NAME]",
   239  		Short: "rename managed cluster.",
   240  		Long:  "rename managed cluster.",
   241  		Args:  cobra.ExactArgs(2),
   242  		RunE: func(cmd *cobra.Command, args []string) error {
   243  			oldClusterName := args[0]
   244  			newClusterName := args[1]
   245  			k8sClient, err := c.GetClient()
   246  			if err != nil {
   247  				return err
   248  			}
   249  			if err := multicluster.RenameCluster(context.Background(), k8sClient, oldClusterName, newClusterName); err != nil {
   250  				return err
   251  			}
   252  			cmd.Printf("Rename cluster %s to %s successfully.\n", oldClusterName, newClusterName)
   253  			return nil
   254  		},
   255  	}
   256  	return cmd
   257  }
   258  
   259  // NewClusterDetachCommand create command to help user detach existing cluster
   260  func NewClusterDetachCommand(c *common.Args) *cobra.Command {
   261  	cmd := &cobra.Command{
   262  		Use:   "detach [CLUSTER_NAME]",
   263  		Short: "detach managed cluster.",
   264  		Long:  "detach managed cluster.",
   265  		Args:  cobra.ExactArgs(1),
   266  		RunE: func(cmd *cobra.Command, args []string) error {
   267  			clusterName := args[0]
   268  			configPath, _ := cmd.Flags().GetString(FlagKubeConfigPath)
   269  			cli, err := c.GetClient()
   270  			if err != nil {
   271  				return err
   272  			}
   273  			err = multicluster.DetachCluster(context.Background(), cli, clusterName,
   274  				multicluster.DetachClusterManagedClusterKubeConfigPathOption(configPath))
   275  			if err != nil {
   276  				return err
   277  			}
   278  			cmd.Printf("Detach cluster %s successfully.\n", clusterName)
   279  			return nil
   280  		},
   281  	}
   282  	cmd.Flags().StringP(FlagKubeConfigPath, "p", "", "Specify the kubeconfig path of managed cluster. If you use ocm to manage your cluster, you must set the kubeconfig-path.")
   283  	return cmd
   284  }
   285  
   286  // NewClusterAliasCommand create an alias to the named cluster
   287  func NewClusterAliasCommand(c *common.Args) *cobra.Command {
   288  	cmd := &cobra.Command{
   289  		Use:   "alias CLUSTER_NAME ALIAS",
   290  		Short: "alias a named cluster.",
   291  		Long:  "alias a named cluster.",
   292  		Args:  cobra.ExactArgs(2),
   293  		RunE: func(cmd *cobra.Command, args []string) error {
   294  			clusterName, aliasName := args[0], args[1]
   295  			k8sClient, err := c.GetClient()
   296  			if err != nil {
   297  				return err
   298  			}
   299  			if err = multicluster.AliasCluster(context.Background(), k8sClient, clusterName, aliasName); err != nil {
   300  				return err
   301  			}
   302  			cmd.Printf("Alias cluster %s as %s.\n", clusterName, aliasName)
   303  			return nil
   304  		},
   305  	}
   306  	return cmd
   307  }
   308  
   309  // NewClusterProbeCommand create command to help user try health probe for existing cluster
   310  // TODO(somefive): move prob logic into cluster management
   311  func NewClusterProbeCommand(c *common.Args) *cobra.Command {
   312  	cmd := &cobra.Command{
   313  		Use:   "probe [CLUSTER_NAME]",
   314  		Short: "health probe managed cluster.",
   315  		Long:  "health probe managed cluster.",
   316  		Args:  cobra.ExactArgs(1),
   317  		RunE: func(cmd *cobra.Command, args []string) error {
   318  			clusterName := args[0]
   319  			config, err := c.GetConfig()
   320  			if err != nil {
   321  				return err
   322  			}
   323  			content, err := multicluster.RequestRawK8sAPIForCluster(context.TODO(), "healthz", clusterName, config)
   324  			if err != nil {
   325  				return errors.Wrapf(err, "failed connect cluster %s", clusterName)
   326  			}
   327  			cmd.Printf("Connect to cluster %s successfully.\n%s\n", clusterName, string(content))
   328  			return nil
   329  		},
   330  	}
   331  	return cmd
   332  }
   333  
   334  // NewClusterLabelCommandGroup create a group of commands to manage cluster labels
   335  func NewClusterLabelCommandGroup(c *common.Args) *cobra.Command {
   336  	cmd := &cobra.Command{
   337  		Use:   "labels",
   338  		Short: "Manage Kubernetes Cluster Labels.",
   339  		Long:  "Manage Kubernetes Cluster Labels for Continuous Delivery.",
   340  	}
   341  	cmd.AddCommand(
   342  		NewClusterAddLabelsCommand(c),
   343  		NewClusterDelLabelsCommand(c),
   344  	)
   345  	return cmd
   346  }
   347  
   348  func updateClusterLabelAndPrint(cmd *cobra.Command, cli client.Client, vc *multicluster.VirtualCluster, clusterName string) (err error) {
   349  	if err = cli.Update(context.Background(), vc.Object); err != nil {
   350  		return errors.Errorf("failed to update labels for cluster %s, type: %s", vc.FullName(), vc.Type)
   351  	}
   352  	if vc, err = multicluster.GetVirtualCluster(context.Background(), cli, clusterName); err != nil {
   353  		return errors.Wrapf(err, "failed to get updated cluster %s", clusterName)
   354  	}
   355  	cmd.Printf("Successfully update labels for cluster %s, type: %s.\n", vc.FullName(), vc.Type)
   356  	if len(vc.Labels) == 0 {
   357  		cmd.Println("No valid label exists.")
   358  	}
   359  	var keys []string
   360  	for k := range vc.Labels {
   361  		if !strings.HasPrefix(k, config.MetaApiGroupName) {
   362  			keys = append(keys, k)
   363  		}
   364  	}
   365  	sort.Strings(keys)
   366  	for _, k := range keys {
   367  		cmd.Println(color.CyanString(k) + "=" + color.GreenString(vc.Labels[k]))
   368  	}
   369  	return nil
   370  }
   371  
   372  // NewClusterAddLabelsCommand create command to add labels for managed cluster
   373  func NewClusterAddLabelsCommand(c *common.Args) *cobra.Command {
   374  	cmd := &cobra.Command{
   375  		Use:     "add CLUSTER_NAME LABELS",
   376  		Short:   "add labels to managed cluster.",
   377  		Long:    "add labels to managed cluster.",
   378  		Example: "vela cluster labels add my-cluster project=kubevela,owner=oam-dev",
   379  		Args:    cobra.ExactArgs(2),
   380  		RunE: func(cmd *cobra.Command, args []string) error {
   381  			clusterName := args[0]
   382  			labels := args[1]
   383  			return addClusterLabels(cmd, c, clusterName, labels)
   384  		},
   385  	}
   386  	return cmd
   387  }
   388  
   389  func addClusterLabels(cmd *cobra.Command, c *common.Args, clusterName, labels string) error {
   390  	addLabels := map[string]string{}
   391  	for _, kv := range strings.Split(labels, ",") {
   392  		parts := strings.Split(kv, "=")
   393  		if len(parts) != 2 {
   394  			return errors.Errorf("invalid label key-value pair %s, should use the format LABEL_KEY=LABEL_VAL", kv)
   395  		}
   396  		addLabels[parts[0]] = parts[1]
   397  	}
   398  
   399  	cli, err := c.GetClient()
   400  	if err != nil {
   401  		return err
   402  	}
   403  	vc, err := multicluster.GetVirtualCluster(context.Background(), cli, clusterName)
   404  	if err != nil {
   405  		return errors.Wrapf(err, "failed to get cluster %s", clusterName)
   406  	}
   407  	if vc.Object == nil {
   408  		return errors.Errorf("cluster type %s do not support add labels now", vc.Type)
   409  	}
   410  	meta.AddLabels(vc.Object, addLabels)
   411  	return updateClusterLabelAndPrint(cmd, cli, vc, clusterName)
   412  }
   413  
   414  // NewClusterDelLabelsCommand create command to delete labels for managed cluster
   415  func NewClusterDelLabelsCommand(c *common.Args) *cobra.Command {
   416  	cmd := &cobra.Command{
   417  		Use:     "del CLUSTER_NAME LABELS",
   418  		Aliases: []string{"delete", "remove"},
   419  		Short:   "Delete labels for managed cluster.",
   420  		Long:    "Delete labels for managed cluster.",
   421  		Args:    cobra.ExactArgs(2),
   422  		Example: "vela cluster labels del my-cluster project,owner",
   423  		RunE: func(cmd *cobra.Command, args []string) error {
   424  			clusterName := args[0]
   425  			removeLabels := strings.Split(args[1], ",")
   426  			cli, err := c.GetClient()
   427  			if err != nil {
   428  				return err
   429  			}
   430  			vc, err := multicluster.GetVirtualCluster(context.Background(), cli, clusterName)
   431  			if err != nil {
   432  				return errors.Wrapf(err, "failed to get cluster %s", clusterName)
   433  			}
   434  			if vc.Object == nil {
   435  				return errors.Errorf("cluster type %s do not support delete labels now", vc.Type)
   436  			}
   437  			for _, l := range removeLabels {
   438  				if _, found := vc.Labels[l]; !found {
   439  					return errors.Errorf("no such label %s", l)
   440  				}
   441  			}
   442  			meta.RemoveLabels(vc.Object, removeLabels...)
   443  			return updateClusterLabelAndPrint(cmd, cli, vc, clusterName)
   444  		},
   445  	}
   446  	return cmd
   447  }
   448  
   449  // NewClusterExportConfigCommand create command to export multi-cluster config
   450  func NewClusterExportConfigCommand(f velacmd.Factory, ioStreams cmdutil.IOStreams) *cobra.Command {
   451  	var labelSelector string
   452  	cmd := &cobra.Command{
   453  		Use:   "export-config",
   454  		Short: i18n.T("Export multi-cluster kubeconfig."),
   455  		Long: templates.LongDesc(i18n.T(`
   456  			Export multi-cluster kubeconfig
   457  
   458  			Load existing cluster kubeconfig and list clusters registered in
   459  			KubeVela. Export the proxy access of these clusters to KubeConfig
   460  			and print it out.
   461  		`)),
   462  		Example: templates.Examples(i18n.T(`
   463  			# Export all clusters to kubeconfig
   464  			vela cluster export-config
   465  
   466  			# Export clusters with specified kubeconfig
   467  			KUBECONFIG=./my-hub-cluster.kubeconfig vela cluster export-config
   468  
   469  			# Export clusters with specified labels
   470  			vela cluster export-config -l gpu-cluster=true
   471  
   472  			# Export clusters to kubeconfig and save in file
   473  			vela cluster export-config > my-vela.kubeconfig
   474  
   475  			# Use the exported kubeconfig in kubectl
   476  			KUBECONFIG=my-vela.kubeconfig kubectl get namespaces --cluster c2
   477  		`)),
   478  		RunE: func(cmd *cobra.Command, args []string) error {
   479  			cfg := runtime.Must(clientcmd.NewDefaultClientConfigLoadingRules().Load())
   480  			ctx, ok := cfg.Contexts[cfg.CurrentContext]
   481  			if !ok {
   482  				return fmt.Errorf("cannot find current context %s in given config", cfg.CurrentContext)
   483  			}
   484  			baseCluster, ok := cfg.Clusters[ctx.Cluster]
   485  			if !ok {
   486  				return fmt.Errorf("cannot find base cluster %s in given config", ctx.Cluster)
   487  			}
   488  			selector, err := labels.Parse(labelSelector)
   489  			if err != nil {
   490  				return fmt.Errorf("invalid selector %s: %w", labelSelector, err)
   491  			}
   492  			clusters, err := multicluster.NewClusterClient(f.Client()).List(cmd.Context(), client.MatchingLabelsSelector{Selector: selector})
   493  			if err != nil {
   494  				return fmt.Errorf("failed to load clusters: %w", err)
   495  			}
   496  			clusterNames := slices.Filter(
   497  				slices.Map(clusters.Items, func(cluster clustergatewayapi.VirtualCluster) string { return cluster.Name }),
   498  				func(s string) bool { return s != multicluster.ClusterLocalName })
   499  
   500  			if len(clusterNames) == 0 {
   501  				return fmt.Errorf("no cluster found")
   502  			}
   503  			_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%d cluster loaded: [%s]\n", len(clusterNames), strings.Join(clusterNames, ", "))
   504  
   505  			delete(cfg.Clusters, ctx.Cluster)
   506  			ctx.Cluster = types.ClusterLocalName
   507  			cfg.Clusters[types.ClusterLocalName] = baseCluster.DeepCopy()
   508  			for _, clusterName := range clusterNames {
   509  				cls := baseCluster.DeepCopy()
   510  				cls.LocationOfOrigin = ""
   511  				cls.Server = strings.Join([]string{cls.Server, "apis",
   512  					clustergatewayapi.SchemeGroupVersion.Group,
   513  					clustergatewayapi.SchemeGroupVersion.Version,
   514  					"clustergateways", clusterName, "proxy"}, "/")
   515  				cfg.Clusters[clusterName] = cls
   516  			}
   517  			bs, err := clientcmd.Write(*cfg)
   518  			if err != nil {
   519  				return fmt.Errorf("failed to marshal generated kubeconfig: %w", err)
   520  			}
   521  			_, _ = ioStreams.Out.Write(bs)
   522  			_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "kubeconfig generated.\n")
   523  			return nil
   524  		},
   525  	}
   526  	cmd.Flags().StringVarP(&labelSelector, "selector", "l", labelSelector, "LabelSelector for select clusters to export.")
   527  
   528  	return velacmd.NewCommandBuilder(f, cmd).WithResponsiveWriter().Build()
   529  }