Alerting: API to delete rule groups using mimirtool (#100687)

* Alerting: API to delete rule groups using mimirtool
This commit is contained in:
Alexander Akhmetov
2025-02-27 13:04:47 +01:00
committed by GitHub
parent 77305325c2
commit d947433d19
14 changed files with 908 additions and 48 deletions
@@ -103,12 +103,46 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.
// RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source
// within a specified namespace.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
return response.Error(501, "Not implemented", nil)
logger := srv.logger.FromContext(c.Req.Context())
logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return namespaceErrorResponse(err)
}
logger.Info("Deleting all Prometheus-imported rule groups", "folder_uid", namespace.UID, "folder_title", namespaceTitle)
filterOpts := &provisioning.FilterOptions{
NamespaceUIDs: []string{namespace.UID},
ImportedPrometheusRule: util.Pointer(true),
}
err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, models.ProvenanceConvertedPrometheus, filterOpts)
if err != nil {
logger.Error("Failed to delete rule groups", "folder_uid", namespace.UID, "error", err)
return errorToResponse(err)
}
return successfulResponse()
}
// RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
return response.Error(501, "Not implemented", nil)
logger := srv.logger.FromContext(c.Req.Context())
logger.Debug("Looking up folder in the root by title", "folder_title", namespaceTitle)
folder, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
if err != nil {
return namespaceErrorResponse(err)
}
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)
if err != nil {
logger.Error("Failed to delete rule group", "folder_uid", folder.UID, "group", group, "error", err)
return errorToResponse(err)
}
return successfulResponse()
}
// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
@@ -220,7 +254,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm
return errorToResponse(err)
}
return response.JSON(http.StatusAccepted, map[string]string{"status": "success"})
return successfulResponse()
}
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger) (*folder.Folder, response.Response) {
@@ -363,3 +397,9 @@ func namespaceErrorResponse(err error) response.Response {
return toNamespaceErrorResponse(err)
}
func successfulResponse() response.Response {
return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{
Status: "success",
})
}
@@ -2,6 +2,7 @@ package api
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
@@ -77,6 +78,79 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
require.Equal(t, http.StatusAccepted, response.Status())
})
t.Run("should replace an existing rule group", 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)
// And a rule
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithGroupName(simpleGroup.Name)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
require.Equal(t, http.StatusAccepted, response.Status())
// Get the updated rule
remaining, err := ruleStore.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
OrgID: 1,
})
require.NoError(t, err)
require.Len(t, remaining, 1)
require.Equal(t, simpleGroup.Name, remaining[0].RuleGroup)
require.Equal(t, fmt.Sprintf("[%s] %s", simpleGroup.Name, simpleGroup.Rules[0].Alert), remaining[0].Title)
promRuleYAML, err := yaml.Marshal(simpleGroup.Rules[0])
require.NoError(t, err)
require.Equal(t, string(promRuleYAML), remaining[0].PrometheusRuleDefinition())
})
t.Run("should fail to replace a provisioned rule group", 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)
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithGroupName(simpleGroup.Name)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("123")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
// mark the rule as provisioned
err := provenanceStore.SetProvenance(context.Background(), rule, 1, models.ProvenanceAPI)
require.NoError(t, err)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusPostRuleGroup(rc, fldr.Title, simpleGroup)
require.Equal(t, http.StatusConflict, response.Status())
// Verify the rule is still present
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remaining)
})
t.Run("with valid pause header values should return 202", func(t *testing.T) {
testCases := []struct {
name string
@@ -420,9 +494,219 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
})
}
func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) {
func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
t.Run("for non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteNamespace(rc, "non-existent")
require.Equal(t, http.StatusNotFound, response.Status())
})
t.Run("valid request should delete rules", func(t *testing.T) {
initNamespace := func(promDefinition string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...)
// 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)
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
return srv, ruleStore, fldr, rule
}
t.Run("valid request should delete rules", func(t *testing.T) {
srv, ruleStore, fldr, rule := initNamespace("prometheus definition")
// Create another rule group in a different namespace that should not be deleted
otherGroupName := "other-group"
otherRule := models.RuleGen.
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(otherGroupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), otherRule)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title)
require.Equal(t, http.StatusAccepted, response.Status())
// Verify the rule in the specified group was deleted
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.Error(t, err)
require.Nil(t, remaining)
// Verify the rule in the other group still exists
remainingOther, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: otherRule.UID,
OrgID: otherRule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remainingOther)
})
t.Run("fails to delete rules when they are provisioned", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, ruleStore, fldr, rule := initNamespace("", withProvenanceStore(provenanceStore))
rc := createRequestCtx()
// Create a provisioned rule
rule2 := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule2)
err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI)
require.NoError(t, err)
response := srv.RouteConvertPrometheusDeleteNamespace(rc, fldr.Title)
require.Equal(t, http.StatusConflict, response.Status())
// Verify the rule is still present
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remaining)
})
})
}
func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
t.Run("for non-existent folder should return 404", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, "non-existent", "test-group")
require.Equal(t, http.StatusNotFound, response.Status())
})
const groupName = "test-group"
t.Run("valid request should delete rules", func(t *testing.T) {
initGroup := func(promDefinition string, groupName string, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, *fakes.RuleStore, *folder.Folder, *models.AlertRule) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t, opts...)
// 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)
rule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(groupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(promDefinition)).
GenerateRef()
ruleStore.PutRule(context.Background(), rule)
return srv, ruleStore, fldr, rule
}
t.Run("valid request should delete rules", func(t *testing.T) {
srv, ruleStore, fldr, rule := initGroup("prometheus definition", groupName)
rc := createRequestCtx()
// Create another rule in a different group that should not be deleted
otherGroupName := "other-group"
otherRule := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(otherGroupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("other prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), otherRule)
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)
// Verify the otherRule from the "other-group" is still present
otherRuleRefreshed, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: otherRule.UID,
OrgID: otherRule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, otherRuleRefreshed)
})
t.Run("fails to delete rules when they are provisioned", func(t *testing.T) {
provenanceStore := fakes.NewFakeProvisioningStore()
srv, ruleStore, fldr, rule := initGroup("", groupName, withProvenanceStore(provenanceStore))
rc := createRequestCtx()
// Create a provisioned rule
rule2 := models.RuleGen.
With(models.RuleGen.WithNamespaceUID(fldr.UID)).
With(models.RuleGen.WithOrgID(1)).
With(models.RuleGen.WithGroupName(groupName)).
With(models.RuleGen.WithPrometheusOriginalRuleDefinition("prometheus definition")).
GenerateRef()
ruleStore.PutRule(context.Background(), rule2)
err := provenanceStore.SetProvenance(context.Background(), rule2, 1, models.ProvenanceAPI)
require.NoError(t, err)
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, groupName)
require.Equal(t, http.StatusConflict, response.Status())
// Verify the rule is still present
remaining, err := ruleStore.GetAlertRuleByUID(context.Background(), &models.GetAlertRuleByUIDQuery{
UID: rule.UID,
OrgID: rule.OrgID,
})
require.NoError(t, err)
require.NotNil(t, remaining)
})
})
}
type convertPrometheusSrvOptions struct {
provenanceStore provisioning.ProvisioningStore
}
type convertPrometheusSrvOptionsFunc func(*convertPrometheusSrvOptions)
func withProvenanceStore(store provisioning.ProvisioningStore) convertPrometheusSrvOptionsFunc {
return func(opts *convertPrometheusSrvOptions) {
opts.provenanceStore = store
}
}
func createConvertPrometheusSrv(t *testing.T, opts ...convertPrometheusSrvOptionsFunc) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) {
t.Helper()
options := convertPrometheusSrvOptions{
provenanceStore: fakes.NewFakeProvisioningStore(),
}
for _, opt := range opts {
opt(&options)
}
ruleStore := fakes.NewRuleStore(t)
folder := randFolder()
ruleStore.Folders[1] = append(ruleStore.Folders[1], folder)
@@ -441,7 +725,7 @@ func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasource
alertRuleService := provisioning.NewAlertRuleService(
ruleStore,
fakes.NewFakeProvisioningStore(),
options.provenanceStore,
folderService,
quotas,
&provisioning.NopTransactionManager{},
@@ -75,6 +75,7 @@ type AlertRuleService interface {
GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error)
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error
DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error
DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance alerting_models.Provenance, opts *provisioning.FilterOptions) error
GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error)
GetAlertRuleGroupWithFolderFullpath(ctx context.Context, u identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderFullpath, error)
GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, opts *provisioning.FilterOptions) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error)
+3 -2
View File
@@ -3,6 +3,7 @@ package api
import (
"errors"
"fmt"
"net/http"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
@@ -33,7 +34,7 @@ func errorToResponse(err error) response.Response {
return response.Err(err)
}
if errors.Is(err, datasources.ErrDataSourceNotFound) {
return ErrResp(404, err, "")
return ErrResp(http.StatusNotFound, err, "")
}
if errors.Is(err, errUnexpectedDatasourceType) {
return ErrResp(400, err, "")
@@ -41,5 +42,5 @@ func errorToResponse(err error) response.Response {
if errors.Is(err, errFolderAccess) {
return toNamespaceErrorResponse(err)
}
return ErrResp(500, err, "")
return ErrResp(http.StatusInternalServerError, err, "")
}
@@ -4932,7 +4932,6 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/definitions/gettableAlert",
"type": "object"
@@ -6,6 +6,7 @@ import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
@@ -29,6 +30,11 @@ type ruleAccessControlService interface {
CanWriteAllRules(ctx context.Context, user identity.Requester) (bool, error)
}
var errProvenanceMismatch = errutil.NewBase(errutil.StatusConflict, "alerting.provenanceMismatch").MustTemplate(
"cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'",
errutil.WithPublic("cannot {{ .Public.Operation }} with provided provenance '{{ .Public.ProvidedProvenance }}', needs '{{ .Public.StoredProvenance }}'"),
)
type NotificationSettingsValidatorProvider interface {
Validator(ctx context.Context, orgID int64) (notifier.NotificationSettingsValidator, error)
}
@@ -445,27 +451,42 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden
}
func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string, provenance models.Provenance) error {
delta, err := store.CalculateRuleGroupDelete(ctx, service.ruleStore, models.AlertRuleGroupKey{
OrgID: user.GetOrgID(),
NamespaceUID: namespaceUID,
RuleGroup: group,
return service.DeleteRuleGroups(ctx, user, provenance, &FilterOptions{
NamespaceUIDs: []string{namespaceUID},
RuleGroups: []string{group},
})
}
// DeleteRuleGroups deletes alert rule groups by the specified filter options.
func (service *AlertRuleService) DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance models.Provenance, filterOpts *FilterOptions) error {
q := models.ListAlertRulesQuery{}
q = filterOpts.apply(q)
q.OrgID = user.GetOrgID()
deltas, err := store.CalculateRuleGroupsDelete(ctx, service.ruleStore, user.GetOrgID(), &q)
if err != nil {
return err
}
// check if the current user has permissions to all rules and can bypass the regular authorization validation.
can, err := service.authz.CanWriteAllRules(ctx, user)
if err != nil {
return err
}
if !can {
if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil {
return err
// Perform all deletions in a transaction
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
for _, delta := range deltas {
can, err := service.authz.CanWriteAllRules(ctx, user)
if err != nil {
return err
}
if !can {
if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil {
return err
}
}
err = service.persistDelta(ctx, user, delta, provenance)
if err != nil {
return err
}
}
}
return service.persistDelta(ctx, user, delta, provenance)
return nil
})
}
func (service *AlertRuleService) calcDelta(ctx context.Context, user identity.Requester, group models.AlertRuleGroup) (*store.GroupDelta, error) {
@@ -526,7 +547,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity
return err
}
if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance)
return errProvenanceMismatch.Build(errutil.TemplateData{
Public: map[string]interface{}{
"ProvidedProvenance": provenance,
"StoredProvenance": storedProvenance,
"Operation": "delete",
},
})
}
}
if err := service.deleteRules(ctx, user.GetOrgID(), delta.Delete...); err != nil {
@@ -543,7 +570,13 @@ func (service *AlertRuleService) persistDelta(ctx context.Context, user identity
return err
}
if canUpdate := validation.CanUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
return errProvenanceMismatch.Build(errutil.TemplateData{
Public: map[string]interface{}{
"ProvidedProvenance": provenance,
"StoredProvenance": storedProvenance,
"Operation": "update",
},
})
}
updates = append(updates, models.UpdateRule{
Existing: update.Existing,
@@ -689,7 +722,13 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, user ident
return err
}
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance)
return errProvenanceMismatch.Build(errutil.TemplateData{
Public: map[string]interface{}{
"ProvidedProvenance": provenance,
"StoredProvenance": storedProvenance,
"Operation": "delete",
},
})
}
can, err := service.authz.CanWriteAllRules(ctx, user)
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"math/rand"
"slices"
"strconv"
"strings"
"testing"
@@ -1577,7 +1578,7 @@ func TestDeleteRuleGroup(t *testing.T) {
assert.Equal(t, u, user)
assert.Equal(t, groupKey, change.GroupKey)
assert.Contains(t, change.AffectedGroups, groupKey)
assert.EqualValues(t, rules, change.AffectedGroups[groupKey])
assert.ElementsMatch(t, rules, change.AffectedGroups[groupKey])
assert.Empty(t, change.Update)
assert.Empty(t, change.New)
assert.Len(t, change.Delete, len(rules))
@@ -1597,6 +1598,228 @@ func TestDeleteRuleGroup(t *testing.T) {
})
}
func TestDeleteRuleGroups(t *testing.T) {
orgID1 := rand.Int63()
orgID2 := rand.Int63()
u := &user.SignedInUser{OrgID: orgID1}
// Create groups across different orgs and namespaces
groupKey1 := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace1",
RuleGroup: "group1",
}
groupKey2 := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace2",
RuleGroup: "group2",
}
groupKey3 := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace3",
RuleGroup: "group3",
}
groupKey4 := models.AlertRuleGroupKey{
OrgID: orgID2, // Different org
NamespaceUID: "namespace1",
RuleGroup: "group1",
}
gen := models.RuleGen
// Create rules for each group
rules1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateManyRef(2)
rules2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateManyRef(3)
rules3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateManyRef(2)
rules4 := gen.With(gen.WithGroupKey(groupKey4)).GenerateManyRef(2)
org1Rules := slices.Concat(rules1, rules2, rules3)
org2Rules := rules4
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
service, ruleStore, provenanceStore, ac := initService(t)
ruleStore.Rules = map[int64][]*models.AlertRule{
orgID1: org1Rules,
orgID2: org2Rules,
}
// Set provenance for all rules
for _, rules := range []([]*models.AlertRule){org1Rules, org2Rules} {
for _, rule := range rules {
err := provenanceStore.SetProvenance(context.Background(), rule, rule.OrgID, models.ProvenanceAPI)
require.NoError(t, err)
}
}
return service, ruleStore, provenanceStore, ac
}
getUIDs := func(rules []*models.AlertRule) []string {
uids := make([]string, 0, len(rules))
for _, rule := range rules {
uids = append(uids, rule.UID)
}
return uids
}
t.Run("when deleting specific groups", func(t *testing.T) {
filterOpts := &FilterOptions{
NamespaceUIDs: []string{"namespace1"},
RuleGroups: []string{"group1"},
}
t.Run("when user can write all rules", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
require.Len(t, ac.Calls, 1)
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
// Verify only rules from group1 in org1 were deleted
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 1)
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
})
t.Run("when user cannot write all rules", func(t *testing.T) {
t.Run("should not delete if not authorized", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return false, nil
}
expectedErr := errors.New("test error")
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
return expectedErr
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.ErrorIs(t, err, expectedErr)
require.Len(t, ac.Calls, 2)
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
deletes := getDeletedRules(t, ruleStore)
require.Empty(t, deletes)
})
t.Run("should delete group1 when authorized", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return false, nil
}
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
assert.Equal(t, u, user)
assert.Equal(t, groupKey1, change.GroupKey)
assert.ElementsMatch(t, rules1, change.AffectedGroups[groupKey1])
assert.Empty(t, change.Update)
assert.Empty(t, change.New)
assert.Len(t, change.Delete, len(rules1))
return nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
require.Len(t, ac.Calls, 2)
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 1)
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
})
})
})
t.Run("when deleting multiple groups from multiple namespaces", func(t *testing.T) {
filterOpts := &FilterOptions{
NamespaceUIDs: []string{"namespace1", "namespace2"},
RuleGroups: []string{"group1", "group2"},
}
t.Run("should delete all matching groups from correct org", func(t *testing.T) {
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 2)
require.ElementsMatch(
t,
slices.Concat(getUIDs(rules1), getUIDs(rules2)),
slices.Concat(deletes[0].uids, deletes[1].uids),
)
})
})
t.Run("when filtering by imported Prometheus rules", func(t *testing.T) {
filterOpts := &FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: []string{"namespace1"},
}
t.Run("when the group is not imported", func(t *testing.T) {
filterOpts.RuleGroups = []string{groupKey1.RuleGroup}
service, _, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
})
t.Run("when the group is imported", func(t *testing.T) {
importedGroup := models.AlertRuleGroupKey{
OrgID: orgID1,
NamespaceUID: "namespace1",
RuleGroup: "newgroup",
}
importedRules := gen.With(
gen.WithGroupKey(importedGroup),
gen.WithPrometheusOriginalRuleDefinition("something"),
).GenerateManyRef(2)
filterOpts.RuleGroups = []string{importedGroup.RuleGroup}
service, ruleStore, _, ac := initServiceWithData(t)
ruleStore.Rules[orgID1] = append(ruleStore.Rules[orgID1], importedRules...)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.NoError(t, err)
deletes := getDeletedRules(t, ruleStore)
require.Len(t, deletes, 1)
require.ElementsMatch(t, getUIDs(importedRules), deletes[0].uids)
})
})
t.Run("with no matching rule groups", func(t *testing.T) {
filterOpts := &FilterOptions{
NamespaceUIDs: []string{"non-existent"},
RuleGroups: []string{"non-existent"},
}
service, ruleStore, _, ac := initServiceWithData(t)
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
return true, nil
}
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
deletes := getDeletedRules(t, ruleStore)
require.Empty(t, deletes)
})
}
func TestProvisiongWithFullpath(t *testing.T) {
tracer := tracing.InitializeTracerForTest()
inProcBus := bus.ProvideBus(tracer)
@@ -1696,6 +1919,31 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
return result
}
type deleteRuleOperation struct {
orgID int64
uids []string
}
func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation {
t.Helper()
queries := getDeleteQueries(ruleStore)
operations := make([]deleteRuleOperation, 0, len(queries))
for _, q := range queries {
orgID, ok := q.Params[0].(int64)
require.True(t, ok, "orgID parameter should be int64")
uids, ok := q.Params[1].([]string)
require.True(t, ok, "uids parameter should be []string")
operations = append(operations, deleteRuleOperation{
orgID: orgID,
uids: uids,
})
}
return operations
}
func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService {
t.Helper()
sqlStore := db.InitTestDB(t)
+39 -15
View File
@@ -221,15 +221,13 @@ func CalculateRuleUpdate(ctx context.Context, ruleReader RuleReader, rule *model
return calculateChanges(ctx, ruleReader, rule.GetGroupKey(), existingGroupRules, newGroup)
}
// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group
func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) {
// List all rules in the group.
q := models.ListAlertRulesQuery{
OrgID: groupKey.OrgID,
NamespaceUIDs: []string{groupKey.NamespaceUID},
RuleGroups: []string{groupKey.RuleGroup},
// CalculateRuleGroupsDelete calculates []*GroupDelta that reflects an operation of removing multiple groups
func CalculateRuleGroupsDelete(ctx context.Context, ruleReader RuleReader, orgID int64, query *models.ListAlertRulesQuery) ([]*GroupDelta, error) {
if query == nil {
query = &models.ListAlertRulesQuery{}
}
ruleList, err := ruleReader.ListAlertRules(ctx, &q)
query.OrgID = orgID
ruleList, err := ruleReader.ListAlertRules(ctx, query)
if err != nil {
return nil, err
}
@@ -237,14 +235,40 @@ func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupK
return nil, models.ErrAlertRuleGroupNotFound.Errorf("")
}
delta := &GroupDelta{
GroupKey: groupKey,
Delete: ruleList,
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
groupKey: ruleList,
},
groups := models.GroupByAlertRuleGroupKey(ruleList)
deltas := make([]*GroupDelta, 0, len(groups))
for groupKey := range groups {
delta := &GroupDelta{
GroupKey: groupKey,
Delete: groups[groupKey],
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
groupKey: groups[groupKey],
},
}
if err != nil {
return nil, err
}
deltas = append(deltas, delta)
}
return delta, nil
return deltas, nil
}
// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group
func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) {
q := &models.ListAlertRulesQuery{
NamespaceUIDs: []string{groupKey.NamespaceUID},
RuleGroups: []string{groupKey.RuleGroup},
}
deltas, err := CalculateRuleGroupsDelete(ctx, ruleReader, groupKey.OrgID, q)
if err != nil {
return nil, err
}
if len(deltas) != 1 {
return nil, fmt.Errorf("expected to get a single group delta, got %d", len(deltas))
}
return deltas[0], nil
}
// CalculateRuleDelete calculates GroupDelta that reflects an operation of removing a rule from the group.
+87 -2
View File
@@ -428,6 +428,91 @@ func TestCalculateAutomaticChanges(t *testing.T) {
})
}
func TestCalculateRuleGroupsDelete(t *testing.T) {
orgId := int64(rand.Int31())
gen := models.RuleGen
t.Run("returns ErrAlertRuleGroupNotFound when namespace has no rules", func(t *testing.T) {
fakeStore := fakes.NewRuleStore(t)
otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUID("ns-1")).GenerateManyRef(3)
fakeStore.Rules[orgId] = otherRules
query := &models.ListAlertRulesQuery{
NamespaceUIDs: []string{"ns-2"},
}
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query)
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
require.Nil(t, deltas)
})
t.Run("returns deltas for all affected groups in namespace", func(t *testing.T) {
fakeStore := fakes.NewRuleStore(t)
folder := randFolder()
// Create rules in two groups in target namespace
group1Key := models.AlertRuleGroupKey{
OrgID: orgId,
NamespaceUID: folder.UID,
RuleGroup: util.GenerateShortUID(),
}
group2Key := models.AlertRuleGroupKey{
OrgID: orgId,
NamespaceUID: folder.UID,
RuleGroup: util.GenerateShortUID(),
}
group1Rules := gen.With(gen.WithGroupKey(group1Key)).GenerateManyRef(3)
group2Rules := gen.With(gen.WithGroupKey(group2Key)).GenerateManyRef(2)
allNamespaceRules := append(group1Rules, group2Rules...)
// Create rules in different namespace
otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUIDNotIn(folder.UID)).GenerateManyRef(3)
fakeStore.Rules[orgId] = append(allNamespaceRules, otherRules...)
query := &models.ListAlertRulesQuery{
NamespaceUIDs: []string{folder.UID},
}
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query)
require.NoError(t, err)
require.Len(t, deltas, 2, "expected deltas for two groups")
// Verify each group's delta
for _, delta := range deltas {
require.True(t, delta.GroupKey == group1Key || delta.GroupKey == group2Key)
require.Empty(t, delta.Update)
require.Empty(t, delta.New)
require.Contains(t, delta.AffectedGroups, delta.GroupKey)
if delta.GroupKey == group1Key {
require.ElementsMatch(t, group1Rules, delta.Delete)
require.ElementsMatch(t, group1Rules, delta.AffectedGroups[delta.GroupKey])
} else {
require.ElementsMatch(t, group2Rules, delta.Delete)
require.ElementsMatch(t, group2Rules, delta.AffectedGroups[delta.GroupKey])
}
}
})
t.Run("fails if store returns error", func(t *testing.T) {
fakeStore := fakes.NewRuleStore(t)
expectedErr := errors.New("store error")
fakeStore.Hook = func(cmd any) error {
switch cmd.(type) {
case models.ListAlertRulesQuery:
return expectedErr
}
return nil
}
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, nil)
require.ErrorIs(t, err, expectedErr)
require.Nil(t, deltas)
})
}
func TestCalculateRuleGroupDelete(t *testing.T) {
gen := models.RuleGen
fakeStore := fakes.NewRuleStore(t)
@@ -449,13 +534,13 @@ func TestCalculateRuleGroupDelete(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, groupKey, delta.GroupKey)
assert.EqualValues(t, groupRules, delta.Delete)
assert.ElementsMatch(t, groupRules, delta.Delete)
assert.Empty(t, delta.Update)
assert.Empty(t, delta.New)
assert.Len(t, delta.AffectedGroups, 1)
assert.Equal(t, models.RulesGroup(groupRules), delta.AffectedGroups[delta.GroupKey])
assert.ElementsMatch(t, groupRules, delta.AffectedGroups[delta.GroupKey])
})
}
+18
View File
@@ -214,6 +214,15 @@ func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQu
if len(q.RuleUIDs) > 0 && !slices.Contains(q.RuleUIDs, r.UID) {
continue
}
if q.ImportedPrometheusRule != nil {
hasOriginalRuleDefinition := r.PrometheusRuleDefinition() != ""
if *q.ImportedPrometheusRule && !hasOriginalRuleDefinition {
continue
}
if !*q.ImportedPrometheusRule && hasOriginalRuleDefinition {
continue
}
}
ruleList = append(ruleList, r)
}
@@ -308,12 +317,21 @@ func (f *RuleStore) InsertAlertRules(_ context.Context, _ *models.UserUID, q []m
defer f.mtx.Unlock()
f.RecordedOps = append(f.RecordedOps, q)
ids := make([]models.AlertRuleKeyWithId, 0, len(q))
rulesPerOrg := map[int64][]models.AlertRule{}
for _, rule := range q {
ids = append(ids, models.AlertRuleKeyWithId{
AlertRuleKey: rule.GetKey(),
ID: rand.Int63(),
})
rulesPerOrg[rule.OrgID] = append(rulesPerOrg[rule.OrgID], rule)
}
for orgID, rules := range rulesPerOrg {
for _, rule := range rules {
f.Rules[orgID] = append(f.Rules[orgID], &rule)
}
}
if err := f.Hook(q); err != nil {
return ids, err
}
@@ -1,6 +1,7 @@
package alerting
import (
"encoding/json"
"net/http"
"testing"
"time"
@@ -8,6 +9,7 @@ import (
prommodel "github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"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/org"
@@ -100,7 +102,7 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
dir, gpath := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
@@ -108,7 +110,7 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
EnableFeatureToggles: []string{"alertingConversionAPI"},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
@@ -169,6 +171,111 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
_, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusForbidden, status, raw)
})
t.Run("delete one rule group", func(t *testing.T) {
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name)
// Check that the promGroup2 and promGroup3 are still there
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
namespace2: {promGroup3},
}
require.Equal(t, expectedNamespaces, namespaces)
// Delete the second namespace
apiClient.ConvertPrometheusDeleteNamespace(t, namespace2)
// Check that only the first namespace is left
namespaces = apiClient.ConvertPrometheusGetAllRules(t)
expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
}
require.Equal(t, expectedNamespaces, namespaces)
})
}
func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) {
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"},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, gpath)
// Create users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
namespace1 := "test-namespace-1"
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
t.Run("cannot overwrite a rule group with different provenance", func(t *testing.T) {
// Create a rule group using the provisioning API and then try to overwrite it
// using the Prometheus Conversion API. It should fail because the provenance
// we set for rules in these two APIs is different and we check that when updating.
provisionedRuleGroup := apimodels.AlertRuleGroup{
Title: promGroup1.Name,
Interval: 60,
FolderUID: namespace1,
Rules: []apimodels.ProvisionedAlertRule{
{
Title: "Rule1",
OrgID: 1,
RuleGroup: promGroup1.Name,
Condition: "A",
NoDataState: apimodels.Alerting,
ExecErrState: apimodels.AlertingErrState,
For: prommodel.Duration(time.Duration(60) * time.Second),
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
},
},
},
},
}
// Create the folder
apiClient.CreateFolder(t, namespace1, namespace1)
// Create rule in the root folder using another API
_, status, response := apiClient.CreateOrUpdateRuleGroupProvisioning(t, provisionedRuleGroup)
require.Equalf(t, http.StatusOK, status, response)
// Should fail to post the group
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusConflict, status, body)
})
}
func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
+16
View File
@@ -1146,6 +1146,22 @@ func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimo
return result
}
func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) {
t.Helper()
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
requireStatusCode(t, http.StatusAccepted, status, raw)
}
func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) {
t.Helper()
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
requireStatusCode(t, http.StatusAccepted, status, raw)
}
func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) {
t.Helper()
client := &http.Client{}
-1
View File
@@ -22771,7 +22771,6 @@
}
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"type": "array",
"items": {
"type": "object",
-1
View File
@@ -12838,7 +12838,6 @@
"type": "object"
},
"gettableAlerts": {
"description": "GettableAlerts gettable alerts",
"items": {
"$ref": "#/components/schemas/gettableAlert"
},