From 85b0b47efdd811b46af2715aec7a54943d985ea6 Mon Sep 17 00:00:00 2001 From: Alexander Akhmetov Date: Tue, 11 Mar 2025 19:53:28 +0100 Subject: [PATCH] Alerting: Allow disabling provenance in the Prometheus conversion API (#101573) When creating Grafana-managed alerts from Prometheus rule definitions with mimirtool or cortextool, the rules are marked as "provisioned" and are not editable in the Grafana UI. This PR allows changing this by providing an extra header: --extra-header="X-Disable-Provenance=true". When provenance is disabled, we do not keep the original rule definition in YAML, so it is impossible to read it back using the Prometheus conversion API (mimirtool/cortextool). This is intentional because if we did keep it and the rule was later changed in the UI, its Prometheus YAML definition would no longer reflect the latest version of the alert rule, as it would be unchanged. --- .../ngalert/api/api_convert_prometheus.go | 38 +++++- .../api/api_convert_prometheus_test.go | 115 +++++++++++++++++ pkg/services/ngalert/prom/convert.go | 32 +++-- pkg/services/ngalert/prom/convert_test.go | 68 ++++++++++ .../ngalert/provisioning/alert_rules_test.go | 36 ++++++ .../provisioning/validation/provenance.go | 20 ++- .../validation/provenance_test.go | 60 +++++++++ .../alerting/api_convert_prometheus_test.go | 117 ++++++++++++++++++ 8 files changed, 466 insertions(+), 20 deletions(-) diff --git a/pkg/services/ngalert/api/api_convert_prometheus.go b/pkg/services/ngalert/api/api_convert_prometheus.go index 3c7878bc500..57d312768cb 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus.go +++ b/pkg/services/ngalert/api/api_convert_prometheus.go @@ -188,11 +188,12 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contex } logger.Info("Deleting all Prometheus-imported rule groups", "folder_uid", namespace.UID, "folder_title", namespaceTitle) + provenance := getProvenance(c) filterOpts := &provisioning.FilterOptions{ NamespaceUIDs: []string{namespace.UID}, ImportedPrometheusRule: util.Pointer(true), } - err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, models.ProvenanceConvertedPrometheus, filterOpts) + err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, provenance, filterOpts) if errors.Is(err, models.ErrAlertRuleGroupNotFound) { return response.Empty(http.StatusNotFound) } @@ -218,7 +219,8 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contex } logger.Info("Deleting Prometheus-imported rule group", "folder_uid", folder.UID, "folder_title", namespaceTitle, "group", group) - err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, models.ProvenanceConvertedPrometheus) + provenance := getProvenance(c) + err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, provenance) if errors.Is(err, models.ErrAlertRuleGroupNotFound) { return response.Empty(http.StatusNotFound) } @@ -352,13 +354,21 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm return errorToResponse(err) } - group, err := srv.convertToGrafanaRuleGroup(c, ds, ns.UID, promGroup, logger) + provenance := getProvenance(c) + + // If the provenance is not ConvertedPrometheus, we don't keep the original rule definition. + // This is because the rules can be modified through the UI, which may break compatibility + // with the Prometheus format. We only preserve the original rule definition + // to ensure we can return them in this API in Prometheus format. + keepOriginalRuleDefinition := provenance == models.ProvenanceConvertedPrometheus + + group, err := srv.convertToGrafanaRuleGroup(c, ds, ns.UID, promGroup, keepOriginalRuleDefinition, logger) if err != nil { logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err) return errorToResponse(err) } - err = srv.alertRuleService.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, *group, models.ProvenanceConvertedPrometheus) + err = srv.alertRuleService.ReplaceRuleGroup(c.Req.Context(), c.SignedInUser, *group, provenance) if err != nil { logger.Error("Failed to replace rule group", "error", err) return errorToResponse(err) @@ -387,7 +397,14 @@ func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext return ns, nil } -func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(c *contextmodel.ReqContext, ds *datasources.DataSource, namespaceUID string, promGroup apimodels.PrometheusRuleGroup, logger log.Logger) (*models.AlertRuleGroup, error) { +func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup( + c *contextmodel.ReqContext, + ds *datasources.DataSource, + namespaceUID string, + promGroup apimodels.PrometheusRuleGroup, + keepOriginalRuleDefinition bool, + logger log.Logger, +) (*models.AlertRuleGroup, error) { logger.Info("Converting Prometheus rules to Grafana rules", "rules", len(promGroup.Rules), "folder_uid", namespaceUID, "datasource_uid", ds.UID, "datasource_type", ds.Type) rules := make([]prom.PrometheusRule, len(promGroup.Rules)) @@ -429,6 +446,7 @@ func (srv *ConvertPrometheusSrv) convertToGrafanaRuleGroup(c *contextmodel.ReqCo AlertRules: prom.RulesConfig{ IsPaused: pauseAlertRules, }, + KeepOriginalRuleDefinition: util.Pointer(keepOriginalRuleDefinition), }, ) if err != nil { @@ -537,3 +555,13 @@ func promGroupHasRecordingRules(promGroup apimodels.PrometheusRuleGroup) bool { } return false } + +// getProvenance determines the provenance value to use for rules created via the Prometheus conversion API. +// If the X-Disable-Provenance header is present in the request, returns ProvenanceNone, +// otherwise returns ProvenanceConvertedPrometheus. +func getProvenance(ctx *contextmodel.ReqContext) models.Provenance { + if _, disabled := ctx.Req.Header[disableProvenanceHeaderName]; disabled { + return models.ProvenanceNone + } + return models.ProvenanceConvertedPrometheus +} diff --git a/pkg/services/ngalert/api/api_convert_prometheus_test.go b/pkg/services/ngalert/api/api_convert_prometheus_test.go index c6b0a33166a..fda8be8cecd 100644 --- a/pkg/services/ngalert/api/api_convert_prometheus_test.go +++ b/pkg/services/ngalert/api/api_convert_prometheus_test.go @@ -144,6 +144,11 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { promDefinition, err := r.PrometheusRuleDefinition() require.NoError(t, err) require.Equal(t, expectedDef, promDefinition) + + // Verify provenance was set to ProvenanceConvertedPrometheus + prov, err := provenanceStore.GetProvenance(context.Background(), r, 1) + require.NoError(t, err) + require.Equal(t, models.ProvenanceConvertedPrometheus, prov) } }) @@ -341,6 +346,41 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) { }) } }) + + t.Run("with disable provenance header should use ProvenanceNone", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, withProvenanceStore(provenanceStore)) + + // Create a folder in the root + fldr := randFolder() + fldr.ParentUID = "" + folderService.ExpectedFolder = fldr + folderService.ExpectedFolders = []*folder.Folder{fldr} + ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr) + + // Create request with the X-Disable-Provenance header + rc := createRequestCtx() + rc.Req.Header.Set("X-Disable-Provenance", "true") + + response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Get the created rules + rules, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{ + OrgID: 1, + }) + require.NoError(t, err) + require.Len(t, rules, 2) + + // Verify provenance was set to ProvenanceNone + for _, r := range rules { + prov, err := provenanceStore.GetProvenance(context.Background(), r, 1) + require.NoError(t, err) + require.Equal(t, models.ProvenanceNone, prov, "Provenance should be ProvenanceNone when X-Disable-Provenance header is set") + // Prometheus rule definition should not be saved when provenance is disabled + require.Nil(t, r.Metadata.PrometheusStyleRule) + } + }) } func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) { @@ -743,6 +783,29 @@ func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) { require.NoError(t, err) require.NotNil(t, remaining) }) + + t.Run("with disable provenance header should still be able to delete rules", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, ruleStore, fldr, rule := initNamespace("prometheus definition", withProvenanceStore(provenanceStore)) + + // Mark the rule as provisioned with API provenance + err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceConvertedPrometheus) + require.NoError(t, err) + + rc := createRequestCtx() + rc.Req.Header.Set("X-Disable-Provenance", "true") + + response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify the rule was deleted + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.Error(t, err) + require.Nil(t, remaining) + }) }) } @@ -854,6 +917,29 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) { require.NoError(t, err) require.NotNil(t, remaining) }) + + t.Run("with disable provenance header should still be able to delete rules", func(t *testing.T) { + provenanceStore := fakes.NewFakeProvisioningStore() + srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore)) + + // Mark the rule as provisioned with API provenance + err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceConvertedPrometheus) + require.NoError(t, err) + + rc := createRequestCtx() + rc.Req.Header.Set("X-Disable-Provenance", "true") + + response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName) + require.Equal(t, http.StatusAccepted, response.Status()) + + // Verify the rule was deleted + remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{ + UID: rule.UID, + OrgID: rule.OrgID, + }) + require.Error(t, err) + require.Nil(t, remaining) + }) }) } @@ -995,3 +1081,32 @@ func TestGetWorkingFolderUID(t *testing.T) { require.Equal(t, specifiedFolderUID, folderUID) }) } + +func TestGetProvenance(t *testing.T) { + t.Run("should return ProvenanceConvertedPrometheus when header is not present", func(t *testing.T) { + rc := createRequestCtx() + // Ensure the header is not present + rc.Req.Header.Del(disableProvenanceHeaderName) + + provenance := getProvenance(rc) + require.Equal(t, models.ProvenanceConvertedPrometheus, provenance) + }) + + t.Run("should return ProvenanceNone when header is present", func(t *testing.T) { + rc := createRequestCtx() + // Set the disable provenance header + rc.Req.Header.Set(disableProvenanceHeaderName, "true") + + provenance := getProvenance(rc) + require.Equal(t, models.ProvenanceNone, provenance) + }) + + t.Run("should return ProvenanceNone when header is present with any value", func(t *testing.T) { + rc := createRequestCtx() + // Set the disable provenance header with an empty value + rc.Req.Header.Set(disableProvenanceHeaderName, "") + + provenance := getProvenance(rc) + require.Equal(t, models.ProvenanceNone, provenance) + }) +} diff --git a/pkg/services/ngalert/prom/convert.go b/pkg/services/ngalert/prom/convert.go index ccbbf89e23b..f64b17bfc5e 100644 --- a/pkg/services/ngalert/prom/convert.go +++ b/pkg/services/ngalert/prom/convert.go @@ -37,8 +37,12 @@ type Config struct { EvaluationOffset *time.Duration ExecErrState models.ExecutionErrorState NoDataState models.NoDataState - RecordingRules RulesConfig - AlertRules RulesConfig + // KeepOriginalRuleDefinition indicates whether the original Prometheus rule definition + // if saved to the alert rule metadata. If not, then it will not be possible to convert + // the alert rule back to Prometheus format. + KeepOriginalRuleDefinition *bool + RecordingRules RulesConfig + AlertRules RulesConfig } // RulesConfig contains configuration that applies to either recording or alerting rules. @@ -51,10 +55,11 @@ var ( defaultEvaluationOffset = 0 * time.Minute defaultConfig = Config{ - FromTimeRange: &defaultTimeRange, - EvaluationOffset: &defaultEvaluationOffset, - ExecErrState: models.ErrorErrState, - NoDataState: models.OK, + FromTimeRange: &defaultTimeRange, + EvaluationOffset: &defaultEvaluationOffset, + ExecErrState: models.ErrorErrState, + NoDataState: models.OK, + KeepOriginalRuleDefinition: util.Pointer(true), } ) @@ -87,7 +92,9 @@ func NewConverter(cfg Config) (*Converter, error) { if cfg.NoDataState == "" { cfg.NoDataState = defaultConfig.NoDataState } - + if cfg.KeepOriginalRuleDefinition == nil { + cfg.KeepOriginalRuleDefinition = defaultConfig.KeepOriginalRuleDefinition + } if cfg.DatasourceType != datasources.DS_PROMETHEUS && cfg.DatasourceType != datasources.DS_LOKI { return nil, fmt.Errorf("invalid datasource type: %s", cfg.DatasourceType) } @@ -233,11 +240,12 @@ func (p *Converter) convertRule(orgID int64, namespaceUID string, promGroup Prom RuleGroup: promGroup.Name, IsPaused: isPaused, Record: record, - Metadata: models.AlertRuleMetadata{ - PrometheusStyleRule: &models.PrometheusStyleRule{ - OriginalRuleDefinition: string(originalRuleDefinition), - }, - }, + } + + if p.cfg.KeepOriginalRuleDefinition != nil && *p.cfg.KeepOriginalRuleDefinition { + result.Metadata.PrometheusStyleRule = &models.PrometheusStyleRule{ + OriginalRuleDefinition: string(originalRuleDefinition), + } } return result, nil diff --git a/pkg/services/ngalert/prom/convert_test.go b/pkg/services/ngalert/prom/convert_test.go index 2d7e2b26535..2281a9da1c0 100644 --- a/pkg/services/ngalert/prom/convert_test.go +++ b/pkg/services/ngalert/prom/convert_test.go @@ -618,3 +618,71 @@ func TestPrometheusRulesToGrafana_UID(t *testing.T) { }) }) } + +func TestPrometheusRulesToGrafana_KeepOriginalRuleDefinition(t *testing.T) { + orgID := int64(1) + namespace := "namespace" + + promGroup := PrometheusRuleGroup{ + Name: "test-group", + Rules: []PrometheusRule{ + { + Alert: "test-alert", + Expr: "up == 0", + }, + }, + } + + testCases := []struct { + name string + keepOriginalRuleDefinition *bool + expectDefinition bool + }{ + { + name: "keep original rule definition is true", + keepOriginalRuleDefinition: util.Pointer(true), + expectDefinition: true, + }, + { + name: "keep original rule definition is false", + keepOriginalRuleDefinition: util.Pointer(false), + expectDefinition: false, + }, + { + name: "keep original rule definition is nil (should use default)", + keepOriginalRuleDefinition: nil, + expectDefinition: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cfg := Config{ + DatasourceUID: "datasource-uid", + DatasourceType: datasources.DS_PROMETHEUS, + DefaultInterval: 1 * time.Minute, + KeepOriginalRuleDefinition: tc.keepOriginalRuleDefinition, + } + + converter, err := NewConverter(cfg) + require.NoError(t, err) + + // Convert the Prometheus rule to Grafana + grafanaGroup, err := converter.PrometheusRulesToGrafana(orgID, namespace, promGroup) + require.NoError(t, err) + require.Len(t, grafanaGroup.Rules, 1) + + if tc.expectDefinition { + originalRuleDefinition, err := yaml.Marshal(promGroup.Rules[0]) + require.NoError(t, err) + require.Equal( + t, + string(originalRuleDefinition), + grafanaGroup.Rules[0].Metadata.PrometheusStyleRule.OriginalRuleDefinition, + ) + } else { + require.Nil(t, grafanaGroup.Rules[0].Metadata.PrometheusStyleRule) + } + }) + } +} diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go index c98c293fd34..68991883c03 100644 --- a/pkg/services/ngalert/provisioning/alert_rules_test.go +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -674,6 +674,42 @@ func TestAlertRuleService(t *testing.T) { to: models.ProvenanceNone, errNil: false, }, + { + name: "should be able to update from provenance none to 'converted prometheus'", + from: models.ProvenanceNone, + to: models.ProvenanceConvertedPrometheus, + errNil: true, + }, + { + name: "should be able to update from provenance 'converted prometheus' to none", + from: models.ProvenanceConvertedPrometheus, + to: models.ProvenanceNone, + errNil: true, + }, + { + name: "should not be able to update from provenance 'converted prometheus' to api", + from: models.ProvenanceConvertedPrometheus, + to: models.ProvenanceAPI, + errNil: false, + }, + { + name: "should not be able to update from provenance 'converted prometheus' to file", + from: models.ProvenanceConvertedPrometheus, + to: models.ProvenanceFile, + errNil: false, + }, + { + name: "should not be able to update from provenance api to 'converted prometheus'", + from: models.ProvenanceAPI, + to: models.ProvenanceConvertedPrometheus, + errNil: false, + }, + { + name: "should not be able to update from provenance file to 'converted prometheus'", + from: models.ProvenanceFile, + to: models.ProvenanceConvertedPrometheus, + errNil: false, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { diff --git a/pkg/services/ngalert/provisioning/validation/provenance.go b/pkg/services/ngalert/provisioning/validation/provenance.go index 387cdd56b3e..b57dcee37db 100644 --- a/pkg/services/ngalert/provisioning/validation/provenance.go +++ b/pkg/services/ngalert/provisioning/validation/provenance.go @@ -7,9 +7,23 @@ import ( // CanUpdateProvenanceInRuleGroup checks if a provenance can be updated for a rule group and its alerts. // ReplaceRuleGroup function intends to replace an entire rule group: inserting, updating, and removing rules. func CanUpdateProvenanceInRuleGroup(storedProvenance, provenance models.Provenance) bool { - return storedProvenance == provenance || - storedProvenance == models.ProvenanceNone || - (storedProvenance == models.ProvenanceAPI && provenance == models.ProvenanceNone) + // Same provenance is always allowed + if storedProvenance == provenance { + return true + } + + // Can always update stored ProvenanceNone + if storedProvenance == models.ProvenanceNone { + return true + } + + // Can reset to ProvenanceNone from specific provenances + if provenance == models.ProvenanceNone { + return storedProvenance == models.ProvenanceAPI || + storedProvenance == models.ProvenanceConvertedPrometheus + } + + return false } type ProvenanceStatusTransitionValidator = func(from, to models.Provenance) error diff --git a/pkg/services/ngalert/provisioning/validation/provenance_test.go b/pkg/services/ngalert/provisioning/validation/provenance_test.go index b98ce241b03..04b9c61eccb 100644 --- a/pkg/services/ngalert/provisioning/validation/provenance_test.go +++ b/pkg/services/ngalert/provisioning/validation/provenance_test.go @@ -15,6 +15,7 @@ func TestValidateProvenanceRelaxed(t *testing.T) { models.ProvenanceNone, models.ProvenanceAPI, models.ProvenanceFile, + models.ProvenanceConvertedPrometheus, models.Provenance(fmt.Sprintf("random-%s", util.GenerateShortUID())), } t.Run("all transitions from 'none' are allowed", func(t *testing.T) { @@ -49,3 +50,62 @@ func TestValidateProvenanceRelaxed(t *testing.T) { } }) } + +func TestCanUpdateProvenanceInRuleGroup(t *testing.T) { + all := []models.Provenance{ + models.ProvenanceNone, + models.ProvenanceAPI, + models.ProvenanceFile, + models.ProvenanceConvertedPrometheus, + models.Provenance(fmt.Sprintf("random-%s", util.GenerateShortUID())), + } + + t.Run("same provenance transitions are allowed", func(t *testing.T) { + for _, provenance := range all { + assert.True(t, CanUpdateProvenanceInRuleGroup(provenance, provenance)) + } + }) + + t.Run("all transitions from 'none' are allowed", func(t *testing.T) { + for _, provenance := range all { + assert.True(t, CanUpdateProvenanceInRuleGroup(models.ProvenanceNone, provenance)) + } + }) + + t.Run("only specific provenances can transition to 'none'", func(t *testing.T) { + allowed := []models.Provenance{ + models.ProvenanceAPI, + models.ProvenanceConvertedPrometheus, + } + + for _, from := range allowed { + assert.True(t, CanUpdateProvenanceInRuleGroup(from, models.ProvenanceNone), + "transition %s -> 'none' should be allowed", from) + } + + notAllowed := []models.Provenance{ + models.ProvenanceFile, + models.Provenance(fmt.Sprintf("random-%s", util.GenerateShortUID())), + } + + for _, from := range notAllowed { + assert.False(t, CanUpdateProvenanceInRuleGroup(from, models.ProvenanceNone), + "transition %s -> 'none' should not be allowed", from) + } + }) + + t.Run("transitions between different provenances are not allowed", func(t *testing.T) { + for _, from := range all { + if from == models.ProvenanceNone { + continue // always allowed + } + for _, to := range all { + if from == to || to == models.ProvenanceNone { + continue // always allowed + } + assert.False(t, CanUpdateProvenanceInRuleGroup(from, to), + "transition %s -> '%s' should not be allowed", from, to) + } + } + }) +} diff --git a/pkg/tests/api/alerting/api_convert_prometheus_test.go b/pkg/tests/api/alerting/api_convert_prometheus_test.go index ec881bc09c5..e07382e6bca 100644 --- a/pkg/tests/api/alerting/api_convert_prometheus_test.go +++ b/pkg/tests/api/alerting/api_convert_prometheus_test.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/expr" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" @@ -603,6 +604,122 @@ func TestIntegrationConvertPrometheusEndpoints_FolderUIDHeader(t *testing.T) { }) } +func TestIntegrationConvertPrometheusEndpoints_Provenance(t *testing.T) { + runTest := func(t *testing.T, enableLokiPaths bool) { + testinfra.SQLiteIntegrationTest(t) + + // Setup Grafana and its Database + dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableLegacyAlerting: true, + EnableUnifiedAlerting: true, + DisableAnonymous: true, + AppModeProduction: true, + EnableFeatureToggles: []string{"alertingConversionAPI", "grafanaManagedRecordingRulesDatasources", "grafanaManagedRecordingRules"}, + EnableRecordingRules: true, + }) + + grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath) + + // Create admin user + createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{ + DefaultOrgRole: string(org.RoleAdmin), + Password: "password", + Login: "admin", + }) + adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "password") + adminClient.prometheusConversionUseLokiPaths = enableLokiPaths + + ds := adminClient.CreateDatasource(t, datasources.DS_PROMETHEUS) + + t.Run("default provenance is ProvenanceConvertedPrometheus", func(t *testing.T) { + namespace := "test-namespace-provenance-" + util.GenerateShortUID() + + // We have to create a folder to get its UID to use in the ruler API later to fetch the rule group. + namespaceUID := util.GenerateShortUID() + adminClient.CreateFolder(t, namespaceUID, namespace) + + adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil) + + // Get the rule group using the ruler API and check its provenance + ruleGroup, status := adminClient.GetRulesGroup(t, namespaceUID, promGroup1.Name) + require.Equal(t, http.StatusAccepted, status) + for _, rule := range ruleGroup.Rules { + require.Equal(t, apimodels.Provenance(models.ProvenanceConvertedPrometheus), rule.GrafanaManagedAlert.Provenance) + } + }) + + t.Run("with disable provenance header should use ProvenanceNone", func(t *testing.T) { + namespace := "test-namespace-provenance-" + util.GenerateShortUID() + + // We have to create a folder to get its UID to use in the ruler API later to fetch the rule group. + namespaceUID := util.GenerateShortUID() + adminClient.CreateFolder(t, namespaceUID, namespace) + + // Create rule group with the X-Disable-Provenance header + headers := map[string]string{ + "X-Disable-Provenance": "true", + } + adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, headers) + + // Get the rule group using the ruler API and check its provenance + ruleGroup, status := adminClient.GetRulesGroup(t, namespaceUID, promGroup1.Name) + require.Equal(t, http.StatusAccepted, status) + for _, rule := range ruleGroup.Rules { + require.Equal(t, apimodels.Provenance(models.ProvenanceNone), rule.GrafanaManagedAlert.Provenance) + } + }) + + t.Run("can delete rule groups with X-Disable-Provenance header", func(t *testing.T) { + namespace := "test-namespace-delete-provenance-" + util.GenerateShortUID() + namespaceUID := util.GenerateShortUID() + adminClient.CreateFolder(t, namespaceUID, namespace) + + // Create a rule group + adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil) + + // Now try to delete with X-Disable-Provenance header + // This should succeed + headers := map[string]string{ + "X-Disable-Provenance": "true", + } + adminClient.ConvertPrometheusDeleteRuleGroup(t, namespace, promGroup1.Name, headers) + + // Verify the rule group is gone + _, status, _ := adminClient.GetRulesGroupWithStatus(t, namespaceUID, promGroup1.Name) + require.Equal(t, http.StatusNotFound, status) + }) + + t.Run("can delete namespaces with X-Disable-Provenance header", func(t *testing.T) { + namespace := "test-namespace-delete-ns-provenance-" + util.GenerateShortUID() + namespaceUID := util.GenerateShortUID() + adminClient.CreateFolder(t, namespaceUID, namespace) + + // Create a rule group with provenance=ProvenanceConvertedPrometheus + adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil) + + // Now delete with X-Disable-Provenance header + // This should succeed + headers := map[string]string{ + "X-Disable-Provenance": "true", + } + adminClient.ConvertPrometheusDeleteNamespace(t, namespace, headers) + + // Verify the namespace has no rule groups + namespaces := adminClient.ConvertPrometheusGetAllRules(t, nil) + _, exists := namespaces[namespace] + require.False(t, exists) + }) + } + + t.Run("with the mimirtool paths", func(t *testing.T) { + runTest(t, false) + }) + + t.Run("with the cortextool Loki paths", func(t *testing.T) { + runTest(t, true) + }) +} + func TestIntegrationConvertPrometheusEndpoints_Delete(t *testing.T) { runTest := func(t *testing.T, enableLokiPaths bool) { testinfra.SQLiteIntegrationTest(t)