feat(pkger): add export functonality to pkger for existing buckets/labels/dashboards

no associations included at this time. Also fixes http response to be just
the pkg without the envelope. Having that envelope makes the API icky to
work with from any shell script or just saving it to file. This feels more
organic to just drop that envelope.
pull/15824/head
Johnny Steenbergen 2019-11-08 11:33:41 -08:00 committed by Johnny Steenbergen
parent f2cda2ae10
commit a64b976561
10 changed files with 1168 additions and 266 deletions

View File

@ -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

View File

@ -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,
})
}

View File

@ -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)
})
})

View File

@ -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:

280
pkger/clone_resource.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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),
},

View File

@ -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)
}
})
})
})
}