update StatusDetails to handle Groups

This commit is contained in:
deads2k
2015-12-10 13:32:29 -05:00
parent 5c4479f542
commit 9fda7f1812
75 changed files with 303 additions and 238 deletions

View File

@@ -93,30 +93,32 @@ func FromObject(obj runtime.Object) error {
}
// NewNotFound returns a new error which indicates that the resource of the kind and the name was not found.
func NewNotFound(kind, name string) error {
func NewNotFound(qualifiedResource unversioned.GroupResource, name string) error {
return &StatusError{unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusNotFound,
Reason: unversioned.StatusReasonNotFound,
Details: &unversioned.StatusDetails{
Kind: kind,
Name: name,
Group: qualifiedResource.Group,
Kind: qualifiedResource.Resource,
Name: name,
},
Message: fmt.Sprintf("%s %q not found", kind, name),
Message: fmt.Sprintf("%s %q not found", qualifiedResource.String(), name),
}}
}
// NewAlreadyExists returns an error indicating the item requested exists by that identifier.
func NewAlreadyExists(kind, name string) error {
func NewAlreadyExists(qualifiedResource unversioned.GroupResource, name string) error {
return &StatusError{unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusConflict,
Reason: unversioned.StatusReasonAlreadyExists,
Details: &unversioned.StatusDetails{
Kind: kind,
Name: name,
Group: qualifiedResource.Group,
Kind: qualifiedResource.Resource,
Name: name,
},
Message: fmt.Sprintf("%s %q already exists", kind, name),
Message: fmt.Sprintf("%s %q already exists", qualifiedResource.String(), name),
}}
}
@@ -136,30 +138,32 @@ func NewUnauthorized(reason string) error {
}
// NewForbidden returns an error indicating the requested action was forbidden
func NewForbidden(kind, name string, err error) error {
func NewForbidden(qualifiedResource unversioned.GroupResource, name string, err error) error {
return &StatusError{unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusForbidden,
Reason: unversioned.StatusReasonForbidden,
Details: &unversioned.StatusDetails{
Kind: kind,
Name: name,
Group: qualifiedResource.Group,
Kind: qualifiedResource.Resource,
Name: name,
},
Message: fmt.Sprintf("%s %q is forbidden: %v", kind, name, err),
Message: fmt.Sprintf("%s %q is forbidden: %v", qualifiedResource.String(), name, err),
}}
}
// NewConflict returns an error indicating the item can't be updated as provided.
func NewConflict(kind, name string, err error) error {
func NewConflict(qualifiedResource unversioned.GroupResource, name string, err error) error {
return &StatusError{unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusConflict,
Reason: unversioned.StatusReasonConflict,
Details: &unversioned.StatusDetails{
Kind: kind,
Name: name,
Group: qualifiedResource.Group,
Kind: qualifiedResource.Resource,
Name: name,
},
Message: fmt.Sprintf("%s %q cannot be updated: %v", kind, name, err),
Message: fmt.Sprintf("%s %q cannot be updated: %v", qualifiedResource.String(), name, err),
}}
}
@@ -174,7 +178,7 @@ func NewGone(message string) error {
}
// NewInvalid returns an error indicating the item is invalid and cannot be processed.
func NewInvalid(kind, name string, errs field.ErrorList) error {
func NewInvalid(qualifiedKind unversioned.GroupKind, name string, errs field.ErrorList) error {
causes := make([]unversioned.StatusCause, 0, len(errs))
for i := range errs {
err := errs[i]
@@ -189,11 +193,12 @@ func NewInvalid(kind, name string, errs field.ErrorList) error {
Code: StatusUnprocessableEntity, // RFC 4918: StatusUnprocessableEntity
Reason: unversioned.StatusReasonInvalid,
Details: &unversioned.StatusDetails{
Kind: kind,
Group: qualifiedKind.Group,
Kind: qualifiedKind.Kind,
Name: name,
Causes: causes,
},
Message: fmt.Sprintf("%s %q is invalid: %v", kind, name, errs.ToAggregate()),
Message: fmt.Sprintf("%s %q is invalid: %v", qualifiedKind.String(), name, errs.ToAggregate()),
}}
}
@@ -218,34 +223,42 @@ func NewServiceUnavailable(reason string) error {
}
// NewMethodNotSupported returns an error indicating the requested action is not supported on this kind.
func NewMethodNotSupported(kind, action string) error {
func NewMethodNotSupported(qualifiedResource unversioned.GroupResource, action string) error {
return &StatusError{unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusMethodNotAllowed,
Reason: unversioned.StatusReasonMethodNotAllowed,
Details: &unversioned.StatusDetails{
Kind: kind,
Group: qualifiedResource.Group,
Kind: qualifiedResource.Resource,
},
Message: fmt.Sprintf("%s is not supported on resources of kind %q", action, kind),
Message: fmt.Sprintf("%s is not supported on resources of kind %q", action, qualifiedResource.String()),
}}
}
// NewServerTimeout returns an error indicating the requested action could not be completed due to a
// transient error, and the client should try again.
func NewServerTimeout(kind, operation string, retryAfterSeconds int) error {
func NewServerTimeout(qualifiedResource unversioned.GroupResource, operation string, retryAfterSeconds int) error {
return &StatusError{unversioned.Status{
Status: unversioned.StatusFailure,
Code: http.StatusInternalServerError,
Reason: unversioned.StatusReasonServerTimeout,
Details: &unversioned.StatusDetails{
Kind: kind,
Group: qualifiedResource.Group,
Kind: qualifiedResource.Resource,
Name: operation,
RetryAfterSeconds: int32(retryAfterSeconds),
},
Message: fmt.Sprintf("The %s operation against %s could not be completed at this time, please try again.", operation, kind),
Message: fmt.Sprintf("The %s operation against %s could not be completed at this time, please try again.", operation, qualifiedResource.String()),
}}
}
// NewServerTimeoutForKind should not exist. Server timeouts happen when accessing resources, the Kind is just what we
// happened to be looking at when the request failed. This delegates to keep code sane, but we should work towards removing this.
func NewServerTimeoutForKind(qualifiedKind unversioned.GroupKind, operation string, retryAfterSeconds int) error {
return NewServerTimeout(unversioned.GroupResource{Group: qualifiedKind.Group, Resource: qualifiedKind.Kind}, operation, retryAfterSeconds)
}
// NewInternalError returns an error indicating the item is invalid and cannot be processed.
func NewInternalError(err error) error {
return &StatusError{unversioned.Status{
@@ -274,7 +287,7 @@ func NewTimeoutError(message string, retryAfterSeconds int) error {
}
// NewGenericServerResponse returns a new error for server responses that are not in a recognizable form.
func NewGenericServerResponse(code int, verb, kind, name, serverMessage string, retryAfterSeconds int, isUnexpectedResponse bool) error {
func NewGenericServerResponse(code int, verb string, qualifiedResource unversioned.GroupResource, name, serverMessage string, retryAfterSeconds int, isUnexpectedResponse bool) error {
reason := unversioned.StatusReasonUnknown
message := fmt.Sprintf("the server responded with the status code %d but did not return more information", code)
switch code {
@@ -316,10 +329,10 @@ func NewGenericServerResponse(code int, verb, kind, name, serverMessage string,
}
}
switch {
case len(kind) > 0 && len(name) > 0:
message = fmt.Sprintf("%s (%s %s %s)", message, strings.ToLower(verb), kind, name)
case len(kind) > 0:
message = fmt.Sprintf("%s (%s %s)", message, strings.ToLower(verb), kind)
case !qualifiedResource.IsEmpty() && len(name) > 0:
message = fmt.Sprintf("%s (%s %s %s)", message, strings.ToLower(verb), qualifiedResource.String(), name)
case !qualifiedResource.IsEmpty():
message = fmt.Sprintf("%s (%s %s)", message, strings.ToLower(verb), qualifiedResource.String())
}
var causes []unversioned.StatusCause
if isUnexpectedResponse {
@@ -337,8 +350,9 @@ func NewGenericServerResponse(code int, verb, kind, name, serverMessage string,
Code: int32(code),
Reason: reason,
Details: &unversioned.StatusDetails{
Kind: kind,
Name: name,
Group: qualifiedResource.Group,
Kind: qualifiedResource.Resource,
Name: name,
Causes: causes,
RetryAfterSeconds: int32(retryAfterSeconds),

View File

@@ -22,13 +22,14 @@ import (
"reflect"
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/validation/field"
)
func TestErrorNew(t *testing.T) {
err := NewAlreadyExists("test", "1")
err := NewAlreadyExists(api.Resource("tests"), "1")
if !IsAlreadyExists(err) {
t.Errorf("expected to be %s", unversioned.StatusReasonAlreadyExists)
}
@@ -54,34 +55,34 @@ func TestErrorNew(t *testing.T) {
t.Errorf("expected to not be %s", unversioned.StatusReasonMethodNotAllowed)
}
if !IsConflict(NewConflict("test", "2", errors.New("message"))) {
if !IsConflict(NewConflict(api.Resource("tests"), "2", errors.New("message"))) {
t.Errorf("expected to be conflict")
}
if !IsNotFound(NewNotFound("test", "3")) {
if !IsNotFound(NewNotFound(api.Resource("tests"), "3")) {
t.Errorf("expected to be %s", unversioned.StatusReasonNotFound)
}
if !IsInvalid(NewInvalid("test", "2", nil)) {
if !IsInvalid(NewInvalid(api.Kind("Test"), "2", nil)) {
t.Errorf("expected to be %s", unversioned.StatusReasonInvalid)
}
if !IsBadRequest(NewBadRequest("reason")) {
t.Errorf("expected to be %s", unversioned.StatusReasonBadRequest)
}
if !IsForbidden(NewForbidden("test", "2", errors.New("reason"))) {
if !IsForbidden(NewForbidden(api.Resource("tests"), "2", errors.New("reason"))) {
t.Errorf("expected to be %s", unversioned.StatusReasonForbidden)
}
if !IsUnauthorized(NewUnauthorized("reason")) {
t.Errorf("expected to be %s", unversioned.StatusReasonUnauthorized)
}
if !IsServerTimeout(NewServerTimeout("test", "reason", 0)) {
if !IsServerTimeout(NewServerTimeout(api.Resource("tests"), "reason", 0)) {
t.Errorf("expected to be %s", unversioned.StatusReasonServerTimeout)
}
if time, ok := SuggestsClientDelay(NewServerTimeout("test", "doing something", 10)); time != 10 || !ok {
if time, ok := SuggestsClientDelay(NewServerTimeout(api.Resource("tests"), "doing something", 10)); time != 10 || !ok {
t.Errorf("expected to be %s", unversioned.StatusReasonServerTimeout)
}
if time, ok := SuggestsClientDelay(NewTimeoutError("test reason", 10)); time != 10 || !ok {
t.Errorf("expected to be %s", unversioned.StatusReasonTimeout)
}
if !IsMethodNotSupported(NewMethodNotSupported("foo", "delete")) {
if !IsMethodNotSupported(NewMethodNotSupported(api.Resource("foos"), "delete")) {
t.Errorf("expected to be %s", unversioned.StatusReasonMethodNotAllowed)
}
}
@@ -94,7 +95,7 @@ func TestNewInvalid(t *testing.T) {
{
field.Duplicate(field.NewPath("field[0].name"), "bar"),
&unversioned.StatusDetails{
Kind: "kind",
Kind: "Kind",
Name: "name",
Causes: []unversioned.StatusCause{{
Type: unversioned.CauseTypeFieldValueDuplicate,
@@ -105,7 +106,7 @@ func TestNewInvalid(t *testing.T) {
{
field.Invalid(field.NewPath("field[0].name"), "bar", "detail"),
&unversioned.StatusDetails{
Kind: "kind",
Kind: "Kind",
Name: "name",
Causes: []unversioned.StatusCause{{
Type: unversioned.CauseTypeFieldValueInvalid,
@@ -116,7 +117,7 @@ func TestNewInvalid(t *testing.T) {
{
field.NotFound(field.NewPath("field[0].name"), "bar"),
&unversioned.StatusDetails{
Kind: "kind",
Kind: "Kind",
Name: "name",
Causes: []unversioned.StatusCause{{
Type: unversioned.CauseTypeFieldValueNotFound,
@@ -127,7 +128,7 @@ func TestNewInvalid(t *testing.T) {
{
field.NotSupported(field.NewPath("field[0].name"), "bar", nil),
&unversioned.StatusDetails{
Kind: "kind",
Kind: "Kind",
Name: "name",
Causes: []unversioned.StatusCause{{
Type: unversioned.CauseTypeFieldValueNotSupported,
@@ -138,7 +139,7 @@ func TestNewInvalid(t *testing.T) {
{
field.Required(field.NewPath("field[0].name")),
&unversioned.StatusDetails{
Kind: "kind",
Kind: "Kind",
Name: "name",
Causes: []unversioned.StatusCause{{
Type: unversioned.CauseTypeFieldValueRequired,
@@ -150,7 +151,7 @@ func TestNewInvalid(t *testing.T) {
for i, testCase := range testCases {
vErr, expected := testCase.Err, testCase.Details
expected.Causes[0].Message = vErr.ErrorBody()
err := NewInvalid("kind", "name", field.ErrorList{vErr})
err := NewInvalid(api.Kind("Kind"), "name", field.ErrorList{vErr})
status := err.(*StatusError).ErrStatus
if status.Code != 422 || status.Reason != unversioned.StatusReasonInvalid {
t.Errorf("%d: unexpected status: %#v", i, status)

View File

@@ -18,17 +18,18 @@ package etcd
import (
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/storage"
)
// InterpretListError converts a generic error on a retrieval
// operation into the appropriate API error.
func InterpretListError(err error, kind string) error {
func InterpretListError(err error, qualifiedResource unversioned.GroupResource) error {
switch {
case storage.IsNotFound(err):
return errors.NewNotFound(kind, "")
return errors.NewNotFound(qualifiedResource, "")
case storage.IsUnreachable(err):
return errors.NewServerTimeout(kind, "list", 2) // TODO: make configurable or handled at a higher level
return errors.NewServerTimeout(qualifiedResource, "list", 2) // TODO: make configurable or handled at a higher level
default:
return err
}
@@ -36,12 +37,12 @@ func InterpretListError(err error, kind string) error {
// InterpretGetError converts a generic error on a retrieval
// operation into the appropriate API error.
func InterpretGetError(err error, kind, name string) error {
func InterpretGetError(err error, qualifiedResource unversioned.GroupResource, name string) error {
switch {
case storage.IsNotFound(err):
return errors.NewNotFound(kind, name)
return errors.NewNotFound(qualifiedResource, name)
case storage.IsUnreachable(err):
return errors.NewServerTimeout(kind, "get", 2) // TODO: make configurable or handled at a higher level
return errors.NewServerTimeout(qualifiedResource, "get", 2) // TODO: make configurable or handled at a higher level
default:
return err
}
@@ -49,12 +50,12 @@ func InterpretGetError(err error, kind, name string) error {
// InterpretCreateError converts a generic error on a create
// operation into the appropriate API error.
func InterpretCreateError(err error, kind, name string) error {
func InterpretCreateError(err error, qualifiedResource unversioned.GroupResource, name string) error {
switch {
case storage.IsNodeExist(err):
return errors.NewAlreadyExists(kind, name)
return errors.NewAlreadyExists(qualifiedResource, name)
case storage.IsUnreachable(err):
return errors.NewServerTimeout(kind, "create", 2) // TODO: make configurable or handled at a higher level
return errors.NewServerTimeout(qualifiedResource, "create", 2) // TODO: make configurable or handled at a higher level
default:
return err
}
@@ -62,12 +63,12 @@ func InterpretCreateError(err error, kind, name string) error {
// InterpretUpdateError converts a generic error on a update
// operation into the appropriate API error.
func InterpretUpdateError(err error, kind, name string) error {
func InterpretUpdateError(err error, qualifiedResource unversioned.GroupResource, name string) error {
switch {
case storage.IsTestFailed(err), storage.IsNodeExist(err):
return errors.NewConflict(kind, name, err)
return errors.NewConflict(qualifiedResource, name, err)
case storage.IsUnreachable(err):
return errors.NewServerTimeout(kind, "update", 2) // TODO: make configurable or handled at a higher level
return errors.NewServerTimeout(qualifiedResource, "update", 2) // TODO: make configurable or handled at a higher level
default:
return err
}
@@ -75,12 +76,12 @@ func InterpretUpdateError(err error, kind, name string) error {
// InterpretDeleteError converts a generic error on a delete
// operation into the appropriate API error.
func InterpretDeleteError(err error, kind, name string) error {
func InterpretDeleteError(err error, qualifiedResource unversioned.GroupResource, name string) error {
switch {
case storage.IsNotFound(err):
return errors.NewNotFound(kind, name)
return errors.NewNotFound(qualifiedResource, name)
case storage.IsUnreachable(err):
return errors.NewServerTimeout(kind, "delete", 2) // TODO: make configurable or handled at a higher level
return errors.NewServerTimeout(qualifiedResource, "delete", 2) // TODO: make configurable or handled at a higher level
default:
return err
}

View File

@@ -19,6 +19,7 @@ package rest
import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/api/validation"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/util/validation/field"
@@ -71,14 +72,14 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx api.Context, obj runtime.Obje
api.GenerateName(strategy, objectMeta)
if errs := strategy.Validate(ctx, obj); len(errs) > 0 {
return errors.NewInvalid(kind, objectMeta.Name, errs)
return errors.NewInvalid(kind.GroupKind(), objectMeta.Name, errs)
}
// Custom validation (including name validation) passed
// Now run common validation on object meta
// Do this *after* custom validation so that specific error messages are shown whenever possible
if errs := validation.ValidateObjectMeta(objectMeta, strategy.NamespaceScoped(), validation.ValidatePathSegmentName, field.NewPath("metadata")); len(errs) > 0 {
return errors.NewInvalid(kind, objectMeta.Name, errs)
return errors.NewInvalid(kind.GroupKind(), objectMeta.Name, errs)
}
strategy.Canonicalize(obj)
@@ -102,18 +103,18 @@ func CheckGeneratedNameError(strategy RESTCreateStrategy, err error, obj runtime
return err
}
return errors.NewServerTimeout(kind, "POST", 0)
return errors.NewServerTimeoutForKind(kind.GroupKind(), "POST", 0)
}
// objectMetaAndKind retrieves kind and ObjectMeta from a runtime object, or returns an error.
func objectMetaAndKind(typer runtime.ObjectTyper, obj runtime.Object) (*api.ObjectMeta, string, error) {
func objectMetaAndKind(typer runtime.ObjectTyper, obj runtime.Object) (*api.ObjectMeta, unversioned.GroupVersionKind, error) {
objectMeta, err := api.ObjectMetaFor(obj)
if err != nil {
return nil, "", errors.NewInternalError(err)
return nil, unversioned.GroupVersionKind{}, errors.NewInternalError(err)
}
gvk, err := typer.ObjectKind(obj)
kind, err := typer.ObjectKind(obj)
if err != nil {
return nil, "", errors.NewInternalError(err)
return nil, unversioned.GroupVersionKind{}, errors.NewInternalError(err)
}
return objectMeta, gvk.Kind, nil
return objectMeta, kind, nil
}

View File

@@ -96,7 +96,7 @@ func BeforeUpdate(strategy RESTUpdateStrategy, ctx api.Context, obj, old runtime
errs = append(errs, strategy.ValidateUpdate(ctx, obj, old)...)
if len(errs) > 0 {
return errors.NewInvalid(kind, objectMeta.Name, errs)
return errors.NewInvalid(kind.GroupKind(), objectMeta.Name, errs)
}
strategy.Canonicalize(obj)

View File

@@ -33,8 +33,15 @@ func (gr GroupResource) WithVersion(version string) GroupVersionResource {
return GroupVersionResource{Group: gr.Group, Version: version, Resource: gr.Resource}
}
func (gr GroupResource) IsEmpty() bool {
return len(gr.Group) == 0 && len(gr.Resource) == 0
}
func (gr *GroupResource) String() string {
return strings.Join([]string{gr.Group, ", Resource=", gr.Resource}, "")
if len(gr.Group) == 0 {
return gr.Resource
}
return gr.Resource + "." + gr.Group
}
// GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
@@ -66,12 +73,19 @@ type GroupKind struct {
Kind string
}
func (gk GroupKind) IsEmpty() bool {
return len(gk.Group) == 0 && len(gk.Kind) == 0
}
func (gk GroupKind) WithVersion(version string) GroupVersionKind {
return GroupVersionKind{Group: gk.Group, Version: version, Kind: gk.Kind}
}
func (gk *GroupKind) String() string {
return gk.Group + ", Kind=" + gk.Kind
if len(gk.Group) == 0 {
return gk.Kind
}
return gk.Kind + "." + gk.Group
}
// GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion

View File

@@ -121,6 +121,8 @@ type StatusDetails struct {
// The name attribute of the resource associated with the status StatusReason
// (when there is a single name which can be described).
Name string `json:"name,omitempty"`
// The group attribute of the resource associated with the status StatusReason.
Group string `json:"group,omitempty"`
// The kind attribute of the resource associated with the status StatusReason.
// On some operations may differ from the requested resource Kind.
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds

View File

@@ -164,6 +164,7 @@ func (StatusCause) SwaggerDoc() map[string]string {
var map_StatusDetails = map[string]string{
"": "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.",
"name": "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).",
"group": "The group attribute of the resource associated with the status StatusReason.",
"kind": "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds",
"causes": "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.",
"retryAfterSeconds": "If specified, the time in seconds before the operation should be retried.",