github.com/pingcap/tiup@v1.15.1/components/dm/command/root.go (about)

     1  // Copyright 2020 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package command
    15  
    16  import (
    17  	"encoding/json"
    18  	"fmt"
    19  	"os"
    20  	"path"
    21  	"strings"
    22  
    23  	"github.com/fatih/color"
    24  	"github.com/joomcode/errorx"
    25  	perrs "github.com/pingcap/errors"
    26  	"github.com/pingcap/tiup/components/dm/spec"
    27  	"github.com/pingcap/tiup/pkg/cluster/executor"
    28  	"github.com/pingcap/tiup/pkg/cluster/manager"
    29  	operator "github.com/pingcap/tiup/pkg/cluster/operation"
    30  	cspec "github.com/pingcap/tiup/pkg/cluster/spec"
    31  	tiupmeta "github.com/pingcap/tiup/pkg/environment"
    32  	"github.com/pingcap/tiup/pkg/localdata"
    33  	"github.com/pingcap/tiup/pkg/logger"
    34  	logprinter "github.com/pingcap/tiup/pkg/logger/printer"
    35  	"github.com/pingcap/tiup/pkg/proxy"
    36  	"github.com/pingcap/tiup/pkg/repository"
    37  	"github.com/pingcap/tiup/pkg/tui"
    38  	"github.com/pingcap/tiup/pkg/utils"
    39  	"github.com/pingcap/tiup/pkg/version"
    40  	"github.com/spf13/cobra"
    41  	"go.uber.org/zap"
    42  )
    43  
    44  var (
    45  	// nolint
    46  	errNS       = errorx.NewNamespace("cmd")
    47  	rootCmd     *cobra.Command
    48  	gOpt        operator.Options
    49  	skipConfirm bool
    50  	log         = logprinter.NewLogger("") // init default logger
    51  )
    52  
    53  var dmspec *cspec.SpecManager
    54  var cm *manager.Manager
    55  
    56  func init() {
    57  	logger.InitGlobalLogger()
    58  
    59  	tui.AddColorFunctionsForCobra()
    60  
    61  	cobra.EnableCommandSorting = false
    62  
    63  	nativeEnvVar := strings.ToLower(os.Getenv(localdata.EnvNameNativeSSHClient))
    64  	if nativeEnvVar == "true" || nativeEnvVar == "1" || nativeEnvVar == "enable" {
    65  		gOpt.NativeSSH = true
    66  	}
    67  
    68  	rootCmd = &cobra.Command{
    69  		Use:   tui.OsArgs0(),
    70  		Short: "(EXPERIMENTAL) Deploy a DM cluster",
    71  		Long: `EXPERIMENTAL: This is an experimental feature, things may or may not work,
    72  please backup your data before process.`,
    73  		SilenceUsage:  true,
    74  		SilenceErrors: true,
    75  		Version:       version.NewTiUPVersion().String(),
    76  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
    77  			// populate logger
    78  			log.SetDisplayModeFromString(gOpt.DisplayMode)
    79  
    80  			var err error
    81  			var env *tiupmeta.Environment
    82  			if err = cspec.Initialize("dm"); err != nil {
    83  				return err
    84  			}
    85  
    86  			dmspec = spec.GetSpecManager()
    87  			logger.EnableAuditLog(cspec.AuditDir())
    88  			cm = manager.NewManager("dm", dmspec, log)
    89  
    90  			// Running in other OS/ARCH Should be fine we only download manifest file.
    91  			env, err = tiupmeta.InitEnv(repository.Options{
    92  				GOOS:   "linux",
    93  				GOARCH: "amd64",
    94  			}, repository.MirrorOptions{})
    95  			if err != nil {
    96  				return err
    97  			}
    98  			tiupmeta.SetGlobalEnv(env)
    99  
   100  			if gOpt.NativeSSH {
   101  				gOpt.SSHType = executor.SSHTypeSystem
   102  				zap.L().Info("System ssh client will be used",
   103  					zap.String(localdata.EnvNameNativeSSHClient, os.Getenv(localdata.EnvNameNativeSSHClient)))
   104  				fmt.Println("The --native-ssh flag has been deprecated, please use --ssh=system")
   105  			}
   106  
   107  			err = proxy.MaybeStartProxy(
   108  				gOpt.SSHProxyHost,
   109  				gOpt.SSHProxyPort,
   110  				gOpt.SSHProxyUser,
   111  				gOpt.SSHProxyUsePassword,
   112  				gOpt.SSHProxyIdentity,
   113  				log,
   114  			)
   115  			if err != nil {
   116  				return perrs.Annotate(err, "start http-proxy")
   117  			}
   118  
   119  			return nil
   120  		},
   121  		PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
   122  			proxy.MaybeStopProxy()
   123  			return tiupmeta.GlobalEnv().V1Repository().Mirror().Close()
   124  		},
   125  	}
   126  
   127  	tui.BeautifyCobraUsageAndHelp(rootCmd)
   128  
   129  	rootCmd.PersistentFlags().Uint64Var(&gOpt.SSHTimeout, "ssh-timeout", 5, "Timeout in seconds to connect host via SSH, ignored for operations that don't need an SSH connection.")
   130  	rootCmd.PersistentFlags().Uint64Var(&gOpt.OptTimeout, "wait-timeout", 120, "Timeout in seconds to wait for an operation to complete, ignored for operations that don't fit.")
   131  	rootCmd.PersistentFlags().BoolVarP(&skipConfirm, "yes", "y", false, "Skip all confirmations and assumes 'yes'")
   132  	rootCmd.PersistentFlags().BoolVar(&gOpt.NativeSSH, "native-ssh", gOpt.NativeSSH, "Use the SSH client installed on local system instead of the build-in one.")
   133  	rootCmd.PersistentFlags().StringVar((*string)(&gOpt.SSHType), "ssh", "", "The executor type: 'builtin', 'system', 'none'")
   134  	rootCmd.PersistentFlags().IntVarP(&gOpt.Concurrency, "concurrency", "c", 5, "max number of parallel tasks allowed")
   135  	rootCmd.PersistentFlags().StringVar(&gOpt.DisplayMode, "format", "default", "(EXPERIMENTAL) The format of output, available values are [default, json]")
   136  	rootCmd.PersistentFlags().StringVar(&gOpt.SSHProxyHost, "ssh-proxy-host", "", "The SSH proxy host used to connect to remote host.")
   137  	rootCmd.PersistentFlags().StringVar(&gOpt.SSHProxyUser, "ssh-proxy-user", utils.CurrentUser(), "The user name used to login the proxy host.")
   138  	rootCmd.PersistentFlags().IntVar(&gOpt.SSHProxyPort, "ssh-proxy-port", 22, "The port used to login the proxy host.")
   139  	rootCmd.PersistentFlags().StringVar(&gOpt.SSHProxyIdentity, "ssh-proxy-identity-file", path.Join(utils.UserHome(), ".ssh", "id_rsa"), "The identity file used to login the proxy host.")
   140  	rootCmd.PersistentFlags().BoolVar(&gOpt.SSHProxyUsePassword, "ssh-proxy-use-password", false, "Use password to login the proxy host.")
   141  	rootCmd.PersistentFlags().Uint64Var(&gOpt.SSHProxyTimeout, "ssh-proxy-timeout", 5, "Timeout in seconds to connect the proxy host via SSH, ignored for operations that don't need an SSH connection.")
   142  	_ = rootCmd.PersistentFlags().MarkHidden("native-ssh")
   143  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-host")
   144  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-user")
   145  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-port")
   146  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-identity-file")
   147  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-use-password")
   148  	_ = rootCmd.PersistentFlags().MarkHidden("ssh-proxy-timeout")
   149  
   150  	rootCmd.AddCommand(
   151  		newDeployCmd(),
   152  		newStartCmd(),
   153  		newStopCmd(),
   154  		newRestartCmd(),
   155  		newListCmd(),
   156  		newDestroyCmd(),
   157  		newAuditCmd(),
   158  		newExecCmd(),
   159  		newEditConfigCmd(),
   160  		newDisplayCmd(),
   161  		newPruneCmd(),
   162  		newReloadCmd(),
   163  		newUpgradeCmd(),
   164  		newPatchCmd(),
   165  		newScaleOutCmd(),
   166  		newScaleInCmd(),
   167  		newImportCmd(),
   168  		newEnableCmd(),
   169  		newDisableCmd(),
   170  		newReplayCmd(),
   171  		newTemplateCmd(),
   172  		newMetaCmd(),
   173  		newRotateSSHCmd(),
   174  	)
   175  }
   176  
   177  func printErrorMessageForNormalError(err error) {
   178  	_, _ = tui.ColorErrorMsg.Fprintf(os.Stderr, "\nError: %s\n", err.Error())
   179  }
   180  
   181  func printErrorMessageForErrorX(err *errorx.Error) {
   182  	msg := ""
   183  	ident := 0
   184  	causeErrX := err
   185  	for causeErrX != nil {
   186  		if ident > 0 {
   187  			msg += strings.Repeat("  ", ident) + "caused by: "
   188  		}
   189  		currentErrMsg := causeErrX.Message()
   190  		if len(currentErrMsg) > 0 {
   191  			if ident == 0 {
   192  				// Print error code only for top level error
   193  				msg += fmt.Sprintf("%s (%s)\n", currentErrMsg, causeErrX.Type().FullName())
   194  			} else {
   195  				msg += fmt.Sprintf("%s\n", currentErrMsg)
   196  			}
   197  			ident++
   198  		}
   199  		cause := causeErrX.Cause()
   200  		if c := errorx.Cast(cause); c != nil {
   201  			causeErrX = c
   202  		} else {
   203  			if cause != nil {
   204  				if ident > 0 {
   205  					// Out most error may have empty message. In this case we treat it as a transparent error.
   206  					// Thus `ident == 0` can be possible.
   207  					msg += strings.Repeat("  ", ident) + "caused by: "
   208  				}
   209  				msg += fmt.Sprintf("%s\n", cause.Error())
   210  			}
   211  			break
   212  		}
   213  	}
   214  	_, _ = tui.ColorErrorMsg.Fprintf(os.Stderr, "\nError: %s", msg)
   215  }
   216  
   217  func extractSuggestionFromErrorX(err *errorx.Error) string {
   218  	cause := err
   219  	for cause != nil {
   220  		v, ok := cause.Property(utils.ErrPropSuggestion)
   221  		if ok {
   222  			if s, ok := v.(string); ok {
   223  				return s
   224  			}
   225  		}
   226  		cause = errorx.Cast(cause.Cause())
   227  	}
   228  
   229  	return ""
   230  }
   231  
   232  // Execute executes the root command
   233  func Execute() {
   234  	zap.L().Info("Execute command", zap.String("command", tui.OsArgs()))
   235  	zap.L().Debug("Environment variables", zap.Strings("env", os.Environ()))
   236  
   237  	code := 0
   238  	err := rootCmd.Execute()
   239  	if err != nil {
   240  		code = 1
   241  	}
   242  
   243  	zap.L().Info("Execute command finished", zap.Int("code", code), zap.Error(err))
   244  
   245  	if err != nil {
   246  		switch strings.ToLower(gOpt.DisplayMode) {
   247  		case "json":
   248  			obj := struct {
   249  				Err string `json:"error"`
   250  			}{
   251  				Err: err.Error(),
   252  			}
   253  			data, err := json.Marshal(obj)
   254  			if err != nil {
   255  				fmt.Printf("{\"error\": \"%s\"}", err)
   256  				break
   257  			}
   258  			fmt.Fprintln(os.Stderr, string(data))
   259  		default:
   260  			if errx := errorx.Cast(err); errx != nil {
   261  				printErrorMessageForErrorX(errx)
   262  			} else {
   263  				printErrorMessageForNormalError(err)
   264  			}
   265  
   266  			if !errorx.HasTrait(err, utils.ErrTraitPreCheck) {
   267  				logger.OutputDebugLog("tiup-dm")
   268  			}
   269  
   270  			if errx := errorx.Cast(err); errx != nil {
   271  				if suggestion := extractSuggestionFromErrorX(errx); len(suggestion) > 0 {
   272  					_, _ = fmt.Fprintf(os.Stderr, "\n%s\n", suggestion)
   273  				}
   274  			}
   275  		}
   276  	}
   277  
   278  	err = logger.OutputAuditLogIfEnabled()
   279  	if err != nil {
   280  		zap.L().Warn("Write audit log file failed", zap.Error(err))
   281  		code = 1
   282  	}
   283  
   284  	color.Unset()
   285  
   286  	if code != 0 {
   287  		os.Exit(code)
   288  	}
   289  }