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	"reflect"
24	"strings"
25
26	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27	"k8s.io/apimachinery/pkg/runtime"
28	"k8s.io/apimachinery/pkg/runtime/schema"
29	"k8s.io/apimachinery/pkg/util/validation/field"
30)
31
32const (
33	// StatusTooManyRequests means the server experienced too many requests within a
34	// given window and that the client must wait to perform the action again.
35	StatusTooManyRequests = 429
36)
37
38// StatusError is an error intended for consumption by a REST API server; it can also be
39// reconstructed by clients from a REST response. Public to allow easy type switches.
40type StatusError struct {
41	ErrStatus metav1.Status
42}
43
44// APIStatus is exposed by errors that can be converted to an api.Status object
45// for finer grained details.
46type APIStatus interface {
47	Status() metav1.Status
48}
49
50var _ error = &StatusError{}
51
52// Error implements the Error interface.
53func (e *StatusError) Error() string {
54	return e.ErrStatus.Message
55}
56
57// Status allows access to e's status without having to know the detailed workings
58// of StatusError.
59func (e *StatusError) Status() metav1.Status {
60	return e.ErrStatus
61}
62
63// DebugError reports extended info about the error to debug output.
64func (e *StatusError) DebugError() (string, []interface{}) {
65	if out, err := json.MarshalIndent(e.ErrStatus, "", "  "); err == nil {
66		return "server response object: %s", []interface{}{string(out)}
67	}
68	return "server response object: %#v", []interface{}{e.ErrStatus}
69}
70
71// UnexpectedObjectError can be returned by FromObject if it's passed a non-status object.
72type UnexpectedObjectError struct {
73	Object runtime.Object
74}
75
76// Error returns an error message describing 'u'.
77func (u *UnexpectedObjectError) Error() string {
78	return fmt.Sprintf("unexpected object: %v", u.Object)
79}
80
81// FromObject generates an StatusError from an metav1.Status, if that is the type of obj; otherwise,
82// returns an UnexpecteObjectError.
83func FromObject(obj runtime.Object) error {
84	switch t := obj.(type) {
85	case *metav1.Status:
86		return &StatusError{ErrStatus: *t}
87	case runtime.Unstructured:
88		var status metav1.Status
89		obj := t.UnstructuredContent()
90		if !reflect.DeepEqual(obj["kind"], "Status") {
91			break
92		}
93		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(t.UnstructuredContent(), &status); err != nil {
94			return err
95		}
96		if status.APIVersion != "v1" && status.APIVersion != "meta.k8s.io/v1" {
97			break
98		}
99		return &StatusError{ErrStatus: status}
100	}
101	return &UnexpectedObjectError{obj}
102}
103
104// NewNotFound returns a new error which indicates that the resource of the kind and the name was not found.
105func NewNotFound(qualifiedResource schema.GroupResource, name string) *StatusError {
106	return &StatusError{metav1.Status{
107		Status: metav1.StatusFailure,
108		Code:   http.StatusNotFound,
109		Reason: metav1.StatusReasonNotFound,
110		Details: &metav1.StatusDetails{
111			Group: qualifiedResource.Group,
112			Kind:  qualifiedResource.Resource,
113			Name:  name,
114		},
115		Message: fmt.Sprintf("%s %q not found", qualifiedResource.String(), name),
116	}}
117}
118
119// NewAlreadyExists returns an error indicating the item requested exists by that identifier.
120func NewAlreadyExists(qualifiedResource schema.GroupResource, name string) *StatusError {
121	return &StatusError{metav1.Status{
122		Status: metav1.StatusFailure,
123		Code:   http.StatusConflict,
124		Reason: metav1.StatusReasonAlreadyExists,
125		Details: &metav1.StatusDetails{
126			Group: qualifiedResource.Group,
127			Kind:  qualifiedResource.Resource,
128			Name:  name,
129		},
130		Message: fmt.Sprintf("%s %q already exists", qualifiedResource.String(), name),
131	}}
132}
133
134// NewUnauthorized returns an error indicating the client is not authorized to perform the requested
135// action.
136func NewUnauthorized(reason string) *StatusError {
137	message := reason
138	if len(message) == 0 {
139		message = "not authorized"
140	}
141	return &StatusError{metav1.Status{
142		Status:  metav1.StatusFailure,
143		Code:    http.StatusUnauthorized,
144		Reason:  metav1.StatusReasonUnauthorized,
145		Message: message,
146	}}
147}
148
149// NewForbidden returns an error indicating the requested action was forbidden
150func NewForbidden(qualifiedResource schema.GroupResource, name string, err error) *StatusError {
151	var message string
152	if qualifiedResource.Empty() {
153		message = fmt.Sprintf("forbidden: %v", err)
154	} else if name == "" {
155		message = fmt.Sprintf("%s is forbidden: %v", qualifiedResource.String(), err)
156	} else {
157		message = fmt.Sprintf("%s %q is forbidden: %v", qualifiedResource.String(), name, err)
158	}
159	return &StatusError{metav1.Status{
160		Status: metav1.StatusFailure,
161		Code:   http.StatusForbidden,
162		Reason: metav1.StatusReasonForbidden,
163		Details: &metav1.StatusDetails{
164			Group: qualifiedResource.Group,
165			Kind:  qualifiedResource.Resource,
166			Name:  name,
167		},
168		Message: message,
169	}}
170}
171
172// NewConflict returns an error indicating the item can't be updated as provided.
173func NewConflict(qualifiedResource schema.GroupResource, name string, err error) *StatusError {
174	return &StatusError{metav1.Status{
175		Status: metav1.StatusFailure,
176		Code:   http.StatusConflict,
177		Reason: metav1.StatusReasonConflict,
178		Details: &metav1.StatusDetails{
179			Group: qualifiedResource.Group,
180			Kind:  qualifiedResource.Resource,
181			Name:  name,
182		},
183		Message: fmt.Sprintf("Operation cannot be fulfilled on %s %q: %v", qualifiedResource.String(), name, err),
184	}}
185}
186
187// NewApplyConflict returns an error including details on the requests apply conflicts
188func NewApplyConflict(causes []metav1.StatusCause, message string) *StatusError {
189	return &StatusError{ErrStatus: metav1.Status{
190		Status: metav1.StatusFailure,
191		Code:   http.StatusConflict,
192		Reason: metav1.StatusReasonConflict,
193		Details: &metav1.StatusDetails{
194			// TODO: Get obj details here?
195			Causes: causes,
196		},
197		Message: message,
198	}}
199}
200
201// NewGone returns an error indicating the item no longer available at the server and no forwarding address is known.
202func NewGone(message string) *StatusError {
203	return &StatusError{metav1.Status{
204		Status:  metav1.StatusFailure,
205		Code:    http.StatusGone,
206		Reason:  metav1.StatusReasonGone,
207		Message: message,
208	}}
209}
210
211// NewResourceExpired creates an error that indicates that the requested resource content has expired from
212// the server (usually due to a resourceVersion that is too old).
213func NewResourceExpired(message string) *StatusError {
214	return &StatusError{metav1.Status{
215		Status:  metav1.StatusFailure,
216		Code:    http.StatusGone,
217		Reason:  metav1.StatusReasonExpired,
218		Message: message,
219	}}
220}
221
222// NewInvalid returns an error indicating the item is invalid and cannot be processed.
223func NewInvalid(qualifiedKind schema.GroupKind, name string, errs field.ErrorList) *StatusError {
224	causes := make([]metav1.StatusCause, 0, len(errs))
225	for i := range errs {
226		err := errs[i]
227		causes = append(causes, metav1.StatusCause{
228			Type:    metav1.CauseType(err.Type),
229			Message: err.ErrorBody(),
230			Field:   err.Field,
231		})
232	}
233	return &StatusError{metav1.Status{
234		Status: metav1.StatusFailure,
235		Code:   http.StatusUnprocessableEntity,
236		Reason: metav1.StatusReasonInvalid,
237		Details: &metav1.StatusDetails{
238			Group:  qualifiedKind.Group,
239			Kind:   qualifiedKind.Kind,
240			Name:   name,
241			Causes: causes,
242		},
243		Message: fmt.Sprintf("%s %q is invalid: %v", qualifiedKind.String(), name, errs.ToAggregate()),
244	}}
245}
246
247// NewBadRequest creates an error that indicates that the request is invalid and can not be processed.
248func NewBadRequest(reason string) *StatusError {
249	return &StatusError{metav1.Status{
250		Status:  metav1.StatusFailure,
251		Code:    http.StatusBadRequest,
252		Reason:  metav1.StatusReasonBadRequest,
253		Message: reason,
254	}}
255}
256
257// NewTooManyRequests creates an error that indicates that the client must try again later because
258// the specified endpoint is not accepting requests. More specific details should be provided
259// if client should know why the failure was limited4.
260func NewTooManyRequests(message string, retryAfterSeconds int) *StatusError {
261	return &StatusError{metav1.Status{
262		Status:  metav1.StatusFailure,
263		Code:    http.StatusTooManyRequests,
264		Reason:  metav1.StatusReasonTooManyRequests,
265		Message: message,
266		Details: &metav1.StatusDetails{
267			RetryAfterSeconds: int32(retryAfterSeconds),
268		},
269	}}
270}
271
272// NewServiceUnavailable creates an error that indicates that the requested service is unavailable.
273func NewServiceUnavailable(reason string) *StatusError {
274	return &StatusError{metav1.Status{
275		Status:  metav1.StatusFailure,
276		Code:    http.StatusServiceUnavailable,
277		Reason:  metav1.StatusReasonServiceUnavailable,
278		Message: reason,
279	}}
280}
281
282// NewMethodNotSupported returns an error indicating the requested action is not supported on this kind.
283func NewMethodNotSupported(qualifiedResource schema.GroupResource, action string) *StatusError {
284	return &StatusError{metav1.Status{
285		Status: metav1.StatusFailure,
286		Code:   http.StatusMethodNotAllowed,
287		Reason: metav1.StatusReasonMethodNotAllowed,
288		Details: &metav1.StatusDetails{
289			Group: qualifiedResource.Group,
290			Kind:  qualifiedResource.Resource,
291		},
292		Message: fmt.Sprintf("%s is not supported on resources of kind %q", action, qualifiedResource.String()),
293	}}
294}
295
296// NewServerTimeout returns an error indicating the requested action could not be completed due to a
297// transient error, and the client should try again.
298func NewServerTimeout(qualifiedResource schema.GroupResource, operation string, retryAfterSeconds int) *StatusError {
299	return &StatusError{metav1.Status{
300		Status: metav1.StatusFailure,
301		Code:   http.StatusInternalServerError,
302		Reason: metav1.StatusReasonServerTimeout,
303		Details: &metav1.StatusDetails{
304			Group:             qualifiedResource.Group,
305			Kind:              qualifiedResource.Resource,
306			Name:              operation,
307			RetryAfterSeconds: int32(retryAfterSeconds),
308		},
309		Message: fmt.Sprintf("The %s operation against %s could not be completed at this time, please try again.", operation, qualifiedResource.String()),
310	}}
311}
312
313// NewServerTimeoutForKind should not exist.  Server timeouts happen when accessing resources, the Kind is just what we
314// happened to be looking at when the request failed.  This delegates to keep code sane, but we should work towards removing this.
315func NewServerTimeoutForKind(qualifiedKind schema.GroupKind, operation string, retryAfterSeconds int) *StatusError {
316	return NewServerTimeout(schema.GroupResource{Group: qualifiedKind.Group, Resource: qualifiedKind.Kind}, operation, retryAfterSeconds)
317}
318
319// NewInternalError returns an error indicating the item is invalid and cannot be processed.
320func NewInternalError(err error) *StatusError {
321	return &StatusError{metav1.Status{
322		Status: metav1.StatusFailure,
323		Code:   http.StatusInternalServerError,
324		Reason: metav1.StatusReasonInternalError,
325		Details: &metav1.StatusDetails{
326			Causes: []metav1.StatusCause{{Message: err.Error()}},
327		},
328		Message: fmt.Sprintf("Internal error occurred: %v", err),
329	}}
330}
331
332// NewTimeoutError returns an error indicating that a timeout occurred before the request
333// could be completed.  Clients may retry, but the operation may still complete.
334func NewTimeoutError(message string, retryAfterSeconds int) *StatusError {
335	return &StatusError{metav1.Status{
336		Status:  metav1.StatusFailure,
337		Code:    http.StatusGatewayTimeout,
338		Reason:  metav1.StatusReasonTimeout,
339		Message: fmt.Sprintf("Timeout: %s", message),
340		Details: &metav1.StatusDetails{
341			RetryAfterSeconds: int32(retryAfterSeconds),
342		},
343	}}
344}
345
346// NewTooManyRequestsError returns an error indicating that the request was rejected because
347// the server has received too many requests. Client should wait and retry. But if the request
348// is perishable, then the client should not retry the request.
349func NewTooManyRequestsError(message string) *StatusError {
350	return &StatusError{metav1.Status{
351		Status:  metav1.StatusFailure,
352		Code:    StatusTooManyRequests,
353		Reason:  metav1.StatusReasonTooManyRequests,
354		Message: fmt.Sprintf("Too many requests: %s", message),
355	}}
356}
357
358// NewRequestEntityTooLargeError returns an error indicating that the request
359// entity was too large.
360func NewRequestEntityTooLargeError(message string) *StatusError {
361	return &StatusError{metav1.Status{
362		Status:  metav1.StatusFailure,
363		Code:    http.StatusRequestEntityTooLarge,
364		Reason:  metav1.StatusReasonRequestEntityTooLarge,
365		Message: fmt.Sprintf("Request entity too large: %s", message),
366	}}
367}
368
369// NewGenericServerResponse returns a new error for server responses that are not in a recognizable form.
370func NewGenericServerResponse(code int, verb string, qualifiedResource schema.GroupResource, name, serverMessage string, retryAfterSeconds int, isUnexpectedResponse bool) *StatusError {
371	reason := metav1.StatusReasonUnknown
372	message := fmt.Sprintf("the server responded with the status code %d but did not return more information", code)
373	switch code {
374	case http.StatusConflict:
375		if verb == "POST" {
376			reason = metav1.StatusReasonAlreadyExists
377		} else {
378			reason = metav1.StatusReasonConflict
379		}
380		message = "the server reported a conflict"
381	case http.StatusNotFound:
382		reason = metav1.StatusReasonNotFound
383		message = "the server could not find the requested resource"
384	case http.StatusBadRequest:
385		reason = metav1.StatusReasonBadRequest
386		message = "the server rejected our request for an unknown reason"
387	case http.StatusUnauthorized:
388		reason = metav1.StatusReasonUnauthorized
389		message = "the server has asked for the client to provide credentials"
390	case http.StatusForbidden:
391		reason = metav1.StatusReasonForbidden
392		// the server message has details about who is trying to perform what action.  Keep its message.
393		message = serverMessage
394	case http.StatusNotAcceptable:
395		reason = metav1.StatusReasonNotAcceptable
396		// the server message has details about what types are acceptable
397		message = serverMessage
398	case http.StatusUnsupportedMediaType:
399		reason = metav1.StatusReasonUnsupportedMediaType
400		// the server message has details about what types are acceptable
401		message = serverMessage
402	case http.StatusMethodNotAllowed:
403		reason = metav1.StatusReasonMethodNotAllowed
404		message = "the server does not allow this method on the requested resource"
405	case http.StatusUnprocessableEntity:
406		reason = metav1.StatusReasonInvalid
407		message = "the server rejected our request due to an error in our request"
408	case http.StatusServiceUnavailable:
409		reason = metav1.StatusReasonServiceUnavailable
410		message = "the server is currently unable to handle the request"
411	case http.StatusGatewayTimeout:
412		reason = metav1.StatusReasonTimeout
413		message = "the server was unable to return a response in the time allotted, but may still be processing the request"
414	case http.StatusTooManyRequests:
415		reason = metav1.StatusReasonTooManyRequests
416		message = "the server has received too many requests and has asked us to try again later"
417	default:
418		if code >= 500 {
419			reason = metav1.StatusReasonInternalError
420			message = fmt.Sprintf("an error on the server (%q) has prevented the request from succeeding", serverMessage)
421		}
422	}
423	switch {
424	case !qualifiedResource.Empty() && len(name) > 0:
425		message = fmt.Sprintf("%s (%s %s %s)", message, strings.ToLower(verb), qualifiedResource.String(), name)
426	case !qualifiedResource.Empty():
427		message = fmt.Sprintf("%s (%s %s)", message, strings.ToLower(verb), qualifiedResource.String())
428	}
429	var causes []metav1.StatusCause
430	if isUnexpectedResponse {
431		causes = []metav1.StatusCause{
432			{
433				Type:    metav1.CauseTypeUnexpectedServerResponse,
434				Message: serverMessage,
435			},
436		}
437	} else {
438		causes = nil
439	}
440	return &StatusError{metav1.Status{
441		Status: metav1.StatusFailure,
442		Code:   int32(code),
443		Reason: reason,
444		Details: &metav1.StatusDetails{
445			Group: qualifiedResource.Group,
446			Kind:  qualifiedResource.Resource,
447			Name:  name,
448
449			Causes:            causes,
450			RetryAfterSeconds: int32(retryAfterSeconds),
451		},
452		Message: message,
453	}}
454}
455
456// IsNotFound returns true if the specified error was created by NewNotFound.
457func IsNotFound(err error) bool {
458	return ReasonForError(err) == metav1.StatusReasonNotFound
459}
460
461// IsAlreadyExists determines if the err is an error which indicates that a specified resource already exists.
462func IsAlreadyExists(err error) bool {
463	return ReasonForError(err) == metav1.StatusReasonAlreadyExists
464}
465
466// IsConflict determines if the err is an error which indicates the provided update conflicts.
467func IsConflict(err error) bool {
468	return ReasonForError(err) == metav1.StatusReasonConflict
469}
470
471// IsInvalid determines if the err is an error which indicates the provided resource is not valid.
472func IsInvalid(err error) bool {
473	return ReasonForError(err) == metav1.StatusReasonInvalid
474}
475
476// IsGone is true if the error indicates the requested resource is no longer available.
477func IsGone(err error) bool {
478	return ReasonForError(err) == metav1.StatusReasonGone
479}
480
481// IsResourceExpired is true if the error indicates the resource has expired and the current action is
482// no longer possible.
483func IsResourceExpired(err error) bool {
484	return ReasonForError(err) == metav1.StatusReasonExpired
485}
486
487// IsNotAcceptable determines if err is an error which indicates that the request failed due to an invalid Accept header
488func IsNotAcceptable(err error) bool {
489	return ReasonForError(err) == metav1.StatusReasonNotAcceptable
490}
491
492// IsUnsupportedMediaType determines if err is an error which indicates that the request failed due to an invalid Content-Type header
493func IsUnsupportedMediaType(err error) bool {
494	return ReasonForError(err) == metav1.StatusReasonUnsupportedMediaType
495}
496
497// IsMethodNotSupported determines if the err is an error which indicates the provided action could not
498// be performed because it is not supported by the server.
499func IsMethodNotSupported(err error) bool {
500	return ReasonForError(err) == metav1.StatusReasonMethodNotAllowed
501}
502
503// IsServiceUnavailable is true if the error indicates the underlying service is no longer available.
504func IsServiceUnavailable(err error) bool {
505	return ReasonForError(err) == metav1.StatusReasonServiceUnavailable
506}
507
508// IsBadRequest determines if err is an error which indicates that the request is invalid.
509func IsBadRequest(err error) bool {
510	return ReasonForError(err) == metav1.StatusReasonBadRequest
511}
512
513// IsUnauthorized determines if err is an error which indicates that the request is unauthorized and
514// requires authentication by the user.
515func IsUnauthorized(err error) bool {
516	return ReasonForError(err) == metav1.StatusReasonUnauthorized
517}
518
519// IsForbidden determines if err is an error which indicates that the request is forbidden and cannot
520// be completed as requested.
521func IsForbidden(err error) bool {
522	return ReasonForError(err) == metav1.StatusReasonForbidden
523}
524
525// IsTimeout determines if err is an error which indicates that request times out due to long
526// processing.
527func IsTimeout(err error) bool {
528	return ReasonForError(err) == metav1.StatusReasonTimeout
529}
530
531// IsServerTimeout determines if err is an error which indicates that the request needs to be retried
532// by the client.
533func IsServerTimeout(err error) bool {
534	return ReasonForError(err) == metav1.StatusReasonServerTimeout
535}
536
537// IsInternalError determines if err is an error which indicates an internal server error.
538func IsInternalError(err error) bool {
539	return ReasonForError(err) == metav1.StatusReasonInternalError
540}
541
542// IsTooManyRequests determines if err is an error which indicates that there are too many requests
543// that the server cannot handle.
544func IsTooManyRequests(err error) bool {
545	if ReasonForError(err) == metav1.StatusReasonTooManyRequests {
546		return true
547	}
548	switch t := err.(type) {
549	case APIStatus:
550		return t.Status().Code == http.StatusTooManyRequests
551	}
552	return false
553}
554
555// IsRequestEntityTooLargeError determines if err is an error which indicates
556// the request entity is too large.
557func IsRequestEntityTooLargeError(err error) bool {
558	if ReasonForError(err) == metav1.StatusReasonRequestEntityTooLarge {
559		return true
560	}
561	switch t := err.(type) {
562	case APIStatus:
563		return t.Status().Code == http.StatusRequestEntityTooLarge
564	}
565	return false
566}
567
568// IsUnexpectedServerError returns true if the server response was not in the expected API format,
569// and may be the result of another HTTP actor.
570func IsUnexpectedServerError(err error) bool {
571	switch t := err.(type) {
572	case APIStatus:
573		if d := t.Status().Details; d != nil {
574			for _, cause := range d.Causes {
575				if cause.Type == metav1.CauseTypeUnexpectedServerResponse {
576					return true
577				}
578			}
579		}
580	}
581	return false
582}
583
584// IsUnexpectedObjectError determines if err is due to an unexpected object from the master.
585func IsUnexpectedObjectError(err error) bool {
586	_, ok := err.(*UnexpectedObjectError)
587	return err != nil && ok
588}
589
590// SuggestsClientDelay returns true if this error suggests a client delay as well as the
591// suggested seconds to wait, or false if the error does not imply a wait. It does not
592// address whether the error *should* be retried, since some errors (like a 3xx) may
593// request delay without retry.
594func SuggestsClientDelay(err error) (int, bool) {
595	switch t := err.(type) {
596	case APIStatus:
597		if t.Status().Details != nil {
598			switch t.Status().Reason {
599			// this StatusReason explicitly requests the caller to delay the action
600			case metav1.StatusReasonServerTimeout:
601				return int(t.Status().Details.RetryAfterSeconds), true
602			}
603			// If the client requests that we retry after a certain number of seconds
604			if t.Status().Details.RetryAfterSeconds > 0 {
605				return int(t.Status().Details.RetryAfterSeconds), true
606			}
607		}
608	}
609	return 0, false
610}
611
612// ReasonForError returns the HTTP status for a particular error.
613func ReasonForError(err error) metav1.StatusReason {
614	switch t := err.(type) {
615	case APIStatus:
616		return t.Status().Reason
617	}
618	return metav1.StatusReasonUnknown
619}
620