70e30df6ce
* 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
228 lines
8.9 KiB
Go
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)
|
|
}
|