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)