Commit 860050b5 authored by Daniel Smith's avatar Daniel Smith

Merge pull request #745 from smarterclayton/add_structured_reason

Evolve the api.Status object with Reason/Details
parents dc0a475b 4f88b778
...@@ -336,21 +336,43 @@ type MinionList struct { ...@@ -336,21 +336,43 @@ type MinionList struct {
} }
// Status is a return value for calls that don't return other objects. // Status is a return value for calls that don't return other objects.
// Arguably, this could go in apiserver, but I'm including it here so clients needn't // TODO: this could go in apiserver, but I'm including it here so clients needn't
// import both. // import both.
type Status struct { type Status struct {
JSONBase `json:",inline" yaml:",inline"` JSONBase `json:",inline" yaml:",inline"`
// One of: "success", "failure", "working" (for operations not yet completed) // One of: "success", "failure", "working" (for operations not yet completed)
// TODO: if "working", include an operation identifier so final status can be
// checked.
Status string `json:"status,omitempty" yaml:"status,omitempty"` Status string `json:"status,omitempty" yaml:"status,omitempty"`
// Details about the status. May be an error description or an // A human readable description of the status of this operation.
// operation number for later polling. Message string `json:"message,omitempty" yaml:"message,omitempty"`
Details string `json:"details,omitempty" yaml:"details,omitempty"` // 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.
Reason ReasonType `json:"reason,omitempty" yaml:"reason,omitempty"`
// Extended data associated with the reason. Each reason may define its
// own extended details. This field is optional and the data returned
// is not guaranteed to conform to any schema except that defined by
// the reason type.
Details *StatusDetails `json:"details,omitempty" yaml:"details,omitempty"`
// Suggested HTTP return code for this status, 0 if not set. // Suggested HTTP return code for this status, 0 if not set.
Code int `json:"code,omitempty" yaml:"code,omitempty"` Code int `json:"code,omitempty" yaml:"code,omitempty"`
} }
// 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.
type StatusDetails struct {
// The ID attribute of the resource associated with the status ReasonType
// (when there is a single ID which can be described).
ID string `json:"id,omitempty" yaml:"id,omitempty"`
// 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"`
}
// Values of Status.Status // Values of Status.Status
const ( const (
StatusSuccess = "success" StatusSuccess = "success"
...@@ -358,6 +380,55 @@ const ( ...@@ -358,6 +380,55 @@ const (
StatusWorking = "working" StatusWorking = "working"
) )
// ReasonType is an enumeration of possible failure causes. Each ReasonType
// must map to a single HTTP status code, but multiple reasons may map
// to the same HTTP status code.
// TODO: move to apiserver
type ReasonType string
const (
// ReasonTypeUnknown means the server has declined to indicate a specific reason.
// The details field may contain other information about this error.
// Status code 500.
ReasonTypeUnknown ReasonType = ""
// ReasonTypeWorking means the server is processing this request and will complete
// at a future time.
// Details (optional):
// "kind" string - the name of the resource being referenced ("operation" today)
// "id" string - the identifier of the Operation resource where updates
// will be returned
// Headers (optional):
// "Location" - HTTP header populated with a URL that can retrieved the final
// status of this operation.
// Status code 202
ReasonTypeWorking ReasonType = "working"
// ResourceTypeNotFound means one or more resources required for this operation
// could not be found.
// Details (optional):
// "kind" string - the kind attribute of the missing resource
// on some operations may differ from the requested
// resource.
// "id" string - the identifier of the missing resource
// Status code 404
ReasonTypeNotFound ReasonType = "not_found"
// ReasonTypeAlreadyExists means the resource you are creating already exists.
// Details (optional):
// "kind" string - the kind attribute of the conflicting resource
// "id" string - the identifier of the conflicting resource
// Status code 409
ReasonTypeAlreadyExists ReasonType = "already_exists"
// ResourceTypeConflict means the requested update operation cannot be completed
// due to a conflict in the operation. The client may need to alter the request.
// Each resource may define custom details that indicate the nature of the
// conflict.
// Status code 409
ReasonTypeConflict ReasonType = "conflict"
)
// ServerOp is an operation delivered to API clients. // ServerOp is an operation delivered to API clients.
type ServerOp struct { type ServerOp struct {
JSONBase `yaml:",inline" json:",inline"` JSONBase `yaml:",inline" json:",inline"`
......
...@@ -339,21 +339,43 @@ type MinionList struct { ...@@ -339,21 +339,43 @@ type MinionList struct {
} }
// Status is a return value for calls that don't return other objects. // Status is a return value for calls that don't return other objects.
// Arguably, this could go in apiserver, but I'm including it here so clients needn't // TODO: this could go in apiserver, but I'm including it here so clients needn't
// import both. // import both.
type Status struct { type Status struct {
JSONBase `json:",inline" yaml:",inline"` JSONBase `json:",inline" yaml:",inline"`
// One of: "success", "failure", "working" (for operations not yet completed) // One of: "success", "failure", "working" (for operations not yet completed)
// TODO: if "working", include an operation identifier so final status can be
// checked.
Status string `json:"status,omitempty" yaml:"status,omitempty"` Status string `json:"status,omitempty" yaml:"status,omitempty"`
// Details about the status. May be an error description or an // A human readable description of the status of this operation.
// operation number for later polling. Message string `json:"message,omitempty" yaml:"message,omitempty"`
Details string `json:"details,omitempty" yaml:"details,omitempty"` // 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.
Reason ReasonType `json:"reason,omitempty" yaml:"reason,omitempty"`
// Extended data associated with the reason. Each reason may define its
// own extended details. This field is optional and the data returned
// is not guaranteed to conform to any schema except that defined by
// the reason type.
Details *StatusDetails `json:"details,omitempty" yaml:"details,omitempty"`
// Suggested HTTP return code for this status, 0 if not set. // Suggested HTTP return code for this status, 0 if not set.
Code int `json:"code,omitempty" yaml:"code,omitempty"` Code int `json:"code,omitempty" yaml:"code,omitempty"`
} }
// 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.
type StatusDetails struct {
// The ID attribute of the resource associated with the status ReasonType
// (when there is a single ID which can be described).
ID string `json:"id,omitempty" yaml:"id,omitempty"`
// 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"`
}
// Values of Status.Status // Values of Status.Status
const ( const (
StatusSuccess = "success" StatusSuccess = "success"
...@@ -361,6 +383,55 @@ const ( ...@@ -361,6 +383,55 @@ const (
StatusWorking = "working" StatusWorking = "working"
) )
// ReasonType is an enumeration of possible failure causes. Each ReasonType
// must map to a single HTTP status code, but multiple reasons may map
// to the same HTTP status code.
// TODO: move to apiserver
type ReasonType string
const (
// ReasonTypeUnknown means the server has declined to indicate a specific reason.
// The details field may contain other information about this error.
// Status code 500.
ReasonTypeUnknown ReasonType = ""
// ReasonTypeWorking means the server is processing this request and will complete
// at a future time.
// Details (optional):
// "kind" string - the name of the resource being referenced ("operation" today)
// "id" string - the identifier of the Operation resource where updates
// will be returned
// Headers (optional):
// "Location" - HTTP header populated with a URL that can retrieved the final
// status of this operation.
// Status code 202
ReasonTypeWorking ReasonType = "working"
// ResourceTypeNotFound means one or more resources required for this operation
// could not be found.
// Details (optional):
// "kind" string - the kind attribute of the missing resource
// on some operations may differ from the requested
// resource.
// "id" string - the identifier of the missing resource
// Status code 404
ReasonTypeNotFound ReasonType = "not_found"
// ReasonTypeAlreadyExists means the resource you are creating already exists.
// Details (optional):
// "kind" string - the kind attribute of the conflicting resource
// "id" string - the identifier of the conflicting resource
// Status code 409
ReasonTypeAlreadyExists ReasonType = "already_exists"
// ResourceTypeConflict means the requested update operation cannot be completed
// due to a conflict in the operation. The client may need to alter the request.
// Each resource may define custom details that indicate the nature of the
// conflict.
// Status code 409
ReasonTypeConflict ReasonType = "conflict"
)
// ServerOp is an operation delivered to API clients. // ServerOp is an operation delivered to API clients.
type ServerOp struct { type ServerOp struct {
JSONBase `yaml:",inline" json:",inline"` JSONBase `yaml:",inline" json:",inline"`
......
...@@ -493,7 +493,7 @@ func TestCreate(t *testing.T) { ...@@ -493,7 +493,7 @@ func TestCreate(t *testing.T) {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
if itemOut.Status != api.StatusWorking || itemOut.Details == "" { if itemOut.Status != api.StatusWorking || itemOut.Details == nil || itemOut.Details.ID == "" {
t.Errorf("Unexpected status: %#v (%s)", itemOut, string(body)) t.Errorf("Unexpected status: %#v (%s)", itemOut, string(body))
} }
} }
...@@ -621,7 +621,7 @@ func TestAsyncDelayReturnsError(t *testing.T) { ...@@ -621,7 +621,7 @@ func TestAsyncDelayReturnsError(t *testing.T) {
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/prefix/version/foo/bar", server.URL), nil, http.StatusInternalServerError) status := expectApiStatus(t, "DELETE", fmt.Sprintf("%s/prefix/version/foo/bar", server.URL), nil, http.StatusInternalServerError)
if status.Status != api.StatusFailure || status.Details == "" { if status.Status != api.StatusFailure || status.Message != "error" || status.Details != nil {
t.Errorf("Unexpected status %#v", status) t.Errorf("Unexpected status %#v", status)
} }
} }
...@@ -642,11 +642,11 @@ func TestAsyncCreateError(t *testing.T) { ...@@ -642,11 +642,11 @@ func TestAsyncCreateError(t *testing.T) {
data, _ := api.Encode(simple) data, _ := api.Encode(simple)
status := expectApiStatus(t, "POST", fmt.Sprintf("%s/prefix/version/foo", server.URL), data, http.StatusAccepted) status := expectApiStatus(t, "POST", fmt.Sprintf("%s/prefix/version/foo", server.URL), data, http.StatusAccepted)
if status.Status != api.StatusWorking || status.Details == "" { if status.Status != api.StatusWorking || status.Details == nil || status.Details.ID == "" {
t.Errorf("Unexpected status %#v", status) t.Errorf("Unexpected status %#v", status)
} }
otherStatus := expectApiStatus(t, "GET", fmt.Sprintf("%s/prefix/version/operations/%s", server.URL, status.Details), []byte{}, http.StatusAccepted) otherStatus := expectApiStatus(t, "GET", fmt.Sprintf("%s/prefix/version/operations/%s", server.URL, status.Details.ID), []byte{}, http.StatusAccepted)
if !reflect.DeepEqual(status, otherStatus) { if !reflect.DeepEqual(status, otherStatus) {
t.Errorf("Expected %#v, Got %#v", status, otherStatus) t.Errorf("Expected %#v, Got %#v", status, otherStatus)
} }
...@@ -654,10 +654,10 @@ func TestAsyncCreateError(t *testing.T) { ...@@ -654,10 +654,10 @@ func TestAsyncCreateError(t *testing.T) {
ch <- struct{}{} ch <- struct{}{}
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
finalStatus := expectApiStatus(t, "GET", fmt.Sprintf("%s/prefix/version/operations/%s?after=1", server.URL, status.Details), []byte{}, http.StatusOK) finalStatus := expectApiStatus(t, "GET", fmt.Sprintf("%s/prefix/version/operations/%s?after=1", server.URL, status.Details.ID), []byte{}, http.StatusOK)
expectedStatus := &api.Status{ expectedStatus := &api.Status{
Code: http.StatusInternalServerError, Code: http.StatusInternalServerError,
Details: "error", Message: "error",
Status: api.StatusFailure, Status: api.StatusFailure,
} }
if !reflect.DeepEqual(expectedStatus, finalStatus) { if !reflect.DeepEqual(expectedStatus, finalStatus) {
...@@ -721,7 +721,7 @@ func TestSyncCreateTimeout(t *testing.T) { ...@@ -721,7 +721,7 @@ func TestSyncCreateTimeout(t *testing.T) {
simple := Simple{Name: "foo"} simple := Simple{Name: "foo"}
data, _ := api.Encode(simple) data, _ := api.Encode(simple)
itemOut := expectApiStatus(t, "POST", server.URL+"/prefix/version/foo?sync=true&timeout=4ms", data, http.StatusAccepted) itemOut := expectApiStatus(t, "POST", server.URL+"/prefix/version/foo?sync=true&timeout=4ms", data, http.StatusAccepted)
if itemOut.Status != api.StatusWorking || itemOut.Details == "" { if itemOut.Status != api.StatusWorking || itemOut.Details == nil || itemOut.Details.ID == "" {
t.Errorf("Unexpected status %#v", itemOut) t.Errorf("Unexpected status %#v", itemOut)
} }
} }
...@@ -44,7 +44,7 @@ func MakeAsync(fn WorkFunc) <-chan interface{} { ...@@ -44,7 +44,7 @@ func MakeAsync(fn WorkFunc) <-chan interface{} {
} }
channel <- &api.Status{ channel <- &api.Status{
Status: api.StatusFailure, Status: api.StatusFailure,
Details: err.Error(), Message: err.Error(),
Code: status, Code: status,
} }
} else { } else {
......
...@@ -206,7 +206,8 @@ func (op *Operation) StatusOrResult() (description interface{}, finished bool) { ...@@ -206,7 +206,8 @@ func (op *Operation) StatusOrResult() (description interface{}, finished bool) {
if op.finished == nil { if op.finished == nil {
return api.Status{ return api.Status{
Status: api.StatusWorking, Status: api.StatusWorking,
Details: op.ID, Reason: api.ReasonTypeWorking,
Details: &api.StatusDetails{ID: op.ID, Kind: "operation"},
}, false }, false
} }
return op.result, true return op.result, true
......
...@@ -177,11 +177,11 @@ func TestOpGet(t *testing.T) { ...@@ -177,11 +177,11 @@ func TestOpGet(t *testing.T) {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
if itemOut.Status != api.StatusWorking || itemOut.Details == "" { if itemOut.Status != api.StatusWorking || itemOut.Details == nil || itemOut.Details.ID == "" {
t.Errorf("Unexpected status: %#v (%s)", itemOut, string(body)) t.Fatalf("Unexpected status: %#v (%s)", itemOut, string(body))
} }
req2, err := http.NewRequest("GET", server.URL+"/prefix/version/operations/"+itemOut.Details, nil) req2, err := http.NewRequest("GET", server.URL+"/prefix/version/operations/"+itemOut.Details.ID, nil)
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
} }
......
...@@ -246,13 +246,18 @@ func (r *Request) Do() Result { ...@@ -246,13 +246,18 @@ func (r *Request) Do() Result {
if err != nil { if err != nil {
if statusErr, ok := err.(*StatusErr); ok { if statusErr, ok := err.(*StatusErr); ok {
if statusErr.Status.Status == api.StatusWorking && r.pollPeriod != 0 { if statusErr.Status.Status == api.StatusWorking && r.pollPeriod != 0 {
glog.Infof("Waiting for completion of /operations/%s", statusErr.Status.Details) if statusErr.Status.Details != nil {
time.Sleep(r.pollPeriod) id := statusErr.Status.Details.ID
// Make a poll request if len(id) > 0 {
pollOp := r.c.PollFor(statusErr.Status.Details).PollPeriod(r.pollPeriod) glog.Infof("Waiting for completion of /operations/%s", id)
// Could also say "return r.Do()" but this way doesn't grow the callstack. time.Sleep(r.pollPeriod)
r = pollOp // Make a poll request
continue pollOp := r.c.PollFor(id).PollPeriod(r.pollPeriod)
// Could also say "return r.Do()" but this way doesn't grow the callstack.
r = pollOp
continue
}
}
} }
} }
} }
......
...@@ -258,10 +258,10 @@ func TestSetPollPeriod(t *testing.T) { ...@@ -258,10 +258,10 @@ func TestSetPollPeriod(t *testing.T) {
func TestPolling(t *testing.T) { func TestPolling(t *testing.T) {
objects := []interface{}{ objects := []interface{}{
&api.Status{Status: api.StatusWorking, Details: "1234"}, &api.Status{Status: api.StatusWorking, Details: &api.StatusDetails{ID: "1234"}},
&api.Status{Status: api.StatusWorking, Details: "1234"}, &api.Status{Status: api.StatusWorking, Details: &api.StatusDetails{ID: "1234"}},
&api.Status{Status: api.StatusWorking, Details: "1234"}, &api.Status{Status: api.StatusWorking, Details: &api.StatusDetails{ID: "1234"}},
&api.Status{Status: api.StatusWorking, Details: "1234"}, &api.Status{Status: api.StatusWorking, Details: &api.StatusDetails{ID: "1234"}},
&api.Status{Status: api.StatusSuccess}, &api.Status{Status: api.StatusSuccess},
} }
......
...@@ -58,7 +58,7 @@ func (s *ProxyServer) doError(w http.ResponseWriter, err error) { ...@@ -58,7 +58,7 @@ func (s *ProxyServer) doError(w http.ResponseWriter, err error) {
w.Header().Add("Content-type", "application/json") w.Header().Add("Content-type", "application/json")
data, _ := api.Encode(api.Status{ data, _ := api.Encode(api.Status{
Status: api.StatusFailure, Status: api.StatusFailure,
Details: fmt.Sprintf("internal error: %#v", err), Message: fmt.Sprintf("internal error: %#v", err),
}) })
w.Write(data) w.Write(data)
} }
......
...@@ -36,8 +36,8 @@ func expectApiStatusError(t *testing.T, ch <-chan interface{}, msg string) { ...@@ -36,8 +36,8 @@ func expectApiStatusError(t *testing.T, ch <-chan interface{}, msg string) {
t.Errorf("Expected an api.Status object, was %#v", out) t.Errorf("Expected an api.Status object, was %#v", out)
return return
} }
if msg != status.Details { if msg != status.Message {
t.Errorf("Expected %#v, was %s", msg, status.Details) t.Errorf("Expected %#v, was %s", msg, status.Message)
} }
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment