Skip to content

Commit

Permalink
Query conversion handler (#1078)
Browse files Browse the repository at this point in the history
  • Loading branch information
andresmgot authored Sep 17, 2024
1 parent 0008ea0 commit 97a7441
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 11 deletions.
5 changes: 5 additions & 0 deletions backend/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ type ConversionHandler interface {

type ConvertObjectsFunc func(context.Context, *ConversionRequest) (*ConversionResponse, error)

// ConvertObjects calls fn(ctx, req).
func (fn ConvertObjectsFunc) ConvertObjects(ctx context.Context, req *ConversionRequest) (*ConversionResponse, error) {
return fn(ctx, req)
}

type GroupVersion struct {
Group string `json:"group,omitempty"`
Version string `json:"version,omitempty"`
Expand Down
62 changes: 59 additions & 3 deletions backend/conversion_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,83 @@ package backend

import (
"context"
"encoding/json"
"fmt"

"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
)

type conversionSDKAdapter struct {
handler ConversionHandler
// queryConversionHandler can be nil if the adapter does not support query conversion.
queryConversionHandler QueryConversionHandler
}

func newConversionSDKAdapter(handler ConversionHandler) *conversionSDKAdapter {
func newConversionSDKAdapter(handler ConversionHandler, queryConversionHandler QueryConversionHandler) *conversionSDKAdapter {
return &conversionSDKAdapter{
handler: handler,
handler: handler,
queryConversionHandler: queryConversionHandler,
}
}

func parseAsQueryRequest(req *ConversionRequest) ([]*QueryDataRequest, error) {
var requests []*QueryDataRequest
for _, obj := range req.Objects {
if obj.ContentType != "application/json" {
return nil, fmt.Errorf("unsupported content type %s", obj.ContentType)
}
input := &QueryDataRequest{}
err := json.Unmarshal(obj.Raw, input)
if err != nil {
return nil, fmt.Errorf("unmarshal: %w", err)
}
input.PluginContext = req.PluginContext
requests = append(requests, input)
}
return requests, nil
}

func (a *conversionSDKAdapter) convertQueryDataRequest(ctx context.Context, requests []*QueryDataRequest) (*ConversionResponse, error) {
resp := &ConversionResponse{}
queries := []any{}
for _, req := range requests {
res, err := a.queryConversionHandler.ConvertQueryDataRequest(ctx, req)
if err != nil {
return nil, err
}
// Queries are flattened into a single array
queries = append(queries, res.Queries...)
}

for _, req := range queries {
newJSON, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal: %w", err)
}
resp.Objects = append(resp.Objects, RawObject{
Raw: newJSON,
ContentType: "application/json",
})
}
return resp, nil
}

func (a *conversionSDKAdapter) ConvertObjects(ctx context.Context, req *pluginv2.ConversionRequest) (*pluginv2.ConversionResponse, error) {
ctx = setupContext(ctx, EndpointConvertObject)
parsedReq := FromProto().ConversionRequest(req)

var resp *ConversionResponse
resp := &ConversionResponse{}
err := wrapHandler(ctx, parsedReq.PluginContext, func(ctx context.Context) (RequestStatus, error) {
var innerErr error
if a.queryConversionHandler != nil {
// Try to parse it as a query data request
reqs, err := parseAsQueryRequest(parsedReq)
if err == nil {
resp, innerErr = a.convertQueryDataRequest(ctx, reqs)
return RequestStatusFromError(innerErr), innerErr
}
// The object cannot be parsed as a query data request, so we will try to convert it as a generic object
}
resp, innerErr = a.handler.ConvertObjects(ctx, parsedReq)
return RequestStatusFromError(innerErr), innerErr
})
Expand Down
48 changes: 48 additions & 0 deletions backend/conversion_adapter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package backend

import (
"context"
"testing"

"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
"github.com/stretchr/testify/require"
)

func TestConvertObjects(t *testing.T) {
t.Run("converts a raw object", func(t *testing.T) {
input := []byte(`{"foo":"bar"}`)
expected := []byte(`{"baz":"qux"}`)

a := newConversionSDKAdapter(ConvertObjectsFunc(func(_ context.Context, r *ConversionRequest) (*ConversionResponse, error) {
require.Equal(t, input, r.Objects[0].Raw)
return &ConversionResponse{
Objects: []RawObject{{Raw: expected}},
}, nil
}), nil)
res, err := a.ConvertObjects(context.Background(), &pluginv2.ConversionRequest{
PluginContext: &pluginv2.PluginContext{},
TargetVersion: &pluginv2.GroupVersion{},
Objects: []*pluginv2.RawObject{{Raw: input}},
})
require.NoError(t, err)
require.Equal(t, []*pluginv2.RawObject{{Raw: expected}}, res.Objects)
})

t.Run("converts a query data request", func(t *testing.T) {
input := []byte(`{"queries":[{"JSON":"foo"}]}`)

a := newConversionSDKAdapter(nil, ConvertQueryFunc(func(_ context.Context, r *QueryDataRequest) (*QueryConversionResponse, error) {
require.Equal(t, `"foo"`, string(r.Queries[0].JSON))
return &QueryConversionResponse{
Queries: []any{DataQuery{JSON: []byte(`"bar"`)}},
}, nil
}))
res, err := a.ConvertObjects(context.Background(), &pluginv2.ConversionRequest{
PluginContext: &pluginv2.PluginContext{},
TargetVersion: &pluginv2.GroupVersion{},
Objects: []*pluginv2.RawObject{{Raw: input, ContentType: "application/json"}},
})
require.NoError(t, err)
require.Contains(t, string(res.Objects[0].Raw), `"JSON":"bar"`)
})
}
14 changes: 8 additions & 6 deletions backend/datasource/manage.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@ func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpt
}
handler := automanagement.NewManager(NewInstanceManager(instanceFactory))
return backend.Manage(pluginID, backend.ServeOpts{
CheckHealthHandler: handler,
CallResourceHandler: handler,
QueryDataHandler: handler,
StreamHandler: handler,
AdmissionHandler: opts.AdmissionHandler,
GRPCSettings: opts.GRPCSettings,
CheckHealthHandler: handler,
CallResourceHandler: handler,
QueryDataHandler: handler,
StreamHandler: handler,
QueryConversionHandler: handler,
AdmissionHandler: opts.AdmissionHandler,
GRPCSettings: opts.GRPCSettings,
ConversionHandler: opts.ConversionHandler,
})
}
24 changes: 24 additions & 0 deletions backend/query_conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package backend

import "context"

// QueryConversionHandler is an EXPERIMENTAL service that allows converting queries between versions

type QueryConversionHandler interface {
// ConvertQuery is called to covert queries between different versions
ConvertQueryDataRequest(ctx context.Context, req *QueryDataRequest) (*QueryConversionResponse, error)
}

type ConvertQueryFunc func(ctx context.Context, req *QueryDataRequest) (*QueryConversionResponse, error)

func (fn ConvertQueryFunc) ConvertQueryDataRequest(ctx context.Context, req *QueryDataRequest) (*QueryConversionResponse, error) {
return fn(ctx, req)
}

type QueryConversionResponse struct {
// Converted queries. It should extend v0alpha1.Query
Queries []any
// Result contains extra details into why an conversion request was denied.
// +optional
Result *StatusResult
}
8 changes: 6 additions & 2 deletions backend/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ type ServeOpts struct {
// This is EXPERIMENTAL and is a subject to change till Grafana 12
ConversionHandler ConversionHandler

// QueryConversionHandler converts queries between versions
// This is EXPERIMENTAL and is a subject to change till Grafana 12
QueryConversionHandler QueryConversionHandler

// GRPCSettings settings for gPRC.
GRPCSettings GRPCSettings
}
Expand All @@ -90,8 +94,8 @@ func GRPCServeOpts(opts ServeOpts) grpcplugin.ServeOpts {
pluginOpts.AdmissionServer = newAdmissionSDKAdapter(opts.AdmissionHandler)
}

if opts.ConversionHandler != nil {
pluginOpts.ConversionServer = newConversionSDKAdapter(opts.ConversionHandler)
if opts.ConversionHandler != nil || opts.QueryConversionHandler != nil {
pluginOpts.ConversionServer = newConversionSDKAdapter(opts.ConversionHandler, opts.QueryConversionHandler)
}
return pluginOpts
}
Expand Down
11 changes: 11 additions & 0 deletions internal/automanagement/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,14 @@ func (m *Manager) RunStream(ctx context.Context, req *backend.RunStreamRequest,
}
return status.Error(codes.Unimplemented, "unimplemented")
}

func (m *Manager) ConvertQueryDataRequest(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryConversionResponse, error) {
h, err := m.Get(ctx, req.PluginContext)
if err != nil {
return nil, err
}
if ds, ok := h.(backend.QueryConversionHandler); ok {
return ds.ConvertQueryDataRequest(ctx, req)
}
return nil, status.Error(codes.Unimplemented, "unimplemented")
}

0 comments on commit 97a7441

Please sign in to comment.