package pkger import ( "context" "errors" "fmt" "regexp" "sort" "strings" "github.com/influxdata/influxdb/v2" ierrors "github.com/influxdata/influxdb/v2/kit/errors" "github.com/influxdata/influxdb/v2/notification" icheck "github.com/influxdata/influxdb/v2/notification/check" "github.com/influxdata/influxdb/v2/notification/endpoint" "github.com/influxdata/influxdb/v2/notification/rule" "github.com/influxdata/influxdb/v2/pkger/internal/wordplay" "github.com/influxdata/influxdb/v2/snowflake" ) var idGenerator = snowflake.NewDefaultIDGenerator() // NameGenerator generates a random name. Includes an optional fuzz option to // further randomize the name. type NameGenerator func() string // ResourceToClone is a resource that will be cloned. type ResourceToClone struct { Kind Kind `json:"kind"` ID influxdb.ID `json:"id"` Name string `json:"name"` // note(jsteenb2): For time being we'll allow this internally, but not externally. A lot of // issues to account for when exposing this to the outside world. Not something I'm keen // to accommodate at this time. MetaName string `json:"-"` } // 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 } var kindPriorities = map[Kind]int{ KindLabel: 1, KindBucket: 2, KindCheck: 3, KindCheckDeadman: 4, KindCheckThreshold: 5, KindNotificationEndpoint: 6, KindNotificationEndpointHTTP: 7, KindNotificationEndpointPagerDuty: 8, KindNotificationEndpointSlack: 9, KindNotificationRule: 10, KindTask: 11, KindVariable: 12, KindDashboard: 13, KindTelegraf: 14, } type exportKey struct { orgID influxdb.ID id influxdb.ID name string kind Kind } func newExportKey(orgID, id influxdb.ID, k Kind, name string) exportKey { return exportKey{ orgID: orgID, id: id, name: name, kind: k, } } type resourceExporter struct { nameGen NameGenerator bucketSVC influxdb.BucketService checkSVC influxdb.CheckService dashSVC influxdb.DashboardService labelSVC influxdb.LabelService endpointSVC influxdb.NotificationEndpointService ruleSVC influxdb.NotificationRuleStore taskSVC influxdb.TaskService teleSVC influxdb.TelegrafConfigStore varSVC influxdb.VariableService mObjects map[exportKey]Object mPkgNames map[string]bool mStackResources map[exportKey]StackResource } func newResourceExporter(svc *Service) *resourceExporter { return &resourceExporter{ nameGen: wordplay.GetRandomName, bucketSVC: svc.bucketSVC, checkSVC: svc.checkSVC, dashSVC: svc.dashSVC, labelSVC: svc.labelSVC, endpointSVC: svc.endpointSVC, ruleSVC: svc.ruleSVC, taskSVC: svc.taskSVC, teleSVC: svc.teleSVC, varSVC: svc.varSVC, mObjects: make(map[exportKey]Object), mPkgNames: make(map[string]bool), mStackResources: make(map[exportKey]StackResource), } } func (ex *resourceExporter) Export(ctx context.Context, resourcesToClone []ResourceToClone, labelNames ...string) error { mLabelIDsToMetaName := make(map[influxdb.ID]string) for _, r := range resourcesToClone { if !r.Kind.is(KindLabel) || r.MetaName == "" { continue } mLabelIDsToMetaName[r.ID] = r.MetaName } cloneAssFn, err := ex.resourceCloneAssociationsGen(ctx, mLabelIDsToMetaName, labelNames...) if err != nil { return err } resourcesToClone = uniqResourcesToClone(resourcesToClone) // sorting this in priority order guarantees that the dependencies/associations // for a resource are handled prior to the resource being processed. // i.e. if a bucket depends on a label, then labels need to be run first // to guarantee they are available before a bucket is exported. sort.Slice(resourcesToClone, func(i, j int) bool { iName, jName := resourcesToClone[i].Name, resourcesToClone[j].Name iKind, jKind := resourcesToClone[i].Kind, resourcesToClone[j].Kind if iKind.is(jKind) { return iName < jName } return kindPriorities[iKind] < kindPriorities[jKind] }) for _, r := range resourcesToClone { err := ex.resourceCloneToKind(ctx, r, cloneAssFn) if err != nil { return internalErr(fmt.Errorf("failed to clone resource: resource_id=%s resource_kind=%s err=%q", r.ID, r.Kind, err)) } } return nil } func (ex *resourceExporter) Objects() []Object { objects := make([]Object, 0, len(ex.mObjects)) for _, obj := range ex.mObjects { objects = append(objects, obj) } return sortObjects(objects) } func (ex *resourceExporter) StackResources() []StackResource { resources := make([]StackResource, 0, len(ex.mStackResources)) for _, res := range ex.mStackResources { resources = append(resources, res) } return resources } func (ex *resourceExporter) uniqByNameResID() influxdb.ID { // we only need an id when we have resources that are not unique by name via the // metastore. resoureces that are unique by name will be provided a default stamp // making looksup unique since each resource will be unique by name. const uniqByNameResID = 0 return uniqByNameResID } type cloneAssociationsFn func(context.Context, ResourceToClone) (associations []ObjectAssociation, skipResource bool, err error) func (ex *resourceExporter) resourceCloneToKind(ctx context.Context, r ResourceToClone, cFn cloneAssociationsFn) (e error) { defer func() { if e != nil { e = ierrors.Wrap(e, "cloning resource") } }() ass, skipResource, err := cFn(ctx, r) if err != nil { return err } if skipResource { return nil } mapResource := func(orgID, uniqResID influxdb.ID, k Kind, object Object) { // overwrite the default metadata.name field with export generated one here metaName := r.MetaName if r.MetaName == "" { metaName = ex.uniqName() } stackResource := StackResource{ APIVersion: APIVersion, ID: r.ID, MetaName: metaName, Kind: r.Kind, } for _, a := range ass { stackResource.Associations = append(stackResource.Associations, StackResourceAssociation(a)) } object.SetMetadataName(metaName) object.AddAssociations(ass...) key := newExportKey(orgID, uniqResID, k, object.Spec.stringShort(fieldName)) ex.mObjects[key] = object ex.mStackResources[key] = stackResource } uniqByNameResID := ex.uniqByNameResID() switch { case r.Kind.is(KindBucket): bkt, err := ex.bucketSVC.FindBucketByID(ctx, r.ID) if err != nil { return err } mapResource(bkt.OrgID, uniqByNameResID, KindBucket, BucketToObject(r.Name, *bkt)) case r.Kind.is(KindCheck), r.Kind.is(KindCheckDeadman), r.Kind.is(KindCheckThreshold): ch, err := ex.checkSVC.FindCheckByID(ctx, r.ID) if err != nil { return err } mapResource(ch.GetOrgID(), uniqByNameResID, KindCheck, CheckToObject(r.Name, ch)) case r.Kind.is(KindDashboard): dash, err := findDashboardByIDFull(ctx, ex.dashSVC, r.ID) if err != nil { return err } mapResource(dash.OrganizationID, dash.ID, KindDashboard, DashboardToObject(r.Name, *dash)) case r.Kind.is(KindLabel): l, err := ex.labelSVC.FindLabelByID(ctx, r.ID) if err != nil { return err } mapResource(l.OrgID, uniqByNameResID, KindLabel, LabelToObject(r.Name, *l)) case r.Kind.is(KindNotificationEndpoint), r.Kind.is(KindNotificationEndpointHTTP), r.Kind.is(KindNotificationEndpointPagerDuty), r.Kind.is(KindNotificationEndpointSlack): e, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, r.ID) if err != nil { return err } mapResource(e.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject(r.Name, e)) case r.Kind.is(KindNotificationRule): rule, ruleEndpoint, err := ex.getEndpointRule(ctx, r.ID) if err != nil { return err } endpointKey := newExportKey(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, ruleEndpoint.GetName()) object, ok := ex.mObjects[endpointKey] if !ok { mapResource(ruleEndpoint.GetOrgID(), uniqByNameResID, KindNotificationEndpoint, NotificationEndpointToObject("", ruleEndpoint)) object = ex.mObjects[endpointKey] } endpointObjectName := object.Name() mapResource(rule.GetOrgID(), rule.GetID(), KindNotificationRule, NotificationRuleToObject(r.Name, endpointObjectName, rule)) case r.Kind.is(KindTask): t, err := ex.taskSVC.FindTaskByID(ctx, r.ID) if err != nil { return err } mapResource(t.OrganizationID, t.ID, KindTask, TaskToObject(r.Name, *t)) case r.Kind.is(KindTelegraf): t, err := ex.teleSVC.FindTelegrafConfigByID(ctx, r.ID) if err != nil { return err } mapResource(t.OrgID, t.ID, KindTelegraf, TelegrafToObject(r.Name, *t)) case r.Kind.is(KindVariable): v, err := ex.varSVC.FindVariableByID(ctx, r.ID) if err != nil { return err } mapResource(v.OrganizationID, uniqByNameResID, KindVariable, VariableToObject(r.Name, *v)) default: return errors.New("unsupported kind provided: " + string(r.Kind)) } return nil } func (ex *resourceExporter) resourceCloneAssociationsGen(ctx context.Context, labelIDsToMetaName map[influxdb.ID]string, labelNames ...string) (cloneAssociationsFn, error) { mLabelNames := make(map[string]bool) for _, labelName := range labelNames { mLabelNames[labelName] = true } mLabelIDs, err := getLabelIDMap(ctx, ex.labelSVC, labelNames) if err != nil { return nil, err } cloneFn := func(ctx context.Context, r ResourceToClone) ([]ObjectAssociation, bool, error) { if r.Kind.is(KindUnknown) { return nil, true, nil } if r.Kind.is(KindLabel) { // check here verifies the label maps to an id of a valid label name shouldSkip := len(mLabelIDs) > 0 && !mLabelIDs[r.ID] return nil, shouldSkip, nil } labels, err := ex.labelSVC.FindResourceLabels(ctx, influxdb.LabelMappingFilter{ ResourceID: r.ID, ResourceType: r.Kind.ResourceType(), }) if err != nil { return nil, false, ierrors.Wrap(err, "finding resource labels") } if len(mLabelNames) > 0 { shouldSkip := true for _, l := range labels { if _, ok := mLabelNames[l.Name]; ok { shouldSkip = false break } } if shouldSkip { return nil, true, nil } } var associations []ObjectAssociation for _, l := range labels { if len(mLabelNames) > 0 { if _, ok := mLabelNames[l.Name]; !ok { continue } } labelObject := LabelToObject("", *l) metaName := labelIDsToMetaName[l.ID] if metaName == "" { metaName = ex.uniqName() } labelObject.Metadata[fieldName] = metaName k := newExportKey(l.OrgID, ex.uniqByNameResID(), KindLabel, l.Name) existing, ok := ex.mObjects[k] if ok { associations = append(associations, ObjectAssociation{ Kind: KindLabel, MetaName: existing.Name(), }) continue } associations = append(associations, ObjectAssociation{ Kind: KindLabel, MetaName: labelObject.Name(), }) ex.mObjects[k] = labelObject } sort.Slice(associations, func(i, j int) bool { return associations[i].MetaName < associations[j].MetaName }) return associations, false, nil } return cloneFn, nil } func (ex *resourceExporter) getEndpointRule(ctx context.Context, id influxdb.ID) (influxdb.NotificationRule, influxdb.NotificationEndpoint, error) { rule, err := ex.ruleSVC.FindNotificationRuleByID(ctx, id) if err != nil { return nil, nil, err } ruleEndpoint, err := ex.endpointSVC.FindNotificationEndpointByID(ctx, rule.GetEndpointID()) if err != nil { return nil, nil, err } return rule, ruleEndpoint, nil } func (ex *resourceExporter) uniqName() string { return uniqMetaName(ex.nameGen, ex.mPkgNames) } func uniqMetaName(nameGen NameGenerator, existingNames map[string]bool) string { uuid := strings.ToLower(idGenerator.ID().String()) name := uuid for i := 1; i < 250; i++ { name = fmt.Sprintf("%s-%s", nameGen(), uuid[10:]) if !existingNames[name] { return name } } return name } func findDashboardByIDFull(ctx context.Context, dashSVC influxdb.DashboardService, id influxdb.ID) (*influxdb.Dashboard, error) { dash, err := dashSVC.FindDashboardByID(ctx, id) if err != nil { return nil, err } for _, cell := range dash.Cells { v, err := dashSVC.GetDashboardCellView(ctx, id, cell.ID) if err != nil { return nil, err } cell.View = v } return dash, nil } func uniqResourcesToClone(resources []ResourceToClone) []ResourceToClone { type key struct { kind Kind id influxdb.ID } m := make(map[key]ResourceToClone) for i := range resources { r := resources[i] rKey := key{kind: r.Kind, id: r.ID} kr, ok := m[rKey] switch { case ok && kr.Name == r.Name && kr.MetaName == r.MetaName: case ok && kr.MetaName != "" && r.MetaName == "": case ok && kr.MetaName == "" && kr.Name != "" && r.Name == "": default: m[rKey] = r } } out := make([]ResourceToClone, 0, len(resources)) for _, r := range m { out = append(out, r) } return out } // BucketToObject converts a influxdb.Bucket into an Object. func BucketToObject(name string, bkt influxdb.Bucket) Object { if name == "" { name = bkt.Name } o := newObject(KindBucket, name) assignNonZeroStrings(o.Spec, map[string]string{fieldDescription: bkt.Description}) if bkt.RetentionPeriod != 0 { o.Spec[fieldBucketRetentionRules] = retentionRules{newRetentionRule(bkt.RetentionPeriod)} } return o } func CheckToObject(name string, ch influxdb.Check) Object { if name == "" { name = ch.GetName() } o := newObject(KindCheck, name) assignNonZeroStrings(o.Spec, map[string]string{ fieldDescription: ch.GetDescription(), fieldStatus: influxdb.TaskStatusActive, }) assignBase := func(base icheck.Base) { o.Spec[fieldQuery] = strings.TrimSpace(base.Query.Text) o.Spec[fieldCheckStatusMessageTemplate] = base.StatusMessageTemplate assignNonZeroFluxDurs(o.Spec, map[string]*notification.Duration{ fieldEvery: base.Every, fieldOffset: base.Offset, }) var tags []Resource for _, t := range base.Tags { if t.Valid() != nil { continue } tags = append(tags, Resource{ fieldKey: t.Key, fieldValue: t.Value, }) } if len(tags) > 0 { o.Spec[fieldCheckTags] = tags } } switch cT := ch.(type) { case *icheck.Deadman: o.Kind = KindCheckDeadman assignBase(cT.Base) assignNonZeroFluxDurs(o.Spec, map[string]*notification.Duration{ fieldCheckTimeSince: cT.TimeSince, fieldCheckStaleTime: cT.StaleTime, }) o.Spec[fieldLevel] = cT.Level.String() assignNonZeroBools(o.Spec, map[string]bool{fieldCheckReportZero: cT.ReportZero}) case *icheck.Threshold: o.Kind = KindCheckThreshold assignBase(cT.Base) var thresholds []Resource for _, th := range cT.Thresholds { thresholds = append(thresholds, convertThreshold(th)) } o.Spec[fieldCheckThresholds] = thresholds } return o } func convertThreshold(th icheck.ThresholdConfig) Resource { r := Resource{fieldLevel: th.GetLevel().String()} assignLesser := func(threshType thresholdType, allValues bool, val float64) { r[fieldType] = string(threshType) assignNonZeroBools(r, map[string]bool{fieldCheckAllValues: allValues}) r[fieldValue] = val } switch realType := th.(type) { case icheck.Lesser: assignLesser(thresholdTypeLesser, realType.AllValues, realType.Value) case *icheck.Lesser: assignLesser(thresholdTypeLesser, realType.AllValues, realType.Value) case icheck.Greater: assignLesser(thresholdTypeGreater, realType.AllValues, realType.Value) case *icheck.Greater: assignLesser(thresholdTypeGreater, realType.AllValues, realType.Value) case icheck.Range: assignRangeThreshold(r, realType) case *icheck.Range: assignRangeThreshold(r, *realType) } return r } func assignRangeThreshold(r Resource, rangeThreshold icheck.Range) { thType := thresholdTypeOutsideRange if rangeThreshold.Within { thType = thresholdTypeInsideRange } r[fieldType] = string(thType) assignNonZeroBools(r, map[string]bool{fieldCheckAllValues: rangeThreshold.AllValues}) r[fieldMax] = rangeThreshold.Max r[fieldMin] = rangeThreshold.Min } func convertCellView(cell influxdb.Cell) chart { var name string if cell.View != nil { name = cell.View.Name } ch := chart{ Name: name, Height: int(cell.H), Width: int(cell.W), XPos: int(cell.X), YPos: int(cell.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 := cell.View.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) ch.TickPrefix = p.TickPrefix ch.TickSuffix = p.TickSuffix case influxdb.HeatmapViewProperties: ch.Kind = chartKindHeatMap ch.Queries = convertQueries(p.Queries) ch.Colors = stringsToColors(p.ViewColors) ch.XCol = p.XColumn ch.YCol = p.YColumn ch.Axes = []axis{ {Label: p.XAxisLabel, Prefix: p.XPrefix, Suffix: p.XSuffix, Name: "x", Domain: p.XDomain}, {Label: p.YAxisLabel, Prefix: p.YPrefix, Suffix: p.YSuffix, Name: "y", Domain: p.YDomain}, } ch.Note = p.Note ch.NoteOnEmpty = p.ShowNoteWhenEmpty ch.BinSize = int(p.BinSize) case influxdb.HistogramViewProperties: ch.Kind = chartKindHistogram ch.Queries = convertQueries(p.Queries) ch.Colors = convertColors(p.ViewColors) ch.FillColumns = p.FillColumns ch.XCol = p.XColumn ch.Axes = []axis{{Label: p.XAxisLabel, Name: "x", Domain: p.XDomain}} ch.Note = p.Note ch.NoteOnEmpty = p.ShowNoteWhenEmpty ch.BinCount = p.BinCount ch.Position = p.Position case influxdb.MarkdownViewProperties: ch.Kind = chartKindMarkdown ch.Note = p.Note 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.HoverDimension = p.HoverDimension ch.XCol = p.XColumn ch.YCol = p.YColumn ch.Position = p.Position case influxdb.SingleStatViewProperties: setCommon(chartKindSingleStat, p.ViewColors, p.DecimalPlaces, p.Queries) setNoteFixes(p.Note, p.ShowNoteWhenEmpty, p.Prefix, p.Suffix) ch.TickPrefix = p.TickPrefix ch.TickSuffix = p.TickSuffix case influxdb.ScatterViewProperties: ch.Kind = chartKindScatter ch.Queries = convertQueries(p.Queries) ch.Colors = stringsToColors(p.ViewColors) ch.XCol = p.XColumn ch.YCol = p.YColumn ch.Axes = []axis{ {Label: p.XAxisLabel, Prefix: p.XPrefix, Suffix: p.XSuffix, Name: "x", Domain: p.XDomain}, {Label: p.YAxisLabel, Prefix: p.YPrefix, Suffix: p.YSuffix, Name: "y", Domain: p.YDomain}, } ch.Note = p.Note ch.NoteOnEmpty = p.ShowNoteWhenEmpty case influxdb.TableViewProperties: setCommon(chartKindTable, p.ViewColors, p.DecimalPlaces, p.Queries) setNoteFixes(p.Note, p.ShowNoteWhenEmpty, "", "") ch.TimeFormat = p.TimeFormat ch.TableOptions = tableOptions{ VerticalTimeAxis: p.TableOptions.VerticalTimeAxis, SortByField: p.TableOptions.SortBy.InternalName, Wrapping: p.TableOptions.Wrapping, FixFirstColumn: p.TableOptions.FixFirstColumn, } for _, fieldOpt := range p.FieldOptions { ch.FieldOptions = append(ch.FieldOptions, fieldOption{ FieldName: fieldOpt.InternalName, DisplayName: fieldOpt.DisplayName, Visible: fieldOpt.Visible, }) } 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.HoverDimension = p.HoverDimension ch.XCol = p.XColumn ch.YCol = p.YColumn ch.Position = p.Position } sort.Slice(ch.Axes, func(i, j int) bool { return ch.Axes[i].Name < ch.Axes[j].Name }) return ch } func convertChartToResource(ch chart) Resource { r := Resource{ fieldKind: ch.Kind.title(), fieldName: ch.Name, fieldChartHeight: ch.Height, fieldChartWidth: ch.Width, } if len(ch.Queries) > 0 { r[fieldChartQueries] = ch.Queries } 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 } if len(ch.FillColumns) > 0 { r[fieldChartFillColumns] = ch.FillColumns } if zero := new(tableOptions); ch.TableOptions != *zero { tRes := make(Resource) assignNonZeroBools(tRes, map[string]bool{ fieldChartTableOptionVerticalTimeAxis: ch.TableOptions.VerticalTimeAxis, fieldChartTableOptionFixFirstColumn: ch.TableOptions.FixFirstColumn, }) assignNonZeroStrings(tRes, map[string]string{ fieldChartTableOptionSortBy: ch.TableOptions.SortByField, fieldChartTableOptionWrapping: ch.TableOptions.Wrapping, }) r[fieldChartTableOptions] = tRes } if len(ch.FieldOptions) > 0 { fieldOpts := make([]Resource, 0, len(ch.FieldOptions)) for _, fo := range ch.FieldOptions { fRes := make(Resource) assignNonZeroBools(fRes, map[string]bool{ fieldChartFieldOptionVisible: fo.Visible, }) assignNonZeroStrings(fRes, map[string]string{ fieldChartFieldOptionDisplayName: fo.DisplayName, fieldChartFieldOptionFieldName: fo.FieldName, }) fieldOpts = append(fieldOpts, fRes) } r[fieldChartFieldOptions] = fieldOpts } assignNonZeroBools(r, map[string]bool{ fieldChartNoteOnEmpty: ch.NoteOnEmpty, fieldChartShade: ch.Shade, }) assignNonZeroStrings(r, map[string]string{ fieldChartNote: ch.Note, fieldPrefix: ch.Prefix, fieldSuffix: ch.Suffix, fieldChartGeom: ch.Geom, fieldChartXCol: ch.XCol, fieldChartYCol: ch.YCol, fieldChartPosition: ch.Position, fieldChartTickPrefix: ch.TickPrefix, fieldChartTickSuffix: ch.TickSuffix, fieldChartTimeFormat: ch.TimeFormat, fieldChartHoverDimension: ch.HoverDimension, }) assignNonZeroInts(r, map[string]int{ fieldChartXPos: ch.XPos, fieldChartYPos: ch.YPos, fieldChartBinCount: ch.BinCount, fieldChartBinSize: ch.BinSize, }) 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: strings.TrimSpace(iq.Text)}) } return out } // DashboardToObject converts an influxdb.Dashboard to an Object. func DashboardToObject(name string, dash influxdb.Dashboard) Object { if name == "" { name = dash.Name } sort.Slice(dash.Cells, func(i, j int) bool { ic, jc := dash.Cells[i], dash.Cells[j] if ic.X == jc.X { return ic.Y < jc.Y } return ic.X < jc.X }) charts := make([]Resource, 0, len(dash.Cells)) for _, cell := range dash.Cells { if cell.View == nil { continue } ch := convertCellView(*cell) if !ch.Kind.ok() { continue } charts = append(charts, convertChartToResource(ch)) } o := newObject(KindDashboard, name) assignNonZeroStrings(o.Spec, map[string]string{ fieldDescription: dash.Description, }) o.Spec[fieldDashCharts] = charts return o } // LabelToObject converts an influxdb.Label to an Object. func LabelToObject(name string, l influxdb.Label) Object { if name == "" { name = l.Name } o := newObject(KindLabel, name) assignNonZeroStrings(o.Spec, map[string]string{ fieldDescription: l.Properties["description"], fieldLabelColor: l.Properties["color"], }) return o } // NotificationEndpointToObject converts an notification endpoint into a pkger Object. func NotificationEndpointToObject(name string, e influxdb.NotificationEndpoint) Object { if name == "" { name = e.GetName() } o := newObject(KindNotificationEndpoint, name) assignNonZeroStrings(o.Spec, map[string]string{ fieldDescription: e.GetDescription(), fieldStatus: string(e.GetStatus()), }) switch actual := e.(type) { case *endpoint.HTTP: o.Kind = KindNotificationEndpointHTTP o.Spec[fieldNotificationEndpointHTTPMethod] = actual.Method o.Spec[fieldNotificationEndpointURL] = actual.URL o.Spec[fieldType] = actual.AuthMethod assignNonZeroSecrets(o.Spec, map[string]influxdb.SecretField{ fieldNotificationEndpointPassword: actual.Password, fieldNotificationEndpointToken: actual.Token, fieldNotificationEndpointUsername: actual.Username, }) case *endpoint.PagerDuty: o.Kind = KindNotificationEndpointPagerDuty o.Spec[fieldNotificationEndpointURL] = actual.ClientURL assignNonZeroSecrets(o.Spec, map[string]influxdb.SecretField{ fieldNotificationEndpointRoutingKey: actual.RoutingKey, }) case *endpoint.Slack: o.Kind = KindNotificationEndpointSlack o.Spec[fieldNotificationEndpointURL] = actual.URL assignNonZeroSecrets(o.Spec, map[string]influxdb.SecretField{ fieldNotificationEndpointToken: actual.Token, }) } return o } // NotificationRuleToObject converts an notification rule into a pkger Object. func NotificationRuleToObject(name, endpointPkgName string, iRule influxdb.NotificationRule) Object { if name == "" { name = iRule.GetName() } o := newObject(KindNotificationRule, name) o.Spec[fieldNotificationRuleEndpointName] = endpointPkgName assignNonZeroStrings(o.Spec, map[string]string{ fieldDescription: iRule.GetDescription(), }) assignBase := func(base rule.Base) { assignNonZeroFluxDurs(o.Spec, map[string]*notification.Duration{ fieldEvery: base.Every, fieldOffset: base.Offset, }) var tagRes []Resource for _, tRule := range base.TagRules { tagRes = append(tagRes, Resource{ fieldKey: tRule.Key, fieldValue: tRule.Value, fieldOperator: tRule.Operator.String(), }) } if len(tagRes) > 0 { o.Spec[fieldNotificationRuleTagRules] = tagRes } var statusRuleRes []Resource for _, sRule := range base.StatusRules { sRes := Resource{ fieldNotificationRuleCurrentLevel: sRule.CurrentLevel.String(), } if sRule.PreviousLevel != nil { sRes[fieldNotificationRulePreviousLevel] = sRule.PreviousLevel.String() } statusRuleRes = append(statusRuleRes, sRes) } if len(statusRuleRes) > 0 { o.Spec[fieldNotificationRuleStatusRules] = statusRuleRes } } switch t := iRule.(type) { case *rule.HTTP: assignBase(t.Base) case *rule.PagerDuty: assignBase(t.Base) o.Spec[fieldNotificationRuleMessageTemplate] = t.MessageTemplate case *rule.Slack: assignBase(t.Base) o.Spec[fieldNotificationRuleMessageTemplate] = t.MessageTemplate assignNonZeroStrings(o.Spec, map[string]string{fieldNotificationRuleChannel: t.Channel}) } return o } // regex used to rip out the hard coded task option stuffs var taskFluxRegex = regexp.MustCompile(`option task = {(.|\n)*?}`) // TaskToObject converts an influxdb.Task into a pkger.Object. func TaskToObject(name string, t influxdb.Task) Object { if name == "" { name = t.Name } query := strings.TrimSpace(taskFluxRegex.ReplaceAllString(t.Flux, "")) o := newObject(KindTask, name) assignNonZeroStrings(o.Spec, map[string]string{ fieldTaskCron: t.Cron, fieldDescription: t.Description, fieldEvery: t.Every, fieldOffset: durToStr(t.Offset), fieldQuery: strings.TrimSpace(query), }) return o } // TelegrafToObject converts an influxdb.TelegrafConfig into a pkger.Object. func TelegrafToObject(name string, t influxdb.TelegrafConfig) Object { if name == "" { name = t.Name } o := newObject(KindTelegraf, name) assignNonZeroStrings(o.Spec, map[string]string{ fieldTelegrafConfig: t.Config, fieldDescription: t.Description, }) return o } // VariableToObject converts an influxdb.Variable to a pkger.Object. func VariableToObject(name string, v influxdb.Variable) Object { if name == "" { name = v.Name } o := newObject(KindVariable, name) assignNonZeroStrings(o.Spec, map[string]string{fieldDescription: v.Description}) if len(v.Selected) > 0 { o.Spec[fieldVariableSelected] = v.Selected } args := v.Arguments if args == nil { return o } o.Spec[fieldType] = args.Type switch args.Type { case fieldArgTypeConstant: vals, ok := args.Values.(influxdb.VariableConstantValues) if ok { o.Spec[fieldValues] = []string(vals) } case fieldArgTypeMap: vals, ok := args.Values.(influxdb.VariableMapValues) if ok { o.Spec[fieldValues] = map[string]string(vals) } case fieldArgTypeQuery: vals, ok := args.Values.(influxdb.VariableQueryValues) if ok { o.Spec[fieldLanguage] = vals.Language o.Spec[fieldQuery] = strings.TrimSpace(vals.Query) } } return o } func newObject(kind Kind, name string) Object { return Object{ APIVersion: APIVersion, Kind: kind, Metadata: Resource{ // this timestamp is added to make the resource unique. Should also indicate // to the end user that this is machine readable and the spec.name field is // the one they want to edit when a name change is desired. fieldName: strings.ToLower(idGenerator.ID().String()), }, Spec: Resource{ fieldName: name, }, } } func assignNonZeroFluxDurs(r Resource, m map[string]*notification.Duration) { for field, dur := range m { if dur == nil { continue } if dur.TimeDuration() == 0 { continue } r[field] = dur.TimeDuration().String() } } func assignNonZeroBools(r Resource, m map[string]bool) { for k, v := range m { if v { r[k] = v } } } func assignNonZeroInts(r Resource, m map[string]int) { for k, v := range m { if v != 0 { r[k] = v } } } func assignNonZeroStrings(r Resource, m map[string]string) { for k, v := range m { if v != "" { r[k] = v } } } func assignNonZeroSecrets(r Resource, m map[string]influxdb.SecretField) { for field, secret := range m { if secret.Key == "" { continue } r[field] = Resource{ fieldReferencesSecret: Resource{ fieldKey: secret.Key, }, } } } func stringsToColors(clrs []string) colors { newColors := make(colors, 0) for _, x := range clrs { newColors = append(newColors, &color{Hex: x}) } return newColors }