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 }