github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/cmd/cli.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package cmd
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"strings"
    26  
    27  	"github.com/spf13/cobra"
    28  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    29  	"k8s.io/cli-runtime/pkg/genericiooptions"
    30  	cliflag "k8s.io/component-base/cli/flag"
    31  	"k8s.io/klog/v2"
    32  	kccmd "k8s.io/kubectl/pkg/cmd"
    33  	kcplugin "k8s.io/kubectl/pkg/cmd/plugin"
    34  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    35  	utilcomp "k8s.io/kubectl/pkg/util/completion"
    36  	"k8s.io/kubectl/pkg/util/templates"
    37  
    38  	"github.com/1aal/kubeblocks/pkg/cli/cmd/addon"
    39  	"github.com/1aal/kubeblocks/pkg/cli/cmd/alert"
    40  	"github.com/1aal/kubeblocks/pkg/cli/cmd/auth"
    41  	"github.com/1aal/kubeblocks/pkg/cli/cmd/backuprepo"
    42  	"github.com/1aal/kubeblocks/pkg/cli/cmd/bench"
    43  	"github.com/1aal/kubeblocks/pkg/cli/cmd/builder"
    44  	"github.com/1aal/kubeblocks/pkg/cli/cmd/class"
    45  	"github.com/1aal/kubeblocks/pkg/cli/cmd/cluster"
    46  	"github.com/1aal/kubeblocks/pkg/cli/cmd/clusterdefinition"
    47  	"github.com/1aal/kubeblocks/pkg/cli/cmd/clusterversion"
    48  	"github.com/1aal/kubeblocks/pkg/cli/cmd/dashboard"
    49  	"github.com/1aal/kubeblocks/pkg/cli/cmd/dataprotection"
    50  	"github.com/1aal/kubeblocks/pkg/cli/cmd/fault"
    51  	infras "github.com/1aal/kubeblocks/pkg/cli/cmd/infrastructure"
    52  	"github.com/1aal/kubeblocks/pkg/cli/cmd/kubeblocks"
    53  	"github.com/1aal/kubeblocks/pkg/cli/cmd/migration"
    54  	"github.com/1aal/kubeblocks/pkg/cli/cmd/options"
    55  	"github.com/1aal/kubeblocks/pkg/cli/cmd/playground"
    56  	"github.com/1aal/kubeblocks/pkg/cli/cmd/plugin"
    57  	"github.com/1aal/kubeblocks/pkg/cli/cmd/report"
    58  	"github.com/1aal/kubeblocks/pkg/cli/cmd/version"
    59  	"github.com/1aal/kubeblocks/pkg/cli/types"
    60  	"github.com/1aal/kubeblocks/pkg/cli/util"
    61  	viper "github.com/1aal/kubeblocks/pkg/viperx"
    62  )
    63  
    64  const (
    65  	cliName = "kbcli"
    66  )
    67  
    68  // TODO: add more commands
    69  var cloudCmds = map[string]bool{
    70  	"org":     true,
    71  	"logout":  true,
    72  	"context": true,
    73  }
    74  
    75  func init() {
    76  	if _, err := util.GetCliHomeDir(); err != nil {
    77  		fmt.Println("Failed to create kbcli home dir:", err)
    78  	}
    79  
    80  	// replace the kubectl plugin filename prefixes with ours
    81  	kcplugin.ValidPluginFilenamePrefixes = plugin.ValidPluginFilenamePrefixes
    82  
    83  	// put the download directory of the plugin into the PATH
    84  	if err := util.AddDirToPath(fmt.Sprintf("%s/.%s/plugins/bin", os.Getenv("HOME"), cliName)); err != nil {
    85  		fmt.Println("Failed to add kbcli bin dir to PATH:", err)
    86  	}
    87  
    88  	// when the kubernetes cluster is not ready, the runtime will output the error
    89  	// message like "couldn't get resource list for", we ignore it
    90  	utilruntime.ErrorHandlers[0] = func(err error) {
    91  		if klog.V(2).Enabled() {
    92  			klog.ErrorDepth(2, err)
    93  		}
    94  	}
    95  }
    96  
    97  func NewDefaultCliCmd() *cobra.Command {
    98  	cmd := NewCliCmd()
    99  
   100  	pluginHandler := kccmd.NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes)
   101  
   102  	if len(os.Args) > 1 {
   103  		cmdPathPieces := os.Args[1:]
   104  
   105  		// only look for suitable extension executables if
   106  		// the specified command does not exist
   107  		if _, _, err := cmd.Find(cmdPathPieces); err != nil {
   108  			var cmdName string
   109  			for _, arg := range cmdPathPieces {
   110  				if !strings.HasPrefix(arg, "-") {
   111  					cmdName = arg
   112  					break
   113  				}
   114  			}
   115  
   116  			switch cmdName {
   117  			case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd:
   118  				// Don't search for a plugin
   119  			default:
   120  				if err := kccmd.HandlePluginCommand(pluginHandler, cmdPathPieces, true); err != nil {
   121  					fmt.Fprintf(os.Stderr, "Error: %v\n", err)
   122  					os.Exit(1)
   123  				}
   124  			}
   125  		}
   126  	}
   127  
   128  	return cmd
   129  }
   130  
   131  func NewCliCmd() *cobra.Command {
   132  	cmd := &cobra.Command{
   133  		Use:   cliName,
   134  		Short: "KubeBlocks CLI.",
   135  		Long: `
   136  =============================================
   137   __    __ _______   ______  __       ______ 
   138  |  \  /  \       \ /      \|  \     |      \
   139  | ▓▓ /  ▓▓ ▓▓▓▓▓▓▓\  ▓▓▓▓▓▓\ ▓▓      \▓▓▓▓▓▓
   140  | ▓▓/  ▓▓| ▓▓__/ ▓▓ ▓▓   \▓▓ ▓▓       | ▓▓  
   141  | ▓▓  ▓▓ | ▓▓    ▓▓ ▓▓     | ▓▓       | ▓▓  
   142  | ▓▓▓▓▓\ | ▓▓▓▓▓▓▓\ ▓▓   __| ▓▓       | ▓▓  
   143  | ▓▓ \▓▓\| ▓▓__/ ▓▓ ▓▓__/  \ ▓▓_____ _| ▓▓_ 
   144  | ▓▓  \▓▓\ ▓▓    ▓▓\▓▓    ▓▓ ▓▓     \   ▓▓ \
   145   \▓▓   \▓▓\▓▓▓▓▓▓▓  \▓▓▓▓▓▓ \▓▓▓▓▓▓▓▓\▓▓▓▓▓▓
   146  
   147  =============================================
   148  A Command Line Interface for KubeBlocks`,
   149  
   150  		Run: func(cmd *cobra.Command, args []string) {
   151  			_ = cmd.Help()
   152  		},
   153  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
   154  			if cmd.Name() == cobra.ShellCompRequestCmd {
   155  				kcplugin.SetupPluginCompletion(cmd, args)
   156  			}
   157  
   158  			commandPath := cmd.CommandPath()
   159  			parts := strings.Split(commandPath, " ")
   160  			if len(parts) < 2 {
   161  				return nil
   162  			}
   163  			subCommand := parts[1]
   164  			if cloudCmds[subCommand] && !auth.IsLoggedIn() {
   165  				return fmt.Errorf("use 'kbcli login' to login first")
   166  			}
   167  			return nil
   168  		},
   169  	}
   170  
   171  	// Start from this point we get warnings on flags that contain "_" separators
   172  	// when adding them with hyphen instead of the original name.
   173  	cmd.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc)
   174  
   175  	flags := cmd.PersistentFlags()
   176  
   177  	// add kubernetes flags like kubectl
   178  	kubeConfigFlags := util.NewConfigFlagNoWarnings()
   179  	kubeConfigFlags.AddFlags(flags)
   180  	matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags)
   181  	matchVersionKubeConfigFlags.AddFlags(flags)
   182  
   183  	// add klog flags
   184  	util.AddKlogFlags(flags)
   185  
   186  	f := cmdutil.NewFactory(matchVersionKubeConfigFlags)
   187  	ioStreams := genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
   188  
   189  	// Add subcommands
   190  	cmd.AddCommand(
   191  		auth.NewLogin(ioStreams),
   192  		auth.NewLogout(ioStreams),
   193  		// organization.NewOrganizationCmd(ioStreams),
   194  		// context.NewContextCmd(ioStreams),
   195  		playground.NewPlaygroundCmd(ioStreams),
   196  		kubeblocks.NewKubeBlocksCmd(f, ioStreams),
   197  		bench.NewBenchCmd(f, ioStreams),
   198  		options.NewCmdOptions(ioStreams.Out),
   199  		version.NewVersionCmd(f),
   200  		dashboard.NewDashboardCmd(f, ioStreams),
   201  		clusterversion.NewClusterVersionCmd(f, ioStreams),
   202  		clusterdefinition.NewClusterDefinitionCmd(f, ioStreams),
   203  		class.NewClassCommand(f, ioStreams),
   204  		alert.NewAlertCmd(f, ioStreams),
   205  		addon.NewAddonCmd(f, ioStreams),
   206  		migration.NewMigrationCmd(f, ioStreams),
   207  		plugin.NewPluginCmd(ioStreams),
   208  		fault.NewFaultCmd(f, ioStreams),
   209  		builder.NewBuilderCmd(f, ioStreams),
   210  		report.NewReportCmd(f, ioStreams),
   211  		infras.NewInfraCmd(ioStreams),
   212  		backuprepo.NewBackupRepoCmd(f, ioStreams),
   213  		dataprotection.NewDataProtectionCmd(f, ioStreams),
   214  	)
   215  
   216  	filters := []string{"options"}
   217  	templates.ActsAsRootCommand(cmd, filters, []templates.CommandGroup{}...)
   218  
   219  	helpFunc := cmd.HelpFunc()
   220  	usageFunc := cmd.UsageFunc()
   221  
   222  	// clusterCmd sets its own usage and help function and its subcommand will inherit it,
   223  	// so we need to set its subcommand's usage and help function back to the root command
   224  	clusterCmd := cluster.NewClusterCmd(f, ioStreams)
   225  	registerUsageAndHelpFuncForSubCommand(clusterCmd, helpFunc, usageFunc)
   226  	cmd.AddCommand(clusterCmd)
   227  
   228  	utilcomp.SetFactoryForCompletion(f)
   229  	registerCompletionFuncForGlobalFlags(cmd, f)
   230  
   231  	cobra.OnInitialize(initConfig)
   232  	return cmd
   233  }
   234  
   235  // initConfig reads in config file and ENV variables if set.
   236  func initConfig() {
   237  	viper.SetConfigName("config")
   238  	viper.SetConfigType("yaml")
   239  	viper.AddConfigPath(fmt.Sprintf("/etc/%s/", cliName))
   240  	viper.AddConfigPath(fmt.Sprintf("$HOME/.%s/", cliName))
   241  	viper.AutomaticEnv() // read in environment variables that match
   242  	viper.SetEnvPrefix(cliName)
   243  
   244  	viper.SetDefault(types.CfgKeyClusterDefaultStorageSize, "20Gi")
   245  	viper.SetDefault(types.CfgKeyClusterDefaultReplicas, 1)
   246  	viper.SetDefault(types.CfgKeyClusterDefaultCPU, "1000m")
   247  	viper.SetDefault(types.CfgKeyClusterDefaultMemory, "1Gi")
   248  
   249  	viper.SetDefault(types.CfgKeyHelmRepoURL, "")
   250  
   251  	// If a config file is found, read it in.
   252  	if err := viper.ReadInConfig(); err == nil {
   253  		fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
   254  	}
   255  }
   256  
   257  func registerCompletionFuncForGlobalFlags(cmd *cobra.Command, f cmdutil.Factory) {
   258  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
   259  		"namespace",
   260  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   261  			return utilcomp.CompGetResource(f, "namespace", toComplete), cobra.ShellCompDirectiveNoFileComp
   262  		}))
   263  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
   264  		"context",
   265  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   266  			return utilcomp.ListContextsInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
   267  		}))
   268  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
   269  		"cluster",
   270  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   271  			return utilcomp.ListClustersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
   272  		}))
   273  	cmdutil.CheckErr(cmd.RegisterFlagCompletionFunc(
   274  		"user",
   275  		func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   276  			return utilcomp.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp
   277  		}))
   278  }
   279  
   280  func registerUsageAndHelpFuncForSubCommand(cmd *cobra.Command, helpFunc func(*cobra.Command, []string), usageFunc func(command *cobra.Command) error) {
   281  	for _, subCmd := range cmd.Commands() {
   282  		subCmd.SetHelpFunc(helpFunc)
   283  		subCmd.SetUsageFunc(usageFunc)
   284  	}
   285  }