diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a8c4c3e469..92360411f53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 1. [16234](https://github.com/influxdata/influxdb/pull/16234): add support for notification endpoints to influx templates/pkgs. 2. [16242](https://github.com/influxdata/influxdb/pull/16242): drop id prefix for secret key requirement for notification endpoints +3. [16259](https://github.com/influxdata/influxdb/pull/16259): add support for check resource to pkger parser ### Bug Fixes diff --git a/pkger/models.go b/pkger/models.go index 52892c3e963..93a56be8671 100644 --- a/pkger/models.go +++ b/pkger/models.go @@ -11,6 +11,8 @@ import ( "time" "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb/notification" + icheck "github.com/influxdata/influxdb/notification/check" "github.com/influxdata/influxdb/notification/endpoint" ) @@ -18,6 +20,9 @@ import ( const ( KindUnknown Kind = "" KindBucket Kind = "bucket" + KindCheck Kind = "check" + KindCheckDeadman Kind = "check_deadman" + KindCheckThreshold Kind = "check_threshold" KindDashboard Kind = "dashboard" KindLabel Kind = "label" KindNotificationEndpoint Kind = "notification_endpoint" @@ -31,6 +36,8 @@ const ( var kinds = map[Kind]bool{ KindBucket: true, + KindCheckDeadman: true, + KindCheckThreshold: true, KindDashboard: true, KindLabel: true, KindNotificationEndpoint: true, @@ -78,6 +85,8 @@ 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: @@ -398,6 +407,7 @@ func (d DiffVariable) hasConflict() bool { // 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"` Labels []SummaryLabel `json:"labels"` @@ -417,6 +427,13 @@ type SummaryBucket struct { LabelAssociations []SummaryLabel `json:"labelAssociations"` } +// SummaryCheck provides a summary of a pkg check. +type SummaryCheck struct { + Check influxdb.Check `json:"check"` + Status influxdb.Status `json:"status"` + LabelAssociations []SummaryLabel `json:"labelAssociations"` +} + // SummaryDashboard provides a summary of a pkg dashboard. type SummaryDashboard struct { ID SafeID `json:"id"` @@ -577,6 +594,8 @@ const ( fieldKey = "key" fieldKind = "kind" fieldLanguage = "language" + fieldMin = "min" + fieldMax = "max" fieldName = "name" fieldPrefix = "prefix" fieldQuery = "query" @@ -723,6 +742,214 @@ func (r retentionRules) valid() []validationErr { return failures } +type checkKind int + +const ( + checkKindDeadman checkKind = iota + 1 + checkKindThreshold +) + +const ( + fieldCheckAllValues = "allValues" + fieldCheckEvery = "every" + fieldCheckLevel = "level" + fieldCheckOffset = "offset" + fieldCheckReportZero = "reportZero" + fieldCheckStaleTime = "staleTime" + fieldCheckStatusMessageTemplate = "statusMessageTemplate" + fieldCheckTags = "tags" + fieldCheckThresholds = "thresholds" + fieldCheckTimeSince = "timeSince" +) + +type check struct { + kind checkKind + name string + 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) Name() string { + return c.name +} + +func (c *check) ResourceType() influxdb.ResourceType { + return KindCheck.ResourceType() +} + +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}) + } + + status := influxdb.Status(c.status) + if status == "" { + status = influxdb.TaskStatusActive + } + + sum := SummaryCheck{ + Status: 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 c.every == 0 { + vErrs = append(vErrs, validationErr{ + Field: fieldCheckEvery, + 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.status != "" && !(c.status == influxdb.TaskStatusActive || c.status == influxdb.TaskStatusInactive) { + vErrs = append(vErrs, validationErr{ + Field: fieldStatus, + Msg: "must be 1 of [active, inactive]", + }) + } + if c.statusMessage == "" { + vErrs = append(vErrs, validationErr{ + Field: fieldCheckStatusMessageTemplate, + Msg: `must provide a template; ex. "Check: ${ r._check_name } is: ${ r._level }"`, + }) + } + 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) + } + } + } + return vErrs +} + +type thresholdType string + +const ( + thresholdTypeGreater thresholdType = "greater" + thresholdTypeLesser thresholdType = "lesser" + thresholdTypeInsideRange thresholdType = "inside_range" + thresholdTypeOutsideRange thresholdType = "outside_range" +) + +var thresholdTypes = map[thresholdType]bool{ + thresholdTypeGreater: true, + thresholdTypeLesser: true, + thresholdTypeInsideRange: true, + thresholdTypeOutsideRange: true, +} + +type threshold struct { + threshType thresholdType + allVals bool + level string + val float64 + min, max float64 +} + +func (t threshold) valid() []validationErr { + var vErrs []validationErr + if notification.ParseCheckLevel(t.level) == notification.Unknown { + vErrs = append(vErrs, validationErr{ + Field: fieldCheckLevel, + Msg: "must be 1 in [CRIT, WARN, INFO, OK]", + }) + } + if !thresholdTypes[t.threshType] { + vErrs = append(vErrs, validationErr{ + Field: fieldType, + Msg: "must be 1 in [Lesser, Greater, Inside_Range, Outside_Range]", + }) + } + if t.min > t.max { + vErrs = append(vErrs, validationErr{ + Field: fieldMin, + Msg: "min must be < max", + }) + } + return vErrs +} + +func toInfluxThresholds(thresholds ...threshold) []icheck.ThresholdConfig { + var iThresh []icheck.ThresholdConfig + for _, th := range thresholds { + base := icheck.ThresholdConfigBase{ + AllValues: th.allVals, + Level: notification.ParseCheckLevel(th.level), + } + switch th.threshType { + case thresholdTypeGreater: + iThresh = append(iThresh, icheck.Greater{ + ThresholdConfigBase: base, + Value: th.val, + }) + case thresholdTypeLesser: + iThresh = append(iThresh, icheck.Lesser{ + ThresholdConfigBase: base, + Value: th.val, + }) + case thresholdTypeInsideRange, thresholdTypeOutsideRange: + iThresh = append(iThresh, icheck.Range{ + ThresholdConfigBase: base, + Max: th.max, + Min: th.min, + Within: th.threshType == thresholdTypeInsideRange, + }) + } + } + return iThresh +} + type assocMapKey struct { resType influxdb.ResourceType name string @@ -763,17 +990,18 @@ func (l *associationMapping) setMapping(v interface { exists: exists, v: v, } - if existing, ok := l.mappings[k]; ok { - for i, ex := range existing { - if ex.v == v { - existing[i].exists = exists - return - } - } - l.mappings[k] = append(l.mappings[k], val) + existing, ok := l.mappings[k] + if !ok { + l.mappings[k] = []assocMapVal{val} return } - l.mappings[k] = []assocMapVal{val} + for i, ex := range existing { + if ex.v == v { + existing[i].exists = exists + return + } + } + l.mappings[k] = append(l.mappings[k], val) } const ( @@ -1862,6 +2090,11 @@ func (r references) SecretField() influxdb.SecretField { return influxdb.SecretField{} } +func toNotificationDuration(dur time.Duration) *notification.Duration { + d, _ := notification.FromTimeDuration(dur) + return &d +} + func flt64Ptr(f float64) *float64 { if f != 0 { return &f diff --git a/pkger/parser.go b/pkger/parser.go index 005605148cb..f62955a4ae0 100644 --- a/pkger/parser.go +++ b/pkger/parser.go @@ -135,6 +135,7 @@ type Pkg struct { mLabels map[string]*label mBuckets map[string]*bucket + mChecks map[string]*check mDashboards []*dashboard mNotificationEndpoints map[string]*notificationEndpoint mTelegrafs []*telegraf @@ -156,6 +157,10 @@ func (p *Pkg) Summary() Summary { sum.Buckets = append(sum.Buckets, b.summarize()) } + for _, c := range p.checks() { + sum.Checks = append(sum.Checks, c.summarize()) + } + for _, d := range p.dashboards() { sum.Dashboards = append(sum.Dashboards, d.summarize()) } @@ -243,6 +248,17 @@ func (p *Pkg) buckets() []*bucket { return buckets } +func (p *Pkg) checks() []*check { + checks := make([]*check, 0, len(p.mChecks)) + for _, c := range p.mChecks { + checks = append(checks, c) + } + + sort.Slice(checks, func(i, j int) bool { return checks[i].Name() < checks[j].Name() }) + + return checks +} + func (p *Pkg) labels() []*label { labels := make(sortedLabels, 0, len(p.mLabels)) for _, b := range p.mLabels { @@ -407,6 +423,7 @@ func (p *Pkg) graphResources() error { p.graphLabels, p.graphVariables, p.graphBuckets, + p.graphChecks, p.graphDashboards, p.graphNotificationEndpoints, p.graphTelegrafs, @@ -487,6 +504,77 @@ func (p *Pkg) graphLabels() *parseErr { }) } +func (p *Pkg) graphChecks() *parseErr { + p.mChecks = make(map[string]*check) + + checkKinds := []struct { + kind Kind + checkKind checkKind + }{ + {kind: KindCheckThreshold, checkKind: checkKindThreshold}, + {kind: KindCheckDeadman, checkKind: checkKindDeadman}, + } + var pErr parseErr + for _, k := range checkKinds { + err := p.eachResource(k.kind, 1, func(r Resource) []validationErr { + if _, ok := p.mChecks[r.Name()]; ok { + return []validationErr{{ + Field: "name", + Msg: "duplicate name: " + r.Name(), + }} + } + + ch := &check{ + kind: k.checkKind, + name: r.Name(), + description: r.stringShort(fieldDescription), + every: r.durationShort(fieldCheckEvery), + level: r.stringShort(fieldCheckLevel), + offset: r.durationShort(fieldCheckOffset), + query: strings.TrimSpace(r.stringShort(fieldQuery)), + reportZero: r.boolShort(fieldCheckReportZero), + staleTime: r.durationShort(fieldCheckStaleTime), + status: normStr(r.stringShort(fieldStatus)), + statusMessage: r.stringShort(fieldCheckStatusMessageTemplate), + timeSince: r.durationShort(fieldCheckTimeSince), + } + for _, tagRes := range r.slcResource(fieldCheckTags) { + ch.tags = append(ch.tags, struct{ k, v string }{ + k: tagRes.stringShort(fieldKey), + v: tagRes.stringShort(fieldValue), + }) + } + for _, th := range r.slcResource(fieldCheckThresholds) { + ch.thresholds = append(ch.thresholds, threshold{ + threshType: thresholdType(normStr(th.stringShort(fieldType))), + allVals: th.boolShort(fieldCheckAllValues), + level: strings.TrimSpace(strings.ToUpper(th.stringShort(fieldCheckLevel))), + max: th.float64Short(fieldMax), + min: th.float64Short(fieldMin), + val: th.float64Short(fieldValue), + }) + } + + failures := p.parseNestedLabels(r, func(l *label) error { + ch.labels = append(ch.labels, l) + p.mLabels[l.Name()].setMapping(ch, false) + return nil + }) + sort.Sort(ch.labels) + + p.mChecks[ch.Name()] = ch + return append(failures, ch.valid()...) + }) + if err != nil { + pErr.append(err.Resources...) + } + } + if len(pErr.Resources) > 0 { + return &pErr + } + return nil +} + func (p *Pkg) graphDashboards() *parseErr { p.mDashboards = make([]*dashboard, 0) return p.eachResource(KindDashboard, 2, func(r Resource) []validationErr { @@ -922,6 +1010,16 @@ func (r Resource) boolShort(key string) bool { return b } +func (r Resource) duration(key string) (time.Duration, bool) { + dur, err := time.ParseDuration(r.stringShort(key)) + return dur, err == nil +} + +func (r Resource) durationShort(key string) time.Duration { + dur, _ := r.duration(key) + return dur +} + func (r Resource) float64(key string) (float64, bool) { f, ok := r[key].(float64) if ok { diff --git a/pkger/parser_test.go b/pkger/parser_test.go index e41f1a9938c..d966163a619 100644 --- a/pkger/parser_test.go +++ b/pkger/parser_test.go @@ -8,6 +8,8 @@ import ( "time" "github.com/influxdata/influxdb" + "github.com/influxdata/influxdb/notification" + icheck "github.com/influxdata/influxdb/notification/check" "github.com/influxdata/influxdb/notification/endpoint" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -442,6 +444,465 @@ spec: }) }) + t.Run("pkg with checks", func(t *testing.T) { + testfileRunner(t, "testdata/checks", func(t *testing.T, pkg *Pkg) { + sum := pkg.Summary() + require.Len(t, sum.Checks, 2) + + check1 := sum.Checks[0] + thresholdCheck, ok := check1.Check.(*icheck.Threshold) + require.Truef(t, ok, "got: %#v", check1) + + expectedBase := icheck.Base{ + Name: "check_0", + Description: "desc_0", + Every: mustDuration(t, time.Minute), + Offset: mustDuration(t, 15*time.Second), + StatusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }", + Tags: []influxdb.Tag{ + {Key: "tag_1", Value: "val_1"}, + {Key: "tag_2", Value: "val_2"}, + }, + } + expectedBase.Query.Text = "from(bucket: \"rucket_1\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"cpu\")\n |> filter(fn: (r) => r._field == \"usage_idle\")\n |> aggregateWindow(every: 1m, fn: mean)\n |> yield(name: \"mean\")" + assert.Equal(t, expectedBase, thresholdCheck.Base) + + expectedThresholds := []icheck.ThresholdConfig{ + icheck.Greater{ + ThresholdConfigBase: icheck.ThresholdConfigBase{ + AllValues: true, + Level: notification.Critical, + }, + Value: 50.0, + }, + icheck.Lesser{ + ThresholdConfigBase: icheck.ThresholdConfigBase{Level: notification.Warn}, + Value: 49.9, + }, + icheck.Range{ + ThresholdConfigBase: icheck.ThresholdConfigBase{Level: notification.Info}, + Within: true, + Min: 30.0, + Max: 45.0, + }, + icheck.Range{ + ThresholdConfigBase: icheck.ThresholdConfigBase{Level: notification.Ok}, + Min: 30.0, + Max: 35.0, + }, + } + assert.Equal(t, expectedThresholds, thresholdCheck.Thresholds) + assert.Equal(t, influxdb.Status(influxdb.TaskStatusInactive), check1.Status) + assert.Len(t, check1.LabelAssociations, 1) + + check2 := sum.Checks[1] + deadmanCheck, ok := check2.Check.(*icheck.Deadman) + require.Truef(t, ok, "got: %#v", check2) + + expectedBase = icheck.Base{ + Name: "check_1", + Description: "desc_1", + Every: mustDuration(t, 5*time.Minute), + Offset: mustDuration(t, 10*time.Second), + StatusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }", + Tags: []influxdb.Tag{ + {Key: "tag_1", Value: "val_1"}, + {Key: "tag_2", Value: "val_2"}, + }, + } + expectedBase.Query.Text = "from(bucket: \"rucket_1\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"cpu\")\n |> filter(fn: (r) => r._field == \"usage_idle\")\n |> aggregateWindow(every: 1m, fn: mean)\n |> yield(name: \"mean\")" + assert.Equal(t, expectedBase, deadmanCheck.Base) + assert.Equal(t, influxdb.Status(influxdb.TaskStatusActive), check2.Status) + assert.Equal(t, mustDuration(t, 10*time.Minute), deadmanCheck.StaleTime) + assert.Equal(t, mustDuration(t, 90*time.Second), deadmanCheck.TimeSince) + assert.True(t, deadmanCheck.ReportZero) + assert.Len(t, check2.LabelAssociations, 1) + + containsLabelMappings(t, sum.LabelMappings, + labelMapping{ + labelName: "label_1", + resName: "check_0", + resType: influxdb.ChecksResourceType, + }, + labelMapping{ + labelName: "label_1", + resName: "check_1", + resType: influxdb.ChecksResourceType, + }, + ) + }) + + t.Run("handles bad config", func(t *testing.T) { + tests := []struct { + kind Kind + resErr testPkgResourceError + }{ + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "duplicate name", + validationErrs: 1, + valFields: []string{fieldName}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 1m + query: > + from(bucket: "rucket_1") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: greater + level: CRIT + value: 50.0 + - kind: Check_Threshold + name: check_0 + every: 1m + query: > + from(bucket: "rucket_1") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: greater + level: CRIT + value: 50.0 +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "missing every duration", + validationErrs: 1, + valFields: []string{fieldCheckEvery}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + query: > + from(bucket: "rucket_1") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: greater + level: CRIT + value: 50.0 +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "invalid threshold value provided", + validationErrs: 1, + valFields: []string{fieldCheckLevel}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 15s + query: > + from(bucket: "rucket_1") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: greater + level: RANDO_WRONGO + value: 50.0 +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "invalid threshold type provided", + validationErrs: 1, + valFields: []string{fieldType}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 15s + query: > + from(bucket: "rucket_1") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: RANDO_TYPE + level: CRIT + value: 50.0 +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "invalid threshold type provided", + validationErrs: 1, + valFields: []string{fieldMin}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 15s + query: > + from(bucket: "rucket_1") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: Inside_Range + level: CRIT + min: 35.0 + max: 30.0 +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "no threshold values provided", + validationErrs: 1, + valFields: []string{fieldCheckThresholds}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 10m + query: > + from(bucket: "rucket_1") |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "missing query", + validationErrs: 1, + valFields: []string{fieldQuery}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 15s + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: greater + level: CRIT + value: 50.0 +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "invalid status provided", + validationErrs: 1, + valFields: []string{fieldStatus}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 15s + query: from("bucketer") + status: RANDO STATUS + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + thresholds: + - type: greater + level: CRIT + value: 50.0 +`, + }, + }, + { + kind: KindCheckThreshold, + resErr: testPkgResourceError{ + name: "missing status message template", + validationErrs: 1, + valFields: []string{fieldCheckStatusMessageTemplate}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Threshold + name: check_0 + every: 15s + query: from("bucketer") + thresholds: + - type: greater + level: CRIT + value: 50.0 +`, + }, + }, + { + kind: KindCheckDeadman, + resErr: testPkgResourceError{ + name: "missing every", + validationErrs: 1, + valFields: []string{fieldCheckEvery}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Deadman + name: check_1 + level: cRiT + query: > + from(bucket: "rucket_1") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + staleTime: 10m + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + timeSince: 90s +`, + }, + }, + { + kind: KindCheckDeadman, + resErr: testPkgResourceError{ + name: "missing every", + validationErrs: 1, + valFields: []string{fieldQuery}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Deadman + name: check_1 + every: 1h + level: cRiT + staleTime: 10m + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + timeSince: 90s +`, + }, + }, + { + kind: KindCheckDeadman, + resErr: testPkgResourceError{ + name: "missing association label", + validationErrs: 1, + valFields: []string{fieldAssociations}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Check_Deadman + name: check_1 + every: 5m + level: cRiT + query: > + from(bucket: "rucket_1") + staleTime: 10m + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + timeSince: 90s + associations: + - kind: Label + name: label_1 +`, + }, + }, + { + kind: KindCheckDeadman, + resErr: testPkgResourceError{ + name: "duplicate association labels", + validationErrs: 1, + valFields: []string{fieldAssociations}, + pkgStr: `apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Label + name: label_1 + - kind: Check_Deadman + name: check_1 + every: 5m + level: cRiT + query: > + from(bucket: "rucket_1") + staleTime: 10m + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + timeSince: 90s + associations: + - kind: Label + name: label_1 + - kind: Label + name: label_1 +`, + }, + }, + } + + for _, tt := range tests { + testPkgErrors(t, tt.kind, tt.resErr) + } + }) + }) + t.Run("pkg with single dashboard and single chart", func(t *testing.T) { t.Run("single gauge chart", func(t *testing.T) { testfileRunner(t, "testdata/dashboard_gauge", func(t *testing.T, pkg *Pkg) { @@ -2771,6 +3232,7 @@ spec: sum := pkg.Summary() endpoints := sum.NotificationEndpoints require.Len(t, endpoints, len(expectedEndpoints)) + require.Len(t, sum.LabelMappings, len(expectedEndpoints)) for i := range expectedEndpoints { expected, actual := expectedEndpoints[i], endpoints[i] @@ -2778,13 +3240,11 @@ spec: require.Len(t, actual.LabelAssociations, 1) assert.Equal(t, "label_1", actual.LabelAssociations[0].Name) - require.Len(t, sum.LabelMappings, len(expectedEndpoints)) - expectedMapping := SummaryLabelMapping{ - ResourceName: expected.NotificationEndpoint.GetName(), - LabelName: "label_1", - ResourceType: influxdb.NotificationEndpointResourceType, - } - assert.Contains(t, sum.LabelMappings, expectedMapping) + containsLabelMappings(t, sum.LabelMappings, labelMapping{ + labelName: "label_1", + resName: expected.NotificationEndpoint.GetName(), + resType: influxdb.NotificationEndpointResourceType, + }) } }) @@ -3643,6 +4103,32 @@ func testfileRunner(t *testing.T, path string, testFn func(t *testing.T, pkg *Pk } } +type labelMapping struct { + labelName string + resName string + resType influxdb.ResourceType +} + +func containsLabelMappings(t *testing.T, labelMappings []SummaryLabelMapping, matches ...labelMapping) { + t.Helper() + + for _, expected := range matches { + expectedMapping := SummaryLabelMapping{ + ResourceName: expected.resName, + LabelName: expected.labelName, + ResourceType: expected.resType, + } + assert.Contains(t, labelMappings, expectedMapping) + } +} + func strPtr(s string) *string { return &s } + +func mustDuration(t *testing.T, d time.Duration) *notification.Duration { + t.Helper() + dur, err := notification.FromTimeDuration(d) + require.NoError(t, err) + return &dur +} diff --git a/pkger/testdata/checks.json b/pkger/testdata/checks.json new file mode 100644 index 00000000000..d9ecaa76c15 --- /dev/null +++ b/pkger/testdata/checks.json @@ -0,0 +1,96 @@ +{ + "apiVersion": "0.1.0", + "kind": "Package", + "meta": { + "pkgName": "pkg_name", + "pkgVersion": "1", + "description": "pack description" + }, + "spec": { + "resources": [ + { + "kind": "Label", + "name": "label_1" + }, + { + "kind": "Check_Threshold", + "name": "check_0", + "description": "desc_0", + "every": "1m", + "offset": "15s", + "query": "from(bucket: \"rucket_1\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"cpu\")\n |> filter(fn: (r) => r._field == \"usage_idle\")\n |> aggregateWindow(every: 1m, fn: mean)\n |> yield(name: \"mean\")", + "status": "inactive", + "statusMessageTemplate": "Check: ${ r._check_name } is: ${ r._level }", + "tags": [ + { + "key": "tag_1", + "value": "val_1" + }, + { + "key": "tag_2", + "value": "val_2" + } + ], + "thresholds": [ + { + "type": "greater", + "level": "CRIT", + "value": 50.0, + "allValues": true + }, + { + "type": "lesser", + "level": "warn", + "value": 49.9 + }, + { + "type": "inside_range", + "level": "INfO", + "min": 30.0, + "max": 45.0 + }, + { + "type": "outside_range", + "level": "ok", + "min": 30.0, + "max": 35.0 + } + ], + "associations": [ + { + "kind": "Label", + "name": "label_1" + } + ] + }, + { + "kind": "Check_Deadman", + "name": "check_1", + "description": "desc_1", + "every": "5m", + "offset": "10s", + "query": "from(bucket: \"rucket_1\")\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r._measurement == \"cpu\")\n |> filter(fn: (r) => r._field == \"usage_idle\")\n |> aggregateWindow(every: 1m, fn: mean)\n |> yield(name: \"mean\")", + "reportZero": true, + "staleTime": "10m", + "statusMessageTemplate": "Check: ${ r._check_name } is: ${ r._level }", + "tags": [ + { + "key": "tag_1", + "value": "val_1" + }, + { + "key": "tag_2", + "value": "val_2" + } + ], + "timeSince": "90s", + "associations": [ + { + "kind": "Label", + "name": "label_1" + } + ] + } + ] + } +} diff --git a/pkger/testdata/checks.yml b/pkger/testdata/checks.yml new file mode 100644 index 00000000000..8d13c3f17ed --- /dev/null +++ b/pkger/testdata/checks.yml @@ -0,0 +1,73 @@ +apiVersion: 0.1.0 +kind: Package +meta: + pkgName: pkg_name + pkgVersion: 1 + description: pack description +spec: + resources: + - kind: Label + name: label_1 + - kind: Check_Threshold + name: check_0 + description: desc_0 + every: 1m + offset: 15s + query: > + from(bucket: "rucket_1") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r._measurement == "cpu") + |> filter(fn: (r) => r._field == "usage_idle") + |> aggregateWindow(every: 1m, fn: mean) + |> yield(name: "mean") + status: inactive + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + tags: + - key: tag_1 + value: val_1 + - key: tag_2 + value: val_2 + thresholds: + - type: greater + level: CRIT + value: 50.0 + allValues: true + - type: lesser + level: warn + value: 49.9 + - type: inside_range + level: INfO + min: 30.0 + max: 45.0 + - type: outside_range + level: ok + min: 30.0 + max: 35.0 + associations: + - kind: Label + name: label_1 + - kind: Check_Deadman + name: check_1 + description: desc_1 + every: 5m + level: cRiT + offset: 10s + query: > + from(bucket: "rucket_1") + |> range(start: v.timeRangeStart, stop: v.timeRangeStop) + |> filter(fn: (r) => r._measurement == "cpu") + |> filter(fn: (r) => r._field == "usage_idle") + |> aggregateWindow(every: 1m, fn: mean) + |> yield(name: "mean") + reportZero: true + staleTime: 10m + statusMessageTemplate: "Check: ${ r._check_name } is: ${ r._level }" + tags: + - key: tag_1 + value: val_1 + - key: tag_2 + value: val_2 + timeSince: 90s + associations: + - kind: Label + name: label_1