parent
a61161d73b
commit
41cb12aeec
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
146
pkger/parser.go
146
pkger/parser.go
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue