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) }