diff --git a/dashboard.go b/dashboard.go index bc66440487..d2fc8cddfd 100644 --- a/dashboard.go +++ b/dashboard.go @@ -299,6 +299,21 @@ type ViewContents struct { Name string `json:"name"` } +// Values for all supported view property types. +const ( + ViewPropertyTypeCheck = "check" + ViewPropertyTypeGauge = "gauge" + ViewPropertyTypeHeatMap = "heatmap" + ViewPropertyTypeHistogram = "histogram" + ViewPropertyTypeLogViewer = "log-viewer" + ViewPropertyTypeMarkdown = "markdown" + ViewPropertyTypeScatter = "scatter" + ViewPropertyTypeSingleStat = "single-stat" + ViewPropertyTypeSingleStatPlusLine = "line-plus-single-stat" + ViewPropertyTypeTable = "table" + ViewPropertyTypeXY = "xy" +) + // ViewProperties is used to mark other structures as conforming to a View. type ViewProperties interface { viewProperties() @@ -340,67 +355,67 @@ func UnmarshalViewPropertiesJSON(b []byte) (ViewProperties, error) { switch t.Shape { case "chronograf-v2": switch t.Type { - case "check": + case ViewPropertyTypeCheck: var cv CheckViewProperties if err := json.Unmarshal(v.B, &cv); err != nil { return nil, err } vis = cv - case "xy": + case ViewPropertyTypeXY: var xyv XYViewProperties if err := json.Unmarshal(v.B, &xyv); err != nil { return nil, err } vis = xyv - case "single-stat": + case ViewPropertyTypeSingleStat: var ssv SingleStatViewProperties if err := json.Unmarshal(v.B, &ssv); err != nil { return nil, err } vis = ssv - case "gauge": + case ViewPropertyTypeGauge: var gv GaugeViewProperties if err := json.Unmarshal(v.B, &gv); err != nil { return nil, err } vis = gv - case "table": + case ViewPropertyTypeTable: var tv TableViewProperties if err := json.Unmarshal(v.B, &tv); err != nil { return nil, err } vis = tv - case "markdown": + case ViewPropertyTypeMarkdown: var mv MarkdownViewProperties if err := json.Unmarshal(v.B, &mv); err != nil { return nil, err } vis = mv - case "log-viewer": // happens in log viewer stays in log viewer. + case ViewPropertyTypeLogViewer: // happens in log viewer stays in log viewer. var lv LogViewProperties if err := json.Unmarshal(v.B, &lv); err != nil { return nil, err } vis = lv - case "line-plus-single-stat": + case ViewPropertyTypeSingleStatPlusLine: var lv LinePlusSingleStatProperties if err := json.Unmarshal(v.B, &lv); err != nil { return nil, err } vis = lv - case "histogram": + case ViewPropertyTypeHistogram: var hv HistogramViewProperties if err := json.Unmarshal(v.B, &hv); err != nil { return nil, err } vis = hv - case "heatmap": + case ViewPropertyTypeHeatMap: var hv HeatmapViewProperties if err := json.Unmarshal(v.B, &hv); err != nil { return nil, err } vis = hv - case "scatter": + case ViewPropertyTypeScatter: var sv ScatterViewProperties if err := json.Unmarshal(v.B, &sv); err != nil { return nil, err diff --git a/http/pkger_http_server.go b/http/pkger_http_server.go index e7a83d7e3b..abf284b5cb 100644 --- a/http/pkger_http_server.go +++ b/http/pkger_http_server.go @@ -54,11 +54,13 @@ type ReqCreatePkg struct { PkgName string `json:"pkgName"` PkgDescription string `json:"pkgDescription"` PkgVersion string `json:"pkgVersion"` + + Resources []pkger.ResourceToClone `json:"resources"` } // RespCreatePkg is a response body for the create pkg endpoint. type RespCreatePkg struct { - Package *pkger.Pkg `json:"package"` + *pkger.Pkg } func (s *HandlerPkg) createPkg(w http.ResponseWriter, r *http.Request) { @@ -75,6 +77,7 @@ func (s *HandlerPkg) createPkg(w http.ResponseWriter, r *http.Request) { Name: reqBody.PkgName, Version: reqBody.PkgVersion, }), + pkger.WithResourceClones(reqBody.Resources...), ) if err != nil { s.HandleHTTPError(r.Context(), err, w) @@ -82,7 +85,7 @@ func (s *HandlerPkg) createPkg(w http.ResponseWriter, r *http.Request) { } s.encResp(r.Context(), w, http.StatusOK, RespCreatePkg{ - Package: newPkg, + Pkg: newPkg, }) } diff --git a/http/pkger_http_server_test.go b/http/pkger_http_server_test.go index 279dc85079..75faf12f19 100644 --- a/http/pkger_http_server_test.go +++ b/http/pkger_http_server_test.go @@ -11,6 +11,7 @@ import ( "github.com/go-chi/chi" "github.com/influxdata/influxdb" fluxTTP "github.com/influxdata/influxdb/http" + "github.com/influxdata/influxdb/mock" "github.com/influxdata/influxdb/pkger" "github.com/jsteenb2/testttp" "github.com/stretchr/testify/assert" @@ -21,13 +22,27 @@ import ( func TestPkgerHTTPServer(t *testing.T) { t.Run("create pkg", func(t *testing.T) { t.Run("should successfully return with valid req body", func(t *testing.T) { - pkgHandler := fluxTTP.NewHandlerPkg(fluxTTP.ErrorHandler(0), new(pkger.Service)) + fakeLabelSVC := mock.NewLabelService() + fakeLabelSVC.FindLabelByIDFn = func(ctx context.Context, id influxdb.ID) (*influxdb.Label, error) { + return &influxdb.Label{ + ID: id, + }, nil + } + svc := pkger.NewService(pkger.WithLabelSVC(fakeLabelSVC)) + pkgHandler := fluxTTP.NewHandlerPkg(fluxTTP.ErrorHandler(0), svc) svr := newMountedHandler(pkgHandler) body := newReqBody(t, fluxTTP.ReqCreatePkg{ PkgName: "name1", PkgDescription: "desc1", PkgVersion: "v1", + Resources: []pkger.ResourceToClone{ + { + Kind: pkger.KindLabel, + ID: 1, + Name: "new name", + }, + }, }) testttp.Post("/api/v2/packages", body). @@ -38,7 +53,8 @@ func TestPkgerHTTPServer(t *testing.T) { var resp fluxTTP.RespCreatePkg decodeBody(t, buf, &resp) - pkg := resp.Package + pkg := resp.Pkg + require.NoError(t, pkg.Validate()) assert.Equal(t, pkger.APIVersion, pkg.APIVersion) assert.Equal(t, "package", pkg.Kind) @@ -47,7 +63,8 @@ func TestPkgerHTTPServer(t *testing.T) { assert.Equal(t, "desc1", meta.Description) assert.Equal(t, "v1", meta.Version) - assert.NotNil(t, pkg.Spec.Resources) + assert.Len(t, pkg.Spec.Resources, 1) + assert.Len(t, pkg.Summary().Labels, 1) }) }) diff --git a/http/swagger.yml b/http/swagger.yml index 255c111cf1..dd5cc7492e 100644 --- a/http/swagger.yml +++ b/http/swagger.yml @@ -7104,6 +7104,21 @@ components: type: string pkgVersion: type: string + resources: + type: object + properties: + id: + type: string + kind: + type: string + enum: + - bucket + - dashboard + - label + - variable + name: + type: string + required: [id, kind] Pkg: type: object properties: diff --git a/pkger/clone_resource.go b/pkger/clone_resource.go new file mode 100644 index 0000000000..b25298ef92 --- /dev/null +++ b/pkger/clone_resource.go @@ -0,0 +1,280 @@ +package pkger + +import ( + "errors" + "sort" + + "github.com/influxdata/influxdb" +) + +// ResourceToClone is a resource that will be cloned. +type ResourceToClone struct { + Kind Kind `json:"kind"` + ID influxdb.ID `json:"id"` + Name string `json:"name"` +} + +// OK validates a resource clone is viable. +func (r ResourceToClone) OK() error { + if err := r.Kind.OK(); err != nil { + return err + } + if r.ID == influxdb.ID(0) { + return errors.New("must provide an ID") + } + return nil +} + +func bucketToResource(bkt influxdb.Bucket, name string) Resource { + if name == "" { + name = bkt.Name + } + return Resource{ + fieldKind: KindBucket.String(), + fieldName: name, + fieldDescription: bkt.Description, + fieldBucketRetentionPeriod: bkt.RetentionPeriod.String(), + } +} + +type cellView struct { + c influxdb.Cell + v influxdb.View +} + +func convertCellView(cv cellView) chart { + ch := chart{ + Name: cv.v.Name, + Height: int(cv.c.H), + Width: int(cv.c.W), + XPos: int(cv.c.X), + YPos: int(cv.c.Y), + } + + setCommon := func(k chartKind, iColors []influxdb.ViewColor, dec influxdb.DecimalPlaces, iQueries []influxdb.DashboardQuery) { + ch.Kind = k + ch.Colors = convertColors(iColors) + ch.DecimalPlaces = int(dec.Digits) + ch.EnforceDecimals = dec.IsEnforced + ch.Queries = convertQueries(iQueries) + } + + setNoteFixes := func(note string, noteOnEmpty bool, prefix, suffix string) { + ch.Note = note + ch.NoteOnEmpty = noteOnEmpty + ch.Prefix = prefix + ch.Suffix = suffix + } + + setLegend := func(l influxdb.Legend) { + ch.Legend.Orientation = l.Orientation + ch.Legend.Type = l.Type + } + + props := cv.v.Properties + switch p := props.(type) { + case influxdb.GaugeViewProperties: + setCommon(chartKindGauge, p.ViewColors, p.DecimalPlaces, p.Queries) + setNoteFixes(p.Note, p.ShowNoteWhenEmpty, p.Prefix, p.Suffix) + case influxdb.LinePlusSingleStatProperties: + setCommon(chartKindSingleStatPlusLine, p.ViewColors, p.DecimalPlaces, p.Queries) + setNoteFixes(p.Note, p.ShowNoteWhenEmpty, p.Prefix, p.Suffix) + setLegend(p.Legend) + ch.Axes = convertAxes(p.Axes) + ch.Shade = p.ShadeBelow + ch.XCol = p.XColumn + ch.YCol = p.YColumn + case influxdb.SingleStatViewProperties: + setCommon(chartKindSingleStat, p.ViewColors, p.DecimalPlaces, p.Queries) + setNoteFixes(p.Note, p.ShowNoteWhenEmpty, p.Prefix, p.Suffix) + case influxdb.XYViewProperties: + setCommon(chartKindXY, p.ViewColors, influxdb.DecimalPlaces{}, p.Queries) + setNoteFixes(p.Note, p.ShowNoteWhenEmpty, "", "") + setLegend(p.Legend) + ch.Axes = convertAxes(p.Axes) + ch.Geom = p.Geom + ch.Shade = p.ShadeBelow + ch.XCol = p.XColumn + ch.YCol = p.YColumn + } + + return ch +} + +func convertChartToResource(ch chart) Resource { + r := Resource{ + fieldKind: string(ch.Kind), + fieldName: ch.Name, + fieldChartQueries: ch.Queries, + fieldChartHeight: ch.Height, + fieldChartWidth: ch.Width, + } + if len(ch.Colors) > 0 { + r[fieldChartColors] = ch.Colors + } + if len(ch.Axes) > 0 { + r[fieldChartAxes] = ch.Axes + } + if ch.EnforceDecimals { + r[fieldChartDecimalPlaces] = ch.DecimalPlaces + } + + if ch.Legend.Type != "" { + r[fieldChartLegend] = ch.Legend + } + + ignoreFalseBools := map[string]bool{ + fieldChartNoteOnEmpty: ch.NoteOnEmpty, + fieldChartShade: ch.Shade, + } + for k, v := range ignoreFalseBools { + if v { + r[k] = v + } + } + + ignoreEmptyStrPairs := map[string]string{ + fieldChartNote: ch.Note, + fieldPrefix: ch.Prefix, + fieldSuffix: ch.Suffix, + fieldChartGeom: ch.Geom, + fieldChartXCol: ch.XCol, + fieldChartYCol: ch.YCol, + } + for k, v := range ignoreEmptyStrPairs { + if v != "" { + r[k] = v + } + } + + ignoreEmptyIntPairs := map[string]int{ + fieldChartXPos: ch.XPos, + fieldChartYPos: ch.YPos, + } + for k, v := range ignoreEmptyIntPairs { + if v != 0 { + r[k] = v + } + } + + return r +} + +func convertAxes(iAxes map[string]influxdb.Axis) axes { + out := make(axes, 0, len(iAxes)) + for name, a := range iAxes { + out = append(out, axis{ + Base: a.Base, + Label: a.Label, + Name: name, + Prefix: a.Prefix, + Scale: a.Scale, + Suffix: a.Suffix, + }) + } + return out +} + +func convertColors(iColors []influxdb.ViewColor) colors { + out := make(colors, 0, len(iColors)) + for _, ic := range iColors { + out = append(out, &color{ + Name: ic.Name, + Type: ic.Type, + Hex: ic.Hex, + Value: flt64Ptr(ic.Value), + }) + } + return out +} + +func convertQueries(iQueries []influxdb.DashboardQuery) queries { + out := make(queries, 0, len(iQueries)) + for _, iq := range iQueries { + out = append(out, query{Query: iq.Text}) + } + return out +} + +func dashboardToResource(dash influxdb.Dashboard, cellViews []cellView, name string) Resource { + if name == "" { + name = dash.Name + } + + sort.Slice(cellViews, func(i, j int) bool { + ic, jc := cellViews[i].c, cellViews[j].c + if ic.X == jc.X { + return ic.Y < jc.Y + } + return ic.X < jc.X + }) + + charts := make([]Resource, 0, len(cellViews)) + for _, cv := range cellViews { + if cv.c.ID == influxdb.ID(0) { + continue + } + ch := convertCellView(cv) + if !ch.Kind.ok() { + continue + } + charts = append(charts, convertChartToResource(ch)) + } + + return Resource{ + fieldKind: KindDashboard.String(), + fieldName: name, + fieldDescription: dash.Description, + fieldDashCharts: charts, + } +} + +func labelToResource(l influxdb.Label, name string) Resource { + if name == "" { + name = l.Name + } + return Resource{ + fieldKind: KindLabel.String(), + fieldName: name, + fieldLabelColor: l.Properties["color"], + fieldDescription: l.Properties["description"], + } +} + +func variableToResource(v influxdb.Variable, name string) Resource { + if name == "" { + name = v.Name + } + + r := Resource{ + fieldKind: KindVariable.String(), + fieldName: name, + fieldDescription: v.Description, + } + args := v.Arguments + if args == nil { + return r + } + r[fieldType] = args.Type + + switch args.Type { + case fieldArgTypeConstant: + vals, ok := args.Values.(influxdb.VariableConstantValues) + if ok { + r[fieldValues] = []string(vals) + } + case fieldArgTypeMap: + vals, ok := args.Values.(influxdb.VariableMapValues) + if ok { + r[fieldValues] = map[string]string(vals) + } + case fieldArgTypeQuery: + vals, ok := args.Values.(influxdb.VariableQueryValues) + if ok { + r[fieldVarLanguage] = vals.Language + r[fieldQuery] = vals.Query + } + } + + return r +} diff --git a/pkger/models.go b/pkger/models.go index eb9e6529be..f9b4ff8a71 100644 --- a/pkger/models.go +++ b/pkger/models.go @@ -1,41 +1,67 @@ package pkger import ( + "errors" "fmt" + "strings" "time" "github.com/influxdata/influxdb" ) +// Package kinds. const ( - kindUnknown kind = "" - kindBucket kind = "bucket" - kindDashboard kind = "dashboard" - kindLabel kind = "label" - kindPackage kind = "package" - kindVariable kind = "variable" + KindUnknown Kind = "" + KindBucket Kind = "bucket" + KindDashboard Kind = "dashboard" + KindLabel Kind = "label" + KindPackage Kind = "package" + KindVariable Kind = "variable" ) -var kinds = map[kind]bool{ - kindBucket: true, - kindDashboard: true, - kindLabel: true, - kindPackage: true, - kindVariable: true, +var kinds = map[Kind]bool{ + KindBucket: true, + KindDashboard: true, + KindLabel: true, + KindPackage: true, + KindVariable: true, } -type kind string +// Kind is a resource kind. +type Kind string -func (k kind) String() string { +func newKind(s string) Kind { + return Kind(strings.TrimSpace(strings.ToLower(s))) +} + +// String provides the kind in human readable form. +func (k Kind) String() string { if kinds[k] { return string(k) } - if k == kindUnknown { + if k == KindUnknown { return "unknown" } return string(k) } +// OK validates the kind is valid. +func (k Kind) OK() error { + newKind := Kind(strings.ToLower(string(k))) + if newKind == KindUnknown { + return errors.New("invalid kind") + } + if !kinds[newKind] { + return errors.New("unsupported kind provided") + } + return nil +} + +func (k Kind) is(comp Kind) bool { + normed := Kind(strings.TrimSpace(strings.ToLower(string(k)))) + return normed == comp +} + // SafeID is an equivalent influxdb.ID that encodes safely with // zero values (influxdb.ID == 0). type SafeID influxdb.ID @@ -271,6 +297,23 @@ type SummaryVariable struct { LabelAssociations []influxdb.Label `json:"labelAssociations"` } +const ( + fieldAssociations = "associations" + fieldDescription = "description" + fieldKind = "kind" + fieldName = "name" + fieldPrefix = "prefix" + fieldQuery = "query" + fieldSuffix = "suffix" + fieldType = "type" + fieldValue = "value" + fieldValues = "values" +) + +const ( + fieldBucketRetentionPeriod = "retention_period" +) + type bucket struct { id influxdb.ID OrgID influxdb.ID @@ -401,6 +444,10 @@ func (l *associationMapping) setVariableMapping(v *variable, exists bool) { l.setMapping(key, val) } +const ( + fieldLabelColor = "color" +) + type label struct { id influxdb.ID OrgID influxdb.ID @@ -499,6 +546,13 @@ func toInfluxLabels(labels ...*label) []influxdb.Label { return iLabels } +const ( + fieldArgTypeConstant = "constant" + fieldArgTypeMap = "map" + fieldArgTypeQuery = "query" + fieldVarLanguage = "language" +) + type variable struct { id influxdb.ID OrgID influxdb.ID @@ -603,6 +657,10 @@ func (v *variable) valid() []failure { return failures } +const ( + fieldDashCharts = "charts" +) + type dashboard struct { id influxdb.ID OrgID influxdb.ID @@ -645,6 +703,24 @@ func (d *dashboard) summarize() SummaryDashboard { return iDash } +const ( + fieldChartAxes = "axes" + fieldChartColors = "colors" + fieldChartDecimalPlaces = "decimalPlaces" + fieldChartGeom = "geom" + fieldChartHeight = "height" + fieldChartLegend = "legend" + fieldChartNote = "note" + fieldChartNoteOnEmpty = "noteOnEmpty" + fieldChartQueries = "queries" + fieldChartShade = "shade" + fieldChartWidth = "width" + fieldChartXCol = "xCol" + fieldChartXPos = "xPos" + fieldChartYCol = "yCol" + fieldChartYPos = "yPos" +) + type chart struct { Kind chartKind Name string @@ -668,9 +744,23 @@ type chart struct { func (c chart) properties() influxdb.ViewProperties { switch c.Kind { + case chartKindGauge: + return influxdb.GaugeViewProperties{ + Type: influxdb.ViewPropertyTypeGauge, + Queries: c.Queries.influxDashQueries(), + Prefix: c.Prefix, + Suffix: c.Suffix, + ViewColors: c.Colors.influxViewColors(), + DecimalPlaces: influxdb.DecimalPlaces{ + IsEnforced: c.EnforceDecimals, + Digits: int32(c.DecimalPlaces), + }, + Note: c.Note, + ShowNoteWhenEmpty: c.NoteOnEmpty, + } case chartKindSingleStat: return influxdb.SingleStatViewProperties{ - Type: "single-stat", + Type: influxdb.ViewPropertyTypeSingleStat, Prefix: c.Prefix, Suffix: c.Suffix, DecimalPlaces: influxdb.DecimalPlaces{ @@ -684,7 +774,7 @@ func (c chart) properties() influxdb.ViewProperties { } case chartKindSingleStatPlusLine: return influxdb.LinePlusSingleStatProperties{ - Type: "line-plus-single-stat", + Type: influxdb.ViewPropertyTypeSingleStatPlusLine, Prefix: c.Prefix, Suffix: c.Suffix, DecimalPlaces: influxdb.DecimalPlaces{ @@ -703,7 +793,7 @@ func (c chart) properties() influxdb.ViewProperties { } case chartKindXY: return influxdb.XYViewProperties{ - Type: "xy", + Type: influxdb.ViewPropertyTypeXY, Note: c.Note, ShowNoteWhenEmpty: c.NoteOnEmpty, XColumn: c.XCol, @@ -715,20 +805,6 @@ func (c chart) properties() influxdb.ViewProperties { Axes: c.Axes.influxAxes(), Geom: c.Geom, } - case chartKindGauge: - return influxdb.GaugeViewProperties{ - Type: "gauge", - Queries: c.Queries.influxDashQueries(), - Prefix: c.Prefix, - Suffix: c.Suffix, - ViewColors: c.Colors.influxViewColors(), - DecimalPlaces: influxdb.DecimalPlaces{ - IsEnforced: c.EnforceDecimals, - Digits: int32(c.DecimalPlaces), - }, - Note: c.Note, - ShowNoteWhenEmpty: c.NoteOnEmpty, - } default: return nil } @@ -753,10 +829,9 @@ func (c chart) validProperties() []failure { case chartKindSingleStat: fails = append(fails, c.Colors.hasTypes(colorTypeText)...) case chartKindSingleStatPlusLine: - fails = append(fails, c.Colors.hasTypes(colorTypeText, colorTypeScale)...) + fails = append(fails, c.Colors.hasTypes(colorTypeText)...) fails = append(fails, c.Axes.hasAxes("x", "y")...) case chartKindXY: - fails = append(fails, c.Colors.hasTypes(colorTypeScale)...) fails = append(fails, validGeometry(c.Geom)...) fails = append(fails, c.Axes.hasAxes("x", "y")...) } @@ -773,9 +848,13 @@ var geometryTypes = map[string]bool{ func validGeometry(geom string) []failure { if !geometryTypes[geom] { + msg := "type not found" + if geom != "" { + msg = "type provided is not supported" + } return []failure{{ Field: "geom", - Msg: fmt.Sprintf("type not found: %q", geom), + Msg: fmt.Sprintf("%s: %q", msg, geom), }} } @@ -808,12 +887,19 @@ const ( colorTypeThreshold = "threshold" ) +const ( + fieldColorHex = "hex" +) + type color struct { - id string - Name string - Type string - Hex string - Value float64 + id string + 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: @@ -822,6 +908,13 @@ type color struct { 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{ @@ -831,12 +924,15 @@ func (c colors) influxViewColors() []influxdb.ViewColor { Type: cc.Type, Hex: cc.Hex, Name: cc.Name, - Value: cc.Value, + Value: ptrToFloat64(cc.Value), }) } return iColors } +// 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) []failure { tMap := make(map[string]bool) for _, cc := range c { @@ -858,13 +954,6 @@ func (c colors) hasTypes(types ...string) []failure { func (c colors) valid() []failure { var fails []failure - if len(c) == 0 { - fails = append(fails, failure{ - Field: "colors", - Msg: "at least 1 color must be provided", - }) - } - for i, cc := range c { if cc.Hex == "" { fails = append(fails, failure{ @@ -878,7 +967,7 @@ func (c colors) valid() []failure { } type query struct { - Query string + Query string `json:"query" yaml:"query"` } type queries []query @@ -918,13 +1007,19 @@ func (q queries) valid() []failure { return fails } +const ( + fieldAxisBase = "base" + fieldAxisLabel = "label" + fieldAxisScale = "scale" +) + type axis struct { - Base string - Label string - Name string - Prefix string - Scale string - Suffix string + 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"` } type axes []axis @@ -963,9 +1058,14 @@ func (a axes) hasAxes(expectedAxes ...string) []failure { return failures } +const ( + fieldLegendLanguage = "language" + fieldLegendOrientation = "orientation" +) + type legend struct { - Orientation string - Type string + Orientation string `json:"orientation,omitempty" yaml:"orientation,omitempty"` + Type string `json:"type" yaml:"type"` } func (l legend) influxLegend() influxdb.Legend { @@ -974,3 +1074,10 @@ func (l legend) influxLegend() influxdb.Legend { Orientation: l.Orientation, } } + +func flt64Ptr(f float64) *float64 { + if f != 0 { + return &f + } + return nil +} diff --git a/pkger/parser.go b/pkger/parser.go index 305bba2eae..87d955681a 100644 --- a/pkger/parser.go +++ b/pkger/parser.go @@ -277,15 +277,15 @@ func (p *Pkg) labelMappings() []SummaryLabelMapping { func (p *Pkg) validMetadata() error { var failures []*failure - if p.APIVersion != "0.1.0" { + if p.APIVersion != APIVersion { failures = append(failures, &failure{ Field: "apiVersion", - Msg: "must be version 0.1.0", + Msg: "must be version " + APIVersion, }) } - mKind := kind(strings.TrimSpace(strings.ToLower(p.Kind))) - if mKind != kindPackage { + mKind := Kind(strings.TrimSpace(strings.ToLower(p.Kind))) + if mKind != KindPackage { failures = append(failures, &failure{ Field: "kind", Msg: `must be of kind "Package"`, @@ -294,14 +294,14 @@ func (p *Pkg) validMetadata() error { if p.Metadata.Version == "" { failures = append(failures, &failure{ - Field: "pkgVersion", + Field: "meta.pkgVersion", Msg: "version is required", }) } if p.Metadata.Name == "" { failures = append(failures, &failure{ - Field: "pkgName", + Field: "meta.pkgName", Msg: "must be at least 1 char", }) } @@ -311,7 +311,7 @@ func (p *Pkg) validMetadata() error { } res := errResource{ - Kind: kindPackage.String(), + Kind: KindPackage.String(), Idx: -1, } for _, f := range failures { @@ -366,7 +366,7 @@ func (p *Pkg) graphResources() error { func (p *Pkg) graphBuckets() error { p.mBuckets = make(map[string]*bucket) - return p.eachResource(kindBucket, func(r Resource) []failure { + return p.eachResource(KindBucket, func(r Resource) []failure { if r.Name() == "" { return []failure{{ Field: "name", @@ -383,8 +383,8 @@ func (p *Pkg) graphBuckets() error { bkt := &bucket{ Name: r.Name(), - Description: r.stringShort("description"), - RetentionPeriod: r.duration("retention_period"), + Description: r.stringShort(fieldDescription), + RetentionPeriod: r.duration(fieldBucketRetentionPeriod), } failures := p.parseNestedLabels(r, func(l *label) error { @@ -407,7 +407,7 @@ func (p *Pkg) graphBuckets() error { func (p *Pkg) graphLabels() error { p.mLabels = make(map[string]*label) - return p.eachResource(kindLabel, func(r Resource) []failure { + return p.eachResource(KindLabel, func(r Resource) []failure { if r.Name() == "" { return []failure{{ Field: "name", @@ -423,8 +423,8 @@ func (p *Pkg) graphLabels() error { } p.mLabels[r.Name()] = &label{ Name: r.Name(), - Color: r.stringShort("color"), - Description: r.stringShort("description"), + Color: r.stringShort(fieldLabelColor), + Description: r.stringShort(fieldDescription), } return nil @@ -433,7 +433,7 @@ func (p *Pkg) graphLabels() error { func (p *Pkg) graphDashboards() error { p.mDashboards = make(map[string]*dashboard) - return p.eachResource(kindDashboard, func(r Resource) []failure { + return p.eachResource(KindDashboard, func(r Resource) []failure { if r.Name() == "" { return []failure{{ Field: "name", @@ -450,7 +450,7 @@ func (p *Pkg) graphDashboards() error { dash := &dashboard{ Name: r.Name(), - Description: r.stringShort("description"), + Description: r.stringShort(fieldDescription), } failures := p.parseNestedLabels(r, func(l *label) error { @@ -462,7 +462,7 @@ func (p *Pkg) graphDashboards() error { return dash.labels[i].Name < dash.labels[j].Name }) - for i, cr := range r.slcResource("charts") { + for i, cr := range r.slcResource(fieldDashCharts) { ch, fails := parseChart(cr) if fails != nil { for _, f := range fails { @@ -488,7 +488,7 @@ func (p *Pkg) graphDashboards() error { func (p *Pkg) graphVariables() error { p.mVariables = make(map[string]*variable) - return p.eachResource(kindVariable, func(r Resource) []failure { + return p.eachResource(KindVariable, func(r Resource) []failure { if r.Name() == "" { return []failure{{ Field: "name", @@ -505,12 +505,12 @@ func (p *Pkg) graphVariables() error { newVar := &variable{ Name: r.Name(), - Description: r.stringShort("description"), - Type: strings.ToLower(r.stringShort("type")), - Query: strings.TrimSpace(r.stringShort("query")), - Language: strings.ToLower(strings.TrimSpace(r.stringShort("language"))), - ConstValues: r.slcStr("values"), - MapValues: r.mapStrStr("values"), + Description: r.stringShort(fieldDescription), + Type: strings.ToLower(r.stringShort(fieldType)), + Query: strings.TrimSpace(r.stringShort(fieldQuery)), + Language: strings.ToLower(strings.TrimSpace(r.stringShort(fieldLegendLanguage))), + ConstValues: r.slcStr(fieldValues), + MapValues: r.mapStrStr(fieldValues), } failures := p.parseNestedLabels(r, func(l *label) error { @@ -536,7 +536,7 @@ func (p *Pkg) graphVariables() error { }) } -func (p *Pkg) eachResource(resourceKind kind, fn func(r Resource) []failure) error { +func (p *Pkg) eachResource(resourceKind Kind, fn func(r Resource) []failure) error { var parseErr ParseErr for i, r := range p.Spec.Resources { k, err := r.kind() @@ -556,7 +556,7 @@ func (p *Pkg) eachResource(resourceKind kind, fn func(r Resource) []failure) err }) continue } - if k != resourceKind { + if !k.is(resourceKind) { continue } @@ -593,7 +593,7 @@ func (p *Pkg) parseNestedLabels(r Resource, fn func(lb *label) error) []failure nestedLabels := make(map[string]*label) var failures []failure - for i, nr := range r.nestedAssociations() { + for i, nr := range r.slcResource(fieldAssociations) { fail := p.parseNestedLabel(i, nr, func(l *label) error { if _, ok := nestedLabels[l.Name]; ok { return fmt.Errorf("duplicate nested label: %q", l.Name) @@ -620,7 +620,7 @@ func (p *Pkg) parseNestedLabel(idx int, nr Resource, fn func(lb *label) error) * assIndex: idx, } } - if k != kindLabel { + if !k.is(KindLabel) { return nil } @@ -657,56 +657,73 @@ func parseChart(r Resource) (chart, []failure) { c := chart{ Kind: ck, Name: r.Name(), - Prefix: r.stringShort("prefix"), - Suffix: r.stringShort("suffix"), - Note: r.stringShort("note"), - NoteOnEmpty: r.boolShort("noteOnEmpty"), - Shade: r.boolShort("shade"), - XCol: r.stringShort("xCol"), - YCol: r.stringShort("yCol"), - XPos: r.intShort("xPos"), - YPos: r.intShort("yPos"), - Height: r.intShort("height"), - Width: r.intShort("width"), - Geom: r.stringShort("geom"), + Prefix: r.stringShort(fieldPrefix), + Suffix: r.stringShort(fieldSuffix), + Note: r.stringShort(fieldChartNote), + NoteOnEmpty: r.boolShort(fieldChartNoteOnEmpty), + Shade: r.boolShort(fieldChartShade), + XCol: r.stringShort(fieldChartXCol), + YCol: r.stringShort(fieldChartYCol), + XPos: r.intShort(fieldChartXPos), + YPos: r.intShort(fieldChartYPos), + Height: r.intShort(fieldChartHeight), + Width: r.intShort(fieldChartWidth), + Geom: r.stringShort(fieldChartGeom), } - if leg, ok := ifaceToResource(r["legend"]); ok { - c.Legend.Type = leg.stringShort("type") - c.Legend.Orientation = leg.stringShort("orientation") + if presLeg, ok := r[fieldChartLegend].(legend); ok { + c.Legend = presLeg + } else { + if leg, ok := ifaceToResource(r[fieldChartLegend]); ok { + c.Legend.Type = leg.stringShort(fieldType) + c.Legend.Orientation = leg.stringShort(fieldLegendOrientation) + } } - if dp, ok := r.int("decimalPlaces"); ok { + if dp, ok := r.int(fieldChartDecimalPlaces); ok { c.EnforceDecimals = true c.DecimalPlaces = dp } var failures []failure - for _, rq := range r.slcResource("queries") { - c.Queries = append(c.Queries, query{ - Query: strings.TrimSpace(rq.stringShort("query")), - }) + 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)), + }) + } } - for _, rc := range r.slcResource("colors") { - c.Colors = append(c.Colors, &color{ - id: influxdb.ID(int(time.Now().UnixNano())).String(), - Name: rc.Name(), - Type: rc.stringShort("type"), - Hex: rc.stringShort("hex"), - Value: rc.float64Short("value"), - }) + if presentColors, ok := r[fieldChartColors].(colors); ok { + c.Colors = presentColors + } else { + for _, rc := range r.slcResource(fieldChartColors) { + c.Colors = append(c.Colors, &color{ + // TODO: think we can just axe the stub here + id: influxdb.ID(int(time.Now().UnixNano())).String(), + Name: rc.Name(), + Type: rc.stringShort(fieldType), + Hex: rc.stringShort(fieldColorHex), + Value: flt64Ptr(rc.float64Short(fieldValue)), + }) + } } - for _, ra := range r.slcResource("axes") { - c.Axes = append(c.Axes, axis{ - Base: ra.stringShort("base"), - Label: ra.stringShort("label"), - Name: ra.Name(), - Prefix: ra.stringShort("prefix"), - Scale: ra.stringShort("scale"), - Suffix: ra.stringShort("suffix"), - }) + if presAxes, ok := r[fieldChartAxes].(axes); ok { + c.Axes = presAxes + } else { + for _, ra := range r.slcResource(fieldChartAxes) { + c.Axes = append(c.Axes, axis{ + Base: ra.stringShort(fieldAxisBase), + Label: ra.stringShort(fieldAxisLabel), + Name: ra.Name(), + Prefix: ra.stringShort(fieldPrefix), + Scale: ra.stringShort(fieldAxisScale), + Suffix: ra.stringShort(fieldSuffix), + }) + } } if fails := c.validProperties(); len(fails) > 0 { @@ -724,25 +741,23 @@ func parseChart(r Resource) (chart, []failure) { // available kinds that are supported. type Resource map[string]interface{} +// Name returns the name of the resource. func (r Resource) Name() string { - return strings.TrimSpace(r.stringShort("name")) + return strings.TrimSpace(r.stringShort(fieldName)) } -func (r Resource) kind() (kind, error) { - resKind, ok := r.string("kind") +func (r Resource) kind() (Kind, error) { + resKind, ok := r.string(fieldKind) if !ok { - return kindUnknown, errors.New("no kind provided") + return KindUnknown, errors.New("no kind provided") } - newKind := kind(strings.TrimSpace(strings.ToLower(resKind))) - if newKind == kindUnknown { - return kindUnknown, errors.New("invalid kind") - } - if !kinds[newKind] { - return newKind, errors.New("unsupported kind provided") + k := newKind(resKind) + if err := k.OK(); err != nil { + return k, err } - return newKind, nil + return k, nil } func (r Resource) chartKind() (chartKind, error) { @@ -754,29 +769,6 @@ func (r Resource) chartKind() (chartKind, error) { return chartKind, nil } -func (r Resource) nestedAssociations() []Resource { - v, ok := r["associations"] - if !ok { - return nil - } - - ifaces, ok := v.([]interface{}) - if !ok { - return nil - } - - var resources []Resource - for _, iface := range ifaces { - newRes, ok := ifaceToResource(iface) - if !ok { - continue - } - resources = append(resources, newRes) - } - - return resources -} - func (r Resource) bool(key string) (bool, bool) { b, ok := r[key].(bool) return b, ok @@ -843,6 +835,10 @@ func (r Resource) slcResource(key string) []Resource { return nil } + if resources, ok := v.([]Resource); ok { + return resources + } + iFaceSlc, ok := v.([]interface{}) if !ok { return nil @@ -866,6 +862,10 @@ func (r Resource) slcStr(key string) []string { return nil } + if strSlc, ok := v.([]string); ok { + return strSlc + } + iFaceSlc, ok := v.([]interface{}) if !ok { return nil @@ -884,7 +884,16 @@ func (r Resource) slcStr(key string) []string { } func (r Resource) mapStrStr(key string) map[string]string { - res, ok := ifaceToResource(r[key]) + v, ok := r[key] + if !ok { + return nil + } + + if m, ok := v.(map[string]string); ok { + return m + } + + res, ok := ifaceToResource(v) if !ok { return nil } @@ -905,8 +914,7 @@ func ifaceToResource(i interface{}) (Resource, bool) { return nil, false } - res, ok := i.(Resource) - if ok { + if res, ok := i.(Resource); ok { return res, true } diff --git a/pkger/parser_test.go b/pkger/parser_test.go index 65d7d2cae1..216420e1c3 100644 --- a/pkger/parser_test.go +++ b/pkger/parser_test.go @@ -74,7 +74,7 @@ spec: name: buck_1 retention_period: 1h `, - valFields: []string{"pkgName"}, + valFields: []string{"meta.pkgName"}, }, { name: "missing pkgVersion", @@ -88,7 +88,7 @@ spec: name: buck_1 retention_period: 1h `, - valFields: []string{"pkgVersion"}, + valFields: []string{"meta.pkgVersion"}, }, { name: "missing multiple", @@ -98,12 +98,12 @@ spec: name: buck_1 retention_period: 1h `, - valFields: []string{"apiVersion", "kind", "pkgVersion", "pkgName"}, + valFields: []string{"apiVersion", "kind", "meta.pkgVersion", "meta.pkgName"}, }, } for _, tt := range tests { - testPkgErrors(t, kindPackage, tt) + testPkgErrors(t, KindPackage, tt) } }) }) @@ -203,7 +203,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindBucket, tt) + testPkgErrors(t, KindBucket, tt) } }) }) @@ -281,7 +281,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindLabel, tt) + testPkgErrors(t, KindLabel, tt) } }) }) @@ -435,7 +435,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindBucket, tt) + testPkgErrors(t, KindBucket, tt) } }) }) @@ -512,33 +512,6 @@ spec: colors: - name: laser type: text -`, - }, - { - name: "no colors provided", - validationErrs: 2, - valFields: []string{"charts[0].colors", "charts[0].colors"}, - pkgStr: `apiVersion: 0.1.0 -kind: Package -meta: - pkgName: pkg_name - pkgVersion: 1 - description: pack description -spec: - resources: - - kind: Dashboard - name: dash_1 - description: desc1 - charts: - - kind: Single_Stat - name: single stat - suffix: days - width: 6 - height: 3 - shade: true - queries: - - query: > - from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last") `, }, { @@ -662,7 +635,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindDashboard, tt) + testPkgErrors(t, KindDashboard, tt) } }) }) @@ -766,40 +739,6 @@ spec: label: y_label base: 10 scale: linear -`, - }, - { - name: "no colors provided", - validationErrs: 3, - valFields: []string{"charts[0].colors", "charts[0].colors", "charts[0].colors"}, - pkgStr: `apiVersion: 0.1.0 -kind: Package -meta: - pkgName: pkg_name - pkgVersion: 1 - description: pack description -spec: - resources: - - kind: Dashboard - name: dash_1 - description: desc1 - charts: - - kind: Single_Stat_Plus_Line - name: single stat plus line - width: 6 - height: 3 - queries: - - query: > - from(bucket: v.bucket) |> range(start: v.timeRangeStart) |> filter(fn: (r) => r._measurement == "system") |> filter(fn: (r) => r._field == "uptime") |> last() |> map(fn: (r) => ({r with _value: r._value / 86400})) |> yield(name: "last") - axes: - - name : "x" - label: x_label - base: 10 - scale: linear - - name: "y" - label: y_label - base: 10 - scale: linear `, }, { @@ -1068,7 +1007,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindDashboard, tt) + testPkgErrors(t, KindDashboard, tt) } }) }) @@ -1268,7 +1207,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindDashboard, tt) + testPkgErrors(t, KindDashboard, tt) } }) }) @@ -1512,7 +1451,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindDashboard, tt) + testPkgErrors(t, KindDashboard, tt) } }) }) @@ -1634,7 +1573,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindDashboard, tt) + testPkgErrors(t, KindDashboard, tt) } }) }) @@ -1829,7 +1768,7 @@ spec: } for _, tt := range tests { - testPkgErrors(t, kindVariable, tt) + testPkgErrors(t, KindVariable, tt) } }) }) @@ -1889,7 +1828,7 @@ type testPkgResourceError struct { // defaults to yaml encoding if encoding not provided // defaults num resources to 1 if resource errs not provided. -func testPkgErrors(t *testing.T, k kind, tt testPkgResourceError) { +func testPkgErrors(t *testing.T, k Kind, tt testPkgResourceError) { t.Helper() encoding := EncodingYAML if tt.encoding != EncodingUnknown { diff --git a/pkger/service.go b/pkger/service.go index b81f162d0e..77683e145f 100644 --- a/pkger/service.go +++ b/pkger/service.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/rand" "sort" "strings" "time" @@ -99,38 +100,119 @@ func NewService(opts ...ServiceSetterFn) *Service { } // CreatePkgSetFn is a functional input for setting the pkg fields. -type CreatePkgSetFn func(ctx context.Context, pkg *Pkg) error +type CreatePkgSetFn func(opt *createOpt) error + +type createOpt struct { + metadata Metadata + resources []ResourceToClone +} // WithMetadata sets the metadata on the pkg in a CreatePkg call. func WithMetadata(meta Metadata) CreatePkgSetFn { - return func(ctx context.Context, pkg *Pkg) error { - pkg.Metadata = meta + return func(opt *createOpt) error { + opt.metadata = meta + return nil + } +} + +// WithResourceClones allows the create method to clone existing resources. +func WithResourceClones(resources ...ResourceToClone) CreatePkgSetFn { + return func(opt *createOpt) error { + for _, r := range resources { + if err := r.OK(); err != nil { + return err + } + } + opt.resources = append(opt.resources, resources...) return nil } } // CreatePkg will produce a pkg from the parameters provided. func (s *Service) CreatePkg(ctx context.Context, setters ...CreatePkgSetFn) (*Pkg, error) { - pkg := &Pkg{ - APIVersion: APIVersion, - Kind: kindPackage.String(), - Spec: struct { - Resources []Resource `yaml:"resources" json:"resources"` - }{ - Resources: []Resource{}, - }, - } - + opt := new(createOpt) for _, setter := range setters { - err := setter(ctx, pkg) - if err != nil { + if err := setter(opt); err != nil { return nil, err } } + pkg := &Pkg{ + APIVersion: APIVersion, + Kind: KindPackage.String(), + Metadata: opt.metadata, + Spec: struct { + Resources []Resource `yaml:"resources" json:"resources"` + }{ + Resources: make([]Resource, 0, len(opt.resources)), + }, + } + if pkg.Metadata.Name == "" { + // sudo randomness, this is not an attempt at making charts unique + // that is a problem for the consumer. + pkg.Metadata.Name = fmt.Sprintf("new_%7d", rand.Int()) + } + if pkg.Metadata.Version == "" { + pkg.Metadata.Version = "v1" + } + + for _, r := range opt.resources { + newResource, err := s.resourceCloneToResource(ctx, r) + if err != nil { + return nil, err + } + pkg.Spec.Resources = append(pkg.Spec.Resources, newResource) + } + + if err := pkg.Validate(); err != nil { + return nil, err + } + return pkg, nil } +func (s *Service) resourceCloneToResource(ctx context.Context, r ResourceToClone) (Resource, error) { + switch { + case r.Kind.is(KindBucket): + bkt, err := s.bucketSVC.FindBucketByID(ctx, r.ID) + if err != nil { + return nil, err + } + return bucketToResource(*bkt, r.Name), nil + case r.Kind.is(KindDashboard): + dash, err := s.dashSVC.FindDashboardByID(ctx, r.ID) + if err != nil { + return nil, err + } + var cellViews []cellView + for _, cell := range dash.Cells { + v, err := s.dashSVC.GetDashboardCellView(ctx, r.ID, cell.ID) + if err != nil { + return nil, err + } + cellViews = append(cellViews, cellView{ + c: *cell, + v: *v, + }) + } + return dashboardToResource(*dash, cellViews, r.Name), nil + case r.Kind.is(KindLabel): + l, err := s.labelSVC.FindLabelByID(ctx, r.ID) + if err != nil { + return nil, err + } + return labelToResource(*l, r.Name), nil + case r.Kind.is(KindVariable): + v, err := s.varSVC.FindVariableByID(ctx, r.ID) + if err != nil { + return nil, err + } + return variableToResource(*v, r.Name), nil + default: + return nil, errors.New("unsupported kind provided: " + string(r.Kind)) + } +} + // DryRun provides a dry run of the pkg application. The pkg will be marked verified // for later calls to Apply. This func will be run on an Apply if it has not been run // already. @@ -657,6 +739,8 @@ func convertChartsToCells(ch []chart) ([]*influxdb.Cell, map[*influxdb.Cell]int) for i, c := range ch { icell := &influxdb.Cell{ CellProperty: influxdb.CellProperty{ + X: int32(c.XPos), + Y: int32(c.YPos), H: int32(c.Height), W: int32(c.Width), }, diff --git a/pkger/service_test.go b/pkger/service_test.go index cddfc2f90c..c635c3fa6c 100644 --- a/pkger/service_test.go +++ b/pkger/service_test.go @@ -659,19 +659,453 @@ func TestService(t *testing.T) { t.Run("CreatePkg", func(t *testing.T) { t.Run("with metadata sets the new pkgs metadata", func(t *testing.T) { - svc := new(Service) + bktSVC := mock.NewBucketService() + bktSVC.FindBucketByIDFn = func(_ context.Context, id influxdb.ID) (*influxdb.Bucket, error) { + return &influxdb.Bucket{ID: 1, Name: "name"}, nil + } + svc := NewService(WithBucketSVC(bktSVC)) expectedMeta := Metadata{ Description: "desc", Name: "name", Version: "v1", } - pkg, err := svc.CreatePkg(context.TODO(), WithMetadata(expectedMeta)) + pkg, err := svc.CreatePkg(context.TODO(), + WithMetadata(expectedMeta), + WithResourceClones(ResourceToClone{ // sets stub resource to pass validation + Kind: KindBucket, + ID: 1, + Name: "name", + }), + ) require.NoError(t, err) assert.Equal(t, APIVersion, pkg.APIVersion) assert.Equal(t, expectedMeta, pkg.Metadata) assert.NotNil(t, pkg.Spec.Resources) }) + + t.Run("with existing resources", func(t *testing.T) { + t.Run("bucket", func(t *testing.T) { + tests := []struct { + name string + newName string + }{ + { + name: "without new name", + }, + { + name: "with new name", + newName: "new name", + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + expected := &influxdb.Bucket{ + ID: 3, + Name: "bucket name", + Description: "desc", + RetentionPeriod: time.Hour, + } + + bktSVC := mock.NewBucketService() + bktSVC.FindBucketByIDFn = func(_ context.Context, id influxdb.ID) (*influxdb.Bucket, error) { + if id != expected.ID { + return nil, errors.New("uh ohhh, wrong id here: " + id.String()) + } + return expected, nil + } + + svc := NewService(WithBucketSVC(bktSVC)) + + resToClone := ResourceToClone{ + Kind: KindBucket, + ID: expected.ID, + Name: tt.newName, + } + pkg, err := svc.CreatePkg(context.TODO(), WithResourceClones(resToClone)) + require.NoError(t, err) + + bkts := pkg.Summary().Buckets + require.Len(t, bkts, 1) + + actual := bkts[0] + expectedName := expected.Name + if tt.newName != "" { + expectedName = tt.newName + } + assert.Equal(t, expectedName, actual.Name) + assert.Equal(t, expected.Description, actual.Description) + assert.Equal(t, expected.RetentionPeriod, actual.RetentionPeriod) + } + t.Run(tt.name, fn) + } + }) + + newQuery := func() influxdb.DashboardQuery { + q := 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")) + return q + } + + newAxes := func() map[string]influxdb.Axis { + return map[string]influxdb.Axis{ + "x": { + Bounds: []string{}, + Label: "labx", + Prefix: "pre", + Suffix: "suf", + Base: "base", + Scale: "linear", + }, + "y": { + Bounds: []string{}, + Label: "laby", + Prefix: "pre", + Suffix: "suf", + Base: "base", + Scale: "linear", + }, + } + } + + newColors := func(types ...string) []influxdb.ViewColor { + var out []influxdb.ViewColor + for _, t := range types { + out = append(out, influxdb.ViewColor{ + Type: t, + Hex: time.Now().Format(time.RFC3339), + Name: time.Now().Format(time.RFC3339), + Value: float64(time.Now().Unix()), + }) + } + return out + } + + t.Run("dashboard", func(t *testing.T) { + tests := []struct { + name string + newName string + expectedView influxdb.View + }{ + { + name: "without new name single stat", + expectedView: influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.SingleStatViewProperties{ + Type: influxdb.ViewPropertyTypeSingleStat, + DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, + Note: "a note", + Queries: []influxdb.DashboardQuery{newQuery()}, + Prefix: "pre", + ShowNoteWhenEmpty: true, + Suffix: "suf", + ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, + }, + }, + }, + { + name: "with new name single stat", + newName: "new name", + expectedView: influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.SingleStatViewProperties{ + Type: influxdb.ViewPropertyTypeSingleStat, + DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, + Note: "a note", + Queries: []influxdb.DashboardQuery{newQuery()}, + Prefix: "pre", + ShowNoteWhenEmpty: true, + Suffix: "suf", + ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, + }, + }, + }, + { + name: "guage", + newName: "new name", + expectedView: influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.GaugeViewProperties{ + Type: influxdb.ViewPropertyTypeGauge, + DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, + Note: "a note", + Prefix: "pre", + Suffix: "suf", + Queries: []influxdb.DashboardQuery{newQuery()}, + ShowNoteWhenEmpty: true, + ViewColors: newColors("min", "max", "threshold"), + }, + }, + }, { + name: "single stat plus line", + newName: "new name", + expectedView: influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.LinePlusSingleStatProperties{ + Type: influxdb.ViewPropertyTypeSingleStatPlusLine, + Axes: newAxes(), + DecimalPlaces: influxdb.DecimalPlaces{IsEnforced: true, Digits: 1}, + Legend: influxdb.Legend{Type: "type", Orientation: "horizontal"}, + Note: "a note", + Prefix: "pre", + Suffix: "suf", + Queries: []influxdb.DashboardQuery{newQuery()}, + ShadeBelow: true, + ShowNoteWhenEmpty: true, + ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, + XColumn: "x", + YColumn: "y", + }, + }, + }, + { + name: "xy", + newName: "new name", + expectedView: influxdb.View{ + ViewContents: influxdb.ViewContents{ + Name: "view name", + }, + Properties: influxdb.XYViewProperties{ + Type: influxdb.ViewPropertyTypeXY, + Axes: newAxes(), + Geom: "step", + Legend: influxdb.Legend{Type: "type", Orientation: "horizontal"}, + Note: "a note", + Queries: []influxdb.DashboardQuery{newQuery()}, + ShadeBelow: true, + ShowNoteWhenEmpty: true, + ViewColors: []influxdb.ViewColor{{Type: "text", Hex: "red"}}, + XColumn: "x", + YColumn: "y", + }, + }, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + expectedCell := &influxdb.Cell{ + ID: 5, + CellProperty: influxdb.CellProperty{X: 1, Y: 2, W: 3, H: 4}, + } + expected := &influxdb.Dashboard{ + ID: 3, + Name: "bucket name", + Description: "desc", + Cells: []*influxdb.Cell{expectedCell}, + } + + dashSVC := mock.NewDashboardService() + dashSVC.FindDashboardByIDF = func(_ context.Context, id influxdb.ID) (*influxdb.Dashboard, error) { + if id != expected.ID { + return nil, errors.New("uh ohhh, wrong id here: " + id.String()) + } + return expected, nil + } + dashSVC.GetDashboardCellViewF = func(_ context.Context, id influxdb.ID, cID influxdb.ID) (*influxdb.View, error) { + if id == expected.ID && cID == expectedCell.ID { + return &tt.expectedView, nil + } + return nil, errors.New("wrongo ids") + } + + svc := NewService(WithDashboardSVC(dashSVC)) + + resToClone := ResourceToClone{ + Kind: KindDashboard, + ID: expected.ID, + Name: tt.newName, + } + pkg, err := svc.CreatePkg(context.TODO(), WithResourceClones(resToClone)) + require.NoError(t, err) + + dashs := pkg.Summary().Dashboards + require.Len(t, dashs, 1) + + actual := dashs[0] + expectedName := expected.Name + if tt.newName != "" { + expectedName = tt.newName + } + assert.Equal(t, expectedName, actual.Name) + assert.Equal(t, expected.Description, actual.Description) + + require.Len(t, actual.Charts, 1) + ch := actual.Charts[0] + assert.Equal(t, int(expectedCell.X), ch.XPosition) + assert.Equal(t, int(expectedCell.Y), ch.YPosition) + assert.Equal(t, int(expectedCell.H), ch.Height) + assert.Equal(t, int(expectedCell.W), ch.Width) + assert.Equal(t, tt.expectedView.Properties, ch.Properties) + } + t.Run(tt.name, fn) + } + }) + + t.Run("label", func(t *testing.T) { + tests := []struct { + name string + newName string + }{ + { + name: "without new name", + }, + { + name: "with new name", + newName: "new name", + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + expectedLabel := &influxdb.Label{ + ID: 3, + Name: "bucket name", + Properties: map[string]string{ + "description": "desc", + "color": "red", + }, + } + + labelSVC := mock.NewLabelService() + labelSVC.FindLabelByIDFn = func(_ context.Context, id influxdb.ID) (*influxdb.Label, error) { + if id != expectedLabel.ID { + return nil, errors.New("uh ohhh, wrong id here: " + id.String()) + } + return expectedLabel, nil + } + + svc := NewService(WithLabelSVC(labelSVC)) + + resToClone := ResourceToClone{ + Kind: KindLabel, + ID: expectedLabel.ID, + Name: tt.newName, + } + pkg, err := svc.CreatePkg(context.TODO(), WithResourceClones(resToClone)) + require.NoError(t, err) + + newLabels := pkg.Summary().Labels + require.Len(t, newLabels, 1) + + actual := newLabels[0] + expectedName := expectedLabel.Name + if tt.newName != "" { + expectedName = tt.newName + } + assert.Equal(t, expectedName, actual.Name) + assert.Equal(t, expectedLabel.Properties, actual.Properties) + } + t.Run(tt.name, fn) + } + }) + + t.Run("variable", func(t *testing.T) { + tests := []struct { + name string + newName string + expectedVar influxdb.Variable + }{ + { + name: "without new name", + expectedVar: influxdb.Variable{ + ID: 1, + Name: "old name", + Description: "desc", + Arguments: &influxdb.VariableArguments{ + Type: "constant", + Values: influxdb.VariableConstantValues{"val"}, + }, + }, + }, + { + name: "with new name", + newName: "new name", + expectedVar: influxdb.Variable{ + ID: 1, + Name: "old name", + Arguments: &influxdb.VariableArguments{ + Type: "constant", + Values: influxdb.VariableConstantValues{"val"}, + }, + }, + }, + { + name: "with map arg", + expectedVar: influxdb.Variable{ + ID: 1, + Name: "old name", + Arguments: &influxdb.VariableArguments{ + Type: "map", + Values: influxdb.VariableMapValues{"k": "v"}, + }, + }, + }, + { + name: "with query arg", + expectedVar: influxdb.Variable{ + ID: 1, + Name: "old name", + Arguments: &influxdb.VariableArguments{ + Type: "query", + Values: influxdb.VariableQueryValues{ + Query: "query", + Language: "flux", + }, + }, + }, + }, + } + + for _, tt := range tests { + fn := func(t *testing.T) { + varSVC := mock.NewVariableService() + varSVC.FindVariableByIDF = func(_ context.Context, id influxdb.ID) (*influxdb.Variable, error) { + if id != tt.expectedVar.ID { + return nil, errors.New("uh ohhh, wrong id here: " + id.String()) + } + return &tt.expectedVar, nil + } + + svc := NewService(WithVariableSVC(varSVC)) + + resToClone := ResourceToClone{ + Kind: KindVariable, + ID: tt.expectedVar.ID, + Name: tt.newName, + } + pkg, err := svc.CreatePkg(context.TODO(), WithResourceClones(resToClone)) + require.NoError(t, err) + + newVars := pkg.Summary().Variables + require.Len(t, newVars, 1) + + actual := newVars[0] + expectedName := tt.expectedVar.Name + if tt.newName != "" { + expectedName = tt.newName + } + assert.Equal(t, expectedName, actual.Name) + assert.Equal(t, tt.expectedVar.Description, actual.Description) + assert.Equal(t, tt.expectedVar.Arguments, actual.Arguments) + } + t.Run(tt.name, fn) + } + }) + }) }) }