mirror of https://github.com/grafana/grafana
Peakq: move templates into query service (#82193)
parent
a439ee46bf
commit
ac5a387086
@ -1,2 +0,0 @@ |
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,QueryTemplateSpec,Variables |
||||
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,TemplateVariable,DefaultValues |
@ -0,0 +1,6 @@ |
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:openapi-gen=true
|
||||
// +k8s:defaulter-gen=TypeMeta
|
||||
// +groupName=query.grafana.app
|
||||
|
||||
package template // import "github.com/grafana/grafana/pkg/apis/query/v0alpha1/template"
|
@ -0,0 +1,81 @@ |
||||
package template |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestFormat(t *testing.T) { |
||||
// Invalid input
|
||||
require.Equal(t, "", FormatVariables(FormatCSV, nil)) |
||||
require.Equal(t, "", FormatVariables(FormatCSV, []string{})) |
||||
|
||||
type check struct { |
||||
name string |
||||
input []string |
||||
output map[VariableFormat]string |
||||
} |
||||
|
||||
tests := []check{ |
||||
{ |
||||
name: "three simple variables", |
||||
input: []string{"a", "b", "c"}, |
||||
output: map[VariableFormat]string{ |
||||
FormatCSV: "a,b,c", |
||||
FormatJSON: `["a","b","c"]`, |
||||
FormatDoubleQuote: `"a","b","c"`, |
||||
FormatSingleQuote: `'a','b','c'`, |
||||
FormatPipe: `a|b|c`, |
||||
FormatRaw: "a,b,c", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "single value", |
||||
input: []string{"a"}, |
||||
output: map[VariableFormat]string{ |
||||
FormatCSV: "a", |
||||
FormatJSON: `["a"]`, |
||||
FormatDoubleQuote: `"a"`, |
||||
FormatSingleQuote: `'a'`, |
||||
FormatPipe: "a", |
||||
FormatRaw: "a", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "value with quote", |
||||
input: []string{`hello "world"`}, |
||||
output: map[VariableFormat]string{ |
||||
FormatCSV: `"hello ""world"""`, // note the double quotes
|
||||
FormatJSON: `["hello \"world\""]`, |
||||
FormatDoubleQuote: `"hello \"world\""`, |
||||
FormatSingleQuote: `'hello "world"'`, |
||||
FormatPipe: `hello "world"`, |
||||
FormatRaw: `hello "world"`, |
||||
}, |
||||
}, |
||||
} |
||||
for _, test := range tests { |
||||
// Make sure all keys are set in tests
|
||||
all := map[VariableFormat]bool{ |
||||
FormatRaw: true, |
||||
FormatCSV: true, |
||||
FormatJSON: true, |
||||
FormatDoubleQuote: true, |
||||
FormatSingleQuote: true, |
||||
FormatPipe: true, |
||||
} |
||||
|
||||
// Check the default (no format) matches CSV
|
||||
require.Equal(t, test.output[FormatRaw], |
||||
FormatVariables("", test.input), |
||||
"test %s default values are not raw", test.name) |
||||
|
||||
// Check each input value
|
||||
for format, v := range test.output { |
||||
require.Equal(t, v, FormatVariables(format, test.input), "Test: %s (format:%s)", test.name, format) |
||||
delete(all, format) |
||||
} |
||||
require.Empty(t, all, "test %s is missing cases for: %v", test.name, all) |
||||
} |
||||
} |
@ -0,0 +1,116 @@ |
||||
package template |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sort" |
||||
|
||||
"github.com/spyzhov/ajson" |
||||
|
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" |
||||
) |
||||
|
||||
// RenderTemplate applies selected values into a query template
|
||||
func RenderTemplate(qt QueryTemplate, selectedValues map[string][]string) ([]Target, error) { |
||||
targets := qt.DeepCopy().Targets |
||||
|
||||
rawTargetObjects := make([]*ajson.Node, len(qt.Targets)) |
||||
for i, t := range qt.Targets { |
||||
b, err := t.Properties.MarshalJSON() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
rawTargetObjects[i], err = ajson.Unmarshal(b) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
rm := getReplacementMap(qt) |
||||
for targetIdx, byTargetIdx := range rm { |
||||
for path, reps := range byTargetIdx { |
||||
o := rawTargetObjects[targetIdx] |
||||
nodes, err := o.JSONPath(path) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("failed to find path %v: %w", path, err) |
||||
} |
||||
if len(nodes) != 1 { |
||||
return nil, fmt.Errorf("expected one lead node at path %v but got %v", path, len(nodes)) |
||||
} |
||||
n := nodes[0] |
||||
if !n.IsString() { |
||||
return nil, fmt.Errorf("only string type leaf notes supported currently, %v is not a string", path) |
||||
} |
||||
s := []rune(n.String()) |
||||
s = s[1 : len(s)-1] |
||||
var offSet int64 |
||||
for _, r := range reps { |
||||
value := []rune(FormatVariables(r.format, selectedValues[r.Key])) |
||||
if r.Position == nil { |
||||
return nil, fmt.Errorf("nil position not support yet, will be full replacement") |
||||
} |
||||
s = append(s[:r.Start+offSet], append(value, s[r.End+offSet:]...)...) |
||||
offSet += int64(len(value)) - (r.End - r.Start) |
||||
} |
||||
if err = n.SetString(string(s)); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
} |
||||
|
||||
for i, aT := range rawTargetObjects { |
||||
raw, err := ajson.Marshal(aT) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
u := query.GenericDataQuery{} |
||||
err = u.UnmarshalJSON(raw) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
targets[i].Properties = u |
||||
} |
||||
|
||||
return targets, nil |
||||
} |
||||
|
||||
type replacement struct { |
||||
*Position |
||||
*TemplateVariable |
||||
format VariableFormat |
||||
} |
||||
|
||||
func getReplacementMap(qt QueryTemplate) map[int]map[string][]replacement { |
||||
byTargetPath := make(map[int]map[string][]replacement) |
||||
|
||||
varMap := make(map[string]*TemplateVariable, len(qt.Variables)) |
||||
for i, v := range qt.Variables { |
||||
varMap[v.Key] = &qt.Variables[i] |
||||
} |
||||
|
||||
for i, target := range qt.Targets { |
||||
if byTargetPath[i] == nil { |
||||
byTargetPath[i] = make(map[string][]replacement) |
||||
} |
||||
for k, vReps := range target.Variables { |
||||
for rI, rep := range vReps { |
||||
byTargetPath[i][rep.Path] = append(byTargetPath[i][rep.Path], |
||||
replacement{ |
||||
Position: vReps[rI].Position, |
||||
TemplateVariable: varMap[k], |
||||
format: rep.Format, |
||||
}, |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
for idx, byTargetIdx := range byTargetPath { |
||||
for path := range byTargetIdx { |
||||
sort.Slice(byTargetPath[idx][path], func(i, j int) bool { |
||||
return byTargetPath[idx][path][i].Start < byTargetPath[idx][path][j].Start |
||||
}) |
||||
} |
||||
} |
||||
|
||||
return byTargetPath |
||||
} |
@ -0,0 +1,112 @@ |
||||
package template |
||||
|
||||
import ( |
||||
"github.com/grafana/grafana-plugin-sdk-go/data" |
||||
|
||||
common "github.com/grafana/grafana/pkg/apis/common/v0alpha1" |
||||
query "github.com/grafana/grafana/pkg/apis/query/v0alpha1" |
||||
) |
||||
|
||||
type QueryTemplate struct { |
||||
// A display name
|
||||
Title string `json:"title,omitempty"` |
||||
|
||||
// Longer description for why it is interesting
|
||||
Description string `json:"description,omitempty"` |
||||
|
||||
// The variables that can be used to render
|
||||
// +listType=map
|
||||
// +listMapKey=key
|
||||
Variables []TemplateVariable `json:"vars,omitempty"` |
||||
|
||||
// Output variables
|
||||
// +listType=set
|
||||
Targets []Target `json:"targets"` |
||||
} |
||||
|
||||
type Target struct { |
||||
// DataType is the returned Dataplane type from the query.
|
||||
DataType data.FrameType `json:"dataType,omitempty"` |
||||
|
||||
// DataTypeVersion is the version for the Dataplane type.
|
||||
// TODO 2[uint] seems to panic, maybe implement DeepCopy on data.FrameTypeVersion?
|
||||
// DataTypeVersion data.FrameTypeVersion `json:"dataTypeVersion,omitempty"`
|
||||
|
||||
// Variables that will be replaced in the query
|
||||
Variables map[string][]VariableReplacement `json:"variables"` |
||||
|
||||
// Query target
|
||||
Properties query.GenericDataQuery `json:"properties"` |
||||
} |
||||
|
||||
// TemplateVariable is the definition of a variable that will be interpolated
|
||||
// in targets.
|
||||
type TemplateVariable struct { |
||||
// Key is the name of the variable.
|
||||
Key string `json:"key"` |
||||
|
||||
// DefaultValue is the value to be used when there is no selected value
|
||||
// during render.
|
||||
// +listType=atomic
|
||||
DefaultValues []string `json:"defaultValues,omitempty"` |
||||
|
||||
// ValueListDefinition is the object definition used by the FE
|
||||
// to get a list of possible values to select for render.
|
||||
ValueListDefinition common.Unstructured `json:"valueListDefinition,omitempty"` |
||||
} |
||||
|
||||
// QueryVariable is the definition of a variable that will be interpolated
|
||||
// in targets.
|
||||
type VariableReplacement struct { |
||||
// Path is the location of the property within a target.
|
||||
// The format for this is not figured out yet (Maybe JSONPath?).
|
||||
// Idea: ["string", int, "string"] where int indicates array offset
|
||||
Path string `json:"path"` |
||||
|
||||
// Positions is a list of where to perform the interpolation
|
||||
// within targets during render.
|
||||
// The first string is the Idx of the target as a string, since openAPI
|
||||
// does not support ints as map keys
|
||||
Position *Position `json:"position,omitempty"` |
||||
|
||||
// How values should be interpolated
|
||||
Format VariableFormat `json:"format,omitempty"` |
||||
} |
||||
|
||||
// Define how to format values in the template.
|
||||
// See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options
|
||||
// +enum
|
||||
type VariableFormat string |
||||
|
||||
// Defines values for ItemType.
|
||||
const ( |
||||
// Formats variables with multiple values as a comma-separated string.
|
||||
FormatCSV VariableFormat = "csv" |
||||
|
||||
// Formats variables with multiple values as a comma-separated string.
|
||||
FormatJSON VariableFormat = "json" |
||||
|
||||
// Formats single- and multi-valued variables into a comma-separated string
|
||||
FormatDoubleQuote VariableFormat = "doublequote" |
||||
|
||||
// Formats single- and multi-valued variables into a comma-separated string
|
||||
FormatSingleQuote VariableFormat = "singlequote" |
||||
|
||||
// Formats variables with multiple values into a pipe-separated string.
|
||||
FormatPipe VariableFormat = "pipe" |
||||
|
||||
// Formats variables with multiple values into comma-separated string.
|
||||
// This is the default behavior when no format is specified
|
||||
FormatRaw VariableFormat = "raw" |
||||
) |
||||
|
||||
// Position is where to do replacement in the targets
|
||||
// during render.
|
||||
type Position struct { |
||||
// Start is the byte offset within TargetKey's property of the variable.
|
||||
// It is the start location for replacements).
|
||||
Start int64 `json:"start"` // TODO: byte, rune?
|
||||
|
||||
// End is the byte offset of the end of the variable.
|
||||
End int64 `json:"end"` |
||||
} |
@ -0,0 +1,131 @@ |
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package template |
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Position) DeepCopyInto(out *Position) { |
||||
*out = *in |
||||
return |
||||
} |
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Position.
|
||||
func (in *Position) DeepCopy() *Position { |
||||
if in == nil { |
||||
return nil |
||||
} |
||||
out := new(Position) |
||||
in.DeepCopyInto(out) |
||||
return out |
||||
} |
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *QueryTemplate) DeepCopyInto(out *QueryTemplate) { |
||||
*out = *in |
||||
if in.Variables != nil { |
||||
in, out := &in.Variables, &out.Variables |
||||
*out = make([]TemplateVariable, len(*in)) |
||||
for i := range *in { |
||||
(*in)[i].DeepCopyInto(&(*out)[i]) |
||||
} |
||||
} |
||||
if in.Targets != nil { |
||||
in, out := &in.Targets, &out.Targets |
||||
*out = make([]Target, len(*in)) |
||||
for i := range *in { |
||||
(*in)[i].DeepCopyInto(&(*out)[i]) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QueryTemplate.
|
||||
func (in *QueryTemplate) DeepCopy() *QueryTemplate { |
||||
if in == nil { |
||||
return nil |
||||
} |
||||
out := new(QueryTemplate) |
||||
in.DeepCopyInto(out) |
||||
return out |
||||
} |
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Target) DeepCopyInto(out *Target) { |
||||
*out = *in |
||||
if in.Variables != nil { |
||||
in, out := &in.Variables, &out.Variables |
||||
*out = make(map[string][]VariableReplacement, len(*in)) |
||||
for key, val := range *in { |
||||
var outVal []VariableReplacement |
||||
if val == nil { |
||||
(*out)[key] = nil |
||||
} else { |
||||
in, out := &val, &outVal |
||||
*out = make([]VariableReplacement, len(*in)) |
||||
for i := range *in { |
||||
(*in)[i].DeepCopyInto(&(*out)[i]) |
||||
} |
||||
} |
||||
(*out)[key] = outVal |
||||
} |
||||
} |
||||
in.Properties.DeepCopyInto(&out.Properties) |
||||
return |
||||
} |
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Target.
|
||||
func (in *Target) DeepCopy() *Target { |
||||
if in == nil { |
||||
return nil |
||||
} |
||||
out := new(Target) |
||||
in.DeepCopyInto(out) |
||||
return out |
||||
} |
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TemplateVariable) DeepCopyInto(out *TemplateVariable) { |
||||
*out = *in |
||||
if in.DefaultValues != nil { |
||||
in, out := &in.DefaultValues, &out.DefaultValues |
||||
*out = make([]string, len(*in)) |
||||
copy(*out, *in) |
||||
} |
||||
in.ValueListDefinition.DeepCopyInto(&out.ValueListDefinition) |
||||
return |
||||
} |
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateVariable.
|
||||
func (in *TemplateVariable) DeepCopy() *TemplateVariable { |
||||
if in == nil { |
||||
return nil |
||||
} |
||||
out := new(TemplateVariable) |
||||
in.DeepCopyInto(out) |
||||
return out |
||||
} |
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *VariableReplacement) DeepCopyInto(out *VariableReplacement) { |
||||
*out = *in |
||||
if in.Position != nil { |
||||
in, out := &in.Position, &out.Position |
||||
*out = new(Position) |
||||
**out = **in |
||||
} |
||||
return |
||||
} |
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VariableReplacement.
|
||||
func (in *VariableReplacement) DeepCopy() *VariableReplacement { |
||||
if in == nil { |
||||
return nil |
||||
} |
||||
out := new(VariableReplacement) |
||||
in.DeepCopyInto(out) |
||||
return out |
||||
} |
@ -0,0 +1,19 @@ |
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Code generated by defaulter-gen. DO NOT EDIT.
|
||||
|
||||
package template |
||||
|
||||
import ( |
||||
runtime "k8s.io/apimachinery/pkg/runtime" |
||||
) |
||||
|
||||
// RegisterDefaults adds defaulters functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
// All generated defaulters are covering - they call all nested defaulters.
|
||||
func RegisterDefaults(scheme *runtime.Scheme) error { |
||||
return nil |
||||
} |
@ -1,83 +0,0 @@ |
||||
package peakq |
||||
|
||||
import ( |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
|
||||
peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" |
||||
) |
||||
|
||||
func TestFormat(t *testing.T) { |
||||
// Invalid input
|
||||
require.Equal(t, "", formatVariables(peakq.FormatCSV, nil)) |
||||
require.Equal(t, "", formatVariables(peakq.FormatCSV, []string{})) |
||||
|
||||
type check struct { |
||||
name string |
||||
input []string |
||||
output map[peakq.VariableFormat]string |
||||
} |
||||
|
||||
tests := []check{ |
||||
{ |
||||
name: "three simple variables", |
||||
input: []string{"a", "b", "c"}, |
||||
output: map[peakq.VariableFormat]string{ |
||||
peakq.FormatCSV: "a,b,c", |
||||
peakq.FormatJSON: `["a","b","c"]`, |
||||
peakq.FormatDoubleQuote: `"a","b","c"`, |
||||
peakq.FormatSingleQuote: `'a','b','c'`, |
||||
peakq.FormatPipe: `a|b|c`, |
||||
peakq.FormatRaw: "a,b,c", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "single value", |
||||
input: []string{"a"}, |
||||
output: map[peakq.VariableFormat]string{ |
||||
peakq.FormatCSV: "a", |
||||
peakq.FormatJSON: `["a"]`, |
||||
peakq.FormatDoubleQuote: `"a"`, |
||||
peakq.FormatSingleQuote: `'a'`, |
||||
peakq.FormatPipe: "a", |
||||
peakq.FormatRaw: "a", |
||||
}, |
||||
}, |
||||
{ |
||||
name: "value with quote", |
||||
input: []string{`hello "world"`}, |
||||
output: map[peakq.VariableFormat]string{ |
||||
peakq.FormatCSV: `"hello ""world"""`, // note the double quotes
|
||||
peakq.FormatJSON: `["hello \"world\""]`, |
||||
peakq.FormatDoubleQuote: `"hello \"world\""`, |
||||
peakq.FormatSingleQuote: `'hello "world"'`, |
||||
peakq.FormatPipe: `hello "world"`, |
||||
peakq.FormatRaw: `hello "world"`, |
||||
}, |
||||
}, |
||||
} |
||||
for _, test := range tests { |
||||
// Make sure all keys are set in tests
|
||||
all := map[peakq.VariableFormat]bool{ |
||||
peakq.FormatRaw: true, |
||||
peakq.FormatCSV: true, |
||||
peakq.FormatJSON: true, |
||||
peakq.FormatDoubleQuote: true, |
||||
peakq.FormatSingleQuote: true, |
||||
peakq.FormatPipe: true, |
||||
} |
||||
|
||||
// Check the default (no format) matches CSV
|
||||
require.Equal(t, test.output[peakq.FormatRaw], |
||||
formatVariables("", test.input), |
||||
"test %s default values are not raw", test.name) |
||||
|
||||
// Check each input value
|
||||
for format, v := range test.output { |
||||
require.Equal(t, v, formatVariables(format, test.input), "Test: %s (format:%s)", test.name, format) |
||||
delete(all, format) |
||||
} |
||||
require.Empty(t, all, "test %s is missing cases for: %v", test.name, all) |
||||
} |
||||
} |
Loading…
Reference in new issue