diff --git a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go index e53c3e61fd1..363b8152b0c 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors.go @@ -18,6 +18,7 @@ package errors import ( "encoding/json" + "errors" "fmt" "net/http" "reflect" @@ -483,127 +484,141 @@ func NewGenericServerResponse(code int, verb string, qualifiedResource schema.Gr } // IsNotFound returns true if the specified error was created by NewNotFound. +// It supports wrapped errors. func IsNotFound(err error) bool { return ReasonForError(err) == metav1.StatusReasonNotFound } // IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists. +// It supports wrapped errors. func IsAlreadyExists(err error) bool { return ReasonForError(err) == metav1.StatusReasonAlreadyExists } // IsConflict determines if the err is an error which indicates the provided update conflicts. +// It supports wrapped errors. func IsConflict(err error) bool { return ReasonForError(err) == metav1.StatusReasonConflict } // IsInvalid determines if the err is an error which indicates the provided resource is not valid. +// It supports wrapped errors. func IsInvalid(err error) bool { return ReasonForError(err) == metav1.StatusReasonInvalid } // IsGone is true if the error indicates the requested resource is no longer available. +// It supports wrapped errors. func IsGone(err error) bool { return ReasonForError(err) == metav1.StatusReasonGone } // IsResourceExpired is true if the error indicates the resource has expired and the current action is // no longer possible. +// It supports wrapped errors. func IsResourceExpired(err error) bool { return ReasonForError(err) == metav1.StatusReasonExpired } // IsNotAcceptable determines if err is an error which indicates that the request failed due to an invalid Accept header +// It supports wrapped errors. func IsNotAcceptable(err error) bool { return ReasonForError(err) == metav1.StatusReasonNotAcceptable } // IsUnsupportedMediaType determines if err is an error which indicates that the request failed due to an invalid Content-Type header +// It supports wrapped errors. func IsUnsupportedMediaType(err error) bool { return ReasonForError(err) == metav1.StatusReasonUnsupportedMediaType } // 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. +// It supports wrapped errors. func IsMethodNotSupported(err error) bool { return ReasonForError(err) == metav1.StatusReasonMethodNotAllowed } // IsServiceUnavailable is true if the error indicates the underlying service is no longer available. +// It supports wrapped errors. func IsServiceUnavailable(err error) bool { return ReasonForError(err) == metav1.StatusReasonServiceUnavailable } // IsBadRequest determines if err is an error which indicates that the request is invalid. +// It supports wrapped errors. func IsBadRequest(err error) bool { return ReasonForError(err) == metav1.StatusReasonBadRequest } // IsUnauthorized determines if err is an error which indicates that the request is unauthorized and // requires authentication by the user. +// It supports wrapped errors. func IsUnauthorized(err error) bool { return ReasonForError(err) == metav1.StatusReasonUnauthorized } // IsForbidden determines if err is an error which indicates that the request is forbidden and cannot // be completed as requested. +// It supports wrapped errors. func IsForbidden(err error) bool { return ReasonForError(err) == metav1.StatusReasonForbidden } // IsTimeout determines if err is an error which indicates that request times out due to long // processing. +// It supports wrapped errors. func IsTimeout(err error) bool { return ReasonForError(err) == metav1.StatusReasonTimeout } // IsServerTimeout determines if err is an error which indicates that the request needs to be retried // by the client. +// It supports wrapped errors. func IsServerTimeout(err error) bool { return ReasonForError(err) == metav1.StatusReasonServerTimeout } // IsInternalError determines if err is an error which indicates an internal server error. +// It supports wrapped errors. func IsInternalError(err error) bool { return ReasonForError(err) == metav1.StatusReasonInternalError } // IsTooManyRequests determines if err is an error which indicates that there are too many requests // that the server cannot handle. +// It supports wrapped errors. func IsTooManyRequests(err error) bool { if ReasonForError(err) == metav1.StatusReasonTooManyRequests { return true } - switch t := err.(type) { - case APIStatus: - return t.Status().Code == http.StatusTooManyRequests + if status := APIStatus(nil); errors.As(err, &status) { + return status.Status().Code == http.StatusTooManyRequests } return false } // IsRequestEntityTooLargeError determines if err is an error which indicates // the request entity is too large. +// It supports wrapped errors. func IsRequestEntityTooLargeError(err error) bool { if ReasonForError(err) == metav1.StatusReasonRequestEntityTooLarge { return true } - switch t := err.(type) { - case APIStatus: - return t.Status().Code == http.StatusRequestEntityTooLarge + if status := APIStatus(nil); errors.As(err, &status) { + return status.Status().Code == http.StatusRequestEntityTooLarge } return false } // IsUnexpectedServerError returns true if the server response was not in the expected API format, // and may be the result of another HTTP actor. +// It supports wrapped errors. func IsUnexpectedServerError(err error) bool { - switch t := err.(type) { - case APIStatus: - if d := t.Status().Details; d != nil { - for _, cause := range d.Causes { - if cause.Type == metav1.CauseTypeUnexpectedServerResponse { - return true - } + if status := APIStatus(nil); errors.As(err, &status) && status.Status().Details != nil { + for _, cause := range status.Status().Details.Causes { + if cause.Type == metav1.CauseTypeUnexpectedServerResponse { + return true } } } @@ -611,38 +626,37 @@ func IsUnexpectedServerError(err error) bool { } // IsUnexpectedObjectError determines if err is due to an unexpected object from the master. +// It supports wrapped errors. func IsUnexpectedObjectError(err error) bool { - _, ok := err.(*UnexpectedObjectError) - return err != nil && ok + uoe := &UnexpectedObjectError{} + return err != nil && errors.As(err, &uoe) } // SuggestsClientDelay returns true if this error suggests a client delay as well as the // suggested seconds to wait, or false if the error does not imply a wait. It does not // address whether the error *should* be retried, since some errors (like a 3xx) may // request delay without retry. +// It supports wrapped errors. func SuggestsClientDelay(err error) (int, bool) { - switch t := err.(type) { - case APIStatus: - if t.Status().Details != nil { - switch t.Status().Reason { - // this StatusReason explicitly requests the caller to delay the action - case metav1.StatusReasonServerTimeout: - return int(t.Status().Details.RetryAfterSeconds), true - } - // If the client requests that we retry after a certain number of seconds - if t.Status().Details.RetryAfterSeconds > 0 { - return int(t.Status().Details.RetryAfterSeconds), true - } + if t := APIStatus(nil); errors.As(err, &t) && t.Status().Details != nil { + switch t.Status().Reason { + // this StatusReason explicitly requests the caller to delay the action + case metav1.StatusReasonServerTimeout: + return int(t.Status().Details.RetryAfterSeconds), true + } + // If the client requests that we retry after a certain number of seconds + if t.Status().Details.RetryAfterSeconds > 0 { + return int(t.Status().Details.RetryAfterSeconds), true } } return 0, false } // ReasonForError returns the HTTP status for a particular error. +// It supports wrapped errors. func ReasonForError(err error) metav1.StatusReason { - switch t := err.(type) { - case APIStatus: - return t.Status().Reason + if status := APIStatus(nil); errors.As(err, &status) { + return status.Status().Reason } return metav1.StatusReasonUnknown } diff --git a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors_test.go b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors_test.go index 303a9d3f48f..6b9e3b81093 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/errors/errors_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/errors/errors_test.go @@ -19,6 +19,7 @@ package errors import ( "errors" "fmt" + "net/http" "reflect" "testing" @@ -220,3 +221,260 @@ func TestFromObject(t *testing.T) { } } } + +func TestReasonForErrorSupportsWrappedErrors(t *testing.T) { + testCases := []struct { + name string + err error + expectedReason metav1.StatusReason + }{ + { + name: "Direct match", + err: &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonUnauthorized}}, + expectedReason: metav1.StatusReasonUnauthorized, + }, + { + name: "No match", + err: errors.New("some other error"), + expectedReason: metav1.StatusReasonUnknown, + }, + { + name: "Nested match", + err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonAlreadyExists}})), + expectedReason: metav1.StatusReasonAlreadyExists, + }, + { + name: "Nested, no match", + err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), + expectedReason: metav1.StatusReasonUnknown, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if result := ReasonForError(tc.err); result != tc.expectedReason { + t.Errorf("expected reason: %q, but got known reason: %q", tc.expectedReason, result) + } + }) + } +} + +func TestIsTooManyRequestsSupportsWrappedErrors(t *testing.T) { + testCases := []struct { + name string + err error + expectMatch bool + }{ + { + name: "Direct match via status reason", + err: &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonTooManyRequests}}, + expectMatch: true, + }, + { + name: "Direct match via status code", + err: &StatusError{ErrStatus: metav1.Status{Code: http.StatusTooManyRequests}}, + expectMatch: true, + }, + { + name: "No match", + err: &StatusError{}, + expectMatch: false, + }, + { + name: "Nested match via status reason", + err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonTooManyRequests}}), + expectMatch: true, + }, + { + name: "Nested match via status code", + err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusTooManyRequests}}), + expectMatch: true, + }, + { + name: "Nested,no match", + err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}}), + expectMatch: false, + }, + } + + for _, tc := range testCases { + if result := IsTooManyRequests(tc.err); result != tc.expectMatch { + t.Errorf("Expect match %t, got match %t", tc.expectMatch, result) + } + } +} +func TestIsRequestEntityTooLargeErrorSupportsWrappedErrors(t *testing.T) { + testCases := []struct { + name string + err error + expectMatch bool + }{ + { + name: "Direct match via status reason", + err: &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonRequestEntityTooLarge}}, + expectMatch: true, + }, + { + name: "Direct match via status code", + err: &StatusError{ErrStatus: metav1.Status{Code: http.StatusRequestEntityTooLarge}}, + expectMatch: true, + }, + { + name: "No match", + err: &StatusError{}, + expectMatch: false, + }, + { + name: "Nested match via status reason", + err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Reason: metav1.StatusReasonRequestEntityTooLarge}}), + expectMatch: true, + }, + { + name: "Nested match via status code", + err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusRequestEntityTooLarge}}), + expectMatch: true, + }, + { + name: "Nested,no match", + err: fmt.Errorf("Wrapping: %w", &StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}}), + expectMatch: false, + }, + } + + for _, tc := range testCases { + if result := IsRequestEntityTooLargeError(tc.err); result != tc.expectMatch { + t.Errorf("Expect match %t, got match %t", tc.expectMatch, result) + } + } +} + +func TestIsUnexpectedServerError(t *testing.T) { + unexpectedServerErr := func() error { + return &StatusError{ + ErrStatus: metav1.Status{ + Details: &metav1.StatusDetails{ + Causes: []metav1.StatusCause{{Type: metav1.CauseTypeUnexpectedServerResponse}}, + }, + }, + } + } + testCases := []struct { + name string + err error + expectMatch bool + }{ + { + name: "Direct match", + err: unexpectedServerErr(), + expectMatch: true, + }, + { + name: "No match", + err: errors.New("some other error"), + expectMatch: false, + }, + { + name: "Nested match", + err: fmt.Errorf("wrapping: %w", unexpectedServerErr()), + expectMatch: true, + }, + { + name: "Nested, no match", + err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), + expectMatch: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if result := IsUnexpectedServerError(tc.err); result != tc.expectMatch { + t.Errorf("expected match: %t, but got match: %t", tc.expectMatch, result) + } + }) + } +} + +func TestIsUnexpectedObjectError(t *testing.T) { + unexpectedObjectErr := func() error { + return &UnexpectedObjectError{} + } + testCases := []struct { + name string + err error + expectMatch bool + }{ + { + name: "Direct match", + err: unexpectedObjectErr(), + expectMatch: true, + }, + { + name: "No match", + err: errors.New("some other error"), + expectMatch: false, + }, + { + name: "Nested match", + err: fmt.Errorf("wrapping: %w", unexpectedObjectErr()), + expectMatch: true, + }, + { + name: "Nested, no match", + err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), + expectMatch: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if result := IsUnexpectedObjectError(tc.err); result != tc.expectMatch { + t.Errorf("expected match: %t, but got match: %t", tc.expectMatch, result) + } + }) + } +} + +func TestSuggestsClientDelaySupportsWrapping(t *testing.T) { + suggestsClientDelayErr := func() error { + return &StatusError{ + ErrStatus: metav1.Status{ + Reason: metav1.StatusReasonServerTimeout, + Details: &metav1.StatusDetails{}, + }, + } + } + testCases := []struct { + name string + err error + expectMatch bool + }{ + { + name: "Direct match", + err: suggestsClientDelayErr(), + expectMatch: true, + }, + { + name: "No match", + err: errors.New("some other error"), + expectMatch: false, + }, + { + name: "Nested match", + err: fmt.Errorf("wrapping: %w", suggestsClientDelayErr()), + expectMatch: true, + }, + { + name: "Nested, no match", + err: fmt.Errorf("wrapping: %w", fmt.Errorf("some more: %w", errors.New("hello"))), + expectMatch: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if _, result := SuggestsClientDelay(tc.err); result != tc.expectMatch { + t.Errorf("expected match: %t, but got match: %t", tc.expectMatch, result) + } + }) + } +}