github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/cli/error.go (about)

     1  // Copyright 2016 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package cli
    12  
    13  import (
    14  	"context"
    15  	"crypto/x509"
    16  	"fmt"
    17  	"io"
    18  	"net"
    19  	"regexp"
    20  	"strconv"
    21  	"strings"
    22  
    23  	"github.com/cockroachdb/cockroach/pkg/security"
    24  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode"
    25  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror"
    26  	"github.com/cockroachdb/cockroach/pkg/util/grpcutil"
    27  	"github.com/cockroachdb/cockroach/pkg/util/log"
    28  	"github.com/cockroachdb/cockroach/pkg/util/netutil"
    29  	"github.com/cockroachdb/errors"
    30  	"github.com/lib/pq"
    31  	"github.com/spf13/cobra"
    32  	"google.golang.org/grpc/codes"
    33  	"google.golang.org/grpc/status"
    34  )
    35  
    36  // cliOutputError prints out an error object on the given writer.
    37  //
    38  // It has a somewhat inconvenient set of requirements: it must make
    39  // the error both palatable to a human user, which mandates some
    40  // beautification, and still retain a few guarantees for automatic
    41  // parsers (and a modicum of care for cross-compatibility across
    42  // versions), including that of keeping the output relatively stable.
    43  //
    44  // As a result, future changes should be careful to properly balance
    45  // changes made in favor of one audience with the needs and
    46  // requirements of the other audience.
    47  func cliOutputError(w io.Writer, err error, showSeverity, verbose bool) {
    48  	f := formattedError{err: err, showSeverity: showSeverity, verbose: verbose}
    49  	fmt.Fprintln(w, f.Error())
    50  }
    51  
    52  type formattedError struct {
    53  	err                   error
    54  	showSeverity, verbose bool
    55  }
    56  
    57  // Error implements the error interface.
    58  func (f *formattedError) Error() string {
    59  	// If we're applying recursively, ignore what's there and display the original error.
    60  	// This happens when the shell reports an error for a second time.
    61  	var other *formattedError
    62  	if errors.As(f.err, &other) {
    63  		return other.Error()
    64  	}
    65  	var buf strings.Builder
    66  
    67  	// If the severity is missing, we're going to assume it's an error.
    68  	severity := "ERROR"
    69  
    70  	// Extract the fields.
    71  	var message, code, hint, detail, location string
    72  	if pqErr := (*pq.Error)(nil); errors.As(f.err, &pqErr) {
    73  		if pqErr.Severity != "" {
    74  			severity = pqErr.Severity
    75  		}
    76  		message = pqErr.Message
    77  		code = string(pqErr.Code)
    78  		hint, detail = pqErr.Hint, pqErr.Detail
    79  		location = formatLocation(pqErr.File, pqErr.Line, pqErr.Routine)
    80  	} else {
    81  		message = f.err.Error()
    82  		code = pgerror.GetPGCode(f.err)
    83  		// Extract the standard hint and details.
    84  		hint = errors.FlattenHints(f.err)
    85  		detail = errors.FlattenDetails(f.err)
    86  		if file, line, fn, ok := errors.GetOneLineSource(f.err); ok {
    87  			location = formatLocation(file, strconv.FormatInt(int64(line), 10), fn)
    88  		}
    89  	}
    90  
    91  	// The order of the printing goes from most to less important.
    92  
    93  	if f.showSeverity && severity != "" {
    94  		fmt.Fprintf(&buf, "%s: ", severity)
    95  	}
    96  	fmt.Fprintln(&buf, message)
    97  
    98  	// Avoid printing the code for NOTICE, as the code is always 00000.
    99  	if severity != "NOTICE" && code != "" {
   100  		// In contrast to `psql` we print the code even when printing
   101  		// non-verbosely, because we want to promote users reporting codes
   102  		// when interacting with support.
   103  		if code == pgcode.Uncategorized && !f.verbose {
   104  			// An exception is made for the "uncategorized" code, because we
   105  			// also don't want users to get the idea they can rely on XXUUU
   106  			// in their apps. That code is special, as we typically seek to
   107  			// replace it over time by something more specific.
   108  			//
   109  			// So in this case, if not printing verbosely, we don't display
   110  			// the code.
   111  		} else {
   112  			fmt.Fprintln(&buf, "SQLSTATE:", code)
   113  		}
   114  	}
   115  
   116  	if detail != "" {
   117  		fmt.Fprintln(&buf, "DETAIL:", detail)
   118  	}
   119  	if hint != "" {
   120  		fmt.Fprintln(&buf, "HINT:", hint)
   121  	}
   122  	if f.verbose && location != "" {
   123  		fmt.Fprintln(&buf, "LOCATION:", location)
   124  	}
   125  
   126  	// The code above is easier to read and write by stripping the
   127  	// extraneous newline at the end, than ensuring it's not there in
   128  	// the first place.
   129  	return strings.TrimRight(buf.String(), "\n")
   130  }
   131  
   132  // formatLocation spells out the error's location in a format
   133  // similar to psql: routine then file:num. The routine part is
   134  // skipped if empty.
   135  func formatLocation(file, line, fn string) string {
   136  	var res strings.Builder
   137  	res.WriteString(fn)
   138  	if file != "" || line != "" {
   139  		if fn != "" {
   140  			res.WriteString(", ")
   141  		}
   142  		if file == "" {
   143  			res.WriteString("<unknown>")
   144  		} else {
   145  			res.WriteString(file)
   146  		}
   147  		if line != "" {
   148  			res.WriteByte(':')
   149  			res.WriteString(line)
   150  		}
   151  	}
   152  	return res.String()
   153  }
   154  
   155  // reConnRefused is a regular expression that can be applied
   156  // to the details of a GRPC connection failure.
   157  //
   158  // On *nix, a connect error looks like:
   159  //    dial tcp <addr>: <syscall>: connection refused
   160  // On Windows, it looks like:
   161  //    dial tcp <addr>: <syscall>: No connection could be made because the target machine actively refused it.
   162  // So we look for the common bit.
   163  var reGRPCConnRefused = regexp.MustCompile(`Error while dialing dial tcp .*: connection.* refused`)
   164  
   165  // reGRPCNoTLS is a regular expression that can be applied to the
   166  // details of a GRPC auth failure when the server is insecure.
   167  var reGRPCNoTLS = regexp.MustCompile(`authentication handshake failed: tls: first record does not look like a TLS handshake`)
   168  
   169  // reGRPCAuthFailure is a regular expression that can be applied to
   170  // the details of a GRPC auth failure when the SSL handshake fails.
   171  var reGRPCAuthFailure = regexp.MustCompile(`authentication handshake failed: x509`)
   172  
   173  // reGRPCConnFailed is a regular expression that can be applied
   174  // to the details of a GRPC connection failure when, perhaps,
   175  // the server was expecting a TLS handshake but the client didn't
   176  // provide one (i.e. the client was started with --insecure).
   177  // Note however in that case it's not certain what the problem is,
   178  // as the same error could be raised for other reasons.
   179  var reGRPCConnFailed = regexp.MustCompile(`desc = (transport is closing|all SubConns are in TransientFailure)`)
   180  
   181  // MaybeDecorateGRPCError catches grpc errors and provides a more helpful error
   182  // message to the user.
   183  func MaybeDecorateGRPCError(
   184  	wrapped func(*cobra.Command, []string) error,
   185  ) func(*cobra.Command, []string) error {
   186  	return func(cmd *cobra.Command, args []string) (err error) {
   187  		err = wrapped(cmd, args)
   188  
   189  		if err == nil {
   190  			return nil
   191  		}
   192  
   193  		defer func() {
   194  			// We want to flatten the error to reveal the hints, details etc.
   195  			// However we can't do it twice, so we need to detect first if
   196  			// some code already added the formattedError{} wrapper.
   197  			var f *formattedError
   198  			if !errors.As(err, &f) {
   199  				err = &formattedError{err: err, showSeverity: true}
   200  			}
   201  		}()
   202  
   203  		extraInsecureHint := func() string {
   204  			extra := ""
   205  			if baseCfg.Insecure {
   206  				extra = "\nIf the node is configured to require secure connections,\n" +
   207  					"remove --insecure and configure secure credentials instead.\n"
   208  			}
   209  			return extra
   210  		}
   211  
   212  		connFailed := func() error {
   213  			const format = "cannot dial server.\n" +
   214  				"Is the server running?\n" +
   215  				"If the server is running, check --host client-side and --advertise server-side.\n\n%v"
   216  			return errors.Errorf(format, err)
   217  		}
   218  
   219  		connSecurityHint := func() error {
   220  			const format = "SSL authentication error while connecting.\n%s\n%v"
   221  			return errors.Errorf(format, extraInsecureHint(), err)
   222  		}
   223  
   224  		connInsecureHint := func() error {
   225  			return errors.Errorf("cannot establish secure connection to insecure server.\n"+
   226  				"Maybe use --insecure?\n\n%v", err)
   227  		}
   228  
   229  		connRefused := func() error {
   230  			extra := extraInsecureHint()
   231  			return errors.Errorf("server closed the connection.\n"+
   232  				"Is this a CockroachDB node?\n%s\n%v", extra, err)
   233  		}
   234  
   235  		// Is this an "unable to connect" type of error?
   236  		if errors.Is(err, pq.ErrSSLNotSupported) {
   237  			// SQL command failed after establishing a TCP connection
   238  			// successfully, but discovering that it cannot use TLS while it
   239  			// expected the server supports TLS.
   240  			return connInsecureHint()
   241  		}
   242  
   243  		if wErr := (*security.Error)(nil); errors.As(err, &wErr) {
   244  			return errors.Errorf("cannot load certificates.\n"+
   245  				"Check your certificate settings, set --certs-dir, or use --insecure for insecure clusters.\n\n%v",
   246  				err)
   247  		}
   248  
   249  		if wErr := (*x509.UnknownAuthorityError)(nil); errors.As(err, &wErr) {
   250  			// A SQL connection was attempted with an incorrect CA.
   251  			return connSecurityHint()
   252  		}
   253  
   254  		if wErr := (*initialSQLConnectionError)(nil); errors.As(err, &wErr) {
   255  			// SQL handshake failed after establishing a TCP connection
   256  			// successfully, something else than CockroachDB responded, was
   257  			// confused and closed the door on us.
   258  			return connRefused()
   259  		}
   260  
   261  		if wErr := (*pq.Error)(nil); errors.As(err, &wErr) {
   262  			// SQL commands will fail with a pq error but only after
   263  			// establishing a TCP connection successfully. So if we got
   264  			// here, there was a TCP connection already.
   265  
   266  			// Did we fail due to security settings?
   267  			if wErr.Code == pgcode.ProtocolViolation {
   268  				return connSecurityHint()
   269  			}
   270  			// Otherwise, there was a regular SQL error. Just report
   271  			// that.
   272  			return err
   273  		}
   274  
   275  		if wErr := (*net.OpError)(nil); errors.As(err, &wErr) {
   276  			// A non-RPC client command was used (e.g. a SQL command) and an
   277  			// error occurred early while establishing the TCP connection.
   278  
   279  			// Is this a TLS error?
   280  			if msg := wErr.Err.Error(); strings.HasPrefix(msg, "tls: ") {
   281  				// Error during the SSL handshake: a provided client
   282  				// certificate was invalid, but the CA was OK. (If the CA was
   283  				// not OK, we'd get a x509 error, see case above.)
   284  				return connSecurityHint()
   285  			}
   286  			return connFailed()
   287  		}
   288  
   289  		if wErr := (*netutil.InitialHeartbeatFailedError)(nil); errors.As(err, &wErr) {
   290  			// A GRPC TCP connection was established but there was an early failure.
   291  			// Try to distinguish the cases.
   292  			msg := wErr.Error()
   293  			if reGRPCConnRefused.MatchString(msg) {
   294  				return connFailed()
   295  			}
   296  			if reGRPCNoTLS.MatchString(msg) {
   297  				return connInsecureHint()
   298  			}
   299  			if reGRPCAuthFailure.MatchString(msg) {
   300  				return connSecurityHint()
   301  			}
   302  			if reGRPCConnFailed.MatchString(msg) /* gRPC 1.21 */ ||
   303  				status.Code(errors.Cause(err)) == codes.Unavailable /* gRPC 1.27 */ {
   304  				return connRefused()
   305  			}
   306  
   307  			// Other cases may be timeouts or other situations, these
   308  			// will be handled below.
   309  		}
   310  
   311  		opTimeout := func() error {
   312  			return errors.Errorf("operation timed out.\n\n%v", err)
   313  		}
   314  
   315  		// Is it a plain context cancellation (i.e. timeout)?
   316  		if errors.IsAny(err,
   317  			context.DeadlineExceeded,
   318  			context.Canceled) {
   319  			return opTimeout()
   320  		}
   321  
   322  		// Is it a GRPC-observed context cancellation (i.e. timeout), a GRPC
   323  		// connection error, or a known indication of a too-old server?
   324  		if code := status.Code(errors.Cause(err)); code == codes.DeadlineExceeded {
   325  			return opTimeout()
   326  		} else if code == codes.Unimplemented &&
   327  			strings.Contains(err.Error(), "unknown method Decommission") ||
   328  			strings.Contains(err.Error(), "unknown service cockroach.server.serverpb.Init") {
   329  			return fmt.Errorf(
   330  				"incompatible client and server versions (likely server version: v1.0, required: >=v1.1)")
   331  		} else if grpcutil.IsClosedConnection(err) {
   332  			return errors.Errorf("connection lost.\n\n%v", err)
   333  		}
   334  
   335  		// Does the server require GSSAPI authentication?
   336  		if strings.Contains(err.Error(), "pq: unknown authentication response: 7") {
   337  			return fmt.Errorf(
   338  				"server requires GSSAPI authentication for this user.\n" +
   339  					"The CockroachDB CLI does not support GSSAPI authentication; use 'psql' instead")
   340  		}
   341  
   342  		// Nothing we can special case, just return what we have.
   343  		return err
   344  	}
   345  }
   346  
   347  // maybeShoutError calls log.Shout on errors, better surfacing problems to the user.
   348  func maybeShoutError(
   349  	wrapped func(*cobra.Command, []string) error,
   350  ) func(*cobra.Command, []string) error {
   351  	return func(cmd *cobra.Command, args []string) error {
   352  		err := wrapped(cmd, args)
   353  		return checkAndMaybeShout(err)
   354  	}
   355  }
   356  
   357  func checkAndMaybeShout(err error) error {
   358  	return checkAndMaybeShoutTo(err, log.Shoutf)
   359  }
   360  
   361  func checkAndMaybeShoutTo(
   362  	err error, logger func(context.Context, log.Severity, string, ...interface{}),
   363  ) error {
   364  	if err == nil {
   365  		return nil
   366  	}
   367  	severity := log.Severity_ERROR
   368  	cause := err
   369  	var ec *cliError
   370  	if errors.As(err, &ec) {
   371  		severity = ec.severity
   372  		cause = ec.cause
   373  	}
   374  	logger(context.Background(), severity, "%v", cause)
   375  	return err
   376  }