Errors: Update errutil to be compatible with k8s errors (#87605)

This commit is contained in:
Ryan McKinley 2024-05-20 18:11:37 +03:00 committed by GitHub
parent 60e7a4e746
commit 6d10797812
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 57 additions and 69 deletions

View File

@ -21,7 +21,6 @@ import (
"github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/util/errutil"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -105,14 +104,14 @@ func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Objec
Message: "datasource not found", Message: "datasource not found",
}} }}
} }
responder.Error(convertToK8sError(err)) responder.Error(err)
return return
} }
// Actually run the query // Actually run the query
rsp, err := b.execute(ctx, req) rsp, err := b.execute(ctx, req)
if err != nil { if err != nil {
responder.Error(convertToK8sError(err)) responder.Error(err)
return return
} }
@ -122,20 +121,6 @@ func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Objec
}), nil }), nil
} }
// Would be really nice if errutil was directly k8s compatible :(
func convertToK8sError(err error) error {
var gErr errutil.Error
if errors.As(err, &gErr) {
return &errorsK8s.StatusError{ErrStatus: metav1.Status{
Status: metav1.StatusFailure,
Code: int32(gErr.Reason.Status().HTTPStatus()),
Reason: metav1.StatusReason(gErr.Reason.Status()), // almost true
Message: gErr.PublicMessage,
}}
}
return err
}
func (b *QueryAPIBuilder) execute(ctx context.Context, req parsedRequestInfo) (qdr *backend.QueryDataResponse, err error) { func (b *QueryAPIBuilder) execute(ctx context.Context, req parsedRequestInfo) (qdr *backend.QueryDataResponse, err error) {
switch len(req.Requests) { switch len(req.Requests) {
case 0: case 0:

View File

@ -144,7 +144,8 @@ func TestIntegrationSimpleQuery(t *testing.T) {
"metadata": {}, "metadata": {},
"status": "Failure", "status": "Failure",
"message": "did not execute expression [Y] due to a failure to of the dependent expression or query [X]", "message": "did not execute expression [Y] due to a failure to of the dependent expression or query [X]",
"reason": "Bad request", "reason": "BadRequest",
"details": { "uid": "sse.dependencyError" },
"code": 400 "code": 400
}`, string(body)) }`, string(body))
// require.JSONEq(t, `{ // require.JSONEq(t, `{

View File

@ -7,7 +7,6 @@ import (
"net/http" "net/http"
"reflect" "reflect"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/endpoints/request"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
@ -23,14 +22,6 @@ type ErrorOptions struct {
logger log.Logger logger log.Logger
} }
type k8sError struct {
metav1.Status `json:",inline"`
// Internal values that do not have a clean home in the standard Status object
MessageID string `json:"messageId"`
Extra map[string]any `json:"extra,omitempty"`
}
// Write writes an error to the provided [http.ResponseWriter] with the // Write writes an error to the provided [http.ResponseWriter] with the
// appropriate HTTP status and JSON payload from [errutil.Error]. // appropriate HTTP status and JSON payload from [errutil.Error].
// Write also logs the provided error to either the "request-errors" // Write also logs the provided error to either the "request-errors"
@ -63,37 +54,9 @@ func Write(ctx context.Context, err error, w http.ResponseWriter, opts ...func(E
// When running in k8s, this will return a v1 status // When running in k8s, this will return a v1 status
// Typically, k8s handlers should directly support error negotiation, however // Typically, k8s handlers should directly support error negotiation, however
// when implementing handlers directly this will maintain compatibility with client-go // when implementing handlers directly this will maintain compatibility with client-go
info, ok := request.RequestInfoFrom(ctx) _, ok := request.RequestInfoFrom(ctx)
if ok { if ok {
status := &k8sError{ rsp = gErr.Status()
Status: metav1.Status{
Status: metav1.StatusFailure,
Code: int32(pub.StatusCode),
Message: pub.Message,
Details: &metav1.StatusDetails{
Name: info.Name,
Group: info.APIGroup,
},
},
// Add the internal values into
MessageID: pub.MessageID,
Extra: pub.Extra,
}
switch pub.StatusCode {
case 400:
status.Reason = metav1.StatusReasonBadRequest
case 401:
status.Reason = metav1.StatusReasonUnauthorized
case 403:
status.Reason = metav1.StatusReasonForbidden
case 404:
status.Reason = metav1.StatusReasonNotFound
case 500: // many reasons things could map here
status.Reason = metav1.StatusReasonInternalError
case 504:
status.Reason = metav1.StatusReasonTimeout
}
rsp = status
} }
err = json.NewEncoder(w).Encode(rsp) err = json.NewEncoder(w).Encode(rsp)

View File

@ -27,9 +27,8 @@ func TestWrite(t *testing.T) {
"status": "Failure", "status": "Failure",
"reason": "Timeout", "reason": "Timeout",
"metadata": {}, "metadata": {},
"messageId": "test.thisIsExpected",
"message": "Timeout", "message": "Timeout",
"details": { "group": "TestGroup" }, "details": { "uid": "test.thisIsExpected" },
"code": 504 "code": 504
}`, recorder.Body.String()) }`, recorder.Body.String())
} }

View File

@ -1,8 +1,13 @@
package errutil package errutil
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
) )
// Base represents the static information about a specific error. // Base represents the static information about a specific error.
@ -294,6 +299,9 @@ func (b Base) Is(err error) bool {
} }
} }
// Allow errorutil errors to be returned as informative k8s errors
var _ = errorsK8s.APIStatus(&Error{})
// Error is the error type for errors within Grafana, extending // Error is the error type for errors within Grafana, extending
// the Go error type with Grafana specific metadata to reduce // the Go error type with Grafana specific metadata to reduce
// boilerplate error handling for status codes and internationalization // boilerplate error handling for status codes and internationalization
@ -365,6 +373,34 @@ func (e Error) Error() string {
return fmt.Sprintf("[%s] %s", e.MessageID, e.LogMessage) return fmt.Sprintf("[%s] %s", e.MessageID, e.LogMessage)
} }
// When the error is rendered by an apiserver, this format is used
func (e Error) Status() metav1.Status {
public := e.Public()
s := metav1.Status{
Status: metav1.StatusFailure,
Code: int32(public.StatusCode),
Reason: metav1.StatusReason(e.Reason.Status()), // almost true
Message: public.Message,
}
// Shove the extra data into details
if public.Extra != nil || public.MessageID != "" {
s.Details = &metav1.StatusDetails{
UID: types.UID(public.MessageID),
}
for k, v := range public.Extra {
v, err := json.Marshal(v)
if err != nil {
s.Details.Causes = append(s.Details.Causes, metav1.StatusCause{
Field: k,
Message: string(v),
})
}
}
}
return s
}
// Unwrap is used by errors.As to iterate over the sequence of // Unwrap is used by errors.As to iterate over the sequence of
// underlying errors until a matching type is found. // underlying errors until a matching type is found.
func (e Error) Unwrap() error { func (e Error) Unwrap() error {

View File

@ -1,6 +1,10 @@
package errutil package errutil
import "net/http" import (
"net/http"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const ( const (
// StatusUnknown implies an error that should be updated to contain // StatusUnknown implies an error that should be updated to contain
@ -11,15 +15,15 @@ const (
// client's authentication, either because it has not been provided // client's authentication, either because it has not been provided
// or is invalid for the operation. // or is invalid for the operation.
// HTTP status code 401. // HTTP status code 401.
StatusUnauthorized CoreStatus = "Unauthorized" StatusUnauthorized CoreStatus = CoreStatus(metav1.StatusReasonUnauthorized)
// StatusForbidden means that the server refuses to perform the // StatusForbidden means that the server refuses to perform the
// requested action for the authenticated uer. // requested action for the authenticated uer.
// HTTP status code 403. // HTTP status code 403.
StatusForbidden CoreStatus = "Forbidden" StatusForbidden CoreStatus = CoreStatus(metav1.StatusReasonForbidden)
// StatusNotFound means that the server does not have any // StatusNotFound means that the server does not have any
// corresponding document to return to the request. // corresponding document to return to the request.
// HTTP status code 404. // HTTP status code 404.
StatusNotFound CoreStatus = "Not found" StatusNotFound CoreStatus = CoreStatus(metav1.StatusReasonNotFound)
// StatusUnprocessableEntity means that the server understands the request, // StatusUnprocessableEntity means that the server understands the request,
// the content type and the syntax but it was unable to process the // the content type and the syntax but it was unable to process the
// contained instructions. // contained instructions.
@ -28,15 +32,15 @@ const (
// StatusConflict means that the server cannot fulfill the request // StatusConflict means that the server cannot fulfill the request
// there is a conflict in the current state of a resource // there is a conflict in the current state of a resource
// HTTP status code 409. // HTTP status code 409.
StatusConflict CoreStatus = "Conflict" StatusConflict CoreStatus = CoreStatus(metav1.StatusReasonConflict)
// StatusTooManyRequests means that the client is rate limited // StatusTooManyRequests means that the client is rate limited
// by the server and should back-off before trying again. // by the server and should back-off before trying again.
// HTTP status code 429. // HTTP status code 429.
StatusTooManyRequests CoreStatus = "Too many requests" StatusTooManyRequests CoreStatus = CoreStatus(metav1.StatusReasonTooManyRequests)
// StatusBadRequest means that the server was unable to parse the // StatusBadRequest means that the server was unable to parse the
// parameters or payload for the request. // parameters or payload for the request.
// HTTP status code 400. // HTTP status code 400.
StatusBadRequest CoreStatus = "Bad request" StatusBadRequest CoreStatus = CoreStatus(metav1.StatusReasonBadRequest)
// StatusClientClosedRequest means that a client closes the connection // StatusClientClosedRequest means that a client closes the connection
// while the server is processing the request. // while the server is processing the request.
// //
@ -52,11 +56,11 @@ const (
// StatusInternal means that the server acknowledges that there's // StatusInternal means that the server acknowledges that there's
// an error, but that there is nothing the client can do to fix it. // an error, but that there is nothing the client can do to fix it.
// HTTP status code 500. // HTTP status code 500.
StatusInternal CoreStatus = "Internal server error" StatusInternal CoreStatus = CoreStatus(metav1.StatusReasonInternalError)
// StatusTimeout means that the server did not complete the request // StatusTimeout means that the server did not complete the request
// within the required time and aborted the action. // within the required time and aborted the action.
// HTTP status code 504. // HTTP status code 504.
StatusTimeout CoreStatus = "Timeout" StatusTimeout CoreStatus = CoreStatus(metav1.StatusReasonTimeout)
// StatusNotImplemented means that the server does not support the // StatusNotImplemented means that the server does not support the
// requested action. Typically used during development of new // requested action. Typically used during development of new
// features. // features.
@ -83,7 +87,7 @@ type StatusReason interface {
Status() CoreStatus Status() CoreStatus
} }
type CoreStatus string type CoreStatus metav1.StatusReason
// Status implements the StatusReason interface. // Status implements the StatusReason interface.
func (s CoreStatus) Status() CoreStatus { func (s CoreStatus) Status() CoreStatus {