574 lines
12 KiB
Go
574 lines
12 KiB
Go
package pkger
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/influxdata/influxdb/v2"
|
|
"github.com/influxdata/influxdb/v2/notification"
|
|
icheck "github.com/influxdata/influxdb/v2/notification/check"
|
|
)
|
|
|
|
type identity struct {
|
|
name *references
|
|
displayName *references
|
|
shouldRemove bool
|
|
}
|
|
|
|
func (i *identity) Name() string {
|
|
if displayName := i.displayName.String(); displayName != "" {
|
|
return displayName
|
|
}
|
|
return i.name.String()
|
|
}
|
|
|
|
func (i *identity) PkgName() string {
|
|
return i.name.String()
|
|
}
|
|
|
|
const (
|
|
fieldAPIVersion = "apiVersion"
|
|
fieldAssociations = "associations"
|
|
fieldDescription = "description"
|
|
fieldEvery = "every"
|
|
fieldKey = "key"
|
|
fieldKind = "kind"
|
|
fieldLanguage = "language"
|
|
fieldLevel = "level"
|
|
fieldMin = "min"
|
|
fieldMax = "max"
|
|
fieldMetadata = "metadata"
|
|
fieldName = "name"
|
|
fieldOffset = "offset"
|
|
fieldOperator = "operator"
|
|
fieldPrefix = "prefix"
|
|
fieldQuery = "query"
|
|
fieldSuffix = "suffix"
|
|
fieldSpec = "spec"
|
|
fieldStatus = "status"
|
|
fieldType = "type"
|
|
fieldValue = "value"
|
|
fieldValues = "values"
|
|
)
|
|
|
|
const (
|
|
fieldBucketRetentionRules = "retentionRules"
|
|
)
|
|
|
|
const bucketNameMinLength = 2
|
|
|
|
type bucket struct {
|
|
identity
|
|
|
|
Description string
|
|
RetentionRules retentionRules
|
|
labels sortedLabels
|
|
}
|
|
|
|
func (b *bucket) summarize() SummaryBucket {
|
|
return SummaryBucket{
|
|
Name: b.Name(),
|
|
PkgName: b.PkgName(),
|
|
Description: b.Description,
|
|
RetentionPeriod: b.RetentionRules.RP(),
|
|
LabelAssociations: toSummaryLabels(b.labels...),
|
|
}
|
|
}
|
|
|
|
func (b *bucket) ResourceType() influxdb.ResourceType {
|
|
return KindBucket.ResourceType()
|
|
}
|
|
|
|
func (b *bucket) valid() []validationErr {
|
|
var vErrs []validationErr
|
|
if err, ok := isValidName(b.Name(), bucketNameMinLength); !ok {
|
|
vErrs = append(vErrs, err)
|
|
}
|
|
vErrs = append(vErrs, b.RetentionRules.valid()...)
|
|
if len(vErrs) == 0 {
|
|
return nil
|
|
}
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, vErrs...),
|
|
}
|
|
}
|
|
|
|
const (
|
|
retentionRuleTypeExpire = "expire"
|
|
)
|
|
|
|
type retentionRule struct {
|
|
Type string `json:"type" yaml:"type"`
|
|
Seconds int `json:"everySeconds" yaml:"everySeconds"`
|
|
}
|
|
|
|
func newRetentionRule(d time.Duration) retentionRule {
|
|
return retentionRule{
|
|
Type: retentionRuleTypeExpire,
|
|
Seconds: int(d.Round(time.Second) / time.Second),
|
|
}
|
|
}
|
|
|
|
func (r retentionRule) valid() []validationErr {
|
|
const hour = 3600
|
|
var ff []validationErr
|
|
if r.Seconds < hour {
|
|
ff = append(ff, validationErr{
|
|
Field: fieldRetentionRulesEverySeconds,
|
|
Msg: "seconds must be a minimum of " + strconv.Itoa(hour),
|
|
})
|
|
}
|
|
if r.Type != retentionRuleTypeExpire {
|
|
ff = append(ff, validationErr{
|
|
Field: fieldType,
|
|
Msg: `type must be "expire"`,
|
|
})
|
|
}
|
|
return ff
|
|
}
|
|
|
|
const (
|
|
fieldRetentionRulesEverySeconds = "everySeconds"
|
|
)
|
|
|
|
type retentionRules []retentionRule
|
|
|
|
func (r retentionRules) RP() time.Duration {
|
|
// TODO: this feels very odd to me, will need to follow up with
|
|
// team to better understand this
|
|
for _, rule := range r {
|
|
return time.Duration(rule.Seconds) * time.Second
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (r retentionRules) valid() []validationErr {
|
|
var failures []validationErr
|
|
for i, rule := range r {
|
|
if ff := rule.valid(); len(ff) > 0 {
|
|
failures = append(failures, validationErr{
|
|
Field: fieldBucketRetentionRules,
|
|
Index: intPtr(i),
|
|
Nested: ff,
|
|
})
|
|
}
|
|
}
|
|
return failures
|
|
}
|
|
|
|
type checkKind int
|
|
|
|
const (
|
|
checkKindDeadman checkKind = iota + 1
|
|
checkKindThreshold
|
|
)
|
|
|
|
const (
|
|
fieldCheckAllValues = "allValues"
|
|
fieldCheckReportZero = "reportZero"
|
|
fieldCheckStaleTime = "staleTime"
|
|
fieldCheckStatusMessageTemplate = "statusMessageTemplate"
|
|
fieldCheckTags = "tags"
|
|
fieldCheckThresholds = "thresholds"
|
|
fieldCheckTimeSince = "timeSince"
|
|
)
|
|
|
|
const checkNameMinLength = 1
|
|
|
|
type check struct {
|
|
identity
|
|
|
|
kind checkKind
|
|
description string
|
|
every time.Duration
|
|
level string
|
|
offset time.Duration
|
|
query string
|
|
reportZero bool
|
|
staleTime time.Duration
|
|
status string
|
|
statusMessage string
|
|
tags []struct{ k, v string }
|
|
timeSince time.Duration
|
|
thresholds []threshold
|
|
|
|
labels sortedLabels
|
|
}
|
|
|
|
func (c *check) Labels() []*label {
|
|
return c.labels
|
|
}
|
|
|
|
func (c *check) ResourceType() influxdb.ResourceType {
|
|
return KindCheck.ResourceType()
|
|
}
|
|
|
|
func (c *check) Status() influxdb.Status {
|
|
status := influxdb.Status(c.status)
|
|
if status == "" {
|
|
status = influxdb.Active
|
|
}
|
|
return status
|
|
}
|
|
|
|
func (c *check) summarize() SummaryCheck {
|
|
base := icheck.Base{
|
|
Name: c.Name(),
|
|
Description: c.description,
|
|
Every: toNotificationDuration(c.every),
|
|
Offset: toNotificationDuration(c.offset),
|
|
StatusMessageTemplate: c.statusMessage,
|
|
}
|
|
base.Query.Text = c.query
|
|
for _, tag := range c.tags {
|
|
base.Tags = append(base.Tags, influxdb.Tag{Key: tag.k, Value: tag.v})
|
|
}
|
|
|
|
sum := SummaryCheck{
|
|
PkgName: c.PkgName(),
|
|
Status: c.Status(),
|
|
LabelAssociations: toSummaryLabels(c.labels...),
|
|
}
|
|
switch c.kind {
|
|
case checkKindThreshold:
|
|
sum.Check = &icheck.Threshold{
|
|
Base: base,
|
|
Thresholds: toInfluxThresholds(c.thresholds...),
|
|
}
|
|
case checkKindDeadman:
|
|
sum.Check = &icheck.Deadman{
|
|
Base: base,
|
|
Level: notification.ParseCheckLevel(strings.ToUpper(c.level)),
|
|
ReportZero: c.reportZero,
|
|
StaleTime: toNotificationDuration(c.staleTime),
|
|
TimeSince: toNotificationDuration(c.timeSince),
|
|
}
|
|
}
|
|
return sum
|
|
}
|
|
|
|
func (c *check) valid() []validationErr {
|
|
var vErrs []validationErr
|
|
if err, ok := isValidName(c.Name(), checkNameMinLength); !ok {
|
|
vErrs = append(vErrs, err)
|
|
}
|
|
if c.every == 0 {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldEvery,
|
|
Msg: "duration value must be provided that is >= 5s (seconds)",
|
|
})
|
|
}
|
|
if c.query == "" {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldQuery,
|
|
Msg: "must provide a non zero value",
|
|
})
|
|
}
|
|
if c.statusMessage == "" {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldCheckStatusMessageTemplate,
|
|
Msg: `must provide a template; ex. "Check: ${ r._check_name } is: ${ r._level }"`,
|
|
})
|
|
}
|
|
if status := c.Status(); status != influxdb.Active && status != influxdb.Inactive {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldStatus,
|
|
Msg: "must be 1 of [active, inactive]",
|
|
})
|
|
}
|
|
|
|
switch c.kind {
|
|
case checkKindThreshold:
|
|
if len(c.thresholds) == 0 {
|
|
vErrs = append(vErrs, validationErr{
|
|
Field: fieldCheckThresholds,
|
|
Msg: "must provide at least 1 threshold entry",
|
|
})
|
|
}
|
|
for i, th := range c.thresholds {
|
|
for _, fail := range th.valid() {
|
|
fail.Index = intPtr(i)
|
|
vErrs = append(vErrs, fail)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(vErrs) > 0 {
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, vErrs...),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type assocMapKey struct {
|
|
resType influxdb.ResourceType
|
|
name string
|
|
}
|
|
|
|
type assocMapVal struct {
|
|
exists bool
|
|
v interface{}
|
|
}
|
|
|
|
func (l assocMapVal) ID() influxdb.ID {
|
|
if t, ok := l.v.(labelAssociater); ok {
|
|
return t.ID()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (l assocMapVal) PkgName() string {
|
|
t, ok := l.v.(interface{ PkgName() string })
|
|
if ok {
|
|
return t.PkgName()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type associationMapping struct {
|
|
mappings map[assocMapKey][]assocMapVal
|
|
}
|
|
|
|
func (l *associationMapping) setMapping(v interface {
|
|
ResourceType() influxdb.ResourceType
|
|
Name() string
|
|
}, exists bool) {
|
|
if l == nil {
|
|
return
|
|
}
|
|
if l.mappings == nil {
|
|
l.mappings = make(map[assocMapKey][]assocMapVal)
|
|
}
|
|
|
|
k := assocMapKey{
|
|
resType: v.ResourceType(),
|
|
name: v.Name(),
|
|
}
|
|
val := assocMapVal{
|
|
exists: exists,
|
|
v: v,
|
|
}
|
|
existing, ok := l.mappings[k]
|
|
if !ok {
|
|
l.mappings[k] = []assocMapVal{val}
|
|
return
|
|
}
|
|
for i, ex := range existing {
|
|
if ex.v == v {
|
|
existing[i].exists = exists
|
|
return
|
|
}
|
|
}
|
|
l.mappings[k] = append(l.mappings[k], val)
|
|
}
|
|
|
|
const (
|
|
fieldLabelColor = "color"
|
|
)
|
|
|
|
const labelNameMinLength = 2
|
|
|
|
type label struct {
|
|
id influxdb.ID
|
|
identity
|
|
|
|
Color string
|
|
Description string
|
|
associationMapping
|
|
|
|
// exists provides context for a resource that already
|
|
// exists in the platform. If a resource already exists(exists=true)
|
|
// then the ID should be populated.
|
|
existing *influxdb.Label
|
|
}
|
|
|
|
func (l *label) summarize() SummaryLabel {
|
|
return SummaryLabel{
|
|
PkgName: l.PkgName(),
|
|
Name: l.Name(),
|
|
Properties: struct {
|
|
Color string `json:"color"`
|
|
Description string `json:"description"`
|
|
}{
|
|
Color: l.Color,
|
|
Description: l.Description,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (l *label) mappingSummary() []SummaryLabelMapping {
|
|
var mappings []SummaryLabelMapping
|
|
for resource, vals := range l.mappings {
|
|
for _, v := range vals {
|
|
status := StateStatusNew
|
|
if v.exists {
|
|
status = StateStatusExists
|
|
}
|
|
mappings = append(mappings, SummaryLabelMapping{
|
|
exists: v.exists,
|
|
Status: status,
|
|
ResourceID: SafeID(v.ID()),
|
|
ResourcePkgName: v.PkgName(),
|
|
ResourceName: resource.name,
|
|
ResourceType: resource.resType,
|
|
LabelID: SafeID(l.ID()),
|
|
LabelPkgName: l.PkgName(),
|
|
LabelName: l.Name(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return mappings
|
|
}
|
|
|
|
func (l *label) ID() influxdb.ID {
|
|
if l.id != 0 {
|
|
return l.id
|
|
}
|
|
if l.existing != nil {
|
|
return l.existing.ID
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (l *label) valid() []validationErr {
|
|
var vErrs []validationErr
|
|
if err, ok := isValidName(l.Name(), labelNameMinLength); !ok {
|
|
vErrs = append(vErrs, err)
|
|
}
|
|
if len(vErrs) == 0 {
|
|
return nil
|
|
}
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, vErrs...),
|
|
}
|
|
}
|
|
|
|
func toSummaryLabels(labels ...*label) []SummaryLabel {
|
|
iLabels := make([]SummaryLabel, 0, len(labels))
|
|
for _, l := range labels {
|
|
iLabels = append(iLabels, l.summarize())
|
|
}
|
|
return iLabels
|
|
}
|
|
|
|
type sortedLabels []*label
|
|
|
|
func (s sortedLabels) Len() int {
|
|
return len(s)
|
|
}
|
|
|
|
func (s sortedLabels) Less(i, j int) bool {
|
|
return s[i].Name() < s[j].Name()
|
|
}
|
|
|
|
func (s sortedLabels) Swap(i, j int) {
|
|
s[i], s[j] = s[j], s[i]
|
|
}
|
|
|
|
const (
|
|
fieldArgTypeConstant = "constant"
|
|
fieldArgTypeMap = "map"
|
|
fieldArgTypeQuery = "query"
|
|
)
|
|
|
|
type variable struct {
|
|
identity
|
|
|
|
Description string
|
|
Type string
|
|
Query string
|
|
Language string
|
|
ConstValues []string
|
|
MapValues map[string]string
|
|
|
|
labels sortedLabels
|
|
}
|
|
|
|
func (v *variable) Labels() []*label {
|
|
return v.labels
|
|
}
|
|
|
|
func (v *variable) ResourceType() influxdb.ResourceType {
|
|
return KindVariable.ResourceType()
|
|
}
|
|
|
|
func (v *variable) summarize() SummaryVariable {
|
|
return SummaryVariable{
|
|
PkgName: v.PkgName(),
|
|
Name: v.Name(),
|
|
Description: v.Description,
|
|
Arguments: v.influxVarArgs(),
|
|
LabelAssociations: toSummaryLabels(v.labels...),
|
|
}
|
|
}
|
|
|
|
func (v *variable) influxVarArgs() *influxdb.VariableArguments {
|
|
// this zero value check is for situations where we want to marshal/unmarshal
|
|
// a variable and not have the invalid args blow up during unmarshaling. When
|
|
// that validation is decoupled from the unmarshaling, we can clean this up.
|
|
if v.Type == "" {
|
|
return nil
|
|
}
|
|
|
|
args := &influxdb.VariableArguments{
|
|
Type: v.Type,
|
|
}
|
|
switch args.Type {
|
|
case "query":
|
|
args.Values = influxdb.VariableQueryValues{
|
|
Query: v.Query,
|
|
Language: v.Language,
|
|
}
|
|
case "constant":
|
|
args.Values = influxdb.VariableConstantValues(v.ConstValues)
|
|
case "map":
|
|
args.Values = influxdb.VariableMapValues(v.MapValues)
|
|
}
|
|
return args
|
|
}
|
|
|
|
func (v *variable) valid() []validationErr {
|
|
var failures []validationErr
|
|
switch v.Type {
|
|
case "map":
|
|
if len(v.MapValues) == 0 {
|
|
failures = append(failures, validationErr{
|
|
Field: fieldValues,
|
|
Msg: "map variable must have at least 1 key/val pair",
|
|
})
|
|
}
|
|
case "constant":
|
|
if len(v.ConstValues) == 0 {
|
|
failures = append(failures, validationErr{
|
|
Field: fieldValues,
|
|
Msg: "constant variable must have a least 1 value provided",
|
|
})
|
|
}
|
|
case "query":
|
|
if v.Query == "" {
|
|
failures = append(failures, validationErr{
|
|
Field: fieldQuery,
|
|
Msg: "query variable must provide a query string",
|
|
})
|
|
}
|
|
if v.Language != "influxql" && v.Language != "flux" {
|
|
failures = append(failures, validationErr{
|
|
Field: fieldLanguage,
|
|
Msg: fmt.Sprintf(`query variable language must be either "influxql" or "flux"; got %q`, v.Language),
|
|
})
|
|
}
|
|
}
|
|
if len(failures) > 0 {
|
|
return []validationErr{
|
|
objectValidationErr(fieldSpec, failures...),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|