Merge pull request #54477 from nikhita/invalid-patch-code

Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

apiserver: return 4xx for invalid patch

Fixes #54423 

Currently, an invalid patch returns 500. The apiserver should return a 400 (`BadRequest`) or 422 (`Unprocessable Entity`).

**Release note**:

```release-note
NONE
```
This commit is contained in:
Kubernetes Submit Queue 2017-10-30 03:30:56 -07:00 committed by GitHub
commit 6659f2a7d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 27 deletions

View File

@ -405,84 +405,84 @@ func NewGenericServerResponse(code int, verb string, qualifiedResource schema.Gr
// IsNotFound returns true if the specified error was created by NewNotFound. // IsNotFound returns true if the specified error was created by NewNotFound.
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return reasonForError(err) == metav1.StatusReasonNotFound return ReasonForError(err) == metav1.StatusReasonNotFound
} }
// 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 {
return reasonForError(err) == metav1.StatusReasonAlreadyExists return ReasonForError(err) == metav1.StatusReasonAlreadyExists
} }
// IsConflict determines if the err is an error which indicates the provided update conflicts. // IsConflict determines if the err is an error which indicates the provided update conflicts.
func IsConflict(err error) bool { func IsConflict(err error) bool {
return reasonForError(err) == metav1.StatusReasonConflict return ReasonForError(err) == metav1.StatusReasonConflict
} }
// IsInvalid determines if the err is an error which indicates the provided resource is not valid. // IsInvalid determines if the err is an error which indicates the provided resource is not valid.
func IsInvalid(err error) bool { func IsInvalid(err error) bool {
return reasonForError(err) == metav1.StatusReasonInvalid return ReasonForError(err) == metav1.StatusReasonInvalid
} }
// IsGone is true if the error indicates the requested resource is no longer available. // IsGone is true if the error indicates the requested resource is no longer available.
func IsGone(err error) bool { func IsGone(err error) bool {
return reasonForError(err) == metav1.StatusReasonGone return ReasonForError(err) == metav1.StatusReasonGone
} }
// IsResourceExpired is true if the error indicates the resource has expired and the current action is // IsResourceExpired is true if the error indicates the resource has expired and the current action is
// no longer possible. // no longer possible.
func IsResourceExpired(err error) bool { func IsResourceExpired(err error) bool {
return reasonForError(err) == metav1.StatusReasonExpired return ReasonForError(err) == metav1.StatusReasonExpired
} }
// IsMethodNotSupported determines if the err is an error which indicates the provided action could not // IsMethodNotSupported determines if the err is an error which indicates the provided action could not
// be performed because it is not supported by the server. // be performed because it is not supported by the server.
func IsMethodNotSupported(err error) bool { func IsMethodNotSupported(err error) bool {
return reasonForError(err) == metav1.StatusReasonMethodNotAllowed return ReasonForError(err) == metav1.StatusReasonMethodNotAllowed
} }
// IsServiceUnavailable is true if the error indicates the underlying service is no longer available. // IsServiceUnavailable is true if the error indicates the underlying service is no longer available.
func IsServiceUnavailable(err error) bool { func IsServiceUnavailable(err error) bool {
return reasonForError(err) == metav1.StatusReasonServiceUnavailable return ReasonForError(err) == metav1.StatusReasonServiceUnavailable
} }
// IsBadRequest determines if err is an error which indicates that the request is invalid. // IsBadRequest determines if err is an error which indicates that the request is invalid.
func IsBadRequest(err error) bool { func IsBadRequest(err error) bool {
return reasonForError(err) == metav1.StatusReasonBadRequest return ReasonForError(err) == metav1.StatusReasonBadRequest
} }
// IsUnauthorized determines if err is an error which indicates that the request is unauthorized and // IsUnauthorized determines if err is an error which indicates that the request is unauthorized and
// requires authentication by the user. // requires authentication by the user.
func IsUnauthorized(err error) bool { func IsUnauthorized(err error) bool {
return reasonForError(err) == metav1.StatusReasonUnauthorized return ReasonForError(err) == metav1.StatusReasonUnauthorized
} }
// IsForbidden determines if err is an error which indicates that the request is forbidden and cannot // IsForbidden determines if err is an error which indicates that the request is forbidden and cannot
// be completed as requested. // be completed as requested.
func IsForbidden(err error) bool { func IsForbidden(err error) bool {
return reasonForError(err) == metav1.StatusReasonForbidden return ReasonForError(err) == metav1.StatusReasonForbidden
} }
// IsTimeout determines if err is an error which indicates that request times out due to long // IsTimeout determines if err is an error which indicates that request times out due to long
// processing. // processing.
func IsTimeout(err error) bool { func IsTimeout(err error) bool {
return reasonForError(err) == metav1.StatusReasonTimeout return ReasonForError(err) == metav1.StatusReasonTimeout
} }
// IsServerTimeout determines if err is an error which indicates that the request needs to be retried // IsServerTimeout determines if err is an error which indicates that the request needs to be retried
// by the client. // by the client.
func IsServerTimeout(err error) bool { func IsServerTimeout(err error) bool {
return reasonForError(err) == metav1.StatusReasonServerTimeout return ReasonForError(err) == metav1.StatusReasonServerTimeout
} }
// IsInternalError determines if err is an error which indicates an internal server error. // IsInternalError determines if err is an error which indicates an internal server error.
func IsInternalError(err error) bool { func IsInternalError(err error) bool {
return reasonForError(err) == metav1.StatusReasonInternalError return ReasonForError(err) == metav1.StatusReasonInternalError
} }
// IsTooManyRequests determines if err is an error which indicates that there are too many requests // IsTooManyRequests determines if err is an error which indicates that there are too many requests
// that the server cannot handle. // that the server cannot handle.
func IsTooManyRequests(err error) bool { func IsTooManyRequests(err error) bool {
if reasonForError(err) == metav1.StatusReasonTooManyRequests { if ReasonForError(err) == metav1.StatusReasonTooManyRequests {
return true return true
} }
switch t := err.(type) { switch t := err.(type) {
@ -536,7 +536,8 @@ func SuggestsClientDelay(err error) (int, bool) {
return 0, false return 0, false
} }
func reasonForError(err error) metav1.StatusReason { // ReasonForError returns the HTTP status for a particular error.
func ReasonForError(err error) metav1.StatusReason {
switch t := err.(type) { switch t := err.(type) {
case APIStatus: case APIStatus:
return t.Status().Reason return t.Status().Reason

View File

@ -188,8 +188,8 @@ func TestNewInvalid(t *testing.T) {
} }
} }
func Test_reasonForError(t *testing.T) { func TestReasonForError(t *testing.T) {
if e, a := metav1.StatusReasonUnknown, reasonForError(nil); e != a { if e, a := metav1.StatusReasonUnknown, ReasonForError(nil); e != a {
t.Errorf("unexpected reason type: %#v", a) t.Errorf("unexpected reason type: %#v", a)
} }
} }

View File

@ -1552,7 +1552,7 @@ func sortMergeListsByName(mapJSON []byte, dataStruct interface{}) ([]byte, error
var m map[string]interface{} var m map[string]interface{}
err := json.Unmarshal(mapJSON, &m) err := json.Unmarshal(mapJSON, &m)
if err != nil { if err != nil {
return nil, err return nil, mergepatch.ErrBadJSONDoc
} }
newM, err := sortMergeListsByNameMap(m, reflect.TypeOf(dataStruct)) newM, err := sortMergeListsByNameMap(m, reflect.TypeOf(dataStruct))

View File

@ -186,7 +186,7 @@ func patchResource(
case types.JSONPatchType, types.MergePatchType: case types.JSONPatchType, types.MergePatchType:
originalJS, patchedJS, err := patchObjectJSON(patchType, codec, currentObject, patchJS, objToUpdate, versionedObj) originalJS, patchedJS, err := patchObjectJSON(patchType, codec, currentObject, patchJS, objToUpdate, versionedObj)
if err != nil { if err != nil {
return nil, err return nil, interpretPatchError(err)
} }
originalObjJS, originalPatchedObjJS = originalJS, patchedJS originalObjJS, originalPatchedObjJS = originalJS, patchedJS
@ -198,13 +198,13 @@ func patchResource(
// Compute once // Compute once
originalPatchBytes, err = strategicpatch.CreateTwoWayMergePatch(originalObjJS, originalPatchedObjJS, versionedObj) originalPatchBytes, err = strategicpatch.CreateTwoWayMergePatch(originalObjJS, originalPatchedObjJS, versionedObj)
if err != nil { if err != nil {
return nil, err return nil, interpretPatchError(err)
} }
} }
// Return a fresh map every time // Return a fresh map every time
originalPatchMap := make(map[string]interface{}) originalPatchMap := make(map[string]interface{})
if err := json.Unmarshal(originalPatchBytes, &originalPatchMap); err != nil { if err := json.Unmarshal(originalPatchBytes, &originalPatchMap); err != nil {
return nil, err return nil, errors.NewBadRequest(err.Error())
} }
return originalPatchMap, nil return originalPatchMap, nil
} }
@ -242,7 +242,7 @@ func patchResource(
getOriginalPatchMap = func() (map[string]interface{}, error) { getOriginalPatchMap = func() (map[string]interface{}, error) {
patchMap := make(map[string]interface{}) patchMap := make(map[string]interface{})
if err := json.Unmarshal(patchJS, &patchMap); err != nil { if err := json.Unmarshal(patchJS, &patchMap); err != nil {
return nil, err return nil, errors.NewBadRequest(err.Error())
} }
return patchMap, nil return patchMap, nil
} }
@ -277,7 +277,7 @@ func patchResource(
var err error var err error
currentPatchMap, err = strategicpatch.CreateTwoWayMergeMapPatch(originalObjMap, currentObjMap, versionedObj) currentPatchMap, err = strategicpatch.CreateTwoWayMergeMapPatch(originalObjMap, currentObjMap, versionedObj)
if err != nil { if err != nil {
return nil, err return nil, interpretPatchError(err)
} }
} else { } else {
// Compute current patch. // Compute current patch.
@ -287,11 +287,11 @@ func patchResource(
} }
currentPatch, err := strategicpatch.CreateTwoWayMergePatch(originalObjJS, currentObjJS, versionedObj) currentPatch, err := strategicpatch.CreateTwoWayMergePatch(originalObjJS, currentObjJS, versionedObj)
if err != nil { if err != nil {
return nil, err return nil, interpretPatchError(err)
} }
currentPatchMap = make(map[string]interface{}) currentPatchMap = make(map[string]interface{})
if err := json.Unmarshal(currentPatch, &currentPatchMap); err != nil { if err := json.Unmarshal(currentPatch, &currentPatchMap); err != nil {
return nil, err return nil, errors.NewBadRequest(err.Error())
} }
} }
@ -422,7 +422,7 @@ func strategicPatchObject(
patchMap := make(map[string]interface{}) patchMap := make(map[string]interface{})
if err := json.Unmarshal(patchJS, &patchMap); err != nil { if err := json.Unmarshal(patchJS, &patchMap); err != nil {
return err return errors.NewBadRequest(err.Error())
} }
if err := applyPatchToObject(codec, defaulter, originalObjMap, patchMap, objToUpdate, versionedObj); err != nil { if err := applyPatchToObject(codec, defaulter, originalObjMap, patchMap, objToUpdate, versionedObj); err != nil {
@ -456,3 +456,15 @@ func applyPatchToObject(
return nil return nil
} }
// interpretPatchError interprets the error type and returns an error with appropriate HTTP code.
func interpretPatchError(err error) error {
switch err {
case mergepatch.ErrBadJSONDoc, mergepatch.ErrBadPatchFormatForPrimitiveList, mergepatch.ErrBadPatchFormatForRetainKeys, mergepatch.ErrBadPatchFormatForSetElementOrderList:
return errors.NewBadRequest(err.Error())
case mergepatch.ErrNoListOfLists, mergepatch.ErrPatchContentNotMatchRetainKeys:
return errors.NewGenericServerResponse(http.StatusUnprocessableEntity, "", schema.GroupResource{}, "", err.Error(), 0, false)
default:
return err
}
}

View File

@ -98,6 +98,29 @@ func TestPatchAnonymousField(t *testing.T) {
} }
} }
func TestPatchInvalid(t *testing.T) {
testGV := schema.GroupVersion{Group: "", Version: "v"}
scheme.AddKnownTypes(testGV, &testPatchType{})
codec := codecs.LegacyCodec(testGV)
defaulter := runtime.ObjectDefaulter(scheme)
original := &testPatchType{
TypeMeta: metav1.TypeMeta{Kind: "testPatchType", APIVersion: "v"},
TestPatchSubType: TestPatchSubType{StringField: "my-value"},
}
patch := `barbaz`
expectedError := "invalid character 'b' looking for beginning of value"
actual := &testPatchType{}
err := strategicPatchObject(codec, defaulter, original, []byte(patch), actual, &testPatchType{})
if apierrors.IsBadRequest(err) == false {
t.Errorf("expected HTTP status: BadRequest, got: %#v", apierrors.ReasonForError(err))
}
if err.Error() != expectedError {
t.Errorf("expected %#v, got %#v", expectedError, err.Error())
}
}
type testPatcher struct { type testPatcher struct {
t *testing.T t *testing.T