Files
grafana/pkg/tests/apis/alerting/rules/compat/alertrule_test.go
T
Peter Štibraný 7604653fd8 Change testing.Short() check with SkipIntegrationTestInShortMode check. (#112442)
* Change testing.Short() check with SkipIntegrationTestInShortMode check.
2025-10-20 09:40:38 +02:00

417 lines
16 KiB
Go

package compat
import (
"context"
"encoding/json"
"fmt"
"testing"
"time"
"github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis/alerting/rules/common"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/testutil"
prom_model "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestIntegrationAlertRuleCompatCreateViaK8s(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := common.GetTestHelper(t)
k8sClient := common.NewAlertRuleClient(t, helper.Org1.Admin)
legacyClient := alerting.NewAlertingLegacyAPIClient(helper.GetListenerAddress(), "admin", "admin")
// Ensure the old provisioning API is enabled
allRules, status, _ := legacyClient.GetAllRulesWithStatus(t)
require.Equal(t, 200, status)
require.NotNil(t, allRules)
// Create test folder first
common.CreateTestFolder(t, helper, "test-folder")
rule := ngmodels.RuleGen.With(
ngmodels.RuleMuts.WithUniqueUID(),
ngmodels.RuleMuts.WithUniqueTitle(),
ngmodels.RuleMuts.WithNamespaceUID("test-folder"),
ngmodels.RuleMuts.WithGroupName("test-group"),
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
).Generate()
alertRule := &v0alpha1.AlertRule{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
Annotations: map[string]string{
"grafana.app/folder": "test-folder",
// use provenance api to allow use of the provisioning api
"grafana.com/provenance": string(ngmodels.ProvenanceAPI),
},
},
Spec: v0alpha1.AlertRuleSpec{
Title: rule.Title,
Expressions: v0alpha1.AlertRuleExpressionMap{
"A": {
QueryType: util.Pointer(rule.Data[0].QueryType),
DatasourceUID: util.Pointer(v0alpha1.AlertRuleDatasourceUID(rule.Data[0].DatasourceUID)),
Model: rule.Data[0].Model,
Source: util.Pointer(true),
RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{
From: v0alpha1.AlertRulePromDurationWMillis("5m"),
To: v0alpha1.AlertRulePromDurationWMillis("0s"),
},
},
},
Trigger: v0alpha1.AlertRuleIntervalTrigger{
Interval: v0alpha1.AlertRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
},
},
}
created, err := k8sClient.Create(ctx, alertRule, v1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, created)
t.Run("should be able to use the provisioning API with this rule", func(t *testing.T) {
retrievedRule, status, _ := legacyClient.GetProvisioningAlertRule(t, created.Name)
require.NotNil(t, retrievedRule)
require.Equal(t, 200, status)
require.Equal(t, created.Spec.Title, retrievedRule.Title)
require.Equal(t, "A", retrievedRule.Condition)
require.Equal(t, "A", retrievedRule.Data[0].RefID)
model := map[string]interface{}{}
err := json.Unmarshal(retrievedRule.Data[0].Model, &model)
require.NoError(t, err)
require.NotNil(t, model)
expectedModel, ok := created.Spec.Expressions["A"].Model.(map[string]interface{})
if !ok {
t.Fatalf("Expected model to be a map[string]interface{}, got %T", created.Spec.Expressions["A"].Model)
}
for k, v := range expectedModel {
require.EqualValues(t, v, model[k], "Model field %s should match", k)
}
require.Equal(t, created.Annotations["grafana.app/folder"], retrievedRule.FolderUID)
// get the group to get the interval
group, status, _ := legacyClient.GetRuleGroupProvisioning(t, retrievedRule.FolderUID, retrievedRule.RuleGroup)
require.NotNil(t, group)
require.Equal(t, 200, status)
parsedDuration, err := prom_model.ParseDuration(fmt.Sprintf("%ds", group.Interval))
require.NoError(t, err)
require.Equal(t, string(created.Spec.Trigger.Interval), parsedDuration.String())
// try to update the rule group title via the provisioning api
groupNameUpdate := group
groupNameUpdate.Title = "New Group Name"
// this should be rejected
_, status, data := legacyClient.CreateOrUpdateRuleGroupProvisioning(t, groupNameUpdate)
require.Equalf(t, 400, status, "Expected status 400 when changing group name, got %d. Data: %s", status, data)
// verify the group name didn't
retrievedRule, status, _ = legacyClient.GetProvisioningAlertRule(t, created.Name)
require.NotNil(t, retrievedRule)
require.Equal(t, 200, status)
require.Equal(t, group.Title, retrievedRule.RuleGroup)
// successful update
groupUpdate := group
groupUpdate.Rules[0].Title = "Updated " + groupUpdate.Rules[0].Title
updatedRule, status, _ := legacyClient.CreateOrUpdateRuleGroupProvisioning(t, groupUpdate)
require.Equal(t, 200, status)
require.NotNil(t, updatedRule)
require.Equal(t, groupUpdate.Rules[0].Title, updatedRule.Rules[0].Title)
// verify the change is reflected in k8s
k8sRetrievedRule, err := k8sClient.Get(ctx, created.Name, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, k8sRetrievedRule)
require.Equal(t, updatedRule.Rules[0].Title, k8sRetrievedRule.Spec.Title)
// delete the rule group via the provisioning API
status, body := legacyClient.DeleteRulesGroupProvisioning(t, group.FolderUID, group.Title)
require.Equalf(t, 204, status, "Expected status 200 when deleting rule group, got %d. Body: %s", status, body)
// verify the rule is deleted in k8s
_, err = k8sClient.Get(ctx, created.Name, v1.GetOptions{})
require.Error(t, err, "Expected error when getting deleted rule")
require.Contains(t, err.Error(), "not found")
})
}
func TestIntegrationAlertRuleCompatCreateViaProvisioning(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := common.GetTestHelper(t)
k8sClient := common.NewAlertRuleClient(t, helper.Org1.Admin)
legacyClient := alerting.NewAlertingLegacyAPIClient(helper.GetListenerAddress(), "admin", "admin")
// Ensure the old provisioning API is enabled
allRules, status, _ := legacyClient.GetAllRulesWithStatus(t)
require.Equal(t, 200, status)
require.NotNil(t, allRules)
// Create test folder first
common.CreateTestFolder(t, helper, "test-folder")
rule := ngmodels.RuleGen.With(
ngmodels.RuleMuts.WithUniqueUID(),
ngmodels.RuleMuts.WithUniqueTitle(),
ngmodels.RuleMuts.WithNamespaceUID("test-folder"),
ngmodels.RuleMuts.WithGroupName("test-group"),
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
).GenerateMany(2)
ruleGroup := apimodels.AlertRuleGroup{
Title: "test-group",
FolderUID: "test-folder",
Interval: rule[0].IntervalSeconds,
Rules: []apimodels.ProvisionedAlertRule{
{
UID: rule[0].UID,
Title: rule[0].Title,
OrgID: 1,
Data: []apimodels.AlertQuery{
{
RefID: "A",
DatasourceUID: rule[0].Data[0].DatasourceUID,
Model: rule[0].Data[0].Model,
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Minute),
To: apimodels.Duration(0),
},
},
},
Condition: "A",
FolderUID: "test-folder",
NoDataState: apimodels.NoDataState(rule[0].NoDataState),
ExecErrState: apimodels.ExecutionErrorState(rule[0].ExecErrState),
},
{
UID: rule[1].UID,
Title: rule[1].Title,
OrgID: 1,
Data: []apimodels.AlertQuery{
{
RefID: "A",
DatasourceUID: rule[1].Data[0].DatasourceUID,
Model: rule[1].Data[0].Model,
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Minute),
To: apimodels.Duration(0),
},
},
},
Condition: "A",
FolderUID: "test-folder",
NoDataState: apimodels.NoDataState(rule[1].NoDataState),
ExecErrState: apimodels.ExecutionErrorState(rule[1].ExecErrState),
},
},
}
created, status, body := legacyClient.CreateOrUpdateRuleGroupProvisioning(t, ruleGroup)
require.Equalf(t, 200, status, "Expected status 200, got %d. Response body: %s", status, body)
require.NotNil(t, created)
t.Run("should be able to use the k8s API with these rules", func(t *testing.T) {
for i, r := range created.Rules {
retrievedRule, err := k8sClient.Get(ctx, r.UID, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, retrievedRule)
require.Equal(t, r.Title, retrievedRule.Spec.Title)
require.NotNil(t, retrievedRule.Spec.Expressions[r.Data[0].RefID].Source)
require.True(t, *retrievedRule.Spec.Expressions[r.Data[0].RefID].Source)
require.Equal(t, r.FolderUID, retrievedRule.Annotations["grafana.app/folder"])
require.Equal(t, created.Title, retrievedRule.Labels[v0alpha1.GroupLabelKey])
require.Equal(t, fmt.Sprintf("%d", i), retrievedRule.Labels[v0alpha1.GroupIndexLabelKey])
require.Equal(t, ngmodels.ProvenanceAPI, ngmodels.Provenance(retrievedRule.GetProvenanceStatus()))
require.EqualValues(t, r.Data[0].DatasourceUID, *retrievedRule.Spec.Expressions["A"].DatasourceUID)
expectedDuration, err := prom_model.ParseDuration(fmt.Sprintf("%ds", created.Interval))
require.NoError(t, err)
require.Equal(t, expectedDuration.String(), string(retrievedRule.Spec.Trigger.Interval))
expectedModel := map[string]interface{}{}
err = json.Unmarshal(r.Data[0].Model, &expectedModel)
require.NoError(t, err)
require.NotNil(t, expectedModel)
retrievedModel, ok := retrievedRule.Spec.Expressions["A"].Model.(map[string]interface{})
if !ok {
t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Expressions["A"].Model)
}
for k, v := range expectedModel {
require.EqualValues(t, v, retrievedModel[k], "Model field %s should match", k)
}
require.EqualValues(t, r.NoDataState, retrievedRule.Spec.NoDataState)
require.EqualValues(t, r.ExecErrState, retrievedRule.Spec.ExecErrState)
// change the title of the rule and check that it's updated in k8s and provisioning API
updatedRule := retrievedRule.DeepCopy()
updatedRule.Spec.Title = "Updated " + retrievedRule.Spec.Title
updatedRule, err = k8sClient.Update(ctx, updatedRule, v1.UpdateOptions{})
require.NoError(t, err)
require.NotNil(t, updatedRule)
retrievedRule, err = k8sClient.Get(ctx, r.UID, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, retrievedRule)
require.Equal(t, updatedRule.Spec.Title, retrievedRule.Spec.Title)
provisioningRetrievedRule, status, _ := legacyClient.GetProvisioningAlertRule(t, r.UID)
require.NotNil(t, provisioningRetrievedRule)
require.Equal(t, 200, status)
require.Equal(t, updatedRule.Spec.Title, provisioningRetrievedRule.Title)
// delete the rule via k8s
err = k8sClient.Delete(ctx, retrievedRule.Name, v1.DeleteOptions{})
require.NoError(t, err)
// check that the rule is deleted in the provisioning API
_, status, body := legacyClient.GetProvisioningAlertRule(t, r.UID)
require.Equal(t, 404, status, "Expected status 404, got %d. Response body: %s", status, body)
// check that the rule is deleted in k8s
_, err = k8sClient.Get(ctx, r.UID, v1.GetOptions{})
require.Error(t, err, "Expected error when getting deleted rule")
require.Contains(t, err.Error(), "not found", "Expected 'not found' error, got %s", err.Error())
}
})
}
func TestIntegrationAlertRuleCompatCreateViaProvisioningChangeGroupInK8s(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := common.GetTestHelper(t)
k8sClient := common.NewAlertRuleClient(t, helper.Org1.Admin)
legacyClient := alerting.NewAlertingLegacyAPIClient(helper.GetListenerAddress(), "admin", "admin")
// Ensure the old provisioning API is enabled
allRules, status, _ := legacyClient.GetAllRulesWithStatus(t)
require.Equal(t, 200, status)
require.NotNil(t, allRules)
// Create test folder first
common.CreateTestFolder(t, helper, "test-folder")
rule := ngmodels.RuleGen.With(
ngmodels.RuleMuts.WithUniqueUID(),
ngmodels.RuleMuts.WithUniqueTitle(),
ngmodels.RuleMuts.WithNamespaceUID("test-folder"),
ngmodels.RuleMuts.WithGroupName("test-group"),
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
).GenerateMany(2)
ruleGroup := apimodels.AlertRuleGroup{
Title: "test-group",
FolderUID: "test-folder",
Interval: rule[0].IntervalSeconds,
Rules: []apimodels.ProvisionedAlertRule{
{
UID: rule[0].UID,
Title: rule[0].Title,
OrgID: 1,
Data: []apimodels.AlertQuery{
{
RefID: "X",
DatasourceUID: rule[0].Data[0].DatasourceUID,
Model: rule[0].Data[0].Model,
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Minute),
To: apimodels.Duration(0),
},
},
},
Condition: "X",
FolderUID: "test-folder",
NoDataState: apimodels.NoDataState(rule[0].NoDataState),
ExecErrState: apimodels.ExecutionErrorState(rule[0].ExecErrState),
},
{
UID: rule[1].UID,
Title: rule[1].Title,
OrgID: 1,
Data: []apimodels.AlertQuery{
{
RefID: "X",
DatasourceUID: rule[1].Data[0].DatasourceUID,
Model: rule[1].Data[0].Model,
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Minute),
To: apimodels.Duration(0),
},
},
},
Condition: "X",
FolderUID: "test-folder",
NoDataState: apimodels.NoDataState(rule[1].NoDataState),
ExecErrState: apimodels.ExecutionErrorState(rule[1].ExecErrState),
},
},
}
created, status, body := legacyClient.CreateOrUpdateRuleGroupProvisioning(t, ruleGroup)
require.Equalf(t, 200, status, "Expected status 200, got %d. Response body: %s", status, body)
require.NotNil(t, created)
t.Run("should be able to use the k8s API to change the group for a rule", func(t *testing.T) {
for i, r := range created.Rules {
retrievedRule, err := k8sClient.Get(ctx, r.UID, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, retrievedRule)
require.Equal(t, r.Title, retrievedRule.Spec.Title)
require.NotNil(t, retrievedRule.Spec.Expressions[r.Data[0].RefID].Source)
require.True(t, *retrievedRule.Spec.Expressions[r.Data[0].RefID].Source)
require.Equal(t, r.FolderUID, retrievedRule.Annotations["grafana.app/folder"])
require.Equal(t, created.Title, retrievedRule.Labels[v0alpha1.GroupLabelKey])
require.Equal(t, fmt.Sprintf("%d", i), retrievedRule.Labels[v0alpha1.GroupIndexLabelKey])
require.Equal(t, ngmodels.ProvenanceAPI, ngmodels.Provenance(retrievedRule.GetProvenanceStatus()))
require.EqualValues(t, r.Data[0].DatasourceUID, *retrievedRule.Spec.Expressions["X"].DatasourceUID)
expectedDuration, err := prom_model.ParseDuration(fmt.Sprintf("%ds", created.Interval))
require.NoError(t, err)
require.Equal(t, expectedDuration.String(), string(retrievedRule.Spec.Trigger.Interval))
expectedModel := map[string]interface{}{}
err = json.Unmarshal(r.Data[0].Model, &expectedModel)
require.NoError(t, err)
require.NotNil(t, expectedModel)
retrievedModel, ok := retrievedRule.Spec.Expressions["X"].Model.(map[string]interface{})
if !ok {
t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Expressions["X"].Model)
}
for k, v := range expectedModel {
require.EqualValues(t, v, retrievedModel[k], "Model field %s should match", k)
}
require.EqualValues(t, r.NoDataState, retrievedRule.Spec.NoDataState)
require.EqualValues(t, r.ExecErrState, retrievedRule.Spec.ExecErrState)
// - change group should be allowed and reflected in the provisioning api
updatedRule := retrievedRule.DeepCopy()
updatedRule.Labels[v0alpha1.GroupLabelKey] = "new-group"
updatedRule, err = k8sClient.Update(ctx, updatedRule, v1.UpdateOptions{})
require.NoError(t, err)
require.NotNil(t, updatedRule)
// verify the change is reflected in k8s
retrievedRule, err = k8sClient.Get(ctx, r.UID, v1.GetOptions{})
require.NoError(t, err)
require.NotNil(t, retrievedRule)
// verify the group label changed
require.Equal(t, "new-group", retrievedRule.Labels[v0alpha1.GroupLabelKey])
// verify the change is reflected in the provisioning API
provisioningRetrievedRule, status, _ := legacyClient.GetProvisioningAlertRule(t, r.UID)
require.NotNil(t, provisioningRetrievedRule)
require.Equal(t, 200, status)
// verify the group label changed
require.Equal(t, "new-group", provisioningRetrievedRule.RuleGroup)
}
})
}