Alerting: API to delete rule groups using mimirtool (#100687)
* Alerting: API to delete rule groups using mimirtool
This commit is contained in:
@@ -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,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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -22771,7 +22771,6 @@
|
||||
}
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
||||
@@ -12838,7 +12838,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/gettableAlert"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user