diff --git a/pkg/api/errors/errors.go b/pkg/api/errors/errors.go index 94177d649ee..5fcfbad5f4c 100644 --- a/pkg/api/errors/errors.go +++ b/pkg/api/errors/errors.go @@ -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 diff --git a/pkg/client/request.go b/pkg/client/request.go index adec12529cd..7fc2500ae47 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -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. diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index b2e88a02b21..533b46c31b3 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -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}, diff --git a/pkg/kubectl/cmd/util/helpers.go b/pkg/kubectl/cmd/util/helpers.go index 158fefbf1b3..d251692a512 100644 --- a/pkg/kubectl/cmd/util/helpers.go +++ b/pkg/kubectl/cmd/util/helpers.go @@ -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())