1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package errors 18 19import ( 20 "encoding/json" 21 "fmt" 22 "net/http" 23 "strings" 24 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/runtime/schema" 28 "k8s.io/apimachinery/pkg/util/validation/field" 29) 30 31// HTTP Status codes not in the golang http package. 32const ( 33 StatusUnprocessableEntity = 422 34 StatusTooManyRequests = 429 35 // StatusServerTimeout is an indication that a transient server error has 36 // occurred and the client *should* retry, with an optional Retry-After 37 // header to specify the back off window. 38 StatusServerTimeout = 504 39) 40 41// StatusError is an error intended for consumption by a REST API server; it can also be 42// reconstructed by clients from a REST response. Public to allow easy type switches. 43type StatusError struct { 44 ErrStatus metav1.Status 45} 46 47// APIStatus is exposed by errors that can be converted to an api.Status object 48// for finer grained details. 49type APIStatus interface { 50 Status() metav1.Status 51} 52 53var _ error = &StatusError{} 54 55// Error implements the Error interface. 56func (e *StatusError) Error() string { 57 return e.ErrStatus.Message 58} 59 60// Status allows access to e's status without having to know the detailed workings 61// of StatusError. 62func (e *StatusError) Status() metav1.Status { 63 return e.ErrStatus 64} 65 66// DebugError reports extended info about the error to debug output. 67func (e *StatusError) DebugError() (string, []interface{}) { 68 if out, err := json.MarshalIndent(e.ErrStatus, "", " "); err == nil { 69 return "server response object: %s", []interface{}{string(out)} 70 } 71 return "server response object: %#v", []interface{}{e.ErrStatus} 72} 73 74// UnexpectedObjectError can be returned by FromObject if it's passed a non-status object. 75type UnexpectedObjectError struct { 76 Object runtime.Object 77} 78 79// Error returns an error message describing 'u'. 80func (u *UnexpectedObjectError) Error() string { 81 return fmt.Sprintf("unexpected object: %v", u.Object) 82} 83 84// FromObject generates an StatusError from an metav1.Status, if that is the type of obj; otherwise, 85// returns an UnexpecteObjectError. 86func FromObject(obj runtime.Object) error { 87 switch t := obj.(type) { 88 case *metav1.Status: 89 return &StatusError{*t} 90 } 91 return &UnexpectedObjectError{obj} 92} 93 94// NewNotFound returns a new error which indicates that the resource of the kind and the name was not found. 95func NewNotFound(qualifiedResource schema.GroupResource, name string) *StatusError { 96 return &StatusError{metav1.Status{ 97 Status: metav1.StatusFailure, 98 Code: http.StatusNotFound, 99 Reason: metav1.StatusReasonNotFound, 100 Details: &metav1.StatusDetails{ 101 Group: qualifiedResource.Group, 102 Kind: qualifiedResource.Resource, 103 Name: name, 104 }, 105 Message: fmt.Sprintf("%s %q not found", qualifiedResource.String(), name), 106 }} 107} 108 109// NewAlreadyExists returns an error indicating the item requested exists by that identifier. 110func NewAlreadyExists(qualifiedResource schema.GroupResource, name string) *StatusError { 111 return &StatusError{metav1.Status{ 112 Status: metav1.StatusFailure, 113 Code: http.StatusConflict, 114 Reason: metav1.StatusReasonAlreadyExists, 115 Details: &metav1.StatusDetails{ 116 Group: qualifiedResource.Group, 117 Kind: qualifiedResource.Resource, 118 Name: name, 119 }, 120 Message: fmt.Sprintf("%s %q already exists", qualifiedResource.String(), name), 121 }} 122} 123 124// NewUnauthorized returns an error indicating the client is not authorized to perform the requested 125// action. 126func NewUnauthorized(reason string) *StatusError { 127 message := reason 128 if len(message) == 0 { 129 message = "not authorized" 130 } 131 return &StatusError{metav1.Status{ 132 Status: metav1.StatusFailure, 133 Code: http.StatusUnauthorized, 134 Reason: metav1.StatusReasonUnauthorized, 135 Message: message, 136 }} 137} 138 139// NewForbidden returns an error indicating the requested action was forbidden 140func NewForbidden(qualifiedResource schema.GroupResource, name string, err error) *StatusError { 141 return &StatusError{metav1.Status{ 142 Status: metav1.StatusFailure, 143 Code: http.StatusForbidden, 144 Reason: metav1.StatusReasonForbidden, 145 Details: &metav1.StatusDetails{ 146 Group: qualifiedResource.Group, 147 Kind: qualifiedResource.Resource, 148 Name: name, 149 }, 150 Message: fmt.Sprintf("%s %q is forbidden: %v", qualifiedResource.String(), name, err), 151 }} 152} 153 154// NewConflict returns an error indicating the item can't be updated as provided. 155func NewConflict(qualifiedResource schema.GroupResource, name string, err error) *StatusError { 156 return &StatusError{metav1.Status{ 157 Status: metav1.StatusFailure, 158 Code: http.StatusConflict, 159 Reason: metav1.StatusReasonConflict, 160 Details: &metav1.StatusDetails{ 161 Group: qualifiedResource.Group, 162 Kind: qualifiedResource.Resource, 163 Name: name, 164 }, 165 Message: fmt.Sprintf("Operation cannot be fulfilled on %s %q: %v", qualifiedResource.String(), name, err), 166 }} 167} 168 169// NewGone returns an error indicating the item no longer available at the server and no forwarding address is known. 170func NewGone(message string) *StatusError { 171 return &StatusError{metav1.Status{ 172 Status: metav1.StatusFailure, 173 Code: http.StatusGone, 174 Reason: metav1.StatusReasonGone, 175 Message: message, 176 }} 177} 178 179// NewInvalid returns an error indicating the item is invalid and cannot be processed. 180func NewInvalid(qualifiedKind schema.GroupKind, name string, errs field.ErrorList) *StatusError { 181 causes := make([]metav1.StatusCause, 0, len(errs)) 182 for i := range errs { 183 err := errs[i] 184 causes = append(causes, metav1.StatusCause{ 185 Type: metav1.CauseType(err.Type), 186 Message: err.ErrorBody(), 187 Field: err.Field, 188 }) 189 } 190 return &StatusError{metav1.Status{ 191 Status: metav1.StatusFailure, 192 Code: StatusUnprocessableEntity, // RFC 4918: StatusUnprocessableEntity 193 Reason: metav1.StatusReasonInvalid, 194 Details: &metav1.StatusDetails{ 195 Group: qualifiedKind.Group, 196 Kind: qualifiedKind.Kind, 197 Name: name, 198 Causes: causes, 199 }, 200 Message: fmt.Sprintf("%s %q is invalid: %v", qualifiedKind.String(), name, errs.ToAggregate()), 201 }} 202} 203 204// NewBadRequest creates an error that indicates that the request is invalid and can not be processed. 205func NewBadRequest(reason string) *StatusError { 206 return &StatusError{metav1.Status{ 207 Status: metav1.StatusFailure, 208 Code: http.StatusBadRequest, 209 Reason: metav1.StatusReasonBadRequest, 210 Message: reason, 211 }} 212} 213 214// NewServiceUnavailable creates an error that indicates that the requested service is unavailable. 215func NewServiceUnavailable(reason string) *StatusError { 216 return &StatusError{metav1.Status{ 217 Status: metav1.StatusFailure, 218 Code: http.StatusServiceUnavailable, 219 Reason: metav1.StatusReasonServiceUnavailable, 220 Message: reason, 221 }} 222} 223 224// NewMethodNotSupported returns an error indicating the requested action is not supported on this kind. 225func NewMethodNotSupported(qualifiedResource schema.GroupResource, action string) *StatusError { 226 return &StatusError{metav1.Status{ 227 Status: metav1.StatusFailure, 228 Code: http.StatusMethodNotAllowed, 229 Reason: metav1.StatusReasonMethodNotAllowed, 230 Details: &metav1.StatusDetails{ 231 Group: qualifiedResource.Group, 232 Kind: qualifiedResource.Resource, 233 }, 234 Message: fmt.Sprintf("%s is not supported on resources of kind %q", action, qualifiedResource.String()), 235 }} 236} 237 238// NewServerTimeout returns an error indicating the requested action could not be completed due to a 239// transient error, and the client should try again. 240func NewServerTimeout(qualifiedResource schema.GroupResource, operation string, retryAfterSeconds int) *StatusError { 241 return &StatusError{metav1.Status{ 242 Status: metav1.StatusFailure, 243 Code: http.StatusInternalServerError, 244 Reason: metav1.StatusReasonServerTimeout, 245 Details: &metav1.StatusDetails{ 246 Group: qualifiedResource.Group, 247 Kind: qualifiedResource.Resource, 248 Name: operation, 249 RetryAfterSeconds: int32(retryAfterSeconds), 250 }, 251 Message: fmt.Sprintf("The %s operation against %s could not be completed at this time, please try again.", operation, qualifiedResource.String()), 252 }} 253} 254 255// NewServerTimeoutForKind should not exist. Server timeouts happen when accessing resources, the Kind is just what we 256// happened to be looking at when the request failed. This delegates to keep code sane, but we should work towards removing this. 257func NewServerTimeoutForKind(qualifiedKind schema.GroupKind, operation string, retryAfterSeconds int) *StatusError { 258 return NewServerTimeout(schema.GroupResource{Group: qualifiedKind.Group, Resource: qualifiedKind.Kind}, operation, retryAfterSeconds) 259} 260 261// NewInternalError returns an error indicating the item is invalid and cannot be processed. 262func NewInternalError(err error) *StatusError { 263 return &StatusError{metav1.Status{ 264 Status: metav1.StatusFailure, 265 Code: http.StatusInternalServerError, 266 Reason: metav1.StatusReasonInternalError, 267 Details: &metav1.StatusDetails{ 268 Causes: []metav1.StatusCause{{Message: err.Error()}}, 269 }, 270 Message: fmt.Sprintf("Internal error occurred: %v", err), 271 }} 272} 273 274// NewTimeoutError returns an error indicating that a timeout occurred before the request 275// could be completed. Clients may retry, but the operation may still complete. 276func NewTimeoutError(message string, retryAfterSeconds int) *StatusError { 277 return &StatusError{metav1.Status{ 278 Status: metav1.StatusFailure, 279 Code: StatusServerTimeout, 280 Reason: metav1.StatusReasonTimeout, 281 Message: fmt.Sprintf("Timeout: %s", message), 282 Details: &metav1.StatusDetails{ 283 RetryAfterSeconds: int32(retryAfterSeconds), 284 }, 285 }} 286} 287 288// NewGenericServerResponse returns a new error for server responses that are not in a recognizable form. 289func NewGenericServerResponse(code int, verb string, qualifiedResource schema.GroupResource, name, serverMessage string, retryAfterSeconds int, isUnexpectedResponse bool) *StatusError { 290 reason := metav1.StatusReasonUnknown 291 message := fmt.Sprintf("the server responded with the status code %d but did not return more information", code) 292 switch code { 293 case http.StatusConflict: 294 if verb == "POST" { 295 reason = metav1.StatusReasonAlreadyExists 296 } else { 297 reason = metav1.StatusReasonConflict 298 } 299 message = "the server reported a conflict" 300 case http.StatusNotFound: 301 reason = metav1.StatusReasonNotFound 302 message = "the server could not find the requested resource" 303 case http.StatusBadRequest: 304 reason = metav1.StatusReasonBadRequest 305 message = "the server rejected our request for an unknown reason" 306 case http.StatusUnauthorized: 307 reason = metav1.StatusReasonUnauthorized 308 message = "the server has asked for the client to provide credentials" 309 case http.StatusForbidden: 310 reason = metav1.StatusReasonForbidden 311 // the server message has details about who is trying to perform what action. Keep its message. 312 message = serverMessage 313 case http.StatusMethodNotAllowed: 314 reason = metav1.StatusReasonMethodNotAllowed 315 message = "the server does not allow this method on the requested resource" 316 case StatusUnprocessableEntity: 317 reason = metav1.StatusReasonInvalid 318 message = "the server rejected our request due to an error in our request" 319 case StatusServerTimeout: 320 reason = metav1.StatusReasonServerTimeout 321 message = "the server cannot complete the requested operation at this time, try again later" 322 case StatusTooManyRequests: 323 reason = metav1.StatusReasonTimeout 324 message = "the server has received too many requests and has asked us to try again later" 325 default: 326 if code >= 500 { 327 reason = metav1.StatusReasonInternalError 328 message = fmt.Sprintf("an error on the server (%q) has prevented the request from succeeding", serverMessage) 329 } 330 } 331 switch { 332 case !qualifiedResource.Empty() && len(name) > 0: 333 message = fmt.Sprintf("%s (%s %s %s)", message, strings.ToLower(verb), qualifiedResource.String(), name) 334 case !qualifiedResource.Empty(): 335 message = fmt.Sprintf("%s (%s %s)", message, strings.ToLower(verb), qualifiedResource.String()) 336 } 337 var causes []metav1.StatusCause 338 if isUnexpectedResponse { 339 causes = []metav1.StatusCause{ 340 { 341 Type: metav1.CauseTypeUnexpectedServerResponse, 342 Message: serverMessage, 343 }, 344 } 345 } else { 346 causes = nil 347 } 348 return &StatusError{metav1.Status{ 349 Status: metav1.StatusFailure, 350 Code: int32(code), 351 Reason: reason, 352 Details: &metav1.StatusDetails{ 353 Group: qualifiedResource.Group, 354 Kind: qualifiedResource.Resource, 355 Name: name, 356 357 Causes: causes, 358 RetryAfterSeconds: int32(retryAfterSeconds), 359 }, 360 Message: message, 361 }} 362} 363 364// IsNotFound returns true if the specified error was created by NewNotFound. 365func IsNotFound(err error) bool { 366 return reasonForError(err) == metav1.StatusReasonNotFound 367} 368 369// IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists. 370func IsAlreadyExists(err error) bool { 371 return reasonForError(err) == metav1.StatusReasonAlreadyExists 372} 373 374// IsConflict determines if the err is an error which indicates the provided update conflicts. 375func IsConflict(err error) bool { 376 return reasonForError(err) == metav1.StatusReasonConflict 377} 378 379// IsInvalid determines if the err is an error which indicates the provided resource is not valid. 380func IsInvalid(err error) bool { 381 return reasonForError(err) == metav1.StatusReasonInvalid 382} 383 384// IsMethodNotSupported determines if the err is an error which indicates the provided action could not 385// be performed because it is not supported by the server. 386func IsMethodNotSupported(err error) bool { 387 return reasonForError(err) == metav1.StatusReasonMethodNotAllowed 388} 389 390// IsBadRequest determines if err is an error which indicates that the request is invalid. 391func IsBadRequest(err error) bool { 392 return reasonForError(err) == metav1.StatusReasonBadRequest 393} 394 395// IsUnauthorized determines if err is an error which indicates that the request is unauthorized and 396// requires authentication by the user. 397func IsUnauthorized(err error) bool { 398 return reasonForError(err) == metav1.StatusReasonUnauthorized 399} 400 401// IsForbidden determines if err is an error which indicates that the request is forbidden and cannot 402// be completed as requested. 403func IsForbidden(err error) bool { 404 return reasonForError(err) == metav1.StatusReasonForbidden 405} 406 407// IsTimeout determines if err is an error which indicates that request times out due to long 408// processing. 409func IsTimeout(err error) bool { 410 return reasonForError(err) == metav1.StatusReasonTimeout 411} 412 413// IsServerTimeout determines if err is an error which indicates that the request needs to be retried 414// by the client. 415func IsServerTimeout(err error) bool { 416 return reasonForError(err) == metav1.StatusReasonServerTimeout 417} 418 419// IsInternalError determines if err is an error which indicates an internal server error. 420func IsInternalError(err error) bool { 421 return reasonForError(err) == metav1.StatusReasonInternalError 422} 423 424// IsTooManyRequests determines if err is an error which indicates that there are too many requests 425// that the server cannot handle. 426// TODO: update IsTooManyRequests() when the TooManyRequests(429) error returned from the API server has a non-empty Reason field 427func IsTooManyRequests(err error) bool { 428 switch t := err.(type) { 429 case APIStatus: 430 return t.Status().Code == StatusTooManyRequests 431 } 432 return false 433} 434 435// IsUnexpectedServerError returns true if the server response was not in the expected API format, 436// and may be the result of another HTTP actor. 437func IsUnexpectedServerError(err error) bool { 438 switch t := err.(type) { 439 case APIStatus: 440 if d := t.Status().Details; d != nil { 441 for _, cause := range d.Causes { 442 if cause.Type == metav1.CauseTypeUnexpectedServerResponse { 443 return true 444 } 445 } 446 } 447 } 448 return false 449} 450 451// IsUnexpectedObjectError determines if err is due to an unexpected object from the master. 452func IsUnexpectedObjectError(err error) bool { 453 _, ok := err.(*UnexpectedObjectError) 454 return err != nil && ok 455} 456 457// SuggestsClientDelay returns true if this error suggests a client delay as well as the 458// suggested seconds to wait, or false if the error does not imply a wait. 459func SuggestsClientDelay(err error) (int, bool) { 460 switch t := err.(type) { 461 case APIStatus: 462 if t.Status().Details != nil { 463 switch t.Status().Reason { 464 case metav1.StatusReasonServerTimeout, metav1.StatusReasonTimeout: 465 return int(t.Status().Details.RetryAfterSeconds), true 466 } 467 } 468 } 469 return 0, false 470} 471 472func reasonForError(err error) metav1.StatusReason { 473 switch t := err.(type) { 474 case APIStatus: 475 return t.Status().Reason 476 } 477 return metav1.StatusReasonUnknown 478} 479