Files
grafana/pkg/tests/apis/alerting/rules/compat/convertprometheus_retrieval_test.go
T
Moustafa Baiou 70e30df6ce Alerting: Fix support for converted Prometheus rules in app-platform apis (#113648)
* Alerting: Fix support for converted Prometheus rules in app-platform apis

Retrieving converted Prometheus retrieval rules was not supported in the app-platform apis and was throwing a 500 error due to the provenance not being handled properly.

Also adds a test to cover converted Prometheus rules when getting rules.

Closes https://github.com/grafana/alerting-squad/issues/1200

* add test to confirm provenance compatibility
2025-11-10 16:43:38 +00:00

228 lines
8.9 KiB
Go

package compat
import (
"context"
"encoding/json"
"testing"
"time"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
prom_model "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"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/testutil"
)
// TestIntegrationConvertPrometheusAlertRuleRetrieval verifies that an alert rule created
// through the Prometheus (mimirtool compatible) conversion API can be retrieved via the
// new Kubernetes rules API and retains expected fields (title, expressions, interval,
// folder annotation, group labels, and provenance converted_prometheus).
func TestIntegrationConvertPrometheusAlertRuleRetrieval(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := common.GetTestHelper(t)
// K8s client for new rules API
k8sAlertClient := common.NewAlertRuleClient(t, helper.Org1.Admin)
// Legacy + conversion API client
legacyClient := alerting.NewAlertingLegacyAPIClient(helper.GetListenerAddress(), "admin", "admin")
// Ensure legacy API is enabled (sanity)
allRules, status, _ := legacyClient.GetAllRulesWithStatus(t)
require.Equal(t, 200, status)
require.NotNil(t, allRules)
// Create folder that will act as namespace title
folderUID := "test-folder-convert"
common.CreateTestFolder(t, helper, folderUID)
// Build a Prometheus-compatible rule group payload (minimal) for conversion API
// We simulate a simple alert rule with expr and for duration; conversion API will set provenance.
// We pick static values; Grafana rule title will mirror Prometheus 'alert' name.
forDuration := prom_model.Duration(10 * time.Second)
interval20, err := prom_model.ParseDuration("20s")
require.NoError(t, err)
promGroup := apimodels.PrometheusRuleGroup{
Name: "test-group",
Interval: interval20,
Rules: []apimodels.PrometheusRule{
{
Alert: "ConvertedAlertTest",
Expr: "vector(1)", // simple always firing expression
For: &forDuration,
Labels: map[string]string{
"severity": "critical",
},
Annotations: map[string]string{
"summary": "Converted alert rule test",
},
},
},
}
// Create a real Prometheus datasource; conversion API requires a Prometheus-compatible datasource (cannot use __expr__).
ds := legacyClient.CreateDatasource(t, "prometheus")
dsUID := ds.Body.Datasource.UID
require.NotEmpty(t, dsUID, "prometheus datasource UID must not be empty")
defer legacyClient.DeleteDatasource(t, dsUID)
headers := map[string]string{}
// Post conversion (client signature: namespaceTitle, datasourceUID, promGroup, headers)
// Use the folder title as the namespace title for conversion API lookup.
// Our helper created a folder with title "Test Folder" and UID folderUID.
resp := legacyClient.ConvertPrometheusPostRuleGroup(t, "Test Folder", dsUID, promGroup, headers)
require.Equal(t, "success", resp.Status)
// Retrieve the converted rule via new K8s API immediately after the API call.
ruleList, err := k8sAlertClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
var found *v0alpha1.AlertRule
for _, item := range ruleList.Items {
if item.Spec.Title == "ConvertedAlertTest" { // Title should match the Prometheus alert name
copy := item.DeepCopy()
found = copy
break
}
}
require.NotNil(t, found, "expected to find converted alert rule via K8s API")
// Assertions on converted rule fields
require.Equal(t, folderUID, found.Annotations["grafana.app/folder"], "folder annotation must match folder UID")
require.Equal(t, "test-group", found.Labels[v0alpha1.GroupLabelKey], "group label must match group name")
require.NotEmpty(t, found.Labels[v0alpha1.GroupIndexLabelKey], "group index label must be populated")
// Interval: parse prom group interval and compare with spec trigger interval
require.Equal(t, promGroup.Interval.String(), string(found.Spec.Trigger.Interval), "interval mismatch")
// Expression/model checks: Alert rule conversion produces query + math + threshold nodes.
require.Equal(t, 3, len(found.Spec.Expressions), "expected three expressions (query, math, threshold) in converted alert rule")
for ref, exp := range found.Spec.Expressions {
require.NotNil(t, exp.Model, "expression model %s should not be nil", ref)
// Only query expression carries a datasource UID; math/threshold may omit it. If present, must match dsUID.
if exp.DatasourceUID != nil {
require.EqualValues(t, dsUID, *exp.DatasourceUID)
}
}
// Provenance should be converted_prometheus
require.Equal(t, ngmodels.ProvenanceConvertedPrometheus, ngmodels.Provenance(found.GetProvenanceStatus()), "provenance mismatch")
// Basic JSON model sanity (non-empty if map)
// Basic JSON model sanity for at least one expression
var sanityChecked bool
for _, exp := range found.Spec.Expressions {
if m, ok := exp.Model.(map[string]interface{}); ok {
require.NotEmpty(t, m)
sanityChecked = true
break
}
}
require.True(t, sanityChecked, "expected at least one expression model to be a non-empty map")
}
// TestIntegrationConvertPrometheusRecordingRuleRetrieval verifies recording rule conversion retrieval via K8s API.
func TestIntegrationConvertPrometheusRecordingRuleRetrieval(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
ctx := context.Background()
helper := common.GetTestHelper(t)
k8sRecordingClient := common.NewRecordingRuleClient(t, helper.Org1.Admin)
legacyClient := alerting.NewAlertingLegacyAPIClient(helper.GetListenerAddress(), "admin", "admin")
allRules, status, _ := legacyClient.GetAllRulesWithStatus(t)
require.Equal(t, 200, status)
require.NotNil(t, allRules)
folderUID := "test-folder-convert-record"
common.CreateTestFolder(t, helper, folderUID)
forDuration := prom_model.Duration(5 * time.Second)
interval20, err := prom_model.ParseDuration("20s")
require.NoError(t, err)
promGroup := apimodels.PrometheusRuleGroup{
Name: "test-group-rec",
Interval: interval20,
Rules: []apimodels.PrometheusRule{
{
Record: "converted_metric_total",
Expr: "vector(2)",
For: &forDuration, // For is ignored for recording rules but included for consistency
Labels: map[string]string{"job": "demo"},
Annotations: map[string]string{"summary": "Converted recording rule test"},
},
},
}
// Create a real Prometheus datasource for recording rule conversion.
ds := legacyClient.CreateDatasource(t, "prometheus")
dsUID := ds.Body.Datasource.UID
require.NotEmpty(t, dsUID, "prometheus datasource UID must not be empty")
defer legacyClient.DeleteDatasource(t, dsUID)
headers := map[string]string{
"X-Grafana-Alerting-Target-Datasource-UID": dsUID,
}
resp := legacyClient.ConvertPrometheusPostRuleGroup(t, "Test Folder", dsUID, promGroup, headers)
require.Equal(t, "success", resp.Status)
// Retrieve the converted recording rule immediately
list, err := k8sRecordingClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
var found *v0alpha1.RecordingRule
for _, item := range list.Items {
if item.Spec.Metric == "converted_metric_total" {
copy := item.DeepCopy()
found = copy
break
}
}
require.NotNil(t, found, "expected to find converted recording rule via K8s API")
require.Equal(t, folderUID, found.Annotations["grafana.app/folder"], "folder annotation must match")
require.Equal(t, "test-group-rec", found.Labels[v0alpha1.GroupLabelKey])
require.NotEmpty(t, found.Labels[v0alpha1.GroupIndexLabelKey])
require.Equal(t, promGroup.Interval.String(), string(found.Spec.Trigger.Interval))
// Verify expressions map non-empty
require.Equal(t, 1, len(found.Spec.Expressions))
var exprSpec v0alpha1.RecordingRuleExpression
for _, v := range found.Spec.Expressions {
exprSpec = v
break
}
require.NotNil(t, exprSpec.Model)
// Datasource should match provided header
require.EqualValues(t, dsUID, *exprSpec.DatasourceUID)
require.Equal(t, ngmodels.ProvenanceConvertedPrometheus, ngmodels.Provenance(found.GetProvenanceStatus()))
if m, ok := exprSpec.Model.(map[string]interface{}); ok {
require.NotEmpty(t, m)
} else if b, ok := exprSpec.Model.([]byte); ok {
tmp := map[string]interface{}{}
_ = json.Unmarshal(b, &tmp)
require.NotEmpty(t, tmp)
}
}
// If this test fails, it indicates that the accepted provenance statuses
// in v0alpha1/ext.go are out of sync with the known provenances in ngalert models.
// Make sure to keep them in sync.
func TestProvenancesMatch(t *testing.T) {
modelProvenances := make([]string, 0, len(ngmodels.KnownProvenances))
for _, p := range ngmodels.KnownProvenances {
modelProvenances = append(modelProvenances, string(p))
}
require.ElementsMatch(t, v0alpha1.AcceptedProvenanceStatuses, modelProvenances)
}