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>
This commit is contained in:
committed by
Moustafa Baiou
co-authored by
William Wernert
parent
0a85a30642
commit
ca8324e62a
@@ -0,0 +1,575 @@
|
||||
package alertrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/tests/apis/alerting/rules/common"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
prom_model "github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
client := common.NewAlertRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// 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()
|
||||
|
||||
newResource := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
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)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
// Test 1: Create with explicit name
|
||||
namedResource := newResource.Copy().(*v0alpha1.AlertRule)
|
||||
namedResource.Name = "explicit-name-rule"
|
||||
namedRule, err := client.Create(ctx, namedResource, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "explicit-name-rule", namedRule.Name)
|
||||
require.NotEmpty(t, namedRule.UID)
|
||||
|
||||
// Test 2: Create without explicit name (auto-generated)
|
||||
autoGenRule, err := client.Create(ctx, newResource, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, autoGenRule.Name)
|
||||
require.NotEmpty(t, autoGenRule.UID)
|
||||
|
||||
// Test 3: Get by identifier
|
||||
retrievedRule, err := client.Get(ctx, autoGenRule.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, autoGenRule.Name, retrievedRule.Name)
|
||||
require.Equal(t, newResource.Spec.Title, retrievedRule.Spec.Title)
|
||||
|
||||
// Test 4: Update (should preserve name)
|
||||
updatedRule := retrievedRule.Copy().(*v0alpha1.AlertRule)
|
||||
updatedRule.Spec.Title = "updated-title"
|
||||
|
||||
finalRule, err := client.Update(ctx, updatedRule, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated-title", finalRule.Spec.Title)
|
||||
require.Equal(t, retrievedRule.Name, finalRule.Name, "Update should preserve the resource name")
|
||||
require.NotEqual(t, retrievedRule.ResourceVersion, finalRule.ResourceVersion, "Update should change the resource version")
|
||||
|
||||
// Test 5: Verify the update persisted
|
||||
finalRetrieved, err := client.Get(ctx, finalRule.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, finalRule.Spec.Title, finalRetrieved.Spec.Title)
|
||||
require.Equal(t, finalRule.Name, finalRetrieved.Name)
|
||||
require.Equal(t, finalRule.ResourceVersion, finalRetrieved.ResourceVersion)
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, client.Delete(ctx, namedRule.Name, v1.DeleteOptions{}))
|
||||
require.NoError(t, client.Delete(ctx, finalRule.Name, v1.DeleteOptions{}))
|
||||
}
|
||||
|
||||
// TestIntegrationResourcePermissions is skipped for now as access control is handled in the service layer
|
||||
func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
t.Skip("Access control tests skipped - handled in service layer")
|
||||
}
|
||||
|
||||
// TestIntegrationAccessControl tests basic access control functionality
|
||||
// Access control is primarily handled in the service layer, so this test focuses on basic CRUD operations
|
||||
func TestIntegrationAccessControl(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
// Test with admin user for basic functionality
|
||||
adminClient := common.NewAlertRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// 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",
|
||||
},
|
||||
},
|
||||
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)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("admin should be able to create rule", func(t *testing.T) {
|
||||
created, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
require.Equal(t, alertRule.Spec.Title, created.Spec.Title)
|
||||
|
||||
// Cleanup
|
||||
defer func() {
|
||||
_ = adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
t.Run("admin should be able to read rule", func(t *testing.T) {
|
||||
read, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, created.Spec.Title, read.Spec.Title)
|
||||
})
|
||||
|
||||
t.Run("admin should be able to update rule", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.AlertRule)
|
||||
updated.Spec.Title = "updated-title"
|
||||
|
||||
result, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated-title", result.Spec.Title)
|
||||
})
|
||||
|
||||
t.Run("admin should be able to delete rule", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationCRUD(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
adminClient := common.NewAlertRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// Create test folder first
|
||||
common.CreateTestFolder(t, helper, "test-folder")
|
||||
|
||||
baseGen := 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),
|
||||
)
|
||||
|
||||
t.Run("should be able to create and read rule", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
"grafana.com/provenance": "",
|
||||
},
|
||||
},
|
||||
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)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
|
||||
t.Run("should be able to read what it is created", func(t *testing.T) {
|
||||
get, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, alertRule.Spec.Title, get.Spec.Title)
|
||||
|
||||
createdDuration, err := prom_model.ParseDuration(string(alertRule.Spec.Trigger.Interval))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, createdDuration.String(), string(get.Spec.Trigger.Interval))
|
||||
|
||||
provenance := get.GetProvenanceStatus()
|
||||
require.Equal(t, v0alpha1.ProvenanceStatusNone, provenance)
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, adminClient.Delete(ctx, created.Name, v1.DeleteOptions{}))
|
||||
})
|
||||
|
||||
t.Run("should fail to create rule with invalid provenance status", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
"grafana.com/provenance": "invalid",
|
||||
},
|
||||
},
|
||||
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)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
_, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.Error(t, err, "Creating invalid rule should fail")
|
||||
})
|
||||
|
||||
t.Run("should fail to create rule with invalid config", func(t *testing.T) {
|
||||
invalidRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.AlertRuleSpec{
|
||||
Title: "invalid-rule",
|
||||
Data: map[string]v0alpha1.AlertRuleQuery{}, // Empty data should fail
|
||||
Trigger: v0alpha1.AlertRuleIntervalTrigger{
|
||||
Interval: "30",
|
||||
},
|
||||
NoDataState: "NoData",
|
||||
ExecErrState: "Error",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := adminClient.Create(ctx, invalidRule, v1.CreateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result")
|
||||
// The validation happens at the service level, so we just need to verify it fails
|
||||
require.Error(t, err, "Creating invalid rule should fail")
|
||||
})
|
||||
|
||||
t.Run("should not be able to add rule to group", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
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)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
|
||||
get, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, created.Spec.Title, get.Spec.Title)
|
||||
|
||||
// Attempt to update the group name via a patch (should fail)
|
||||
update := get
|
||||
if update.Labels == nil {
|
||||
update.Labels = map[string]string{}
|
||||
}
|
||||
update.Labels[v0alpha1.GroupLabelKey] = "new-group-name"
|
||||
_, err = adminClient.Update(ctx, update, v1.UpdateOptions{})
|
||||
require.Error(t, err, "Updating the group name should fail")
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, adminClient.Delete(ctx, created.Name, v1.DeleteOptions{}))
|
||||
})
|
||||
t.Run("should not be able to create rule without any source query", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
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,
|
||||
RelativeTimeRange: &v0alpha1.AlertRuleRelativeTimeRange{
|
||||
From: v0alpha1.AlertRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.AlertRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.AlertRuleIntervalTrigger{
|
||||
Interval: v0alpha1.AlertRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.ErrorContains(t, err, "no query marked as source")
|
||||
require.Nil(t, created)
|
||||
})
|
||||
t.Run("should not be able to create rule with interval less than base", func(t *testing.T) {
|
||||
rule := baseGen.With(
|
||||
ngmodels.RuleMuts.WithInterval(time.Duration(1) * time.Second),
|
||||
).Generate()
|
||||
|
||||
alertRule := &v0alpha1.AlertRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
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)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.ErrorContains(t, err, "invalid alert rule")
|
||||
require.Nil(t, created)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationPatch(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
adminClient := common.NewAlertRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// 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",
|
||||
},
|
||||
},
|
||||
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)),
|
||||
},
|
||||
NoDataState: string(rule.NoDataState),
|
||||
ExecErrState: string(rule.ExecErrState),
|
||||
},
|
||||
}
|
||||
|
||||
current, err := adminClient.Create(ctx, alertRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, current)
|
||||
|
||||
t.Run("should patch with json patch", func(t *testing.T) {
|
||||
current, err := adminClient.Get(ctx, current.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
patch := []map[string]any{
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/spec/title",
|
||||
"value": "patched-title",
|
||||
},
|
||||
}
|
||||
|
||||
patchData, err := json.Marshal(patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "patched-title", result.Spec.Title)
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, adminClient.Delete(ctx, current.Name, v1.DeleteOptions{}))
|
||||
}
|
||||
|
||||
func TestIntegrationBasicAPI(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
client := common.NewAlertRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
t.Run("should be able to list rules", func(t *testing.T) {
|
||||
list, err := client.List(ctx, v1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
// Should at least be able to list, even if empty
|
||||
require.NotNil(t, list)
|
||||
})
|
||||
|
||||
t.Run("should handle get of non-existent rule", func(t *testing.T) {
|
||||
_, err := client.Get(ctx, "non-existent", v1.GetOptions{})
|
||||
// The API might return different error types, so just check that it's an error
|
||||
require.Error(t, err)
|
||||
t.Logf("Got error: %s", err)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
folders "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/tests/apis"
|
||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
)
|
||||
|
||||
func NewAlertRuleClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.AlertRule, v0alpha1.AlertRuleList] {
|
||||
t.Helper()
|
||||
|
||||
client, err := dynamic.NewForConfig(user.NewRestConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
return &apis.TypedClient[v0alpha1.AlertRule, v0alpha1.AlertRuleList]{
|
||||
Client: client.Resource(
|
||||
schema.GroupVersionResource{
|
||||
Group: v0alpha1.AlertRuleKind().Group(),
|
||||
Version: v0alpha1.AlertRuleKind().Version(),
|
||||
Resource: v0alpha1.AlertRuleKind().Plural(),
|
||||
}).Namespace("default"),
|
||||
}
|
||||
}
|
||||
|
||||
func NewRecordingRuleClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.RecordingRule, v0alpha1.RecordingRuleList] {
|
||||
t.Helper()
|
||||
|
||||
client, err := dynamic.NewForConfig(user.NewRestConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
return &apis.TypedClient[v0alpha1.RecordingRule, v0alpha1.RecordingRuleList]{
|
||||
Client: client.Resource(
|
||||
schema.GroupVersionResource{
|
||||
Group: v0alpha1.RecordingRuleKind().Group(),
|
||||
Version: v0alpha1.RecordingRuleKind().Version(),
|
||||
Resource: v0alpha1.RecordingRuleKind().Plural(),
|
||||
}).Namespace("default"),
|
||||
}
|
||||
}
|
||||
|
||||
func NewFolderClient(t *testing.T, user apis.User) *apis.TypedClient[folders.Folder, folders.FolderList] {
|
||||
t.Helper()
|
||||
|
||||
client, err := dynamic.NewForConfig(user.NewRestConfig())
|
||||
require.NoError(t, err)
|
||||
|
||||
return &apis.TypedClient[folders.Folder, folders.FolderList]{
|
||||
Client: client.Resource(
|
||||
schema.GroupVersionResource{
|
||||
Group: folders.FolderKind().Group(),
|
||||
Version: folders.FolderKind().Version(),
|
||||
Resource: folders.FolderKind().Plural(),
|
||||
}).Namespace("default"),
|
||||
}
|
||||
}
|
||||
|
||||
func GetTestHelper(t *testing.T) *apis.K8sTestHelper {
|
||||
return apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
EnableFeatureToggles: []string{
|
||||
"kubernetesAlertingRules",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func CreateTestFolder(t *testing.T, helper *apis.K8sTestHelper, folderUID string) {
|
||||
ctx := context.Background()
|
||||
folderClient := NewFolderClient(t, helper.Org1.Admin)
|
||||
|
||||
folder := &folders.Folder{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: folderUID,
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: folders.FolderSpec{
|
||||
Title: "Test Folder",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := folderClient.Create(ctx, folder, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,421 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
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/tests/testsuite"
|
||||
"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 TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestIntegrationRecordingRuleCompatCreateViaK8s(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
k8sClient := common.NewRecordingRuleClient(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.WithAllRecordingRules(),
|
||||
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
|
||||
).Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
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.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := k8sClient.Create(ctx, recordingRule, 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, created.Spec.Metric, retrievedRule.Record.Metric)
|
||||
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 TestIntegrationRecordingRuleCompatCreateViaProvisioning(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
k8sClient := common.NewRecordingRuleClient(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.WithAllRecordingRules(),
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
Record: &apimodels.Record{
|
||||
Metric: rule[0].Record.Metric,
|
||||
From: "A",
|
||||
TargetDatasourceUID: rule[0].Record.TargetDatasourceUID,
|
||||
},
|
||||
FolderUID: "test-folder",
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
Record: &apimodels.Record{
|
||||
Metric: rule[1].Record.Metric,
|
||||
From: "A",
|
||||
TargetDatasourceUID: rule[1].Record.TargetDatasourceUID,
|
||||
},
|
||||
FolderUID: "test-folder",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.Equal(t, r.Record.Metric, retrievedRule.Spec.Metric)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 TestIntegrationRecordingRuleCompatCreateViaProvisioningChangeGroupInK8s(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
k8sClient := common.NewRecordingRuleClient(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.WithAllRecordingRules(),
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
Record: &apimodels.Record{
|
||||
Metric: rule[0].Record.Metric,
|
||||
From: "X",
|
||||
TargetDatasourceUID: rule[0].Record.TargetDatasourceUID,
|
||||
},
|
||||
FolderUID: "test-folder",
|
||||
},
|
||||
{
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
Record: &apimodels.Record{
|
||||
Metric: rule[1].Record.Metric,
|
||||
From: "X",
|
||||
TargetDatasourceUID: rule[1].Record.TargetDatasourceUID,
|
||||
},
|
||||
FolderUID: "test-folder",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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.Equal(t, r.Record.Metric, retrievedRule.Spec.Metric)
|
||||
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)
|
||||
}
|
||||
|
||||
// - 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,568 @@
|
||||
package recordingrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/grafana/grafana/apps/alerting/rules/pkg/apis/alerting/v0alpha1"
|
||||
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/tests/apis/alerting/rules/common"
|
||||
"github.com/grafana/grafana/pkg/tests/testsuite"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
prom_model "github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
func TestIntegrationResourceIdentifier(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
client := common.NewRecordingRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// 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.WithAllRecordingRules(),
|
||||
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
|
||||
).Generate()
|
||||
|
||||
newResource := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Test 1: Create with explicit name
|
||||
namedResource := newResource.Copy().(*v0alpha1.RecordingRule)
|
||||
namedResource.Name = "explicit-name-recording-rule"
|
||||
namedRule, err := client.Create(ctx, namedResource, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "explicit-name-recording-rule", namedRule.Name)
|
||||
require.NotEmpty(t, namedRule.UID)
|
||||
|
||||
// Test 2: Create without explicit name (auto-generated)
|
||||
autoGenRule, err := client.Create(ctx, newResource, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, autoGenRule.Name)
|
||||
require.NotEmpty(t, autoGenRule.UID)
|
||||
|
||||
// Test 3: Get by identifier
|
||||
retrievedRule, err := client.Get(ctx, autoGenRule.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, autoGenRule.Name, retrievedRule.Name)
|
||||
require.Equal(t, newResource.Spec.Title, retrievedRule.Spec.Title)
|
||||
|
||||
// Test 4: Update (should preserve name)
|
||||
updatedRule := retrievedRule.Copy().(*v0alpha1.RecordingRule)
|
||||
updatedRule.Spec.Title = "updated-recording-rule-title"
|
||||
|
||||
finalRule, err := client.Update(ctx, updatedRule, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated-recording-rule-title", finalRule.Spec.Title)
|
||||
require.Equal(t, retrievedRule.Name, finalRule.Name, "Update should preserve the resource name")
|
||||
// Note: Recording rule backend doesn't implement ResourceVersion, unlike alert rules
|
||||
|
||||
// Test 5: Verify the update persisted
|
||||
finalRetrieved, err := client.Get(ctx, finalRule.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, finalRule.Spec.Title, finalRetrieved.Spec.Title)
|
||||
require.Equal(t, finalRule.Name, finalRetrieved.Name)
|
||||
require.Equal(t, finalRule.ResourceVersion, finalRetrieved.ResourceVersion)
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, client.Delete(ctx, namedRule.Name, v1.DeleteOptions{}))
|
||||
require.NoError(t, client.Delete(ctx, finalRule.Name, v1.DeleteOptions{}))
|
||||
}
|
||||
|
||||
// TestIntegrationResourcePermissions is skipped for now as access control is handled in the service layer
|
||||
func TestIntegrationResourcePermissions(t *testing.T) {
|
||||
t.Skip("Access control tests skipped - handled in service layer")
|
||||
}
|
||||
|
||||
// TestIntegrationAccessControl tests basic access control functionality
|
||||
// Access control is primarily handled in the service layer, so this test focuses on basic CRUD operations
|
||||
func TestIntegrationAccessControl(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
// Test with admin user for basic functionality
|
||||
adminClient := common.NewRecordingRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// 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.WithAllRecordingRules(),
|
||||
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
|
||||
).Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("admin should be able to create recording rule", func(t *testing.T) {
|
||||
created, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
require.Equal(t, recordingRule.Spec.Title, created.Spec.Title)
|
||||
|
||||
// Cleanup
|
||||
defer func() {
|
||||
_ = adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
}()
|
||||
|
||||
t.Run("admin should be able to read recording rule", func(t *testing.T) {
|
||||
read, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, created.Spec.Title, read.Spec.Title)
|
||||
})
|
||||
|
||||
t.Run("admin should be able to update recording rule", func(t *testing.T) {
|
||||
updated := created.Copy().(*v0alpha1.RecordingRule)
|
||||
updated.Spec.Title = "updated-title"
|
||||
|
||||
result, err := adminClient.Update(ctx, updated, v1.UpdateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "updated-title", result.Spec.Title)
|
||||
})
|
||||
|
||||
t.Run("admin should be able to delete recording rule", func(t *testing.T) {
|
||||
err := adminClient.Delete(ctx, created.Name, v1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationCRUD(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
adminClient := common.NewRecordingRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// Create test folder first
|
||||
common.CreateTestFolder(t, helper, "test-folder")
|
||||
|
||||
baseGen := ngmodels.RuleGen.With(
|
||||
ngmodels.RuleMuts.WithUniqueUID(),
|
||||
ngmodels.RuleMuts.WithUniqueTitle(),
|
||||
ngmodels.RuleMuts.WithNamespaceUID("test-folder"),
|
||||
ngmodels.RuleMuts.WithGroupName("test-group"),
|
||||
ngmodels.RuleMuts.WithAllRecordingRules(),
|
||||
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
|
||||
)
|
||||
|
||||
t.Run("should be able to create and read recording rule", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
"grafana.com/provenance": "",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
|
||||
t.Run("should be able to read what it is created", func(t *testing.T) {
|
||||
get, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, created.Spec.Title, get.Spec.Title)
|
||||
createdDuration, err := prom_model.ParseDuration(string(recordingRule.Spec.Trigger.Interval))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, createdDuration.String(), string(get.Spec.Trigger.Interval))
|
||||
|
||||
provenance := get.GetProvenanceStatus()
|
||||
require.Equal(t, v0alpha1.ProvenanceStatusNone, provenance)
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, adminClient.Delete(ctx, created.Name, v1.DeleteOptions{}))
|
||||
})
|
||||
|
||||
t.Run("should fail to create recording rule with invalid provenance status", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
"grafana.com/provenance": "invalid",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.Error(t, err, "Creating invalid rule should fail")
|
||||
})
|
||||
|
||||
t.Run("should fail to create recording rule with invalid config", func(t *testing.T) {
|
||||
invalidRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: "invalid-recording-rule",
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{}, // Empty data should fail
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: "30s",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := adminClient.Create(ctx, invalidRule, v1.CreateOptions{})
|
||||
require.Errorf(t, err, "Expected error but got successful result")
|
||||
// The validation happens at the service level, so we just need to verify it fails
|
||||
require.Error(t, err, "Creating invalid rule should fail")
|
||||
})
|
||||
|
||||
t.Run("should not be able to add rule to group", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, created)
|
||||
|
||||
get, err := adminClient.Get(ctx, created.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, created.Spec.Title, get.Spec.Title)
|
||||
|
||||
// Attempt to update the group name via a patch (should fail)
|
||||
update := get
|
||||
if update.Labels == nil {
|
||||
update.Labels = map[string]string{}
|
||||
}
|
||||
update.Labels[v0alpha1.GroupLabelKey] = "new-group-name"
|
||||
_, err = adminClient.Update(ctx, update, v1.UpdateOptions{})
|
||||
require.Error(t, err, "Updating the group name should fail")
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, adminClient.Delete(ctx, created.Name, v1.DeleteOptions{}))
|
||||
})
|
||||
|
||||
t.Run("should not be able to create rule without any source query", func(t *testing.T) {
|
||||
rule := baseGen.Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.ErrorContains(t, err, "no query marked as source")
|
||||
require.Nil(t, created)
|
||||
})
|
||||
t.Run("should not be able to create rule with interval less than base", func(t *testing.T) {
|
||||
rule := baseGen.With(
|
||||
ngmodels.RuleMuts.WithInterval(time.Duration(1) * time.Second),
|
||||
).Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.ErrorContains(t, err, "invalid alert rule")
|
||||
require.Nil(t, created)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationPatch(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
|
||||
adminClient := common.NewRecordingRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
// 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.WithAllRecordingRules(),
|
||||
ngmodels.RuleMuts.WithIntervalMatching(time.Duration(10)*time.Second),
|
||||
).Generate()
|
||||
|
||||
recordingRule := &v0alpha1.RecordingRule{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Annotations: map[string]string{
|
||||
"grafana.app/folder": "test-folder",
|
||||
},
|
||||
},
|
||||
Spec: v0alpha1.RecordingRuleSpec{
|
||||
Title: rule.Title,
|
||||
Metric: rule.Record.Metric,
|
||||
Data: map[string]v0alpha1.RecordingRuleQuery{
|
||||
"A": {
|
||||
QueryType: "query",
|
||||
DatasourceUID: v0alpha1.RecordingRuleDatasourceUID(rule.Data[0].DatasourceUID),
|
||||
Model: rule.Data[0].Model,
|
||||
Source: util.Pointer(true),
|
||||
RelativeTimeRange: &v0alpha1.RecordingRuleRelativeTimeRange{
|
||||
From: v0alpha1.RecordingRulePromDurationWMillis("5m"),
|
||||
To: v0alpha1.RecordingRulePromDurationWMillis("0s"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: v0alpha1.RecordingRuleIntervalTrigger{
|
||||
Interval: v0alpha1.RecordingRulePromDuration(fmt.Sprintf("%ds", rule.IntervalSeconds)),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
current, err := adminClient.Create(ctx, recordingRule, v1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, current)
|
||||
|
||||
t.Run("should patch with json patch", func(t *testing.T) {
|
||||
current, err := adminClient.Get(ctx, current.Name, v1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
patch := []map[string]any{
|
||||
{
|
||||
"op": "replace",
|
||||
"path": "/spec/title",
|
||||
"value": "patched-title",
|
||||
},
|
||||
}
|
||||
|
||||
patchData, err := json.Marshal(patch)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := adminClient.Patch(ctx, current.Name, types.JSONPatchType, patchData, v1.PatchOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "patched-title", result.Spec.Title)
|
||||
})
|
||||
|
||||
// Cleanup
|
||||
require.NoError(t, adminClient.Delete(ctx, current.Name, v1.DeleteOptions{}))
|
||||
}
|
||||
|
||||
func TestIntegrationBasicAPI(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
helper := common.GetTestHelper(t)
|
||||
adminClient := common.NewRecordingRuleClient(t, helper.Org1.Admin)
|
||||
|
||||
t.Run("should be able to list rules", func(t *testing.T) {
|
||||
list, err := adminClient.List(ctx, v1.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
// Should at least be able to list, even if empty
|
||||
require.NotNil(t, list)
|
||||
})
|
||||
|
||||
t.Run("should handle get of non-existent rule", func(t *testing.T) {
|
||||
_, err := adminClient.Get(ctx, "non-existent", v1.GetOptions{})
|
||||
// The API might return different error types, so just check that it's an error
|
||||
require.Error(t, err)
|
||||
t.Logf("Got error: %s", err)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user