grafana/pkg/services/ngalert/models/testing.go

1338 lines
36 KiB
Go

package models
import (
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"slices"
"sync"
"testing"
"time"
"github.com/go-openapi/strfmt"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/grafana-plugin-sdk-go/data"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
alertingModels "github.com/grafana/alerting/models"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/util"
)
var (
RuleMuts = AlertRuleMutators{}
NSMuts = NotificationSettingsMutators{}
RuleGen = &AlertRuleGenerator{
mutators: []AlertRuleMutator{
RuleMuts.WithUniqueUID(), RuleMuts.WithUniqueTitle(),
},
}
)
type AlertRuleMutator func(r *AlertRule)
type AlertRuleGenerator struct {
AlertRuleMutators
mutators []AlertRuleMutator
}
func (g *AlertRuleGenerator) With(mutators ...AlertRuleMutator) *AlertRuleGenerator {
return &AlertRuleGenerator{
AlertRuleMutators: g.AlertRuleMutators,
mutators: append(g.mutators, mutators...),
}
}
func (g *AlertRuleGenerator) Generate() AlertRule {
randNoDataState := func() NoDataState {
s := [...]NoDataState{
Alerting,
NoData,
OK,
}
return s[rand.Intn(len(s))]
}
randErrState := func() ExecutionErrorState {
s := [...]ExecutionErrorState{
AlertingErrState,
ErrorErrState,
OkErrState,
}
return s[rand.Intn(len(s))]
}
interval := (rand.Int63n(6) + 1) * 10
forInterval := time.Duration(interval*rand.Int63n(6)) * time.Second
var annotations map[string]string = nil
if rand.Int63()%2 == 0 {
annotations = GenerateAlertLabels(rand.Intn(5), "ann-")
}
var labels map[string]string = nil
if rand.Int63()%2 == 0 {
labels = GenerateAlertLabels(rand.Intn(5), "lbl-")
}
var dashUID *string = nil
var panelID *int64 = nil
if rand.Int63()%2 == 0 {
d := util.GenerateShortUID()
dashUID = &d
p := rand.Int63n(1500)
panelID = &p
}
var ns []NotificationSettings
if rand.Int63()%2 == 0 {
ns = append(ns, NotificationSettingsGen()())
}
rule := AlertRule{
ID: 0,
OrgID: rand.Int63n(1500) + 1, // Prevent OrgID=0 as this does not pass alert rule validation.
Title: fmt.Sprintf("title-%s", util.GenerateShortUID()),
Condition: "A",
Data: []AlertQuery{g.GenerateQuery()},
Updated: time.Now().Add(-time.Duration(rand.Intn(100) + 1)),
IntervalSeconds: rand.Int63n(60) + 1,
Version: rand.Int63n(1500), // Don't generate a rule ID too big for postgres
UID: util.GenerateShortUID(),
NamespaceUID: util.GenerateShortUID(),
DashboardUID: dashUID,
PanelID: panelID,
RuleGroup: fmt.Sprintf("group-%s,", util.GenerateShortUID()),
RuleGroupIndex: rand.Intn(1500),
NoDataState: randNoDataState(),
ExecErrState: randErrState(),
For: forInterval,
Annotations: annotations,
Labels: labels,
NotificationSettings: ns,
}
for _, mutator := range g.mutators {
mutator(&rule)
}
return rule
}
func (g *AlertRuleGenerator) GenerateRef() *AlertRule {
r := g.Generate()
return &r
}
func (g *AlertRuleGenerator) getCount(bounds ...int) int {
count := 0
if len(bounds) == 0 {
count = rand.Intn(5) + 1
}
if len(bounds) == 1 {
count = bounds[0]
}
if len(bounds) == 2 {
if bounds[0] > bounds[1] {
panic("min should not be greater than max")
} else if bounds[0] < bounds[1] {
count = rand.Intn(bounds[1]-bounds[0]) + bounds[0]
} else {
count = bounds[0]
}
}
if len(bounds) > 2 {
panic("invalid number of parameter must be up to 2")
}
return count
}
func (g *AlertRuleGenerator) GenerateMany(bounds ...int) []AlertRule {
count := g.getCount(bounds...)
result := make([]AlertRule, 0, count)
for i := 0; i < count; i++ {
result = append(result, g.Generate())
}
return result
}
func (g *AlertRuleGenerator) GenerateManyRef(bounds ...int) []*AlertRule {
count := g.getCount(bounds...)
result := make([]*AlertRule, 0)
for i := 0; i < count; i++ {
r := g.Generate()
result = append(result, &r)
}
return result
}
type AlertRuleMutators struct {
}
func (a *AlertRuleMutators) WithNotEmptyLabels(count int, prefix string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Labels = GenerateAlertLabels(count, prefix)
}
}
func (a *AlertRuleMutators) WithUniqueID() AlertRuleMutator {
ids := sync.Map{}
return func(rule *AlertRule) {
id := rule.ID
for {
_, exists := ids.LoadOrStore(id, struct{}{})
if !exists {
rule.ID = id
return
}
id = rand.Int63n(1500) + 1
}
}
}
func (a *AlertRuleMutators) WithEditorSettingsSimplifiedQueryAndExpressionsSection(mode bool) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Metadata.EditorSettings.SimplifiedQueryAndExpressionsSection = mode
}
}
func (a *AlertRuleMutators) WithGroupIndex(groupIndex int) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroupIndex = groupIndex
}
}
func (a *AlertRuleMutators) WithUniqueGroupIndex() AlertRuleMutator {
usedIdx := sync.Map{}
return func(rule *AlertRule) {
idx := rule.RuleGroupIndex
for {
if _, exists := usedIdx.LoadOrStore(idx, struct{}{}); !exists {
rule.RuleGroupIndex = idx
return
}
idx = rand.Int()
}
}
}
func (a *AlertRuleMutators) WithSequentialGroupIndex() AlertRuleMutator {
idx := 1
m := sync.Mutex{}
return func(rule *AlertRule) {
m.Lock()
defer m.Unlock()
rule.RuleGroupIndex = idx
idx++
}
}
func (a *AlertRuleMutators) WithOrgID(orgId int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.OrgID = orgId
}
}
func (a *AlertRuleMutators) WithUniqueOrgID() AlertRuleMutator {
orgs := sync.Map{}
return func(rule *AlertRule) {
orgID := rule.OrgID
for {
if _, exist := orgs.LoadOrStore(orgID, struct{}{}); !exist {
rule.OrgID = orgID
return
}
orgID = rand.Int63()
}
}
}
// WithNamespaceUIDNotIn generates a random namespace UID if it is among excluded
func (a *AlertRuleMutators) WithNamespaceUIDNotIn(exclude ...string) AlertRuleMutator {
return func(rule *AlertRule) {
for {
if !slices.Contains(exclude, rule.NamespaceUID) {
return
}
rule.NamespaceUID = util.GenerateShortUID()
}
}
}
func (a *AlertRuleMutators) WithNamespaceUID(namespaceUID string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NamespaceUID = namespaceUID
}
}
func (a *AlertRuleMutators) WithNamespace(namespace *folder.Folder) AlertRuleMutator {
return a.WithNamespaceUID(namespace.UID)
}
func (a *AlertRuleMutators) WithInterval(interval time.Duration) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = int64(interval.Seconds())
}
}
func (a *AlertRuleMutators) WithIntervalSeconds(seconds int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = seconds
}
}
// WithIntervalMatching mutator that generates random interval and `for` duration that are times of the provided base interval.
func (a *AlertRuleMutators) WithIntervalMatching(baseInterval time.Duration) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = int64(baseInterval.Seconds()) * (rand.Int63n(10) + 1)
rule.For = time.Duration(rule.IntervalSeconds*rand.Int63n(9)+1) * time.Second
}
}
func (a *AlertRuleMutators) WithIntervalBetween(min, max int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IntervalSeconds = rand.Int63n(max-min) + min
}
}
func (a *AlertRuleMutators) WithTitle(title string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Title = title
}
}
func (a *AlertRuleMutators) WithFor(duration time.Duration) AlertRuleMutator {
return func(rule *AlertRule) {
rule.For = duration
}
}
func (a *AlertRuleMutators) WithForNTimes(timesOfInterval int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.For = time.Duration(rule.IntervalSeconds*timesOfInterval) * time.Second
}
}
func (a *AlertRuleMutators) WithNoDataExecAs(nodata NoDataState) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NoDataState = nodata
}
}
func (a *AlertRuleMutators) WithErrorExecAs(err ExecutionErrorState) AlertRuleMutator {
return func(rule *AlertRule) {
rule.ExecErrState = err
}
}
func (a *AlertRuleMutators) WithAnnotations(lbls data.Labels) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Annotations = lbls
}
}
func (a *AlertRuleMutators) WithAnnotation(key, value string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Annotations == nil {
rule.Annotations = data.Labels{}
}
rule.Annotations[key] = value
}
}
func (a *AlertRuleMutators) WithLabels(lbls data.Labels) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Labels = lbls
}
}
func (a *AlertRuleMutators) WithLabel(key, value string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Labels == nil {
rule.Labels = data.Labels{}
}
rule.Labels[key] = value
}
}
func (a *AlertRuleMutators) WithDashboardAndPanel(dashboardUID *string, panelID *int64) AlertRuleMutator {
return func(rule *AlertRule) {
rule.DashboardUID = dashboardUID
rule.PanelID = panelID
}
}
// WithUniqueUID returns AlertRuleMutator that generates a random UID if it is among UIDs known by the instance of mutator.
// NOTE: two instances of the mutator do not share known UID.
// Example #1 reuse mutator instance:
//
// mut := WithUniqueUID()
// rule1 := RuleGen.With(mut).Generate()
// rule2 := RuleGen.With(mut).Generate()
//
// Example #2 reuse generator:
//
// gen := RuleGen.With(WithUniqueUID())
// rule1 := gen.Generate()
// rule2 := gen.Generate()
//
// Example #3 non-unique:
//
// rule1 := RuleGen.With(WithUniqueUID()).Generate
// rule2 := RuleGen.With(WithUniqueUID()).Generate
func (a *AlertRuleMutators) WithUniqueUID() AlertRuleMutator {
uids := sync.Map{}
return func(rule *AlertRule) {
uid := rule.UID
for {
_, exist := uids.LoadOrStore(uid, struct{}{})
if !exist {
rule.UID = uid
return
}
uid = util.GenerateShortUID()
}
}
}
// WithUniqueTitle returns AlertRuleMutator that generates a random title if the rule's title is among titles known by the instance of mutator.
// Two instances of the mutator do not share known titles.
// Example #1 reuse mutator instance:
//
// mut := WithUniqueTitle()
// rule1 := RuleGen.With(mut).Generate()
// rule2 := RuleGen.With(mut).Generate()
//
// Example #2 reuse generator:
//
// gen := RuleGen.With(WithUniqueTitle())
// rule1 := gen.Generate()
// rule2 := gen.Generate()
//
// Example #3 non-unique:
//
// rule1 := RuleGen.With(WithUniqueTitle()).Generate
// rule2 := RuleGen.With(WithUniqueTitle()).Generate
func (a *AlertRuleMutators) WithUniqueTitle() AlertRuleMutator {
titles := sync.Map{}
return func(rule *AlertRule) {
title := rule.Title
for {
_, exist := titles.LoadOrStore(title, struct{}{})
if !exist {
rule.Title = title
return
}
title = fmt.Sprintf("title-%s", util.GenerateShortUID())
}
}
}
func (a *AlertRuleMutators) WithQuery(query ...AlertQuery) AlertRuleMutator {
return func(rule *AlertRule) {
rule.Data = query
if len(query) > 1 {
rule.Condition = query[0].RefID
}
}
}
func (a *AlertRuleMutators) WithGroupName(groupName string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroup = groupName
}
}
func (a *AlertRuleMutators) WithGroupPrefix(prefix string) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroup = fmt.Sprintf("%s%s", prefix, util.GenerateShortUID())
}
}
func (a *AlertRuleMutators) WithGroupKey(groupKey AlertRuleGroupKey) AlertRuleMutator {
return func(rule *AlertRule) {
rule.RuleGroup = groupKey.RuleGroup
rule.OrgID = groupKey.OrgID
rule.NamespaceUID = groupKey.NamespaceUID
}
}
// WithSameGroup generates a random group name and assigns it to all rules passed to it
func (a *AlertRuleMutators) WithSameGroup() AlertRuleMutator {
once := sync.Once{}
name := ""
return func(rule *AlertRule) {
once.Do(func() {
name = util.GenerateShortUID()
})
rule.RuleGroup = name
}
}
func (a *AlertRuleMutators) WithNotificationSettingsGen(ns func() NotificationSettings) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NotificationSettings = []NotificationSettings{ns()}
}
}
func (a *AlertRuleMutators) WithNotificationSettings(ns NotificationSettings) AlertRuleMutator {
return func(rule *AlertRule) {
rule.NotificationSettings = []NotificationSettings{ns}
}
}
func (a *AlertRuleMutators) WithNoNotificationSettings() AlertRuleMutator {
return func(rule *AlertRule) {
rule.NotificationSettings = nil
}
}
func (a *AlertRuleMutators) WithIsPaused(paused bool) AlertRuleMutator {
return func(rule *AlertRule) {
rule.IsPaused = paused
}
}
func (a *AlertRuleMutators) WithRandomRecordingRules() AlertRuleMutator {
return func(rule *AlertRule) {
if rand.Int63()%2 == 0 {
return
}
ConvertToRecordingRule(rule)
}
}
func (a *AlertRuleMutators) WithAllRecordingRules() AlertRuleMutator {
return func(rule *AlertRule) {
ConvertToRecordingRule(rule)
}
}
func (a *AlertRuleMutators) WithMetric(metric string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Record == nil {
rule.Record = &Record{}
}
rule.Record.Metric = metric
}
}
func (a *AlertRuleMutators) WithRecordFrom(from string) AlertRuleMutator {
return func(rule *AlertRule) {
if rule.Record == nil {
rule.Record = &Record{}
}
rule.Record.From = from
}
}
func (g *AlertRuleGenerator) GenerateLabels(min, max int, prefix string) data.Labels {
count := max
if min > max {
panic("min should not be greater than max")
} else if min < max {
count = rand.Intn(max-min) + min
}
labels := make(data.Labels, count)
for i := 0; i < count; i++ {
labels[prefix+"key-"+util.GenerateShortUID()] = prefix + "value-" + util.GenerateShortUID()
}
return labels
}
func GenerateAlertLabels(count int, prefix string) data.Labels {
return RuleGen.GenerateLabels(count, count, prefix)
}
func GenerateAlertQuery() AlertQuery {
return RuleGen.GenerateQuery()
}
func (g *AlertRuleGenerator) GenerateQuery() AlertQuery {
f := rand.Intn(10) + 5
t := rand.Intn(f)
return AlertQuery{
DatasourceUID: util.GenerateShortUID(),
Model: json.RawMessage(fmt.Sprintf(`{
"%s": "%s",
"%s":"%d"
}`, util.GenerateShortUID(), util.GenerateShortUID(), util.GenerateShortUID(), rand.Int())),
RelativeTimeRange: RelativeTimeRange{
From: Duration(time.Duration(f) * time.Minute),
To: Duration(time.Duration(t) * time.Minute),
},
RefID: util.GenerateShortUID(),
QueryType: util.GenerateShortUID(),
}
}
func (g *AlertRuleGenerator) WithCondition(condition string) AlertRuleMutator {
return func(r *AlertRule) {
r.Condition = condition
}
}
// GenerateRuleKey generates a random alert rule key
func GenerateRuleKey(orgID int64) AlertRuleKey {
return AlertRuleKey{
OrgID: orgID,
UID: util.GenerateShortUID(),
}
}
// GenerateGroupKey generates a random group key
func GenerateGroupKey(orgID int64) AlertRuleGroupKey {
return AlertRuleGroupKey{
OrgID: orgID,
NamespaceUID: util.GenerateShortUID(),
RuleGroup: util.GenerateShortUID(),
}
}
// CopyRule creates a deep copy of AlertRule
func CopyRule(r *AlertRule, mutators ...AlertRuleMutator) *AlertRule {
result := AlertRule{
ID: r.ID,
OrgID: r.OrgID,
Title: r.Title,
Condition: r.Condition,
Updated: r.Updated,
IntervalSeconds: r.IntervalSeconds,
Version: r.Version,
UID: r.UID,
NamespaceUID: r.NamespaceUID,
RuleGroup: r.RuleGroup,
RuleGroupIndex: r.RuleGroupIndex,
NoDataState: r.NoDataState,
ExecErrState: r.ExecErrState,
For: r.For,
Record: r.Record,
}
if r.DashboardUID != nil {
dash := *r.DashboardUID
result.DashboardUID = &dash
}
if r.PanelID != nil {
p := *r.PanelID
result.PanelID = &p
}
for _, d := range r.Data {
q := AlertQuery{
RefID: d.RefID,
QueryType: d.QueryType,
RelativeTimeRange: d.RelativeTimeRange,
DatasourceUID: d.DatasourceUID,
}
q.Model = make([]byte, 0, cap(d.Model))
q.Model = append(q.Model, d.Model...)
result.Data = append(result.Data, q)
}
if r.Annotations != nil {
result.Annotations = make(map[string]string, len(r.Annotations))
for s, s2 := range r.Annotations {
result.Annotations[s] = s2
}
}
if r.Labels != nil {
result.Labels = make(map[string]string, len(r.Labels))
for s, s2 := range r.Labels {
result.Labels[s] = s2
}
}
if r.Record != nil {
result.Record = &Record{
From: r.Record.From,
Metric: r.Record.Metric,
}
}
for _, s := range r.NotificationSettings {
result.NotificationSettings = append(result.NotificationSettings, CopyNotificationSettings(s))
}
if len(mutators) > 0 {
for _, mutator := range mutators {
mutator(&result)
}
}
return &result
}
func CreateClassicConditionExpression(refID string, inputRefID string, reducer string, operation string, threshold int) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: expr.DatasourceType,
DatasourceUID: expr.DatasourceUID,
// the format corresponds to model `ClassicConditionJSON` in /pkg/expr/classic/classic.go
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"hide": false,
"type": "classic_conditions",
"datasource": {
"uid": "%[6]s",
"type": "%[7]s"
},
"conditions": [
{
"type": "query",
"evaluator": {
"params": [
%[4]d
],
"type": "%[3]s"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"%[2]s"
]
},
"reducer": {
"params": [],
"type": "%[5]s"
}
}
]
}`, refID, inputRefID, operation, threshold, reducer, expr.DatasourceUID, expr.DatasourceType)),
}
}
func CreateReduceExpression(refID string, inputRefID string, reducer string) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: expr.DatasourceType,
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"hide": false,
"type": "reduce",
"expression": "%[2]s",
"reducer": "%[3]s",
"datasource": {
"uid": "%[4]s",
"type": "%[5]s"
}
}`, refID, inputRefID, reducer, expr.DatasourceUID, expr.DatasourceType)),
}
}
func CreatePrometheusQuery(refID string, expr string, intervalMs int64, maxDataPoints int64, isInstant bool, datasourceUID string) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: "",
DatasourceUID: datasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"expr": "%[2]s",
"intervalMs": %[3]d,
"maxDataPoints": %[4]d,
"exemplar": false,
"instant": %[5]t,
"range": %[6]t,
"datasource": {
"uid": "%[7]s",
"type": "%[8]s"
}
}`, refID, expr, intervalMs, maxDataPoints, isInstant, !isInstant, datasourceUID, datasources.DS_PROMETHEUS)),
}
}
func CreateLokiQuery(refID string, expr string, intervalMs int64, maxDataPoints int64, queryType string, datasourceUID string) AlertQuery {
return AlertQuery{
RefID: refID,
QueryType: queryType,
DatasourceUID: datasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"expr": "%[2]s",
"intervalMs": %[3]d,
"maxDataPoints": %[4]d,
"queryType": "%[5]s",
"datasource": {
"uid": "%[6]s",
"type": "%[7]s"
}
}`, refID, expr, intervalMs, maxDataPoints, queryType, datasourceUID, datasources.DS_LOKI)),
}
}
func CreateHysteresisExpression(t *testing.T, refID string, inputRefID string, threshold int, recoveryThreshold int) AlertQuery {
t.Helper()
q := AlertQuery{
RefID: refID,
QueryType: expr.DatasourceType,
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(fmt.Sprintf(`
{
"refId": "%[1]s",
"type": "threshold",
"datasource": {
"uid": "%[5]s",
"type": "%[6]s"
},
"expression": "%[2]s",
"conditions": [
{
"type": "query",
"evaluator": {
"params": [
%[3]d
],
"type": "gt"
},
"unloadEvaluator": {
"params": [
%[4]d
],
"type": "lt"
}
}
]
}`, refID, inputRefID, threshold, recoveryThreshold, expr.DatasourceUID, expr.DatasourceType)),
}
h, err := q.IsHysteresisExpression()
require.NoError(t, err)
require.Truef(t, h, "test model is expected to be a hysteresis expression")
return q
}
type AlertInstanceMutator func(*AlertInstance)
// AlertInstanceGen provides a factory function that generates a random AlertInstance.
// The mutators arguments allows changing fields of the resulting structure.
func AlertInstanceGen(mutators ...AlertInstanceMutator) *AlertInstance {
var labels map[string]string = nil
if rand.Int63()%2 == 0 {
labels = GenerateAlertLabels(rand.Intn(5), "lbl-")
}
randState := func() InstanceStateType {
s := [...]InstanceStateType{
InstanceStateFiring,
InstanceStateNormal,
InstanceStatePending,
InstanceStateNoData,
InstanceStateError,
}
return s[rand.Intn(len(s))]
}
currentStateSince := time.Now().Add(-time.Duration(rand.Intn(100) + 1))
instance := &AlertInstance{
AlertInstanceKey: AlertInstanceKey{
RuleOrgID: rand.Int63n(1500),
RuleUID: util.GenerateShortUID(),
LabelsHash: util.GenerateShortUID(),
},
Labels: labels,
CurrentState: randState(),
CurrentReason: "TEST-REASON-" + util.GenerateShortUID(),
CurrentStateSince: currentStateSince,
CurrentStateEnd: currentStateSince.Add(time.Duration(rand.Intn(100) + 200)),
LastEvalTime: time.Now().Add(-time.Duration(rand.Intn(100) + 50)),
LastSentAt: util.Pointer(time.Now().Add(-time.Duration(rand.Intn(100) + 50))),
}
if instance.CurrentState == InstanceStateNormal && rand.Intn(2) == 1 {
instance.ResolvedAt = util.Pointer(time.Now().Add(-time.Duration(rand.Intn(100) + 50)))
}
for _, mutator := range mutators {
mutator(instance)
}
return instance
}
type Mutator[T any] func(*T)
// CopyNotificationSettings creates a deep copy of NotificationSettings.
func CopyNotificationSettings(ns NotificationSettings, mutators ...Mutator[NotificationSettings]) NotificationSettings {
c := NotificationSettings{
Receiver: ns.Receiver,
}
if ns.GroupWait != nil {
c.GroupWait = util.Pointer(*ns.GroupWait)
}
if ns.GroupInterval != nil {
c.GroupInterval = util.Pointer(*ns.GroupInterval)
}
if ns.RepeatInterval != nil {
c.RepeatInterval = util.Pointer(*ns.RepeatInterval)
}
if ns.GroupBy != nil {
c.GroupBy = make([]string, len(ns.GroupBy))
copy(c.GroupBy, ns.GroupBy)
}
if ns.MuteTimeIntervals != nil {
c.MuteTimeIntervals = make([]string, len(ns.MuteTimeIntervals))
copy(c.MuteTimeIntervals, ns.MuteTimeIntervals)
}
for _, mutator := range mutators {
mutator(&c)
}
return c
}
// NotificationSettingsGen generates NotificationSettings using a base and mutators.
func NotificationSettingsGen(mutators ...Mutator[NotificationSettings]) func() NotificationSettings {
return func() NotificationSettings {
c := NotificationSettings{
Receiver: util.GenerateShortUID(),
GroupBy: []string{model.AlertNameLabel, FolderTitleLabel, util.GenerateShortUID()},
GroupWait: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
GroupInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
RepeatInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
MuteTimeIntervals: []string{util.GenerateShortUID(), util.GenerateShortUID()},
}
for _, mutator := range mutators {
mutator(&c)
}
return c
}
}
type NotificationSettingsMutators struct{}
func (n NotificationSettingsMutators) WithReceiver(receiver string) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
ns.Receiver = receiver
}
}
func (n NotificationSettingsMutators) WithGroupWait(groupWait *time.Duration) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
if groupWait == nil {
ns.GroupWait = nil
return
}
dur := model.Duration(*groupWait)
ns.GroupWait = &dur
}
}
func (n NotificationSettingsMutators) WithGroupInterval(groupInterval *time.Duration) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
if groupInterval == nil {
ns.GroupInterval = nil
return
}
dur := model.Duration(*groupInterval)
ns.GroupInterval = &dur
}
}
func (n NotificationSettingsMutators) WithRepeatInterval(repeatInterval *time.Duration) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
if repeatInterval == nil {
ns.RepeatInterval = nil
return
}
dur := model.Duration(*repeatInterval)
ns.RepeatInterval = &dur
}
}
func (n NotificationSettingsMutators) WithGroupBy(groupBy ...string) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
ns.GroupBy = groupBy
}
}
func (n NotificationSettingsMutators) WithMuteTimeIntervals(muteTimeIntervals ...string) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
ns.MuteTimeIntervals = muteTimeIntervals
}
}
// Silences
// CopySilenceWith creates a deep copy of Silence and then applies mutators to it.
func CopySilenceWith(s Silence, mutators ...Mutator[Silence]) Silence {
c := CopySilence(s)
for _, mutator := range mutators {
mutator(&c)
}
return c
}
// CopySilence creates a deep copy of Silence.
func CopySilence(s Silence) Silence {
c := Silence{
Silence: amv2.Silence{},
}
if s.ID != nil {
c.ID = util.Pointer(*s.ID)
}
if s.Status != nil {
c.Status = util.Pointer(*s.Status)
}
if s.UpdatedAt != nil {
c.UpdatedAt = util.Pointer(*s.UpdatedAt)
}
if s.Silence.Comment != nil {
c.Silence.Comment = util.Pointer(*s.Silence.Comment)
}
if s.Silence.CreatedBy != nil {
c.Silence.CreatedBy = util.Pointer(*s.Silence.CreatedBy)
}
if s.Silence.EndsAt != nil {
c.Silence.EndsAt = util.Pointer(*s.Silence.EndsAt)
}
if s.Silence.StartsAt != nil {
c.Silence.StartsAt = util.Pointer(*s.Silence.StartsAt)
}
if s.Silence.Matchers != nil {
c.Silence.Matchers = CopyMatchers(s.Silence.Matchers)
}
return c
}
// CopyMatchers creates a deep copy of Matchers.
func CopyMatchers(matchers []*amv2.Matcher) []*amv2.Matcher {
copies := make([]*amv2.Matcher, len(matchers))
for i, m := range matchers {
c := amv2.Matcher{}
if m.IsEqual != nil {
c.IsEqual = util.Pointer(*m.IsEqual)
}
if m.IsRegex != nil {
c.IsRegex = util.Pointer(*m.IsRegex)
}
if m.Name != nil {
c.Name = util.Pointer(*m.Name)
}
if m.Value != nil {
c.Value = util.Pointer(*m.Value)
}
copies[i] = &c
}
return copies
}
// SilenceGen generates Silence using a base and mutators.
func SilenceGen(mutators ...Mutator[Silence]) func() Silence {
return func() Silence {
now := time.Now()
c := Silence{
ID: util.Pointer(util.GenerateShortUID()),
Status: util.Pointer(amv2.SilenceStatus{State: util.Pointer(amv2.SilenceStatusStateActive)}),
UpdatedAt: util.Pointer(strfmt.DateTime(now.Add(time.Minute))),
Silence: amv2.Silence{
Comment: util.Pointer(util.GenerateShortUID()),
CreatedBy: util.Pointer(util.GenerateShortUID()),
StartsAt: util.Pointer(strfmt.DateTime(now.Add(-time.Minute))),
EndsAt: util.Pointer(strfmt.DateTime(now.Add(time.Minute))),
Matchers: []*amv2.Matcher{{Name: util.Pointer(util.GenerateShortUID()), Value: util.Pointer(util.GenerateShortUID()), IsRegex: util.Pointer(false), IsEqual: util.Pointer(true)}},
},
}
for _, mutator := range mutators {
mutator(&c)
}
return c
}
}
var (
SilenceMuts = SilenceMutators{}
)
type SilenceMutators struct{}
func (n SilenceMutators) WithMatcher(name, value string, matchType labels.MatchType) Mutator[Silence] {
return func(s *Silence) {
m := amv2.Matcher{
Name: &name,
Value: &value,
IsRegex: util.Pointer(matchType == labels.MatchRegexp || matchType == labels.MatchNotRegexp),
IsEqual: util.Pointer(matchType == labels.MatchRegexp || matchType == labels.MatchEqual),
}
s.Silence.Matchers = append(s.Silence.Matchers, &m)
}
}
func (n SilenceMutators) WithRuleUID(value string) Mutator[Silence] {
return func(s *Silence) {
name := alertingModels.RuleUIDLabel
m := amv2.Matcher{
Name: &name,
Value: &value,
IsRegex: util.Pointer(false),
IsEqual: util.Pointer(true),
}
for _, matcher := range s.Silence.Matchers {
if isRuleUIDMatcher(*matcher) {
*matcher = m
return
}
}
s.Silence.Matchers = append(s.Silence.Matchers, &m)
}
}
func (n SilenceMutators) Expired() Mutator[Silence] {
return func(s *Silence) {
s.EndsAt = util.Pointer(strfmt.DateTime(time.Now().Add(-time.Minute)))
}
}
func (n SilenceMutators) WithEmptyId() Mutator[Silence] {
return func(s *Silence) {
s.ID = util.Pointer("")
}
}
// Receivers
// CopyReceiverWith creates a deep copy of Receiver and then applies mutators to it.
func CopyReceiverWith(r Receiver, mutators ...Mutator[Receiver]) Receiver {
c := r.Clone()
for _, mutator := range mutators {
mutator(&c)
}
c.Version = c.Fingerprint()
return c
}
// ReceiverGen generates Receiver using a base and mutators.
func ReceiverGen(mutators ...Mutator[Receiver]) func() Receiver {
return func() Receiver {
name := util.GenerateShortUID()
integration := IntegrationGen(IntegrationMuts.WithName(name))()
c := Receiver{
UID: nameToUid(name),
Name: name,
Integrations: []*Integration{&integration},
Provenance: ProvenanceNone,
}
for _, mutator := range mutators {
mutator(&c)
}
c.Version = c.Fingerprint()
return c
}
}
var (
ReceiverMuts = ReceiverMutators{}
)
type ReceiverMutators struct{}
func (n ReceiverMutators) WithName(name string) Mutator[Receiver] {
return func(r *Receiver) {
r.Name = name
r.UID = nameToUid(name)
}
}
func (n ReceiverMutators) WithProvenance(provenance Provenance) Mutator[Receiver] {
return func(r *Receiver) {
r.Provenance = provenance
}
}
func (n ReceiverMutators) WithValidIntegration(integrationType string) Mutator[Receiver] {
return func(r *Receiver) {
integration := IntegrationGen(IntegrationMuts.WithValidConfig(integrationType))()
r.Integrations = []*Integration{&integration}
}
}
func (n ReceiverMutators) WithInvalidIntegration(integrationType string) Mutator[Receiver] {
return func(r *Receiver) {
integration := IntegrationGen(IntegrationMuts.WithInvalidConfig(integrationType))()
r.Integrations = []*Integration{&integration}
}
}
func (n ReceiverMutators) WithIntegrations(integration ...Integration) Mutator[Receiver] {
return func(r *Receiver) {
integrations := make([]*Integration, len(integration))
for i, v := range integration {
clone := v.Clone()
integrations[i] = &clone
}
r.Integrations = integrations
}
}
func (n ReceiverMutators) Encrypted(fn EncryptFn) Mutator[Receiver] {
return func(r *Receiver) {
_ = r.Encrypt(fn)
}
}
func (n ReceiverMutators) Decrypted(fn DecryptFn) Mutator[Receiver] {
return func(r *Receiver) {
_ = r.Decrypt(fn)
}
}
// Integrations
// CopyIntegrationWith creates a deep copy of Integration and then applies mutators to it.
func CopyIntegrationWith(r Integration, mutators ...Mutator[Integration]) Integration {
c := r.Clone()
for _, mutator := range mutators {
mutator(&c)
}
return c
}
// IntegrationGen generates Integration using a base and mutators.
func IntegrationGen(mutators ...Mutator[Integration]) func() Integration {
return func() Integration {
name := util.GenerateShortUID()
randomIntegrationType, _ := randomMapKey(alertingNotify.AllKnownConfigsForTesting)
c := Integration{
UID: util.GenerateShortUID(),
Name: name,
DisableResolveMessage: rand.Intn(2) == 1,
Settings: make(map[string]any),
SecureSettings: make(map[string]string),
}
IntegrationMuts.WithValidConfig(randomIntegrationType)(&c)
for _, mutator := range mutators {
mutator(&c)
}
return c
}
}
var (
IntegrationMuts = IntegrationMutators{}
Base64Enrypt = func(s string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(s)), nil
}
Base64Decrypt = func(s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
return string(b), err
}
)
type IntegrationMutators struct{}
func (n IntegrationMutators) WithUID(uid string) Mutator[Integration] {
return func(s *Integration) {
s.UID = uid
}
}
func (n IntegrationMutators) WithName(name string) Mutator[Integration] {
return func(s *Integration) {
s.Name = name
}
}
func (n IntegrationMutators) WithValidConfig(integrationType string) Mutator[Integration] {
return func(c *Integration) {
config := alertingNotify.AllKnownConfigsForTesting[integrationType].GetRawNotifierConfig(c.Name)
integrationConfig, _ := IntegrationConfigFromType(integrationType)
c.Config = integrationConfig
var settings map[string]any
_ = json.Unmarshal(config.Settings, &settings)
c.Settings = settings
// Decrypt secure settings over to normal settings.
for k, v := range c.SecureSettings {
decodeValue, _ := base64.StdEncoding.DecodeString(v)
settings[k] = string(decodeValue)
}
}
}
func (n IntegrationMutators) WithInvalidConfig(integrationType string) Mutator[Integration] {
return func(c *Integration) {
integrationConfig, _ := IntegrationConfigFromType(integrationType)
c.Config = integrationConfig
c.Settings = map[string]interface{}{}
c.SecureSettings = map[string]string{}
if integrationType == "webex" {
// Webex passes validation without any settings but should fail with an unparsable URL.
c.Settings["api_url"] = "(*^$*^%!@#$*()"
}
}
}
func (n IntegrationMutators) WithSettings(settings map[string]any) Mutator[Integration] {
return func(c *Integration) {
c.Settings = maps.Clone(settings)
}
}
func (n IntegrationMutators) AddSetting(key string, val any) Mutator[Integration] {
return func(c *Integration) {
c.Settings[key] = val
}
}
func (n IntegrationMutators) WithSecureSettings(secureSettings map[string]string) Mutator[Integration] {
return func(r *Integration) {
r.SecureSettings = maps.Clone(secureSettings)
}
}
func (n IntegrationMutators) AddSecureSetting(key, val string) Mutator[Integration] {
return func(r *Integration) {
r.SecureSettings[key] = val
}
}
func randomMapKey[K comparable, V any](m map[K]V) (K, V) {
randIdx := rand.Intn(len(m))
i := 0
for key, val := range m {
if i == randIdx {
return key, val
}
i++
}
return *new(K), *new(V)
}
func ConvertToRecordingRule(rule *AlertRule) {
if rule.Record == nil {
rule.Record = &Record{}
}
if rule.Record.From == "" {
rule.Record.From = rule.Condition
}
if rule.Record.Metric == "" {
rule.Record.Metric = fmt.Sprintf("some_metric_%s", util.GenerateShortUID())
}
rule.Condition = ""
rule.NoDataState = ""
rule.ExecErrState = ""
rule.For = 0
rule.NotificationSettings = nil
}
func nameToUid(name string) string { // Avoid legacy_storage.NameToUid import cycle.
return base64.RawURLEncoding.EncodeToString([]byte(name))
}