Alerting: API to read rule groups using mimirtool (#100674)
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
prommodel "github.com/prometheus/common/model"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
@@ -17,6 +23,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/prom"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,6 +46,18 @@ func errInvalidHeaderValue(header string) error {
|
||||
return errInvalidHeaderValueBase.Build(errutil.TemplateData{Public: map[string]any{"Header": header}})
|
||||
}
|
||||
|
||||
// ConvertPrometheusSrv converts Prometheus rules to Grafana rules
|
||||
// and retrieves them in a Prometheus-compatible format.
|
||||
//
|
||||
// It is designed to support mimirtool integration, so that rules that work with Mimir
|
||||
// can be imported into Grafana. It works similarly to the provisioning API,
|
||||
// where once a rule group is created, it is marked as "provisioned" (via provenance mechanism)
|
||||
// and is not editable in the UI.
|
||||
//
|
||||
// This service returns only rule groups that were initially imported from Prometheus-compatible sources.
|
||||
// Rule groups not imported from Prometheus are excluded because their original rule definitions are unavailable.
|
||||
// When a rule group is converted from Prometheus to Grafana, the original definition is preserved alongside
|
||||
// the Grafana rule and used for reading requests here.
|
||||
type ConvertPrometheusSrv struct {
|
||||
cfg *setting.UnifiedAlertingSettings
|
||||
logger log.Logger
|
||||
@@ -57,27 +76,117 @@ func NewConvertPrometheusSrv(cfg *setting.UnifiedAlertingSettings, logger log.Lo
|
||||
}
|
||||
}
|
||||
|
||||
// RouteConvertPrometheusGetRules returns all Grafana-managed alert rules in all namespaces (folders)
|
||||
// that were imported from a Prometheus-compatible source.
|
||||
// It responds with a YAML containing a mapping of folders to arrays of Prometheus rule groups.
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response {
|
||||
return response.Error(501, "Not implemented", nil)
|
||||
logger := srv.logger.FromContext(c.Req.Context())
|
||||
|
||||
filterOpts := &provisioning.FilterOptions{
|
||||
ImportedPrometheusRule: util.Pointer(true),
|
||||
}
|
||||
groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get alert groups", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
namespaces, err := grafanaNamespacesToPrometheus(groups)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
return response.YAML(http.StatusOK, namespaces)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
|
||||
// It responds with a YAML containing a mapping of a single folder to an array of Prometheus rule groups.
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(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 {
|
||||
logger.Error("Failed to get folder", "error", err)
|
||||
return namespaceErrorResponse(err)
|
||||
}
|
||||
|
||||
filterOpts := &provisioning.FilterOptions{
|
||||
ImportedPrometheusRule: util.Pointer(true),
|
||||
NamespaceUIDs: []string{namespace.UID},
|
||||
}
|
||||
groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get alert groups", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
ns, err := grafanaNamespacesToPrometheus(groups)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Grafana rules to Prometheus format", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
return response.YAML(http.StatusOK, ns)
|
||||
}
|
||||
|
||||
// RouteConvertPrometheusGetRuleGroup retrieves a single rule group for a given namespace (folder)
|
||||
// in Prometheus-compatible YAML format if it was imported from a Prometheus-compatible source.
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
|
||||
// Just to make the mimirtool rules load work. It first checks if the group exists, and if the endpoint returns 501 it fails.
|
||||
return response.YAML(http.StatusOK, apimodels.PrometheusRuleGroup{})
|
||||
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 {
|
||||
logger.Error("Failed to get folder", "error", err)
|
||||
return namespaceErrorResponse(err)
|
||||
}
|
||||
|
||||
filterOpts := &provisioning.FilterOptions{
|
||||
ImportedPrometheusRule: util.Pointer(true),
|
||||
NamespaceUIDs: []string{namespace.UID},
|
||||
RuleGroups: []string{group},
|
||||
}
|
||||
groupsWithFolders, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get alert group", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
if len(groupsWithFolders) == 0 {
|
||||
return response.Error(http.StatusNotFound, "Rule group not found", nil)
|
||||
}
|
||||
if len(groupsWithFolders) > 1 {
|
||||
logger.Error("Multiple rule groups found when only one was expected", "folder_title", namespaceTitle, "group", group)
|
||||
// It shouldn't happen, but if we get more than 1 group, we return an error.
|
||||
return response.Error(http.StatusInternalServerError, "Multiple rule groups found", nil)
|
||||
}
|
||||
|
||||
promGroup, err := grafanaRuleGroupToPrometheus(groupsWithFolders[0].Title, groupsWithFolders[0].Rules)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Grafana rule to Prometheus format", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
return response.YAML(http.StatusOK, promGroup)
|
||||
}
|
||||
|
||||
// RouteConvertPrometheusPostRuleGroup converts a Prometheus rule group into a Grafana rule group
|
||||
// and creates or updates it within the specified namespace (folder).
|
||||
//
|
||||
// If the group already exists and was not imported from a Prometheus-compatible source initially,
|
||||
// it will not be replaced and an error will be returned.
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, promGroup apimodels.PrometheusRuleGroup) response.Response {
|
||||
logger := srv.logger.FromContext(c.Req.Context())
|
||||
logger = logger.New("folder_title", namespaceTitle, "group", promGroup.Name)
|
||||
@@ -101,6 +210,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm
|
||||
|
||||
group, err := srv.convertToGrafanaRuleGroup(c, ds, ns.UID, promGroup, logger)
|
||||
if err != nil {
|
||||
logger.Error("Failed to convert Prometheus rules to Grafana rules", "error", err)
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
@@ -202,3 +312,54 @@ func parseBooleanHeader(header string, headerName string) (bool, error) {
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func grafanaNamespacesToPrometheus(groups []models.AlertRuleGroupWithFolderFullpath) (map[string][]apimodels.PrometheusRuleGroup, error) {
|
||||
result := map[string][]apimodels.PrometheusRuleGroup{}
|
||||
|
||||
for _, group := range groups {
|
||||
promGroup, err := grafanaRuleGroupToPrometheus(group.Title, group.Rules)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[group.FolderFullpath] = append(result[group.FolderFullpath], promGroup)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func grafanaRuleGroupToPrometheus(group string, rules []models.AlertRule) (apimodels.PrometheusRuleGroup, error) {
|
||||
if len(rules) == 0 {
|
||||
return apimodels.PrometheusRuleGroup{}, nil
|
||||
}
|
||||
|
||||
interval := time.Duration(rules[0].IntervalSeconds) * time.Second
|
||||
promGroup := apimodels.PrometheusRuleGroup{
|
||||
Name: group,
|
||||
Interval: prommodel.Duration(interval),
|
||||
Rules: make([]apimodels.PrometheusRule, len(rules)),
|
||||
}
|
||||
|
||||
for i, rule := range rules {
|
||||
promDefinition := rule.PrometheusRuleDefinition()
|
||||
if promDefinition == "" {
|
||||
return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to get the Prometheus definition of the rule with UID %s", rule.UID)
|
||||
}
|
||||
var r apimodels.PrometheusRule
|
||||
if err := yaml.Unmarshal([]byte(promDefinition), &r); err != nil {
|
||||
return apimodels.PrometheusRuleGroup{}, fmt.Errorf("failed to unmarshal Prometheus rule definition of the rule with UID %s: %w", rule.UID, err)
|
||||
}
|
||||
promGroup.Rules[i] = r
|
||||
}
|
||||
|
||||
return promGroup, nil
|
||||
}
|
||||
|
||||
func namespaceErrorResponse(err error) response.Response {
|
||||
if errors.Is(err, dashboards.ErrFolderAccessDenied) {
|
||||
// If there is no such folder, the error is ErrFolderAccessDenied.
|
||||
// We should return 404 in this case, otherwise mimirtool does not work correctly.
|
||||
return response.Empty(http.StatusNotFound)
|
||||
}
|
||||
|
||||
return toNamespaceErrorResponse(err)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -8,14 +9,17 @@ import (
|
||||
|
||||
prommodel "github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
dsfakes "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/folder/foldertest"
|
||||
acfakes "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol/fakes"
|
||||
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/tests/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
@@ -45,7 +49,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
}
|
||||
|
||||
t.Run("without datasource UID header should return 400", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(datasourceUIDHeader, "")
|
||||
|
||||
@@ -56,7 +60,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("with invalid datasource should return error", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(datasourceUIDHeader, "non-existing-ds")
|
||||
|
||||
@@ -66,7 +70,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("with rule group without evaluation interval should return 202", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
@@ -103,7 +107,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(tc.headerName, tc.headerValue)
|
||||
|
||||
@@ -136,7 +140,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
rc.Req.Header.Set(tc.headerName, tc.headerValue)
|
||||
|
||||
@@ -148,7 +152,7 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("with valid request should return 202", func(t *testing.T) {
|
||||
srv, _ := createConvertPrometheusSrv(t)
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusPostRuleGroup(rc, "test", simpleGroup)
|
||||
@@ -156,7 +160,267 @@ func TestRouteConvertPrometheusPostRuleGroup(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService) {
|
||||
func TestRouteConvertPrometheusGetRuleGroup(t *testing.T) {
|
||||
promRule := apimodels.PrometheusRule{
|
||||
Alert: "test alert",
|
||||
Expr: "vector(1) > 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "test alert",
|
||||
},
|
||||
}
|
||||
promRuleYAML, err := yaml.Marshal(promRule)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("with non-existent folder should return 404", func(t *testing.T) {
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusGetRuleGroup(rc, "non-existent", "test")
|
||||
require.Equal(t, http.StatusNotFound, response.Status(), string(response.Body()))
|
||||
})
|
||||
|
||||
t.Run("with non-existent group should return 404", func(t *testing.T) {
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusGetRuleGroup(rc, "test", "non-existent")
|
||||
require.Equal(t, http.StatusNotFound, response.Status(), string(response.Body()))
|
||||
})
|
||||
|
||||
t.Run("with valid request should return 200", func(t *testing.T) {
|
||||
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
// Create two folders in the root folder
|
||||
fldr := randFolder()
|
||||
fldr.ParentUID = ""
|
||||
folderService.ExpectedFolder = fldr
|
||||
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
||||
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
||||
|
||||
// Create rules in both folders
|
||||
groupKey := models.GenerateGroupKey(rc.SignedInUser.OrgID)
|
||||
groupKey.NamespaceUID = fldr.UID
|
||||
groupKey.RuleGroup = "test-group"
|
||||
rule := models.RuleGen.
|
||||
With(models.RuleGen.WithGroupKey(groupKey)).
|
||||
With(models.RuleGen.WithTitle("TestAlert")).
|
||||
With(models.RuleGen.WithIntervalSeconds(60)).
|
||||
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))).
|
||||
GenerateRef()
|
||||
ruleStore.PutRule(context.Background(), rule)
|
||||
|
||||
// Create a rule in another group
|
||||
groupKeyNotFromProm := models.GenerateGroupKey(rc.SignedInUser.OrgID)
|
||||
groupKeyNotFromProm.NamespaceUID = fldr.UID
|
||||
groupKeyNotFromProm.RuleGroup = "test-group-2"
|
||||
ruleInOtherFolder := models.RuleGen.
|
||||
With(models.RuleGen.WithGroupKey(groupKeyNotFromProm)).
|
||||
With(models.RuleGen.WithTitle("in another group")).
|
||||
With(models.RuleGen.WithIntervalSeconds(60)).
|
||||
GenerateRef()
|
||||
ruleStore.PutRule(context.Background(), ruleInOtherFolder)
|
||||
|
||||
getResp := srv.RouteConvertPrometheusGetRuleGroup(rc, fldr.Title, groupKey.RuleGroup)
|
||||
require.Equal(t, http.StatusOK, getResp.Status())
|
||||
|
||||
var respGroup apimodels.PrometheusRuleGroup
|
||||
err := yaml.Unmarshal(getResp.Body(), &respGroup)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, groupKey.RuleGroup, respGroup.Name)
|
||||
require.Equal(t, prommodel.Duration(time.Duration(rule.IntervalSeconds)*time.Second), respGroup.Interval)
|
||||
require.Len(t, respGroup.Rules, 1)
|
||||
require.Equal(t, promRule.Alert, respGroup.Rules[0].Alert)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteConvertPrometheusGetNamespace(t *testing.T) {
|
||||
promRule1 := apimodels.PrometheusRule{
|
||||
Alert: "test alert",
|
||||
Expr: "vector(1) > 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "test alert",
|
||||
},
|
||||
}
|
||||
|
||||
promRule2 := apimodels.PrometheusRule{
|
||||
Alert: "test alert 2",
|
||||
Expr: "vector(1) > 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "also critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "test alert 2",
|
||||
},
|
||||
}
|
||||
|
||||
promGroup1 := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
promRule1,
|
||||
},
|
||||
}
|
||||
promGroup2 := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group 2",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
promRule2,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("with non-existent folder should return 404", func(t *testing.T) {
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusGetNamespace(rc, "non-existent")
|
||||
require.Equal(t, http.StatusNotFound, response.Status())
|
||||
})
|
||||
|
||||
t.Run("with valid request should return 200", func(t *testing.T) {
|
||||
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
// Create two folders in the root folder
|
||||
fldr := randFolder()
|
||||
fldr.ParentUID = ""
|
||||
fldr2 := randFolder()
|
||||
fldr2.ParentUID = ""
|
||||
folderService.ExpectedFolders = []*folder.Folder{fldr, fldr2}
|
||||
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr, fldr2)
|
||||
|
||||
// Create a Grafana rule for each Prometheus rule
|
||||
for _, promGroup := range []apimodels.PrometheusRuleGroup{promGroup1, promGroup2} {
|
||||
groupKey := models.GenerateGroupKey(rc.SignedInUser.OrgID)
|
||||
groupKey.NamespaceUID = fldr.UID
|
||||
groupKey.RuleGroup = promGroup.Name
|
||||
promRuleYAML, err := yaml.Marshal(promGroup.Rules[0])
|
||||
require.NoError(t, err)
|
||||
rule := models.RuleGen.
|
||||
With(models.RuleGen.WithGroupKey(groupKey)).
|
||||
With(models.RuleGen.WithTitle(promGroup.Rules[0].Alert)).
|
||||
With(models.RuleGen.WithIntervalSeconds(60)).
|
||||
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))).
|
||||
GenerateRef()
|
||||
ruleStore.PutRule(context.Background(), rule)
|
||||
}
|
||||
|
||||
response := srv.RouteConvertPrometheusGetNamespace(rc, fldr.Title)
|
||||
require.Equal(t, http.StatusOK, response.Status())
|
||||
|
||||
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, respNamespaces, 1)
|
||||
require.Contains(t, respNamespaces, fldr.Fullpath)
|
||||
require.ElementsMatch(t, respNamespaces[fldr.Fullpath], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteConvertPrometheusGetRules(t *testing.T) {
|
||||
promRule1 := apimodels.PrometheusRule{
|
||||
Alert: "test alert",
|
||||
Expr: "vector(1) > 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "test alert",
|
||||
},
|
||||
}
|
||||
|
||||
promRule2 := apimodels.PrometheusRule{
|
||||
Alert: "test alert 2",
|
||||
Expr: "vector(1) > 0",
|
||||
For: util.Pointer(prommodel.Duration(5 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "also critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"summary": "test alert 2",
|
||||
},
|
||||
}
|
||||
|
||||
promGroup1 := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
promRule1,
|
||||
},
|
||||
}
|
||||
promGroup2 := apimodels.PrometheusRuleGroup{
|
||||
Name: "Test Group 2",
|
||||
Interval: prommodel.Duration(1 * time.Minute),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
promRule2,
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("with no rules should return empty response", func(t *testing.T) {
|
||||
srv, _, _, _ := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
response := srv.RouteConvertPrometheusGetRules(rc)
|
||||
require.Equal(t, http.StatusOK, response.Status())
|
||||
|
||||
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, respNamespaces)
|
||||
})
|
||||
|
||||
t.Run("with rules should return 200 with rules", func(t *testing.T) {
|
||||
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
|
||||
rc := createRequestCtx()
|
||||
|
||||
// Create a folder in the root
|
||||
fldr := randFolder()
|
||||
fldr.ParentUID = ""
|
||||
folderService.ExpectedFolders = []*folder.Folder{fldr}
|
||||
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
|
||||
|
||||
// Create a Grafana rule for each Prometheus rule
|
||||
for _, promGroup := range []apimodels.PrometheusRuleGroup{promGroup1, promGroup2} {
|
||||
groupKey := models.GenerateGroupKey(rc.SignedInUser.OrgID)
|
||||
groupKey.NamespaceUID = fldr.UID
|
||||
groupKey.RuleGroup = promGroup.Name
|
||||
promRuleYAML, err := yaml.Marshal(promGroup.Rules[0])
|
||||
require.NoError(t, err)
|
||||
rule := models.RuleGen.
|
||||
With(models.RuleGen.WithGroupKey(groupKey)).
|
||||
With(models.RuleGen.WithTitle(promGroup.Rules[0].Alert)).
|
||||
With(models.RuleGen.WithIntervalSeconds(60)).
|
||||
With(models.RuleGen.WithPrometheusOriginalRuleDefinition(string(promRuleYAML))).
|
||||
GenerateRef()
|
||||
ruleStore.PutRule(context.Background(), rule)
|
||||
}
|
||||
|
||||
response := srv.RouteConvertPrometheusGetRules(rc)
|
||||
require.Equal(t, http.StatusOK, response.Status())
|
||||
|
||||
var respNamespaces map[string][]apimodels.PrometheusRuleGroup
|
||||
err := yaml.Unmarshal(response.Body(), &respNamespaces)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, respNamespaces, 1)
|
||||
require.Contains(t, respNamespaces, fldr.Fullpath)
|
||||
require.ElementsMatch(t, respNamespaces[fldr.Fullpath], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
|
||||
})
|
||||
}
|
||||
|
||||
func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasources.CacheService, *fakes.RuleStore, *foldertest.FakeService) {
|
||||
t.Helper()
|
||||
|
||||
ruleStore := fakes.NewRuleStore(t)
|
||||
@@ -195,7 +459,7 @@ func createConvertPrometheusSrv(t *testing.T) (*ConvertPrometheusSrv, datasource
|
||||
|
||||
srv := NewConvertPrometheusSrv(cfg, log.NewNopLogger(), ruleStore, dsCache, alertRuleService)
|
||||
|
||||
return srv, dsCache
|
||||
return srv, dsCache, ruleStore, folderService
|
||||
}
|
||||
|
||||
func createRequestCtx() *contextmodel.ReqContext {
|
||||
|
||||
@@ -77,7 +77,7 @@ type AlertRuleService interface {
|
||||
DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) 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, folderUIDs []string) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error)
|
||||
GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, opts *provisioning.FilterOptions) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error)
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RouteGetPolicyTree(c *contextmodel.ReqContext) response.Response {
|
||||
@@ -452,7 +452,7 @@ func (srv *ProvisioningSrv) RouteGetAlertRulesExport(c *contextmodel.ReqContext)
|
||||
return srv.RouteGetAlertRuleGroupExport(c, folderUIDs[0], group)
|
||||
}
|
||||
|
||||
groupsWithFullpath, err := srv.alertRules.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, folderUIDs)
|
||||
groupsWithFullpath, err := srv.alertRules.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, &provisioning.FilterOptions{NamespaceUIDs: folderUIDs})
|
||||
if err != nil {
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get alert rules", err)
|
||||
}
|
||||
|
||||
@@ -4932,6 +4932,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert",
|
||||
"type": "object"
|
||||
|
||||
@@ -6,10 +6,10 @@ import (
|
||||
|
||||
// swagger:route GET /convert/prometheus/config/v1/rules convert_prometheus RouteConvertPrometheusGetRules
|
||||
//
|
||||
// Gets all namespaces with their rule groups in Prometheus format.
|
||||
// Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - application/yaml
|
||||
//
|
||||
// Responses:
|
||||
// 200: PrometheusNamespace
|
||||
@@ -18,10 +18,10 @@ import (
|
||||
|
||||
// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusGetNamespace
|
||||
//
|
||||
// Gets rules in prometheus format for a given namespace.
|
||||
// Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - application/yaml
|
||||
//
|
||||
// Responses:
|
||||
// 200: PrometheusNamespace
|
||||
@@ -30,10 +30,10 @@ import (
|
||||
|
||||
// swagger:route GET /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusGetRuleGroup
|
||||
//
|
||||
// Gets a rule group in Prometheus format.
|
||||
// Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
// - application/yaml
|
||||
//
|
||||
// Responses:
|
||||
// 200: PrometheusRuleGroup
|
||||
@@ -42,7 +42,9 @@ import (
|
||||
|
||||
// swagger:route POST /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusPostRuleGroup
|
||||
//
|
||||
// Creates or updates a rule group in Prometheus format.
|
||||
// Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.
|
||||
// If the group already exists and was not imported from a Prometheus-compatible source initially,
|
||||
// it will not be replaced and an error will be returned.
|
||||
//
|
||||
// Consumes:
|
||||
// - application/yaml
|
||||
@@ -59,7 +61,7 @@ import (
|
||||
|
||||
// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle} convert_prometheus RouteConvertPrometheusDeleteNamespace
|
||||
//
|
||||
// Deletes all rule groups in the given namespace.
|
||||
// Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
@@ -70,7 +72,7 @@ import (
|
||||
|
||||
// swagger:route DELETE /convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group} convert_prometheus RouteConvertPrometheusDeleteRuleGroup
|
||||
//
|
||||
// Deletes a rule group in Prometheus format.
|
||||
// Deletes a specific rule group if it was imported from a Prometheus-compatible source.
|
||||
//
|
||||
// Produces:
|
||||
// - application/json
|
||||
|
||||
@@ -4770,7 +4770,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup",
|
||||
"type": "object"
|
||||
@@ -6403,7 +6402,7 @@
|
||||
"get": {
|
||||
"operationId": "RouteConvertPrometheusGetRules",
|
||||
"produces": [
|
||||
"application/json"
|
||||
"application/yaml"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -6425,7 +6424,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Gets all namespaces with their rule groups in Prometheus format.",
|
||||
"summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
@@ -6459,7 +6458,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Deletes all rule groups in the given namespace.",
|
||||
"summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
@@ -6475,7 +6474,7 @@
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
"application/yaml"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -6497,7 +6496,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Gets rules in prometheus format for a given namespace.",
|
||||
"summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
@@ -6506,6 +6505,7 @@
|
||||
"consumes": [
|
||||
"application/yaml"
|
||||
],
|
||||
"description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.",
|
||||
"operationId": "RouteConvertPrometheusPostRuleGroup",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -6554,7 +6554,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Creates or updates a rule group in Prometheus format.",
|
||||
"summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
@@ -6595,7 +6595,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Deletes a rule group in Prometheus format.",
|
||||
"summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
@@ -6617,7 +6617,7 @@
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
"application/yaml"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -6639,7 +6639,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Gets a rule group in Prometheus format.",
|
||||
"summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.",
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
]
|
||||
|
||||
@@ -1105,12 +1105,12 @@
|
||||
"/convert/prometheus/config/v1/rules": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
"application/yaml"
|
||||
],
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Gets all namespaces with their rule groups in Prometheus format.",
|
||||
"summary": "Gets all Grafana-managed alert rules that were imported from Prometheus-compatible sources, grouped by namespace.",
|
||||
"operationId": "RouteConvertPrometheusGetRules",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -1137,12 +1137,12 @@
|
||||
"/convert/prometheus/config/v1/rules/{NamespaceTitle}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
"application/yaml"
|
||||
],
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Gets rules in prometheus format for a given namespace.",
|
||||
"summary": "Gets Grafana-managed alert rules that were imported from Prometheus-compatible sources for a specified namespace (folder).",
|
||||
"operationId": "RouteConvertPrometheusGetNamespace",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1174,6 +1174,7 @@
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"description": "If the group already exists and was not imported from a Prometheus-compatible source initially,\nit will not be replaced and an error will be returned.",
|
||||
"consumes": [
|
||||
"application/yaml"
|
||||
],
|
||||
@@ -1183,7 +1184,7 @@
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Creates or updates a rule group in Prometheus format.",
|
||||
"summary": "Converts a Prometheus rule group into a Grafana rule group and creates or updates it within the specified namespace.",
|
||||
"operationId": "RouteConvertPrometheusPostRuleGroup",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1238,7 +1239,7 @@
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Deletes all rule groups in the given namespace.",
|
||||
"summary": "Deletes all rule groups that were imported from Prometheus-compatible sources within the specified namespace.",
|
||||
"operationId": "RouteConvertPrometheusDeleteNamespace",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1267,12 +1268,12 @@
|
||||
"/convert/prometheus/config/v1/rules/{NamespaceTitle}/{Group}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
"application/yaml"
|
||||
],
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Gets a rule group in Prometheus format.",
|
||||
"summary": "Gets a single rule group in Prometheus-compatible format if it was imported from a Prometheus-compatible source.",
|
||||
"operationId": "RouteConvertPrometheusGetRuleGroup",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1316,7 +1317,7 @@
|
||||
"tags": [
|
||||
"convert_prometheus"
|
||||
],
|
||||
"summary": "Deletes a rule group in Prometheus format.",
|
||||
"summary": "Deletes a specific rule group if it was imported from a Prometheus-compatible source.",
|
||||
"operationId": "RouteConvertPrometheusDeleteRuleGroup",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -8710,7 +8711,6 @@
|
||||
}
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
||||
@@ -394,6 +394,22 @@ func WithoutInternalLabels() LabelOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (alertRule *AlertRule) ImportedFromPrometheus() bool {
|
||||
if alertRule.Metadata.PrometheusStyleRule == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return alertRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition != ""
|
||||
}
|
||||
|
||||
func (alertRule *AlertRule) PrometheusRuleDefinition() string {
|
||||
if !alertRule.ImportedFromPrometheus() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return alertRule.Metadata.PrometheusStyleRule.OriginalRuleDefinition
|
||||
}
|
||||
|
||||
// GetLabels returns the labels specified as part of the alert rule.
|
||||
func (alertRule *AlertRule) GetLabels(opts ...LabelOption) map[string]string {
|
||||
labels := alertRule.Labels
|
||||
@@ -806,6 +822,8 @@ type ListAlertRulesQuery struct {
|
||||
|
||||
ReceiverName string
|
||||
TimeIntervalName string
|
||||
|
||||
ImportedPrometheusRule *bool
|
||||
}
|
||||
|
||||
// CountAlertRulesQuery is the query for counting alert rules
|
||||
|
||||
@@ -216,6 +216,14 @@ func (a *AlertRuleMutators) WithEditorSettingsSimplifiedNotificationsSection(ena
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRuleMutators) WithPrometheusOriginalRuleDefinition(definition string) AlertRuleMutator {
|
||||
return func(rule *AlertRule) {
|
||||
rule.Metadata.PrometheusStyleRule = &PrometheusStyleRule{
|
||||
OriginalRuleDefinition: definition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AlertRuleMutators) WithGroupIndex(groupIndex int) AlertRuleMutator {
|
||||
return func(rule *AlertRule) {
|
||||
rule.RuleGroupIndex = groupIndex
|
||||
|
||||
@@ -262,12 +262,41 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, user ident
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
// FilterOptions provides filtering for alert rule queries.
|
||||
// All fields are optional and will be applied as filters if provided.
|
||||
type FilterOptions struct {
|
||||
ImportedPrometheusRule *bool
|
||||
RuleGroups []string
|
||||
NamespaceUIDs []string
|
||||
}
|
||||
|
||||
func (opts *FilterOptions) apply(q models.ListAlertRulesQuery) models.ListAlertRulesQuery {
|
||||
if opts == nil {
|
||||
return q
|
||||
}
|
||||
|
||||
if opts.ImportedPrometheusRule != nil {
|
||||
q.ImportedPrometheusRule = opts.ImportedPrometheusRule
|
||||
}
|
||||
|
||||
if len(opts.NamespaceUIDs) > 0 {
|
||||
q.NamespaceUIDs = opts.NamespaceUIDs
|
||||
}
|
||||
|
||||
if len(opts.RuleGroups) > 0 {
|
||||
q.RuleGroups = opts.RuleGroups
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (service *AlertRuleService) GetRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string) (models.AlertRuleGroup, error) {
|
||||
q := models.ListAlertRulesQuery{
|
||||
OrgID: user.GetOrgID(),
|
||||
NamespaceUIDs: []string{namespaceUID},
|
||||
RuleGroups: []string{group},
|
||||
}
|
||||
|
||||
ruleList, err := service.ruleStore.ListAlertRules(ctx, &q)
|
||||
if err != nil {
|
||||
return models.AlertRuleGroup{}, err
|
||||
@@ -748,15 +777,17 @@ func (service *AlertRuleService) GetAlertRuleGroupWithFolderFullpath(ctx context
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// GetAlertGroupsWithFolderFullpath returns all groups with folder's full path in the folders identified by folderUID that have at least one alert. If argument folderUIDs is nil or empty - returns groups in all folders.
|
||||
func (service *AlertRuleService) GetAlertGroupsWithFolderFullpath(ctx context.Context, user identity.Requester, folderUIDs []string) ([]models.AlertRuleGroupWithFolderFullpath, error) {
|
||||
// GetAlertGroupsWithFolderFullpath returns all groups that have at least one alert with the full folder path for each group.
|
||||
|
||||
// It queries all alert rules for the user's organization, applies optional filtering specified in filterOpts,
|
||||
// and groups the rules by groups. The function then fetches folder details (including the full path)
|
||||
// for each namespace (folder UID) associated with the rule groups. If the user lacks blanket read permissions,
|
||||
// only the groups that the user is authorized to view are returned.
|
||||
func (service *AlertRuleService) GetAlertGroupsWithFolderFullpath(ctx context.Context, user identity.Requester, filterOpts *FilterOptions) ([]models.AlertRuleGroupWithFolderFullpath, error) {
|
||||
q := models.ListAlertRulesQuery{
|
||||
OrgID: user.GetOrgID(),
|
||||
}
|
||||
|
||||
if len(folderUIDs) > 0 {
|
||||
q.NamespaceUIDs = folderUIDs
|
||||
}
|
||||
q = filterOpts.apply(q)
|
||||
|
||||
ruleList, err := service.ruleStore.ListAlertRules(ctx, &q)
|
||||
if err != nil {
|
||||
|
||||
@@ -1644,7 +1644,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, namespaceTitle, res2.FolderFullpath)
|
||||
|
||||
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, []string{namespaceUID})
|
||||
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{namespaceUID}})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, namespaceTitle, res3[0].FolderFullpath)
|
||||
})
|
||||
@@ -1675,7 +1675,7 @@ func TestProvisiongWithFullpath(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res2.FolderFullpath)
|
||||
|
||||
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, []string{otherNamespaceUID})
|
||||
res3, err := ruleService.GetAlertGroupsWithFolderFullpath(context.Background(), &signedInUser, &FilterOptions{NamespaceUIDs: []string{otherNamespaceUID}})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "my-namespace/my-other-namespace containing multiple \\/\\/", res3[0].FolderFullpath)
|
||||
})
|
||||
|
||||
@@ -554,6 +554,13 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
|
||||
}
|
||||
}
|
||||
|
||||
if query.ImportedPrometheusRule != nil {
|
||||
q, err = st.filterImportedPrometheusRules(*query.ImportedPrometheusRule, q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
q = q.Asc("namespace_uid", "rule_group", "rule_group_idx", "id")
|
||||
|
||||
alertRules := make([]*ngmodels.AlertRule, 0)
|
||||
@@ -593,6 +600,14 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
|
||||
continue
|
||||
}
|
||||
}
|
||||
if query.ImportedPrometheusRule != nil { // remove false-positive hits from the result
|
||||
hasOriginalRuleDefinition := converted.Metadata.PrometheusStyleRule != nil && len(converted.Metadata.PrometheusStyleRule.OriginalRuleDefinition) > 0
|
||||
if *query.ImportedPrometheusRule && !hasOriginalRuleDefinition {
|
||||
continue
|
||||
} else if !*query.ImportedPrometheusRule && hasOriginalRuleDefinition {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// MySQL (and potentially other databases) can use case-insensitive comparison.
|
||||
// This code makes sure we return groups that only exactly match the filter.
|
||||
if groupsMap != nil {
|
||||
@@ -928,6 +943,23 @@ func (st DBstore) filterByContentInNotificationSettings(value string, sess *xorm
|
||||
return sess.And(fmt.Sprintf("notification_settings %s ?", st.SQLStore.GetDialect().LikeStr()), "%"+search+"%"), nil
|
||||
}
|
||||
|
||||
func (st DBstore) filterImportedPrometheusRules(value bool, sess *xorm.Session) (*xorm.Session, error) {
|
||||
if value {
|
||||
// Filter for rules that have both prometheus_style_rule and original_rule_definition in metadata
|
||||
return sess.And(
|
||||
"metadata LIKE ? AND metadata LIKE ?",
|
||||
"%prometheus_style_rule%",
|
||||
"%original_rule_definition%",
|
||||
), nil
|
||||
}
|
||||
// Filter for rules that don't have prometheus_style_rule and original_rule_definition in metadata
|
||||
return sess.And(
|
||||
"metadata NOT LIKE ? AND metadata NOT LIKE ?",
|
||||
"%prometheus_style_rule%",
|
||||
"%original_rule_definition%",
|
||||
), nil
|
||||
}
|
||||
|
||||
func (st DBstore) RenameReceiverInNotificationSettings(ctx context.Context, orgID int64, oldReceiver, newReceiver string, validateProvenance func(ngmodels.Provenance) bool, dryRun bool) ([]ngmodels.AlertRuleKey, []ngmodels.AlertRuleKey, error) {
|
||||
// fetch entire rules because Update method requires it because it copies rules to version table
|
||||
rules, err := st.ListAlertRules(ctx, &ngmodels.ListAlertRulesQuery{
|
||||
|
||||
@@ -1828,6 +1828,64 @@ func TestIntegration_AlertRuleVersionsCleanup(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegration_ListAlertRules(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{
|
||||
BaseInterval: time.Duration(rand.Int63n(100)) * time.Second,
|
||||
}
|
||||
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
|
||||
b := &fakeBus{}
|
||||
orgID := int64(1)
|
||||
ruleGen := models.RuleGen
|
||||
ruleGen = ruleGen.With(
|
||||
ruleGen.WithIntervalMatching(cfg.UnifiedAlerting.BaseInterval),
|
||||
ruleGen.WithOrgID(orgID),
|
||||
)
|
||||
t.Run("filter by ImportedPrometheusRule", func(t *testing.T) {
|
||||
store := createTestStore(sqlStore, folderService, &logtest.Fake{}, cfg.UnifiedAlerting, b)
|
||||
regularRule := createRule(t, store, ruleGen)
|
||||
importedRule := createRule(t, store, ruleGen.With(
|
||||
models.RuleMuts.WithPrometheusOriginalRuleDefinition("data"),
|
||||
))
|
||||
tc := []struct {
|
||||
name string
|
||||
importedPrometheusRule *bool
|
||||
expectedRules []*models.AlertRule
|
||||
}{
|
||||
{
|
||||
name: "should return only imported prometheus rules when filter is true",
|
||||
importedPrometheusRule: util.Pointer(true),
|
||||
expectedRules: []*models.AlertRule{importedRule},
|
||||
},
|
||||
{
|
||||
name: "should return only non-imported rules when filter is false",
|
||||
importedPrometheusRule: util.Pointer(false),
|
||||
expectedRules: []*models.AlertRule{regularRule},
|
||||
},
|
||||
{
|
||||
name: "should return all rules when filter is not set",
|
||||
importedPrometheusRule: nil,
|
||||
expectedRules: []*models.AlertRule{regularRule, importedRule},
|
||||
},
|
||||
}
|
||||
for _, tt := range tc {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
query := &models.ListAlertRulesQuery{
|
||||
OrgID: orgID,
|
||||
ImportedPrometheusRule: tt.importedPrometheusRule,
|
||||
}
|
||||
result, err := store.ListAlertRules(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, tt.expectedRules, result)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func createTestStore(
|
||||
sqlStore db.DB,
|
||||
folderService folder.Service,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@@ -289,7 +290,7 @@ func (f *RuleStore) GetNamespaceInRootByTitle(ctx context.Context, title string,
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("namespace with title '%s' not found", title)
|
||||
return nil, dashboards.ErrFolderNotFound
|
||||
}
|
||||
|
||||
func (f *RuleStore) UpdateAlertRules(_ context.Context, _ *models.UserUID, q []models.UpdateRule) error {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,31 +16,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
// Setup Grafana and its Database
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{"alertingConversionAPI"},
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
// Create a user 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")
|
||||
namespace := "test-namespace"
|
||||
|
||||
promGroup1 := apimodels.PrometheusRuleGroup{
|
||||
var (
|
||||
promGroup1 = apimodels.PrometheusRuleGroup{
|
||||
Name: "test-group-1",
|
||||
Interval: prommodel.Duration(60 * time.Second),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
@@ -80,7 +58,7 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
promGroup2 := apimodels.PrometheusRuleGroup{
|
||||
promGroup2 = apimodels.PrometheusRuleGroup{
|
||||
Name: "test-group-2",
|
||||
Interval: prommodel.Duration(60 * time.Second),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
@@ -99,25 +77,129 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
promGroup3 = apimodels.PrometheusRuleGroup{
|
||||
Name: "test-group-3",
|
||||
Interval: prommodel.Duration(60 * time.Second),
|
||||
Rules: []apimodels.PrometheusRule{
|
||||
{
|
||||
Alert: "ServiceDown",
|
||||
Expr: "up == 0",
|
||||
For: util.Pointer(prommodel.Duration(2 * time.Minute)),
|
||||
Labels: map[string]string{
|
||||
"severity": "critical",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"annotation-1": "value-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
// Setup Grafana and its Database
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{"alertingConversionAPI"},
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
// 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",
|
||||
})
|
||||
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password")
|
||||
|
||||
namespace1 := "test-namespace-1"
|
||||
namespace2 := "test-namespace-2"
|
||||
|
||||
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
|
||||
|
||||
t.Run("create two rule groups and get them back", func(t *testing.T) {
|
||||
apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
|
||||
apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup2, nil)
|
||||
t.Run("create rule groups and get them back", 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)
|
||||
|
||||
ns, _, _ := apiClient.GetAllRulesWithStatus(t)
|
||||
// create a third group in a different namespace
|
||||
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
|
||||
requireStatusCode(t, http.StatusAccepted, status, body)
|
||||
|
||||
require.Len(t, ns[namespace], 2)
|
||||
// And a non-provisioned rule in another namespace
|
||||
namespace3UID := util.GenerateShortUID()
|
||||
apiClient.CreateFolder(t, namespace3UID, "folder")
|
||||
createRule(t, apiClient, namespace3UID)
|
||||
|
||||
rulesByGroupName := map[string][]apimodels.GettableExtendedRuleNode{}
|
||||
for _, group := range ns[namespace] {
|
||||
rulesByGroupName[group.Name] = append(rulesByGroupName[group.Name], group.Rules...)
|
||||
// Now get the first group
|
||||
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name)
|
||||
require.Equal(t, promGroup1, group1)
|
||||
|
||||
// Get namespace1
|
||||
ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1)
|
||||
expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{
|
||||
namespace1: {promGroup1, promGroup2},
|
||||
}
|
||||
require.Equal(t, expectedNs1, ns1)
|
||||
|
||||
require.Len(t, rulesByGroupName[promGroup1.Name], 3)
|
||||
require.Len(t, rulesByGroupName[promGroup2.Name], 1)
|
||||
// Get all namespaces
|
||||
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
|
||||
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
|
||||
namespace1: {promGroup1, promGroup2},
|
||||
namespace2: {promGroup3},
|
||||
}
|
||||
require.Equal(t, expectedNamespaces, namespaces)
|
||||
})
|
||||
|
||||
t.Run("without permissions to create folders cannot create rule groups either", func(t *testing.T) {
|
||||
_, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
|
||||
requireStatusCode(t, http.StatusForbidden, status, raw)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
|
||||
testinfra.SQLiteIntegrationTest(t)
|
||||
|
||||
// Setup Grafana and its Database
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
EnableFeatureToggles: []string{"alertingConversionAPI"},
|
||||
})
|
||||
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
// 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")
|
||||
|
||||
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
|
||||
|
||||
namespace1 := "test-namespace-1"
|
||||
|
||||
namespace1UID := util.GenerateShortUID()
|
||||
apiClient.CreateFolder(t, namespace1UID, namespace1)
|
||||
|
||||
t.Run("when pausing header is set, rules should be paused", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -155,21 +237,17 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
|
||||
if tc.alertPaused {
|
||||
headers["X-Grafana-Alerting-Alert-Rules-Paused"] = "true"
|
||||
}
|
||||
apiClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, headers)
|
||||
|
||||
ns, _, _ := apiClient.GetAllRulesWithStatus(t)
|
||||
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, headers)
|
||||
|
||||
rulesByGroupName := map[string][]apimodels.GettableExtendedRuleNode{}
|
||||
for _, group := range ns[namespace] {
|
||||
rulesByGroupName[group.Name] = append(rulesByGroupName[group.Name], group.Rules...)
|
||||
}
|
||||
gr, _, _ := apiClient.GetRulesGroupWithStatus(t, namespace1UID, promGroup1.Name)
|
||||
|
||||
require.Len(t, rulesByGroupName[promGroup1.Name], 3)
|
||||
require.Len(t, gr.Rules, 3)
|
||||
|
||||
pausedRecordingRules := 0
|
||||
pausedAlertRules := 0
|
||||
|
||||
for _, rule := range rulesByGroupName[promGroup1.Name] {
|
||||
for _, rule := range gr.Rules {
|
||||
if rule.GrafanaManagedAlert.IsPaused {
|
||||
if rule.GrafanaManagedAlert.Record != nil {
|
||||
pausedRecordingRules++
|
||||
|
||||
@@ -546,14 +546,14 @@ func (a apiClient) PostSilence(t *testing.T, s apimodels.PostableSilence) (apimo
|
||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/alertmanager/grafana/api/v2/silences", a.url), bytes.NewReader(b))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return sendRequest[apimodels.PostSilencesOKBody](t, req, http.StatusAccepted)
|
||||
return sendRequestJSON[apimodels.PostSilencesOKBody](t, req, http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (a apiClient) GetSilence(t *testing.T, id string) (apimodels.GettableSilence, int, string) {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/api/v2/silence/%s", a.url, id), nil)
|
||||
require.NoError(t, err)
|
||||
return sendRequest[apimodels.GettableSilence](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.GettableSilence](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetSilences(t *testing.T, filters ...string) (apimodels.GettableSilences, int, string) {
|
||||
@@ -568,7 +568,7 @@ func (a apiClient) GetSilences(t *testing.T, filters ...string) (apimodels.Getta
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.GettableSilences](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.GettableSilences](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) DeleteSilence(t *testing.T, id string) (any, int, string) {
|
||||
@@ -580,7 +580,7 @@ func (a apiClient) DeleteSilence(t *testing.T, id string) (any, int, string) {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
return sendRequest[dynamic](t, req, http.StatusOK)
|
||||
return sendRequestJSON[dynamic](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetRulesGroup(t *testing.T, folder string, group string) (apimodels.RuleGroupConfigResponse, int) {
|
||||
@@ -694,7 +694,7 @@ func (a apiClient) GetRuleGroupProvisioning(t *testing.T, folderUID string, grou
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/folder/%s/rule-groups/%s", a.url, folderUID, groupName), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.AlertRuleGroup](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.AlertRuleGroup](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) CreateOrUpdateRuleGroupProvisioning(t *testing.T, group apimodels.AlertRuleGroup) (apimodels.AlertRuleGroup, int, string) {
|
||||
@@ -709,7 +709,7 @@ func (a apiClient) CreateOrUpdateRuleGroupProvisioning(t *testing.T, group apimo
|
||||
require.NoError(t, err)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
return sendRequest[apimodels.AlertRuleGroup](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.AlertRuleGroup](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) SubmitRuleForBacktesting(t *testing.T, config apimodels.BacktestConfig) (int, string) {
|
||||
@@ -808,7 +808,7 @@ func (a apiClient) GetAllMuteTimingsWithStatus(t *testing.T) (apimodels.MuteTimi
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/mute-timings", a.url), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.MuteTimings](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.MuteTimings](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetMuteTimingByNameWithStatus(t *testing.T, name string) (apimodels.MuteTimeInterval, int, string) {
|
||||
@@ -817,7 +817,7 @@ func (a apiClient) GetMuteTimingByNameWithStatus(t *testing.T, name string) (api
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/mute-timings/%s", a.url, name), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.MuteTimeInterval](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) CreateMuteTimingWithStatus(t *testing.T, interval apimodels.MuteTimeInterval) (apimodels.MuteTimeInterval, int, string) {
|
||||
@@ -832,7 +832,7 @@ func (a apiClient) CreateMuteTimingWithStatus(t *testing.T, interval apimodels.M
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusCreated)
|
||||
return sendRequestJSON[apimodels.MuteTimeInterval](t, req, http.StatusCreated)
|
||||
}
|
||||
|
||||
func (a apiClient) EnsureMuteTiming(t *testing.T, interval apimodels.MuteTimeInterval) {
|
||||
@@ -854,7 +854,7 @@ func (a apiClient) UpdateMuteTimingWithStatus(t *testing.T, interval apimodels.M
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.MuteTimeInterval](t, req, http.StatusAccepted)
|
||||
return sendRequestJSON[apimodels.MuteTimeInterval](t, req, http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (a apiClient) DeleteMuteTimingWithStatus(t *testing.T, name string) (int, string) {
|
||||
@@ -908,7 +908,7 @@ func (a apiClient) GetRouteWithStatus(t *testing.T) (apimodels.Route, int, strin
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/provisioning/policies", a.url), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.Route](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.Route](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetRoute(t *testing.T) apimodels.Route {
|
||||
@@ -989,7 +989,7 @@ func (a apiClient) GetRuleHistoryWithStatus(t *testing.T, ruleUID string) (data.
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[data.Frame](t, req, http.StatusOK)
|
||||
return sendRequestJSON[data.Frame](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetAllTimeIntervalsWithStatus(t *testing.T) ([]apimodels.GettableTimeIntervals, int, string) {
|
||||
@@ -998,7 +998,7 @@ func (a apiClient) GetAllTimeIntervalsWithStatus(t *testing.T) ([]apimodels.Gett
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/notifications/time-intervals", a.url), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[[]apimodels.GettableTimeIntervals](t, req, http.StatusOK)
|
||||
return sendRequestJSON[[]apimodels.GettableTimeIntervals](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetTimeIntervalByNameWithStatus(t *testing.T, name string) (apimodels.GettableTimeIntervals, int, string) {
|
||||
@@ -1007,7 +1007,7 @@ func (a apiClient) GetTimeIntervalByNameWithStatus(t *testing.T, name string) (a
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/notifications/time-intervals/%s", a.url, name), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.GettableTimeIntervals](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.GettableTimeIntervals](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) CreateReceiverWithStatus(t *testing.T, receiver apimodels.EmbeddedContactPoint) (apimodels.EmbeddedContactPoint, int, string) {
|
||||
@@ -1022,7 +1022,7 @@ func (a apiClient) CreateReceiverWithStatus(t *testing.T, receiver apimodels.Emb
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.EmbeddedContactPoint](t, req, http.StatusAccepted)
|
||||
return sendRequestJSON[apimodels.EmbeddedContactPoint](t, req, http.StatusAccepted)
|
||||
}
|
||||
|
||||
func (a apiClient) EnsureReceiver(t *testing.T, receiver apimodels.EmbeddedContactPoint) {
|
||||
@@ -1075,33 +1075,33 @@ func (a apiClient) GetAlertmanagerConfigWithStatus(t *testing.T) (apimodels.Gett
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/config/api/v1/alerts", a.url), nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
return sendRequest[apimodels.GettableUserConfig](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.GettableUserConfig](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetActiveAlertsWithStatus(t *testing.T) (apimodels.AlertGroups, int, string) {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/alertmanager/grafana/api/v2/alerts/groups", a.url), nil)
|
||||
require.NoError(t, err)
|
||||
return sendRequest[apimodels.AlertGroups](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.AlertGroups](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetRuleVersionsWithStatus(t *testing.T, ruleUID string) (apimodels.GettableRuleVersions, int, string) {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s/versions", a.url, ruleUID), nil)
|
||||
require.NoError(t, err)
|
||||
return sendRequest[apimodels.GettableRuleVersions](t, req, http.StatusOK)
|
||||
return sendRequestJSON[apimodels.GettableRuleVersions](t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func (a apiClient) GetRuleByUID(t *testing.T, ruleUID string) apimodels.GettableExtendedRuleNode {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/ruler/grafana/api/v1/rule/%s", a.url, ruleUID), nil)
|
||||
require.NoError(t, err)
|
||||
rule, status, raw := sendRequest[apimodels.GettableExtendedRuleNode](t, req, http.StatusOK)
|
||||
rule, status, raw := sendRequestJSON[apimodels.GettableExtendedRuleNode](t, req, http.StatusOK)
|
||||
requireStatusCode(t, http.StatusOK, status, raw)
|
||||
return rule
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) {
|
||||
func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
|
||||
t.Helper()
|
||||
|
||||
data, err := yaml.Marshal(promGroup)
|
||||
@@ -1116,30 +1116,85 @@ func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle,
|
||||
req.Header.Add(key, value)
|
||||
}
|
||||
|
||||
_, status, raw := sendRequest[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
|
||||
requireStatusCode(t, http.StatusAccepted, status, raw)
|
||||
return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
|
||||
}
|
||||
|
||||
func sendRequest[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) {
|
||||
func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string) apimodels.PrometheusRuleGroup {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil)
|
||||
require.NoError(t, err)
|
||||
rule, status, raw := sendRequestYAML[apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
|
||||
requireStatusCode(t, http.StatusOK, status, raw)
|
||||
return rule
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string) map[string][]apimodels.PrometheusRuleGroup {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil)
|
||||
require.NoError(t, err)
|
||||
ns, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
|
||||
requireStatusCode(t, http.StatusOK, status, raw)
|
||||
return ns
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimodels.PrometheusRuleGroup {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules", a.url), nil)
|
||||
require.NoError(t, err)
|
||||
result, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
|
||||
requireStatusCode(t, http.StatusOK, status, raw)
|
||||
return result
|
||||
}
|
||||
|
||||
func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) {
|
||||
t.Helper()
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return body, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func sendRequestJSON[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) {
|
||||
t.Helper()
|
||||
var result T
|
||||
|
||||
if resp.StatusCode != successStatusCode {
|
||||
return result, resp.StatusCode, string(body)
|
||||
body, statusCode, err := sendRequestRaw(t, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
if statusCode != successStatusCode {
|
||||
return result, statusCode, string(body)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &result)
|
||||
require.NoError(t, err)
|
||||
return result, resp.StatusCode, string(body)
|
||||
return result, statusCode, string(body)
|
||||
}
|
||||
|
||||
func sendRequestYAML[T any](t *testing.T, req *http.Request, successStatusCode int) (T, int, string) {
|
||||
t.Helper()
|
||||
var result T
|
||||
|
||||
body, statusCode, err := sendRequestRaw(t, req)
|
||||
require.NoError(t, err)
|
||||
|
||||
if statusCode != successStatusCode {
|
||||
return result, statusCode, string(body)
|
||||
}
|
||||
|
||||
err = yaml.Unmarshal(body, &result)
|
||||
require.NoError(t, err)
|
||||
return result, statusCode, string(body)
|
||||
}
|
||||
|
||||
func requireStatusCode(t *testing.T, expected, actual int, response string) {
|
||||
|
||||
@@ -22771,6 +22771,7 @@
|
||||
}
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
||||
@@ -12838,6 +12838,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/gettableAlert"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user