2085 lines
54 KiB
Go
2085 lines
54 KiB
Go
package pkger
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/influxdata/influxdb/v2"
|
|
"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"
|
|
)
|
|
|
|
// Package kind types.
|
|
const (
|
|
KindUnknown Kind = ""
|
|
KindBucket Kind = "Bucket"
|
|
KindCheck Kind = "Check"
|
|
KindCheckDeadman Kind = "CheckDeadman"
|
|
KindCheckThreshold Kind = "CheckThreshold"
|
|
KindDashboard Kind = "Dashboard"
|
|
KindLabel Kind = "Label"
|
|
KindNotificationEndpoint Kind = "NotificationEndpoint"
|
|
KindNotificationEndpointHTTP Kind = "NotificationEndpointHTTP"
|
|
KindNotificationEndpointPagerDuty Kind = "NotificationEndpointPagerDuty"
|
|
KindNotificationEndpointSlack Kind = "NotificationEndpointSlack"
|
|
KindNotificationRule Kind = "NotificationRule"
|
|
KindPackage Kind = "Package"
|
|
KindTask Kind = "Task"
|
|
KindTelegraf Kind = "Telegraf"
|
|
KindVariable Kind = "Variable"
|
|
)
|
|
|
|
var kinds = map[Kind]bool{
|
|
KindBucket: true,
|
|
KindCheck: true,
|
|
KindCheckDeadman: true,
|
|
KindCheckThreshold: true,
|
|
KindDashboard: true,
|
|
KindLabel: true,
|
|
KindNotificationEndpoint: true,
|
|
KindNotificationEndpointHTTP: true,
|
|
KindNotificationEndpointPagerDuty: true,
|
|
KindNotificationEndpointSlack: true,
|
|
KindNotificationRule: true,
|
|
KindTask: true,
|
|
KindTelegraf: true,
|
|
KindVariable: true,
|
|
}
|
|
|
|
// Kind is a resource kind.
|
|
type Kind string
|
|
|
|
// String provides the kind in human readable form.
|
|
func (k Kind) String() string {
|
|
if kinds[k] {
|
|
return string(k)
|
|
}
|
|
if k == KindUnknown {
|
|
return "unknown"
|
|
}
|
|
return string(k)
|
|
}
|
|
|
|
// OK validates the kind is valid.
|
|
func (k Kind) OK() error {
|
|
if k == KindUnknown {
|
|
return errors.New("invalid kind")
|
|
}
|
|
if !kinds[k] {
|
|
return errors.New("unsupported kind provided")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ResourceType converts a kind to a known resource type (if applicable).
|
|
func (k Kind) ResourceType() influxdb.ResourceType {
|
|
switch k {
|
|
case KindBucket:
|
|
return influxdb.BucketsResourceType
|
|
case KindCheck, KindCheckDeadman, KindCheckThreshold:
|
|
return influxdb.ChecksResourceType
|
|
case KindDashboard:
|
|
return influxdb.DashboardsResourceType
|
|
case KindLabel:
|
|
return influxdb.LabelsResourceType
|
|
case KindNotificationEndpoint,
|
|
KindNotificationEndpointHTTP,
|
|
KindNotificationEndpointPagerDuty,
|
|
KindNotificationEndpointSlack:
|
|
return influxdb.NotificationEndpointResourceType
|
|
case KindNotificationRule:
|
|
return influxdb.NotificationRuleResourceType
|
|
case KindTask:
|
|
return influxdb.TasksResourceType
|
|
case KindTelegraf:
|
|
return influxdb.TelegrafsResourceType
|
|
case KindVariable:
|
|
return influxdb.VariablesResourceType
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (k Kind) is(comps ...Kind) bool {
|
|
for _, c := range comps {
|
|
if c == k {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SafeID is an equivalent influxdb.ID that encodes safely with
|
|
// zero values (influxdb.ID == 0).
|
|
type SafeID influxdb.ID
|
|
|
|
// Encode will safely encode the id.
|
|
func (s SafeID) Encode() ([]byte, error) {
|
|
id := influxdb.ID(s)
|
|
b, _ := id.Encode()
|
|
return b, nil
|
|
}
|
|
|
|
// String prints a encoded string representation of the id.
|
|
func (s SafeID) String() string {
|
|
return influxdb.ID(s).String()
|
|
}
|
|
|
|
// DiffIdentifier are the identifying fields for any given resource. Each resource
|
|
// dictates if the resource is new, to be removed, or will remain.
|
|
type DiffIdentifier struct {
|
|
ID SafeID `json:"id"`
|
|
Remove bool `json:"bool"`
|
|
StateStatus StateStatus `json:"stateStatus"`
|
|
PkgName string `json:"pkgName"`
|
|
}
|
|
|
|
// IsNew indicates the resource is new to the platform.
|
|
func (d DiffIdentifier) IsNew() bool {
|
|
return d.ID == 0
|
|
}
|
|
|
|
// Diff is the result of a service DryRun call. The diff outlines
|
|
// what is new and or updated from the current state of the platform.
|
|
type Diff struct {
|
|
Buckets []DiffBucket `json:"buckets"`
|
|
Checks []DiffCheck `json:"checks"`
|
|
Dashboards []DiffDashboard `json:"dashboards"`
|
|
Labels []DiffLabel `json:"labels"`
|
|
LabelMappings []DiffLabelMapping `json:"labelMappings"`
|
|
NotificationEndpoints []DiffNotificationEndpoint `json:"notificationEndpoints"`
|
|
NotificationRules []DiffNotificationRule `json:"notificationRules"`
|
|
Tasks []DiffTask `json:"tasks"`
|
|
Telegrafs []DiffTelegraf `json:"telegrafConfigs"`
|
|
Variables []DiffVariable `json:"variables"`
|
|
}
|
|
|
|
// HasConflicts provides a binary t/f if there are any changes within package
|
|
// after dry run is complete.
|
|
func (d Diff) HasConflicts() bool {
|
|
for _, b := range d.Buckets {
|
|
if b.hasConflict() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
for _, l := range d.Labels {
|
|
if l.hasConflict() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
for _, v := range d.Variables {
|
|
if v.hasConflict() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type (
|
|
// DiffBucket is a diff of an individual bucket.
|
|
DiffBucket struct {
|
|
DiffIdentifier
|
|
|
|
New DiffBucketValues `json:"new"`
|
|
Old *DiffBucketValues `json:"old"`
|
|
}
|
|
|
|
// DiffBucketValues are the varying values for a bucket.
|
|
DiffBucketValues struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
RetentionRules retentionRules `json:"retentionRules"`
|
|
}
|
|
)
|
|
|
|
func (d DiffBucket) hasConflict() bool {
|
|
return !d.IsNew() && d.Old != nil && !reflect.DeepEqual(*d.Old, d.New)
|
|
}
|
|
|
|
// DiffCheckValues are the varying values for a check.
|
|
type DiffCheckValues struct {
|
|
influxdb.Check
|
|
}
|
|
|
|
// MarshalJSON implementation here is forced by the embedded check value here.
|
|
func (d DiffCheckValues) MarshalJSON() ([]byte, error) {
|
|
if d.Check == nil {
|
|
return json.Marshal(nil)
|
|
}
|
|
return json.Marshal(d.Check)
|
|
}
|
|
|
|
// UnmarshalJSON decodes the check values.
|
|
func (d *DiffCheckValues) UnmarshalJSON(b []byte) (err error) {
|
|
d.Check, err = icheck.UnmarshalJSON(b)
|
|
if influxdb.EInternal == influxdb.ErrorCode(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// DiffCheck is a diff of an individual check.
|
|
type DiffCheck struct {
|
|
DiffIdentifier
|
|
|
|
New DiffCheckValues `json:"new"`
|
|
Old *DiffCheckValues `json:"old"`
|
|
}
|
|
|
|
type (
|
|
// DiffDashboard is a diff of an individual dashboard.
|
|
DiffDashboard struct {
|
|
DiffIdentifier
|
|
|
|
New DiffDashboardValues `json:"new"`
|
|
Old *DiffDashboardValues `json:"old"`
|
|
}
|
|
|
|
// DiffDashboardValues are values for a dashboard.
|
|
DiffDashboardValues struct {
|
|
Name string `json:"name"`
|
|
Desc string `json:"description"`
|
|
Charts []DiffChart `json:"charts"`
|
|
}
|
|
)
|
|
|
|
func newDiffDashboard(d *dashboard) DiffDashboard {
|
|
diff := DiffDashboard{
|
|
DiffIdentifier: DiffIdentifier{
|
|
ID: SafeID(d.ID()),
|
|
Remove: d.shouldRemove,
|
|
PkgName: d.PkgName(),
|
|
},
|
|
New: DiffDashboardValues{
|
|
Name: d.Name(),
|
|
Desc: d.Description,
|
|
Charts: make([]DiffChart, 0, len(d.Charts)),
|
|
},
|
|
}
|
|
|
|
for _, c := range d.Charts {
|
|
diff.New.Charts = append(diff.New.Charts, DiffChart{
|
|
Properties: c.properties(),
|
|
Height: c.Height,
|
|
Width: c.Width,
|
|
})
|
|
}
|
|
|
|
if !d.Exists() {
|
|
return diff
|
|
}
|
|
|
|
oldDiff := DiffDashboardValues{
|
|
Name: d.existing.Name,
|
|
Desc: d.existing.Description,
|
|
Charts: make([]DiffChart, 0, len(d.existing.Cells)),
|
|
}
|
|
|
|
for _, c := range d.existing.Cells {
|
|
var props influxdb.ViewProperties
|
|
if c.View != nil {
|
|
props = c.View.Properties
|
|
}
|
|
|
|
oldDiff.Charts = append(oldDiff.Charts, DiffChart{
|
|
Properties: props,
|
|
XPosition: int(c.X),
|
|
YPosition: int(c.Y),
|
|
Height: int(c.H),
|
|
Width: int(c.W),
|
|
})
|
|
}
|
|
|
|
diff.Old = &oldDiff
|
|
|
|
return diff
|
|
}
|
|
|
|
// DiffChart is a diff of oa chart. Since all charts are new right now.
|
|
// the SummaryChart is reused here.
|
|
type DiffChart SummaryChart
|
|
|
|
type (
|
|
// DiffLabel is a diff of an individual label.
|
|
DiffLabel struct {
|
|
DiffIdentifier
|
|
|
|
New DiffLabelValues `json:"new"`
|
|
Old *DiffLabelValues `json:"old"`
|
|
}
|
|
|
|
// DiffLabelValues are the varying values for a label.
|
|
DiffLabelValues struct {
|
|
Name string `json:"name"`
|
|
Color string `json:"color"`
|
|
Description string `json:"description"`
|
|
}
|
|
)
|
|
|
|
func (d DiffLabel) hasConflict() bool {
|
|
return !d.IsNew() && d.Old != nil && *d.Old != d.New
|
|
}
|
|
|
|
// StateStatus indicates the status of a diff or summary resource
|
|
type StateStatus string
|
|
|
|
const (
|
|
StateStatusExists StateStatus = "exists"
|
|
StateStatusNew StateStatus = "new"
|
|
StateStatusRemove StateStatus = "remove"
|
|
)
|
|
|
|
// DiffLabelMapping is a diff of an individual label mapping. A
|
|
// single resource may have multiple mappings to multiple labels.
|
|
// A label can have many mappings to other resources.
|
|
type DiffLabelMapping struct {
|
|
StateStatus StateStatus `json:"stateStatus"`
|
|
|
|
ResType influxdb.ResourceType `json:"resourceType"`
|
|
ResID SafeID `json:"resourceID"`
|
|
ResName string `json:"resourceName"`
|
|
ResPkgName string `json:"resourcePkgName"`
|
|
|
|
LabelID SafeID `json:"labelID"`
|
|
LabelName string `json:"labelName"`
|
|
LabelPkgName string `json:"labelPkgName"`
|
|
}
|
|
|
|
//func (d DiffLabelMapping) IsNew() bool {
|
|
// return d.StateStatus == StateStatusNew
|
|
//}
|
|
|
|
// DiffNotificationEndpointValues are the varying values for a notification endpoint.
|
|
type DiffNotificationEndpointValues struct {
|
|
influxdb.NotificationEndpoint
|
|
}
|
|
|
|
// MarshalJSON implementation here is forced by the embedded check value here.
|
|
func (d DiffNotificationEndpointValues) MarshalJSON() ([]byte, error) {
|
|
if d.NotificationEndpoint == nil {
|
|
return json.Marshal(nil)
|
|
}
|
|
return json.Marshal(d.NotificationEndpoint)
|
|
}
|
|
|
|
// UnmarshalJSON decodes the notification endpoint. This is necessary unfortunately.
|
|
func (d *DiffNotificationEndpointValues) UnmarshalJSON(b []byte) (err error) {
|
|
d.NotificationEndpoint, err = endpoint.UnmarshalJSON(b)
|
|
if influxdb.EInvalid == influxdb.ErrorCode(err) {
|
|
return nil
|
|
}
|
|
return
|
|
}
|
|
|
|
// DiffNotificationEndpoint is a diff of an individual notification endpoint.
|
|
type DiffNotificationEndpoint struct {
|
|
DiffIdentifier
|
|
|
|
New DiffNotificationEndpointValues `json:"new"`
|
|
Old *DiffNotificationEndpointValues `json:"old"`
|
|
}
|
|
|
|
type (
|
|
// DiffNotificationRule is a diff of an individual notification rule.
|
|
DiffNotificationRule struct {
|
|
DiffIdentifier
|
|
|
|
New DiffNotificationRuleValues `json:"new"`
|
|
Old *DiffNotificationRuleValues `json:"old"`
|
|
}
|
|
|
|
// DiffNotificationRuleValues are the values for an individual rule.
|
|
DiffNotificationRuleValues struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
|
|
// These 3 fields represent the relationship of the rule to the endpoint.
|
|
EndpointID SafeID `json:"endpointID"`
|
|
EndpointName string `json:"endpointName"`
|
|
EndpointType string `json:"endpointType"`
|
|
|
|
Every string `json:"every"`
|
|
Offset string `json:"offset"`
|
|
MessageTemplate string `json:"messageTemplate"`
|
|
StatusRules []SummaryStatusRule `json:"statusRules"`
|
|
TagRules []SummaryTagRule `json:"tagRules"`
|
|
}
|
|
)
|
|
|
|
func newDiffNotificationRule(r *notificationRule, iEndpoint influxdb.NotificationEndpoint) DiffNotificationRule {
|
|
sum := DiffNotificationRule{
|
|
DiffIdentifier: DiffIdentifier{
|
|
ID: SafeID(r.ID()),
|
|
Remove: r.shouldRemove,
|
|
PkgName: r.PkgName(),
|
|
},
|
|
New: DiffNotificationRuleValues{
|
|
Name: r.Name(),
|
|
Description: r.description,
|
|
EndpointName: r.endpointName.String(),
|
|
Every: r.every.String(),
|
|
Offset: r.offset.String(),
|
|
MessageTemplate: r.msgTemplate,
|
|
StatusRules: toSummaryStatusRules(r.statusRules),
|
|
TagRules: toSummaryTagRules(r.tagRules),
|
|
},
|
|
}
|
|
if iEndpoint != nil {
|
|
sum.New.EndpointID = SafeID(iEndpoint.GetID())
|
|
sum.New.EndpointType = iEndpoint.Type()
|
|
}
|
|
|
|
if r.existing == nil {
|
|
return sum
|
|
}
|
|
|
|
sum.Old = &DiffNotificationRuleValues{
|
|
Name: r.existing.rule.GetName(),
|
|
Description: r.existing.rule.GetDescription(),
|
|
EndpointName: r.existing.endpointName,
|
|
EndpointID: SafeID(r.existing.rule.GetEndpointID()),
|
|
EndpointType: r.existing.endpointType,
|
|
}
|
|
|
|
assignBase := func(b rule.Base) {
|
|
if b.Every != nil {
|
|
sum.Old.Every = b.Every.TimeDuration().String()
|
|
}
|
|
if b.Offset != nil {
|
|
sum.Old.Offset = b.Offset.TimeDuration().String()
|
|
}
|
|
for _, tr := range b.TagRules {
|
|
sum.Old.TagRules = append(sum.Old.TagRules, SummaryTagRule{
|
|
Key: tr.Key,
|
|
Value: tr.Value,
|
|
Operator: tr.Operator.String(),
|
|
})
|
|
}
|
|
for _, sr := range b.StatusRules {
|
|
sRule := SummaryStatusRule{CurrentLevel: sr.CurrentLevel.String()}
|
|
if sr.PreviousLevel != nil {
|
|
sRule.PreviousLevel = sr.PreviousLevel.String()
|
|
}
|
|
sum.Old.StatusRules = append(sum.Old.StatusRules, sRule)
|
|
}
|
|
}
|
|
|
|
switch p := r.existing.rule.(type) {
|
|
case *rule.HTTP:
|
|
assignBase(p.Base)
|
|
case *rule.Slack:
|
|
assignBase(p.Base)
|
|
sum.Old.MessageTemplate = p.MessageTemplate
|
|
case *rule.PagerDuty:
|
|
assignBase(p.Base)
|
|
sum.Old.MessageTemplate = p.MessageTemplate
|
|
}
|
|
|
|
return sum
|
|
}
|
|
|
|
type (
|
|
// DiffTask is a diff of an individual task.
|
|
DiffTask struct {
|
|
DiffIdentifier
|
|
|
|
New DiffTaskValues `json:"new"`
|
|
Old *DiffTaskValues `json:"old"`
|
|
}
|
|
|
|
// DiffTaskValues are the values for an individual task.
|
|
DiffTaskValues struct {
|
|
Name string `json:"name"`
|
|
Cron string `json:"cron"`
|
|
Description string `json:"description"`
|
|
Every string `json:"every"`
|
|
Offset string `json:"offset"`
|
|
Query string `json:"query"`
|
|
Status influxdb.Status `json:"status"`
|
|
}
|
|
)
|
|
|
|
func newDiffTask(t *task) DiffTask {
|
|
diff := DiffTask{
|
|
DiffIdentifier: DiffIdentifier{
|
|
ID: SafeID(t.ID()),
|
|
Remove: t.shouldRemove,
|
|
PkgName: t.PkgName(),
|
|
},
|
|
New: DiffTaskValues{
|
|
Name: t.Name(),
|
|
Cron: t.cron,
|
|
Description: t.description,
|
|
Every: durToStr(t.every),
|
|
Offset: durToStr(t.offset),
|
|
Query: t.query,
|
|
Status: t.Status(),
|
|
},
|
|
}
|
|
|
|
if !t.Exists() {
|
|
return diff
|
|
}
|
|
|
|
diff.Old = &DiffTaskValues{
|
|
Name: t.existing.Name,
|
|
Cron: t.existing.Cron,
|
|
Description: t.existing.Description,
|
|
Every: t.existing.Every,
|
|
Offset: t.existing.Offset.String(),
|
|
Query: t.existing.Flux,
|
|
Status: influxdb.Status(t.existing.Status),
|
|
}
|
|
|
|
return diff
|
|
}
|
|
|
|
// DiffTelegraf is a diff of an individual telegraf. This resource is always new.
|
|
type DiffTelegraf struct {
|
|
DiffIdentifier
|
|
|
|
New influxdb.TelegrafConfig
|
|
Old *influxdb.TelegrafConfig
|
|
}
|
|
|
|
func newDiffTelegraf(t *telegraf) DiffTelegraf {
|
|
return DiffTelegraf{
|
|
DiffIdentifier: DiffIdentifier{
|
|
ID: SafeID(t.ID()),
|
|
Remove: t.shouldRemove,
|
|
PkgName: t.PkgName(),
|
|
},
|
|
New: t.config,
|
|
Old: t.existing,
|
|
}
|
|
}
|
|
|
|
type (
|
|
// DiffVariable is a diff of an individual variable.
|
|
DiffVariable struct {
|
|
DiffIdentifier
|
|
|
|
New DiffVariableValues `json:"new"`
|
|
Old *DiffVariableValues `json:"old,omitempty"` // using omitempty here to signal there was no prev state with a nil
|
|
}
|
|
|
|
// DiffVariableValues are the varying values for a variable.
|
|
DiffVariableValues struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Args *influxdb.VariableArguments `json:"args"`
|
|
}
|
|
)
|
|
|
|
func (d DiffVariable) hasConflict() bool {
|
|
return !d.IsNew() && d.Old != nil && !reflect.DeepEqual(*d.Old, d.New)
|
|
}
|
|
|
|
// Summary is a definition of all the resources that have or
|
|
// will be created from a pkg.
|
|
type Summary struct {
|
|
Buckets []SummaryBucket `json:"buckets"`
|
|
Checks []SummaryCheck `json:"checks"`
|
|
Dashboards []SummaryDashboard `json:"dashboards"`
|
|
NotificationEndpoints []SummaryNotificationEndpoint `json:"notificationEndpoints"`
|
|
NotificationRules []SummaryNotificationRule `json:"notificationRules"`
|
|
Labels []SummaryLabel `json:"labels"`
|
|
LabelMappings []SummaryLabelMapping `json:"labelMappings"`
|
|
MissingEnvs []string `json:"missingEnvRefs"`
|
|
MissingSecrets []string `json:"missingSecrets"`
|
|
Tasks []SummaryTask `json:"summaryTask"`
|
|
TelegrafConfigs []SummaryTelegraf `json:"telegrafConfigs"`
|
|
Variables []SummaryVariable `json:"variables"`
|
|
}
|
|
|
|
// SummaryBucket provides a summary of a pkg bucket.
|
|
type SummaryBucket struct {
|
|
ID SafeID `json:"id,omitempty"`
|
|
OrgID SafeID `json:"orgID,omitempty"`
|
|
Name string `json:"name"`
|
|
PkgName string `json:"pkgName"`
|
|
Description string `json:"description"`
|
|
// TODO: return retention rules?
|
|
RetentionPeriod time.Duration `json:"retentionPeriod"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
|
|
// SummaryCheck provides a summary of a pkg check.
|
|
type SummaryCheck struct {
|
|
PkgName string `json:"pkgName"`
|
|
Check influxdb.Check `json:"check"`
|
|
Status influxdb.Status `json:"status"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
|
|
func (s *SummaryCheck) UnmarshalJSON(b []byte) error {
|
|
var out struct {
|
|
PkgName string `json:"pkgName"`
|
|
Status string `json:"status"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
Check json.RawMessage `json:"check"`
|
|
}
|
|
if err := json.Unmarshal(b, &out); err != nil {
|
|
return err
|
|
}
|
|
s.PkgName = out.PkgName
|
|
s.Status = influxdb.Status(out.Status)
|
|
s.LabelAssociations = out.LabelAssociations
|
|
|
|
var err error
|
|
s.Check, err = icheck.UnmarshalJSON(out.Check)
|
|
return err
|
|
}
|
|
|
|
// SummaryDashboard provides a summary of a pkg dashboard.
|
|
type SummaryDashboard struct {
|
|
ID SafeID `json:"id"`
|
|
OrgID SafeID `json:"orgID"`
|
|
PkgName string `json:"pkgName"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Charts []SummaryChart `json:"charts"`
|
|
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
|
|
// chartKind identifies what kind of chart is eluded too. Each
|
|
// chart kind has their own requirements for what constitutes
|
|
// a chart.
|
|
type chartKind string
|
|
|
|
// available chart kinds
|
|
const (
|
|
chartKindUnknown chartKind = ""
|
|
chartKindGauge chartKind = "gauge"
|
|
chartKindHeatMap chartKind = "heatmap"
|
|
chartKindHistogram chartKind = "histogram"
|
|
chartKindMarkdown chartKind = "markdown"
|
|
chartKindScatter chartKind = "scatter"
|
|
chartKindSingleStat chartKind = "single_stat"
|
|
chartKindSingleStatPlusLine chartKind = "single_stat_plus_line"
|
|
chartKindTable chartKind = "table"
|
|
chartKindXY chartKind = "xy"
|
|
)
|
|
|
|
func (c chartKind) ok() bool {
|
|
switch c {
|
|
case chartKindGauge, chartKindHeatMap, chartKindHistogram,
|
|
chartKindMarkdown, chartKindScatter, chartKindSingleStat,
|
|
chartKindSingleStatPlusLine, chartKindTable, chartKindXY:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (c chartKind) title() string {
|
|
spacedKind := strings.ReplaceAll(string(c), "_", " ")
|
|
return strings.ReplaceAll(strings.Title(spacedKind), " ", "_")
|
|
}
|
|
|
|
// SummaryChart provides a summary of a pkg dashboard's chart.
|
|
type SummaryChart struct {
|
|
Properties influxdb.ViewProperties `json:"-"`
|
|
|
|
XPosition int `json:"xPos"`
|
|
YPosition int `json:"yPos"`
|
|
Height int `json:"height"`
|
|
Width int `json:"width"`
|
|
}
|
|
|
|
// MarshalJSON marshals a summary chart.
|
|
func (s *SummaryChart) MarshalJSON() ([]byte, error) {
|
|
b, err := influxdb.MarshalViewPropertiesJSON(s.Properties)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
type alias SummaryChart
|
|
|
|
out := struct {
|
|
Props json.RawMessage `json:"properties"`
|
|
alias
|
|
}{
|
|
Props: b,
|
|
alias: alias(*s),
|
|
}
|
|
return json.Marshal(out)
|
|
}
|
|
|
|
// UnmarshalJSON unmarshals a view properities and other data.
|
|
func (s *SummaryChart) UnmarshalJSON(b []byte) error {
|
|
type alias SummaryChart
|
|
a := (*alias)(s)
|
|
if err := json.Unmarshal(b, a); err != nil {
|
|
return err
|
|
}
|
|
s.XPosition = a.XPosition
|
|
s.XPosition = a.YPosition
|
|
s.Height = a.Height
|
|
s.Width = a.Width
|
|
|
|
vp, err := influxdb.UnmarshalViewPropertiesJSON(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.Properties = vp
|
|
return nil
|
|
}
|
|
|
|
// SummaryNotificationEndpoint provides a summary of a pkg notification endpoint.
|
|
type SummaryNotificationEndpoint struct {
|
|
PkgName string `json:"pkgName"`
|
|
NotificationEndpoint influxdb.NotificationEndpoint `json:"notificationEndpoint"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
|
|
// UnmarshalJSON unmarshals the notificatio endpoint. This is necessary b/c of
|
|
// the notification endpoint does not have a means ot unmarshal itself.
|
|
func (s *SummaryNotificationEndpoint) UnmarshalJSON(b []byte) error {
|
|
var a struct {
|
|
PkgName string `json:"pkgName"`
|
|
NotificationEndpoint json.RawMessage `json:"notificationEndpoint"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
if err := json.Unmarshal(b, &a); err != nil {
|
|
return err
|
|
}
|
|
s.PkgName = a.PkgName
|
|
s.LabelAssociations = a.LabelAssociations
|
|
|
|
e, err := endpoint.UnmarshalJSON(a.NotificationEndpoint)
|
|
s.NotificationEndpoint = e
|
|
return err
|
|
}
|
|
|
|
// Summary types for NotificationRules which provide a summary of a pkg notification rule.
|
|
type (
|
|
SummaryNotificationRule struct {
|
|
ID SafeID `json:"id"`
|
|
PkgName string `json:"pkgName"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
|
|
// These 3 fields represent the relationship of the rule to the endpoint.
|
|
EndpointID SafeID `json:"endpointID"`
|
|
EndpointName string `json:"endpointName"`
|
|
EndpointType string `json:"endpointType"`
|
|
|
|
Every string `json:"every"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
Offset string `json:"offset"`
|
|
MessageTemplate string `json:"messageTemplate"`
|
|
Status influxdb.Status `json:"status"`
|
|
StatusRules []SummaryStatusRule `json:"statusRules"`
|
|
TagRules []SummaryTagRule `json:"tagRules"`
|
|
}
|
|
|
|
SummaryStatusRule struct {
|
|
CurrentLevel string `json:"currentLevel"`
|
|
PreviousLevel string `json:"previousLevel"`
|
|
}
|
|
|
|
SummaryTagRule struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Operator string `json:"operator"`
|
|
}
|
|
)
|
|
|
|
// SummaryLabel provides a summary of a pkg label.
|
|
type SummaryLabel struct {
|
|
ID SafeID `json:"id"`
|
|
OrgID SafeID `json:"orgID"`
|
|
PkgName string `json:"pkgName"`
|
|
Name string `json:"name"`
|
|
Properties struct {
|
|
Color string `json:"color"`
|
|
Description string `json:"description"`
|
|
} `json:"properties"`
|
|
}
|
|
|
|
// SummaryLabelMapping provides a summary of a label mapped with a single resource.
|
|
type SummaryLabelMapping struct {
|
|
exists bool
|
|
Status StateStatus `json:"status,omitempty"`
|
|
ResourceID SafeID `json:"resourceID"`
|
|
ResourcePkgName string `json:"resourcePkgName"`
|
|
ResourceName string `json:"resourceName"`
|
|
ResourceType influxdb.ResourceType `json:"resourceType"`
|
|
LabelPkgName string `json:"labelPkgName"`
|
|
LabelName string `json:"labelName"`
|
|
LabelID SafeID `json:"labelID"`
|
|
}
|
|
|
|
// SummaryTask provides a summary of a task.
|
|
type SummaryTask struct {
|
|
ID SafeID `json:"id"`
|
|
PkgName string `json:"pkgName"`
|
|
Name string `json:"name"`
|
|
Cron string `json:"cron"`
|
|
Description string `json:"description"`
|
|
Every string `json:"every"`
|
|
Offset string `json:"offset"`
|
|
Query string `json:"query"`
|
|
Status influxdb.Status `json:"status"`
|
|
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
|
|
// SummaryTelegraf provides a summary of a pkg telegraf config.
|
|
type SummaryTelegraf struct {
|
|
PkgName string `json:"pkgName"`
|
|
TelegrafConfig influxdb.TelegrafConfig `json:"telegrafConfig"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
|
|
// SummaryVariable provides a summary of a pkg variable.
|
|
type SummaryVariable struct {
|
|
ID SafeID `json:"id,omitempty"`
|
|
PkgName string `json:"pkgName"`
|
|
OrgID SafeID `json:"orgID,omitempty"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Arguments *influxdb.VariableArguments `json:"arguments"`
|
|
LabelAssociations []SummaryLabel `json:"labelAssociations"`
|
|
}
|
|
|
|
const (
|
|
fieldNotificationRuleChannel = "channel"
|
|
fieldNotificationRuleCurrentLevel = "currentLevel"
|
|
fieldNotificationRuleEndpointName = "endpointName"
|
|
fieldNotificationRuleMessageTemplate = "messageTemplate"
|
|
fieldNotificationRulePreviousLevel = "previousLevel"
|
|
fieldNotificationRuleStatusRules = "statusRules"
|
|
fieldNotificationRuleTagRules = "tagRules"
|
|
)
|
|
|
|
type notificationRule struct {
|
|
identity
|
|
|
|
id influxdb.ID
|
|
orgID influxdb.ID
|
|
|
|
channel string
|
|
description string
|
|
every time.Duration
|
|
msgTemplate string
|
|
offset time.Duration
|
|
status string
|
|
statusRules []struct{ curLvl, prevLvl string }
|
|
tagRules []struct{ k, v, op string }
|
|
|
|
endpointID influxdb.ID
|
|
endpointName *references
|
|
endpointType string
|
|
|
|
existing *existingRule
|
|
|
|
labels sortedLabels
|
|
}
|
|
|
|
type existingRule struct {
|
|
rule influxdb.NotificationRule
|
|
endpointName string
|
|
endpointType string
|
|
}
|
|
|
|
func (r *notificationRule) Exists() bool {
|
|
return r.existing != nil
|
|
}
|
|
|
|
func (r *notificationRule) ID() influxdb.ID {
|
|
if r.existing != nil {
|
|
return r.existing.rule.GetID()
|
|
}
|
|
return r.id
|
|
}
|
|
|
|
func (r *notificationRule) Labels() []*label {
|
|
return r.labels
|
|
}
|
|
|
|
func (r *notificationRule) ResourceType() influxdb.ResourceType {
|
|
return KindNotificationRule.ResourceType()
|
|
}
|
|
|
|
func (r *notificationRule) Status() influxdb.Status {
|
|
if r.status == "" {
|
|
return influxdb.Active
|
|
}
|
|
return influxdb.Status(r.status)
|
|
}
|
|
|
|
func (r *notificationRule) summarize() SummaryNotificationRule {
|
|
return SummaryNotificationRule{
|
|
ID: SafeID(r.ID()),
|
|
PkgName: r.PkgName(),
|
|
Name: r.Name(),
|
|
EndpointID: SafeID(r.endpointID),
|
|
EndpointName: r.endpointName.String(),
|
|
EndpointType: r.endpointType,
|
|
Description: r.description,
|
|
Every: r.every.String(),
|
|
LabelAssociations: toSummaryLabels(r.labels...),
|
|
Offset: r.offset.String(),
|
|
MessageTemplate: r.msgTemplate,
|
|
Status: r.Status(),
|
|
StatusRules: toSummaryStatusRules(r.statusRules),
|
|
TagRules: toSummaryTagRules(r.tagRules),
|
|
}
|
|
}
|
|
|
|
func (r *notificationRule) toInfluxRule() influxdb.NotificationRule {
|
|
base := rule.Base{
|
|
ID: r.ID(),
|
|
Name: r.Name(),
|
|
Description: r.description,
|
|
EndpointID: r.endpointID,
|
|
OrgID: r.orgID,
|
|
Every: toNotificationDuration(r.every),
|
|
Offset: toNotificationDuration(r.offset),
|
|
}
|
|
for _, sr := range r.statusRules {
|
|
var prevLvl *notification.CheckLevel
|
|
if lvl := notification.ParseCheckLevel(sr.prevLvl); lvl != notification.Unknown {
|
|
prevLvl = &lvl
|
|
}
|
|
base.StatusRules = append(base.StatusRules, notification.StatusRule{
|
|
CurrentLevel: notification.ParseCheckLevel(sr.curLvl),
|
|
PreviousLevel: prevLvl,
|
|
})
|
|
}
|
|
for _, tr := range r.tagRules {
|
|
op, _ := influxdb.ToOperator(tr.op)
|
|
base.TagRules = append(base.TagRules, notification.TagRule{
|
|
Tag: influxdb.Tag{
|
|
Key: tr.k,
|
|
Value: tr.v,
|
|
},
|
|
Operator: op,
|
|
})
|
|
}
|
|
|
|
switch r.endpointType {
|
|
case "http":
|
|
return &rule.HTTP{Base: base}
|
|
case "pagerduty":
|
|
return &rule.PagerDuty{
|
|
Base: base,
|
|
MessageTemplate: r.msgTemplate,
|
|
}
|
|
case "slack":
|
|
return &rule.Slack{
|
|
Base: base,
|
|
Channel: r.channel,
|
|
MessageTemplate: r.msgTemplate,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *notificationRule) valid() []validationErr {
|
|
var vErrs []validationErr
|
|
if !r.endpointName.hasValue() {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldNotificationRuleEndpointName,
|
|
Msg: "must be provided",
|
|
})
|
|
}
|
|
if r.every == 0 {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldEvery,
|
|
Msg: "must be provided",
|
|
})
|
|
}
|
|
if status := r.Status(); status != influxdb.Active && status != influxdb.Inactive {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldStatus,
|
|
Msg: fmt.Sprintf("must be 1 in [active, inactive]; got=%q", r.status),
|
|
})
|
|
}
|
|
|
|
if len(r.statusRules) == 0 {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldNotificationRuleStatusRules,
|
|
Msg: "must provide at least 1",
|
|
})
|
|
}
|
|
|
|
var sRuleErrs []validationErr
|
|
for i, sRule := range r.statusRules {
|
|
if notification.ParseCheckLevel(sRule.curLvl) == notification.Unknown {
|
|
sRuleErrs = append(sRuleErrs, validationErr{
|
|
Field: fieldNotificationRuleCurrentLevel,
|
|
Msg: fmt.Sprintf("must be 1 in [CRIT, WARN, INFO, OK]; got=%q", sRule.curLvl),
|
|
Index: intPtr(i),
|
|
})
|
|
}
|
|
if sRule.prevLvl != "" && notification.ParseCheckLevel(sRule.prevLvl) == notification.Unknown {
|
|
sRuleErrs = append(sRuleErrs, validationErr{
|
|
Field: fieldNotificationRulePreviousLevel,
|
|
Msg: fmt.Sprintf("must be 1 in [CRIT, WARN, INFO, OK]; got=%q", sRule.prevLvl),
|
|
Index: intPtr(i),
|
|
})
|
|
}
|
|
}
|
|
if len(sRuleErrs) > 0 {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldNotificationRuleStatusRules,
|
|
Nested: sRuleErrs,
|
|
})
|
|
}
|
|
|
|
var tagErrs []validationErr
|
|
for i, tRule := range r.tagRules {
|
|
if _, ok := influxdb.ToOperator(tRule.op); !ok {
|
|
tagErrs = append(tagErrs, validationErr{
|
|
Field: fieldOperator,
|
|
Msg: fmt.Sprintf("must be 1 in [equal]; got=%q", tRule.op),
|
|
Index: intPtr(i),
|
|
})
|
|
}
|
|
}
|
|
if len(tagErrs) > 0 {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldNotificationRuleTagRules,
|
|
Nested: tagErrs,
|
|
})
|
|
}
|
|
|
|
if len(vErrs) > 0 {
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, vErrs...),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func toSummaryStatusRules(statusRules []struct{ curLvl, prevLvl string }) []SummaryStatusRule {
|
|
out := make([]SummaryStatusRule, 0, len(statusRules))
|
|
for _, sRule := range statusRules {
|
|
out = append(out, SummaryStatusRule{
|
|
CurrentLevel: sRule.curLvl,
|
|
PreviousLevel: sRule.prevLvl,
|
|
})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
si, sj := out[i], out[j]
|
|
if si.CurrentLevel == sj.CurrentLevel {
|
|
return si.PreviousLevel < sj.PreviousLevel
|
|
}
|
|
return si.CurrentLevel < sj.CurrentLevel
|
|
})
|
|
return out
|
|
}
|
|
|
|
func toSummaryTagRules(tagRules []struct{ k, v, op string }) []SummaryTagRule {
|
|
out := make([]SummaryTagRule, 0, len(tagRules))
|
|
for _, tRule := range tagRules {
|
|
out = append(out, SummaryTagRule{
|
|
Key: tRule.k,
|
|
Value: tRule.v,
|
|
Operator: tRule.op,
|
|
})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool {
|
|
ti, tj := out[i], out[j]
|
|
if ti.Key == tj.Key && ti.Value == tj.Value {
|
|
return ti.Operator < tj.Operator
|
|
}
|
|
if ti.Key == tj.Key {
|
|
return ti.Value < tj.Value
|
|
}
|
|
return ti.Key < tj.Key
|
|
})
|
|
return out
|
|
}
|
|
|
|
type mapperNotificationRules []*notificationRule
|
|
|
|
func (r mapperNotificationRules) Association(i int) labelAssociater {
|
|
return r[i]
|
|
}
|
|
|
|
func (r mapperNotificationRules) Len() int {
|
|
return len(r)
|
|
}
|
|
|
|
const (
|
|
fieldTaskCron = "cron"
|
|
)
|
|
|
|
type task struct {
|
|
identity
|
|
|
|
id influxdb.ID
|
|
orgID influxdb.ID
|
|
cron string
|
|
description string
|
|
every time.Duration
|
|
offset time.Duration
|
|
query string
|
|
status string
|
|
|
|
labels sortedLabels
|
|
|
|
existing *influxdb.Task
|
|
}
|
|
|
|
func (t *task) Exists() bool {
|
|
return t.existing != nil
|
|
}
|
|
|
|
func (t *task) ID() influxdb.ID {
|
|
if t.existing != nil {
|
|
return t.existing.ID
|
|
}
|
|
return t.id
|
|
}
|
|
|
|
func (t *task) Labels() []*label {
|
|
return t.labels
|
|
}
|
|
|
|
func (t *task) ResourceType() influxdb.ResourceType {
|
|
return KindTask.ResourceType()
|
|
}
|
|
|
|
func (t *task) Status() influxdb.Status {
|
|
if t.status == "" {
|
|
return influxdb.Active
|
|
}
|
|
return influxdb.Status(t.status)
|
|
}
|
|
|
|
var fluxRegex = regexp.MustCompile(`import\s+\".*\"`)
|
|
|
|
func (t *task) flux() string {
|
|
taskOpts := []string{fmt.Sprintf("name: %q", t.Name())}
|
|
if t.cron != "" {
|
|
taskOpts = append(taskOpts, fmt.Sprintf("cron: %q", t.cron))
|
|
}
|
|
if t.every > 0 {
|
|
taskOpts = append(taskOpts, fmt.Sprintf("every: %s", t.every))
|
|
}
|
|
if t.offset > 0 {
|
|
taskOpts = append(taskOpts, fmt.Sprintf("offset: %s", t.offset))
|
|
}
|
|
|
|
// this is required by the API, super nasty. Will be super challenging for
|
|
// anyone outside org to figure out how to do this within an hour of looking
|
|
// at the API :sadpanda:. Would be ideal to let the API translate the arguments
|
|
// into this required form instead of forcing that complexity on the caller.
|
|
taskOptStr := fmt.Sprintf("\noption task = { %s }", strings.Join(taskOpts, ", "))
|
|
|
|
if indices := fluxRegex.FindAllIndex([]byte(t.query), -1); len(indices) > 0 {
|
|
lastImportIdx := indices[len(indices)-1][1]
|
|
pieces := append([]string{},
|
|
t.query[:lastImportIdx],
|
|
taskOptStr,
|
|
t.query[lastImportIdx:],
|
|
)
|
|
return fmt.Sprint(strings.Join(pieces, "\n"))
|
|
}
|
|
|
|
return fmt.Sprintf("%s\n%s", taskOptStr, t.query)
|
|
}
|
|
|
|
func (t *task) summarize() SummaryTask {
|
|
return SummaryTask{
|
|
ID: SafeID(t.ID()),
|
|
PkgName: t.PkgName(),
|
|
Name: t.Name(),
|
|
Cron: t.cron,
|
|
Description: t.description,
|
|
Every: durToStr(t.every),
|
|
Offset: durToStr(t.offset),
|
|
Query: t.query,
|
|
Status: t.Status(),
|
|
|
|
LabelAssociations: toSummaryLabels(t.labels...),
|
|
}
|
|
}
|
|
|
|
func (t *task) valid() []validationErr {
|
|
var vErrs []validationErr
|
|
if t.cron == "" && t.every == 0 {
|
|
vErrs = append(vErrs,
|
|
validationErr{
|
|
Field: fieldEvery,
|
|
Msg: "must provide if cron field is not provided",
|
|
},
|
|
validationErr{
|
|
Field: fieldTaskCron,
|
|
Msg: "must provide if every field is not provided",
|
|
},
|
|
)
|
|
}
|
|
|
|
if t.query == "" {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldQuery,
|
|
Msg: "must provide a non zero value",
|
|
})
|
|
}
|
|
|
|
if status := t.Status(); status != influxdb.Active && status != influxdb.Inactive {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldStatus,
|
|
Msg: "must be 1 of [active, inactive]",
|
|
})
|
|
}
|
|
|
|
if len(vErrs) > 0 {
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, vErrs...),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type mapperTasks []*task
|
|
|
|
func (m mapperTasks) Association(i int) labelAssociater {
|
|
return m[i]
|
|
}
|
|
|
|
func (m mapperTasks) Len() int {
|
|
return len(m)
|
|
}
|
|
|
|
const (
|
|
fieldTelegrafConfig = "config"
|
|
)
|
|
|
|
type telegraf struct {
|
|
identity
|
|
|
|
config influxdb.TelegrafConfig
|
|
|
|
labels sortedLabels
|
|
|
|
existing *influxdb.TelegrafConfig
|
|
}
|
|
|
|
func (t *telegraf) ID() influxdb.ID {
|
|
if t.existing != nil {
|
|
return t.existing.ID
|
|
}
|
|
return t.config.ID
|
|
}
|
|
|
|
func (t *telegraf) Labels() []*label {
|
|
return t.labels
|
|
}
|
|
|
|
func (t *telegraf) ResourceType() influxdb.ResourceType {
|
|
return KindTelegraf.ResourceType()
|
|
}
|
|
|
|
func (t *telegraf) Exists() bool {
|
|
return t.existing != nil
|
|
}
|
|
|
|
func (t *telegraf) summarize() SummaryTelegraf {
|
|
cfg := t.config
|
|
cfg.Name = t.Name()
|
|
return SummaryTelegraf{
|
|
PkgName: t.PkgName(),
|
|
TelegrafConfig: cfg,
|
|
LabelAssociations: toSummaryLabels(t.labels...),
|
|
}
|
|
}
|
|
|
|
func (t *telegraf) valid() []validationErr {
|
|
var vErrs []validationErr
|
|
if t.config.Config == "" {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldTelegrafConfig,
|
|
Msg: "no config provided",
|
|
})
|
|
}
|
|
|
|
if len(vErrs) > 0 {
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, vErrs...),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type mapperTelegrafs []*telegraf
|
|
|
|
func (m mapperTelegrafs) Association(i int) labelAssociater {
|
|
return m[i]
|
|
}
|
|
|
|
func (m mapperTelegrafs) Len() int {
|
|
return len(m)
|
|
}
|
|
|
|
const (
|
|
fieldDashCharts = "charts"
|
|
)
|
|
|
|
const dashboardNameMinLength = 2
|
|
|
|
type dashboard struct {
|
|
identity
|
|
|
|
id influxdb.ID
|
|
OrgID influxdb.ID
|
|
Description string
|
|
Charts []chart
|
|
|
|
labels sortedLabels
|
|
|
|
existing *influxdb.Dashboard
|
|
}
|
|
|
|
func (d *dashboard) ID() influxdb.ID {
|
|
if d.existing != nil {
|
|
return d.existing.ID
|
|
}
|
|
return d.id
|
|
}
|
|
|
|
func (d *dashboard) Labels() []*label {
|
|
return d.labels
|
|
}
|
|
|
|
func (d *dashboard) ResourceType() influxdb.ResourceType {
|
|
return KindDashboard.ResourceType()
|
|
}
|
|
|
|
func (d *dashboard) Exists() bool {
|
|
return d.existing != nil
|
|
}
|
|
|
|
func (d *dashboard) summarize() SummaryDashboard {
|
|
iDash := SummaryDashboard{
|
|
ID: SafeID(d.ID()),
|
|
OrgID: SafeID(d.OrgID),
|
|
PkgName: d.PkgName(),
|
|
Name: d.Name(),
|
|
Description: d.Description,
|
|
LabelAssociations: toSummaryLabels(d.labels...),
|
|
}
|
|
for _, c := range d.Charts {
|
|
iDash.Charts = append(iDash.Charts, SummaryChart{
|
|
Properties: c.properties(),
|
|
Height: c.Height,
|
|
Width: c.Width,
|
|
XPosition: c.XPos,
|
|
YPosition: c.YPos,
|
|
})
|
|
}
|
|
return iDash
|
|
}
|
|
|
|
func (d *dashboard) valid() []validationErr {
|
|
var vErrs []validationErr
|
|
if err, ok := isValidName(d.Name(), dashboardNameMinLength); !ok {
|
|
vErrs = append(vErrs, err)
|
|
}
|
|
if len(vErrs) == 0 {
|
|
return nil
|
|
}
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, vErrs...),
|
|
}
|
|
}
|
|
|
|
type mapperDashboards []*dashboard
|
|
|
|
func (m mapperDashboards) Association(i int) labelAssociater {
|
|
return m[i]
|
|
}
|
|
|
|
func (m mapperDashboards) Len() int {
|
|
return len(m)
|
|
}
|
|
|
|
const (
|
|
fieldChartAxes = "axes"
|
|
fieldChartBinCount = "binCount"
|
|
fieldChartBinSize = "binSize"
|
|
fieldChartColors = "colors"
|
|
fieldChartDecimalPlaces = "decimalPlaces"
|
|
fieldChartDomain = "domain"
|
|
fieldChartGeom = "geom"
|
|
fieldChartHeight = "height"
|
|
fieldChartLegend = "legend"
|
|
fieldChartNote = "note"
|
|
fieldChartNoteOnEmpty = "noteOnEmpty"
|
|
fieldChartPosition = "position"
|
|
fieldChartQueries = "queries"
|
|
fieldChartShade = "shade"
|
|
fieldChartFieldOptions = "fieldOptions"
|
|
fieldChartTableOptions = "tableOptions"
|
|
fieldChartTickPrefix = "tickPrefix"
|
|
fieldChartTickSuffix = "tickSuffix"
|
|
fieldChartTimeFormat = "timeFormat"
|
|
fieldChartWidth = "width"
|
|
fieldChartXCol = "xCol"
|
|
fieldChartXPos = "xPos"
|
|
fieldChartYCol = "yCol"
|
|
fieldChartYPos = "yPos"
|
|
)
|
|
|
|
type chart struct {
|
|
Kind chartKind
|
|
Name string
|
|
Prefix string
|
|
TickPrefix string
|
|
Suffix string
|
|
TickSuffix string
|
|
Note string
|
|
NoteOnEmpty bool
|
|
DecimalPlaces int
|
|
EnforceDecimals bool
|
|
Shade bool
|
|
Legend legend
|
|
Colors colors
|
|
Queries queries
|
|
Axes axes
|
|
Geom string
|
|
XCol, YCol string
|
|
XPos, YPos int
|
|
Height, Width int
|
|
BinSize int
|
|
BinCount int
|
|
Position string
|
|
FieldOptions []fieldOption
|
|
TableOptions tableOptions
|
|
TimeFormat string
|
|
}
|
|
|
|
func (c chart) properties() influxdb.ViewProperties {
|
|
switch c.Kind {
|
|
case chartKindGauge:
|
|
return influxdb.GaugeViewProperties{
|
|
Type: influxdb.ViewPropertyTypeGauge,
|
|
Queries: c.Queries.influxDashQueries(),
|
|
Prefix: c.Prefix,
|
|
TickPrefix: c.TickPrefix,
|
|
Suffix: c.Suffix,
|
|
TickSuffix: c.TickSuffix,
|
|
ViewColors: c.Colors.influxViewColors(),
|
|
DecimalPlaces: influxdb.DecimalPlaces{
|
|
IsEnforced: c.EnforceDecimals,
|
|
Digits: int32(c.DecimalPlaces),
|
|
},
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
}
|
|
case chartKindHeatMap:
|
|
return influxdb.HeatmapViewProperties{
|
|
Type: influxdb.ViewPropertyTypeHeatMap,
|
|
Queries: c.Queries.influxDashQueries(),
|
|
ViewColors: c.Colors.strings(),
|
|
BinSize: int32(c.BinSize),
|
|
XColumn: c.XCol,
|
|
YColumn: c.YCol,
|
|
XDomain: c.Axes.get("x").Domain,
|
|
YDomain: c.Axes.get("y").Domain,
|
|
XPrefix: c.Axes.get("x").Prefix,
|
|
YPrefix: c.Axes.get("y").Prefix,
|
|
XSuffix: c.Axes.get("x").Suffix,
|
|
YSuffix: c.Axes.get("y").Suffix,
|
|
XAxisLabel: c.Axes.get("x").Label,
|
|
YAxisLabel: c.Axes.get("y").Label,
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
TimeFormat: c.TimeFormat,
|
|
}
|
|
case chartKindHistogram:
|
|
return influxdb.HistogramViewProperties{
|
|
Type: influxdb.ViewPropertyTypeHistogram,
|
|
Queries: c.Queries.influxDashQueries(),
|
|
ViewColors: c.Colors.influxViewColors(),
|
|
FillColumns: []string{},
|
|
XColumn: c.XCol,
|
|
XDomain: c.Axes.get("x").Domain,
|
|
XAxisLabel: c.Axes.get("x").Label,
|
|
Position: c.Position,
|
|
BinCount: c.BinCount,
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
}
|
|
case chartKindMarkdown:
|
|
return influxdb.MarkdownViewProperties{
|
|
Type: influxdb.ViewPropertyTypeMarkdown,
|
|
Note: c.Note,
|
|
}
|
|
case chartKindScatter:
|
|
return influxdb.ScatterViewProperties{
|
|
Type: influxdb.ViewPropertyTypeScatter,
|
|
Queries: c.Queries.influxDashQueries(),
|
|
ViewColors: c.Colors.strings(),
|
|
XColumn: c.XCol,
|
|
YColumn: c.YCol,
|
|
XDomain: c.Axes.get("x").Domain,
|
|
YDomain: c.Axes.get("y").Domain,
|
|
XPrefix: c.Axes.get("x").Prefix,
|
|
YPrefix: c.Axes.get("y").Prefix,
|
|
XSuffix: c.Axes.get("x").Suffix,
|
|
YSuffix: c.Axes.get("y").Suffix,
|
|
XAxisLabel: c.Axes.get("x").Label,
|
|
YAxisLabel: c.Axes.get("y").Label,
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
TimeFormat: c.TimeFormat,
|
|
}
|
|
case chartKindSingleStat:
|
|
return influxdb.SingleStatViewProperties{
|
|
Type: influxdb.ViewPropertyTypeSingleStat,
|
|
Prefix: c.Prefix,
|
|
TickPrefix: c.TickPrefix,
|
|
Suffix: c.Suffix,
|
|
TickSuffix: c.TickSuffix,
|
|
DecimalPlaces: influxdb.DecimalPlaces{
|
|
IsEnforced: c.EnforceDecimals,
|
|
Digits: int32(c.DecimalPlaces),
|
|
},
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
Queries: c.Queries.influxDashQueries(),
|
|
ViewColors: c.Colors.influxViewColors(),
|
|
}
|
|
case chartKindSingleStatPlusLine:
|
|
return influxdb.LinePlusSingleStatProperties{
|
|
Type: influxdb.ViewPropertyTypeSingleStatPlusLine,
|
|
Prefix: c.Prefix,
|
|
Suffix: c.Suffix,
|
|
DecimalPlaces: influxdb.DecimalPlaces{
|
|
IsEnforced: c.EnforceDecimals,
|
|
Digits: int32(c.DecimalPlaces),
|
|
},
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
XColumn: c.XCol,
|
|
YColumn: c.YCol,
|
|
ShadeBelow: c.Shade,
|
|
Legend: c.Legend.influxLegend(),
|
|
Queries: c.Queries.influxDashQueries(),
|
|
ViewColors: c.Colors.influxViewColors(),
|
|
Axes: c.Axes.influxAxes(),
|
|
Position: c.Position,
|
|
}
|
|
case chartKindTable:
|
|
fieldOptions := make([]influxdb.RenamableField, 0, len(c.FieldOptions))
|
|
for _, fieldOpt := range c.FieldOptions {
|
|
fieldOptions = append(fieldOptions, influxdb.RenamableField{
|
|
InternalName: fieldOpt.FieldName,
|
|
DisplayName: fieldOpt.DisplayName,
|
|
Visible: fieldOpt.Visible,
|
|
})
|
|
}
|
|
|
|
return influxdb.TableViewProperties{
|
|
Type: influxdb.ViewPropertyTypeTable,
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
DecimalPlaces: influxdb.DecimalPlaces{
|
|
IsEnforced: c.EnforceDecimals,
|
|
Digits: int32(c.DecimalPlaces),
|
|
},
|
|
Queries: c.Queries.influxDashQueries(),
|
|
ViewColors: c.Colors.influxViewColors(),
|
|
TableOptions: influxdb.TableOptions{
|
|
VerticalTimeAxis: c.TableOptions.VerticalTimeAxis,
|
|
SortBy: influxdb.RenamableField{
|
|
InternalName: c.TableOptions.SortByField,
|
|
},
|
|
Wrapping: c.TableOptions.Wrapping,
|
|
FixFirstColumn: c.TableOptions.FixFirstColumn,
|
|
},
|
|
FieldOptions: fieldOptions,
|
|
TimeFormat: c.TimeFormat,
|
|
}
|
|
case chartKindXY:
|
|
return influxdb.XYViewProperties{
|
|
Type: influxdb.ViewPropertyTypeXY,
|
|
Note: c.Note,
|
|
ShowNoteWhenEmpty: c.NoteOnEmpty,
|
|
XColumn: c.XCol,
|
|
YColumn: c.YCol,
|
|
ShadeBelow: c.Shade,
|
|
Legend: c.Legend.influxLegend(),
|
|
Queries: c.Queries.influxDashQueries(),
|
|
ViewColors: c.Colors.influxViewColors(),
|
|
Axes: c.Axes.influxAxes(),
|
|
Geom: c.Geom,
|
|
Position: c.Position,
|
|
TimeFormat: c.TimeFormat,
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (c chart) validProperties() []validationErr {
|
|
if c.Kind == chartKindMarkdown {
|
|
// at the time of writing, there's nothing to validate for markdown types
|
|
return nil
|
|
}
|
|
|
|
var fails []validationErr
|
|
|
|
validatorFns := []func() []validationErr{
|
|
c.validBaseProps,
|
|
c.Queries.valid,
|
|
c.Colors.valid,
|
|
}
|
|
for _, validatorFn := range validatorFns {
|
|
fails = append(fails, validatorFn()...)
|
|
}
|
|
|
|
// chart kind specific validations
|
|
switch c.Kind {
|
|
case chartKindGauge:
|
|
fails = append(fails, c.Colors.hasTypes(colorTypeMin, colorTypeMax)...)
|
|
case chartKindHeatMap:
|
|
fails = append(fails, c.Axes.hasAxes("x", "y")...)
|
|
case chartKindHistogram:
|
|
fails = append(fails, c.Axes.hasAxes("x")...)
|
|
case chartKindScatter:
|
|
fails = append(fails, c.Axes.hasAxes("x", "y")...)
|
|
case chartKindSingleStat:
|
|
case chartKindSingleStatPlusLine:
|
|
fails = append(fails, c.Axes.hasAxes("x", "y")...)
|
|
fails = append(fails, validPosition(c.Position)...)
|
|
case chartKindTable:
|
|
fails = append(fails, validTableOptions(c.TableOptions)...)
|
|
case chartKindXY:
|
|
fails = append(fails, validGeometry(c.Geom)...)
|
|
fails = append(fails, c.Axes.hasAxes("x", "y")...)
|
|
fails = append(fails, validPosition(c.Position)...)
|
|
}
|
|
|
|
return fails
|
|
}
|
|
|
|
func validPosition(pos string) []validationErr {
|
|
pos = strings.ToLower(pos)
|
|
if pos != "" && pos != "overlaid" && pos != "stacked" {
|
|
return []validationErr{{
|
|
Field: fieldChartPosition,
|
|
Msg: fmt.Sprintf("invalid position supplied %q; valid positions is one of [overlaid, stacked]", pos),
|
|
}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var geometryTypes = map[string]bool{
|
|
"line": true,
|
|
"step": true,
|
|
"stacked": true,
|
|
"monotoneX": true,
|
|
"bar": true,
|
|
}
|
|
|
|
func validGeometry(geom string) []validationErr {
|
|
if !geometryTypes[geom] {
|
|
msg := "type not found"
|
|
if geom != "" {
|
|
msg = "type provided is not supported"
|
|
}
|
|
return []validationErr{{
|
|
Field: fieldChartGeom,
|
|
Msg: fmt.Sprintf("%s: %q", msg, geom),
|
|
}}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c chart) validBaseProps() []validationErr {
|
|
var fails []validationErr
|
|
if c.Width <= 0 {
|
|
fails = append(fails, validationErr{
|
|
Field: fieldChartWidth,
|
|
Msg: "must be greater than 0",
|
|
})
|
|
}
|
|
|
|
if c.Height <= 0 {
|
|
fails = append(fails, validationErr{
|
|
Field: fieldChartHeight,
|
|
Msg: "must be greater than 0",
|
|
})
|
|
}
|
|
return fails
|
|
}
|
|
|
|
const (
|
|
fieldChartFieldOptionDisplayName = "displayName"
|
|
fieldChartFieldOptionFieldName = "fieldName"
|
|
fieldChartFieldOptionVisible = "visible"
|
|
)
|
|
|
|
type fieldOption struct {
|
|
FieldName string
|
|
DisplayName string
|
|
Visible bool
|
|
}
|
|
|
|
const (
|
|
fieldChartTableOptionVerticalTimeAxis = "verticalTimeAxis"
|
|
fieldChartTableOptionSortBy = "sortBy"
|
|
fieldChartTableOptionWrapping = "wrapping"
|
|
fieldChartTableOptionFixFirstColumn = "fixFirstColumn"
|
|
)
|
|
|
|
type tableOptions struct {
|
|
VerticalTimeAxis bool
|
|
SortByField string
|
|
Wrapping string
|
|
FixFirstColumn bool
|
|
}
|
|
|
|
func validTableOptions(opts tableOptions) []validationErr {
|
|
var fails []validationErr
|
|
|
|
switch opts.Wrapping {
|
|
case "", "single-line", "truncate", "wrap":
|
|
default:
|
|
fails = append(fails, validationErr{
|
|
Field: fieldChartTableOptionWrapping,
|
|
Msg: `chart table option should 1 in ["single-line", "truncate", "wrap"]`,
|
|
})
|
|
}
|
|
|
|
if len(fails) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return []validationErr{
|
|
{
|
|
Field: fieldChartTableOptions,
|
|
Nested: fails,
|
|
},
|
|
}
|
|
}
|
|
|
|
const (
|
|
colorTypeBackground = "background"
|
|
colorTypeMin = "min"
|
|
colorTypeMax = "max"
|
|
colorTypeScale = "scale"
|
|
colorTypeText = "text"
|
|
colorTypeThreshold = "threshold"
|
|
)
|
|
|
|
const (
|
|
fieldColorHex = "hex"
|
|
)
|
|
|
|
type color struct {
|
|
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
|
Type string `json:"type,omitempty" yaml:"type,omitempty"`
|
|
Hex string `json:"hex,omitempty" yaml:"hex,omitempty"`
|
|
// using reference for Value here so we can set to nil and
|
|
// it will be ignored during encoding, keeps our exported pkgs
|
|
// clear of unneeded entries.
|
|
Value *float64 `json:"value,omitempty" yaml:"value,omitempty"`
|
|
}
|
|
|
|
// TODO:
|
|
// - verify templates are desired
|
|
// - template colors so references can be shared
|
|
type colors []*color
|
|
|
|
func (c colors) influxViewColors() []influxdb.ViewColor {
|
|
ptrToFloat64 := func(f *float64) float64 {
|
|
if f == nil {
|
|
return 0
|
|
}
|
|
return *f
|
|
}
|
|
|
|
var iColors []influxdb.ViewColor
|
|
for _, cc := range c {
|
|
iColors = append(iColors, influxdb.ViewColor{
|
|
Type: cc.Type,
|
|
Hex: cc.Hex,
|
|
Name: cc.Name,
|
|
Value: ptrToFloat64(cc.Value),
|
|
})
|
|
}
|
|
return iColors
|
|
}
|
|
|
|
func (c colors) strings() []string {
|
|
clrs := []string{}
|
|
|
|
for _, clr := range c {
|
|
clrs = append(clrs, clr.Hex)
|
|
}
|
|
|
|
return clrs
|
|
}
|
|
|
|
// TODO: looks like much of these are actually getting defaults in
|
|
// the UI. looking at sytem charts, seeign lots of failures for missing
|
|
// color types or no colors at all.
|
|
func (c colors) hasTypes(types ...string) []validationErr {
|
|
tMap := make(map[string]bool)
|
|
for _, cc := range c {
|
|
tMap[cc.Type] = true
|
|
}
|
|
|
|
var failures []validationErr
|
|
for _, t := range types {
|
|
if !tMap[t] {
|
|
failures = append(failures, validationErr{
|
|
Field: "colors",
|
|
Msg: fmt.Sprintf("type not found: %q", t),
|
|
})
|
|
}
|
|
}
|
|
|
|
return failures
|
|
}
|
|
|
|
func (c colors) valid() []validationErr {
|
|
var fails []validationErr
|
|
for i, cc := range c {
|
|
cErr := validationErr{
|
|
Field: fieldChartColors,
|
|
Index: intPtr(i),
|
|
}
|
|
if cc.Hex == "" {
|
|
cErr.Nested = append(cErr.Nested, validationErr{
|
|
Field: fieldColorHex,
|
|
Msg: "a color must have a hex value provided",
|
|
})
|
|
}
|
|
if len(cErr.Nested) > 0 {
|
|
fails = append(fails, cErr)
|
|
}
|
|
}
|
|
|
|
return fails
|
|
}
|
|
|
|
type query struct {
|
|
Query string `json:"query" yaml:"query"`
|
|
}
|
|
|
|
type queries []query
|
|
|
|
func (q queries) influxDashQueries() []influxdb.DashboardQuery {
|
|
var iQueries []influxdb.DashboardQuery
|
|
for _, qq := range q {
|
|
newQuery := influxdb.DashboardQuery{
|
|
Text: qq.Query,
|
|
EditMode: "advanced",
|
|
}
|
|
// TODO: axe this builder configs when issue https://github.com/influxdata/influxdb/issues/15708 is fixed up
|
|
newQuery.BuilderConfig.Tags = append(newQuery.BuilderConfig.Tags, influxdb.NewBuilderTag("_measurement", "filter", ""))
|
|
iQueries = append(iQueries, newQuery)
|
|
}
|
|
return iQueries
|
|
}
|
|
|
|
func (q queries) valid() []validationErr {
|
|
var fails []validationErr
|
|
if len(q) == 0 {
|
|
fails = append(fails, validationErr{
|
|
Field: fieldChartQueries,
|
|
Msg: "at least 1 query must be provided",
|
|
})
|
|
}
|
|
|
|
for i, qq := range q {
|
|
qErr := validationErr{
|
|
Field: fieldChartQueries,
|
|
Index: intPtr(i),
|
|
}
|
|
if qq.Query == "" {
|
|
qErr.Nested = append(fails, validationErr{
|
|
Field: fieldQuery,
|
|
Msg: "a query must be provided",
|
|
})
|
|
}
|
|
if len(qErr.Nested) > 0 {
|
|
fails = append(fails, qErr)
|
|
}
|
|
}
|
|
|
|
return fails
|
|
}
|
|
|
|
const (
|
|
fieldAxisBase = "base"
|
|
fieldAxisLabel = "label"
|
|
fieldAxisScale = "scale"
|
|
)
|
|
|
|
type axis struct {
|
|
Base string `json:"base,omitempty" yaml:"base,omitempty"`
|
|
Label string `json:"label,omitempty" yaml:"label,omitempty"`
|
|
Name string `json:"name,omitempty" yaml:"name,omitempty"`
|
|
Prefix string `json:"prefix,omitempty" yaml:"prefix,omitempty"`
|
|
Scale string `json:"scale,omitempty" yaml:"scale,omitempty"`
|
|
Suffix string `json:"suffix,omitempty" yaml:"suffix,omitempty"`
|
|
Domain []float64 `json:"domain,omitempty" yaml:"domain,omitempty"`
|
|
}
|
|
|
|
type axes []axis
|
|
|
|
func (a axes) get(name string) axis {
|
|
for _, ax := range a {
|
|
if name == ax.Name {
|
|
return ax
|
|
}
|
|
}
|
|
return axis{}
|
|
}
|
|
|
|
func (a axes) influxAxes() map[string]influxdb.Axis {
|
|
m := make(map[string]influxdb.Axis)
|
|
for _, ax := range a {
|
|
m[ax.Name] = influxdb.Axis{
|
|
Bounds: []string{},
|
|
Label: ax.Label,
|
|
Prefix: ax.Prefix,
|
|
Suffix: ax.Suffix,
|
|
Base: ax.Base,
|
|
Scale: ax.Scale,
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (a axes) hasAxes(expectedAxes ...string) []validationErr {
|
|
mAxes := make(map[string]bool)
|
|
for _, ax := range a {
|
|
mAxes[ax.Name] = true
|
|
}
|
|
|
|
var failures []validationErr
|
|
for _, expected := range expectedAxes {
|
|
if !mAxes[expected] {
|
|
failures = append(failures, validationErr{
|
|
Field: fieldChartAxes,
|
|
Msg: fmt.Sprintf("axis not found: %q", expected),
|
|
})
|
|
}
|
|
}
|
|
|
|
return failures
|
|
}
|
|
|
|
const (
|
|
fieldLegendOrientation = "orientation"
|
|
)
|
|
|
|
type legend struct {
|
|
Orientation string `json:"orientation,omitempty" yaml:"orientation,omitempty"`
|
|
Type string `json:"type" yaml:"type"`
|
|
}
|
|
|
|
func (l legend) influxLegend() influxdb.Legend {
|
|
return influxdb.Legend{
|
|
Type: l.Type,
|
|
Orientation: l.Orientation,
|
|
}
|
|
}
|
|
|
|
const (
|
|
fieldReferencesEnv = "envRef"
|
|
fieldReferencesSecret = "secretRef"
|
|
)
|
|
|
|
type references struct {
|
|
val interface{}
|
|
EnvRef string
|
|
Secret string
|
|
}
|
|
|
|
func (r *references) hasValue() bool {
|
|
return r.EnvRef != "" || r.Secret != "" || r.val != nil
|
|
}
|
|
|
|
func (r *references) String() string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
if v := r.StringVal(); v != "" {
|
|
return v
|
|
}
|
|
if r.EnvRef != "" {
|
|
return "$" + r.EnvRef
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (r *references) StringVal() string {
|
|
if r.val != nil {
|
|
s, _ := r.val.(string)
|
|
return s
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (r *references) SecretField() influxdb.SecretField {
|
|
if secret := r.Secret; secret != "" {
|
|
return influxdb.SecretField{Key: secret}
|
|
}
|
|
if str := r.StringVal(); str != "" {
|
|
return influxdb.SecretField{Value: &str}
|
|
}
|
|
return influxdb.SecretField{}
|
|
}
|
|
|
|
func isValidName(name string, minLength int) (validationErr, bool) {
|
|
if len(name) >= minLength {
|
|
return validationErr{}, true
|
|
}
|
|
return validationErr{
|
|
Field: fieldName,
|
|
Msg: fmt.Sprintf("must be a string of at least %d chars in length", minLength),
|
|
}, false
|
|
}
|
|
|
|
func toNotificationDuration(dur time.Duration) *notification.Duration {
|
|
d, _ := notification.FromTimeDuration(dur)
|
|
return &d
|
|
}
|
|
|
|
func durToStr(dur time.Duration) string {
|
|
if dur == 0 {
|
|
return ""
|
|
}
|
|
return dur.String()
|
|
}
|
|
|
|
func flt64Ptr(f float64) *float64 {
|
|
if f != 0 {
|
|
return &f
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func intPtr(i int) *int {
|
|
return &i
|
|
}
|