feat(pkger): add parameterization to dashboard queries

references: #18237
pull/19198/head
Johnny Steenbergen 2020-07-30 11:26:17 -07:00 committed by Johnny Steenbergen
parent a61161d73b
commit 41cb12aeec
11 changed files with 787 additions and 66 deletions

View File

@ -2305,7 +2305,7 @@ func TestLauncher_Pkger(t *testing.T) {
})
})
t.Run("errors incurred during application of package rolls back to state before package", func(t *testing.T) {
t.Run("errors incurred during application of template rolls back to state before template", func(t *testing.T) {
stacks, err := svc.ListStacks(ctx, l.Org.ID, pkger.ListFilter{})
require.NoError(t, err)
require.Empty(t, stacks)
@ -2560,24 +2560,21 @@ spec:
t.Run("dashboards", func(t *testing.T) {
newQuery := func() influxdb.DashboardQuery {
q := influxdb.DashboardQuery{
return influxdb.DashboardQuery{
BuilderConfig: influxdb.BuilderConfig{
Buckets: []string{},
Tags: nil,
Tags: []struct {
Key string `json:"key"`
Values []string `json:"values"`
AggregateFunctionType string `json:"aggregateFunctionType"`
}{},
Functions: []struct {
Name string `json:"name"`
}{},
AggregateWindow: struct {
Period string `json:"period"`
FillValues bool `json:"fillValues"`
}{},
},
Text: "from(v.bucket) |> count()",
EditMode: "advanced",
}
// TODO: remove this when issue that forced the builder tag to be here to render in UI.
q.BuilderConfig.Tags = append(q.BuilderConfig.Tags, influxdb.NewBuilderTag("_measurement", "filter", ""))
return q
}
newAxes := func() map[string]influxdb.Axis {
@ -3236,7 +3233,7 @@ spec:
})
t.Run("pkg with same bkt-var-label does nto create new resources for them", func(t *testing.T) {
// validate the new package doesn't create new resources for bkts/labels/vars
// validate the new template doesn't create new resources for bkts/labels/vars
// since names collide.
impact, err := svc.Apply(ctx, l.Org.ID, l.User.ID, pkger.ApplyWithTemplate(newCompletePkg(t)))
require.NoError(t, err)
@ -3435,7 +3432,7 @@ spec:
}, varArgs.Values)
})
t.Run("error incurs during package application when resources already exist rollsback to prev state", func(t *testing.T) {
t.Run("error incurs during template application when resources already exist rollsback to prev state", func(t *testing.T) {
updatePkg, err := pkger.Parse(pkger.EncodingYAML, pkger.FromString(updatePkgYMLStr))
require.NoError(t, err)
@ -3589,7 +3586,7 @@ spec:
assert.Equal(t, influxdb.ID(impact.Summary.Buckets[0].ID), ev.Resources[0].ID)
})
t.Run("apply a package with env refs", func(t *testing.T) {
t.Run("apply a template with env refs", func(t *testing.T) {
pkgStr := fmt.Sprintf(`
apiVersion: %[1]s
kind: Bucket
@ -3763,6 +3760,277 @@ spec:
assert.Equal(t, "var_threeve", sum.Variables[0].Name)
assert.Empty(t, sum.MissingEnvs)
})
t.Run("apply a template with query refs", func(t *testing.T) {
dashName := "dash-1"
newDashTmpl := func(t *testing.T) *pkger.Template {
t.Helper()
tmplStr := `
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
name: %s
spec:
charts:
- kind: Single_Stat
name: single stat
xPos: 1
yPos: 2
width: 6
height: 3
queries:
- query: |
option params = {
bucket: "foo",
start: -1d,
stop: now(),
name: "max",
floatVal: 1.0,
minVal: 10
}
from(bucket: params.bucket)
|> range(start: params.start, end: params.stop)
|> filter(fn: (r) => r._measurement == "processes")
|> filter(fn: (r) => r.floater == params.floatVal)
|> filter(fn: (r) => r._value > params.minVal)
|> aggregateWindow(every: v.windowPeriod, fn: max)
|> yield(name: params.name)
params:
- key: bucket
default: "bar"
type: string
- key: start
type: duration
- key: stop
type: time
- key: floatVal
default: 37.2
type: float
- key: minVal
type: int
- key: name # infer type
colors:
- name: laser
type: text
hex: "#8F8AF4"
value: 3`
tmplStr = fmt.Sprintf(tmplStr, dashName)
template, err := pkger.Parse(pkger.EncodingYAML, pkger.FromString(tmplStr))
require.NoError(t, err)
return template
}
isExpectedQuery := func(t *testing.T, actual pkger.SummaryDashboard, expectedParams string) {
t.Helper()
require.Len(t, actual.Charts, 1)
props, ok := actual.Charts[0].Properties.(influxdb.SingleStatViewProperties)
require.True(t, ok, "unexpected chart properties")
require.Len(t, props.Queries, 1)
expectedQuery := expectedParams + `
from(bucket: params.bucket)
|> range(start: params.start, end: params.stop)
|> filter(fn: (r) =>
(r._measurement == "processes"))
|> filter(fn: (r) =>
(r.floater == params.floatVal))
|> filter(fn: (r) =>
(r._value > params.minVal))
|> aggregateWindow(every: v.windowPeriod, fn: max)
|> yield(name: params.name)`
assert.Equal(t, expectedQuery, props.Queries[0].Text)
assert.Equal(t, "advanced", props.Queries[0].EditMode)
}
envKey := func(paramKey string) string {
return fmt.Sprintf(
"dashboards[%s].spec.charts[0].queries[0].params.%s",
dashName,
paramKey,
)
}
t.Run("using default values", func(t *testing.T) {
stack, cleanup := newStackFn(t, pkger.StackCreate{})
defer cleanup()
impact, err := svc.Apply(ctx, l.Org.ID, l.User.ID,
pkger.ApplyWithStackID(stack.ID),
pkger.ApplyWithTemplate(newDashTmpl(t)),
)
require.NoError(t, err)
require.Len(t, impact.Summary.Dashboards, 1)
actual := impact.Summary.Dashboards[0]
expectedParams := `option params = {
bucket: "bar",
start: -24h0m0s,
stop: now(),
name: "max",
floatVal: 37.2,
minVal: 10,
}`
isExpectedQuery(t, actual, expectedParams)
require.Len(t, actual.EnvReferences, 6)
expectedRefs := []pkger.SummaryReference{
{
Field: "spec.charts[0].queries[0].params.bucket",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.bucket`,
ValType: "string",
DefaultValue: "bar",
},
{
Field: "spec.charts[0].queries[0].params.floatVal",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.floatVal`,
ValType: "float",
DefaultValue: 37.2,
},
}
assert.Equal(t, expectedRefs, actual.EnvReferences[:len(expectedRefs)])
// check necessary since json can flip int to float type and fail assertions
// in a flakey manner
expectedIntRef := pkger.SummaryReference{
Field: "spec.charts[0].queries[0].params.minVal",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.minVal`,
ValType: "integer",
DefaultValue: int64(10),
}
actualIntRef := actual.EnvReferences[len(expectedRefs)]
if f, ok := actualIntRef.DefaultValue.(float64); ok {
actualIntRef.DefaultValue = int64(f)
}
assert.Equal(t, expectedIntRef, actualIntRef)
expectedRefs = []pkger.SummaryReference{
{
Field: "spec.charts[0].queries[0].params.name",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.name`,
ValType: "string",
DefaultValue: "max",
},
{
Field: "spec.charts[0].queries[0].params.start",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.start`,
ValType: "duration",
DefaultValue: "-24h0m0s",
},
{
Field: "spec.charts[0].queries[0].params.stop",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.stop`,
ValType: "time",
DefaultValue: "now()",
},
}
assert.Equal(t, expectedRefs, actual.EnvReferences[3:])
})
t.Run("with user provided values", func(t *testing.T) {
stack, cleanup := newStackFn(t, pkger.StackCreate{})
defer cleanup()
impact, err := svc.Apply(ctx, l.Org.ID, l.User.ID,
pkger.ApplyWithStackID(stack.ID),
pkger.ApplyWithTemplate(newDashTmpl(t)),
pkger.ApplyWithEnvRefs(map[string]interface{}{
envKey("bucket"): "foobar",
envKey("name"): "min",
envKey("start"): "-5d",
envKey("floatVal"): 33.3,
envKey("minVal"): 3,
}),
)
require.NoError(t, err)
require.Len(t, impact.Summary.Dashboards, 1)
actual := impact.Summary.Dashboards[0]
expectedParams := `option params = {
bucket: "foobar",
start: -5d,
stop: now(),
name: "min",
floatVal: 33.3,
minVal: 3,
}`
isExpectedQuery(t, actual, expectedParams)
require.Len(t, actual.EnvReferences, 6)
expectedRefs := []pkger.SummaryReference{
{
Field: "spec.charts[0].queries[0].params.bucket",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.bucket`,
ValType: "string",
Value: "foobar",
DefaultValue: "bar",
},
{
Field: "spec.charts[0].queries[0].params.floatVal",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.floatVal`,
ValType: "float",
Value: 33.3,
DefaultValue: 37.2,
},
}
assert.Equal(t, expectedRefs, actual.EnvReferences[:len(expectedRefs)])
// check necessary since json can flip int to float type and fail assertions
// in a flakey manner
expectedIntRef := pkger.SummaryReference{
Field: "spec.charts[0].queries[0].params.minVal",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.minVal`,
ValType: "integer",
Value: int64(3),
DefaultValue: int64(10),
}
actualIntRef := actual.EnvReferences[len(expectedRefs)]
if f, ok := actualIntRef.DefaultValue.(float64); ok {
actualIntRef.DefaultValue = int64(f)
}
if f, ok := actualIntRef.Value.(float64); ok {
actualIntRef.Value = int64(f)
}
assert.Equal(t, expectedIntRef, actualIntRef)
expectedRefs = []pkger.SummaryReference{
{
Field: "spec.charts[0].queries[0].params.name",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.name`,
ValType: "string",
Value: "min",
DefaultValue: "max",
},
{
Field: "spec.charts[0].queries[0].params.start",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.start`,
ValType: "duration",
Value: "-5d",
DefaultValue: "-24h0m0s",
},
{
Field: "spec.charts[0].queries[0].params.stop",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.stop`,
ValType: "time",
DefaultValue: "now()",
},
}
assert.Equal(t, expectedRefs, actual.EnvReferences[3:])
})
})
}
func newCompletePkg(t *testing.T) *pkger.Template {

View File

@ -7531,8 +7531,13 @@ components:
type: string
description: Key identified as environment reference and is the key identified in the template
value:
type: string
description: Value provided to fulfill reference
nullable: true
oneOf:
- type: string
- type: integer
- type: number
- type: boolean
defaultValue:
description: Default value that will be provided for the reference when no value is provided
nullable: true

View File

@ -708,8 +708,14 @@ func convertChartToResource(ch chart) Resource {
fieldChartHeight: ch.Height,
fieldChartWidth: ch.Width,
}
if len(ch.Queries) > 0 {
r[fieldChartQueries] = ch.Queries
var qq []Resource
for _, q := range ch.Queries {
qq = append(qq, Resource{
fieldQuery: q.DashboardQuery(),
})
}
if len(qq) > 0 {
r[fieldChartQueries] = qq
}
if len(ch.Colors) > 0 {
r[fieldChartColors] = ch.Colors

View File

@ -650,7 +650,8 @@ type SummaryLabelMapping struct {
type SummaryReference struct {
Field string `json:"resourceField"`
EnvRefKey string `json:"envRefKey"`
Value string `json:"value"`
ValType string `json:"valueType"`
Value interface{} `json:"value"`
DefaultValue interface{} `json:"defaultValue"`
}

View File

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestPkg(t *testing.T) {
func TestTemplate(t *testing.T) {
t.Run("Summary", func(t *testing.T) {
t.Run("buckets returned in asc order by name", func(t *testing.T) {
pkg := Template{

View File

@ -16,6 +16,9 @@ import (
"strings"
"time"
"github.com/influxdata/flux/ast"
"github.com/influxdata/flux/ast/edit"
"github.com/influxdata/flux/parser"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/pkg/jsonnet"
"gopkg.in/yaml.v3"
@ -948,7 +951,7 @@ func (p *Template) graphDashboards() *parseErr {
sort.Sort(dash.labels)
for i, cr := range o.Spec.slcResource(fieldDashCharts) {
ch, fails := parseChart(cr)
ch, fails := p.parseChart(dash.MetaName(), i, cr)
if fails != nil {
failures = append(failures,
objectValidationErr(fieldSpec, validationErr{
@ -963,7 +966,7 @@ func (p *Template) graphDashboards() *parseErr {
}
p.mDashboards[dash.MetaName()] = dash
p.setRefs(dash.name, dash.displayName)
p.setRefs(dash.refs()...)
return append(failures, dash.valid()...)
})
@ -1383,10 +1386,10 @@ func (p *Template) setRefs(refs ...*references) {
}
}
func parseChart(r Resource) (chart, []validationErr) {
func (p *Template) parseChart(dashMetaName string, chartIdx int, r Resource) (*chart, []validationErr) {
ck, err := r.chartKind()
if err != nil {
return chart{}, []validationErr{{
return nil, []validationErr{{
Field: fieldKind,
Msg: err.Error(),
}}
@ -1436,11 +1439,14 @@ func parseChart(r Resource) (chart, []validationErr) {
if presentQueries, ok := r[fieldChartQueries].(queries); ok {
c.Queries = presentQueries
} else {
for _, rq := range r.slcResource(fieldChartQueries) {
c.Queries = append(c.Queries, query{
Query: strings.TrimSpace(rq.stringShort(fieldQuery)),
q, vErrs := p.parseChartQueries(dashMetaName, chartIdx, r.slcResource(fieldChartQueries))
if len(vErrs) > 0 {
failures = append(failures, validationErr{
Field: "queries",
Nested: vErrs,
})
}
c.Queries = q
}
if presentColors, ok := r[fieldChartColors].(colors); ok {
@ -1505,10 +1511,132 @@ func parseChart(r Resource) (chart, []validationErr) {
}
if failures = append(failures, c.validProperties()...); len(failures) > 0 {
return chart{}, failures
return nil, failures
}
return c, nil
return &c, nil
}
func (p *Template) parseChartQueries(dashMetaName string, chartIdx int, resources []Resource) (queries, []validationErr) {
var (
q queries
vErrs []validationErr
)
for i, rq := range resources {
source := rq.stringShort(fieldQuery)
if source == "" {
continue
}
prefix := fmt.Sprintf("dashboards[%s].spec.charts[%d].queries[%d]", dashMetaName, chartIdx, i)
qq, err := p.parseQuery(prefix, source, rq.slcResource(fieldParams))
if err != nil {
vErrs = append(vErrs, validationErr{
Field: "query",
Index: intPtr(i),
Msg: err.Error(),
})
}
q = append(q, qq)
}
return q, vErrs
}
func (p *Template) parseQuery(prefix, source string, params []Resource) (query, error) {
files := parser.ParseSource(source).Files
if len(files) != 1 {
return query{}, influxErr(influxdb.EInvalid, "invalid query source")
}
q := query{
Query: strings.TrimSpace(source),
}
opt, err := edit.GetOption(files[0], "params")
if err != nil {
return q, nil
}
obj, ok := opt.(*ast.ObjectExpression)
if !ok {
return q, nil
}
mParams := make(map[string]*references)
for _, p := range obj.Properties {
sl, ok := p.Key.(*ast.Identifier)
if !ok {
continue
}
mParams[sl.Name] = &references{
EnvRef: sl.Name,
defaultVal: valFromExpr(p.Value),
valType: p.Value.Type(),
}
}
for _, pr := range params {
field := pr.stringShort(fieldKey)
if field == "" {
continue
}
if _, ok := mParams[field]; !ok {
mParams[field] = &references{EnvRef: field}
}
if def, ok := pr[fieldDefault]; ok {
mParams[field].defaultVal = def
}
if valtype, ok := pr.string(fieldType); ok {
mParams[field].valType = valtype
}
}
for _, ref := range mParams {
envRef := fmt.Sprintf("%s.params.%s", prefix, ref.EnvRef)
q.params = append(q.params, &references{
EnvRef: envRef,
defaultVal: ref.defaultVal,
val: p.mEnvVals[envRef],
valType: ref.valType,
})
}
return q, nil
}
func valFromExpr(p ast.Expression) interface{} {
switch literal := p.(type) {
case *ast.CallExpression:
sl, ok := literal.Callee.(*ast.Identifier)
if ok && sl.Name == "now" {
return "now()"
}
return nil
case *ast.DateTimeLiteral:
return ast.DateTimeFromLiteral(literal)
case *ast.FloatLiteral:
return ast.FloatFromLiteral(literal)
case *ast.IntegerLiteral:
return ast.IntegerFromLiteral(literal)
case *ast.DurationLiteral:
dur, _ := ast.DurationFrom(literal, time.Time{})
return dur
case *ast.StringLiteral:
return ast.StringFromLiteral(literal)
case *ast.UnaryExpression:
// a signed duration is represented by a UnaryExpression.
// it is the only unary expression allowed.
v := valFromExpr(literal.Argument)
if dur, ok := v.(time.Duration); ok {
switch literal.Operator {
case ast.SubtractionOperator:
return "-" + dur.String()
}
}
return v
default:
return nil
}
}
// dns1123LabelMaxLength is a label's max length in DNS (RFC 1123)

View File

@ -9,6 +9,9 @@ import (
"strings"
"time"
"github.com/influxdata/flux/ast"
"github.com/influxdata/flux/ast/edit"
"github.com/influxdata/flux/parser"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/notification"
icheck "github.com/influxdata/influxdb/v2/notification/check"
@ -63,6 +66,7 @@ const (
fieldName = "name"
fieldOffset = "offset"
fieldOperator = "operator"
fieldParams = "params"
fieldPrefix = "prefix"
fieldQuery = "query"
fieldSuffix = "suffix"
@ -457,7 +461,7 @@ type dashboard struct {
identity
Description string
Charts []chart
Charts []*chart
labels sortedLabels
}
@ -470,8 +474,16 @@ func (d *dashboard) ResourceType() influxdb.ResourceType {
return KindDashboard.ResourceType()
}
func (d *dashboard) refs() []*references {
var queryRefs []*references
for _, c := range d.Charts {
queryRefs = append(queryRefs, c.Queries.references()...)
}
return append([]*references{d.name, d.displayName}, queryRefs...)
}
func (d *dashboard) summarize() SummaryDashboard {
iDash := SummaryDashboard{
sum := SummaryDashboard{
SummaryIdentifier: SummaryIdentifier{
Kind: KindDashboard,
MetaName: d.MetaName(),
@ -481,16 +493,27 @@ func (d *dashboard) summarize() SummaryDashboard {
Description: d.Description,
LabelAssociations: toSummaryLabels(d.labels...),
}
for _, c := range d.Charts {
iDash.Charts = append(iDash.Charts, SummaryChart{
for chartIdx, c := range d.Charts {
sum.Charts = append(sum.Charts, SummaryChart{
Properties: c.properties(),
Height: c.Height,
Width: c.Width,
XPosition: c.XPos,
YPosition: c.YPos,
})
for qIdx, q := range c.Queries {
for _, ref := range q.params {
parts := strings.Split(ref.EnvRef, ".")
field := fmt.Sprintf("spec.charts[%d].queries[%d].params.%s", chartIdx, qIdx, parts[len(parts)-1])
sum.EnvReferences = append(sum.EnvReferences, convertRefToRefSummary(field, ref))
sort.Slice(sum.EnvReferences, func(i, j int) bool {
return sum.EnvReferences[i].EnvRefKey < sum.EnvReferences[j].EnvRefKey
})
}
}
}
return iDash
return sum
}
func (d *dashboard) valid() []validationErr {
@ -567,7 +590,7 @@ type chart struct {
TimeFormat string
}
func (c chart) properties() influxdb.ViewProperties {
func (c *chart) properties() influxdb.ViewProperties {
switch c.Kind {
case chartKindGauge:
return influxdb.GaugeViewProperties{
@ -752,7 +775,7 @@ func (c chart) properties() influxdb.ViewProperties {
}
}
func (c chart) validProperties() []validationErr {
func (c *chart) validProperties() []validationErr {
if c.Kind == chartKindMarkdown {
// at the time of writing, there's nothing to validate for markdown types
return nil
@ -804,6 +827,24 @@ func validPosition(pos string) []validationErr {
return nil
}
func (c *chart) validBaseProps() []validationErr {
var fails []validationErr
if c.Width <= 0 {
fails = append(fails, validationErr{
Field: fieldChartWidth,
Msg: "must be greater than 0",
})
}
if c.Height <= 0 {
fails = append(fails, validationErr{
Field: fieldChartHeight,
Msg: "must be greater than 0",
})
}
return fails
}
var geometryTypes = map[string]bool{
"line": true,
"step": true,
@ -827,24 +868,6 @@ func validGeometry(geom string) []validationErr {
return nil
}
func (c chart) validBaseProps() []validationErr {
var fails []validationErr
if c.Width <= 0 {
fails = append(fails, validationErr{
Field: fieldChartWidth,
Msg: "must be greater than 0",
})
}
if c.Height <= 0 {
fails = append(fails, validationErr{
Field: fieldChartHeight,
Msg: "must be greater than 0",
})
}
return fails
}
const (
fieldChartFieldOptionDisplayName = "displayName"
fieldChartFieldOptionFieldName = "fieldName"
@ -954,7 +977,7 @@ func (c colors) strings() []string {
}
// TODO: looks like much of these are actually getting defaults in
// the UI. looking at sytem charts, seeign lots of failures for missing
// the UI. looking at system charts, seeing lots of failures for missing
// color types or no colors at all.
func (c colors) hasTypes(types ...string) []validationErr {
tMap := make(map[string]bool)
@ -997,7 +1020,40 @@ func (c colors) valid() []validationErr {
}
type query struct {
Query string `json:"query" yaml:"query"`
Query string `json:"query" yaml:"query"`
params []*references
}
func (q query) DashboardQuery() string {
if len(q.params) == 0 {
return q.Query
}
files := parser.ParseSource(q.Query).Files
if len(files) != 1 {
return q.Query
}
opt, err := edit.GetOption(files[0], "params")
if err != nil {
// no params option present in query
return q.Query
}
obj, ok := opt.(*ast.ObjectExpression)
if !ok {
// params option present is invalid. Should always be an Object.
return q.Query
}
for _, ref := range q.params {
parts := strings.Split(ref.EnvRef, ".")
key := parts[len(parts)-1]
edit.SetProperty(obj, key, ref.expression())
}
edit.SetOption(files[0], "params", obj)
return ast.Format(files[0])
}
type queries []query
@ -1005,17 +1061,22 @@ type queries []query
func (q queries) influxDashQueries() []influxdb.DashboardQuery {
var iQueries []influxdb.DashboardQuery
for _, qq := range q {
newQuery := influxdb.DashboardQuery{
Text: qq.Query,
iQueries = append(iQueries, influxdb.DashboardQuery{
Text: qq.DashboardQuery(),
EditMode: "advanced",
}
// TODO: axe this builder configs when issue https://github.com/influxdata/influxdb/issues/15708 is fixed up
newQuery.BuilderConfig.Tags = append(newQuery.BuilderConfig.Tags, influxdb.NewBuilderTag("_measurement", "filter", ""))
iQueries = append(iQueries, newQuery)
})
}
return iQueries
}
func (q queries) references() []*references {
var refs []*references
for _, qq := range q {
refs = append(refs, qq.params...)
}
return refs
}
const (
fieldAxisBase = "base"
fieldAxisLabel = "label"
@ -2078,6 +2139,7 @@ type references struct {
val interface{}
defaultVal interface{}
valType string
}
func (r *references) hasValue() bool {
@ -2088,6 +2150,51 @@ func (r *references) hasEnvRef() bool {
return r != nil && r.EnvRef != ""
}
func (r *references) expression() ast.Expression {
v := r.val
if v == nil {
v = r.defaultVal
}
if v == nil {
return nil
}
switch strings.ToLower(r.valType) {
case "bool", "booleanliteral":
return astBoolFromIface(v)
case "duration", "durationliteral":
return astDurationFromIface(v)
case "float", "floatliteral":
return astFloatFromIface(v)
case "int", "integerliteral":
return astIntegerFromIface(v)
case "string", "stringliteral":
return astStringFromIface(v)
case "time", "datetimeliteral":
if v == "now()" {
return astNow()
}
return astTimeFromIface(v)
}
return nil
}
func (r *references) Float64() float64 {
if r == nil || r.val == nil {
return 0
}
i, _ := r.val.(float64)
return i
}
func (r *references) Int64() int64 {
if r == nil || r.val == nil {
return 0
}
i, _ := r.val.(int64)
return i
}
func (r *references) String() string {
if r == nil {
return ""
@ -2120,14 +2227,86 @@ func (r *references) SecretField() influxdb.SecretField {
}
func convertRefToRefSummary(field string, ref *references) SummaryReference {
var valType string
switch strings.ToLower(ref.valType) {
case "bool", "booleanliteral":
valType = "bool"
case "duration", "durationliteral":
valType = "duration"
case "float", "floatliteral":
valType = "float"
case "int", "integerliteral":
valType = "integer"
case "string", "stringliteral":
valType = "string"
case "time", "datetimeliteral":
valType = "time"
}
return SummaryReference{
Field: field,
EnvRefKey: ref.EnvRef,
Value: ref.StringVal(),
ValType: valType,
Value: ref.val,
DefaultValue: ref.defaultVal,
}
}
func astBoolFromIface(v interface{}) *ast.BooleanLiteral {
b, _ := v.(bool)
return ast.BooleanLiteralFromValue(b)
}
func astDurationFromIface(v interface{}) *ast.DurationLiteral {
s, ok := v.(string)
if !ok {
return nil
}
dur, _ := parser.ParseSignedDuration(s)
return dur
}
func astFloatFromIface(v interface{}) *ast.FloatLiteral {
if i, ok := v.(int); ok {
return ast.FloatLiteralFromValue(float64(i))
}
f, _ := v.(float64)
return ast.FloatLiteralFromValue(f)
}
func astIntegerFromIface(v interface{}) *ast.IntegerLiteral {
if f, ok := v.(float64); ok {
return ast.IntegerLiteralFromValue(int64(f))
}
i, _ := v.(int64)
return ast.IntegerLiteralFromValue(i)
}
func astNow() *ast.CallExpression {
return &ast.CallExpression{
Callee: &ast.Identifier{Name: "now"},
}
}
func astStringFromIface(v interface{}) *ast.StringLiteral {
s, _ := v.(string)
return ast.StringLiteralFromValue(s)
}
func astTimeFromIface(v interface{}) *ast.DateTimeLiteral {
if t, ok := v.(time.Time); ok {
return ast.DateTimeLiteralFromValue(t)
}
s, ok := v.(string)
if !ok {
return nil
}
t, _ := parser.ParseTime(s)
return t
}
func isValidName(name string, minLength int) (validationErr, bool) {
if len(name) >= minLength {
return validationErr{}, true

View File

@ -2330,6 +2330,94 @@ spec:
})
})
t.Run("with params option should be parameterizable", func(t *testing.T) {
testfileRunner(t, "testdata/dashboard_params.yml", func(t *testing.T, template *Template) {
sum := template.Summary()
require.Len(t, sum.Dashboards, 1)
actual := sum.Dashboards[0]
assert.Equal(t, KindDashboard, actual.Kind)
assert.Equal(t, "dash-1", actual.MetaName)
require.Len(t, actual.Charts, 1)
actualChart := actual.Charts[0]
assert.Equal(t, 3, actualChart.Height)
assert.Equal(t, 6, actualChart.Width)
assert.Equal(t, 1, actualChart.XPosition)
assert.Equal(t, 2, actualChart.YPosition)
props, ok := actualChart.Properties.(influxdb.SingleStatViewProperties)
require.True(t, ok)
assert.Equal(t, "single-stat", props.GetType())
require.Len(t, props.Queries, 1)
queryText := `option params = {
bucket: "bar",
start: -24h0m0s,
stop: now(),
name: "max",
floatVal: 37.2,
minVal: 10,
}
from(bucket: params.bucket)
|> range(start: params.start, end: params.stop)
|> filter(fn: (r) =>
(r._measurement == "processes"))
|> filter(fn: (r) =>
(r.floater == params.floatVal))
|> filter(fn: (r) =>
(r._value > params.minVal))
|> aggregateWindow(every: v.windowPeriod, fn: max)
|> yield(name: params.name)`
q := props.Queries[0]
assert.Equal(t, queryText, q.Text)
assert.Equal(t, "advanced", q.EditMode)
expectedRefs := []SummaryReference{
{
Field: "spec.charts[0].queries[0].params.bucket",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.bucket`,
ValType: "string",
DefaultValue: "bar",
},
{
Field: "spec.charts[0].queries[0].params.floatVal",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.floatVal`,
ValType: "float",
DefaultValue: 37.2,
},
{
Field: "spec.charts[0].queries[0].params.minVal",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.minVal`,
ValType: "integer",
DefaultValue: int64(10),
},
{
Field: "spec.charts[0].queries[0].params.name",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.name`,
ValType: "string",
DefaultValue: "max",
},
{
Field: "spec.charts[0].queries[0].params.start",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.start`,
ValType: "duration",
DefaultValue: "-24h0m0s",
},
{
Field: "spec.charts[0].queries[0].params.stop",
EnvRefKey: `dashboards[dash-1].spec.charts[0].queries[0].params.stop`,
ValType: "time",
DefaultValue: "now()",
},
}
assert.Equal(t, expectedRefs, actual.EnvReferences)
})
})
t.Run("with env refs should be valid", func(t *testing.T) {
testfileRunner(t, "testdata/dashboard_ref.yml", func(t *testing.T, template *Template) {
actual := template.Summary().Dashboards

View File

@ -1968,7 +1968,7 @@ func (s *Service) rollbackDashboards(ctx context.Context, dashs []*stateDashboar
return nil
}
func convertChartsToCells(ch []chart) []*influxdb.Cell {
func convertChartsToCells(ch []*chart) []*influxdb.Cell {
icells := make([]*influxdb.Cell, 0, len(ch))
for _, c := range ch {
icell := &influxdb.Cell{

View File

@ -1962,13 +1962,10 @@ func TestService(t *testing.T) {
})
newQuery := func() influxdb.DashboardQuery {
q := influxdb.DashboardQuery{
return influxdb.DashboardQuery{
Text: "from(v.bucket) |> count()",
EditMode: "advanced",
}
// TODO: remove this when issue that forced the builder tag to be here to render in UI.
q.BuilderConfig.Tags = append(q.BuilderConfig.Tags, influxdb.NewBuilderTag("_measurement", "filter", ""))
return q
}
newAxes := func() map[string]influxdb.Axis {

49
pkger/testdata/dashboard_params.yml vendored Normal file
View File

@ -0,0 +1,49 @@
apiVersion: influxdata.com/v2alpha1
kind: Dashboard
metadata:
name: dash-1
spec:
charts:
- kind: Single_Stat
name: single stat
xPos: 1
yPos: 2
width: 6
height: 3
queries:
- query: |
option params = {
bucket: "foo",
start: -1d,
stop: now(),
name: "max",
floatVal: 1.0,
minVal: 10
}
from(bucket: params.bucket)
|> range(start: params.start, end: params.stop)
|> filter(fn: (r) => r._measurement == "processes")
|> filter(fn: (r) => r.floater == params.floatVal)
|> filter(fn: (r) => r._value > params.minVal)
|> aggregateWindow(every: v.windowPeriod, fn: max)
|> yield(name: params.name)
params:
- key: bucket
default: "bar"
type: string
- key: start
type: duration
- key: stop
type: time
- key: floatVal
default: 37.2
type: float
- key: minVal
type: int
- key: name # infer type
colors:
- name: laser
type: text
hex: "#8F8AF4"
value: 3