Merge pull request #722 from smarterclayton/improve_errors

Normalize apiserver error handling of standard errors
This commit is contained in:
Daniel Smith 2014-08-11 12:43:06 -07:00
commit 9050c819dc
6 changed files with 202 additions and 113 deletions

View File

@ -82,7 +82,7 @@ func New(storage map[string]RESTStorage, codec Codec, prefix string) *APIServer
// Watch API handlers // Watch API handlers
watchPrefix := path.Join(prefix, "watch") + "/" watchPrefix := path.Join(prefix, "watch") + "/"
mux.Handle(watchPrefix, http.StripPrefix(watchPrefix, &WatchHandler{storage})) mux.Handle(watchPrefix, http.StripPrefix(watchPrefix, &WatchHandler{storage, codec}))
// Support services for the apiserver // Support services for the apiserver
logsPrefix := "/logs/" logsPrefix := "/logs/"
@ -167,23 +167,19 @@ func (s *APIServer) handleRESTStorage(parts []string, req *http.Request, w http.
case 1: case 1:
selector, err := labels.ParseSelector(req.URL.Query().Get("labels")) selector, err := labels.ParseSelector(req.URL.Query().Get("labels"))
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
list, err := storage.List(selector) list, err := storage.List(selector)
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
writeJSON(http.StatusOK, s.codec, list, w) writeJSON(http.StatusOK, s.codec, list, w)
case 2: case 2:
item, err := storage.Get(parts[1]) item, err := storage.Get(parts[1])
if IsNotFound(err) {
notFound(w, req)
return
}
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
writeJSON(http.StatusOK, s.codec, item, w) writeJSON(http.StatusOK, s.codec, item, w)
@ -198,26 +194,18 @@ func (s *APIServer) handleRESTStorage(parts []string, req *http.Request, w http.
} }
body, err := readBody(req) body, err := readBody(req)
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
obj := storage.New() obj := storage.New()
err = s.codec.DecodeInto(body, obj) err = s.codec.DecodeInto(body, obj)
if IsNotFound(err) {
notFound(w, req)
return
}
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
out, err := storage.Create(obj) out, err := storage.Create(obj)
if IsNotFound(err) {
notFound(w, req)
return
}
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
op := s.createOperation(out, sync, timeout) op := s.createOperation(out, sync, timeout)
@ -229,12 +217,8 @@ func (s *APIServer) handleRESTStorage(parts []string, req *http.Request, w http.
return return
} }
out, err := storage.Delete(parts[1]) out, err := storage.Delete(parts[1])
if IsNotFound(err) {
notFound(w, req)
return
}
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
op := s.createOperation(out, sync, timeout) op := s.createOperation(out, sync, timeout)
@ -247,26 +231,18 @@ func (s *APIServer) handleRESTStorage(parts []string, req *http.Request, w http.
} }
body, err := readBody(req) body, err := readBody(req)
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
obj := storage.New() obj := storage.New()
err = s.codec.DecodeInto(body, obj) err = s.codec.DecodeInto(body, obj)
if IsNotFound(err) {
notFound(w, req)
return
}
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
out, err := storage.Update(obj) out, err := storage.Update(obj)
if IsNotFound(err) {
notFound(w, req)
return
}
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, s.codec, w)
return return
} }
op := s.createOperation(out, sync, timeout) op := s.createOperation(out, sync, timeout)
@ -320,7 +296,7 @@ func (s *APIServer) finishReq(op *Operation, w http.ResponseWriter) {
func writeJSON(statusCode int, codec Codec, object interface{}, w http.ResponseWriter) { func writeJSON(statusCode int, codec Codec, object interface{}, w http.ResponseWriter) {
output, err := codec.Encode(object) output, err := codec.Encode(object)
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, codec, w)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -328,11 +304,17 @@ func writeJSON(statusCode int, codec Codec, object interface{}, w http.ResponseW
w.Write(output) w.Write(output)
} }
// errorJSON renders an error to the response
func errorJSON(err error, codec Codec, w http.ResponseWriter) {
status := errToAPIStatus(err)
writeJSON(status.Code, codec, status, w)
}
// writeRawJSON writes a non-API object in JSON. // writeRawJSON writes a non-API object in JSON.
func writeRawJSON(statusCode int, object interface{}, w http.ResponseWriter) { func writeRawJSON(statusCode int, object interface{}, w http.ResponseWriter) {
output, err := json.Marshal(object) output, err := json.Marshal(object)
if err != nil { if err != nil {
internalError(err, w) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")

View File

@ -25,6 +25,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"reflect" "reflect"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@ -433,26 +434,6 @@ func TestUpdateMissing(t *testing.T) {
} }
} }
func TestBadPath(t *testing.T) {
handler := New(map[string]RESTStorage{}, codec, "/prefix/version")
server := httptest.NewServer(handler)
client := http.Client{}
request, err := http.NewRequest("GET", server.URL+"/foobar", nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
response, err := client.Do(request)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if response.StatusCode != http.StatusNotFound {
t.Errorf("Unexpected response %#v", response)
}
}
func TestCreate(t *testing.T) { func TestCreate(t *testing.T) {
simpleStorage := &SimpleRESTStorage{} simpleStorage := &SimpleRESTStorage{}
handler := New(map[string]RESTStorage{ handler := New(map[string]RESTStorage{
@ -588,33 +569,64 @@ func expectApiStatus(t *testing.T, method, url string, data []byte, code int) *a
} }
response, err := client.Do(request) response, err := client.Do(request)
if err != nil { if err != nil {
t.Fatalf("unexpected error %#v", err) t.Fatalf("unexpected error on %s %s: %v", method, url, err)
return nil return nil
} }
var status api.Status var status api.Status
_, err = extractBody(response, &status) _, err = extractBody(response, &status)
if err != nil { if err != nil {
t.Fatalf("unexpected error %#v", err) t.Fatalf("unexpected error on %s %s: %v", method, url, err)
return nil return nil
} }
if code != response.StatusCode { if code != response.StatusCode {
t.Fatalf("Expected %d, Got %d", code, response.StatusCode) t.Fatalf("Expected %s %s to return %d, Got %d", method, url, code, response.StatusCode)
} }
return &status return &status
} }
func TestErrorsToAPIStatus(t *testing.T) {
cases := map[error]api.Status{
NewAlreadyExistsErr("foo", "bar"): api.Status{
Status: api.StatusFailure,
Code: http.StatusConflict,
Reason: "already_exists",
Message: "foo \"bar\" already exists",
Details: &api.StatusDetails{
Kind: "foo",
ID: "bar",
},
},
NewConflictErr("foo", "bar", errors.New("failure")): api.Status{
Status: api.StatusFailure,
Code: http.StatusConflict,
Reason: "conflict",
Message: "foo \"bar\" cannot be updated: failure",
Details: &api.StatusDetails{
Kind: "foo",
ID: "bar",
},
},
}
for k, v := range cases {
actual := errToAPIStatus(k)
if !reflect.DeepEqual(actual, &v) {
t.Errorf("Expected %#v, Got %#v", v, actual)
}
}
}
func TestAsyncDelayReturnsError(t *testing.T) { func TestAsyncDelayReturnsError(t *testing.T) {
storage := SimpleRESTStorage{ storage := SimpleRESTStorage{
injectedFunction: func(obj interface{}) (interface{}, error) { injectedFunction: func(obj interface{}) (interface{}, error) {
return nil, errors.New("error") return nil, NewAlreadyExistsErr("foo", "bar")
}, },
} }
handler := New(map[string]RESTStorage{"foo": &storage}, codec, "/prefix/version") handler := New(map[string]RESTStorage{"foo": &storage}, codec, "/prefix/version")
handler.asyncOpWait = time.Millisecond / 2 handler.asyncOpWait = time.Millisecond / 2
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/prefix/version/foo/bar", server.URL), nil, http.StatusInternalServerError) status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/prefix/version/foo/bar", server.URL), nil, http.StatusConflict)
if status.Status != api.StatusFailure || status.Message != "error" || status.Details != nil { if status.Status != api.StatusFailure || status.Message == "" || status.Details == nil || status.Reason != api.ReasonTypeAlreadyExists {
t.Errorf("Unexpected status %#v", status) t.Errorf("Unexpected status %#v", status)
} }
} }
@ -624,7 +636,7 @@ func TestAsyncCreateError(t *testing.T) {
storage := SimpleRESTStorage{ storage := SimpleRESTStorage{
injectedFunction: func(obj interface{}) (interface{}, error) { injectedFunction: func(obj interface{}) (interface{}, error) {
<-ch <-ch
return nil, errors.New("error") return nil, NewAlreadyExistsErr("foo", "bar")
}, },
} }
handler := New(map[string]RESTStorage{"foo": &storage}, codec, "/prefix/version") handler := New(map[string]RESTStorage{"foo": &storage}, codec, "/prefix/version")
@ -648,13 +660,22 @@ func TestAsyncCreateError(t *testing.T) {
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
finalStatus := expectApiStatus(t, "GET", fmt.Sprintf("%s/prefix/version/operations/%s?after=1", server.URL, status.Details.ID), []byte{}, http.StatusOK) finalStatus := expectApiStatus(t, "GET", fmt.Sprintf("%s/prefix/version/operations/%s?after=1", server.URL, status.Details.ID), []byte{}, http.StatusOK)
expectedErr := NewAlreadyExistsErr("foo", "bar")
expectedStatus := &api.Status{ expectedStatus := &api.Status{
Code: http.StatusInternalServerError,
Message: "error",
Status: api.StatusFailure, Status: api.StatusFailure,
Code: http.StatusConflict,
Reason: "already_exists",
Message: expectedErr.Error(),
Details: &api.StatusDetails{
Kind: "foo",
ID: "bar",
},
} }
if !reflect.DeepEqual(expectedStatus, finalStatus) { if !reflect.DeepEqual(expectedStatus, finalStatus) {
t.Errorf("Expected %#v, Got %#v", expectedStatus, finalStatus) t.Errorf("Expected %#v, Got %#v", expectedStatus, finalStatus)
if finalStatus.Details != nil {
t.Logf("Details %#v, Got %#v", *expectedStatus.Details, *finalStatus.Details)
}
} }
} }
@ -665,14 +686,12 @@ func TestWriteJSONDecodeError(t *testing.T) {
} }
writeJSON(http.StatusOK, api.Codec, &T{"Undecodable"}, w) writeJSON(http.StatusOK, api.Codec, &T{"Undecodable"}, w)
})) }))
client := http.Client{} status := expectApiStatus(t, "GET", server.URL, nil, http.StatusInternalServerError)
resp, err := client.Get(server.URL) if status.Reason != api.ReasonTypeUnknown {
if err != nil { t.Errorf("unexpected reason %#v", status)
t.Errorf("unexpected error: %v", err)
} }
if !strings.Contains(status.Message, "type apiserver.T is not registered") {
if resp.StatusCode != http.StatusInternalServerError { t.Errorf("unexpected message %#v", status)
t.Errorf("unexpected status code %d", resp.StatusCode)
} }
} }

View File

@ -17,10 +17,6 @@ limitations under the License.
package apiserver package apiserver
import ( import (
"net/http"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util"
) )
@ -37,16 +33,7 @@ func MakeAsync(fn WorkFunc) <-chan interface{} {
defer util.HandleCrash() defer util.HandleCrash()
obj, err := fn() obj, err := fn()
if err != nil { if err != nil {
status := http.StatusInternalServerError channel <- errToAPIStatus(err)
switch {
case tools.IsEtcdTestFailed(err):
status = http.StatusConflict
}
channel <- &api.Status{
Status: api.StatusFailure,
Message: err.Error(),
Code: status,
}
} else { } else {
channel <- obj channel <- obj
} }

View File

@ -19,50 +19,108 @@ package apiserver
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/tools"
) )
// errNotFound is an error which indicates that a specified resource is not found. // apiServerError is an error intended for consumption by a REST API server
type errNotFound string type apiServerError struct {
api.Status
// Error returns a string representation of the err.
func (err errNotFound) Error() string {
return string(err)
} }
// IsNotFound determines if the err is an error which indicates that a specified resource was not found. // Error implements the Error interface.
func IsNotFound(err error) bool { func (e *apiServerError) Error() string {
_, ok := err.(errNotFound) return e.Status.Message
return ok
} }
// NewNotFoundErr returns a new error which indicates that the resource of the kind and the name was not found. // NewNotFoundErr returns a new error which indicates that the resource of the kind and the name was not found.
func NewNotFoundErr(kind, name string) error { func NewNotFoundErr(kind, name string) error {
return errNotFound(fmt.Sprintf("%s %q not found", kind, name)) return &apiServerError{api.Status{
Status: api.StatusFailure,
Code: http.StatusNotFound,
Reason: api.ReasonTypeNotFound,
Details: &api.StatusDetails{
Kind: kind,
ID: name,
},
Message: fmt.Sprintf("%s %q not found", kind, name),
}}
} }
// errAlreadyExists is an error which indicates that a specified resource already exists. // NewAlreadyExistsErr returns an error indicating the item requested exists by that identifier
type errAlreadyExists string func NewAlreadyExistsErr(kind, name string) error {
return &apiServerError{api.Status{
Status: api.StatusFailure,
Code: http.StatusConflict,
Reason: api.ReasonTypeAlreadyExists,
Details: &api.StatusDetails{
Kind: kind,
ID: name,
},
Message: fmt.Sprintf("%s %q already exists", kind, name),
}}
}
// Error returns a string representation of the err. // NewConflictErr returns an error indicating the item can't be updated as provided.
func (err errAlreadyExists) Error() string { func NewConflictErr(kind, name string, err error) error {
return string(err) return &apiServerError{api.Status{
Status: api.StatusFailure,
Code: http.StatusConflict,
Reason: api.ReasonTypeConflict,
Details: &api.StatusDetails{
Kind: kind,
ID: name,
},
Message: fmt.Sprintf("%s %q cannot be updated: %s", kind, name, err),
}}
}
// IsNotFound returns true if the specified error was created by NewNotFoundErr
func IsNotFound(err error) bool {
return reasonForError(err) == api.ReasonTypeNotFound
} }
// IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists. // IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists.
func IsAlreadyExists(err error) bool { func IsAlreadyExists(err error) bool {
_, ok := err.(errAlreadyExists) return reasonForError(err) == api.ReasonTypeAlreadyExists
return ok
} }
// NewAlreadyExistsErr returns a new error which indicates that the resource of the kind and the name was not found. // IsConflict determines if the err is an error which indicates the provided update conflicts
func NewAlreadyExistsErr(kind, name string) error { func IsConflict(err error) bool {
return errAlreadyExists(fmt.Sprintf("%s %q already exists", kind, name)) return reasonForError(err) == api.ReasonTypeConflict
} }
// internalError renders a generic error to the response func reasonForError(err error) api.ReasonType {
func internalError(err error, w http.ResponseWriter) { switch t := err.(type) {
w.WriteHeader(http.StatusInternalServerError) case *apiServerError:
fmt.Fprintf(w, "Internal Error: %#v", err) return t.Status.Reason
}
return api.ReasonTypeUnknown
}
// errToAPIStatus converts an error to an api.Status object.
func errToAPIStatus(err error) *api.Status {
switch t := err.(type) {
case *apiServerError:
status := t.Status
status.Status = api.StatusFailure
//TODO: check for invalid responses
return &status
default:
status := http.StatusInternalServerError
switch {
//TODO: replace me with NewUpdateConflictErr
case tools.IsEtcdTestFailed(err):
status = http.StatusConflict
}
return &api.Status{
Status: api.StatusFailure,
Code: status,
Reason: api.ReasonTypeUnknown,
Message: err.Error(),
}
}
} }
// notFound renders a simple not found error // notFound renders a simple not found error

View File

@ -0,0 +1,42 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apiserver
import (
"errors"
"testing"
)
func TestErrorNew(t *testing.T) {
err := NewAlreadyExistsErr("test", "1")
if !IsAlreadyExists(err) {
t.Errorf("expected to be already_exists")
}
if IsConflict(err) {
t.Errorf("expected to not be confict")
}
if IsNotFound(err) {
t.Errorf("expected to not be not_found")
}
if !IsConflict(NewConflictErr("test", "2", errors.New("message"))) {
t.Errorf("expected to be confict")
}
if !IsNotFound(NewNotFoundErr("test", "3")) {
t.Errorf("expected to be not found")
}
}

View File

@ -31,6 +31,7 @@ import (
type WatchHandler struct { type WatchHandler struct {
storage map[string]RESTStorage storage map[string]RESTStorage
codec Codec
} }
func getWatchParams(query url.Values) (label, field labels.Selector, resourceVersion uint64) { func getWatchParams(query url.Values) (label, field labels.Selector, resourceVersion uint64) {
@ -66,7 +67,7 @@ func (h *WatchHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
label, field, resourceVersion := getWatchParams(req.URL.Query()) label, field, resourceVersion := getWatchParams(req.URL.Query())
watching, err := watcher.Watch(label, field, resourceVersion) watching, err := watcher.Watch(label, field, resourceVersion)
if err != nil { if err != nil {
internalError(err, w) errorJSON(err, h.codec, w)
return return
} }