mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
Apierrors Is<<ErrType>>: Support wrapped errors
This commit is contained in:
parent
650220fa64
commit
3244350046
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user