cb7abbaa0f
This renames `data` to `expressions` for clarity in the rules apis. Also makes certain fields that are redundant optional in the case of pure expressions, so that users don't have to specify them when they are not needed (e.g. not datasource queries).
422 lines
16 KiB
Go
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,
|
|
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) {
|
|
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.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) {
|
|
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.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)
|
|
}
|
|
})
|
|
}
|