grafana/pkg/tests/api/alerting/api_ruler_test.go
Sofia Papagiannaki d1dab5828d
Alerting: Update rule API to address folders by UID (#74600)
* Change ruler API to expect the folder UID as namespace

* Update example requests

* Fix tests

* Update swagger

* Modify FIle field in /api/prometheus/grafana/api/v1/rules

* Fix ruler export

* Modify folder in responses to be formatted as <parent UID>/<title>

* Add alerting test with nested folders

* Apply suggestion from code review

* Alerting: use folder UID instead of title in rule API (#77166)

Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>

* Drop a few more latent uses of namespace_id

* move getNamespaceKey to models package

* switch GetAlertRulesForScheduling to use folder table

* update GetAlertRulesForScheduling to return folder titles in format `parent_uid/title`.

* fi tests

* add tests for GetAlertRulesForScheduling when parent uid

* fix integration tests after merge

* fix test after merge

* change format of the namespace to JSON array

this is needed for forward compatibility, when we migrate to full paths

* update EF code to decode nested folder

---------

Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com>
Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
Co-authored-by: Alex Weaver <weaver.alex.d@gmail.com>
Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
2024-01-17 11:07:39 +02:00

1751 lines
62 KiB
Go

package alerting
import (
"context"
"embed"
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"path"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
"github.com/grafana/grafana/pkg/services/datasources"
datasourceService "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/util"
)
//go:embed test-data/*.*
var testData embed.FS
func TestIntegrationAlertRulePermissions(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, p)
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder2", "folder2")
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-post.json"))
require.NoError(t, err)
var group1 apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &group1))
// Create rule under folder1
_, status, response := apiClient.PostRulesGroupWithStatus(t, "folder1", &group1)
require.Equalf(t, http.StatusAccepted, status, response)
postGroupRaw, err = testData.ReadFile(path.Join("test-data", "rulegroup-2-post.json"))
require.NoError(t, err)
var group2 apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &group2))
// Create rule under folder2
_, status, response = apiClient.PostRulesGroupWithStatus(t, "folder2", &group2)
require.Equalf(t, http.StatusAccepted, status, response)
// With the rules created, let's make sure that rule definitions are stored.
allRules, status, _ := apiClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
status, allExportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
})
require.Equal(t, http.StatusOK, status)
var allExport apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(allExportRaw), &allExport))
t.Run("when user has all permissions", func(t *testing.T) {
t.Run("Get all returns all rules", func(t *testing.T) {
var group1, group2 apimodels.GettableRuleGroupConfig
getGroup1Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-get.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup1Raw, &group1))
getGroup2Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-2-get.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup2Raw, &group2))
expected := apimodels.NamespaceConfigResponse{
"folder1": []apimodels.GettableRuleGroupConfig{
group1,
},
"folder2": []apimodels.GettableRuleGroupConfig{
group2,
},
}
pathsToIgnore := []string{
"GrafanaManagedAlert.Updated",
"GrafanaManagedAlert.UID",
"GrafanaManagedAlert.ID",
"GrafanaManagedAlert.Data.Model",
"GrafanaManagedAlert.NamespaceUID",
"GrafanaManagedAlert.NamespaceID",
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(expected, allRules, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.Contains(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
require.Empty(t, diff)
for _, rule := range allRules["folder1"][0].Rules {
assert.Equal(t, "folder1", rule.GrafanaManagedAlert.NamespaceUID)
}
for _, rule := range allRules["folder2"][0].Rules {
assert.Equal(t, "folder2", rule.GrafanaManagedAlert.NamespaceUID)
}
})
t.Run("Get by folder returns groups in folder", func(t *testing.T) {
rules, status, _ := apiClient.GetAllRulesGroupInFolderWithStatus(t, "folder1")
require.Equal(t, http.StatusAccepted, status)
require.Contains(t, rules, "folder1")
require.Len(t, rules["folder1"], 1)
require.Equal(t, allRules["folder1"], rules["folder1"])
})
t.Run("Get group returns a single group", func(t *testing.T) {
rules := apiClient.GetRulesGroup(t, "folder2", allRules["folder2"][0].Name)
cmp.Diff(allRules["folder2"][0], rules.GettableRuleGroupConfig)
})
t.Run("Export returns all rules", func(t *testing.T) {
var group1File, group2File apimodels.AlertingFileExport
getGroup1Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-export.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup1Raw, &group1File))
getGroup2Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-2-export.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup2Raw, &group2File))
group1File.Groups = append(group1File.Groups, group2File.Groups...)
expected := group1File
pathsToIgnore := []string{
"Groups.Rules.UID",
"Groups.Folder",
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(expected, allExport, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.Contains(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
require.Empty(t, diff)
require.Equal(t, "folder1", allExport.Groups[0].Folder)
require.Equal(t, "folder2", allExport.Groups[1].Folder)
})
t.Run("Export from one folder", func(t *testing.T) {
expected := allExport.Groups[0]
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder1"},
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Export from one group", func(t *testing.T) {
expected := allExport.Groups[0]
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder1"},
GroupName: expected.Name,
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Export single rule", func(t *testing.T) {
expected := allExport.Groups[0]
expected.Rules = []apimodels.AlertRuleExport{
expected.Rules[0],
}
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
RuleUID: expected.Rules[0].UID,
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
t.Log(exportRaw)
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
})
t.Run("when permissions for folder2 removed", func(t *testing.T) {
// remove permissions from folder2
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder2")
apiClient.ReloadCachedPermissions(t)
t.Run("Get all returns all rules", func(t *testing.T) {
newAll, status, _ := apiClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.NotContains(t, newAll, "folder2")
require.Contains(t, newAll, "folder1")
})
t.Run("Get by folder returns groups in folder", func(t *testing.T) {
_, status, _ := apiClient.GetAllRulesGroupInFolderWithStatus(t, "folder2")
require.Equal(t, http.StatusForbidden, status)
})
t.Run("Get group returns a single group", func(t *testing.T) {
u := fmt.Sprintf("%s/api/ruler/grafana/api/v1/rules/folder2/arulegroup", apiClient.url)
// nolint:gosec
resp, err := http.Get(u)
require.NoError(t, err)
defer func() {
_ = resp.Body.Close()
}()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("Export returns all rules", func(t *testing.T) {
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Equal(t, http.StatusOK, status)
require.Len(t, export.Groups, 1)
require.Equal(t, "folder1", export.Groups[0].Folder)
})
t.Run("Export from one folder", func(t *testing.T) {
status, _ := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder2"},
})
assert.Equal(t, http.StatusForbidden, status)
})
t.Run("Export from one group", func(t *testing.T) {
status, _ := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder2"},
GroupName: "arulegroup",
})
assert.Equal(t, http.StatusForbidden, status)
})
t.Run("Export single rule", func(t *testing.T) {
uid := allRules["folder2"][0].Rules[0].GrafanaManagedAlert.UID
status, _ := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
RuleUID: uid,
})
require.Equal(t, http.StatusForbidden, status)
})
t.Run("when all permissions are revoked", func(t *testing.T) {
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder1")
apiClient.ReloadCachedPermissions(t)
rules, status, _ := apiClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.Empty(t, rules)
status, _ = apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
})
require.Equal(t, http.StatusNotFound, status)
})
})
}
func TestIntegrationAlertRuleNestedPermissions(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{featuremgmt.FlagNestedFolders},
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, p)
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder2", "folder2")
// Create a subfolder
apiClient.CreateFolder(t, "subfolder", "subfolder", "folder1")
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-post.json"))
require.NoError(t, err)
var group1 apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &group1))
// Create rule under folder1
_, status, response := apiClient.PostRulesGroupWithStatus(t, "folder1", &group1)
require.Equalf(t, http.StatusAccepted, status, response)
postGroupRaw, err = testData.ReadFile(path.Join("test-data", "rulegroup-2-post.json"))
require.NoError(t, err)
var group2 apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &group2))
// Create rule under folder2
_, status, response = apiClient.PostRulesGroupWithStatus(t, "folder2", &group2)
require.Equalf(t, http.StatusAccepted, status, response)
postGroupRaw, err = testData.ReadFile(path.Join("test-data", "rulegroup-3-post.json"))
require.NoError(t, err)
var group3 apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(postGroupRaw, &group3))
// Create rule under subfolder
_, status, response = apiClient.PostRulesGroupWithStatus(t, "subfolder", &group3)
require.Equalf(t, http.StatusAccepted, status, response)
// With the rules created, let's make sure that rule definitions are stored.
allRules, status, _ := apiClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
status, allExportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
})
require.Equal(t, http.StatusOK, status)
var allExport apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(allExportRaw), &allExport))
t.Run("when user has all permissions", func(t *testing.T) {
t.Run("Get all returns all rules", func(t *testing.T) {
var group1, group2, group3 apimodels.GettableRuleGroupConfig
getGroup1Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-get.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup1Raw, &group1))
getGroup2Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-2-get.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup2Raw, &group2))
getGroup3Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-3-get.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup3Raw, &group3))
nestedKey := ngmodels.GetNamespaceKey("folder1", "subfolder")
expected := apimodels.NamespaceConfigResponse{
"folder1": []apimodels.GettableRuleGroupConfig{
group1,
},
"folder2": []apimodels.GettableRuleGroupConfig{
group2,
},
nestedKey: []apimodels.GettableRuleGroupConfig{
group3,
},
}
pathsToIgnore := []string{
"GrafanaManagedAlert.Updated",
"GrafanaManagedAlert.UID",
"GrafanaManagedAlert.ID",
"GrafanaManagedAlert.Data.Model",
"GrafanaManagedAlert.NamespaceUID",
"GrafanaManagedAlert.NamespaceID",
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(expected, allRules, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.Contains(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
require.Empty(t, diff)
for _, rule := range allRules["folder1"][0].Rules {
assert.Equal(t, "folder1", rule.GrafanaManagedAlert.NamespaceUID)
}
for _, rule := range allRules["folder2"][0].Rules {
assert.Equal(t, "folder2", rule.GrafanaManagedAlert.NamespaceUID)
}
for _, rule := range allRules[nestedKey][0].Rules {
assert.Equal(t, "subfolder", rule.GrafanaManagedAlert.NamespaceUID)
}
})
t.Run("Get by folder returns groups in folder", func(t *testing.T) {
rules, status, _ := apiClient.GetAllRulesGroupInFolderWithStatus(t, "folder1")
require.Equal(t, http.StatusAccepted, status)
require.Contains(t, rules, "folder1")
require.Len(t, rules["folder1"], 1)
require.Equal(t, allRules["folder1"], rules["folder1"])
})
t.Run("Get group returns a single group", func(t *testing.T) {
rules := apiClient.GetRulesGroup(t, "folder2", allRules["folder2"][0].Name)
cmp.Diff(allRules["folder2"][0], rules.GettableRuleGroupConfig)
})
t.Run("Get by folder returns groups in folder with nested folder format", func(t *testing.T) {
rules, status, _ := apiClient.GetAllRulesGroupInFolderWithStatus(t, "subfolder")
require.Equal(t, http.StatusAccepted, status)
nestedKey := ngmodels.GetNamespaceKey("folder1", "subfolder")
require.Contains(t, rules, nestedKey)
require.Len(t, rules[nestedKey], 1)
require.Equal(t, allRules[nestedKey], rules[nestedKey])
})
t.Run("Export returns all rules", func(t *testing.T) {
var group1File, group2File, group3File apimodels.AlertingFileExport
getGroup1Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-export.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup1Raw, &group1File))
getGroup2Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-2-export.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup2Raw, &group2File))
getGroup3Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-3-export.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup3Raw, &group3File))
group1File.Groups = append(group1File.Groups, group2File.Groups...)
group1File.Groups = append(group1File.Groups, group3File.Groups...)
expected := group1File
pathsToIgnore := []string{
"Groups.Rules.UID",
"Groups.Folder",
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(expected, allExport, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.Contains(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
require.Empty(t, diff)
require.Equal(t, "folder1", allExport.Groups[0].Folder)
require.Equal(t, "folder2", allExport.Groups[1].Folder)
require.Equal(t, "subfolder", allExport.Groups[2].Folder)
})
t.Run("Export from one folder", func(t *testing.T) {
expected := allExport.Groups[0]
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder1"},
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Export from a subfolder", func(t *testing.T) {
expected := allExport.Groups[2]
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"subfolder"},
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Export from one group", func(t *testing.T) {
expected := allExport.Groups[0]
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder1"},
GroupName: expected.Name,
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Export from one group under subfolder", func(t *testing.T) {
expected := allExport.Groups[2]
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"subfolder"},
GroupName: expected.Name,
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
t.Run("Export single rule", func(t *testing.T) {
expected := allExport.Groups[0]
expected.Rules = []apimodels.AlertRuleExport{
expected.Rules[0],
}
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
RuleUID: expected.Rules[0].UID,
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
t.Log(exportRaw)
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Len(t, export.Groups, 1)
require.Equal(t, expected, export.Groups[0])
})
})
t.Run("when permissions for folder2 removed", func(t *testing.T) {
// remove permissions for folder2
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder2")
// remove permissions for subfolder (inherits from folder1)
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "subfolder")
apiClient.ReloadCachedPermissions(t)
t.Run("Get all returns all rules", func(t *testing.T) {
newAll, status, _ := apiClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.Contains(t, newAll, "folder1")
require.NotContains(t, newAll, "folder2")
require.Contains(t, newAll, ngmodels.GetNamespaceKey("folder1", "subfolder"))
})
t.Run("Get by folder returns groups in folder", func(t *testing.T) {
_, status, _ := apiClient.GetAllRulesGroupInFolderWithStatus(t, "folder2")
require.Equal(t, http.StatusForbidden, status)
})
t.Run("Get group returns a single group", func(t *testing.T) {
u := fmt.Sprintf("%s/api/ruler/grafana/api/v1/rules/folder2/arulegroup", apiClient.url)
// nolint:gosec
resp, err := http.Get(u)
require.NoError(t, err)
defer func() {
_ = resp.Body.Close()
}()
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
t.Run("Export returns all rules", func(t *testing.T) {
status, exportRaw := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
})
require.Equal(t, http.StatusOK, status)
var export apimodels.AlertingFileExport
require.NoError(t, json.Unmarshal([]byte(exportRaw), &export))
require.Equal(t, http.StatusOK, status)
require.Len(t, export.Groups, 2)
require.Equal(t, "folder1", export.Groups[0].Folder)
require.Equal(t, "subfolder", export.Groups[1].Folder)
})
t.Run("Export from one folder", func(t *testing.T) {
status, _ := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder2"},
})
assert.Equal(t, http.StatusForbidden, status)
})
t.Run("Export from one group", func(t *testing.T) {
status, _ := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
FolderUID: []string{"folder2"},
GroupName: "arulegroup",
})
assert.Equal(t, http.StatusForbidden, status)
})
t.Run("Export single rule", func(t *testing.T) {
uid := allRules["folder2"][0].Rules[0].GrafanaManagedAlert.UID
status, _ := apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
RuleUID: uid,
})
require.Equal(t, http.StatusForbidden, status)
})
t.Run("when all permissions are revoked", func(t *testing.T) {
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder1")
apiClient.ReloadCachedPermissions(t)
rules, status, _ := apiClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.Empty(t, rules)
status, _ = apiClient.ExportRulesWithStatus(t, &apimodels.AlertRulesExportParameters{
ExportQueryParams: apimodels.ExportQueryParams{Format: "json"},
})
require.Equal(t, http.StatusNotFound, status)
})
})
}
func createRule(t *testing.T, client apiClient, folder string) (apimodels.PostableRuleGroupConfig, string) {
t.Helper()
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
doubleInterval := 2 * interval
rules := apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Interval: interval,
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &doubleInterval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: fmt.Sprintf("rule under folder %s", folder),
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
},
}
resp, status, _ := client.PostRulesGroupWithStatus(t, folder, &rules)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, resp.Created, 1)
return rules, resp.Created[0]
}
func TestAlertRulePostExport(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, p)
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")
var group1 apimodels.PostableRuleGroupConfig
group1Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-post.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(group1Raw, &group1))
t.Run("should return in export format", func(t *testing.T) {
var expected, actual apimodels.AlertingFileExport
getGroup1Raw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1-export.json"))
require.NoError(t, err)
require.NoError(t, json.Unmarshal(getGroup1Raw, &expected))
status, actualRaw := apiClient.PostRulesExportWithStatus(t, "folder1", &group1, &apimodels.ExportQueryParams{
Download: false,
Format: "json",
})
require.Equal(t, http.StatusOK, status)
require.NoError(t, json.Unmarshal([]byte(actualRaw), &actual))
pathsToIgnore := []string{
"Groups.Rules.UID",
"Groups.Folder",
"Data.Model", // Model is not amended with default values
}
// compare expected and actual and ignore the dynamic fields
diff := cmp.Diff(expected, actual, cmp.FilterPath(func(path cmp.Path) bool {
for _, s := range pathsToIgnore {
if strings.Contains(path.String(), s) {
return true
}
}
return false
}, cmp.Ignore()))
require.Empty(t, diff)
require.Equal(t, actual.Groups[0].Folder, "folder1")
})
t.Run("should return 403 when no access to folder", func(t *testing.T) {
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder1")
apiClient.ReloadCachedPermissions(t)
status, _ := apiClient.PostRulesExportWithStatus(t, "folder1", &group1, &apimodels.ExportQueryParams{
Download: false,
Format: "json",
})
require.Equal(t, http.StatusForbidden, status)
})
}
func TestIntegrationAlertRuleConflictingTitle(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
EnableQuota: true,
DisableAnonymous: true,
ViewersCanEdit: true,
AppModeProduction: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
// Create user
createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder1", "folder1")
// Create the namespace we'll save our alerts to.
apiClient.CreateFolder(t, "folder2", "folder2")
rules := newTestingRuleConfig(t)
respModel, status, _ := apiClient.PostRulesGroupWithStatus(t, "folder1", &rules)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, respModel.Created, len(rules.Rules))
// fetch the created rules, so we can get the uid's and trigger
// and update by reusing the uid's
createdRuleGroup := apiClient.GetRulesGroup(t, "folder1", rules.Name).GettableRuleGroupConfig
require.Len(t, createdRuleGroup.Rules, 2)
t.Run("trying to create alert with same title under same folder should fail", func(t *testing.T) {
rulesWithUID := convertGettableRuleGroupToPostable(createdRuleGroup)
rulesWithUID.Rules = append(rulesWithUID.Rules, rules.Rules[0]) // Create new copy of first rule.
_, status, body := apiClient.PostRulesGroupWithStatus(t, "folder1", &rulesWithUID)
assert.Equal(t, http.StatusInternalServerError, status)
var res map[string]any
require.NoError(t, json.Unmarshal([]byte(body), &res))
require.Equal(t, "failed to update rule group: failed to add rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique", res["message"])
})
t.Run("trying to update an alert to the title of an existing alert in the same folder should fail", func(t *testing.T) {
rulesWithUID := convertGettableRuleGroupToPostable(createdRuleGroup)
rulesWithUID.Rules[1].GrafanaManagedAlert.Title = "AlwaysFiring"
_, status, body := apiClient.PostRulesGroupWithStatus(t, "folder1", &rulesWithUID)
assert.Equal(t, http.StatusInternalServerError, status)
var res map[string]any
require.NoError(t, json.Unmarshal([]byte(body), &res))
require.Equal(t, "failed to update rule group: failed to update rules: a conflicting alert rule is found: rule title under the same organisation and folder should be unique", res["message"])
})
t.Run("trying to create alert with same title under another folder should succeed", func(t *testing.T) {
rules := newTestingRuleConfig(t)
resp, status, _ := apiClient.PostRulesGroupWithStatus(t, "folder2", &rules)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, resp.Created, len(rules.Rules))
})
t.Run("trying to swap titles of existing alerts in the same folder should work", func(t *testing.T) {
rulesWithUID := convertGettableRuleGroupToPostable(createdRuleGroup)
title0 := rulesWithUID.Rules[0].GrafanaManagedAlert.Title
title1 := rulesWithUID.Rules[1].GrafanaManagedAlert.Title
rulesWithUID.Rules[0].GrafanaManagedAlert.Title = title1
rulesWithUID.Rules[1].GrafanaManagedAlert.Title = title0
resp, status, _ := apiClient.PostRulesGroupWithStatus(t, "folder1", &rulesWithUID)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, resp.Updated, 2)
})
t.Run("trying to update titles of existing alerts in a chain in the same folder should work", func(t *testing.T) {
rulesWithUID := convertGettableRuleGroupToPostable(createdRuleGroup)
rulesWithUID.Rules[0].GrafanaManagedAlert.Title = rulesWithUID.Rules[1].GrafanaManagedAlert.Title
rulesWithUID.Rules[1].GrafanaManagedAlert.Title = "something new"
resp, status, _ := apiClient.PostRulesGroupWithStatus(t, "folder1", &rulesWithUID)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, resp.Updated, len(rulesWithUID.Rules))
})
}
func TestIntegrationRulerRulesFilterByDashboard(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
// Create a user to make authenticated requests
createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
dashboardUID := "default"
// Create the namespace under default organisation (orgID = 1) where we'll save our alerts to.
apiClient.CreateFolder(t, "default", "default")
interval, err := model.ParseDuration("10s")
require.NoError(t, err)
// Now, let's create some rules
{
rules := apimodels.PostableRuleGroupConfig{
Name: "anotherrulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{},
Annotations: map[string]string{
"__dashboardUid__": dashboardUID,
"__panelId__": "1",
},
},
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiringButSilenced",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
},
},
},
}
resp, status, _ := apiClient.PostRulesGroupWithStatus(t, "default", &rules)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, resp.Created, len(rules.Rules))
}
expectedAllJSON := fmt.Sprintf(`
{
"default": [{
"name": "anotherrulegroup",
"interval": "1m",
"rules": [{
"expr": "",
"for": "10s",
"annotations": {
"__dashboardUid__": "%s",
"__panelId__": "1"
},
"grafana_alert": {
"id": 1,
"orgId": 1,
"title": "AlwaysFiring",
"condition": "A",
"data": [{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "__expr__",
"model": {
"expression": "2 + 3 \u003e 1",
"intervalMs": 1000,
"maxDataPoints": 43200,
"type": "math"
}
}],
"updated": "2021-02-21T01:10:30Z",
"intervalSeconds": 60,
"is_paused": false,
"version": 1,
"uid": "uid",
"namespace_uid": "nsuid",
"rule_group": "anotherrulegroup",
"no_data_state": "NoData",
"exec_err_state": "Alerting"
}
}, {
"expr": "",
"for":"0s",
"grafana_alert": {
"id": 2,
"orgId": 1,
"title": "AlwaysFiringButSilenced",
"condition": "A",
"data": [{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "__expr__",
"model": {
"expression": "2 + 3 \u003e 1",
"intervalMs": 1000,
"maxDataPoints": 43200,
"type": "math"
}
}],
"updated": "2021-02-21T01:10:30Z",
"intervalSeconds": 60,
"is_paused": false,
"version": 1,
"uid": "uid",
"namespace_uid": "nsuid",
"rule_group": "anotherrulegroup",
"no_data_state": "Alerting",
"exec_err_state": "Alerting"
}
}]
}]
}`, dashboardUID)
expectedFilteredByJSON := fmt.Sprintf(`
{
"default": [{
"name": "anotherrulegroup",
"interval": "1m",
"rules": [{
"expr": "",
"for": "10s",
"annotations": {
"__dashboardUid__": "%s",
"__panelId__": "1"
},
"grafana_alert": {
"id": 1,
"orgId": 1,
"title": "AlwaysFiring",
"condition": "A",
"data": [{
"refId": "A",
"queryType": "",
"relativeTimeRange": {
"from": 18000,
"to": 10800
},
"datasourceUid": "__expr__",
"model": {
"expression": "2 + 3 \u003e 1",
"intervalMs": 1000,
"maxDataPoints": 43200,
"type": "math"
}
}],
"updated": "2021-02-21T01:10:30Z",
"intervalSeconds": 60,
"is_paused": false,
"version": 1,
"uid": "uid",
"namespace_uid": "nsuid",
"rule_group": "anotherrulegroup",
"no_data_state": "NoData",
"exec_err_state": "Alerting"
}
}]
}]
}`, dashboardUID)
expectedNoneJSON := `{}`
// Now, let's see how this looks like.
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
body, _ := rulesNamespaceWithoutVariableValues(t, b)
require.JSONEq(t, expectedAllJSON, body)
}
// Now, let's check we get the same rule when filtering by dashboard_uid
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules?dashboard_uid=%s", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
body, _ := rulesNamespaceWithoutVariableValues(t, b)
require.JSONEq(t, expectedFilteredByJSON, body)
}
// Now, let's check we get no rules when filtering by an unknown dashboard_uid
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules?dashboard_uid=%s", grafanaListedAddr, "abc")
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, expectedNoneJSON, string(b))
}
// Now, let's check we get the same rule when filtering by dashboard_uid and panel_id
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules?dashboard_uid=%s&panel_id=1", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
body, _ := rulesNamespaceWithoutVariableValues(t, b)
require.JSONEq(t, expectedFilteredByJSON, body)
}
// Now, let's check we get no rules when filtering by dashboard_uid and unknown panel_id
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules?dashboard_uid=%s&panel_id=2", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, 200, resp.StatusCode)
require.JSONEq(t, expectedNoneJSON, string(b))
}
// Now, let's check an invalid panel_id returns a 400 Bad Request response
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules?dashboard_uid=%s&panel_id=invalid", grafanaListedAddr, dashboardUID)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var res map[string]any
require.NoError(t, json.Unmarshal(b, &res))
require.Equal(t, `invalid panel_id: strconv.ParseInt: parsing "invalid": invalid syntax`, res["message"])
}
// Now, let's check a panel_id without dashboard_uid returns a 400 Bad Request response
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules?panel_id=1", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var res map[string]any
require.NoError(t, json.Unmarshal(b, &res))
require.Equal(t, "panel_id must be set with dashboard_uid", res["message"])
}
}
func TestIntegrationRuleGroupSequence(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,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
// Create a user to make authenticated requests
createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
client := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
parentFolderUID := util.GenerateShortUID()
client.CreateFolder(t, parentFolderUID, "parent")
folderUID := util.GenerateShortUID()
client.CreateFolder(t, folderUID, "folder1", parentFolderUID)
group1 := generateAlertRuleGroup(5, alertRuleGen())
group2 := generateAlertRuleGroup(5, alertRuleGen())
_, status, _ := client.PostRulesGroupWithStatus(t, folderUID, &group1)
require.Equal(t, http.StatusAccepted, status)
_, status, _ = client.PostRulesGroupWithStatus(t, folderUID, &group2)
require.Equal(t, http.StatusAccepted, status)
t.Run("should persist order of the rules in a group", func(t *testing.T) {
group1Get := client.GetRulesGroup(t, folderUID, group1.Name)
assert.Equal(t, group1.Name, group1Get.Name)
assert.Equal(t, group1.Interval, group1Get.Interval)
assert.Len(t, group1Get.Rules, len(group1.Rules))
for i, getRule := range group1Get.Rules {
rule := group1.Rules[i]
assert.Equal(t, getRule.GrafanaManagedAlert.Title, rule.GrafanaManagedAlert.Title)
assert.NotEmpty(t, getRule.GrafanaManagedAlert.UID)
}
// now shuffle the rules
postableGroup1 := convertGettableRuleGroupToPostable(group1Get.GettableRuleGroupConfig)
rand.Shuffle(len(postableGroup1.Rules), func(i, j int) {
postableGroup1.Rules[i], postableGroup1.Rules[j] = postableGroup1.Rules[j], postableGroup1.Rules[i]
})
expectedUids := make([]string, 0, len(postableGroup1.Rules))
for _, rule := range postableGroup1.Rules {
expectedUids = append(expectedUids, rule.GrafanaManagedAlert.UID)
}
_, status, _ := client.PostRulesGroupWithStatus(t, folderUID, &postableGroup1)
require.Equal(t, http.StatusAccepted, status)
group1Get = client.GetRulesGroup(t, folderUID, group1.Name)
require.Len(t, group1Get.Rules, len(postableGroup1.Rules))
actualUids := make([]string, 0, len(group1Get.Rules))
for _, getRule := range group1Get.Rules {
actualUids = append(actualUids, getRule.GrafanaManagedAlert.UID)
}
assert.Equal(t, expectedUids, actualUids)
})
t.Run("should be able to move a rule from another group in a specific position", func(t *testing.T) {
group1Get := client.GetRulesGroup(t, folderUID, group1.Name)
group2Get := client.GetRulesGroup(t, folderUID, group2.Name)
movedRule := convertGettableRuleToPostable(group2Get.Rules[3])
// now shuffle the rules
postableGroup1 := convertGettableRuleGroupToPostable(group1Get.GettableRuleGroupConfig)
postableGroup1.Rules = append(append(append([]apimodels.PostableExtendedRuleNode{}, postableGroup1.Rules[0:1]...), movedRule), postableGroup1.Rules[2:]...)
expectedUids := make([]string, 0, len(postableGroup1.Rules))
for _, rule := range postableGroup1.Rules {
expectedUids = append(expectedUids, rule.GrafanaManagedAlert.UID)
}
_, status, _ := client.PostRulesGroupWithStatus(t, folderUID, &postableGroup1)
require.Equal(t, http.StatusAccepted, status)
group1Get = client.GetRulesGroup(t, folderUID, group1.Name)
require.Len(t, group1Get.Rules, len(postableGroup1.Rules))
actualUids := make([]string, 0, len(group1Get.Rules))
for _, getRule := range group1Get.Rules {
actualUids = append(actualUids, getRule.GrafanaManagedAlert.UID)
}
assert.Equal(t, expectedUids, actualUids)
group2Get = client.GetRulesGroup(t, folderUID, group2.Name)
assert.Len(t, group2Get.Rules, len(group2.Rules)-1)
for _, rule := range group2Get.Rules {
require.NotEqual(t, movedRule.GrafanaManagedAlert.UID, rule.GrafanaManagedAlert.UID)
}
})
}
func TestIntegrationRuleUpdate(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,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
permissionsStore := resourcepermissions.NewStore(store, featuremgmt.WithFeatures())
// Create a user to make authenticated requests
userID := createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
if setting.IsEnterprise {
// add blanket access to data sources.
_, err := permissionsStore.SetUserResourcePermission(context.Background(),
1,
accesscontrol.User{ID: userID},
resourcepermissions.SetResourcePermissionCommand{
Actions: []string{
datasources.ActionQuery,
},
Resource: datasources.ScopeRoot,
ResourceID: "*",
ResourceAttribute: "uid",
}, nil)
require.NoError(t, err)
}
// Create a user to make authenticated requests
createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
adminClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
client := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
folderUID := util.GenerateShortUID()
client.CreateFolder(t, folderUID, "folder1")
t.Run("should be able to reset 'for' to 0", func(t *testing.T) {
group := generateAlertRuleGroup(1, alertRuleGen())
expected := model.Duration(10 * time.Second)
group.Rules[0].ApiRuleNode.For = &expected
_, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
require.Equal(t, expected, *getGroup.Rules[0].ApiRuleNode.For)
group = convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
expected = 0
group.Rules[0].ApiRuleNode.For = &expected
_, status, body = client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
getGroup = client.GetRulesGroup(t, folderUID, group.Name)
require.Equal(t, expected, *getGroup.Rules[0].ApiRuleNode.For)
})
t.Run("when data source missing", func(t *testing.T) {
var groupName string
{
ds1 := adminClient.CreateTestDatasource(t)
group := generateAlertRuleGroup(3, alertRuleGen(withDatasourceQuery(ds1.Body.Datasource.UID)))
_, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
group = convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
require.Len(t, group.Rules, 3)
adminClient.DeleteDatasource(t, ds1.Body.Datasource.UID)
// expire datasource caching
<-time.After(datasourceService.DefaultCacheTTL + 1*time.Second) // TODO delete when TTL could be configured
groupName = group.Name
}
t.Run("noop should not fail", func(t *testing.T) {
getGroup := client.GetRulesGroup(t, folderUID, groupName)
group := convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
_, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body)
})
t.Run("should not let update rule if it does not fix datasource", func(t *testing.T) {
getGroup := client.GetRulesGroup(t, folderUID, groupName)
group := convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
group.Rules[0].GrafanaManagedAlert.Title = uuid.NewString()
resp, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
if status == http.StatusAccepted {
assert.Len(t, resp.Deleted, 1)
getGroup = client.GetRulesGroup(t, folderUID, group.Name)
assert.NotEqualf(t, group.Rules[0].GrafanaManagedAlert.Title, getGroup.Rules[0].GrafanaManagedAlert.Title, "group was updated")
}
require.Equalf(t, http.StatusBadRequest, status, "expected BadRequest. Response: %s", body)
assert.Contains(t, body, "data source not found")
})
t.Run("should let delete broken rule", func(t *testing.T) {
getGroup := client.GetRulesGroup(t, folderUID, groupName)
group := convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
// remove the last rule.
group.Rules = group.Rules[0 : len(group.Rules)-1]
resp, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to delete last rule from group. Response: %s", body)
assert.Len(t, resp.Deleted, 1)
getGroup = client.GetRulesGroup(t, folderUID, group.Name)
group = convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
require.Len(t, group.Rules, 2)
})
t.Run("should let fix single rule", func(t *testing.T) {
getGroup := client.GetRulesGroup(t, folderUID, groupName)
group := convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
ds2 := adminClient.CreateTestDatasource(t)
withDatasourceQuery(ds2.Body.Datasource.UID)(&group.Rules[0])
resp, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body)
assert.Len(t, resp.Deleted, 0)
assert.Len(t, resp.Updated, 2)
assert.Len(t, resp.Created, 0)
getGroup = client.GetRulesGroup(t, folderUID, group.Name)
group = convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
require.Equal(t, ds2.Body.Datasource.UID, group.Rules[0].GrafanaManagedAlert.Data[0].DatasourceUID)
})
t.Run("should let delete group", func(t *testing.T) {
status, body := client.DeleteRulesGroup(t, folderUID, groupName)
require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body)
})
})
}
func newTestingRuleConfig(t *testing.T) apimodels.PostableRuleGroupConfig {
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
firstRule := apimodels.PostableExtendedRuleNode{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
// this rule does not explicitly set no data and error states
// therefore it should get the default values
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
}
secondRule := apimodels.PostableExtendedRuleNode{
ApiRuleNode: &apimodels.ApiRuleNode{
For: &interval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
// this rule does not explicitly set no data and error states
// therefore it should get the default values
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "AlwaysFiring2",
Condition: "A",
Data: []apimodels.AlertQuery{
{
RefID: "A",
RelativeTimeRange: apimodels.RelativeTimeRange{
From: apimodels.Duration(time.Duration(5) * time.Hour),
To: apimodels.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
}
return apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Rules: []apimodels.PostableExtendedRuleNode{
firstRule,
secondRule,
},
}
}
func TestIntegrationRulePause(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,
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
// Create a user to make authenticated requests
createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
})
client := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
folderUID := util.GenerateShortUID()
client.CreateFolder(t, folderUID, "folder1")
t.Run("should create a paused rule if isPaused is true", func(t *testing.T) {
group := generateAlertRuleGroup(1, alertRuleGen())
expectedIsPaused := true
group.Rules[0].GrafanaManagedAlert.IsPaused = &expectedIsPaused
resp, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
require.Len(t, resp.Created, 1)
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
require.Equalf(t, http.StatusAccepted, status, "failed to get rule group. Response: %s", body)
require.Equal(t, expectedIsPaused, getGroup.Rules[0].GrafanaManagedAlert.IsPaused)
})
t.Run("should create a unpaused rule if isPaused is false", func(t *testing.T) {
group := generateAlertRuleGroup(1, alertRuleGen())
expectedIsPaused := false
group.Rules[0].GrafanaManagedAlert.IsPaused = &expectedIsPaused
resp, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
require.Len(t, resp.Created, 1)
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
require.Equalf(t, http.StatusAccepted, status, "failed to get rule group. Response: %s", body)
require.Equal(t, expectedIsPaused, getGroup.Rules[0].GrafanaManagedAlert.IsPaused)
})
t.Run("should create a unpaused rule if isPaused is not present", func(t *testing.T) {
group := generateAlertRuleGroup(1, alertRuleGen())
group.Rules[0].GrafanaManagedAlert.IsPaused = nil
resp, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
require.Len(t, resp.Created, 1)
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
require.Equalf(t, http.StatusAccepted, status, "failed to get rule group. Response: %s", body)
require.False(t, getGroup.Rules[0].GrafanaManagedAlert.IsPaused)
})
getBooleanPointer := func(b bool) *bool { return &b }
testCases := []struct {
description string
isPausedInDb bool
isPausedInBody *bool
expectedIsPausedInDb bool
}{
{
description: "should pause rule if there is a paused rule in DB and isPaused is true",
isPausedInDb: true,
isPausedInBody: getBooleanPointer(true),
expectedIsPausedInDb: true,
},
{
description: "should unpause rule if there is a paused rule in DB and isPaused is false",
isPausedInDb: true,
isPausedInBody: getBooleanPointer(false),
expectedIsPausedInDb: false,
},
{
description: "should keep rule paused if there is a paused rule in DB and isPaused is not present",
isPausedInDb: true,
isPausedInBody: nil,
expectedIsPausedInDb: true,
},
{
description: "should pause rule if there is an unpaused rule in DB and isPaused is true",
isPausedInDb: false,
isPausedInBody: getBooleanPointer(true),
expectedIsPausedInDb: true,
},
{
description: "should unpause rule if there is an unpaused rule in DB and isPaused is false",
isPausedInDb: false,
isPausedInBody: getBooleanPointer(false),
expectedIsPausedInDb: false,
},
{
description: "should keep rule unpaused if there is an unpaused rule in DB and isPaused is not present",
isPausedInDb: false,
isPausedInBody: nil,
expectedIsPausedInDb: false,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
group := generateAlertRuleGroup(1, alertRuleGen())
group.Rules[0].GrafanaManagedAlert.IsPaused = &tc.isPausedInDb
_, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
require.Equalf(t, http.StatusAccepted, status, "failed to get rule group. Response: %s", body)
group = convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
group.Rules[0].GrafanaManagedAlert.IsPaused = tc.isPausedInBody
_, status, body = client.PostRulesGroupWithStatus(t, folderUID, &group)
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
getGroup = client.GetRulesGroup(t, folderUID, group.Name)
require.Equal(t, tc.expectedIsPausedInDb, getGroup.Rules[0].GrafanaManagedAlert.IsPaused)
})
}
}
func TestIntegrationHysteresisRule(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
// Setup Grafana and its Database. Scheduler is set to evaluate every 1 second
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
NGAlertSchedulerBaseInterval: 1 * time.Second,
EnableFeatureToggles: []string{featuremgmt.FlagConfigurableSchedulerTick, featuremgmt.FlagRecoveryThreshold},
})
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, p)
// Create a user to make authenticated requests
createUser(t, store, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "password",
Login: "grafana",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
folder := "hysteresis"
testDs := apiClient.CreateTestDatasource(t)
apiClient.CreateFolder(t, folder, folder)
bodyRaw, err := testData.ReadFile("test-data/hysteresis_rule.json")
require.NoError(t, err)
var postData apimodels.PostableRuleGroupConfig
require.NoError(t, json.Unmarshal(bodyRaw, &postData))
for _, rule := range postData.Rules {
for i := range rule.GrafanaManagedAlert.Data {
rule.GrafanaManagedAlert.Data[i].DatasourceUID = strings.ReplaceAll(rule.GrafanaManagedAlert.Data[i].DatasourceUID, "REPLACE_ME", testDs.Body.Datasource.UID)
}
}
changes, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &postData)
require.Equalf(t, http.StatusAccepted, status, body)
require.Len(t, changes.Created, 1)
ruleUid := changes.Created[0]
var frame data.Frame
require.Eventuallyf(t, func() bool {
frame, status, body = apiClient.GetRuleHistoryWithStatus(t, ruleUid)
require.Equalf(t, http.StatusOK, status, body)
return frame.Rows() > 1
}, 15*time.Second, 1*time.Second, "Alert state history expected to have more than one record but got %d. Body: %s", frame.Rows(), body)
f, _ := frame.FieldByName("next")
alertingIdx := 0
normalIdx := 1
if f.At(alertingIdx).(string) != "Alerting" {
alertingIdx = 1
normalIdx = 0
}
assert.Equalf(t, "Alerting", f.At(alertingIdx).(string), body)
assert.Equalf(t, "Normal", f.At(normalIdx).(string), body)
type HistoryData struct {
Values map[string]int64
}
f, _ = frame.FieldByName("data")
var d HistoryData
require.NoErrorf(t, json.Unmarshal([]byte(f.At(alertingIdx).(string)), &d), body)
assert.EqualValuesf(t, 5, d.Values["B"], body)
require.NoErrorf(t, json.Unmarshal([]byte(f.At(normalIdx).(string)), &d), body)
assert.EqualValuesf(t, 1, d.Values["B"], body)
}