Alerting: Allow specifying a folder for Prometheus rule import (#101406)

What is this feature?

Allows the creation of alert rules with mimirtool in a specified folder.

Why do we need this feature?

Currently, the APIs for mimirtool create namespaces and rule groups in the root folder without the ability to set a custom folder. For example, it could be a special "Imported" folder, etc.

This PR makes it possible with a special header: mimirtool ... --extra-headers="X-Grafana-Alerting-Folder-UID=123". If it's not present, the root folder is used, otherwise, the specified one is used.

mimirtool does not support nested folder structures, while Grafana allows folder nesting. To keep compatibility, we return only direct child folders of the working folder (as namespaces) with rule groups and rules that are directly in these child folders as if there are no nested folders.

For example, given this folder structure in Grafana:

```
	grafana/
	├── production/
	│   ├── service1/
	│   │   └── alerts/
	│   └── service2/
	└── testing/
	    └── service3/
```

If the working folder is "grafana":

    Only namespaces "production" and "testing" are returned
    Only rule groups directly within these folders are included

If the working folder is "production":
   -  Only namespaces "service1" and "service2" are returned
    Only rule groups directly within these folders are included
This commit is contained in:
Alexander Akhmetov
2025-03-03 17:59:01 +01:00
committed by GitHub
parent b7a0aeeb0d
commit c7c68322b1
9 changed files with 997 additions and 109 deletions
@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
@@ -27,7 +28,14 @@ import (
)
const (
datasourceUIDHeader = "X-Grafana-Alerting-Datasource-UID"
// datasourceUIDHeader is the name of the header that specifies the UID of the datasource to be used for the rules.
datasourceUIDHeader = "X-Grafana-Alerting-Datasource-UID"
// If the folderUIDHeader is present, namespaces and rule groups will be created in the specified folder.
// If not, the root folder will be used as the default.
folderUIDHeader = "X-Grafana-Alerting-Folder-UID"
// These headers control the paused state of newly created rules. By default, rules are not paused.
recordingRulesPausedHeader = "X-Grafana-Alerting-Recording-Rules-Paused"
alertRulesPausedHeader = "X-Grafana-Alerting-Alert-Rules-Paused"
)
@@ -58,6 +66,32 @@ func errInvalidHeaderValue(header string) error {
// 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.
//
// Folder Structure Handling:
// mimirtool does not support nested folder structures, while Grafana allows folder nesting.
// To keep compatibility, this service only returns direct child folders of the working folder
// as namespaces, and rule groups and rules that are directly in these child folders.
//
// For example, given this folder structure in Grafana:
//
// grafana/
// ├── production/
// │ ├── service1/
// │ │ └── alerts/
// │ └── service2/
// └── testing/
// └── service3/
//
// If the working folder is "grafana":
// - Only namespaces "production" and "testing" are returned
// - Only rule groups directly within these folders are included
//
// If the working folder is "production":
// - Only namespaces "service1" and "service2" are returned
// - Only rule groups directly within these folders are included
//
// The "working folder" is specified by the X-Grafana-Alerting-Folder-UID header, which can be set to any folder UID,
// and defaults to the root folder if not provided.
type ConvertPrometheusSrv struct {
cfg *setting.UnifiedAlertingSettings
logger log.Logger
@@ -82,8 +116,27 @@ func NewConvertPrometheusSrv(cfg *setting.UnifiedAlertingSettings, logger log.Lo
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.ReqContext) response.Response {
logger := srv.logger.FromContext(c.Req.Context())
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
folders, err := srv.ruleStore.GetNamespaceChildren(c.Req.Context(), workingFolderUID, c.SignedInUser.GetOrgID(), c.SignedInUser)
if len(folders) == 0 || errors.Is(err, dashboards.ErrFolderNotFound) {
// If there is no such folder or no children, return empty response
// because mimirtool expects 200 OK response in this case.
return response.YAML(http.StatusOK, map[string][]apimodels.PrometheusRuleGroup{})
}
if err != nil {
logger.Error("Failed to get folders", "error", err)
return errorToResponse(err)
}
folderUIDs := make([]string, 0, len(folders))
for _, f := range folders {
folderUIDs = append(folderUIDs, f.UID)
}
filterOpts := &provisioning.FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
NamespaceUIDs: folderUIDs,
}
groups, err := srv.alertRuleService.GetAlertGroupsWithFolderFullpath(c.Req.Context(), c.SignedInUser, filterOpts)
if err != nil {
@@ -105,8 +158,11 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
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)
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser, workingFolderUID)
if err != nil {
return namespaceErrorResponse(err)
}
@@ -117,6 +173,9 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contex
ImportedPrometheusRule: util.Pointer(true),
}
err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, models.ProvenanceConvertedPrometheus, filterOpts)
if errors.Is(err, models.ErrAlertRuleGroupNotFound) {
return response.Empty(http.StatusNotFound)
}
if err != nil {
logger.Error("Failed to delete rule groups", "folder_uid", namespace.UID, "error", err)
return errorToResponse(err)
@@ -129,14 +188,20 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contex
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
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)
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
folder, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser, workingFolderUID)
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 errors.Is(err, models.ErrAlertRuleGroupNotFound) {
return response.Empty(http.StatusNotFound)
}
if err != nil {
logger.Error("Failed to delete rule group", "folder_uid", folder.UID, "group", group, "error", err)
return errorToResponse(err)
@@ -150,8 +215,11 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contex
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
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)
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser, workingFolderUID)
if err != nil {
logger.Error("Failed to get folder", "error", err)
return namespaceErrorResponse(err)
@@ -181,12 +249,18 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmo
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
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)
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("working_folder_uid", workingFolderUID)
logger.Debug("Looking up folder by title", "folder_title", namespaceTitle)
namespace, err := srv.ruleStore.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser, workingFolderUID)
if err != nil {
logger.Error("Failed to get folder", "error", err)
return namespaceErrorResponse(err)
}
if namespace == nil {
return response.Error(http.StatusNotFound, "Folder not found", nil)
}
filterOpts := &provisioning.FilterOptions{
ImportedPrometheusRule: util.Pointer(true),
@@ -223,11 +297,13 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmo
// 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)
workingFolderUID := getWorkingFolderUID(c)
logger = logger.New("folder_title", namespaceTitle, "group", promGroup.Name, "working_folder_uid", workingFolderUID)
logger.Info("Converting Prometheus rule group", "rules", len(promGroup.Rules))
ns, errResp := srv.getOrCreateNamespace(c, namespaceTitle, logger)
ns, errResp := srv.getOrCreateNamespace(c, namespaceTitle, logger, workingFolderUID)
if errResp != nil {
return errResp
}
@@ -257,18 +333,19 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm
return successfulResponse()
}
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger) (*folder.Folder, response.Response) {
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger, workingFolderUID string) (*folder.Folder, response.Response) {
logger.Debug("Getting or creating a new folder")
ns, err := srv.ruleStore.GetOrCreateNamespaceInRootByTitle(
ns, err := srv.ruleStore.GetOrCreateNamespaceByTitle(
c.Req.Context(),
title,
c.SignedInUser.GetOrgID(),
c.SignedInUser,
workingFolderUID,
)
if err != nil {
logger.Error("Failed to get or create a new folder", "error", err)
return nil, toNamespaceErrorResponse(err)
return nil, namespaceErrorResponse(err)
}
logger.Debug("Using folder for the converted rules", "folder_uid", ns.UID)
@@ -351,11 +428,17 @@ func grafanaNamespacesToPrometheus(groups []models.AlertRuleGroupWithFolderFullp
result := map[string][]apimodels.PrometheusRuleGroup{}
for _, group := range groups {
// Since the folder can be nested but mimirtool does not support nested paths,
// we need to use only the last folder in the full path.
// For example, if the current working folder is "general" and the full path is "grafana/some folder/general/production",
// we should use the "production" folder.
folder := filepath.Base(group.FolderFullpath)
promGroup, err := grafanaRuleGroupToPrometheus(group.Title, group.Rules)
if err != nil {
return nil, err
}
result[group.FolderFullpath] = append(result[group.FolderFullpath], promGroup)
result[folder] = append(result[folder], promGroup)
}
return result, nil
@@ -388,18 +471,26 @@ func grafanaRuleGroupToPrometheus(group string, rules []models.AlertRule) (apimo
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)
}
func successfulResponse() response.Response {
return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{
Status: "success",
})
}
// getWorkingFolderUID returns the value of the folderUIDHeader
// if present. Otherwise, it returns the UID of the root folder.
func getWorkingFolderUID(c *contextmodel.ReqContext) string {
folderUID := strings.TrimSpace(c.Req.Header.Get(folderUIDHeader))
if folderUID != "" {
return folderUID
}
return folder.RootFolderUID
}
func namespaceErrorResponse(err error) response.Response {
if errors.Is(err, dashboards.ErrFolderNotFound) {
return response.Empty(http.StatusNotFound)
}
return toNamespaceErrorResponse(err)
}
@@ -397,8 +397,8 @@ func TestRouteConvertPrometheusGetNamespace(t *testing.T) {
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})
require.Contains(t, respNamespaces, fldr.Title)
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
})
}
@@ -442,17 +442,53 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
},
}
t.Run("with no rules should return empty response", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
rc := createRequestCtx()
assertEmptyResponse := func(t *testing.T, srv *ConvertPrometheusSrv, reqCtx *contextmodel.ReqContext) {
t.Helper()
response := srv.RouteConvertPrometheusGetRules(rc)
response := srv.RouteConvertPrometheusGetRules(reqCtx)
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)
}
// testForEmptyResponses tests that RouteConvertPrometheusGetRules returns an empty response
// when there are no rules in the folder or the folder does not exist.
testForEmptyResponses := func(t *testing.T, withCustomFolderHeader bool) {
rc := createRequestCtx()
unknownFolderUID := "some unknown folder"
rootFolderUID := ""
if withCustomFolderHeader {
rootFolderUID = unknownFolderUID
rc.Context.Req.Header.Set(folderUIDHeader, unknownFolderUID)
}
t.Run("for non-existent folder should return empty response", func(t *testing.T) {
srv, _, _, _ := createConvertPrometheusSrv(t)
assertEmptyResponse(t, srv, rc)
})
t.Run("for existing folder with no children should return empty response", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
fldr := randFolder()
fldr.UID = unknownFolderUID
fldr.ParentUID = rootFolderUID
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
assertEmptyResponse(t, srv, rc)
})
}
t.Run("without custom root folder", func(t *testing.T) {
testForEmptyResponses(t, false)
})
t.Run("with custom root folder", func(t *testing.T) {
testForEmptyResponses(t, true)
})
t.Run("with rules should return 200 with rules", func(t *testing.T) {
@@ -489,8 +525,8 @@ func TestRouteConvertPrometheusGetRules(t *testing.T) {
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})
require.Contains(t, respNamespaces, fldr.Title)
require.ElementsMatch(t, respNamespaces[fldr.Title], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
})
}
@@ -503,6 +539,20 @@ func TestRouteConvertPrometheusDeleteNamespace(t *testing.T) {
require.Equal(t, http.StatusNotFound, response.Status())
})
t.Run("for existing folder with no groups should return 404", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
rc := createRequestCtx()
fldr := randFolder()
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
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...)
@@ -596,6 +646,20 @@ func TestRouteConvertPrometheusDeleteRuleGroup(t *testing.T) {
require.Equal(t, http.StatusNotFound, response.Status())
})
t.Run("for existing folder with no group should return 404", func(t *testing.T) {
srv, _, ruleStore, folderService := createConvertPrometheusSrv(t)
rc := createRequestCtx()
fldr := randFolder()
fldr.ParentUID = ""
folderService.ExpectedFolder = fldr
folderService.ExpectedFolders = []*folder.Folder{fldr}
ruleStore.Folders[1] = append(ruleStore.Folders[1], fldr)
response := srv.RouteConvertPrometheusDeleteRuleGroup(rc, fldr.Title, "test-group")
require.Equal(t, http.StatusNotFound, response.Status())
})
const groupName = "test-group"
t.Run("valid request should delete rules", func(t *testing.T) {
@@ -758,3 +822,39 @@ func createRequestCtx() *contextmodel.ReqContext {
SignedInUser: &user.SignedInUser{OrgID: 1},
}
}
func TestGetWorkingFolderUID(t *testing.T) {
t.Run("should return root folder UID when header is not present", func(t *testing.T) {
rc := createRequestCtx()
rc.Req.Header.Del(folderUIDHeader)
folderUID := getWorkingFolderUID(rc)
require.Equal(t, folder.RootFolderUID, folderUID)
})
t.Run("should return specified folder UID when header is present", func(t *testing.T) {
rc := createRequestCtx()
specifiedFolderUID := "specified-folder-uid"
rc.Req.Header.Set(folderUIDHeader, specifiedFolderUID)
folderUID := getWorkingFolderUID(rc)
require.Equal(t, specifiedFolderUID, folderUID)
})
t.Run("should return root folder UID when header is empty", func(t *testing.T) {
rc := createRequestCtx()
rc.Req.Header.Set(folderUIDHeader, "")
folderUID := getWorkingFolderUID(rc)
require.Equal(t, folder.RootFolderUID, folderUID)
})
t.Run("should trim whitespace from header value", func(t *testing.T) {
rc := createRequestCtx()
specifiedFolderUID := "specified-folder-uid"
rc.Req.Header.Set(folderUIDHeader, " "+specifiedFolderUID+" ")
folderUID := getWorkingFolderUID(rc)
require.Equal(t, specifiedFolderUID, folderUID)
})
}
+5 -7
View File
@@ -151,13 +151,11 @@ func (api *API) authorize(method, path string) web.Handler {
http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}/{Group}",
http.MethodDelete + "/api/convert/prometheus/config/v1/rules/{NamespaceTitle}",
http.MethodDelete + "/api/convert/api/prom/rules/{NamespaceTitle}":
eval = ac.EvalAny(
ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
ac.EvalPermission(dashboards.ActionFoldersRead),
ac.EvalPermission(ac.ActionAlertingRuleDelete),
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
),
eval = ac.EvalAll(
ac.EvalPermission(ac.ActionAlertingRuleRead),
ac.EvalPermission(dashboards.ActionFoldersRead),
ac.EvalPermission(ac.ActionAlertingRuleDelete),
ac.EvalPermission(ac.ActionAlertingProvisioningSetStatus),
)
// Alert Instances and Silences
+4 -2
View File
@@ -15,8 +15,10 @@ type RuleStore interface {
// by returning map[string]struct{} instead of map[string]*folder.Folder
GetUserVisibleNamespaces(context.Context, int64, identity.Requester) (map[string]*folder.Folder, error)
GetNamespaceByUID(ctx context.Context, uid string, orgID int64, user identity.Requester) (*folder.Folder, error)
GetNamespaceInRootByTitle(ctx context.Context, fullpath string, orgID int64, user identity.Requester) (*folder.Folder, error)
GetOrCreateNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error)
GetNamespaceByTitle(ctx context.Context, fullpath string, orgID int64, user identity.Requester, parentUID string) (*folder.Folder, error)
GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.Folder, error)
// GetNamespaceChildren returns all children (first level) of the namespace with the given id.
GetNamespaceChildren(ctx context.Context, uid string, orgID int64, user identity.Requester) ([]*folder.Folder, error)
GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) (*ngmodels.AlertRule, error)
GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmodels.GetAlertRulesGroupByRuleUIDQuery) ([]*ngmodels.AlertRule, error)
+27 -9
View File
@@ -40,10 +40,10 @@ func (st DBstore) GetNamespaceByUID(ctx context.Context, uid string, orgID int64
return f[0], nil
}
// GetNamespaceInRootByTitle gets namespace by its title in the root folder.
func (st DBstore) GetNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
// GetNamespaceChildren gets namespace (folder) children (first level) by its UID.
func (st DBstore) GetNamespaceChildren(ctx context.Context, uid string, orgID int64, user identity.Requester) ([]*folder.Folder, error) {
q := &folder.GetChildrenQuery{
UID: folder.RootFolderUID,
UID: uid,
OrgID: orgID,
SignedInUser: user,
}
@@ -52,15 +52,32 @@ func (st DBstore) GetNamespaceInRootByTitle(ctx context.Context, title string, o
return nil, err
}
found := make([]*folder.Folder, 0, len(folders))
for _, f := range folders {
if f.ParentUID == uid {
found = append(found, f)
}
}
return found, nil
}
// GetNamespaceByTitle gets namespace by its title in the specified folder.
func (st DBstore) GetNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.Folder, error) {
folders, err := st.GetNamespaceChildren(ctx, parentUID, orgID, user)
if err != nil {
return nil, err
}
foundByTitle := []*folder.Folder{}
for _, f := range folders {
if f.Title == title && f.ParentUID == folder.RootFolderUID {
if f.Title == title {
foundByTitle = append(foundByTitle, f)
}
}
if len(foundByTitle) == 0 {
return nil, dashboards.ErrFolderAccessDenied
return nil, dashboards.ErrFolderNotFound
}
// Sort by UID to return the first folder in case of multiple folders with the same title
@@ -71,13 +88,13 @@ func (st DBstore) GetNamespaceInRootByTitle(ctx context.Context, title string, o
return foundByTitle[0], nil
}
// GetOrCreateNamespaceInRootByTitle gets or creates a namespace by title in the _root_ folder.
func (st DBstore) GetOrCreateNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
// GetOrCreateNamespaceByTitle gets or creates a namespace by title in the specified folder.
func (st DBstore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.Folder, error) {
var f *folder.Folder
var err error
f, err = st.GetNamespaceInRootByTitle(ctx, title, orgID, user)
if err != nil && !errors.Is(err, dashboards.ErrFolderAccessDenied) {
f, err = st.GetNamespaceByTitle(ctx, title, orgID, user, parentUID)
if err != nil && !errors.Is(err, dashboards.ErrFolderNotFound) {
return nil, err
}
@@ -86,6 +103,7 @@ func (st DBstore) GetOrCreateNamespaceInRootByTitle(ctx context.Context, title s
OrgID: orgID,
Title: title,
SignedInUser: user,
ParentUID: parentUID,
}
f, err = st.FolderService.Create(ctx, cmd)
if err != nil {
+210 -11
View File
@@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/setting"
@@ -123,7 +124,7 @@ func TestIntegration_GetNamespaceByUID(t *testing.T) {
})
}
func TestIntegration_GetNamespaceInRootByTitle(t *testing.T) {
func TestIntegration_GetNamespaceByTitle(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -143,17 +144,45 @@ func TestIntegration_GetNamespaceInRootByTitle(t *testing.T) {
IsGrafanaAdmin: true,
}
uid := uuid.NewString()
title := "folder-title"
createFolder(t, store, uid, title, 1, "")
// Create parent folder
parentUID := uuid.NewString()
parentTitle := "parent-folder"
createFolder(t, store, parentUID, parentTitle, 1, "")
actual, err := store.GetNamespaceInRootByTitle(context.Background(), title, 1, u)
require.NoError(t, err)
require.Equal(t, title, actual.Title)
require.Equal(t, uid, actual.UID)
// Create child folder under parent
childUID := uuid.NewString()
childTitle := "child-folder"
createFolder(t, store, childUID, childTitle, 1, parentUID)
// Create another folder with same title but under root
sameTitleInRoot := uuid.NewString()
createFolder(t, store, sameTitleInRoot, childTitle, 1, "")
t.Run("should find folder by title and parent UID", func(t *testing.T) {
actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, parentUID)
require.NoError(t, err)
require.Equal(t, childTitle, actual.Title)
require.Equal(t, childUID, actual.UID)
require.Equal(t, parentUID, actual.ParentUID)
})
t.Run("should find folder by title in root", func(t *testing.T) {
actual, err := store.GetNamespaceByTitle(context.Background(), childTitle, 1, u, folder.RootFolderUID)
require.NoError(t, err)
require.Equal(t, childTitle, actual.Title)
require.Equal(t, sameTitleInRoot, actual.UID)
require.Equal(t, folder.RootFolderUID, actual.ParentUID)
})
t.Run("should return ErrFolderNotFound when folder with title doesn't exist under specified parent", func(t *testing.T) {
nonExistentTitle := "non-existent-folder"
f, err := store.GetNamespaceByTitle(context.Background(), nonExistentTitle, 1, u, parentUID)
require.Nil(t, f)
require.ErrorIs(t, err, dashboards.ErrFolderNotFound)
})
}
func TestIntegration_GetOrCreateNamespaceInRootByTitle(t *testing.T) {
func TestIntegration_GetOrCreateNamespaceByTitle(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
@@ -180,10 +209,11 @@ func TestIntegration_GetOrCreateNamespaceInRootByTitle(t *testing.T) {
t.Run("should create folder when it does not exist", func(t *testing.T) {
store := setupStore(t)
f, err := store.GetOrCreateNamespaceInRootByTitle(context.Background(), "new folder", 1, u)
f, err := store.GetOrCreateNamespaceByTitle(context.Background(), "new folder", 1, u, folder.RootFolderUID)
require.NoError(t, err)
require.Equal(t, "new folder", f.Title)
require.NotEmpty(t, f.UID)
require.Equal(t, folder.RootFolderUID, f.ParentUID)
folders, err := store.FolderService.GetFolders(
context.Background(),
@@ -202,7 +232,7 @@ func TestIntegration_GetOrCreateNamespaceInRootByTitle(t *testing.T) {
title := "existing folder"
createFolder(t, store, "", title, 1, "")
f, err := store.GetOrCreateNamespaceInRootByTitle(context.Background(), title, 1, u)
f, err := store.GetOrCreateNamespaceByTitle(context.Background(), title, 1, u, folder.RootFolderUID)
require.NoError(t, err)
require.Equal(t, title, f.Title)
@@ -217,4 +247,173 @@ func TestIntegration_GetOrCreateNamespaceInRootByTitle(t *testing.T) {
require.NoError(t, err)
require.Len(t, folders, 1)
})
t.Run("should create folder under specified parent when it does not exist", func(t *testing.T) {
store := setupStore(t)
// Create parent folder first
parentTitle := "parent folder"
parentFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), parentTitle, 1, u, folder.RootFolderUID)
require.NoError(t, err)
// Now create a child folder under the parent
childTitle := "child folder"
childFolder, err := store.GetOrCreateNamespaceByTitle(context.Background(), childTitle, 1, u, parentFolder.UID)
require.NoError(t, err)
// Verify the child folder was created under the parent
folders, err := store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: parentFolder.UID, OrgID: 1, SignedInUser: u})
require.NoError(t, err)
require.Len(t, folders, 1)
require.Equal(t, childFolder.UID, folders[0].UID)
folders, err = store.FolderService.GetChildren(context.Background(), &folder.GetChildrenQuery{UID: folder.RootFolderUID, OrgID: 1, SignedInUser: u})
require.NoError(t, err)
require.Len(t, folders, 1)
require.Equal(t, parentFolder.UID, folders[0].UID)
})
t.Run("should get correct folder when same title exists under different parents", func(t *testing.T) {
store := setupStore(t)
// Create first parent folder
parent1Title := "parent folder 1"
parent1, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent1Title, 1, u, folder.RootFolderUID)
require.NoError(t, err)
// Create second parent folder
parent2Title := "parent folder 2"
parent2, err := store.GetOrCreateNamespaceByTitle(context.Background(), parent2Title, 1, u, folder.RootFolderUID)
require.NoError(t, err)
// Create folders with same title under different parents
sameTitle := "same title folder"
// Create under first parent
folder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID)
require.NoError(t, err)
// Create under second parent
folder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID)
require.NoError(t, err)
// Create under root
folder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID)
require.NoError(t, err)
// Verify we get the correct folders when specifying the parent
gotFolder1, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent1.UID)
require.NoError(t, err)
require.Equal(t, folder1.UID, gotFolder1.UID)
require.Equal(t, parent1.UID, gotFolder1.ParentUID)
gotFolder2, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, parent2.UID)
require.NoError(t, err)
require.Equal(t, folder2.UID, gotFolder2.UID)
require.Equal(t, parent2.UID, gotFolder2.ParentUID)
gotFolder3, err := store.GetOrCreateNamespaceByTitle(context.Background(), sameTitle, 1, u, folder.RootFolderUID)
require.NoError(t, err)
require.Equal(t, folder3.UID, gotFolder3.UID)
require.Equal(t, folder.RootFolderUID, gotFolder3.ParentUID)
})
}
func TestIntegration_GetNamespaceChildren(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
folderService := setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures())
b := &fakeBus{}
logger := log.New("test-dbstore")
store := createTestStore(sqlStore, folderService, logger, cfg.UnifiedAlerting, b)
store.FolderService = setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures(featuremgmt.FlagNestedFolders))
admin := &user.SignedInUser{
UserID: 1,
OrgID: 1,
OrgRole: org.RoleAdmin,
IsGrafanaAdmin: true,
}
// Create root folders
rootFolder1 := uuid.NewString()
rootFolder2 := uuid.NewString()
createFolder(t, store, rootFolder1, "Root Folder 1", 1, "")
createFolder(t, store, rootFolder2, "Root Folder 2", 1, "")
// Create child folders under root folder 1
child1 := uuid.NewString()
child2 := uuid.NewString()
createFolder(t, store, child1, "Child Folder 1", 1, rootFolder1)
createFolder(t, store, child2, "Child Folder 2", 1, rootFolder1)
// Create nested child under child1
nestedChild := uuid.NewString()
createFolder(t, store, nestedChild, "Nested Child", 1, child1)
differentOrgID := int64(999)
createFolder(t, store, util.GenerateShortUID(), "Root Folder 1", differentOrgID, "")
/*
* Folder structure:
*
* Root Folder 1
* - Child Folder 1
* - Nested Child
* - Child Folder 2
* Root Folder 2
*/
t.Run("should return direct children of a folder", func(t *testing.T) {
children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, 1, admin)
require.NoError(t, err)
require.Len(t, children, 2)
require.ElementsMatch(t, []string{child1, child2}, []string{children[0].UID, children[1].UID})
// Verify parent UID
for _, child := range children {
require.Equal(t, rootFolder1, child.ParentUID)
}
})
t.Run("should return direct children of a nested folder", func(t *testing.T) {
children, err := store.GetNamespaceChildren(context.Background(), child1, 1, admin)
require.NoError(t, err)
require.Len(t, children, 1)
require.Equal(t, nestedChild, children[0].UID)
require.Equal(t, child1, children[0].ParentUID)
})
t.Run("should return nil when folder does not exist", func(t *testing.T) {
nonExistentUID := uuid.NewString()
children, err := store.GetNamespaceChildren(context.Background(), nonExistentUID, 1, admin)
require.NotNil(t, children)
require.Empty(t, children)
require.Nil(t, err)
})
t.Run("should return empty array for folders with no children", func(t *testing.T) {
children, err := store.GetNamespaceChildren(context.Background(), rootFolder2, 1, admin)
require.Empty(t, children)
require.NotNil(t, children)
require.Nil(t, err)
})
t.Run("should return no children for a different org", func(t *testing.T) {
children, err := store.GetNamespaceChildren(context.Background(), rootFolder1, differentOrgID, admin)
require.Empty(t, children)
require.Nil(t, err)
})
t.Run("should return children from root folder", func(t *testing.T) {
children, err := store.GetNamespaceChildren(context.Background(), "", 1, admin)
require.NoError(t, err)
require.Equal(t, len(children), 2)
require.ElementsMatch(t, []string{rootFolder1, rootFolder2}, []string{children[0].UID, children[1].UID})
})
}
+28 -8
View File
@@ -268,33 +268,34 @@ func (f *RuleStore) GetNamespaceByUID(_ context.Context, uid string, orgID int64
return nil, fmt.Errorf("not found")
}
func (f *RuleStore) GetOrCreateNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
func (f *RuleStore) GetOrCreateNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.Folder, error) {
f.mtx.Lock()
defer f.mtx.Unlock()
for _, folder := range f.Folders[orgID] {
if folder.Title == title {
if folder.Title == title && folder.ParentUID == parentUID {
return folder, nil
}
}
newFolder := &folder.Folder{
ID: rand.Int63(), // nolint:staticcheck
UID: util.GenerateShortUID(),
Title: title,
Fullpath: "fullpath_" + title,
ID: rand.Int63(), // nolint:staticcheck
UID: util.GenerateShortUID(),
Title: title,
ParentUID: parentUID,
Fullpath: "fullpath_" + title,
}
f.Folders[orgID] = append(f.Folders[orgID], newFolder)
return newFolder, nil
}
func (f *RuleStore) GetNamespaceInRootByTitle(ctx context.Context, title string, orgID int64, user identity.Requester) (*folder.Folder, error) {
func (f *RuleStore) GetNamespaceByTitle(ctx context.Context, title string, orgID int64, user identity.Requester, parentUID string) (*folder.Folder, error) {
f.mtx.Lock()
defer f.mtx.Unlock()
for _, folder := range f.Folders[orgID] {
if folder.Title == title && folder.ParentUID == "" {
if folder.Title == title && folder.ParentUID == parentUID {
return folder, nil
}
}
@@ -302,6 +303,25 @@ func (f *RuleStore) GetNamespaceInRootByTitle(ctx context.Context, title string,
return nil, dashboards.ErrFolderNotFound
}
func (f *RuleStore) GetNamespaceChildren(ctx context.Context, uid string, orgID int64, user identity.Requester) ([]*folder.Folder, error) {
f.mtx.Lock()
defer f.mtx.Unlock()
result := []*folder.Folder{}
for _, folder := range f.Folders[orgID] {
if folder.ParentUID == uid {
result = append(result, folder)
}
}
if len(result) == 0 {
return nil, dashboards.ErrFolderNotFound
}
return result, nil
}
func (f *RuleStore) UpdateAlertRules(_ context.Context, _ *models.UserUID, q []models.UpdateRule) error {
f.mtx.Lock()
defer f.mtx.Unlock()
@@ -135,14 +135,11 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
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)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
// 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)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
// And a non-provisioned rule in another namespace
namespace3UID := util.GenerateShortUID()
@@ -150,18 +147,18 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
createRule(t, apiClient, namespace3UID)
// Now get the first group
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name)
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup1.Name, nil)
require.Equal(t, promGroup1, group1)
// Get namespace1
ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1)
ns1 := apiClient.ConvertPrometheusGetNamespaceRules(t, namespace1, nil)
expectedNs1 := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
}
require.Equal(t, expectedNs1, ns1)
// Get all namespaces
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
namespaces := apiClient.ConvertPrometheusGetAllRules(t, nil)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup1, promGroup2},
namespace2: {promGroup3},
@@ -170,22 +167,21 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
})
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)
_, status, raw := viewerClient.RawConvertPrometheusPostRuleGroup(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)
// Create three groups
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name)
// delete the first one
apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name, nil)
// Check that the promGroup2 and promGroup3 are still there
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
namespaces := apiClient.ConvertPrometheusGetAllRules(t, nil)
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
namespace2: {promGroup3},
@@ -193,10 +189,10 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
require.Equal(t, expectedNamespaces, namespaces)
// Delete the second namespace
apiClient.ConvertPrometheusDeleteNamespace(t, namespace2)
apiClient.ConvertPrometheusDeleteNamespace(t, namespace2, nil)
// Check that only the first namespace is left
namespaces = apiClient.ConvertPrometheusGetAllRules(t)
namespaces = apiClient.ConvertPrometheusGetAllRules(t, nil)
expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{
namespace1: {promGroup2},
}
@@ -268,11 +264,10 @@ func TestIntegrationConvertPrometheusEndpoints_UpdateRule(t *testing.T) {
t.Run("update a rule", func(t *testing.T) {
// Create the rule group
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup, nil)
// Now get the group
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup.Name)
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup.Name, nil)
require.Equal(t, promGroup, group1)
// Update the rule group interval
@@ -283,11 +278,10 @@ func TestIntegrationConvertPrometheusEndpoints_UpdateRule(t *testing.T) {
promGroup.Rules[0].Labels["another-label"] = "something"
promGroup.Rules[0].Annotations["another-annotation"] = "also-something"
// Update the group
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup, nil)
requireStatusCode(t, http.StatusAccepted, status, body)
apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup, nil)
// Now get the group again and check that the rule group has been updated
group1 = apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup.Name)
group1 = apiClient.ConvertPrometheusGetRuleGroupRules(t, namespace1, promGroup.Name, nil)
require.Equal(t, promGroup, group1)
})
}
@@ -374,7 +368,7 @@ func TestIntegrationConvertPrometheusEndpoints_Conflict(t *testing.T) {
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)
_, status, body := apiClient.RawConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
requireStatusCode(t, http.StatusConflict, status, body)
})
}
@@ -410,6 +404,7 @@ func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
apiClient.prometheusConversionUseLokiPaths = enableLokiPaths
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
@@ -499,3 +494,406 @@ func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
runTest(t, true)
})
}
func TestIntegrationConvertPrometheusEndpoints_FolderUIDHeader(t *testing.T) {
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
folderUIDHeader := "X-Grafana-Alerting-Folder-UID"
// 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)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
apiClient.prometheusConversionUseLokiPaths = enableLokiPaths
ds := apiClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
// Create a parent folder
parentFolderUID := util.GenerateShortUID()
parentFolderTitle := "parent-folder"
apiClient.CreateFolder(t, parentFolderUID, parentFolderTitle)
// Create a child folder inside the parent folder
childFolderUID := util.GenerateShortUID()
childFolderTitle := "child-folder"
apiClient.CreateFolder(t, childFolderUID, childFolderTitle, parentFolderUID)
// Create another folder in root
otherFolderUID := util.GenerateShortUID()
otherFolderTitle := "other-folder"
apiClient.CreateFolder(t, otherFolderUID, otherFolderTitle)
t.Run("create and delete rule groups with folder UID header", func(t *testing.T) {
// Post the namespace to parentFolderUID, it should create a new folder with the namespace name,
// and put the rule group in it.
headers := map[string]string{
folderUIDHeader: parentFolderUID,
}
apiClient.ConvertPrometheusPostRuleGroup(t, childFolderTitle, ds.Body.Datasource.UID, promGroup1, headers)
// Check that it's not visible when we get all namespaces from the root.
namespaces := apiClient.ConvertPrometheusGetAllRules(t, nil)
require.Empty(t, namespaces)
// Post the group2 to the root, it should create a new folder with the namespace name.
apiClient.ConvertPrometheusPostRuleGroup(t, otherFolderTitle, ds.Body.Datasource.UID, promGroup2, nil)
// Now we should have:
// - parentFolderUID/child-folder/test-group-1
// - other-folder/test-group-2
// Verify the rule group was created in the child folder
//
// First try to get the group in the root folder, it should not be found
_, status, resp := apiClient.RawConvertPrometheusGetRuleGroupRules(t, childFolderTitle, promGroup1.Name, nil)
require.Equal(t, http.StatusNotFound, status, resp)
// Now try to get the group in the child folder
group1 := apiClient.ConvertPrometheusGetRuleGroupRules(t, childFolderTitle, promGroup1.Name, headers)
require.Equal(t, promGroup1, group1)
// Verify the rule group was created in the other folder
group2 := apiClient.ConvertPrometheusGetRuleGroupRules(t, otherFolderTitle, promGroup2.Name, nil)
require.Equal(t, promGroup2, group2)
})
t.Run("empty folder UID header defaults to root", func(t *testing.T) {
// Create a folder at root level
rootFolderUID := util.GenerateShortUID()
rootFolderTitle := "root-folder"
apiClient.CreateFolder(t, rootFolderUID, rootFolderTitle)
// Use empty folder UID header which should default to root
headers := map[string]string{
folderUIDHeader: "",
}
apiClient.ConvertPrometheusPostRuleGroup(t, rootFolderTitle, ds.Body.Datasource.UID, promGroup3, headers)
// Verify the rule group was created
group := apiClient.ConvertPrometheusGetRuleGroupRules(t, rootFolderTitle, promGroup3.Name, headers)
require.Equal(t, promGroup3, group)
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
}
func TestIntegrationConvertPrometheusEndpoints_Delete(t *testing.T) {
runTest := func(t *testing.T, enableLokiPaths bool) {
testinfra.SQLiteIntegrationTest(t)
// 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 with different permissions
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "admin",
})
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "password")
adminClient.prometheusConversionUseLokiPaths = enableLokiPaths
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "editor",
})
editorClient := newAlertingApiClient(grafanaListedAddr, "editor", "password")
editorClient.prometheusConversionUseLokiPaths = enableLokiPaths
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "password",
Login: "viewer",
})
viewerClient := newAlertingApiClient(grafanaListedAddr, "viewer", "password")
viewerClient.prometheusConversionUseLokiPaths = enableLokiPaths
// Create a user with no access
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleNone),
Password: "password",
Login: "no-role-user",
})
noRoleClient := newAlertingApiClient(grafanaListedAddr, "no-role-user", "password")
noRoleClient.prometheusConversionUseLokiPaths = enableLokiPaths
ds := adminClient.CreateDatasource(t, datasources.DS_PROMETHEUS)
t.Run("delete non-existent namespace returns 404", func(t *testing.T) {
nonExistentNamespace := "non-existent-namespace-" + util.GenerateShortUID()
_, status, raw := adminClient.RawConvertPrometheusDeleteNamespace(t, nonExistentNamespace, nil)
requireStatusCode(t, http.StatusNotFound, status, raw)
})
t.Run("delete non-existent rule group returns not found", func(t *testing.T) {
nonExistentNamespace := "non-existent-namespace-" + util.GenerateShortUID()
nonExistentGroup := "non-existent-group-" + util.GenerateShortUID()
_, status, raw := adminClient.RawConvertPrometheusDeleteRuleGroup(t, nonExistentNamespace, nonExistentGroup, nil)
requireStatusCode(t, http.StatusNotFound, status, raw)
})
t.Run("delete rule group from existing namespace that has no rule groups", func(t *testing.T) {
// Create a namespace but don't add any rule groups to it
emptyNamespace := "empty-namespace-" + util.GenerateShortUID()
emptyNamespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, emptyNamespaceUID, emptyNamespace)
// Try to delete a non-existent rule group from that namespace
nonExistentGroup := "non-existent-group-" + util.GenerateShortUID()
_, status, raw := adminClient.RawConvertPrometheusDeleteRuleGroup(t, emptyNamespace, nonExistentGroup, nil)
requireStatusCode(t, http.StatusNotFound, status, raw)
})
t.Run("delete rule group then verify it's gone", func(t *testing.T) {
// Create namespace and rule group
namespace := "test-namespace-delete-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
// Create rule group
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
// Verify the rule group exists
group := adminClient.ConvertPrometheusGetRuleGroupRules(t, namespace, promGroup1.Name, nil)
require.Equal(t, promGroup1.Name, group.Name)
// Delete the rule group
adminClient.ConvertPrometheusDeleteRuleGroup(t, namespace, promGroup1.Name, nil)
// Verify the rule group is gone
_, status, _ := adminClient.RawConvertPrometheusGetRuleGroupRules(t, namespace, promGroup1.Name, nil)
require.Equal(t, http.StatusNotFound, status)
})
t.Run("delete namespace then verify it's empty", func(t *testing.T) {
// Create namespace with two rule groups
namespace := "test-namespace-delete-all-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
// Create rule groups
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup2, nil)
// Verify the namespace has rule groups
groups := adminClient.ConvertPrometheusGetNamespaceRules(t, namespace, nil)
require.ElementsMatch(t, groups[namespace], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
// Delete the namespace
adminClient.ConvertPrometheusDeleteNamespace(t, namespace, nil)
// Verify the namespace is empty
namespaces := adminClient.ConvertPrometheusGetAllRules(t, nil)
_, exists := namespaces[namespace]
require.False(t, exists)
})
t.Run("delete specific rule group leaves other groups intact", func(t *testing.T) {
// Create namespace with two rule groups
namespace := "test-namespace-delete-one-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
// Create rule groups
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup2, nil)
// Delete one rule group
adminClient.ConvertPrometheusDeleteRuleGroup(t, namespace, promGroup1.Name, nil)
// Verify the other rule group still exists
groups := adminClient.ConvertPrometheusGetNamespaceRules(t, namespace, nil)
require.ElementsMatch(t, groups[namespace], []apimodels.PrometheusRuleGroup{promGroup2})
})
t.Run("viewer cannot delete rule groups", func(t *testing.T) {
// Create namespace and rule group as admin
namespace := "test-namespace-viewer-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
// Try to delete as viewer - this should return 403 Forbidden
_, status, body := viewerClient.RawConvertPrometheusDeleteRuleGroup(t, namespace, promGroup1.Name, nil)
requireStatusCode(t, http.StatusForbidden, status, body)
// Verify the rule group still exists
group := adminClient.ConvertPrometheusGetRuleGroupRules(t, namespace, promGroup1.Name, nil)
require.Equal(t, promGroup1.Name, group.Name)
})
t.Run("viewer cannot delete namespaces", func(t *testing.T) {
// Create namespace and rule group as admin
namespace := "test-namespace-viewer-ns-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
// Try to delete as viewer - this should return 403 Forbidden
_, status, body := viewerClient.RawConvertPrometheusDeleteNamespace(t, namespace, nil)
requireStatusCode(t, http.StatusForbidden, status, body)
// Verify the namespace still exists
namespaces := adminClient.ConvertPrometheusGetAllRules(t, nil)
_, exists := namespaces[namespace]
require.True(t, exists)
})
t.Run("deleting rule group with nested folder structure using header", func(t *testing.T) {
// Create parent folder
parentFolder := "parent-folder-" + util.GenerateShortUID()
parentFolderUID := util.GenerateShortUID()
adminClient.CreateFolder(t, parentFolderUID, parentFolder)
// Create child folder inside parent
childFolder := "child-folder-" + util.GenerateShortUID()
childFolderUID := util.GenerateShortUID()
adminClient.CreateFolder(t, childFolderUID, childFolder, parentFolderUID)
// Create rule group in child folder
headers := map[string]string{
"X-Grafana-Alerting-Folder-UID": parentFolderUID,
}
adminClient.ConvertPrometheusPostRuleGroup(t, childFolder, ds.Body.Datasource.UID, promGroup1, headers)
// Verify the rule group exists
group := adminClient.ConvertPrometheusGetRuleGroupRules(t, childFolder, promGroup1.Name, headers)
require.Equal(t, promGroup1.Name, group.Name)
// Delete the rule group
adminClient.ConvertPrometheusDeleteRuleGroup(t, childFolder, promGroup1.Name, headers)
// Verify the rule group is gone
_, status, _ := adminClient.RawConvertPrometheusGetRuleGroupRules(t, childFolder, promGroup1.Name, headers)
require.Equal(t, http.StatusNotFound, status)
})
t.Run("deleting namespace with nested folder structure using header", func(t *testing.T) {
// Create parent folder
parentFolder := "parent-folder-ns-" + util.GenerateShortUID()
parentFolderUID := util.GenerateShortUID()
adminClient.CreateFolder(t, parentFolderUID, parentFolder)
// Create child folder inside parent
childFolder := "child-folder-ns-" + util.GenerateShortUID()
childFolderUID := util.GenerateShortUID()
adminClient.CreateFolder(t, childFolderUID, childFolder, parentFolderUID)
// Create rule groups in child folder
headers := map[string]string{
"X-Grafana-Alerting-Folder-UID": parentFolderUID,
}
adminClient.ConvertPrometheusPostRuleGroup(t, childFolder, ds.Body.Datasource.UID, promGroup1, headers)
adminClient.ConvertPrometheusPostRuleGroup(t, childFolder, ds.Body.Datasource.UID, promGroup2, headers)
// And a rule group in the parent folder
adminClient.ConvertPrometheusPostRuleGroup(t, parentFolder, ds.Body.Datasource.UID, promGroup3, nil)
// Verify both namespaces have rule groups
groups := adminClient.ConvertPrometheusGetNamespaceRules(t, childFolder, headers)
require.ElementsMatch(t, groups[childFolder], []apimodels.PrometheusRuleGroup{promGroup1, promGroup2})
require.Empty(t, groups[parentFolder])
parentGroups := adminClient.ConvertPrometheusGetNamespaceRules(t, parentFolder, nil)
require.Empty(t, parentGroups[childFolder])
require.ElementsMatch(t, parentGroups[parentFolder], []apimodels.PrometheusRuleGroup{promGroup3})
// Delete the child namespace
adminClient.ConvertPrometheusDeleteNamespace(t, childFolder, headers)
// Verify the namespace is empty
namespaces := adminClient.ConvertPrometheusGetAllRules(t, headers)
_, exists := namespaces[childFolder]
require.False(t, exists)
// But the parent folder still has its rule group
parentGroups = adminClient.ConvertPrometheusGetNamespaceRules(t, parentFolder, nil)
require.ElementsMatch(t, parentGroups[parentFolder], []apimodels.PrometheusRuleGroup{promGroup3})
})
t.Run("editor can delete rule group they created", func(t *testing.T) {
// Create namespace as admin
namespace := "test-namespace-editor-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
// Create rule group as editor
editorClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
// Verify the rule group exists
group := editorClient.ConvertPrometheusGetRuleGroupRules(t, namespace, promGroup1.Name, nil)
require.Equal(t, promGroup1.Name, group.Name)
// Delete as editor
editorClient.ConvertPrometheusDeleteRuleGroup(t, namespace, promGroup1.Name, nil)
// Verify the rule group is gone
_, status, _ := editorClient.RawConvertPrometheusGetRuleGroupRules(t, namespace, promGroup1.Name, nil)
require.Equal(t, http.StatusNotFound, status)
})
t.Run("user with no role cannot delete rule groups", func(t *testing.T) {
// Create namespace and rule group as admin
namespace := "test-namespace-no-role-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
_, status, body := noRoleClient.RawConvertPrometheusDeleteRuleGroup(t, namespace, promGroup1.Name, nil)
requireStatusCode(t, http.StatusForbidden, status, body)
})
t.Run("user with no role cannot delete namespaces", func(t *testing.T) {
// Create namespace and rule group as admin
namespace := "test-namespace-no-role-ns-" + util.GenerateShortUID()
namespaceUID := util.GenerateShortUID()
adminClient.CreateFolder(t, namespaceUID, namespace)
adminClient.ConvertPrometheusPostRuleGroup(t, namespace, ds.Body.Datasource.UID, promGroup1, nil)
_, status, body := noRoleClient.RawConvertPrometheusDeleteNamespace(t, namespace, nil)
requireStatusCode(t, http.StatusForbidden, status, body)
})
}
t.Run("with the mimirtool paths", func(t *testing.T) {
runTest(t, false)
})
t.Run("with the cortextool Loki paths", func(t *testing.T) {
runTest(t, true)
})
}
+74 -12
View File
@@ -1102,14 +1102,28 @@ func (a apiClient) GetRuleByUID(t *testing.T, ruleUID string) apimodels.Gettable
return rule
}
func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) apimodels.ConvertPrometheusResponse {
t.Helper()
resp, status, body := a.RawConvertPrometheusPostRuleGroup(t, namespaceTitle, datasourceUID, promGroup, headers)
requireStatusCode(t, http.StatusAccepted, status, body)
return resp
}
func (a apiClient) RawConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle, datasourceUID string, promGroup apimodels.PrometheusRuleGroup, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules/%s"
if a.prometheusConversionUseLokiPaths {
path = "%s/api/convert/api/prom/rules/%s"
}
data, err := yaml.Marshal(promGroup)
require.NoError(t, err)
buf := bytes.NewReader(data)
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), buf)
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf(path, a.url, namespaceTitle), buf)
require.NoError(t, err)
req.Header.Add("X-Grafana-Alerting-Datasource-UID", datasourceUID)
@@ -1120,7 +1134,16 @@ func (a apiClient) ConvertPrometheusPostRuleGroup(t *testing.T, namespaceTitle,
return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
}
func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string) apimodels.PrometheusRuleGroup {
func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string, headers map[string]string) apimodels.PrometheusRuleGroup {
t.Helper()
rule, status, raw := a.RawConvertPrometheusGetRuleGroupRules(t, namespaceTitle, groupName, headers)
requireStatusCode(t, http.StatusOK, status, raw)
return rule
}
func (a apiClient) RawConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTitle, groupName string, headers map[string]string) (apimodels.PrometheusRuleGroup, int, string) {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules/%s/%s"
@@ -1130,12 +1153,17 @@ func (a apiClient) ConvertPrometheusGetRuleGroupRules(t *testing.T, namespaceTit
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil)
require.NoError(t, err)
for key, value := range headers {
req.Header.Add(key, value)
}
rule, status, raw := sendRequestYAML[apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
return rule
return rule, status, raw
}
func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string) map[string][]apimodels.PrometheusRuleGroup {
func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTitle string, headers map[string]string) map[string][]apimodels.PrometheusRuleGroup {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules/%s"
@@ -1145,12 +1173,18 @@ func (a apiClient) ConvertPrometheusGetNamespaceRules(t *testing.T, namespaceTit
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url, namespaceTitle), nil)
require.NoError(t, err)
for key, value := range headers {
req.Header.Add(key, value)
}
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 {
func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T, headers map[string]string) map[string][]apimodels.PrometheusRuleGroup {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules"
@@ -1160,12 +1194,25 @@ func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimo
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf(path, a.url), nil)
require.NoError(t, err)
for key, value := range headers {
req.Header.Add(key, value)
}
result, status, raw := sendRequestYAML[map[string][]apimodels.PrometheusRuleGroup](t, req, http.StatusOK)
requireStatusCode(t, http.StatusOK, status, raw)
return result
}
func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) {
func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string, headers map[string]string) {
t.Helper()
_, status, raw := a.RawConvertPrometheusDeleteRuleGroup(t, namespaceTitle, groupName, headers)
requireStatusCode(t, http.StatusAccepted, status, raw)
}
func (a apiClient) RawConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules/%s/%s"
@@ -1175,11 +1222,22 @@ func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle, groupName), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
for key, value := range headers {
req.Header.Add(key, value)
}
return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
}
func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string, headers map[string]string) {
t.Helper()
_, status, raw := a.RawConvertPrometheusDeleteNamespace(t, namespaceTitle, headers)
requireStatusCode(t, http.StatusAccepted, status, raw)
}
func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) {
func (a apiClient) RawConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string, headers map[string]string) (apimodels.ConvertPrometheusResponse, int, string) {
t.Helper()
path := "%s/api/convert/prometheus/config/v1/rules/%s"
@@ -1189,8 +1247,12 @@ func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf(path, a.url, namespaceTitle), nil)
require.NoError(t, err)
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
requireStatusCode(t, http.StatusAccepted, status, raw)
for key, value := range headers {
req.Header.Add(key, value)
}
return sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
}
func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) {