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