Make kubectl errors even more user-friendly

Omit glog prefix when v < 2, show multiline errors for configuration
problems, add new generic messages for server errors that hide some
complexity that is not relevant for users.
This commit is contained in:
Clayton Coleman 2015-04-01 23:20:09 -04:00
parent 9b5b27a12e
commit 323a44e54a
4 changed files with 86 additions and 66 deletions

View File

@ -17,6 +17,7 @@ limitations under the License.
package errors
import (
"encoding/json"
"fmt"
"net/http"
"strings"
@ -58,6 +59,14 @@ func (e *StatusError) Status() api.Status {
return e.ErrStatus
}
// DebugError reports extended info about the error to debug output.
func (e *StatusError) DebugError() (string, []interface{}) {
if out, err := json.MarshalIndent(e.ErrStatus, "", " "); err == nil {
return "server response object: %s", []interface{}{string(out)}
}
return "server response object: %#v", []interface{}{e.ErrStatus}
}
// UnexpectedObjectError can be returned by FromObject if it's passed a non-status object.
type UnexpectedObjectError struct {
Object runtime.Object
@ -355,16 +364,26 @@ func IsServerTimeout(err error) bool {
return reasonForError(err) == api.StatusReasonServerTimeout
}
// IsStatusError determines if err is an API Status error received from the master.
func IsStatusError(err error) bool {
_, ok := err.(*StatusError)
return ok
// IsUnexpectedServerError returns true if the server response was not in the expected API format,
// and may be the result of another HTTP actor.
func IsUnexpectedServerError(err error) bool {
switch t := err.(type) {
case *StatusError:
if d := t.Status().Details; d != nil {
for _, cause := range d.Causes {
if cause.Type == api.CauseTypeUnexpectedServerResponse {
return true
}
}
}
}
return false
}
// IsUnexpectedObjectError determines if err is due to an unexpected object from the master.
func IsUnexpectedObjectError(err error) bool {
_, ok := err.(*UnexpectedObjectError)
return ok
return err != nil && ok
}
// SuggestsClientDelay returns true if this error suggests a client delay as well as the

View File

@ -51,25 +51,6 @@ type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// UnexpectedStatusError is returned as an error if a response's body and HTTP code don't
// make sense together.
type UnexpectedStatusError struct {
Request *http.Request
Response *http.Response
Body string
}
// Error returns a textual description of 'u'.
func (u *UnexpectedStatusError) Error() string {
return fmt.Sprintf("request [%+v] failed (%d) %s: %s", u.Request, u.Response.StatusCode, u.Response.Status, u.Body)
}
// IsUnexpectedStatusError determines if err is due to an unexpected status from the server.
func IsUnexpectedStatusError(err error) bool {
_, ok := err.(*UnexpectedStatusError)
return ok
}
// RequestConstructionError is returned when there's an error assembling a request.
type RequestConstructionError struct {
Err error
@ -661,7 +642,6 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request, body
// initial contact, the presence of mismatched body contents from posted content types
// - Give these a separate distinct error type and capture as much as possible of the original message
//
// TODO: introduce further levels of refinement that allow a client to distinguish between 1 and 2-3.
// TODO: introduce transformation of generic http.Client.Do() errors that separates 4.
func (r *Request) transformUnstructuredResponseError(resp *http.Response, req *http.Request, body []byte) error {
if body == nil && resp.Body != nil {
@ -669,43 +649,12 @@ func (r *Request) transformUnstructuredResponseError(resp *http.Response, req *h
body = data
}
}
var err error = &UnexpectedStatusError{
Request: req,
Response: resp,
Body: string(body),
}
message := "unknown"
if isTextResponse(resp) {
message = strings.TrimSpace(string(body))
}
// TODO: handle other error classes we know about
switch resp.StatusCode {
case http.StatusConflict:
if req.Method == "POST" {
err = errors.NewAlreadyExists(r.resource, r.resourceName)
} else {
err = errors.NewConflict(r.resource, r.resourceName, err)
}
case http.StatusNotFound:
err = errors.NewNotFound(r.resource, r.resourceName)
case http.StatusBadRequest:
err = errors.NewBadRequest(message)
case http.StatusUnauthorized:
err = errors.NewUnauthorized(message)
case http.StatusForbidden:
err = errors.NewForbidden(r.resource, r.resourceName, err)
case errors.StatusUnprocessableEntity:
err = errors.NewInvalid(r.resource, r.resourceName, nil)
case errors.StatusServerTimeout:
retryAfterSeconds, _ := retryAfterSeconds(resp)
err = errors.NewServerTimeout(r.resource, r.verb, retryAfterSeconds)
case errors.StatusTooManyRequests:
retryAfterSeconds, _ := retryAfterSeconds(resp)
err = errors.NewServerTimeout(r.resource, r.verb, retryAfterSeconds)
case http.StatusInternalServerError:
err = errors.NewInternalError(fmt.Errorf(message))
}
return err
retryAfter, _ := retryAfterSeconds(resp)
return errors.NewGenericServerResponse(resp.StatusCode, req.Method, r.resource, r.resourceName, message, retryAfter)
}
// isTextResponse returns true if the response appears to be a textual media type.

View File

@ -251,7 +251,7 @@ func TestTransformResponse(t *testing.T) {
},
Error: true,
ErrFn: func(err error) bool {
return err.Error() == "aaaaa" && apierrors.IsUnauthorized(err)
return strings.Contains(err.Error(), "server has asked for the client to provide") && apierrors.IsUnauthorized(err)
},
},
{Response: &http.Response{StatusCode: 403}, Error: true},

View File

@ -17,11 +17,13 @@ limitations under the License.
package util
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"
"strconv"
"strings"
@ -30,7 +32,9 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors"
"github.com/evanphx/json-patch"
"github.com/golang/glog"
@ -38,21 +42,69 @@ import (
"github.com/spf13/pflag"
)
type debugError interface {
DebugError() (msg string, args []interface{})
}
func CheckErr(err error) {
if err != nil {
if errors.IsStatusError(err) {
glog.FatalDepth(1, fmt.Sprintf("Error received from API: %s", err.Error()))
if debugErr, ok := err.(debugError); ok {
glog.V(4).Infof(debugErr.DebugError())
}
if errors.IsUnexpectedObjectError(err) {
glog.FatalDepth(1, fmt.Sprintf("Unexpected object received from server: %s", err.Error()))
_, isStatus := err.(client.APIStatus)
switch {
case clientcmd.IsConfigurationInvalid(err):
fatal(MultilineError("Error in configuration: ", err))
case isStatus:
fatal(fmt.Sprintf("Error from server: %s", err.Error()))
case errors.IsUnexpectedObjectError(err):
fatal(fmt.Sprintf("Server returned an unexpected response: %s", err.Error()))
}
if client.IsUnexpectedStatusError(err) {
glog.FatalDepth(1, fmt.Sprintf("Unexpected status received from server: %s", err.Error()))
switch t := err.(type) {
case *url.Error:
glog.V(4).Infof("Connection error: %s %s: %v", t.Op, t.URL, t.Err)
switch {
case strings.Contains(t.Err.Error(), "connection refused"):
host := t.URL
if server, err := url.Parse(t.URL); err == nil {
host = server.Host
}
fatal(fmt.Sprintf("The connection to the server %s was refused - did you specify the right host or port?", host))
}
fatal(fmt.Sprintf("Unable to connect to the server: %v", t.Err))
}
glog.FatalDepth(1, fmt.Sprintf("Client error: %s", err.Error()))
fatal(fmt.Sprintf("Error: %s", err.Error()))
}
}
func MultilineError(prefix string, err error) string {
if agg, ok := err.(utilerrors.Aggregate); ok {
errs := agg.Errors()
buf := &bytes.Buffer{}
switch len(errs) {
case 0:
return fmt.Sprintf("%s%v", prefix, err)
case 1:
return fmt.Sprintf("%s%v", prefix, errs[0])
default:
fmt.Fprintln(buf, prefix)
for _, err := range errs {
fmt.Fprintf(buf, "* %v\n", err)
}
return buf.String()
}
}
return fmt.Sprintf("%s%s", prefix, err)
}
func fatal(msg string) {
if glog.V(2) {
glog.FatalDepth(2, msg)
}
fmt.Fprintln(os.Stderr, msg)
os.Exit(1)
}
func UsageError(cmd *cobra.Command, format string, args ...interface{}) error {
msg := fmt.Sprintf(format, args...)
return fmt.Errorf("%s\nsee '%s -h' for help.", msg, cmd.CommandPath())