QueryService: Move raw http.Handler to rest.Connector (#87595)
* query connector * improve error wrappers * return 400 for missing datasource (not 404) * fix errors * message
This commit is contained in:
@@ -2,7 +2,6 @@ package query
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -12,57 +11,129 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/experimental/apis/data/v0alpha1"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"golang.org/x/sync/errgroup"
|
||||
errorsK8s "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/grafana/grafana/pkg/util/errutil/errhttp"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
// The query method (not really a create)
|
||||
func (b *QueryAPIBuilder) doQuery(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := b.tracer.Start(r.Context(), "QueryService.Query")
|
||||
defer span.End()
|
||||
type queryREST struct {
|
||||
builder *QueryAPIBuilder
|
||||
}
|
||||
|
||||
raw := &query.QueryDataRequest{}
|
||||
err := web.Bind(r, raw)
|
||||
if err != nil {
|
||||
errhttp.Write(ctx, errutil.BadRequest(
|
||||
"query.bind",
|
||||
errutil.WithPublicMessage("Error reading query")).
|
||||
Errorf("error reading: %w", err), w)
|
||||
return
|
||||
var (
|
||||
_ rest.Storage = (*queryREST)(nil)
|
||||
_ rest.SingularNameProvider = (*queryREST)(nil)
|
||||
_ rest.Connecter = (*queryREST)(nil)
|
||||
_ rest.Scoper = (*queryREST)(nil)
|
||||
_ rest.StorageMetadata = (*queryREST)(nil)
|
||||
)
|
||||
|
||||
func (r *queryREST) New() runtime.Object {
|
||||
// This is added as the "ResponseType" regarless what ProducesObject() says :)
|
||||
return &query.QueryDataResponse{}
|
||||
}
|
||||
|
||||
func (r *queryREST) Destroy() {}
|
||||
|
||||
func (r *queryREST) NamespaceScoped() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *queryREST) GetSingularName() string {
|
||||
return "QueryResults" // Used for the
|
||||
}
|
||||
|
||||
func (r *queryREST) ProducesMIMETypes(verb string) []string {
|
||||
return []string{"application/json"} // and parquet!
|
||||
}
|
||||
|
||||
func (r *queryREST) ProducesObject(verb string) interface{} {
|
||||
return &query.QueryDataResponse{}
|
||||
}
|
||||
|
||||
func (r *queryREST) ConnectMethods() []string {
|
||||
return []string{"POST"}
|
||||
}
|
||||
|
||||
func (r *queryREST) NewConnectOptions() (runtime.Object, bool, string) {
|
||||
return nil, false, "" // true means you can use the trailing path as a variable
|
||||
}
|
||||
|
||||
func (r *queryREST) Connect(ctx context.Context, name string, opts runtime.Object, responder rest.Responder) (http.Handler, error) {
|
||||
// See: /pkg/apiserver/builder/helper.go#L34
|
||||
// The name is set with a rewriter hack
|
||||
if name != "name" {
|
||||
return nil, errorsK8s.NewNotFound(schema.GroupResource{}, name)
|
||||
}
|
||||
b := r.builder
|
||||
|
||||
// Parses the request and splits it into multiple sub queries (if necessary)
|
||||
req, err := b.parser.parseRequest(ctx, raw)
|
||||
if err != nil {
|
||||
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
||||
errhttp.Write(ctx, errutil.BadRequest(
|
||||
"query.datasource.notfound",
|
||||
errutil.WithPublicMessage(err.Error())), w)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, httpreq *http.Request) {
|
||||
ctx, span := b.tracer.Start(httpreq.Context(), "QueryService.Query")
|
||||
defer span.End()
|
||||
|
||||
raw := &query.QueryDataRequest{}
|
||||
err := web.Bind(httpreq, raw)
|
||||
if err != nil {
|
||||
err = errorsK8s.NewBadRequest("error reading query")
|
||||
// TODO: can we wrap the error so details are not lost?!
|
||||
// errutil.BadRequest(
|
||||
// "query.bind",
|
||||
// errutil.WithPublicMessage("Error reading query")).
|
||||
// Errorf("error reading: %w", err)
|
||||
responder.Error(err)
|
||||
return
|
||||
}
|
||||
errhttp.Write(ctx, err, w)
|
||||
return
|
||||
}
|
||||
|
||||
// Actually run the query
|
||||
rsp, err := b.execute(ctx, req)
|
||||
if err != nil {
|
||||
errhttp.Write(ctx, errutil.Internal(
|
||||
"query.execution",
|
||||
errutil.WithPublicMessage("Error executing query")).
|
||||
Errorf("execution error: %w", err), w)
|
||||
return
|
||||
}
|
||||
// Parses the request and splits it into multiple sub queries (if necessary)
|
||||
req, err := b.parser.parseRequest(ctx, raw)
|
||||
if err != nil {
|
||||
if errors.Is(err, datasources.ErrDataSourceNotFound) {
|
||||
// TODO, can we wrap the error somehow?
|
||||
err = &errorsK8s.StatusError{ErrStatus: metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Code: http.StatusBadRequest, // the URL is found, but includes bad requests
|
||||
Reason: metav1.StatusReasonNotFound,
|
||||
Message: "datasource not found",
|
||||
}}
|
||||
}
|
||||
responder.Error(convertToK8sError(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(query.GetResponseCode(rsp))
|
||||
_ = json.NewEncoder(w).Encode(rsp)
|
||||
// Actually run the query
|
||||
rsp, err := b.execute(ctx, req)
|
||||
if err != nil {
|
||||
responder.Error(convertToK8sError(err))
|
||||
return
|
||||
}
|
||||
|
||||
responder.Object(query.GetResponseCode(rsp), &query.QueryDataResponse{
|
||||
QueryDataResponse: *rsp, // wrap the backend response as a QueryDataResponse
|
||||
})
|
||||
}), 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) {
|
||||
|
||||
Reference in New Issue
Block a user