Files
grafana/pkg/tests/apis/alerting/rules/compat/alertrule_test.go
T
Moustafa Baiou ca8324e62a Alerting: Add support for alpha rules apis in legacy storage
Rules created in the new api makes the rule have no group in the database, but the rule is returned in the old group api with a sentinel group name formatted with the rule uid for compatiblity with the old api.
This makes the UI continue to work with the rules without a group, and the ruler will continue to work with the rules without a group.

Rules are not allowed to be created in the provisioning api with a NoGroup sentinel mask, but NoGroup rules can be manipulated through both the new and old apis.

Co-authored-by: William Wernert <william.wernert@grafana.com>
2025-09-10 09:30:56 -04:00

422 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"
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) {
if testing.Short() {
t.Skip("skipping integration test")
}
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,
Data: map[string]v0alpha1.AlertRuleQuery{
"A": {
QueryType: "query",
DatasourceUID: 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.Data["A"].Model.(map[string]interface{})
if !ok {
t.Fatalf("Expected model to be a map[string]interface{}, got %T", created.Spec.Data["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) {
if testing.Short() {
t.Skip("skipping integration test")
}
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.Data[r.Data[0].RefID].Source)
require.True(t, *retrievedRule.Spec.Data[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.Data["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.Data["A"].Model.(map[string]interface{})
if !ok {
t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Data["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) {
if testing.Short() {
t.Skip("skipping integration test")
}
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.Data[r.Data[0].RefID].Source)
require.True(t, *retrievedRule.Spec.Data[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.Data["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.Data["X"].Model.(map[string]interface{})
if !ok {
t.Fatalf("Expected model to be a map[string]interface{}, got %T", retrievedRule.Spec.Data["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)
}
})
}