Peakq: support multi-value template variables (#82036)

This commit is contained in:
Ryan McKinley 2024-02-07 08:46:43 -08:00 committed by GitHub
parent e77ec26897
commit 3464b6e581
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 187 additions and 111 deletions

View File

@ -75,24 +75,35 @@ type VariableReplacement struct {
Position *Position `json:"position,omitempty"` Position *Position `json:"position,omitempty"`
// How values should be interpolated // How values should be interpolated
// See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options Format VariableFormat `json:"format,omitempty"`
// NOTE: the format parameter is not yet supported!
Format string `json:"format,omitempty"`
// Keep track of the values from previous iterations
History []ReplacementHistory `json:"history,omitempty"`
} }
type ReplacementHistory struct { // Define how to format values in the template.
// Who/what made the change // See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options
Source string `json:"source,omitempty"` // +enum
type VariableFormat string
// Value before replacement // Defines values for ItemType.
Previous string `json:"previous"` const (
// Formats variables with multiple values as a comma-separated string.
FormatCSV VariableFormat = "csv"
// The value(s) that replaced the section // Formats variables with multiple values as a comma-separated string.
Replacement []string `json:"replacement"` 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 // Position is where to do replacement in the targets
// during render. // during render.

View File

@ -149,27 +149,6 @@ func (in *RenderedQuery) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ReplacementHistory) DeepCopyInto(out *ReplacementHistory) {
*out = *in
if in.Replacement != nil {
in, out := &in.Replacement, &out.Replacement
*out = make([]string, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplacementHistory.
func (in *ReplacementHistory) DeepCopy() *ReplacementHistory {
if in == nil {
return nil
}
out := new(ReplacementHistory)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Target) DeepCopyInto(out *Target) { func (in *Target) DeepCopyInto(out *Target) {
*out = *in *out = *in
@ -234,13 +213,6 @@ func (in *VariableReplacement) DeepCopyInto(out *VariableReplacement) {
*out = new(Position) *out = new(Position)
**out = **in **out = **in
} }
if in.History != nil {
in, out := &in.History, &out.History
*out = make([]ReplacementHistory, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return return
} }

View File

@ -21,7 +21,6 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateList": schema_pkg_apis_peakq_v0alpha1_QueryTemplateList(ref), "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateList": schema_pkg_apis_peakq_v0alpha1_QueryTemplateList(ref),
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec": schema_pkg_apis_peakq_v0alpha1_QueryTemplateSpec(ref), "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.QueryTemplateSpec": schema_pkg_apis_peakq_v0alpha1_QueryTemplateSpec(ref),
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.RenderedQuery": schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref), "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.RenderedQuery": schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref),
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory": schema_pkg_apis_peakq_v0alpha1_ReplacementHistory(ref),
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target": schema_pkg_apis_peakq_v0alpha1_Target(ref), "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Target": schema_pkg_apis_peakq_v0alpha1_Target(ref),
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.TemplateVariable": schema_pkg_apis_peakq_v0alpha1_TemplateVariable(ref), "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.TemplateVariable": schema_pkg_apis_peakq_v0alpha1_TemplateVariable(ref),
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.VariableReplacement": schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref), "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.VariableReplacement": schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref),
@ -255,49 +254,6 @@ func schema_pkg_apis_peakq_v0alpha1_RenderedQuery(ref common.ReferenceCallback)
} }
} }
func schema_pkg_apis_peakq_v0alpha1_ReplacementHistory(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
SchemaProps: spec.SchemaProps{
Type: []string{"object"},
Properties: map[string]spec.Schema{
"source": {
SchemaProps: spec.SchemaProps{
Description: "Who/what made the change",
Type: []string{"string"},
Format: "",
},
},
"previous": {
SchemaProps: spec.SchemaProps{
Description: "Value before replacement",
Default: "",
Type: []string{"string"},
Format: "",
},
},
"replacement": {
SchemaProps: spec.SchemaProps{
Description: "The value(s) that replaced the section",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
},
Required: []string{"previous", "replacement"},
},
},
}
}
func schema_pkg_apis_peakq_v0alpha1_Target(ref common.ReferenceCallback) common.OpenAPIDefinition { func schema_pkg_apis_peakq_v0alpha1_Target(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{ return common.OpenAPIDefinition{
Schema: spec.Schema{ Schema: spec.Schema{
@ -421,23 +377,10 @@ func schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref common.ReferenceCall
}, },
"format": { "format": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "How values should be interpolated See: https://grafana.com/docs/grafana/latest/dashboards/variables/variable-syntax/#advanced-variable-format-options NOTE: the format parameter is not yet supported!", Description: "How values should be interpolated See: NOTE: the format parameter is not yet supported!\n\nPossible enum values:\n - `\"csv\"` Formats variables with multiple values as a comma-separated string.\n - `\"doublequote\"` Formats single- and multi-valued variables into a comma-separated string\n - `\"json\"` Formats variables with multiple values as a comma-separated string.\n - `\"pipe\"` Formats variables with multiple values into a pipe-separated string.\n - `\"raw\"` Formats variables with multiple values into comma-separated string. This is the default behavior when no format is specified\n - `\"singlequote\"` Formats single- and multi-valued variables into a comma-separated string",
Type: []string{"string"}, Type: []string{"string"},
Format: "", Format: "",
}, Enum: []interface{}{"csv", "doublequote", "json", "pipe", "raw", "singlequote"},
},
"history": {
SchemaProps: spec.SchemaProps{
Description: "Keep track of the values from previous iterations",
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: map[string]interface{}{},
Ref: ref("github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory"),
},
},
},
}, },
}, },
}, },
@ -445,6 +388,6 @@ func schema_pkg_apis_peakq_v0alpha1_VariableReplacement(ref common.ReferenceCall
}, },
}, },
Dependencies: []string{ Dependencies: []string{
"github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Position", "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.ReplacementHistory"}, "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1.Position"},
} }
} }

View File

@ -1,4 +1,2 @@
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,ReplacementHistory,Replacement
API rule violation: list_type_missing,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,VariableReplacement,History
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,QueryTemplateSpec,Variables
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,TemplateVariable,DefaultValues API rule violation: names_match,github.com/grafana/grafana/pkg/apis/peakq/v0alpha1,TemplateVariable,DefaultValues

View File

@ -0,0 +1,70 @@
package peakq
import (
"bytes"
"encoding/csv"
"encoding/json"
"strings"
peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1"
)
func formatVariables(fmt peakq.VariableFormat, input []string) string {
if len(input) < 1 {
return ""
}
// MultiValued formats
// nolint: exhaustive
switch fmt {
case peakq.FormatJSON:
v, _ := json.Marshal(input)
return string(v)
case peakq.FormatDoubleQuote:
sb := bytes.NewBufferString("")
for idx, val := range input {
if idx > 0 {
_, _ = sb.WriteRune(',')
}
_, _ = sb.WriteRune('"')
_, _ = sb.WriteString(strings.ReplaceAll(val, `"`, `\"`))
_, _ = sb.WriteRune('"')
}
return sb.String()
case peakq.FormatSingleQuote:
sb := bytes.NewBufferString("")
for idx, val := range input {
if idx > 0 {
_, _ = sb.WriteRune(',')
}
_, _ = sb.WriteRune('\'')
_, _ = sb.WriteString(strings.ReplaceAll(val, `'`, `\'`))
_, _ = sb.WriteRune('\'')
}
return sb.String()
case peakq.FormatCSV:
sb := bytes.NewBufferString("")
w := csv.NewWriter(sb)
_ = w.Write(input)
w.Flush()
v := sb.Bytes()
return string(v[:len(v)-1])
}
// Single valued formats
if len(input) == 1 {
return input[0]
}
// nolint: exhaustive
switch fmt {
case peakq.FormatPipe:
return strings.Join(input, "|")
}
// Raw output (joined with a comma)
return strings.Join(input, ",")
}

View File

@ -0,0 +1,83 @@
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)
}
}

View File

@ -9,13 +9,13 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/spyzhov/ajson"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
"github.com/grafana/grafana/pkg/apis/common/v0alpha1" common "github.com/grafana/grafana/pkg/apis/common/v0alpha1"
peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1" peakq "github.com/grafana/grafana/pkg/apis/peakq/v0alpha1"
"github.com/spyzhov/ajson"
) )
type renderREST struct { type renderREST struct {
@ -107,6 +107,7 @@ func makeVarMapFromParams(v url.Values) (map[string][]string, error) {
type replacement struct { type replacement struct {
*peakq.Position *peakq.Position
*peakq.TemplateVariable *peakq.TemplateVariable
format peakq.VariableFormat
} }
func getReplacementMap(qt peakq.QueryTemplateSpec) map[int]map[string][]replacement { func getReplacementMap(qt peakq.QueryTemplateSpec) map[int]map[string][]replacement {
@ -127,6 +128,7 @@ func getReplacementMap(qt peakq.QueryTemplateSpec) map[int]map[string][]replacem
replacement{ replacement{
Position: vReps[rI].Position, Position: vReps[rI].Position,
TemplateVariable: varMap[k], TemplateVariable: varMap[k],
format: rep.Format,
}, },
) )
} }
@ -178,13 +180,10 @@ func Render(qt peakq.QueryTemplateSpec, selectedValues map[string][]string) (*pe
s = s[1 : len(s)-1] s = s[1 : len(s)-1]
var offSet int64 var offSet int64
for _, r := range reps { for _, r := range reps {
value := []rune(formatVariables(r.format, selectedValues[r.Key]))
if r.Position == nil { if r.Position == nil {
return nil, fmt.Errorf("nil position not support yet, will be full replacement") return nil, fmt.Errorf("nil position not support yet, will be full replacement")
} }
if len(selectedValues[r.Key]) != 1 {
return nil, fmt.Errorf("selected value missing, or more then one provided")
}
value := []rune(selectedValues[r.Key][0])
s = append(s[:r.Start+offSet], append(value, s[r.End+offSet:]...)...) s = append(s[:r.Start+offSet], append(value, s[r.End+offSet:]...)...)
offSet += int64(len(value)) - (r.End - r.Start) offSet += int64(len(value)) - (r.End - r.Start)
} }
@ -199,7 +198,7 @@ func Render(qt peakq.QueryTemplateSpec, selectedValues map[string][]string) (*pe
if err != nil { if err != nil {
return nil, err return nil, err
} }
u := v0alpha1.Unstructured{} u := common.Unstructured{}
err = u.UnmarshalJSON(raw) err = u.UnmarshalJSON(raw)
if err != nil { if err != nil {
return nil, err return nil, err