diff --git a/http/swagger.yml b/http/swagger.yml index cb674297ae..1bf975cf4b 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -7579,8 +7579,8 @@ components: items: type: object properties: - remove: - type: boolean + stateStatus: + type: string id: type: string pkgName: diff --git a/pkger/models.go b/pkger/models.go index d31e9fc94c..4e2c095104 100644 --- a/pkger/models.go +++ b/pkger/models.go @@ -6,7 +6,6 @@ import ( "fmt" "reflect" "sort" - "strings" "time" "github.com/influxdata/influxdb/v2" @@ -253,58 +252,6 @@ type ( } ) -func newDiffDashboard(d *dashboard) DiffDashboard { - diff := DiffDashboard{ - DiffIdentifier: DiffIdentifier{ - ID: SafeID(d.ID()), - Remove: d.shouldRemove, - PkgName: d.PkgName(), - }, - New: DiffDashboardValues{ - Name: d.Name(), - Desc: d.Description, - Charts: make([]DiffChart, 0, len(d.Charts)), - }, - } - - for _, c := range d.Charts { - diff.New.Charts = append(diff.New.Charts, DiffChart{ - Properties: c.properties(), - Height: c.Height, - Width: c.Width, - }) - } - - if !d.Exists() { - return diff - } - - oldDiff := DiffDashboardValues{ - Name: d.existing.Name, - Desc: d.existing.Description, - Charts: make([]DiffChart, 0, len(d.existing.Cells)), - } - - for _, c := range d.existing.Cells { - var props influxdb.ViewProperties - if c.View != nil { - props = c.View.Properties - } - - oldDiff.Charts = append(oldDiff.Charts, DiffChart{ - Properties: props, - XPosition: int(c.X), - YPosition: int(c.Y), - Height: int(c.H), - Width: int(c.W), - }) - } - - diff.Old = &oldDiff - - return diff -} - // DiffChart is a diff of oa chart. Since all charts are new right now. // the SummaryChart is reused here. type DiffChart SummaryChart @@ -606,41 +553,6 @@ type SummaryDashboard struct { LabelAssociations []SummaryLabel `json:"labelAssociations"` } -// chartKind identifies what kind of chart is eluded too. Each -// chart kind has their own requirements for what constitutes -// a chart. -type chartKind string - -// available chart kinds -const ( - chartKindUnknown chartKind = "" - chartKindGauge chartKind = "gauge" - chartKindHeatMap chartKind = "heatmap" - chartKindHistogram chartKind = "histogram" - chartKindMarkdown chartKind = "markdown" - chartKindScatter chartKind = "scatter" - chartKindSingleStat chartKind = "single_stat" - chartKindSingleStatPlusLine chartKind = "single_stat_plus_line" - chartKindTable chartKind = "table" - chartKindXY chartKind = "xy" -) - -func (c chartKind) ok() bool { - switch c { - case chartKindGauge, chartKindHeatMap, chartKindHistogram, - chartKindMarkdown, chartKindScatter, chartKindSingleStat, - chartKindSingleStatPlusLine, chartKindTable, chartKindXY: - return true - default: - return false - } -} - -func (c chartKind) title() string { - spacedKind := strings.ReplaceAll(string(c), "_", " ") - return strings.ReplaceAll(strings.Title(spacedKind), " ", "_") -} - // SummaryChart provides a summary of a pkg dashboard's chart. type SummaryChart struct { Properties influxdb.ViewProperties `json:"-"` @@ -1070,677 +982,6 @@ func (r mapperNotificationRules) Len() int { return len(r) } -const ( - fieldDashCharts = "charts" -) - -const dashboardNameMinLength = 2 - -type dashboard struct { - identity - - id influxdb.ID - OrgID influxdb.ID - Description string - Charts []chart - - labels sortedLabels - - existing *influxdb.Dashboard -} - -func (d *dashboard) ID() influxdb.ID { - if d.existing != nil { - return d.existing.ID - } - return d.id -} - -func (d *dashboard) Labels() []*label { - return d.labels -} - -func (d *dashboard) ResourceType() influxdb.ResourceType { - return KindDashboard.ResourceType() -} - -func (d *dashboard) Exists() bool { - return d.existing != nil -} - -func (d *dashboard) summarize() SummaryDashboard { - iDash := SummaryDashboard{ - ID: SafeID(d.ID()), - OrgID: SafeID(d.OrgID), - PkgName: d.PkgName(), - Name: d.Name(), - Description: d.Description, - LabelAssociations: toSummaryLabels(d.labels...), - } - for _, c := range d.Charts { - iDash.Charts = append(iDash.Charts, SummaryChart{ - Properties: c.properties(), - Height: c.Height, - Width: c.Width, - XPosition: c.XPos, - YPosition: c.YPos, - }) - } - return iDash -} - -func (d *dashboard) valid() []validationErr { - var vErrs []validationErr - if err, ok := isValidName(d.Name(), dashboardNameMinLength); !ok { - vErrs = append(vErrs, err) - } - if len(vErrs) == 0 { - return nil - } - return []validationErr{ - objectValidationErr(fieldSpec, vErrs...), - } -} - -type mapperDashboards []*dashboard - -func (m mapperDashboards) Association(i int) labelAssociater { - return m[i] -} - -func (m mapperDashboards) Len() int { - return len(m) -} - -const ( - fieldChartAxes = "axes" - fieldChartBinCount = "binCount" - fieldChartBinSize = "binSize" - fieldChartColors = "colors" - fieldChartDecimalPlaces = "decimalPlaces" - fieldChartDomain = "domain" - fieldChartGeom = "geom" - fieldChartHeight = "height" - fieldChartLegend = "legend" - fieldChartNote = "note" - fieldChartNoteOnEmpty = "noteOnEmpty" - fieldChartPosition = "position" - fieldChartQueries = "queries" - fieldChartShade = "shade" - fieldChartFieldOptions = "fieldOptions" - fieldChartTableOptions = "tableOptions" - fieldChartTickPrefix = "tickPrefix" - fieldChartTickSuffix = "tickSuffix" - fieldChartTimeFormat = "timeFormat" - fieldChartWidth = "width" - fieldChartXCol = "xCol" - fieldChartXPos = "xPos" - fieldChartYCol = "yCol" - fieldChartYPos = "yPos" -) - -type chart struct { - Kind chartKind - Name string - Prefix string - TickPrefix string - Suffix string - TickSuffix string - Note string - NoteOnEmpty bool - DecimalPlaces int - EnforceDecimals bool - Shade bool - Legend legend - Colors colors - Queries queries - Axes axes - Geom string - XCol, YCol string - XPos, YPos int - Height, Width int - BinSize int - BinCount int - Position string - FieldOptions []fieldOption - TableOptions tableOptions - TimeFormat string -} - -func (c chart) properties() influxdb.ViewProperties { - switch c.Kind { - case chartKindGauge: - return influxdb.GaugeViewProperties{ - Type: influxdb.ViewPropertyTypeGauge, - Queries: c.Queries.influxDashQueries(), - Prefix: c.Prefix, - TickPrefix: c.TickPrefix, - Suffix: c.Suffix, - TickSuffix: c.TickSuffix, - ViewColors: c.Colors.influxViewColors(), - DecimalPlaces: influxdb.DecimalPlaces{ - IsEnforced: c.EnforceDecimals, - Digits: int32(c.DecimalPlaces), - }, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - } - case chartKindHeatMap: - return influxdb.HeatmapViewProperties{ - Type: influxdb.ViewPropertyTypeHeatMap, - Queries: c.Queries.influxDashQueries(), - ViewColors: c.Colors.strings(), - BinSize: int32(c.BinSize), - XColumn: c.XCol, - YColumn: c.YCol, - XDomain: c.Axes.get("x").Domain, - YDomain: c.Axes.get("y").Domain, - XPrefix: c.Axes.get("x").Prefix, - YPrefix: c.Axes.get("y").Prefix, - XSuffix: c.Axes.get("x").Suffix, - YSuffix: c.Axes.get("y").Suffix, - XAxisLabel: c.Axes.get("x").Label, - YAxisLabel: c.Axes.get("y").Label, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - TimeFormat: c.TimeFormat, - } - case chartKindHistogram: - return influxdb.HistogramViewProperties{ - Type: influxdb.ViewPropertyTypeHistogram, - Queries: c.Queries.influxDashQueries(), - ViewColors: c.Colors.influxViewColors(), - FillColumns: []string{}, - XColumn: c.XCol, - XDomain: c.Axes.get("x").Domain, - XAxisLabel: c.Axes.get("x").Label, - Position: c.Position, - BinCount: c.BinCount, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - } - case chartKindMarkdown: - return influxdb.MarkdownViewProperties{ - Type: influxdb.ViewPropertyTypeMarkdown, - Note: c.Note, - } - case chartKindScatter: - return influxdb.ScatterViewProperties{ - Type: influxdb.ViewPropertyTypeScatter, - Queries: c.Queries.influxDashQueries(), - ViewColors: c.Colors.strings(), - XColumn: c.XCol, - YColumn: c.YCol, - XDomain: c.Axes.get("x").Domain, - YDomain: c.Axes.get("y").Domain, - XPrefix: c.Axes.get("x").Prefix, - YPrefix: c.Axes.get("y").Prefix, - XSuffix: c.Axes.get("x").Suffix, - YSuffix: c.Axes.get("y").Suffix, - XAxisLabel: c.Axes.get("x").Label, - YAxisLabel: c.Axes.get("y").Label, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - TimeFormat: c.TimeFormat, - } - case chartKindSingleStat: - return influxdb.SingleStatViewProperties{ - Type: influxdb.ViewPropertyTypeSingleStat, - Prefix: c.Prefix, - TickPrefix: c.TickPrefix, - Suffix: c.Suffix, - TickSuffix: c.TickSuffix, - DecimalPlaces: influxdb.DecimalPlaces{ - IsEnforced: c.EnforceDecimals, - Digits: int32(c.DecimalPlaces), - }, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - Queries: c.Queries.influxDashQueries(), - ViewColors: c.Colors.influxViewColors(), - } - case chartKindSingleStatPlusLine: - return influxdb.LinePlusSingleStatProperties{ - Type: influxdb.ViewPropertyTypeSingleStatPlusLine, - Prefix: c.Prefix, - Suffix: c.Suffix, - DecimalPlaces: influxdb.DecimalPlaces{ - IsEnforced: c.EnforceDecimals, - Digits: int32(c.DecimalPlaces), - }, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - XColumn: c.XCol, - YColumn: c.YCol, - ShadeBelow: c.Shade, - Legend: c.Legend.influxLegend(), - Queries: c.Queries.influxDashQueries(), - ViewColors: c.Colors.influxViewColors(), - Axes: c.Axes.influxAxes(), - Position: c.Position, - } - case chartKindTable: - fieldOptions := make([]influxdb.RenamableField, 0, len(c.FieldOptions)) - for _, fieldOpt := range c.FieldOptions { - fieldOptions = append(fieldOptions, influxdb.RenamableField{ - InternalName: fieldOpt.FieldName, - DisplayName: fieldOpt.DisplayName, - Visible: fieldOpt.Visible, - }) - } - - return influxdb.TableViewProperties{ - Type: influxdb.ViewPropertyTypeTable, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - DecimalPlaces: influxdb.DecimalPlaces{ - IsEnforced: c.EnforceDecimals, - Digits: int32(c.DecimalPlaces), - }, - Queries: c.Queries.influxDashQueries(), - ViewColors: c.Colors.influxViewColors(), - TableOptions: influxdb.TableOptions{ - VerticalTimeAxis: c.TableOptions.VerticalTimeAxis, - SortBy: influxdb.RenamableField{ - InternalName: c.TableOptions.SortByField, - }, - Wrapping: c.TableOptions.Wrapping, - FixFirstColumn: c.TableOptions.FixFirstColumn, - }, - FieldOptions: fieldOptions, - TimeFormat: c.TimeFormat, - } - case chartKindXY: - return influxdb.XYViewProperties{ - Type: influxdb.ViewPropertyTypeXY, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - XColumn: c.XCol, - YColumn: c.YCol, - ShadeBelow: c.Shade, - Legend: c.Legend.influxLegend(), - Queries: c.Queries.influxDashQueries(), - ViewColors: c.Colors.influxViewColors(), - Axes: c.Axes.influxAxes(), - Geom: c.Geom, - Position: c.Position, - TimeFormat: c.TimeFormat, - } - default: - return nil - } -} - -func (c chart) validProperties() []validationErr { - if c.Kind == chartKindMarkdown { - // at the time of writing, there's nothing to validate for markdown types - return nil - } - - var fails []validationErr - - validatorFns := []func() []validationErr{ - c.validBaseProps, - c.Queries.valid, - c.Colors.valid, - } - for _, validatorFn := range validatorFns { - fails = append(fails, validatorFn()...) - } - - // chart kind specific validations - switch c.Kind { - case chartKindGauge: - fails = append(fails, c.Colors.hasTypes(colorTypeMin, colorTypeMax)...) - case chartKindHeatMap: - fails = append(fails, c.Axes.hasAxes("x", "y")...) - case chartKindHistogram: - fails = append(fails, c.Axes.hasAxes("x")...) - case chartKindScatter: - fails = append(fails, c.Axes.hasAxes("x", "y")...) - case chartKindSingleStat: - case chartKindSingleStatPlusLine: - fails = append(fails, c.Axes.hasAxes("x", "y")...) - fails = append(fails, validPosition(c.Position)...) - case chartKindTable: - fails = append(fails, validTableOptions(c.TableOptions)...) - case chartKindXY: - fails = append(fails, validGeometry(c.Geom)...) - fails = append(fails, c.Axes.hasAxes("x", "y")...) - fails = append(fails, validPosition(c.Position)...) - } - - return fails -} - -func validPosition(pos string) []validationErr { - pos = strings.ToLower(pos) - if pos != "" && pos != "overlaid" && pos != "stacked" { - return []validationErr{{ - Field: fieldChartPosition, - Msg: fmt.Sprintf("invalid position supplied %q; valid positions is one of [overlaid, stacked]", pos), - }} - } - return nil -} - -var geometryTypes = map[string]bool{ - "line": true, - "step": true, - "stacked": true, - "monotoneX": true, - "bar": true, -} - -func validGeometry(geom string) []validationErr { - if !geometryTypes[geom] { - msg := "type not found" - if geom != "" { - msg = "type provided is not supported" - } - return []validationErr{{ - Field: fieldChartGeom, - Msg: fmt.Sprintf("%s: %q", msg, geom), - }} - } - - 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" - fieldChartFieldOptionVisible = "visible" -) - -type fieldOption struct { - FieldName string - DisplayName string - Visible bool -} - -const ( - fieldChartTableOptionVerticalTimeAxis = "verticalTimeAxis" - fieldChartTableOptionSortBy = "sortBy" - fieldChartTableOptionWrapping = "wrapping" - fieldChartTableOptionFixFirstColumn = "fixFirstColumn" -) - -type tableOptions struct { - VerticalTimeAxis bool - SortByField string - Wrapping string - FixFirstColumn bool -} - -func validTableOptions(opts tableOptions) []validationErr { - var fails []validationErr - - switch opts.Wrapping { - case "", "single-line", "truncate", "wrap": - default: - fails = append(fails, validationErr{ - Field: fieldChartTableOptionWrapping, - Msg: `chart table option should 1 in ["single-line", "truncate", "wrap"]`, - }) - } - - if len(fails) == 0 { - return nil - } - - return []validationErr{ - { - Field: fieldChartTableOptions, - Nested: fails, - }, - } -} - -const ( - colorTypeBackground = "background" - colorTypeMin = "min" - colorTypeMax = "max" - colorTypeScale = "scale" - colorTypeText = "text" - colorTypeThreshold = "threshold" -) - -const ( - fieldColorHex = "hex" -) - -type color struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` - Hex string `json:"hex,omitempty" yaml:"hex,omitempty"` - // using reference for Value here so we can set to nil and - // it will be ignored during encoding, keeps our exported pkgs - // clear of unneeded entries. - Value *float64 `json:"value,omitempty" yaml:"value,omitempty"` -} - -// TODO: -// - verify templates are desired -// - template colors so references can be shared -type colors []*color - -func (c colors) influxViewColors() []influxdb.ViewColor { - ptrToFloat64 := func(f *float64) float64 { - if f == nil { - return 0 - } - return *f - } - - var iColors []influxdb.ViewColor - for _, cc := range c { - iColors = append(iColors, influxdb.ViewColor{ - Type: cc.Type, - Hex: cc.Hex, - Name: cc.Name, - Value: ptrToFloat64(cc.Value), - }) - } - return iColors -} - -func (c colors) strings() []string { - clrs := []string{} - - for _, clr := range c { - clrs = append(clrs, clr.Hex) - } - - return clrs -} - -// TODO: looks like much of these are actually getting defaults in -// the UI. looking at sytem charts, seeign lots of failures for missing -// color types or no colors at all. -func (c colors) hasTypes(types ...string) []validationErr { - tMap := make(map[string]bool) - for _, cc := range c { - tMap[cc.Type] = true - } - - var failures []validationErr - for _, t := range types { - if !tMap[t] { - failures = append(failures, validationErr{ - Field: "colors", - Msg: fmt.Sprintf("type not found: %q", t), - }) - } - } - - return failures -} - -func (c colors) valid() []validationErr { - var fails []validationErr - for i, cc := range c { - cErr := validationErr{ - Field: fieldChartColors, - Index: intPtr(i), - } - if cc.Hex == "" { - cErr.Nested = append(cErr.Nested, validationErr{ - Field: fieldColorHex, - Msg: "a color must have a hex value provided", - }) - } - if len(cErr.Nested) > 0 { - fails = append(fails, cErr) - } - } - - return fails -} - -type query struct { - Query string `json:"query" yaml:"query"` -} - -type queries []query - -func (q queries) influxDashQueries() []influxdb.DashboardQuery { - var iQueries []influxdb.DashboardQuery - for _, qq := range q { - newQuery := influxdb.DashboardQuery{ - Text: qq.Query, - 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) valid() []validationErr { - var fails []validationErr - if len(q) == 0 { - fails = append(fails, validationErr{ - Field: fieldChartQueries, - Msg: "at least 1 query must be provided", - }) - } - - for i, qq := range q { - qErr := validationErr{ - Field: fieldChartQueries, - Index: intPtr(i), - } - if qq.Query == "" { - qErr.Nested = append(fails, validationErr{ - Field: fieldQuery, - Msg: "a query must be provided", - }) - } - if len(qErr.Nested) > 0 { - fails = append(fails, qErr) - } - } - - return fails -} - -const ( - fieldAxisBase = "base" - fieldAxisLabel = "label" - fieldAxisScale = "scale" -) - -type axis struct { - Base string `json:"base,omitempty" yaml:"base,omitempty"` - Label string `json:"label,omitempty" yaml:"label,omitempty"` - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` - Scale string `json:"scale,omitempty" yaml:"scale,omitempty"` - Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"` - Domain []float64 `json:"domain,omitempty" yaml:"domain,omitempty"` -} - -type axes []axis - -func (a axes) get(name string) axis { - for _, ax := range a { - if name == ax.Name { - return ax - } - } - return axis{} -} - -func (a axes) influxAxes() map[string]influxdb.Axis { - m := make(map[string]influxdb.Axis) - for _, ax := range a { - m[ax.Name] = influxdb.Axis{ - Bounds: []string{}, - Label: ax.Label, - Prefix: ax.Prefix, - Suffix: ax.Suffix, - Base: ax.Base, - Scale: ax.Scale, - } - } - return m -} - -func (a axes) hasAxes(expectedAxes ...string) []validationErr { - mAxes := make(map[string]bool) - for _, ax := range a { - mAxes[ax.Name] = true - } - - var failures []validationErr - for _, expected := range expectedAxes { - if !mAxes[expected] { - failures = append(failures, validationErr{ - Field: fieldChartAxes, - Msg: fmt.Sprintf("axis not found: %q", expected), - }) - } - } - - return failures -} - -const ( - fieldLegendOrientation = "orientation" -) - -type legend struct { - Orientation string `json:"orientation,omitempty" yaml:"orientation,omitempty"` - Type string `json:"type" yaml:"type"` -} - -func (l legend) influxLegend() influxdb.Legend { - return influxdb.Legend{ - Type: l.Type, - Orientation: l.Orientation, - } -} - const ( fieldReferencesEnv = "envRef" fieldReferencesSecret = "secretRef" diff --git a/pkger/models_test.go b/pkger/models_test.go index 338ae4b456..20e68de5a6 100644 --- a/pkger/models_test.go +++ b/pkger/models_test.go @@ -476,11 +476,6 @@ func TestPkg(t *testing.T) { kind Kind validName string }{ - { - pkgFile: "testdata/dashboard.yml", - kind: KindDashboard, - validName: "dash_1", - }, { pkgFile: "testdata/label.yml", kind: KindLabel, diff --git a/pkger/parser.go b/pkger/parser.go index 42d84b2ad9..543fd54b67 100644 --- a/pkger/parser.go +++ b/pkger/parser.go @@ -409,11 +409,6 @@ func (p *Pkg) addObjectForRemoval(k Kind, pkgName string, id influxdb.ID) { } switch k { - case KindDashboard: - p.mDashboards[pkgName] = &dashboard{ - identity: newIdentity, - id: id, - } case KindLabel: p.mLabels[pkgName] = &label{ identity: newIdentity, @@ -429,11 +424,6 @@ func (p *Pkg) addObjectForRemoval(k Kind, pkgName string, id influxdb.ID) { func (p *Pkg) getObjectIDSetter(k Kind, pkgName string) (func(influxdb.ID), bool) { switch k { - case KindDashboard: - d, ok := p.mDashboards[pkgName] - return func(id influxdb.ID) { - d.id = id - }, ok case KindLabel: l, ok := p.mLabels[pkgName] return func(id influxdb.ID) { @@ -558,7 +548,7 @@ func (p *Pkg) dashboards() []*dashboard { for _, d := range p.mDashboards { dashes = append(dashes, d) } - sort.Slice(dashes, func(i, j int) bool { return dashes[i].Name() < dashes[j].Name() }) + sort.Slice(dashes, func(i, j int) bool { return dashes[i].PkgName() < dashes[j].PkgName() }) return dashes } diff --git a/pkger/parser_models.go b/pkger/parser_models.go index 7fc9e7f79d..79c6b7575b 100644 --- a/pkger/parser_models.go +++ b/pkger/parser_models.go @@ -307,6 +307,685 @@ func (c *check) valid() []validationErr { return nil } +// chartKind identifies what kind of chart is eluded too. Each +// chart kind has their own requirements for what constitutes +// a chart. +type chartKind string + +// available chart kinds +const ( + chartKindUnknown chartKind = "" + chartKindGauge chartKind = "gauge" + chartKindHeatMap chartKind = "heatmap" + chartKindHistogram chartKind = "histogram" + chartKindMarkdown chartKind = "markdown" + chartKindScatter chartKind = "scatter" + chartKindSingleStat chartKind = "single_stat" + chartKindSingleStatPlusLine chartKind = "single_stat_plus_line" + chartKindTable chartKind = "table" + chartKindXY chartKind = "xy" +) + +func (c chartKind) ok() bool { + switch c { + case chartKindGauge, chartKindHeatMap, chartKindHistogram, + chartKindMarkdown, chartKindScatter, chartKindSingleStat, + chartKindSingleStatPlusLine, chartKindTable, chartKindXY: + return true + default: + return false + } +} + +func (c chartKind) title() string { + spacedKind := strings.ReplaceAll(string(c), "_", " ") + return strings.ReplaceAll(strings.Title(spacedKind), " ", "_") +} + +const ( + fieldDashCharts = "charts" +) + +const dashboardNameMinLength = 2 + +type dashboard struct { + identity + + Description string + Charts []chart + + labels sortedLabels +} + +func (d *dashboard) Labels() []*label { + return d.labels +} + +func (d *dashboard) ResourceType() influxdb.ResourceType { + return KindDashboard.ResourceType() +} + +func (d *dashboard) summarize() SummaryDashboard { + iDash := SummaryDashboard{ + PkgName: d.PkgName(), + Name: d.Name(), + Description: d.Description, + LabelAssociations: toSummaryLabels(d.labels...), + } + for _, c := range d.Charts { + iDash.Charts = append(iDash.Charts, SummaryChart{ + Properties: c.properties(), + Height: c.Height, + Width: c.Width, + XPosition: c.XPos, + YPosition: c.YPos, + }) + } + return iDash +} + +func (d *dashboard) valid() []validationErr { + var vErrs []validationErr + if err, ok := isValidName(d.Name(), dashboardNameMinLength); !ok { + vErrs = append(vErrs, err) + } + if len(vErrs) == 0 { + return nil + } + return []validationErr{ + objectValidationErr(fieldSpec, vErrs...), + } +} + +const ( + fieldChartAxes = "axes" + fieldChartBinCount = "binCount" + fieldChartBinSize = "binSize" + fieldChartColors = "colors" + fieldChartDecimalPlaces = "decimalPlaces" + fieldChartDomain = "domain" + fieldChartGeom = "geom" + fieldChartHeight = "height" + fieldChartLegend = "legend" + fieldChartNote = "note" + fieldChartNoteOnEmpty = "noteOnEmpty" + fieldChartPosition = "position" + fieldChartQueries = "queries" + fieldChartShade = "shade" + fieldChartFieldOptions = "fieldOptions" + fieldChartTableOptions = "tableOptions" + fieldChartTickPrefix = "tickPrefix" + fieldChartTickSuffix = "tickSuffix" + fieldChartTimeFormat = "timeFormat" + fieldChartWidth = "width" + fieldChartXCol = "xCol" + fieldChartXPos = "xPos" + fieldChartYCol = "yCol" + fieldChartYPos = "yPos" +) + +type chart struct { + Kind chartKind + Name string + Prefix string + TickPrefix string + Suffix string + TickSuffix string + Note string + NoteOnEmpty bool + DecimalPlaces int + EnforceDecimals bool + Shade bool + Legend legend + Colors colors + Queries queries + Axes axes + Geom string + XCol, YCol string + XPos, YPos int + Height, Width int + BinSize int + BinCount int + Position string + FieldOptions []fieldOption + TableOptions tableOptions + TimeFormat string +} + +func (c chart) properties() influxdb.ViewProperties { + switch c.Kind { + case chartKindGauge: + return influxdb.GaugeViewProperties{ + Type: influxdb.ViewPropertyTypeGauge, + Queries: c.Queries.influxDashQueries(), + Prefix: c.Prefix, + TickPrefix: c.TickPrefix, + Suffix: c.Suffix, + TickSuffix: c.TickSuffix, + ViewColors: c.Colors.influxViewColors(), + DecimalPlaces: influxdb.DecimalPlaces{ + IsEnforced: c.EnforceDecimals, + Digits: int32(c.DecimalPlaces), + }, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + } + case chartKindHeatMap: + return influxdb.HeatmapViewProperties{ + Type: influxdb.ViewPropertyTypeHeatMap, + Queries: c.Queries.influxDashQueries(), + ViewColors: c.Colors.strings(), + BinSize: int32(c.BinSize), + XColumn: c.XCol, + YColumn: c.YCol, + XDomain: c.Axes.get("x").Domain, + YDomain: c.Axes.get("y").Domain, + XPrefix: c.Axes.get("x").Prefix, + YPrefix: c.Axes.get("y").Prefix, + XSuffix: c.Axes.get("x").Suffix, + YSuffix: c.Axes.get("y").Suffix, + XAxisLabel: c.Axes.get("x").Label, + YAxisLabel: c.Axes.get("y").Label, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + TimeFormat: c.TimeFormat, + } + case chartKindHistogram: + return influxdb.HistogramViewProperties{ + Type: influxdb.ViewPropertyTypeHistogram, + Queries: c.Queries.influxDashQueries(), + ViewColors: c.Colors.influxViewColors(), + FillColumns: []string{}, + XColumn: c.XCol, + XDomain: c.Axes.get("x").Domain, + XAxisLabel: c.Axes.get("x").Label, + Position: c.Position, + BinCount: c.BinCount, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + } + case chartKindMarkdown: + return influxdb.MarkdownViewProperties{ + Type: influxdb.ViewPropertyTypeMarkdown, + Note: c.Note, + } + case chartKindScatter: + return influxdb.ScatterViewProperties{ + Type: influxdb.ViewPropertyTypeScatter, + Queries: c.Queries.influxDashQueries(), + ViewColors: c.Colors.strings(), + XColumn: c.XCol, + YColumn: c.YCol, + XDomain: c.Axes.get("x").Domain, + YDomain: c.Axes.get("y").Domain, + XPrefix: c.Axes.get("x").Prefix, + YPrefix: c.Axes.get("y").Prefix, + XSuffix: c.Axes.get("x").Suffix, + YSuffix: c.Axes.get("y").Suffix, + XAxisLabel: c.Axes.get("x").Label, + YAxisLabel: c.Axes.get("y").Label, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + TimeFormat: c.TimeFormat, + } + case chartKindSingleStat: + return influxdb.SingleStatViewProperties{ + Type: influxdb.ViewPropertyTypeSingleStat, + Prefix: c.Prefix, + TickPrefix: c.TickPrefix, + Suffix: c.Suffix, + TickSuffix: c.TickSuffix, + DecimalPlaces: influxdb.DecimalPlaces{ + IsEnforced: c.EnforceDecimals, + Digits: int32(c.DecimalPlaces), + }, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + Queries: c.Queries.influxDashQueries(), + ViewColors: c.Colors.influxViewColors(), + } + case chartKindSingleStatPlusLine: + return influxdb.LinePlusSingleStatProperties{ + Type: influxdb.ViewPropertyTypeSingleStatPlusLine, + Prefix: c.Prefix, + Suffix: c.Suffix, + DecimalPlaces: influxdb.DecimalPlaces{ + IsEnforced: c.EnforceDecimals, + Digits: int32(c.DecimalPlaces), + }, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + XColumn: c.XCol, + YColumn: c.YCol, + ShadeBelow: c.Shade, + Legend: c.Legend.influxLegend(), + Queries: c.Queries.influxDashQueries(), + ViewColors: c.Colors.influxViewColors(), + Axes: c.Axes.influxAxes(), + Position: c.Position, + } + case chartKindTable: + fieldOptions := make([]influxdb.RenamableField, 0, len(c.FieldOptions)) + for _, fieldOpt := range c.FieldOptions { + fieldOptions = append(fieldOptions, influxdb.RenamableField{ + InternalName: fieldOpt.FieldName, + DisplayName: fieldOpt.DisplayName, + Visible: fieldOpt.Visible, + }) + } + + return influxdb.TableViewProperties{ + Type: influxdb.ViewPropertyTypeTable, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + DecimalPlaces: influxdb.DecimalPlaces{ + IsEnforced: c.EnforceDecimals, + Digits: int32(c.DecimalPlaces), + }, + Queries: c.Queries.influxDashQueries(), + ViewColors: c.Colors.influxViewColors(), + TableOptions: influxdb.TableOptions{ + VerticalTimeAxis: c.TableOptions.VerticalTimeAxis, + SortBy: influxdb.RenamableField{ + InternalName: c.TableOptions.SortByField, + }, + Wrapping: c.TableOptions.Wrapping, + FixFirstColumn: c.TableOptions.FixFirstColumn, + }, + FieldOptions: fieldOptions, + TimeFormat: c.TimeFormat, + } + case chartKindXY: + return influxdb.XYViewProperties{ + Type: influxdb.ViewPropertyTypeXY, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + XColumn: c.XCol, + YColumn: c.YCol, + ShadeBelow: c.Shade, + Legend: c.Legend.influxLegend(), + Queries: c.Queries.influxDashQueries(), + ViewColors: c.Colors.influxViewColors(), + Axes: c.Axes.influxAxes(), + Geom: c.Geom, + Position: c.Position, + TimeFormat: c.TimeFormat, + } + default: + return nil + } +} + +func (c chart) validProperties() []validationErr { + if c.Kind == chartKindMarkdown { + // at the time of writing, there's nothing to validate for markdown types + return nil + } + + var fails []validationErr + + validatorFns := []func() []validationErr{ + c.validBaseProps, + c.Queries.valid, + c.Colors.valid, + } + for _, validatorFn := range validatorFns { + fails = append(fails, validatorFn()...) + } + + // chart kind specific validations + switch c.Kind { + case chartKindGauge: + fails = append(fails, c.Colors.hasTypes(colorTypeMin, colorTypeMax)...) + case chartKindHeatMap: + fails = append(fails, c.Axes.hasAxes("x", "y")...) + case chartKindHistogram: + fails = append(fails, c.Axes.hasAxes("x")...) + case chartKindScatter: + fails = append(fails, c.Axes.hasAxes("x", "y")...) + case chartKindSingleStat: + case chartKindSingleStatPlusLine: + fails = append(fails, c.Axes.hasAxes("x", "y")...) + fails = append(fails, validPosition(c.Position)...) + case chartKindTable: + fails = append(fails, validTableOptions(c.TableOptions)...) + case chartKindXY: + fails = append(fails, validGeometry(c.Geom)...) + fails = append(fails, c.Axes.hasAxes("x", "y")...) + fails = append(fails, validPosition(c.Position)...) + } + + return fails +} + +func validPosition(pos string) []validationErr { + pos = strings.ToLower(pos) + if pos != "" && pos != "overlaid" && pos != "stacked" { + return []validationErr{{ + Field: fieldChartPosition, + Msg: fmt.Sprintf("invalid position supplied %q; valid positions is one of [overlaid, stacked]", pos), + }} + } + return nil +} + +var geometryTypes = map[string]bool{ + "line": true, + "step": true, + "stacked": true, + "monotoneX": true, + "bar": true, +} + +func validGeometry(geom string) []validationErr { + if !geometryTypes[geom] { + msg := "type not found" + if geom != "" { + msg = "type provided is not supported" + } + return []validationErr{{ + Field: fieldChartGeom, + Msg: fmt.Sprintf("%s: %q", msg, geom), + }} + } + + 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" + fieldChartFieldOptionVisible = "visible" +) + +type fieldOption struct { + FieldName string + DisplayName string + Visible bool +} + +const ( + fieldChartTableOptionVerticalTimeAxis = "verticalTimeAxis" + fieldChartTableOptionSortBy = "sortBy" + fieldChartTableOptionWrapping = "wrapping" + fieldChartTableOptionFixFirstColumn = "fixFirstColumn" +) + +type tableOptions struct { + VerticalTimeAxis bool + SortByField string + Wrapping string + FixFirstColumn bool +} + +func validTableOptions(opts tableOptions) []validationErr { + var fails []validationErr + + switch opts.Wrapping { + case "", "single-line", "truncate", "wrap": + default: + fails = append(fails, validationErr{ + Field: fieldChartTableOptionWrapping, + Msg: `chart table option should 1 in ["single-line", "truncate", "wrap"]`, + }) + } + + if len(fails) == 0 { + return nil + } + + return []validationErr{ + { + Field: fieldChartTableOptions, + Nested: fails, + }, + } +} + +const ( + colorTypeBackground = "background" + colorTypeMin = "min" + colorTypeMax = "max" + colorTypeScale = "scale" + colorTypeText = "text" + colorTypeThreshold = "threshold" +) + +const ( + fieldColorHex = "hex" +) + +type color struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Type string `json:"type,omitempty" yaml:"type,omitempty"` + Hex string `json:"hex,omitempty" yaml:"hex,omitempty"` + // using reference for Value here so we can set to nil and + // it will be ignored during encoding, keeps our exported pkgs + // clear of unneeded entries. + Value *float64 `json:"value,omitempty" yaml:"value,omitempty"` +} + +// TODO: +// - verify templates are desired +// - template colors so references can be shared +type colors []*color + +func (c colors) influxViewColors() []influxdb.ViewColor { + ptrToFloat64 := func(f *float64) float64 { + if f == nil { + return 0 + } + return *f + } + + var iColors []influxdb.ViewColor + for _, cc := range c { + iColors = append(iColors, influxdb.ViewColor{ + Type: cc.Type, + Hex: cc.Hex, + Name: cc.Name, + Value: ptrToFloat64(cc.Value), + }) + } + return iColors +} + +func (c colors) strings() []string { + clrs := []string{} + + for _, clr := range c { + clrs = append(clrs, clr.Hex) + } + + return clrs +} + +// TODO: looks like much of these are actually getting defaults in +// the UI. looking at sytem charts, seeign lots of failures for missing +// color types or no colors at all. +func (c colors) hasTypes(types ...string) []validationErr { + tMap := make(map[string]bool) + for _, cc := range c { + tMap[cc.Type] = true + } + + var failures []validationErr + for _, t := range types { + if !tMap[t] { + failures = append(failures, validationErr{ + Field: "colors", + Msg: fmt.Sprintf("type not found: %q", t), + }) + } + } + + return failures +} + +func (c colors) valid() []validationErr { + var fails []validationErr + for i, cc := range c { + cErr := validationErr{ + Field: fieldChartColors, + Index: intPtr(i), + } + if cc.Hex == "" { + cErr.Nested = append(cErr.Nested, validationErr{ + Field: fieldColorHex, + Msg: "a color must have a hex value provided", + }) + } + if len(cErr.Nested) > 0 { + fails = append(fails, cErr) + } + } + + return fails +} + +type query struct { + Query string `json:"query" yaml:"query"` +} + +type queries []query + +func (q queries) influxDashQueries() []influxdb.DashboardQuery { + var iQueries []influxdb.DashboardQuery + for _, qq := range q { + newQuery := influxdb.DashboardQuery{ + Text: qq.Query, + 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) valid() []validationErr { + var fails []validationErr + if len(q) == 0 { + fails = append(fails, validationErr{ + Field: fieldChartQueries, + Msg: "at least 1 query must be provided", + }) + } + + for i, qq := range q { + qErr := validationErr{ + Field: fieldChartQueries, + Index: intPtr(i), + } + if qq.Query == "" { + qErr.Nested = append(fails, validationErr{ + Field: fieldQuery, + Msg: "a query must be provided", + }) + } + if len(qErr.Nested) > 0 { + fails = append(fails, qErr) + } + } + + return fails +} + +const ( + fieldAxisBase = "base" + fieldAxisLabel = "label" + fieldAxisScale = "scale" +) + +type axis struct { + Base string `json:"base,omitempty" yaml:"base,omitempty"` + Label string `json:"label,omitempty" yaml:"label,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"` + Scale string `json:"scale,omitempty" yaml:"scale,omitempty"` + Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"` + Domain []float64 `json:"domain,omitempty" yaml:"domain,omitempty"` +} + +type axes []axis + +func (a axes) get(name string) axis { + for _, ax := range a { + if name == ax.Name { + return ax + } + } + return axis{} +} + +func (a axes) influxAxes() map[string]influxdb.Axis { + m := make(map[string]influxdb.Axis) + for _, ax := range a { + m[ax.Name] = influxdb.Axis{ + Bounds: []string{}, + Label: ax.Label, + Prefix: ax.Prefix, + Suffix: ax.Suffix, + Base: ax.Base, + Scale: ax.Scale, + } + } + return m +} + +func (a axes) hasAxes(expectedAxes ...string) []validationErr { + mAxes := make(map[string]bool) + for _, ax := range a { + mAxes[ax.Name] = true + } + + var failures []validationErr + for _, expected := range expectedAxes { + if !mAxes[expected] { + failures = append(failures, validationErr{ + Field: fieldChartAxes, + Msg: fmt.Sprintf("axis not found: %q", expected), + }) + } + } + + return failures +} + +const ( + fieldLegendOrientation = "orientation" +) + +type legend struct { + Orientation string `json:"orientation,omitempty" yaml:"orientation,omitempty"` + Type string `json:"type" yaml:"type"` +} + +func (l legend) influxLegend() influxdb.Legend { + return influxdb.Legend{ + Type: l.Type, + Orientation: l.Orientation, + } +} + type assocMapKey struct { resType influxdb.ResourceType name string diff --git a/pkger/parser_test.go b/pkger/parser_test.go index 888e1c5e14..a469c49a35 100644 --- a/pkger/parser_test.go +++ b/pkger/parser_test.go @@ -1662,9 +1662,10 @@ spec: t.Run("happy path", func(t *testing.T) { testfileRunner(t, "testdata/dashboard", func(t *testing.T, pkg *Pkg) { sum := pkg.Summary() - require.Len(t, sum.Dashboards, 1) + require.Len(t, sum.Dashboards, 2) actual := sum.Dashboards[0] + assert.Equal(t, "dash_1", actual.PkgName) assert.Equal(t, "display name", actual.Name) assert.Equal(t, "desc1", actual.Description) @@ -1699,6 +1700,11 @@ spec: assert.Equal(t, "text", c.Type) assert.Equal(t, "#8F8AF4", c.Hex) assert.Equal(t, 3.0, c.Value) + + actual2 := sum.Dashboards[1] + assert.Equal(t, "dash_2", actual2.PkgName) + assert.Equal(t, "dash_2", actual2.Name) + assert.Equal(t, "desc", actual2.Description) }) }) @@ -2600,18 +2606,32 @@ spec: actual := sum.Dashboards[0] assert.Equal(t, "dash_1", actual.Name) - require.Len(t, actual.LabelAssociations, 1) - actualLabel := actual.LabelAssociations[0] - assert.Equal(t, "label_1", actualLabel.Name) + require.Len(t, actual.LabelAssociations, 2) + assert.Equal(t, "label_1", actual.LabelAssociations[0].Name) + assert.Equal(t, "label_2", actual.LabelAssociations[1].Name) - assert.Contains(t, sum.LabelMappings, SummaryLabelMapping{ - Status: StateStatusNew, - ResourceType: influxdb.DashboardsResourceType, - ResourcePkgName: "dash_1", - ResourceName: "dash_1", - LabelPkgName: "label_1", - LabelName: "label_1", - }) + expectedMappings := []SummaryLabelMapping{ + { + Status: StateStatusNew, + ResourceType: influxdb.DashboardsResourceType, + ResourcePkgName: "dash_1", + ResourceName: "dash_1", + LabelPkgName: "label_1", + LabelName: "label_1", + }, + { + Status: StateStatusNew, + ResourceType: influxdb.DashboardsResourceType, + ResourcePkgName: "dash_1", + ResourceName: "dash_1", + LabelPkgName: "label_2", + LabelName: "label_2", + }, + } + + for _, expectedMapping := range expectedMappings { + assert.Contains(t, sum.LabelMappings, expectedMapping) + } }) }) diff --git a/pkger/service.go b/pkger/service.go index 2e98b962f9..c3dc24f976 100644 --- a/pkger/service.go +++ b/pkger/service.go @@ -686,8 +686,6 @@ func (s *Service) dryRun(ctx context.Context, orgID influxdb.ID, pkg *Pkg, opts } var diff Diff - diff.Dashboards = s.dryRunDashboards(pkg) - diffRules, err := s.dryRunNotificationRules(ctx, orgID, pkg) if err != nil { return Summary{}, Diff{}, nil, err @@ -710,6 +708,7 @@ func (s *Service) dryRun(ctx context.Context, orgID influxdb.ID, pkg *Pkg, opts diff.Buckets = stateDiff.Buckets diff.Checks = stateDiff.Checks + diff.Dashboards = stateDiff.Dashboards diff.NotificationEndpoints = stateDiff.NotificationEndpoints diff.Labels = stateDiff.Labels diff.Tasks = stateDiff.Tasks @@ -757,16 +756,6 @@ func (s *Service) dryRunChecks(ctx context.Context, orgID influxdb.ID, checks ma } } -func (s *Service) dryRunDashboards(pkg *Pkg) []DiffDashboard { - dashs := pkg.dashboards() - - diffs := make([]DiffDashboard, 0, len(dashs)) - for _, d := range dashs { - diffs = append(diffs, newDiffDashboard(d)) - } - return diffs -} - func (s *Service) dryRunLabels(ctx context.Context, orgID influxdb.ID, labels map[string]*stateLabel) { for _, pkgLabel := range labels { pkgLabel.orgID = orgID @@ -941,7 +930,6 @@ type ( func (s *Service) dryRunLabelMappings(ctx context.Context, pkg *Pkg, state *stateCoordinator) ([]DiffLabelMapping, error) { mappers := []labelMappers{ - mapperDashboards(pkg.dashboards()), mapperNotificationRules(pkg.notificationRules()), } @@ -1068,6 +1056,17 @@ func (s *Service) dryRunLabelMappingsV2(ctx context.Context, state *stateCoordin mappings = append(mappings, mm...) } + for _, d := range state.mDashboards { + if IsRemoval(d.stateStatus) { + continue + } + mm, err := s.dryRunResourceLabelMappingV2(ctx, state, stateLabelsByResName, d) + if err != nil { + return nil, err + } + mappings = append(mappings, mm...) + } + for _, e := range state.mEndpoints { if IsRemoval(e.stateStatus) { continue @@ -1323,7 +1322,7 @@ func (s *Service) apply(ctx context.Context, coordinator *rollbackCoordinator, o s.applyVariables(ctx, state.variables()), s.applyBuckets(ctx, state.buckets()), s.applyChecks(ctx, state.checks()), - s.applyDashboards(pkg.dashboards()), + s.applyDashboards(state.dashboards()), s.applyNotificationEndpoints(ctx, userID, state.endpoints()), s.applyTasks(state.tasks()), s.applyTelegrafs(state.telegrafConfigs()), @@ -1583,23 +1582,23 @@ func (s *Service) applyCheck(ctx context.Context, c *stateCheck, userID influxdb } } -func (s *Service) applyDashboards(dashboards []*dashboard) applier { +func (s *Service) applyDashboards(dashboards []*stateDashboard) applier { const resource = "dashboard" mutex := new(doMutex) - rollbackDashboards := make([]*dashboard, 0, len(dashboards)) + rollbackDashboards := make([]*stateDashboard, 0, len(dashboards)) createFn := func(ctx context.Context, i int, orgID, userID influxdb.ID) *applyErrBody { - var d dashboard + var d *stateDashboard mutex.Do(func() { - dashboards[i].OrgID = orgID - d = *dashboards[i] + dashboards[i].orgID = orgID + d = dashboards[i] }) influxBucket, err := s.applyDashboard(ctx, d) if err != nil { return &applyErrBody{ - name: d.Name(), + name: d.parserDash.Name(), msg: err.Error(), } } @@ -1627,12 +1626,12 @@ func (s *Service) applyDashboards(dashboards []*dashboard) applier { } } -func (s *Service) applyDashboard(ctx context.Context, d dashboard) (influxdb.Dashboard, error) { - cells := convertChartsToCells(d.Charts) +func (s *Service) applyDashboard(ctx context.Context, d *stateDashboard) (influxdb.Dashboard, error) { + cells := convertChartsToCells(d.parserDash.Charts) influxDashboard := influxdb.Dashboard{ - OrganizationID: d.OrgID, - Description: d.Description, - Name: d.Name(), + OrganizationID: d.orgID, + Description: d.parserDash.Description, + Name: d.parserDash.Name(), Cells: cells, } err := s.dashSVC.CreateDashboard(ctx, &influxDashboard) @@ -2615,6 +2614,7 @@ func newSummaryFromStatePkg(pkg *Pkg, state *stateCoordinator) Summary { pkgSum := pkg.Summary() pkgSum.Buckets = stateSum.Buckets pkgSum.Checks = stateSum.Checks + pkgSum.Dashboards = stateSum.Dashboards pkgSum.NotificationEndpoints = stateSum.NotificationEndpoints pkgSum.Labels = stateSum.Labels pkgSum.Tasks = stateSum.Tasks @@ -2626,6 +2626,7 @@ func newSummaryFromStatePkg(pkg *Pkg, state *stateCoordinator) Summary { resourcesToSkip := map[influxdb.ResourceType]bool{ influxdb.BucketsResourceType: true, influxdb.ChecksResourceType: true, + influxdb.DashboardsResourceType: true, influxdb.NotificationEndpointResourceType: true, influxdb.TasksResourceType: true, influxdb.TelegrafsResourceType: true, diff --git a/pkger/service_models.go b/pkger/service_models.go index 0d289ccb8e..52e1003a26 100644 --- a/pkger/service_models.go +++ b/pkger/service_models.go @@ -8,26 +8,28 @@ import ( ) type stateCoordinator struct { - mBuckets map[string]*stateBucket - mChecks map[string]*stateCheck - mEndpoints map[string]*stateEndpoint - mLabels map[string]*stateLabel - mTasks map[string]*stateTask - mTelegrafs map[string]*stateTelegraf - mVariables map[string]*stateVariable + mBuckets map[string]*stateBucket + mChecks map[string]*stateCheck + mDashboards map[string]*stateDashboard + mEndpoints map[string]*stateEndpoint + mLabels map[string]*stateLabel + mTasks map[string]*stateTask + mTelegrafs map[string]*stateTelegraf + mVariables map[string]*stateVariable labelMappings []stateLabelMapping } func newStateCoordinator(pkg *Pkg) *stateCoordinator { state := stateCoordinator{ - mBuckets: make(map[string]*stateBucket), - mChecks: make(map[string]*stateCheck), - mEndpoints: make(map[string]*stateEndpoint), - mLabels: make(map[string]*stateLabel), - mTasks: make(map[string]*stateTask), - mTelegrafs: make(map[string]*stateTelegraf), - mVariables: make(map[string]*stateVariable), + mBuckets: make(map[string]*stateBucket), + mChecks: make(map[string]*stateCheck), + mDashboards: make(map[string]*stateDashboard), + mEndpoints: make(map[string]*stateEndpoint), + mLabels: make(map[string]*stateLabel), + mTasks: make(map[string]*stateTask), + mTelegrafs: make(map[string]*stateTelegraf), + mVariables: make(map[string]*stateVariable), } for _, pkgBkt := range pkg.buckets() { @@ -42,6 +44,12 @@ func newStateCoordinator(pkg *Pkg) *stateCoordinator { stateStatus: StateStatusNew, } } + for _, pkgDash := range pkg.dashboards() { + state.mDashboards[pkgDash.PkgName()] = &stateDashboard{ + parserDash: pkgDash, + stateStatus: StateStatusNew, + } + } for _, pkgEndpoint := range pkg.notificationEndpoints() { state.mEndpoints[pkgEndpoint.PkgName()] = &stateEndpoint{ parserEndpoint: pkgEndpoint, @@ -92,6 +100,14 @@ func (s *stateCoordinator) checks() []*stateCheck { return out } +func (s *stateCoordinator) dashboards() []*stateDashboard { + out := make([]*stateDashboard, 0, len(s.mDashboards)) + for _, d := range s.mDashboards { + out = append(out, d) + } + return out +} + func (s *stateCoordinator) endpoints() []*stateEndpoint { out := make([]*stateEndpoint, 0, len(s.mEndpoints)) for _, e := range s.mEndpoints { @@ -148,6 +164,13 @@ func (s *stateCoordinator) diff() Diff { return diff.Checks[i].PkgName < diff.Checks[j].PkgName }) + for _, d := range s.mDashboards { + diff.Dashboards = append(diff.Dashboards, d.diffDashboard()) + } + sort.Slice(diff.Dashboards, func(i, j int) bool { + return diff.Dashboards[i].PkgName < diff.Dashboards[j].PkgName + }) + for _, e := range s.mEndpoints { diff.NotificationEndpoints = append(diff.NotificationEndpoints, e.diffEndpoint()) } @@ -228,12 +251,25 @@ func (s *stateCoordinator) summary() Summary { return sum.Checks[i].PkgName < sum.Checks[j].PkgName }) + for _, d := range s.mDashboards { + if IsRemoval(d.stateStatus) { + continue + } + sum.Dashboards = append(sum.Dashboards, d.summarize()) + } + sort.Slice(sum.Dashboards, func(i, j int) bool { + return sum.Dashboards[i].PkgName < sum.Dashboards[j].PkgName + }) + for _, e := range s.mEndpoints { if IsRemoval(e.stateStatus) { continue } sum.NotificationEndpoints = append(sum.NotificationEndpoints, e.summarize()) } + sort.Slice(sum.NotificationEndpoints, func(i, j int) bool { + return sum.NotificationEndpoints[i].PkgName < sum.NotificationEndpoints[j].PkgName + }) for _, v := range s.mLabels { if IsRemoval(v.stateStatus) { @@ -569,6 +605,99 @@ func (c *stateCheck) summarize() SummaryCheck { return sum } +type stateDashboard struct { + id, orgID influxdb.ID + stateStatus StateStatus + + parserDash *dashboard + existing *influxdb.Dashboard +} + +func (d *stateDashboard) ID() influxdb.ID { + if IsExisting(d.stateStatus) && d.existing != nil { + return d.existing.ID + } + return d.id +} + +func (d *stateDashboard) labels() []*label { + return d.parserDash.labels +} + +func (d *stateDashboard) resourceType() influxdb.ResourceType { + return KindDashboard.ResourceType() +} + +func (d *stateDashboard) stateIdentity() stateIdentity { + return stateIdentity{ + id: d.ID(), + name: d.parserDash.Name(), + pkgName: d.parserDash.PkgName(), + resourceType: d.resourceType(), + stateStatus: d.stateStatus, + } +} + +func (d *stateDashboard) diffDashboard() DiffDashboard { + diff := DiffDashboard{ + DiffIdentifier: DiffIdentifier{ + ID: SafeID(d.ID()), + Remove: IsRemoval(d.stateStatus), + StateStatus: d.stateStatus, + PkgName: d.parserDash.PkgName(), + }, + New: DiffDashboardValues{ + Name: d.parserDash.Name(), + Desc: d.parserDash.Description, + Charts: make([]DiffChart, 0, len(d.parserDash.Charts)), + }, + } + + for _, c := range d.parserDash.Charts { + diff.New.Charts = append(diff.New.Charts, DiffChart{ + Properties: c.properties(), + Height: c.Height, + Width: c.Width, + }) + } + + if d.existing == nil { + return diff + } + + oldDiff := DiffDashboardValues{ + Name: d.existing.Name, + Desc: d.existing.Description, + Charts: make([]DiffChart, 0, len(d.existing.Cells)), + } + + for _, c := range d.existing.Cells { + var props influxdb.ViewProperties + if c.View != nil { + props = c.View.Properties + } + + oldDiff.Charts = append(oldDiff.Charts, DiffChart{ + Properties: props, + XPosition: int(c.X), + YPosition: int(c.Y), + Height: int(c.H), + Width: int(c.W), + }) + } + + diff.Old = &oldDiff + + return diff +} + +func (d *stateDashboard) summarize() SummaryDashboard { + sum := d.parserDash.summarize() + sum.ID = SafeID(d.ID()) + sum.OrgID = SafeID(d.orgID) + return sum +} + type stateLabel struct { id, orgID influxdb.ID stateStatus StateStatus diff --git a/pkger/service_test.go b/pkger/service_test.go index 7899f3f542..9d4ccfba9f 100644 --- a/pkger/service_test.go +++ b/pkger/service_test.go @@ -829,12 +829,19 @@ func TestService(t *testing.T) { sum, _, err := svc.Apply(context.TODO(), orgID, 0, pkg) require.NoError(t, err) - require.Len(t, sum.Dashboards, 1) + require.Len(t, sum.Dashboards, 2) dash1 := sum.Dashboards[0] - assert.Equal(t, SafeID(1), dash1.ID) - assert.Equal(t, SafeID(orgID), dash1.OrgID) + assert.NotZero(t, dash1.ID) + assert.NotZero(t, dash1.OrgID) + assert.Equal(t, "dash_1", dash1.PkgName) assert.Equal(t, "display name", dash1.Name) require.Len(t, dash1.Charts, 1) + + dash2 := sum.Dashboards[1] + assert.NotZero(t, dash2.ID) + assert.Equal(t, "dash_2", dash2.PkgName) + assert.Equal(t, "dash_2", dash2.Name) + require.Empty(t, dash2.Charts) }) }) @@ -855,8 +862,6 @@ func TestService(t *testing.T) { return nil } - pkg.mDashboards["dash_1_copy"] = pkg.mDashboards["dash_1"] - svc := newTestService(WithDashboardSVC(fakeDashSVC)) orgID := influxdb.ID(9000) @@ -1071,19 +1076,22 @@ func TestService(t *testing.T) { }) t.Run("maps dashboards with labels", func(t *testing.T) { - testLabelMappingFn( - t, - "testdata/dashboard_associates_label.yml", - 1, - func() []ServiceSetterFn { - fakeDashSVC := mock.NewDashboardService() - fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error { - d.ID = influxdb.ID(rand.Int()) - return nil - } - return []ServiceSetterFn{WithDashboardSVC(fakeDashSVC)} - }, - ) + opts := func() []ServiceSetterFn { + fakeDashSVC := mock.NewDashboardService() + fakeDashSVC.CreateDashboardF = func(_ context.Context, d *influxdb.Dashboard) error { + d.ID = influxdb.ID(rand.Int()) + return nil + } + return []ServiceSetterFn{WithDashboardSVC(fakeDashSVC)} + } + + t.Run("applies successfully", func(t *testing.T) { + testLabelMappingV2ApplyFn(t, "testdata/dashboard_associates_label.yml", 2, opts) + }) + + t.Run("deletes new label mappings on error", func(t *testing.T) { + testLabelMappingV2RollbackFn(t, "testdata/dashboard_associates_label.yml", 1, opts) + }) }) t.Run("maps notification endpoints with labels", func(t *testing.T) { diff --git a/pkger/testdata/dashboard.json b/pkger/testdata/dashboard.json index 54624a152e..a8b05557e7 100644 --- a/pkger/testdata/dashboard.json +++ b/pkger/testdata/dashboard.json @@ -42,5 +42,15 @@ } ] } + }, + { + "apiVersion": "influxdata.com/v2alpha1", + "kind": "Dashboard", + "metadata": { + "name": "dash_2" + }, + "spec": { + "description": "desc" + } } ] diff --git a/pkger/testdata/dashboard.yml b/pkger/testdata/dashboard.yml index 9ae8c5996e..5513d0d469 100644 --- a/pkger/testdata/dashboard.yml +++ b/pkger/testdata/dashboard.yml @@ -27,3 +27,10 @@ spec: type: text hex: "#8F8AF4" value: 3 +--- +apiVersion: influxdata.com/v2alpha1 +kind: Dashboard +metadata: + name: dash_2 +spec: + description: desc diff --git a/pkger/testdata/dashboard_associates_label.json b/pkger/testdata/dashboard_associates_label.json index 67dcf7f7e1..b0b8352f09 100644 --- a/pkger/testdata/dashboard_associates_label.json +++ b/pkger/testdata/dashboard_associates_label.json @@ -6,6 +6,13 @@ "name": "label_1" } }, + { + "apiVersion": "influxdata.com/v2alpha1", + "kind": "Label", + "metadata": { + "name": "label_2" + } + }, { "apiVersion": "influxdata.com/v2alpha1", "kind": "Dashboard", @@ -17,6 +24,10 @@ { "kind": "Label", "name": "label_1" + }, + { + "kind": "Label", + "name": "label_2" } ] } diff --git a/pkger/testdata/dashboard_associates_label.yml b/pkger/testdata/dashboard_associates_label.yml index 98c2e8087f..369cffd0ef 100644 --- a/pkger/testdata/dashboard_associates_label.yml +++ b/pkger/testdata/dashboard_associates_label.yml @@ -1,10 +1,14 @@ ---- apiVersion: influxdata.com/v2alpha1 kind: Label metadata: name: label_1 --- apiVersion: influxdata.com/v2alpha1 +kind: Label +metadata: + name: label_2 +--- +apiVersion: influxdata.com/v2alpha1 kind: Dashboard metadata: name: dash_1 @@ -12,3 +16,5 @@ spec: associations: - kind: Label name: label_1 + - kind: Label + name: label_2