diff --git a/pkg/api/types.go b/pkg/api/types.go index 1d290b4d42c..f283b351dc6 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -362,9 +362,9 @@ type Status struct { JSONBase `json:",inline" yaml:",inline"` // One of: "success", "failure", "working" (for operations not yet completed) Status string `json:"status,omitempty" yaml:"status,omitempty"` - // A human readable description of the status of this operation. + // A human-readable description of the status of this operation. Message string `json:"message,omitempty" yaml:"message,omitempty"` - // A machine readable description of why this operation is in the + // A machine-readable description of why this operation is in the // "failure" or "working" status. If this value is empty there // is no information available. A Reason clarifies an HTTP status // code but does not override it. @@ -391,6 +391,9 @@ type StatusDetails struct { // The kind attribute of the resource associated with the status ReasonType. // On some operations may differ from the requested resource Kind. Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + // The Causes array includes more details associated with the ReasonType + // failure. Not all ReasonTypes may provide detailed causes. + Causes []StatusCause `json:"causes,omitempty" yaml:"causes,omitempty"` } // Values of Status.Status @@ -447,6 +450,62 @@ const ( // conflict. // Status code 409 ReasonTypeConflict ReasonType = "conflict" + + // ResourceTypeInvalid means the requested create or update operation cannot be + // completed due to invalid data provided as part of the request. The client may + // need to alter the request. When set, the client may use the StatusDetails + // message field as a summary of the issues encountered. + // Details (optional): + // "kind" string - the kind attribute of the invalid resource + // "id" string - the identifier of the invalid resource + // "causes" - one or more StatusCause entries indicating the data in the + // provided resource that was invalid. The code, message, and + // field attributes will be set. + // Status code 422 + ReasonTypeInvalid ReasonType = "invalid" +) + +// StatusCause provides more information about an api.Status failure, including +// cases when multiple errors are encountered. +type StatusCause struct { + // A machine-readable description of the cause of the error. If this value is + // empty there is no information available. + Reason CauseReasonType `json:"reason,omitempty" yaml:"reason,omitempty"` + // A human-readable description of the cause of the error. This field may be + // presented as-is to a reader. + Message string `json:"message,omitempty" yaml:"message,omitempty"` + // The field of the resource that has caused this error, as named by its JSON + // serialization. May include dot and postfix notation for nested attributes. + // Arrays are zero-indexed. Fields may appear more than once in an array of + // causes due to fields having multiple errors. + // Optional. + // + // Examples: + // "name" - the field "name" on the current resource + // "items[0].name" - the field "name" on the first array entry in "items" + Field string `json:"field,omitempty" yaml:"field,omitempty"` +} + +// CauseReasonType is a machine readable value providing more detail about why +// an operation failed. An operation may have multiple causes for a failure. +type CauseReasonType string + +const ( + // CauseReasonTypeFieldValueNotFound is used to report failure to find a requested value + // (e.g. looking up an ID). + CauseReasonTypeFieldValueNotFound CauseReasonType = "fieldValueNotFound" + // CauseReasonTypeFieldValueInvalid is used to report required values that are not + // provided (e.g. empty strings, null values, or empty arrays). + CauseReasonTypeFieldValueRequired CauseReasonType = "fieldValueRequired" + // CauseReasonTypeFieldValueDuplicate is used to report collisions of values that must be + // unique (e.g. unique IDs). + CauseReasonTypeFieldValueDuplicate CauseReasonType = "fieldValueDuplicate" + // CauseReasonTypeFieldValueInvalid is used to report malformed values (e.g. failed regex + // match). + CauseReasonTypeFieldValueInvalid CauseReasonType = "fieldValueInvalid" + // CauseReasonTypeFieldValueNotSupported is used to report valid (as per formatting rules) + // values that can not be handled (e.g. an enumerated string). + CauseReasonTypeFieldValueNotSupported CauseReasonType = "fieldValueNotSupported" ) // ServerOp is an operation delivered to API clients. diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index a076b5986b7..c0969003421 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -395,6 +395,9 @@ type StatusDetails struct { // The kind attribute of the resource associated with the status ReasonType. // On some operations may differ from the requested resource Kind. Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + // The Causes array includes more details associated with the ReasonType + // failure. Not all ReasonTypes may provide detailed causes. + Causes []StatusCause `json:"causes,omitempty" yaml:"causes,omitempty"` } // Values of Status.Status @@ -453,6 +456,49 @@ const ( ReasonTypeConflict ReasonType = "conflict" ) +// StatusCause provides more information about an api.Status failure, including +// cases when multiple errors are encountered. +type StatusCause struct { + // A machine-readable description of the cause of the error. If this value is + // empty there is no information available. + Reason CauseReasonType `json:"reason,omitempty" yaml:"reason,omitempty"` + // A human-readable description of the cause of the error. This field may be + // presented as-is to a reader. + Message string `json:"message,omitempty" yaml:"message,omitempty"` + // The field of the resource that has caused this error, as named by its JSON + // serialization. May include dot and postfix notation for nested attributes. + // Arrays are zero-indexed. Fields may appear more than once in an array of + // causes due to fields having multiple errors. + // Optional. + // + // Examples: + // "name" - the field "name" on the current resource + // "items[0].name" - the field "name" on the first array entry in "items" + Field string `json:"field,omitempty" yaml:"field,omitempty"` +} + +// CauseReasonType is a machine readable value providing more detail about why +// an operation failed. An operation may have multiple causes for a failure. +type CauseReasonType string + +const ( + // CauseReasonTypeFieldValueNotFound is used to report failure to find a requested value + // (e.g. looking up an ID). + CauseReasonTypeFieldValueNotFound CauseReasonType = "fieldValueNotFound" + // CauseReasonTypeFieldValueInvalid is used to report required values that are not + // provided (e.g. empty strings, null values, or empty arrays). + CauseReasonTypeFieldValueRequired CauseReasonType = "fieldValueRequired" + // CauseReasonTypeFieldValueDuplicate is used to report collisions of values that must be + // unique (e.g. unique IDs). + CauseReasonTypeFieldValueDuplicate CauseReasonType = "fieldValueDuplicate" + // CauseReasonTypeFieldValueInvalid is used to report malformed values (e.g. failed regex + // match). + CauseReasonTypeFieldValueInvalid CauseReasonType = "fieldValueInvalid" + // CauseReasonTypeFieldValueNotSupported is used to report valid (as per formatting rules) + // values that can not be handled (e.g. an enumerated string). + CauseReasonTypeFieldValueNotSupported CauseReasonType = "fieldValueNotSupported" +) + // ServerOp is an operation delivered to API clients. type ServerOp struct { JSONBase `yaml:",inline" json:",inline"` diff --git a/pkg/apiserver/errors.go b/pkg/apiserver/errors.go index 1a20b793fea..20e0a503f49 100644 --- a/pkg/apiserver/errors.go +++ b/pkg/apiserver/errors.go @@ -21,6 +21,7 @@ import ( "net/http" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" ) @@ -76,6 +77,21 @@ func NewConflictErr(kind, name string, err error) error { }} } +// NewInvalidError returns an error indicating the item is invalid and cannot be processed. +func NewInvalidError(kind, name string, errs errors.ErrorList) error { + return &apiServerError{api.Status{ + Status: api.StatusFailure, + Code: 422, // RFC 4918 + Reason: api.ReasonTypeInvalid, + Details: &api.StatusDetails{ + Kind: kind, + ID: name, + // TODO: causes + }, + Message: fmt.Sprintf("%s %q is invalid: %s", kind, name, errs.ToError()), + }} +} + // IsNotFound returns true if the specified error was created by NewNotFoundErr func IsNotFound(err error) bool { return reasonForError(err) == api.ReasonTypeNotFound @@ -91,6 +107,11 @@ func IsConflict(err error) bool { return reasonForError(err) == api.ReasonTypeConflict } +// IsInvalid determines if the err is an error which indicates the provided resource is not valid +func IsInvalid(err error) bool { + return reasonForError(err) == api.ReasonTypeInvalid +} + func reasonForError(err error) api.ReasonType { switch t := err.(type) { case *apiServerError: @@ -110,7 +131,7 @@ func errToAPIStatus(err error) *api.Status { default: status := http.StatusInternalServerError switch { - //TODO: replace me with NewUpdateConflictErr + //TODO: replace me with NewConflictErr case tools.IsEtcdTestFailed(err): status = http.StatusConflict } diff --git a/pkg/apiserver/errors_test.go b/pkg/apiserver/errors_test.go index 295560ffc4f..a53161933e2 100644 --- a/pkg/apiserver/errors_test.go +++ b/pkg/apiserver/errors_test.go @@ -35,11 +35,31 @@ func TestErrorNew(t *testing.T) { if IsNotFound(err) { t.Errorf(fmt.Sprintf("expected to not be %s", api.ReasonTypeNotFound)) } + if IsInvalid(err) { + t.Errorf("expected to not be invalid") + } if !IsConflict(NewConflictErr("test", "2", errors.New("message"))) { - t.Errorf("expected to be confict") + t.Errorf("expected to be conflict") } if !IsNotFound(NewNotFoundErr("test", "3")) { t.Errorf("expected to be not found") } + if !IsInvalid(NewInvalidError("test", "2", nil)) { + t.Errorf("expected to be invalid") + } +} + +func Test_errToAPIStatus(t *testing.T) { + err := &apiServerError{} + status := errToAPIStatus(err) + if status.Reason != api.ReasonTypeUnknown || status.Status != api.StatusFailure { + t.Errorf("unexpected status object: %#v", status) + } +} + +func Test_reasonForError(t *testing.T) { + if e, a := api.ReasonTypeUnknown, reasonForError(nil); e != a { + t.Errorf("unexpected reason type: %#v", a) + } }