2021-04-30 00:15:15 +08:00
package alerting
import (
2023-06-16 01:33:42 +08:00
"context"
2023-10-06 03:47:49 +08:00
"embed"
2021-04-30 00:15:15 +08:00
"encoding/json"
"fmt"
2022-08-10 21:37:51 +08:00
"io"
2025-03-12 00:40:44 +08:00
"maps"
2022-06-22 22:52:46 +08:00
"math/rand"
2021-04-30 00:15:15 +08:00
"net/http"
2023-10-06 03:47:49 +08:00
"path"
2024-02-15 22:45:10 +08:00
"slices"
2023-10-06 03:47:49 +08:00
"strings"
2021-04-30 00:15:15 +08:00
"testing"
"time"
2023-10-06 03:47:49 +08:00
"github.com/google/go-cmp/cmp"
2025-03-12 00:40:44 +08:00
"github.com/google/go-cmp/cmp/cmpopts"
2023-06-16 01:33:42 +08:00
"github.com/google/uuid"
2024-01-05 00:47:13 +08:00
"github.com/grafana/grafana-plugin-sdk-go/data"
2024-02-15 22:45:10 +08:00
"github.com/prometheus/alertmanager/pkg/labels"
2022-02-24 00:30:04 +08:00
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2025-01-25 03:49:05 +08:00
"github.com/grafana/grafana/pkg/apimachinery/errutil"
2023-06-16 01:33:42 +08:00
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/accesscontrol"
2022-08-18 15:43:45 +08:00
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
2023-06-16 01:33:42 +08:00
"github.com/grafana/grafana/pkg/services/datasources"
datasourceService "github.com/grafana/grafana/pkg/services/datasources/service"
2023-07-21 22:23:01 +08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2021-04-30 00:15:15 +08:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2025-01-25 03:49:05 +08:00
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
2022-08-10 17:56:48 +08:00
"github.com/grafana/grafana/pkg/services/org"
2022-06-28 20:32:25 +08:00
"github.com/grafana/grafana/pkg/services/user"
2023-06-16 01:33:42 +08:00
"github.com/grafana/grafana/pkg/setting"
2021-04-30 00:15:15 +08:00
"github.com/grafana/grafana/pkg/tests/testinfra"
2022-06-22 22:52:46 +08:00
"github.com/grafana/grafana/pkg/util"
2021-04-30 00:15:15 +08:00
)
2023-10-06 03:47:49 +08:00
//go:embed test-data/*.*
var testData embed . FS
2022-12-09 15:11:56 +08:00
func TestIntegrationAlertRulePermissions ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
2021-04-30 00:15:15 +08:00
// Setup Grafana and its Database
2023-10-06 03:47:49 +08:00
dir , p := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
2021-09-29 22:16:40 +08:00
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
DisableAnonymous : true ,
2022-02-09 17:26:06 +08:00
AppModeProduction : true ,
2021-04-30 00:15:15 +08:00
} )
2021-08-25 21:11:22 +08:00
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
2024-05-28 23:32:23 +08:00
permissionsStore := resourcepermissions . NewStore ( env . Cfg , env . SQLStore , featuremgmt . WithFeatures ( ) )
2021-04-30 00:15:15 +08:00
2021-05-05 00:16:28 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
userID := createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2022-08-10 17:56:48 +08:00
DefaultOrgRole : string ( org . RoleEditor ) ,
2021-08-12 21:04:09 +08:00
Password : "password" ,
Login : "grafana" ,
} )
2021-05-05 00:16:28 +08:00
2022-06-21 23:39:22 +08:00
apiClient := newAlertingApiClient ( grafanaListedAddr , "grafana" , "password" )
2021-04-30 00:15:15 +08:00
// Create the namespace we'll save our alerts to.
2022-06-21 23:39:22 +08:00
apiClient . CreateFolder ( t , "folder1" , "folder1" )
// Create the namespace we'll save our alerts to.
apiClient . CreateFolder ( t , "folder2" , "folder2" )
2021-04-30 00:15:15 +08:00
2023-10-06 03:47:49 +08:00
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 ) )
2021-04-30 00:15:15 +08:00
// Create rule under folder1
2025-03-15 04:14:06 +08:00
_ , status , response := apiClient . PostRulesGroupWithStatus ( t , "folder1" , & group1 , false )
2023-10-06 03:47:49 +08:00
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 ) )
2021-04-30 00:15:15 +08:00
// Create rule under folder2
2025-03-15 04:14:06 +08:00
_ , status , response = apiClient . PostRulesGroupWithStatus ( t , "folder2" , & group2 , false )
2023-10-06 03:47:49 +08:00
require . Equalf ( t , http . StatusAccepted , status , response )
2021-04-30 00:15:15 +08:00
// With the rules created, let's make sure that rule definitions are stored.
2023-10-06 03:47:49 +08:00
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" ) )
2021-04-30 00:15:15 +08:00
require . NoError ( t , err )
2023-10-06 03:47:49 +08:00
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" ,
2025-01-28 03:31:40 +08:00
"GrafanaManagedAlert.UpdatedBy" ,
2023-10-06 03:47:49 +08:00
"GrafanaManagedAlert.UID" ,
"GrafanaManagedAlert.ID" ,
"GrafanaManagedAlert.Data.Model" ,
"GrafanaManagedAlert.NamespaceUID" ,
"GrafanaManagedAlert.NamespaceID" ,
2025-03-15 04:14:06 +08:00
"GrafanaManagedAlert.GUID" ,
2023-10-06 03:47:49 +08:00
}
// 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" ] )
2021-04-30 00:15:15 +08:00
} )
2023-10-06 03:47:49 +08:00
t . Run ( "Get group returns a single group" , func ( t * testing . T ) {
2025-02-08 00:24:28 +08:00
rules , status := apiClient . GetRulesGroup ( t , "folder2" , allRules [ "folder2" ] [ 0 ] . Name )
require . Equal ( t , http . StatusAccepted , status )
2023-10-06 03:47:49 +08:00
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
2021-04-30 00:15:15 +08:00
}
2023-10-06 03:47:49 +08:00
}
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 ] ,
2021-04-30 00:15:15 +08:00
}
2023-10-06 03:47:49 +08:00
status , exportRaw := apiClient . ExportRulesWithStatus ( t , & apimodels . AlertRulesExportParameters {
ExportQueryParams : apimodels . ExportQueryParams { Format : "json" } ,
RuleUID : expected . Rules [ 0 ] . UID ,
} )
2021-04-30 00:15:15 +08:00
2023-10-06 03:47:49 +08:00
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 ] )
} )
2025-02-04 02:26:18 +08:00
t . Run ( "Get versions of any rule" , func ( t * testing . T ) {
for _ , groups := range allRules { // random rule from each folder
group := groups [ rand . Intn ( len ( groups ) ) ]
rule := group . Rules [ rand . Intn ( len ( group . Rules ) ) ]
versions , status , raw := apiClient . GetRuleVersionsWithStatus ( t , rule . GrafanaManagedAlert . UID )
if assert . Equalf ( t , http . StatusOK , status , "Expected status 200, got %d: %s" , status , raw ) {
assert . NotEmpty ( t , versions )
assert . Equal ( t , rule , versions [ 0 ] ) // the first version in the collection should always be the current
}
}
} )
2023-10-06 03:47:49 +08:00
} )
t . Run ( "when permissions for folder2 removed" , func ( t * testing . T ) {
2021-04-30 00:15:15 +08:00
// remove permissions from folder2
2022-08-10 17:56:48 +08:00
removeFolderPermission ( t , permissionsStore , 1 , userID , org . RoleEditor , "folder2" )
2022-06-27 21:31:49 +08:00
apiClient . ReloadCachedPermissions ( t )
2021-04-30 00:15:15 +08:00
2023-10-06 03:47:49 +08:00
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" )
2021-04-30 00:15:15 +08:00
} )
2023-10-06 03:47:49 +08:00
t . Run ( "Get by folder returns groups in folder" , func ( t * testing . T ) {
_ , status , _ := apiClient . GetAllRulesGroupInFolderWithStatus ( t , "folder2" )
require . Equal ( t , http . StatusForbidden , status )
} )
2021-11-08 21:26:08 +08:00
2023-10-06 03:47:49 +08:00
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 )
2021-11-08 21:26:08 +08:00
require . NoError ( t , err )
2023-10-06 03:47:49 +08:00
defer func ( ) {
_ = resp . Body . Close ( )
} ( )
assert . Equal ( t , http . StatusForbidden , resp . StatusCode )
2021-11-08 21:26:08 +08:00
} )
2023-10-06 03:47:49 +08:00
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" } ,
} )
2023-12-08 02:43:58 +08:00
assert . Equal ( t , http . StatusForbidden , status )
2023-10-06 03:47:49 +08:00
} )
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 )
} )
2025-02-04 02:26:18 +08:00
t . Run ( "Versions of rule" , func ( t * testing . T ) {
uid := allRules [ "folder2" ] [ 0 ] . Rules [ 0 ] . GrafanaManagedAlert . UID
_ , status , raw := apiClient . GetRuleVersionsWithStatus ( t , uid )
require . Equalf ( t , http . StatusForbidden , status , "Expected status 403, got %d: %s" , status , raw )
} )
2023-10-06 03:47:49 +08:00
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 )
} )
} )
2021-04-30 00:15:15 +08:00
}
2024-01-17 17:07:39 +08:00
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 ,
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
2024-05-28 23:32:23 +08:00
permissionsStore := resourcepermissions . NewStore ( env . Cfg , env . SQLStore , featuremgmt . WithFeatures ( ) )
2024-01-17 17:07:39 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
userID := createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2024-01-17 17:07:39 +08:00
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
2025-03-15 04:14:06 +08:00
_ , status , response := apiClient . PostRulesGroupWithStatus ( t , "folder1" , & group1 , false )
2024-01-17 17:07:39 +08:00
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
2025-03-15 04:14:06 +08:00
_ , status , response = apiClient . PostRulesGroupWithStatus ( t , "folder2" , & group2 , false )
2024-01-17 17:07:39 +08:00
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
2025-03-15 04:14:06 +08:00
_ , status , response = apiClient . PostRulesGroupWithStatus ( t , "subfolder" , & group3 , false )
2024-01-17 17:07:39 +08:00
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 ) )
expected := apimodels . NamespaceConfigResponse {
"folder1" : [ ] apimodels . GettableRuleGroupConfig {
group1 ,
} ,
"folder2" : [ ] apimodels . GettableRuleGroupConfig {
group2 ,
} ,
2024-02-07 06:12:13 +08:00
"folder1/subfolder" : [ ] apimodels . GettableRuleGroupConfig {
2024-01-17 17:07:39 +08:00
group3 ,
} ,
}
pathsToIgnore := [ ] string {
"GrafanaManagedAlert.Updated" ,
2025-01-28 03:31:40 +08:00
"GrafanaManagedAlert.UpdatedBy" ,
2024-01-17 17:07:39 +08:00
"GrafanaManagedAlert.UID" ,
"GrafanaManagedAlert.ID" ,
"GrafanaManagedAlert.Data.Model" ,
"GrafanaManagedAlert.NamespaceUID" ,
"GrafanaManagedAlert.NamespaceID" ,
2025-03-15 04:14:06 +08:00
"GrafanaManagedAlert.GUID" ,
2024-01-17 17:07:39 +08:00
}
// 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 )
}
2024-02-07 06:12:13 +08:00
for _ , rule := range allRules [ "folder1/subfolder" ] [ 0 ] . Rules {
2024-01-17 17:07:39 +08:00
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 ) {
2025-02-08 00:24:28 +08:00
rules , status := apiClient . GetRulesGroup ( t , "folder2" , allRules [ "folder2" ] [ 0 ] . Name )
require . Equal ( t , http . StatusAccepted , status )
2024-01-17 17:07:39 +08:00
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 )
2024-02-07 06:12:13 +08:00
nestedKey := "folder1/subfolder"
2024-01-17 17:07:39 +08:00
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 )
2024-05-31 16:09:20 +08:00
require . Equal ( t , "folder1/subfolder" , allExport . Groups [ 2 ] . Folder )
2024-01-17 17:07:39 +08:00
} )
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" )
2024-02-07 06:12:13 +08:00
require . Contains ( t , newAll , "folder1/subfolder" )
2024-01-17 17:07:39 +08:00
} )
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 )
2024-05-31 16:09:20 +08:00
require . Equal ( t , "folder1/subfolder" , export . Groups [ 1 ] . Folder )
2024-01-17 17:07:39 +08:00
} )
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 )
} )
} )
}
2023-10-06 03:47:49 +08:00
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 ,
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
2024-05-28 23:32:23 +08:00
permissionsStore := resourcepermissions . NewStore ( env . Cfg , env . SQLStore , featuremgmt . WithFeatures ( ) )
2023-10-06 03:47:49 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
userID := createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2023-10-06 03:47:49 +08:00
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 )
} )
2021-04-30 00:15:15 +08:00
}
2021-06-05 01:45:26 +08:00
2024-09-19 22:43:41 +08:00
func TestIntegrationAlertRuleEditorSettings ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
const folderName = "folder1"
const groupName = "test-group"
// 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 , env := testinfra . StartGrafanaEnv ( t , dir , path )
apiClient := newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "admin" ,
Login : "admin" ,
} )
apiClient . CreateFolder ( t , folderName , folderName )
createAlertInGrafana := func ( metadata * apimodels . AlertRuleMetadata ) apimodels . GettableRuleGroupConfig {
interval , err := model . ParseDuration ( "1m" )
require . NoError ( t , err )
alertRule := apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
2025-03-18 18:24:48 +08:00
For : & interval ,
KeepFiringFor : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
2024-09-19 22:43:41 +08:00
} ,
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"
} ` ) ,
} ,
} ,
Metadata : metadata ,
} ,
}
rules := apimodels . PostableRuleGroupConfig {
Name : groupName ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
alertRule ,
} ,
}
2025-03-15 04:14:06 +08:00
respModel , status , _ := apiClient . PostRulesGroupWithStatus ( t , folderName , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2024-09-19 22:43:41 +08:00
require . Len ( t , respModel . Created , 1 )
2025-02-08 00:24:28 +08:00
createdRuleGroup , status := apiClient . GetRulesGroup ( t , folderName , rules . Name )
require . Equal ( t , http . StatusAccepted , status )
2024-09-19 22:43:41 +08:00
require . Len ( t , createdRuleGroup . Rules , 1 )
expectedMetadata := alertRule . GrafanaManagedAlert . Metadata
if metadata == nil {
expectedMetadata = & apimodels . AlertRuleMetadata {
EditorSettings : apimodels . AlertRuleEditorSettings {
SimplifiedQueryAndExpressionsSection : false ,
} ,
}
}
require . Equal ( t , expectedMetadata , createdRuleGroup . Rules [ 0 ] . GrafanaManagedAlert . Metadata )
2025-02-08 00:24:28 +08:00
return createdRuleGroup . GettableRuleGroupConfig
2024-09-19 22:43:41 +08:00
}
t . Run ( "set simplified query editor in editor settings" , func ( t * testing . T ) {
metadata := & apimodels . AlertRuleMetadata {
EditorSettings : apimodels . AlertRuleEditorSettings {
SimplifiedQueryAndExpressionsSection : false ,
} ,
}
createdRuleGroup := createAlertInGrafana ( metadata )
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup )
rulesWithUID . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedQueryAndExpressionsSection = true
2025-03-15 04:14:06 +08:00
_ , status , _ := apiClient . PostRulesGroupWithStatus ( t , folderName , & rulesWithUID , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2024-09-19 22:43:41 +08:00
2025-02-08 00:24:28 +08:00
updatedRuleGroup , status := apiClient . GetRulesGroup ( t , folderName , groupName )
require . Equal ( t , http . StatusAccepted , status )
2024-09-19 22:43:41 +08:00
require . Len ( t , updatedRuleGroup . Rules , 1 )
2024-11-07 01:39:15 +08:00
require . True ( t , updatedRuleGroup . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedQueryAndExpressionsSection )
} )
t . Run ( "disable simplified query editor in editor settings" , func ( t * testing . T ) {
metadata := & apimodels . AlertRuleMetadata {
EditorSettings : apimodels . AlertRuleEditorSettings {
SimplifiedQueryAndExpressionsSection : true ,
} ,
}
createdRuleGroup := createAlertInGrafana ( metadata )
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup )
// disabling the editor
rulesWithUID . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedQueryAndExpressionsSection = false
2025-03-15 04:14:06 +08:00
_ , status , _ := apiClient . PostRulesGroupWithStatus ( t , folderName , & rulesWithUID , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2024-11-07 01:39:15 +08:00
2025-02-08 00:24:28 +08:00
updatedRuleGroup , status := apiClient . GetRulesGroup ( t , folderName , groupName )
require . Equal ( t , http . StatusAccepted , status )
2024-11-07 01:39:15 +08:00
require . Len ( t , updatedRuleGroup . Rules , 1 )
require . False ( t , updatedRuleGroup . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedQueryAndExpressionsSection )
2024-09-19 22:43:41 +08:00
} )
2024-11-14 19:55:54 +08:00
t . Run ( "set simplified notifications editor in editor settings" , func ( t * testing . T ) {
metadata := & apimodels . AlertRuleMetadata {
EditorSettings : apimodels . AlertRuleEditorSettings {
SimplifiedQueryAndExpressionsSection : false ,
} ,
}
createdRuleGroup := createAlertInGrafana ( metadata )
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup )
rulesWithUID . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedNotificationsSection = true
2025-03-15 04:14:06 +08:00
_ , status , _ := apiClient . PostRulesGroupWithStatus ( t , folderName , & rulesWithUID , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2024-11-14 19:55:54 +08:00
2025-02-08 00:24:28 +08:00
updatedRuleGroup , status := apiClient . GetRulesGroup ( t , folderName , groupName )
require . Equal ( t , http . StatusAccepted , status )
2024-11-14 19:55:54 +08:00
require . Len ( t , updatedRuleGroup . Rules , 1 )
require . True ( t , updatedRuleGroup . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedNotificationsSection )
} )
t . Run ( "disable simplified notifications editor in editor settings" , func ( t * testing . T ) {
metadata := & apimodels . AlertRuleMetadata {
EditorSettings : apimodels . AlertRuleEditorSettings {
SimplifiedNotificationsSection : true ,
} ,
}
createdRuleGroup := createAlertInGrafana ( metadata )
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup )
// disabling the editor
rulesWithUID . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedNotificationsSection = false
2025-03-15 04:14:06 +08:00
_ , status , _ := apiClient . PostRulesGroupWithStatus ( t , folderName , & rulesWithUID , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2024-11-14 19:55:54 +08:00
2025-02-08 00:24:28 +08:00
updatedRuleGroup , status := apiClient . GetRulesGroup ( t , folderName , groupName )
require . Equal ( t , http . StatusAccepted , status )
2024-11-14 19:55:54 +08:00
require . Len ( t , updatedRuleGroup . Rules , 1 )
require . False ( t , updatedRuleGroup . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedNotificationsSection )
} )
2024-09-19 22:43:41 +08:00
t . Run ( "post alert without metadata" , func ( t * testing . T ) {
createAlertInGrafana ( nil )
2025-02-08 00:24:28 +08:00
createdRuleGroup , status := apiClient . GetRulesGroup ( t , folderName , groupName )
require . Equal ( t , http . StatusAccepted , status )
2024-09-19 22:43:41 +08:00
require . Len ( t , createdRuleGroup . Rules , 1 )
2024-11-07 01:39:15 +08:00
require . False ( t , createdRuleGroup . Rules [ 0 ] . GrafanaManagedAlert . Metadata . EditorSettings . SimplifiedQueryAndExpressionsSection )
2024-09-19 22:43:41 +08:00
} )
}
2022-12-09 15:11:56 +08:00
func TestIntegrationAlertRuleConflictingTitle ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
2021-06-05 01:45:26 +08:00
// Setup Grafana and its Database
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
2021-09-29 22:16:40 +08:00
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
EnableQuota : true ,
DisableAnonymous : true ,
ViewersCanEdit : true ,
2022-02-09 17:26:06 +08:00
AppModeProduction : true ,
2021-06-05 01:45:26 +08:00
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
2021-06-05 01:45:26 +08:00
// Create user
2024-04-04 21:04:47 +08:00
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2022-08-10 17:56:48 +08:00
DefaultOrgRole : string ( org . RoleAdmin ) ,
2021-08-12 21:04:09 +08:00
Password : "admin" ,
Login : "admin" ,
} )
2021-06-05 01:45:26 +08:00
2022-06-21 23:39:22 +08:00
apiClient := newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" )
2022-05-16 18:45:41 +08:00
// Create the namespace we'll save our alerts to.
2022-06-21 23:39:22 +08:00
apiClient . CreateFolder ( t , "folder1" , "folder1" )
2022-05-16 18:45:41 +08:00
// Create the namespace we'll save our alerts to.
2022-06-21 23:39:22 +08:00
apiClient . CreateFolder ( t , "folder2" , "folder2" )
2022-05-16 18:45:41 +08:00
2022-04-14 20:21:36 +08:00
rules := newTestingRuleConfig ( t )
2021-06-05 01:45:26 +08:00
2025-03-15 04:14:06 +08:00
respModel , status , _ := apiClient . PostRulesGroupWithStatus ( t , "folder1" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2023-10-07 06:11:24 +08:00
require . Len ( t , respModel . Created , len ( rules . Rules ) )
2021-06-05 01:45:26 +08:00
2022-04-14 20:21:36 +08:00
// fetch the created rules, so we can get the uid's and trigger
// and update by reusing the uid's
2025-02-08 00:24:28 +08:00
createdRuleGroup , status := apiClient . GetRulesGroup ( t , "folder1" , rules . Name )
require . Equal ( t , http . StatusAccepted , status )
2022-04-14 20:21:36 +08:00
require . Len ( t , createdRuleGroup . Rules , 2 )
2025-03-19 01:27:44 +08:00
t . Run ( "trying to create alert with same title under same folder should not fail" , func ( t * testing . T ) {
2025-02-08 00:24:28 +08:00
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup . GettableRuleGroupConfig )
2023-06-09 06:51:50 +08:00
rulesWithUID . Rules = append ( rulesWithUID . Rules , rules . Rules [ 0 ] ) // Create new copy of first rule.
2022-04-14 20:21:36 +08:00
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , "folder1" , & rulesWithUID , false )
2025-03-19 01:27:44 +08:00
requireStatusCode ( t , http . StatusAccepted , status , body )
2022-04-14 20:21:36 +08:00
} )
2025-03-19 01:27:44 +08:00
t . Run ( "trying to update an alert to the title of an existing alert in the same folder should not fail" , func ( t * testing . T ) {
2025-02-08 00:24:28 +08:00
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup . GettableRuleGroupConfig )
2023-06-09 06:51:50 +08:00
rulesWithUID . Rules [ 1 ] . GrafanaManagedAlert . Title = "AlwaysFiring"
2021-06-05 01:45:26 +08:00
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , "folder1" , & rulesWithUID , false )
2025-03-19 01:27:44 +08:00
requireStatusCode ( t , http . StatusAccepted , status , body )
2021-06-05 01:45:26 +08:00
} )
t . Run ( "trying to create alert with same title under another folder should succeed" , func ( t * testing . T ) {
2022-04-14 20:21:36 +08:00
rules := newTestingRuleConfig ( t )
2025-03-15 04:14:06 +08:00
resp , status , _ := apiClient . PostRulesGroupWithStatus ( t , "folder2" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2023-10-07 06:11:24 +08:00
require . Len ( t , resp . Created , len ( rules . Rules ) )
2023-06-09 06:51:50 +08:00
} )
t . Run ( "trying to swap titles of existing alerts in the same folder should work" , func ( t * testing . T ) {
2025-02-08 00:24:28 +08:00
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup . GettableRuleGroupConfig )
2023-06-09 06:51:50 +08:00
title0 := rulesWithUID . Rules [ 0 ] . GrafanaManagedAlert . Title
title1 := rulesWithUID . Rules [ 1 ] . GrafanaManagedAlert . Title
rulesWithUID . Rules [ 0 ] . GrafanaManagedAlert . Title = title1
rulesWithUID . Rules [ 1 ] . GrafanaManagedAlert . Title = title0
2025-03-15 04:14:06 +08:00
resp , status , _ := apiClient . PostRulesGroupWithStatus ( t , "folder1" , & rulesWithUID , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2023-10-07 06:11:24 +08:00
require . Len ( t , resp . Updated , 2 )
2023-06-09 06:51:50 +08:00
} )
t . Run ( "trying to update titles of existing alerts in a chain in the same folder should work" , func ( t * testing . T ) {
2025-02-08 00:24:28 +08:00
rulesWithUID := convertGettableRuleGroupToPostable ( createdRuleGroup . GettableRuleGroupConfig )
2023-06-09 06:51:50 +08:00
rulesWithUID . Rules [ 0 ] . GrafanaManagedAlert . Title = rulesWithUID . Rules [ 1 ] . GrafanaManagedAlert . Title
rulesWithUID . Rules [ 1 ] . GrafanaManagedAlert . Title = "something new"
2025-03-15 04:14:06 +08:00
resp , status , _ := apiClient . PostRulesGroupWithStatus ( t , "folder1" , & rulesWithUID , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2023-10-07 06:11:24 +08:00
require . Len ( t , resp . Updated , len ( rulesWithUID . Rules ) )
2021-06-05 01:45:26 +08:00
} )
}
2021-10-04 23:33:55 +08:00
2022-12-09 15:11:56 +08:00
func TestIntegrationRulerRulesFilterByDashboard ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
2021-10-04 23:33:55 +08:00
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
EnableFeatureToggles : [ ] string { "ngalert" } ,
DisableAnonymous : true ,
2022-02-09 17:26:06 +08:00
AppModeProduction : true ,
2021-10-04 23:33:55 +08:00
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
2021-10-04 23:33:55 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2022-08-10 17:56:48 +08:00
DefaultOrgRole : string ( org . RoleEditor ) ,
2021-10-04 23:33:55 +08:00
Password : "password" ,
Login : "grafana" ,
} )
2022-06-21 23:39:22 +08:00
apiClient := newAlertingApiClient ( grafanaListedAddr , "grafana" , "password" )
2022-05-16 18:45:41 +08:00
dashboardUID := "default"
// Create the namespace under default organisation (orgID = 1) where we'll save our alerts to.
2022-06-21 23:39:22 +08:00
apiClient . CreateFolder ( t , "default" , "default" )
2021-10-04 23:33:55 +08:00
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 {
2025-03-18 18:24:48 +08:00
For : & interval ,
KeepFiringFor : & interval ,
Labels : map [ string ] string { } ,
2021-10-04 23:33:55 +08:00
Annotations : map [ string ] string {
"__dashboardUid__" : dashboardUID ,
"__panelId__" : "1" ,
} ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "AlwaysFiring" ,
Condition : "A" ,
2023-03-27 23:55:13 +08:00
Data : [ ] apimodels . AlertQuery {
2021-10-04 23:33:55 +08:00
{
RefID : "A" ,
2023-03-27 23:55:13 +08:00
RelativeTimeRange : apimodels . RelativeTimeRange {
From : apimodels . Duration ( time . Duration ( 5 ) * time . Hour ) ,
To : apimodels . Duration ( time . Duration ( 3 ) * time . Hour ) ,
2021-10-04 23:33:55 +08:00
} ,
2023-02-01 01:50:10 +08:00
DatasourceUID : expr . DatasourceUID ,
2021-10-04 23:33:55 +08:00
Model : json . RawMessage ( ` {
"type" : "math" ,
"expression" : "2 + 3 > 1"
} ` ) ,
} ,
} ,
} ,
} ,
{
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "AlwaysFiringButSilenced" ,
Condition : "A" ,
2023-03-27 23:55:13 +08:00
Data : [ ] apimodels . AlertQuery {
2021-10-04 23:33:55 +08:00
{
RefID : "A" ,
2023-03-27 23:55:13 +08:00
RelativeTimeRange : apimodels . RelativeTimeRange {
From : apimodels . Duration ( time . Duration ( 5 ) * time . Hour ) ,
To : apimodels . Duration ( time . Duration ( 3 ) * time . Hour ) ,
2021-10-04 23:33:55 +08:00
} ,
2023-02-01 01:50:10 +08:00
DatasourceUID : expr . DatasourceUID ,
2021-10-04 23:33:55 +08:00
Model : json . RawMessage ( ` {
"type" : "math" ,
"expression" : "2 + 3 > 1"
} ` ) ,
} ,
} ,
NoDataState : apimodels . NoDataState ( ngmodels . Alerting ) ,
ExecErrState : apimodels . ExecutionErrorState ( ngmodels . AlertingErrState ) ,
} ,
} ,
} ,
}
2025-03-15 04:14:06 +08:00
resp , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2023-10-07 06:11:24 +08:00
require . Len ( t , resp . Created , len ( rules . Rules ) )
2021-10-04 23:33:55 +08:00
}
expectedAllJSON := fmt . Sprintf ( `
{
"default" : [ {
"name" : "anotherrulegroup" ,
"interval" : "1m" ,
"rules" : [ {
"expr" : "" ,
"for" : "10s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "10s" ,
2021-10-04 23:33:55 +08:00
"annotations" : {
"__dashboardUid__" : "%s" ,
"__panelId__" : "1"
} ,
"grafana_alert" : {
"title" : "AlwaysFiring" ,
"condition" : "A" ,
"data" : [ {
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
2023-02-01 01:50:10 +08:00
"datasourceUid" : "__expr__" ,
2021-10-04 23:33:55 +08:00
"model" : {
"expression" : "2 + 3 \u003e 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
} ] ,
"updated" : "2021-02-21T01:10:30Z" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
"uid" : "uid" ,
"name" : "grafana"
} ,
2021-10-04 23:33:55 +08:00
"intervalSeconds" : 60 ,
2023-02-01 20:15:03 +08:00
"is_paused" : false ,
2021-10-04 23:33:55 +08:00
"version" : 1 ,
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2021-10-04 23:33:55 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "anotherrulegroup" ,
"no_data_state" : "NoData" ,
2024-09-19 22:43:41 +08:00
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
2024-11-14 19:55:54 +08:00
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
2024-09-19 22:43:41 +08:00
}
}
2021-10-04 23:33:55 +08:00
}
} , {
"expr" : "" ,
2022-06-30 23:46:26 +08:00
"for" : "0s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "0s" ,
2021-10-04 23:33:55 +08:00
"grafana_alert" : {
"title" : "AlwaysFiringButSilenced" ,
"condition" : "A" ,
"data" : [ {
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
2023-02-01 01:50:10 +08:00
"datasourceUid" : "__expr__" ,
2021-10-04 23:33:55 +08:00
"model" : {
"expression" : "2 + 3 \u003e 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
} ] ,
"updated" : "2021-02-21T01:10:30Z" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
"uid" : "uid" ,
"name" : "grafana"
} ,
2021-10-04 23:33:55 +08:00
"intervalSeconds" : 60 ,
2023-02-01 20:15:03 +08:00
"is_paused" : false ,
2021-10-04 23:33:55 +08:00
"version" : 1 ,
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2021-10-04 23:33:55 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "anotherrulegroup" ,
"no_data_state" : "Alerting" ,
2024-09-19 22:43:41 +08:00
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
2024-11-14 19:55:54 +08:00
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
2024-09-19 22:43:41 +08:00
}
}
2021-10-04 23:33:55 +08:00
}
} ]
} ]
} ` , dashboardUID )
expectedFilteredByJSON := fmt . Sprintf ( `
{
"default" : [ {
"name" : "anotherrulegroup" ,
"interval" : "1m" ,
"rules" : [ {
"expr" : "" ,
"for" : "10s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "10s" ,
2021-10-04 23:33:55 +08:00
"annotations" : {
"__dashboardUid__" : "%s" ,
"__panelId__" : "1"
} ,
"grafana_alert" : {
"title" : "AlwaysFiring" ,
"condition" : "A" ,
"data" : [ {
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
2023-02-01 01:50:10 +08:00
"datasourceUid" : "__expr__" ,
2021-10-04 23:33:55 +08:00
"model" : {
"expression" : "2 + 3 \u003e 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
} ] ,
"updated" : "2021-02-21T01:10:30Z" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
"uid" : "uid" ,
"name" : "grafana"
} ,
2021-10-04 23:33:55 +08:00
"intervalSeconds" : 60 ,
2023-02-01 20:15:03 +08:00
"is_paused" : false ,
2021-10-04 23:33:55 +08:00
"version" : 1 ,
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2021-10-04 23:33:55 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "anotherrulegroup" ,
"no_data_state" : "NoData" ,
2024-09-19 22:43:41 +08:00
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
2024-11-14 19:55:54 +08:00
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
2024-09-19 22:43:41 +08:00
}
}
2021-10-04 23:33:55 +08:00
}
} ]
} ]
} ` , 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 )
} )
2022-08-10 21:37:51 +08:00
b , err := io . ReadAll ( resp . Body )
2021-10-04 23:33:55 +08:00
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 )
} )
2022-08-10 21:37:51 +08:00
b , err := io . ReadAll ( resp . Body )
2021-10-04 23:33:55 +08:00
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 )
} )
2022-08-10 21:37:51 +08:00
b , err := io . ReadAll ( resp . Body )
2021-10-04 23:33:55 +08:00
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 )
} )
2022-08-10 21:37:51 +08:00
b , err := io . ReadAll ( resp . Body )
2021-10-04 23:33:55 +08:00
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 )
} )
2022-08-10 21:37:51 +08:00
b , err := io . ReadAll ( resp . Body )
2021-10-04 23:33:55 +08:00
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 )
2022-08-10 21:37:51 +08:00
b , err := io . ReadAll ( resp . Body )
2021-10-04 23:33:55 +08:00
require . NoError ( t , err )
2023-08-30 23:46:47 +08:00
var res map [ string ] any
2022-04-14 23:54:49 +08:00
require . NoError ( t , json . Unmarshal ( b , & res ) )
require . Equal ( t , ` invalid panel_id: strconv.ParseInt: parsing "invalid": invalid syntax ` , res [ "message" ] )
2021-10-04 23:33:55 +08:00
}
// 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 )
2022-08-10 21:37:51 +08:00
b , err := io . ReadAll ( resp . Body )
2021-10-04 23:33:55 +08:00
require . NoError ( t , err )
2023-08-30 23:46:47 +08:00
var res map [ string ] any
2022-04-14 23:54:49 +08:00
require . NoError ( t , json . Unmarshal ( b , & res ) )
require . Equal ( t , "panel_id must be set with dashboard_uid" , res [ "message" ] )
2021-10-04 23:33:55 +08:00
}
}
2022-04-14 20:21:36 +08:00
2022-12-09 15:11:56 +08:00
func TestIntegrationRuleGroupSequence ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
2022-06-22 22:52:46 +08:00
// Setup Grafana and its Database
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
DisableAnonymous : true ,
AppModeProduction : true ,
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
2022-06-22 22:52:46 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2022-08-10 17:56:48 +08:00
DefaultOrgRole : string ( org . RoleEditor ) ,
2022-06-22 22:52:46 +08:00
Password : "password" ,
Login : "grafana" ,
} )
client := newAlertingApiClient ( grafanaListedAddr , "grafana" , "password" )
2024-01-17 17:07:39 +08:00
parentFolderUID := util . GenerateShortUID ( )
client . CreateFolder ( t , parentFolderUID , "parent" )
folderUID := util . GenerateShortUID ( )
client . CreateFolder ( t , folderUID , "folder1" , parentFolderUID )
2022-06-22 22:52:46 +08:00
group1 := generateAlertRuleGroup ( 5 , alertRuleGen ( ) )
group2 := generateAlertRuleGroup ( 5 , alertRuleGen ( ) )
2025-03-15 04:14:06 +08:00
_ , status , _ := client . PostRulesGroupWithStatus ( t , folderUID , & group1 , false )
2022-06-22 22:52:46 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-03-15 04:14:06 +08:00
_ , status , _ = client . PostRulesGroupWithStatus ( t , folderUID , & group2 , false )
2022-06-22 22:52:46 +08:00
require . Equal ( t , http . StatusAccepted , status )
t . Run ( "should persist order of the rules in a group" , func ( t * testing . T ) {
2025-02-08 00:24:28 +08:00
group1Get , status := client . GetRulesGroup ( t , folderUID , group1 . Name )
require . Equal ( t , http . StatusAccepted , status )
2022-06-22 22:52:46 +08:00
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 )
}
2025-03-15 04:14:06 +08:00
_ , status , _ = client . PostRulesGroupWithStatus ( t , folderUID , & postableGroup1 , false )
2022-06-22 22:52:46 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-02-08 00:24:28 +08:00
group1Get , status = client . GetRulesGroup ( t , folderUID , group1 . Name )
require . Equal ( t , http . StatusAccepted , status )
2022-06-22 22:52:46 +08:00
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 ) {
2025-02-08 00:24:28 +08:00
group1Get , status := client . GetRulesGroup ( t , folderUID , group1 . Name )
require . Equal ( t , http . StatusAccepted , status )
group2Get , status := client . GetRulesGroup ( t , folderUID , group2 . Name )
require . Equal ( t , http . StatusAccepted , status )
2022-06-22 22:52:46 +08:00
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 )
}
2025-03-15 04:14:06 +08:00
_ , status , _ = client . PostRulesGroupWithStatus ( t , folderUID , & postableGroup1 , false )
2022-06-22 22:52:46 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-02-08 00:24:28 +08:00
group1Get , status = client . GetRulesGroup ( t , folderUID , group1 . Name )
require . Equal ( t , http . StatusAccepted , status )
2022-06-22 22:52:46 +08:00
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 )
2025-02-08 00:24:28 +08:00
group2Get , status = client . GetRulesGroup ( t , folderUID , group2 . Name )
require . Equal ( t , http . StatusAccepted , status )
2022-06-22 22:52:46 +08:00
assert . Len ( t , group2Get . Rules , len ( group2 . Rules ) - 1 )
for _ , rule := range group2Get . Rules {
require . NotEqual ( t , movedRule . GrafanaManagedAlert . UID , rule . GrafanaManagedAlert . UID )
}
} )
}
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
func TestIntegrationRuleCreate ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
AppModeProduction : true ,
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "admin" ,
Login : "admin" ,
} )
client := newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" )
namespaceUID := "default"
client . CreateFolder ( t , namespaceUID , namespaceUID )
cases := [ ] struct {
2025-01-28 03:31:40 +08:00
name string
config apimodels . PostableRuleGroupConfig
expected apimodels . GettableRuleGroupConfig
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
} { {
name : "can create a rule with UTF-8" ,
config : apimodels . PostableRuleGroupConfig {
Name : "test1" ,
Interval : model . Duration ( time . Minute ) ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
{
ApiRuleNode : & apimodels . ApiRuleNode {
2025-03-18 18:24:48 +08:00
For : util . Pointer ( model . Duration ( 2 * time . Minute ) ) ,
KeepFiringFor : util . Pointer ( model . Duration ( 1 * time . Minute ) ) ,
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
Labels : map [ string ] string {
"foo🙂" : "bar" ,
"_bar1" : "baz🙂" ,
} ,
Annotations : map [ string ] string {
2025-01-28 03:31:40 +08:00
"Προμηθέας" : "prom" , // Prometheus in Greek
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
} ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "test1 rule1" ,
Condition : "A" ,
Data : [ ] apimodels . AlertQuery {
{
RefID : "A" ,
RelativeTimeRange : apimodels . RelativeTimeRange {
From : apimodels . Duration ( 0 ) ,
To : apimodels . Duration ( 15 * time . Minute ) ,
} ,
DatasourceUID : expr . DatasourceUID ,
Model : json . RawMessage ( ` { "type": "math","expression": "1"} ` ) ,
} ,
} ,
} ,
} ,
} ,
} ,
2025-01-28 03:31:40 +08:00
expected : apimodels . GettableRuleGroupConfig {
Name : "test1" ,
Interval : model . Duration ( time . Minute ) ,
Rules : [ ] apimodels . GettableExtendedRuleNode {
{
ApiRuleNode : & apimodels . ApiRuleNode {
2025-03-18 18:24:48 +08:00
For : util . Pointer ( model . Duration ( 2 * time . Minute ) ) ,
KeepFiringFor : util . Pointer ( model . Duration ( 1 * time . Minute ) ) ,
2025-01-28 03:31:40 +08:00
Labels : map [ string ] string {
"foo🙂" : "bar" ,
"_bar1" : "baz🙂" ,
} ,
Annotations : map [ string ] string {
"Προμηθέας" : "prom" , // Prometheus in Greek
} ,
} ,
GrafanaManagedAlert : & apimodels . GettableGrafanaRule {
Title : "test1 rule1" ,
Condition : "A" ,
Data : [ ] apimodels . AlertQuery {
{
RefID : "A" ,
RelativeTimeRange : apimodels . RelativeTimeRange {
From : apimodels . Duration ( 0 ) ,
To : apimodels . Duration ( 15 * time . Minute ) ,
} ,
DatasourceUID : expr . DatasourceUID ,
Model : json . RawMessage ( ` { "expression":"1","intervalMs":1000,"maxDataPoints":43200,"type":"math"} ` ) ,
} ,
} ,
UpdatedBy : & apimodels . UserInfo {
Name : "admin" ,
} ,
IntervalSeconds : 60 ,
Version : 1 ,
NamespaceUID : namespaceUID ,
RuleGroup : "test1" ,
NoDataState : "NoData" ,
ExecErrState : "Alerting" ,
Provenance : "" ,
IsPaused : false ,
Metadata : & apimodels . AlertRuleMetadata { } ,
} ,
} ,
} ,
} ,
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
} }
for _ , tc := range cases {
t . Run ( tc . name , func ( t * testing . T ) {
2025-03-15 04:14:06 +08:00
resp , status , _ := client . PostRulesGroupWithStatus ( t , namespaceUID , & tc . config , false )
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
require . Equal ( t , http . StatusAccepted , status )
require . Len ( t , resp . Created , 1 )
require . Len ( t , resp . Updated , 0 )
require . Len ( t , resp . Deleted , 0 )
2025-01-28 03:31:40 +08:00
got , _ , _ := client . GetRulesGroupWithStatus ( t , namespaceUID , tc . config . Name )
pathsToIgnore := [ ] string {
"GrafanaManagedAlert.Updated" ,
"GrafanaManagedAlert.UpdatedBy.UID" ,
"GrafanaManagedAlert.UID" ,
"GrafanaManagedAlert.ID" ,
"GrafanaManagedAlert.NamespaceID" ,
2025-03-15 04:14:06 +08:00
"GrafanaManagedAlert.GUID" ,
2025-01-28 03:31:40 +08:00
}
// compare expected and actual and ignore the dynamic fields
diff := cmp . Diff ( tc . expected , got . GettableRuleGroupConfig , cmp . FilterPath ( func ( path cmp . Path ) bool {
for _ , s := range pathsToIgnore {
if strings . HasSuffix ( path . String ( ) , s ) {
return true
}
}
return false
} , cmp . Ignore ( ) ) )
require . Empty ( t , diff )
Alerting: Support UTF-8 (#81512)
This pull request updates our fork of Alertmanager to commit 65bdab0, which is based on commit 5658f8c in Prometheus Alertmanager.
It applies the changes from grafana/alerting#155 which removes the overrides for validation of alerts, labels and silences that we had put in place to allow alerts and silences to work for non-Prometheus datasources. However, as this is now supported in Alertmanager with the UTF-8 work, we can use the new upstream functions and remove these overrides.
The compat package is a package in Alertmanager that takes care of backwards compatibility when parsing matchers, validating alerts, labels and silences. It has three modes: classic mode, UTF-8 strict mode, fallback mode. These modes are controlled via compat.InitFromFlags. Grafana initializes the compat package without any feature flags, which is the equivalent of fallback mode. Classic and UTF-8 strict mode are used in Mimir.
While Grafana Managed Alerts have no need for fallback mode, Grafana can still be used as an interface to manage the configurations of Mimir Alertmanagers and view configurations of Prometheus Alertmanager, and those installations might not have migrated or being running on older versions. Such installations behave as if in classic mode, and Grafana must be able to parse their configurations to interact with them for some period of time. As such, Grafana uses fallback mode until we are ready to drop support for outdated installations of Mimir and the Prometheus Alertmanager.
2024-02-06 16:33:47 +08:00
} )
}
}
2022-12-09 15:11:56 +08:00
func TestIntegrationRuleUpdate ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
2022-06-30 23:46:26 +08:00
// Setup Grafana and its Database
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
DisableAnonymous : true ,
AppModeProduction : true ,
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
2024-05-28 23:32:23 +08:00
permissionsStore := resourcepermissions . NewStore ( env . Cfg , env . SQLStore , featuremgmt . WithFeatures ( ) )
2022-06-30 23:46:26 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
userID := createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2022-08-10 17:56:48 +08:00
DefaultOrgRole : string ( org . RoleEditor ) ,
2022-06-30 23:46:26 +08:00
Password : "password" ,
Login : "grafana" ,
} )
2023-06-16 01:33:42 +08:00
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
2024-04-04 21:04:47 +08:00
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2023-06-16 01:33:42 +08:00
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "admin" ,
Login : "admin" ,
} )
adminClient := newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" )
2022-06-30 23:46:26 +08:00
client := newAlertingApiClient ( grafanaListedAddr , "grafana" , "password" )
2024-01-17 17:07:39 +08:00
folderUID := util . GenerateShortUID ( )
client . CreateFolder ( t , folderUID , "folder1" )
2022-06-30 23:46:26 +08:00
t . Run ( "should be able to reset 'for' to 0" , func ( t * testing . T ) {
group := generateAlertRuleGroup ( 1 , alertRuleGen ( ) )
expected := model . Duration ( 10 * time . Second )
2025-04-10 20:42:23 +08:00
group . Rules [ 0 ] . For = & expected
2022-06-30 23:46:26 +08:00
2025-03-15 04:14:06 +08:00
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2022-06-30 23:46:26 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2025-04-10 20:42:23 +08:00
require . Equal ( t , expected , * getGroup . Rules [ 0 ] . For )
2022-06-30 23:46:26 +08:00
group = convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
expected = 0
2025-04-10 20:42:23 +08:00
group . Rules [ 0 ] . For = & expected
2025-03-15 04:14:06 +08:00
_ , status , body = client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2022-06-30 23:46:26 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2025-04-10 20:42:23 +08:00
require . Equal ( t , expected , * getGroup . Rules [ 0 ] . For )
2022-06-30 23:46:26 +08:00
} )
2025-03-18 18:24:48 +08:00
t . Run ( "should be able to reset 'keep_firing_for' to 0" , func ( t * testing . T ) {
group := generateAlertRuleGroup ( 1 , alertRuleGen ( ) )
keepFiringFor := model . Duration ( 10 * time . Second )
2025-04-10 20:42:23 +08:00
group . Rules [ 0 ] . KeepFiringFor = & keepFiringFor
2025-03-18 18:24:48 +08:00
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
getGroup , _ := client . GetRulesGroup ( t , folderUID , group . Name )
2025-04-10 20:42:23 +08:00
require . Equal ( t , keepFiringFor , * getGroup . Rules [ 0 ] . KeepFiringFor )
2025-03-18 18:24:48 +08:00
group = convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
newKeepFiringFor := model . Duration ( 0 )
2025-04-10 20:42:23 +08:00
group . Rules [ 0 ] . KeepFiringFor = & newKeepFiringFor
2025-03-18 18:24:48 +08:00
_ , status , body = client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
getGroup , _ = client . GetRulesGroup ( t , folderUID , group . Name )
2025-04-10 20:42:23 +08:00
require . Equal ( t , newKeepFiringFor , * getGroup . Rules [ 0 ] . KeepFiringFor )
2025-03-18 18:24:48 +08:00
} )
2025-03-26 20:34:53 +08:00
t . Run ( "missing_series_evals_to_resolve" , func ( t * testing . T ) {
testCases := [ ] struct {
name string
initialValue * int
updatedValue * int
expectedValue * int
expectedStatus int
} {
{
name : "should be able to set missing_series_evals_to_resolve to 5" ,
initialValue : nil ,
updatedValue : util . Pointer ( 5 ) ,
expectedValue : util . Pointer ( 5 ) ,
expectedStatus : http . StatusAccepted ,
} ,
{
name : "should be able to update missing_series_evals_to_resolve" ,
initialValue : util . Pointer ( 1 ) ,
updatedValue : util . Pointer ( 2 ) ,
expectedValue : util . Pointer ( 2 ) ,
expectedStatus : http . StatusAccepted ,
} ,
{
name : "should preserve missing_series_evals_to_resolve when it's set nil" ,
initialValue : util . Pointer ( 5 ) ,
updatedValue : nil ,
expectedValue : util . Pointer ( 5 ) ,
expectedStatus : http . StatusAccepted ,
} ,
{
name : "should reject missing_series_evals_to_resolve < 0" ,
initialValue : util . Pointer ( 1 ) ,
updatedValue : util . Pointer ( - 1 ) ,
expectedStatus : http . StatusBadRequest ,
} ,
{
name : "should be able to reset missing_series_evals_to_resolve by setting it to 0" ,
initialValue : util . Pointer ( 1 ) ,
updatedValue : util . Pointer ( 0 ) ,
expectedValue : nil ,
expectedStatus : http . StatusAccepted ,
} ,
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
// Create a new rule
group := generateAlertRuleGroup ( 1 , alertRuleGen ( ) )
group . Rules [ 0 ] . GrafanaManagedAlert . MissingSeriesEvalsToResolve = tc . initialValue
// Post the rule group with our alert rule
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
// and the value of the missing_series_evals_to_resolve
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
if tc . initialValue == nil {
require . Nil ( t , getGroup . Rules [ 0 ] . GrafanaManagedAlert . MissingSeriesEvalsToResolve )
} else {
require . Equal ( t , * tc . initialValue , * getGroup . Rules [ 0 ] . GrafanaManagedAlert . MissingSeriesEvalsToResolve )
}
// Now let's update the initial value with the updated value
group = convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
group . Rules [ 0 ] . GrafanaManagedAlert . MissingSeriesEvalsToResolve = tc . updatedValue
_ , status , body = client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
require . Equalf ( t , tc . expectedStatus , status , "failed to post rule group. Response: %s" , body )
// Check the response status
require . Equal ( t , tc . expectedStatus , status )
if tc . expectedStatus != http . StatusAccepted {
// If the status is not accepted, we don't need to check the response body
return
}
// Get the group again and check that the value is updated to updatedValue
getGroup , status = client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
if tc . expectedValue == nil {
require . Nil ( t , getGroup . Rules [ 0 ] . GrafanaManagedAlert . MissingSeriesEvalsToResolve )
} else {
require . Equal ( t , * tc . expectedValue , * getGroup . Rules [ 0 ] . GrafanaManagedAlert . MissingSeriesEvalsToResolve )
}
} )
}
} )
2023-06-16 01:33:42 +08:00
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 ) ) )
2025-03-15 04:14:06 +08:00
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-06-16 01:33:42 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
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 ) {
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , groupName )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
group := convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
2025-03-15 04:14:06 +08:00
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-06-16 01:33:42 +08:00
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 ) {
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , groupName )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
group := convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
group . Rules [ 0 ] . GrafanaManagedAlert . Title = uuid . NewString ( )
2025-03-15 04:14:06 +08:00
resp , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-06-16 01:33:42 +08:00
if status == http . StatusAccepted {
2023-10-07 06:11:24 +08:00
assert . Len ( t , resp . Deleted , 1 )
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
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 ) {
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , groupName )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
group := convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
// remove the last rule.
group . Rules = group . Rules [ 0 : len ( group . Rules ) - 1 ]
2025-03-15 04:14:06 +08:00
resp , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-06-16 01:33:42 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to delete last rule from group. Response: %s" , body )
2023-10-07 06:11:24 +08:00
assert . Len ( t , resp . Deleted , 1 )
2023-06-16 01:33:42 +08:00
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
group = convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
require . Len ( t , group . Rules , 2 )
} )
t . Run ( "should let fix single rule" , func ( t * testing . T ) {
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , groupName )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
group := convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
ds2 := adminClient . CreateTestDatasource ( t )
withDatasourceQuery ( ds2 . Body . Datasource . UID ) ( & group . Rules [ 0 ] )
2025-03-15 04:14:06 +08:00
resp , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-06-16 01:33:42 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post noop rule group. Response: %s" , body )
2023-10-07 06:11:24 +08:00
assert . Len ( t , resp . Deleted , 0 )
assert . Len ( t , resp . Updated , 2 )
assert . Len ( t , resp . Created , 0 )
2023-06-16 01:33:42 +08:00
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2023-06-16 01:33:42 +08:00
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 ) {
2025-03-15 04:14:06 +08:00
status , body := client . DeleteRulesGroup ( t , folderUID , groupName , false )
2023-06-16 01:33:42 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post noop rule group. Response: %s" , body )
} )
} )
2025-01-28 03:31:40 +08:00
t . Run ( "should set updated_by" , func ( t * testing . T ) {
group := generateAlertRuleGroup ( 1 , alertRuleGen ( ) )
expected := model . Duration ( 10 * time . Second )
2025-04-10 20:42:23 +08:00
group . Rules [ 0 ] . For = & expected
2025-01-28 03:31:40 +08:00
2025-03-15 04:14:06 +08:00
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2025-01-28 03:31:40 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2025-01-28 03:31:40 +08:00
require . NotNil ( t , getGroup . Rules [ 0 ] . GrafanaManagedAlert . UpdatedBy )
assert . NotEmpty ( t , getGroup . Rules [ 0 ] . GrafanaManagedAlert . UpdatedBy . UID )
assert . Equal ( t , "grafana" , getGroup . Rules [ 0 ] . GrafanaManagedAlert . UpdatedBy . Name )
} )
2022-06-30 23:46:26 +08:00
}
2025-01-25 03:49:05 +08:00
func TestIntegrationAlertAndGroupsQuery ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
2022-04-14 20:21:36 +08:00
2025-01-25 03:49:05 +08:00
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
DisableAnonymous : true ,
AppModeProduction : true ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
// unauthenticated request to get the alerts should fail
{
alertsURL := fmt . Sprintf ( "http://%s/api/alertmanager/grafana/api/v2/alerts" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( alertsURL )
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 , http . StatusUnauthorized , resp . StatusCode )
require . Contains ( t , string ( b ) , ` "message":"Unauthorized" ` )
2022-04-14 20:21:36 +08:00
}
2025-01-25 03:49:05 +08:00
// Create a user to make authenticated requests
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleEditor ) ,
Password : "password" ,
Login : "grafana" ,
} )
apiClient := newAlertingApiClient ( grafanaListedAddr , "grafana" , "password" )
// invalid credentials request to get the alerts should fail
{
alertsURL := fmt . Sprintf ( "http://grafana:invalid@%s/api/alertmanager/grafana/api/v2/alerts" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( alertsURL )
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 )
assert . Equal ( t , http . StatusUnauthorized , resp . StatusCode )
var res map [ string ] any
require . NoError ( t , json . Unmarshal ( b , & res ) )
assert . Equal ( t , "Invalid username or password" , res [ "message" ] )
}
// When there are no alerts available, it returns an empty list.
{
alertsURL := fmt . Sprintf ( "http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( alertsURL )
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 , "[]" , string ( b ) )
}
// When are there no alerts available, it returns an empty list of groups.
{
alertsURL := fmt . Sprintf ( "http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts/groups" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( alertsURL )
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 . NoError ( t , err )
require . Equal ( t , 200 , resp . StatusCode )
require . JSONEq ( t , "[]" , string ( b ) )
}
// Now, let's test the endpoint with some alerts.
{
// Create the namespace we'll save our alerts to.
apiClient . CreateFolder ( t , "default" , "default" )
}
// Create an alert that will fire as quickly as possible
{
interval , err := model . ParseDuration ( "10s" )
require . NoError ( t , err )
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Interval : interval ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
2022-04-14 20:21:36 +08:00
{
2025-01-25 03:49:05 +08:00
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"
} ` ) ,
} ,
} ,
2022-04-14 20:21:36 +08:00
} ,
} ,
} ,
2025-01-25 03:49:05 +08:00
}
2025-03-15 04:14:06 +08:00
_ , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2022-04-14 20:21:36 +08:00
}
2025-01-25 03:49:05 +08:00
// Eventually, we'll get an alert with its state being active.
{
alertsURL := fmt . Sprintf ( "http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts" , grafanaListedAddr )
// nolint:gosec
require . Eventually ( t , func ( ) bool {
resp , err := http . Get ( alertsURL )
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 )
var alerts apimodels . GettableAlerts
err = json . Unmarshal ( b , & alerts )
require . NoError ( t , err )
if len ( alerts ) > 0 {
status := alerts [ 0 ] . Status
return status != nil && status . State != nil && * status . State == "active"
}
return false
} , 18 * time . Second , 2 * time . Second )
2022-04-14 20:21:36 +08:00
}
}
2023-02-01 20:15:03 +08:00
2025-01-25 03:49:05 +08:00
func TestIntegrationRulerAccess ( t * testing . T ) {
2023-02-01 20:15:03 +08:00
testinfra . SQLiteIntegrationTest ( t )
// Setup Grafana and its Database
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
2025-01-25 03:49:05 +08:00
EnableQuota : true ,
2023-02-01 20:15:03 +08:00
DisableAnonymous : true ,
2025-01-25 03:49:05 +08:00
ViewersCanEdit : true ,
2023-02-01 20:15:03 +08:00
AppModeProduction : true ,
} )
2025-01-25 03:49:05 +08:00
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
2023-02-01 20:15:03 +08:00
2025-01-25 03:49:05 +08:00
// Create a users to make authenticated requests
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleViewer ) ,
Password : "viewer" ,
Login : "viewer" ,
} )
2024-04-04 21:04:47 +08:00
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2023-02-01 20:15:03 +08:00
DefaultOrgRole : string ( org . RoleEditor ) ,
2025-01-25 03:49:05 +08:00
Password : "editor" ,
Login : "editor" ,
} )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "admin" ,
Login : "admin" ,
2023-02-01 20:15:03 +08:00
} )
2025-01-25 03:49:05 +08:00
client := newAlertingApiClient ( grafanaListedAddr , "editor" , "editor" )
2023-02-01 20:15:03 +08:00
2025-01-25 03:49:05 +08:00
// Create the namespace we'll save our alerts to.
client . CreateFolder ( t , "default" , "default" )
2023-02-01 20:15:03 +08:00
2025-01-25 03:49:05 +08:00
// Now, let's test the access policies.
testCases := [ ] struct {
desc string
client apiClient
expStatus int
expectedMessage string
} {
{
desc : "un-authenticated request should fail" ,
client : newAlertingApiClient ( grafanaListedAddr , "" , "" ) ,
expStatus : http . StatusUnauthorized ,
expectedMessage : ` Unauthorized ` ,
} ,
{
desc : "viewer request should fail" ,
client : newAlertingApiClient ( grafanaListedAddr , "viewer" , "viewer" ) ,
expStatus : http . StatusForbidden ,
expectedMessage : ` You'll need additional permissions to perform this action. Permissions needed: all of alert.rules:read, folders:read, any of alert.rules:write, alert.rules:create, alert.rules:delete ` ,
} ,
{
desc : "editor request should succeed" ,
client : newAlertingApiClient ( grafanaListedAddr , "editor" , "editor" ) ,
expStatus : http . StatusAccepted ,
expectedMessage : ` rule group updated successfully ` ,
} ,
{
desc : "admin request should succeed" ,
client : newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" ) ,
expStatus : http . StatusAccepted ,
expectedMessage : ` rule group updated successfully ` ,
} ,
}
2023-02-01 20:15:03 +08:00
2025-01-25 03:49:05 +08:00
for i , tc := range testCases {
t . Run ( tc . desc , func ( t * testing . T ) {
interval , err := model . ParseDuration ( "1m" )
require . NoError ( t , err )
2023-02-01 20:15:03 +08:00
2025-01-25 03:49:05 +08:00
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] 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 : fmt . Sprintf ( "AlwaysFiring %d" , i ) ,
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"
} ` ) ,
} ,
} ,
} ,
} ,
} ,
}
2025-03-15 04:14:06 +08:00
_ , status , body := tc . client . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-01-25 03:49:05 +08:00
assert . Equal ( t , tc . expStatus , status )
res := & Response { }
err = json . Unmarshal ( [ ] byte ( body ) , & res )
require . NoError ( t , err )
require . Equal ( t , tc . expectedMessage , res . Message )
} )
}
}
func TestIntegrationEval ( 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 ,
AppModeProduction : true ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
createUser ( t , env . SQLStore , env . Cfg , 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 , "default" , "default" )
// test eval conditions
testCases := [ ] struct {
desc string
payload string
expectedStatusCode func ( ) int
expectedResponse func ( ) string
expectedMessage func ( ) string
} {
{
desc : "alerting condition" ,
payload : `
{
"data" : [
{
"refId" : "A" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"type" : "math" ,
"expression" : "1 < 2"
}
}
] ,
"condition" : "A" ,
"now" : "2021-04-11T14:38:14Z"
}
` ,
expectedMessage : func ( ) string { return "" } ,
expectedStatusCode : func ( ) int { return http . StatusOK } ,
expectedResponse : func ( ) string {
return ` {
"results" : {
"A" : {
"status" : 200 ,
"frames" : [
{
"schema" : {
"refId" : "A" ,
"fields" : [
{
"name" : "A" ,
"type" : "number" ,
"typeInfo" : {
"frame" : "float64" ,
"nullable" : true
}
}
] ,
"meta" : {
"type" : "numeric-multi" ,
"typeVersion" : [ 0 , 1 ]
}
} ,
"data" : {
"values" : [
[
1
]
]
}
}
]
}
}
} `
} ,
} ,
{
desc : "normal condition" ,
payload : `
{
"data" : [
{
"refId" : "A" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"type" : "math" ,
"expression" : "1 > 2"
}
}
] ,
"condition" : "A" ,
"now" : "2021-04-11T14:38:14Z"
}
` ,
expectedMessage : func ( ) string { return "" } ,
expectedStatusCode : func ( ) int { return http . StatusOK } ,
expectedResponse : func ( ) string {
return ` {
"results" : {
"A" : {
"status" : 200 ,
"frames" : [
{
"schema" : {
"refId" : "A" ,
"fields" : [
{
"name" : "A" ,
"type" : "number" ,
"typeInfo" : {
"frame" : "float64" ,
"nullable" : true
}
}
] ,
"meta" : {
"type" : "numeric-multi" ,
"typeVersion" : [ 0 , 1 ]
}
} ,
"data" : {
"values" : [
[
0
]
]
}
}
]
}
}
} `
} ,
} ,
{
desc : "unknown query datasource" ,
payload : `
{
"data" : [
{
"refId" : "A" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "unknown" ,
"model" : {
}
}
] ,
"condition" : "A" ,
"now" : "2021-04-11T14:38:14Z"
}
` ,
expectedResponse : func ( ) string { return "" } ,
expectedStatusCode : func ( ) int {
if setting . IsEnterprise {
return http . StatusForbidden
}
return http . StatusBadRequest
} ,
expectedMessage : func ( ) string {
if setting . IsEnterprise {
return "user is not authorized to access one or many data sources"
}
return "Failed to build evaluator for queries and expressions: failed to build query 'A': data source not found"
} ,
} ,
{
desc : "condition is empty" ,
payload : `
{
"data" : [
{
"refId" : "A" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"type" : "math" ,
"expression" : "1 > 2"
}
}
] ,
"now" : "2021-04-11T14:38:14Z"
}
` ,
expectedStatusCode : func ( ) int { return http . StatusOK } ,
expectedMessage : func ( ) string { return "" } ,
expectedResponse : func ( ) string {
return ` {
"results" : {
"A" : {
"status" : 200 ,
"frames" : [
{
"schema" : {
"refId" : "A" ,
"fields" : [
{
"name" : "A" ,
"type" : "number" ,
"typeInfo" : {
"frame" : "float64" ,
"nullable" : true
}
}
] ,
"meta" : {
"type" : "numeric-multi" ,
"typeVersion" : [ 0 , 1 ]
}
} ,
"data" : {
"values" : [
[
0
]
]
}
}
]
}
}
} `
} ,
} ,
}
for _ , tc := range testCases {
t . Run ( tc . desc , func ( t * testing . T ) {
u := fmt . Sprintf ( "http://grafana:password@%s/api/v1/eval" , grafanaListedAddr )
r := strings . NewReader ( tc . payload )
// nolint:gosec
resp , err := http . Post ( u , "application/json" , r )
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 )
res := Response { }
err = json . Unmarshal ( b , & res )
require . NoError ( t , err )
assert . Equal ( t , tc . expectedStatusCode ( ) , resp . StatusCode )
if tc . expectedResponse ( ) != "" {
require . JSONEq ( t , tc . expectedResponse ( ) , string ( b ) )
}
if tc . expectedMessage ( ) != "" {
require . Equal ( t , tc . expectedMessage ( ) , res . Message )
}
} )
}
}
func TestIntegrationQuota ( 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 ,
AppModeProduction : true ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
// Create a user to make authenticated requests
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
// needs permission to update org quota
IsAdmin : true ,
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 , "default" , "default" )
interval , err := model . ParseDuration ( "1m" )
require . NoError ( t , err )
// Create rule under folder1
createRule ( t , apiClient , "default" )
// get the generated rule UID
var ruleUID string
{
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , resp . StatusCode , 202 )
_ , m := rulesNamespaceWithoutVariableValues ( t , b )
generatedUIDs , ok := m [ "default,arulegroup" ]
assert . True ( t , ok )
assert . Equal ( t , 1 , len ( generatedUIDs ) )
ruleUID = generatedUIDs [ 0 ]
}
// check quota limits
t . Run ( "when quota limit exceed creating new rule should fail" , func ( t * testing . T ) {
// get existing org quota
limit , used := apiClient . GetOrgQuotaLimits ( t , 1 )
apiClient . UpdateAlertRuleOrgQuota ( t , 1 , used )
t . Cleanup ( func ( ) {
apiClient . UpdateAlertRuleOrgQuota ( t , 1 , limit )
} )
// try to create an alert rule
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Interval : interval ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
{
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "One more alert rule" ,
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"
} ` ) ,
} ,
} ,
} ,
} ,
} ,
}
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-01-25 03:49:05 +08:00
assert . Equal ( t , http . StatusForbidden , status )
var res map [ string ] any
require . NoError ( t , json . Unmarshal ( [ ] byte ( body ) , & res ) )
require . Equal ( t , "quota has been exceeded" , res [ "message" ] )
} )
t . Run ( "when quota limit exceed updating existing rule should succeed" , func ( t * testing . T ) {
// try to create an alert rule
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Interval : interval ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
{
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "Updated alert rule" ,
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 + 4 > 1"
} ` ) ,
} ,
} ,
UID : ruleUID ,
} ,
} ,
} ,
}
2025-03-15 04:14:06 +08:00
respModel , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
require . Len ( t , respModel . Updated , 1 )
// let's make sure that rule definitions are updated correctly.
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , resp . StatusCode , 202 )
body , m := rulesNamespaceWithoutVariableValues ( t , b )
returnedUIDs , ok := m [ "default,arulegroup" ]
assert . True ( t , ok )
assert . Equal ( t , 1 , len ( returnedUIDs ) )
assert . Equal ( t , ruleUID , returnedUIDs [ 0 ] )
assert . JSONEq ( t , `
{
"default" : [
{
"name" : "arulegroup" ,
"interval" : "1m" ,
"rules" : [
{
"expr" : "" ,
"for" : "2m" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "0s" ,
2025-01-25 03:49:05 +08:00
"grafana_alert" : {
"title" : "Updated alert rule" ,
"condition" : "A" ,
"data" : [
{
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"expression" : "2 + 4 \u003E 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
}
] ,
"updated" : "2021-02-21T01:10:30Z" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
"uid" : "uid" ,
"name" : "grafana"
} ,
2025-01-25 03:49:05 +08:00
"intervalSeconds" : 60 ,
"is_paused" : false ,
"version" : 2 ,
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-01-25 03:49:05 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "NoData" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
}
]
}
]
} ` , body )
} )
}
func TestIntegrationDeleteFolderWithRules ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
opts := testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
EnableQuota : true ,
DisableAnonymous : true ,
ViewersCanEdit : true ,
AppModeProduction : true ,
}
// Setup Grafana and its Database
dir , path := testinfra . CreateGrafDir ( t , opts )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleViewer ) ,
Password : "viewer" ,
Login : "viewer" ,
} )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleEditor ) ,
Password : "editor" ,
Login : "editor" ,
} )
apiClient := newAlertingApiClient ( grafanaListedAddr , "editor" , "editor" )
// Create the namespace we'll save our alerts to.
namespaceUID := "default" //nolint:goconst
apiClient . CreateFolder ( t , namespaceUID , namespaceUID )
createRule ( t , apiClient , "default" )
t . Run ( "editor create a rule within the folder/namespace" , func ( t * testing . T ) {
u := fmt . Sprintf ( "http://editor:editor@%s/api/ruler/grafana/api/v1/rules" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , 200 , resp . StatusCode )
2025-01-28 03:31:40 +08:00
body , _ := rulesNamespaceWithoutVariableValues ( t , b )
expectedGetRulesResponseBody := ` {
2025-01-25 03:49:05 +08:00
"default" : [
{
"name" : "arulegroup" ,
"interval" : "1m" ,
"rules" : [
{
"expr" : "" ,
"for" : "2m" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "0s" ,
2025-01-25 03:49:05 +08:00
"labels" : {
"label1" : "val1"
} ,
"annotations" : {
"annotation1" : "val1"
} ,
"grafana_alert" : {
"title" : "rule under folder default" ,
"condition" : "A" ,
"data" : [
{
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"expression" : "2 + 3 > 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
}
] ,
2025-01-28 03:31:40 +08:00
"updated" : "2021-02-21T01:10:30Z" ,
"updated_by" : {
"uid" : "uid" ,
2025-02-08 00:24:28 +08:00
"name" : "editor"
2025-01-28 03:31:40 +08:00
} ,
2025-01-25 03:49:05 +08:00
"intervalSeconds" : 60 ,
"is_paused" : false ,
"version" : 1 ,
2025-01-28 03:31:40 +08:00
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-01-28 03:31:40 +08:00
"namespace_uid" : "nsuid" ,
2025-01-25 03:49:05 +08:00
"rule_group" : "arulegroup" ,
"no_data_state" : "NoData" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
}
]
}
]
2025-01-28 03:31:40 +08:00
} `
assert . JSONEq ( t , expectedGetRulesResponseBody , body )
2025-01-25 03:49:05 +08:00
} )
t . Run ( "editor can not delete the folder because it contains Grafana 8 alerts" , func ( t * testing . T ) {
u := fmt . Sprintf ( "http://editor:editor@%s/api/folders/%s" , grafanaListedAddr , namespaceUID )
req , err := http . NewRequest ( http . MethodDelete , u , nil )
require . NoError ( t , err )
client := & http . Client { }
resp , err := client . Do ( req )
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 , http . StatusBadRequest , resp . StatusCode )
var errutilErr errutil . PublicError
err = json . Unmarshal ( b , & errutilErr )
require . NoError ( t , err )
assert . Equal ( t , "Folder cannot be deleted: folder is not empty" , errutilErr . Message )
} )
t . Run ( "editor can delete the folder if forceDeleteRules is true" , func ( t * testing . T ) {
u := fmt . Sprintf ( "http://editor:editor@%s/api/folders/%s?forceDeleteRules=true" , grafanaListedAddr , namespaceUID )
req , err := http . NewRequest ( http . MethodDelete , u , nil )
require . NoError ( t , err )
client := & http . Client { }
resp , err := client . Do ( req )
require . NoError ( t , err )
t . Cleanup ( func ( ) {
err := resp . Body . Close ( )
require . NoError ( t , err )
} )
_ , err = io . ReadAll ( resp . Body )
require . NoError ( t , err )
require . Equal ( t , 200 , resp . StatusCode )
} )
t . Run ( "editor can delete rules" , func ( t * testing . T ) {
u := fmt . Sprintf ( "http://editor:editor@%s/api/ruler/grafana/api/v1/rules" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , 200 , resp . StatusCode )
assert . JSONEq ( t , "{}" , string ( b ) )
} )
// TODO(@leonorfmartins): write tests for uni store when we are able to support it
}
func TestIntegrationAlertRuleCRUD ( 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 ,
AppModeProduction : true ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
createUser ( t , env . SQLStore , env . Cfg , 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 , "default" , "default" )
interval , err := model . ParseDuration ( "1m" )
require . NoError ( t , err )
invalidInterval , err := model . ParseDuration ( "1s" )
require . NoError ( t , err )
// Now, let's try to create some invalid alert rules.
{
testCases := [ ] struct {
desc string
rulegroup string
interval model . Duration
rule apimodels . PostableExtendedRuleNode
expectedCode int
expectedMessage string
} {
{
desc : "alert rule without queries and expressions" ,
rulegroup : "arulegroup" ,
rule : apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "AlwaysFiring" ,
Condition : "A" ,
Data : [ ] apimodels . AlertQuery { } ,
} ,
} ,
expectedMessage : "invalid rule specification at index [0]: invalid alert rule: no queries or expressions are found" ,
} ,
{
desc : "alert rule with empty title" ,
rulegroup : "arulegroup" ,
rule : apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "" ,
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"
} ` ) ,
} ,
} ,
} ,
} ,
expectedMessage : "invalid rule specification at index [0]: alert rule title cannot be empty" ,
} ,
{
desc : "alert rule with too long name" ,
rulegroup : "arulegroup" ,
rule : apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : getLongString ( t , ngstore . AlertRuleMaxTitleLength + 1 ) ,
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"
} ` ) ,
} ,
} ,
} ,
} ,
expectedMessage : "invalid rule specification at index [0]: alert rule title is too long. Max length is 190" ,
} ,
{
desc : "alert rule with too long rulegroup" ,
rulegroup : getLongString ( t , ngstore . AlertRuleMaxTitleLength + 1 ) ,
rule : apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
} ,
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"
} ` ) ,
} ,
} ,
} ,
} ,
expectedMessage : "rule group name is too long. Max length is 190" ,
} ,
{
desc : "alert rule with invalid interval" ,
rulegroup : "arulegroup" ,
interval : invalidInterval ,
rule : apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
} ,
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"
} ` ) ,
} ,
} ,
} ,
} ,
expectedMessage : "rule evaluation interval (1 second) should be positive number that is multiple of the base interval of 10 seconds" ,
} ,
{
desc : "alert rule with unknown datasource" ,
rulegroup : "arulegroup" ,
rule : apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
} ,
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 : "unknown" ,
Model : json . RawMessage ( ` {
"type" : "math" ,
"expression" : "2 + 3 > 1"
} ` ) ,
} ,
} ,
} ,
} ,
expectedCode : func ( ) int {
if setting . IsEnterprise {
return http . StatusForbidden
}
return http . StatusBadRequest
} ( ) ,
expectedMessage : func ( ) string {
if setting . IsEnterprise {
return "user is not authorized to create a new alert rule 'AlwaysFiring'"
}
return "failed to update rule group: invalid alert rule 'AlwaysFiring': failed to build query 'A': data source not found"
} ( ) ,
} ,
{
desc : "alert rule with invalid condition" ,
rulegroup : "arulegroup" ,
rule : apimodels . PostableExtendedRuleNode {
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string { "label1" : "val1" } ,
Annotations : map [ string ] string { "annotation1" : "val1" } ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
Title : "AlwaysFiring" ,
Condition : "B" ,
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"
} ` ) ,
} ,
} ,
} ,
} ,
expectedMessage : "invalid rule specification at index [0]: invalid alert rule: condition B does not exist, must be one of [A]" ,
} ,
}
for _ , tc := range testCases {
t . Run ( tc . desc , func ( t * testing . T ) {
rules := apimodels . PostableRuleGroupConfig {
Name : tc . rulegroup ,
Interval : tc . interval ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
tc . rule ,
} ,
}
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-01-25 03:49:05 +08:00
res := & Response { }
err = json . Unmarshal ( [ ] byte ( body ) , & res )
require . NoError ( t , err )
assert . Equal ( t , tc . expectedMessage , res . Message )
expectedCode := tc . expectedCode
if expectedCode == 0 {
expectedCode = http . StatusBadRequest
}
assert . Equal ( t , expectedCode , status )
} )
}
}
var ruleUID string
var expectedGetNamespaceResponseBody string
// Now, let's create two alerts.
{
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] 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"
} ` ) ,
} ,
} ,
} ,
} ,
{
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 ) ,
} ,
} ,
} ,
}
2025-03-15 04:14:06 +08:00
resp , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
require . Equal ( t , "rule group updated successfully" , resp . Message )
assert . Len ( t , resp . Created , 2 )
assert . Empty ( t , resp . Updated )
assert . Empty ( t , resp . Deleted )
}
2025-02-10 23:28:34 +08:00
createdRuleUIDs := make ( map [ string ] string )
2025-01-25 03:49:05 +08:00
// With the rules created, let's make sure that rule definition is stored correctly.
{
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , resp . StatusCode , 202 )
body , m := rulesNamespaceWithoutVariableValues ( t , b )
generatedUIDs , ok := m [ "default,arulegroup" ]
assert . True ( t , ok )
assert . Equal ( t , 2 , len ( generatedUIDs ) )
// assert that generated UIDs are unique
assert . NotEqual ( t , generatedUIDs [ 0 ] , generatedUIDs [ 1 ] )
// copy result to a variable with a wider scope
// to be used by the next test
ruleUID = generatedUIDs [ 0 ]
expectedGetNamespaceResponseBody = `
{
"default" : [
{
"name" : "arulegroup" ,
"interval" : "1m" ,
"rules" : [
{
"annotations" : {
"annotation1" : "val1"
} ,
"expr" : "" ,
"for" : "1m" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "0s" ,
2025-01-25 03:49:05 +08:00
"labels" : {
"label1" : "val1"
} ,
"grafana_alert" : {
"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" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
2025-02-08 00:24:28 +08:00
"uid" : "uid" ,
2025-01-28 03:31:40 +08:00
"name" : "grafana"
} ,
2025-01-25 03:49:05 +08:00
"intervalSeconds" : 60 ,
"is_paused" : false ,
"version" : 1 ,
2025-04-12 05:38:53 +08:00
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-01-25 03:49:05 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "NoData" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
} ,
{
"expr" : "" ,
"for" : "0s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "0s" ,
2025-01-25 03:49:05 +08:00
"grafana_alert" : {
"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" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
2025-02-08 00:24:28 +08:00
"uid" : "uid" ,
2025-01-28 03:31:40 +08:00
"name" : "grafana"
} ,
2025-01-25 03:49:05 +08:00
"intervalSeconds" : 60 ,
"is_paused" : false ,
"version" : 1 ,
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-01-25 03:49:05 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "Alerting" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
}
]
}
]
} `
assert . JSONEq ( t , expectedGetNamespaceResponseBody , body )
2025-02-10 23:28:34 +08:00
createdRuleUIDs [ "AlwaysFiring" ] = generatedUIDs [ 0 ]
createdRuleUIDs [ "AlwaysFiringButSilenced" ] = generatedUIDs [ 1 ]
2025-01-25 03:49:05 +08:00
}
2025-02-10 23:28:34 +08:00
// validate that a rulegroup with a new rule with a user specified UID can be created while others updated
2025-01-25 03:49:05 +08:00
{
interval , err := model . ParseDuration ( "30s" )
require . NoError ( t , err )
2025-03-18 18:24:48 +08:00
keepFiringFor , err := model . ParseDuration ( "10s" )
require . NoError ( t , err )
2025-01-25 03:49:05 +08:00
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
2025-02-10 23:28:34 +08:00
{
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" ,
UID : createdRuleUIDs [ "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" ,
UID : createdRuleUIDs [ "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 ) ,
} ,
} ,
2025-01-25 03:49:05 +08:00
{
ApiRuleNode : & apimodels . ApiRuleNode {
2025-03-18 18:24:48 +08:00
For : & interval ,
KeepFiringFor : & keepFiringFor ,
2025-01-25 03:49:05 +08:00
Labels : map [ string ] string {
"label1" : "val42" ,
"foo" : "bar" ,
} ,
Annotations : map [ string ] string {
"annotation1" : "val42" ,
"foo" : "bar" ,
} ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
UID : "unknown" ,
Title : "AlwaysNormal" ,
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"
} ` ) ,
} ,
} ,
2025-03-26 20:34:53 +08:00
NoDataState : apimodels . NoDataState ( ngmodels . Alerting ) ,
ExecErrState : apimodels . ExecutionErrorState ( ngmodels . AlertingErrState ) ,
MissingSeriesEvalsToResolve : util . Pointer ( 2 ) , // If UID is specified, this field is required
2025-01-25 03:49:05 +08:00
} ,
} ,
} ,
Interval : interval ,
}
2025-03-26 20:34:53 +08:00
response , status , body := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
assert . Equal ( t , http . StatusAccepted , status , body )
2025-01-25 03:49:05 +08:00
2025-02-10 23:28:34 +08:00
require . Len ( t , response . Created , 1 )
require . Len ( t , response . Updated , 2 )
require . Len ( t , response . Deleted , 0 )
}
// remove the added rule and set the interval back to 1m
{
interval , err := model . ParseDuration ( "1m" )
2025-01-25 03:49:05 +08:00
require . NoError ( t , err )
2025-02-10 23:28:34 +08:00
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] 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" ,
UID : createdRuleUIDs [ "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" ,
UID : createdRuleUIDs [ "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 ) ,
} ,
} ,
} ,
Interval : interval ,
}
2025-01-25 03:49:05 +08:00
2025-03-15 04:14:06 +08:00
response , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-10 23:28:34 +08:00
assert . Equal ( t , http . StatusAccepted , status )
require . Len ( t , response . Created , 0 )
require . Len ( t , response . Updated , 2 )
require . Len ( t , response . Deleted , 1 )
2025-01-25 03:49:05 +08:00
}
// try to update by pass two rules with conflicting UIDs
{
interval , err := model . ParseDuration ( "30s" )
require . NoError ( t , err )
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
{
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string {
"label1" : "val42" ,
"foo" : "bar" ,
} ,
Annotations : map [ string ] string {
"annotation1" : "val42" ,
"foo" : "bar" ,
} ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
UID : ruleUID ,
Title : "AlwaysNormal" ,
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 ) ,
} ,
} ,
{
ApiRuleNode : & apimodels . ApiRuleNode {
For : & interval ,
Labels : map [ string ] string {
"label1" : "val42" ,
"foo" : "bar" ,
} ,
Annotations : map [ string ] string {
"annotation1" : "val42" ,
"foo" : "bar" ,
} ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
UID : ruleUID ,
Title : "AlwaysAlerting" ,
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 ) ,
} ,
} ,
} ,
Interval : interval ,
}
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-01-25 03:49:05 +08:00
assert . Equal ( t , http . StatusBadRequest , status )
var res map [ string ] any
require . NoError ( t , json . Unmarshal ( [ ] byte ( body ) , & res ) )
require . Equal ( t , fmt . Sprintf ( "rule [1] has UID %s that is already assigned to another rule at index 0" , ruleUID ) , res [ "message" ] )
// let's make sure that rule definitions are not affected by the failed POST request.
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , resp . StatusCode , 202 )
body , m := rulesNamespaceWithoutVariableValues ( t , b )
returnedUIDs , ok := m [ "default,arulegroup" ]
assert . True ( t , ok )
assert . Equal ( t , 2 , len ( returnedUIDs ) )
2025-02-10 23:28:34 +08:00
expectedGetNamespaceResponseBody = `
{
"default" : [
{
"name" : "arulegroup" ,
"interval" : "1m" ,
"rules" : [
{
"annotations" : {
"annotation1" : "val1"
} ,
"expr" : "" ,
"for" : "1m" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "0s" ,
2025-02-10 23:28:34 +08:00
"labels" : {
"label1" : "val1"
} ,
"grafana_alert" : {
"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" ,
"updated_by" : {
"uid" : "uid" ,
"name" : "grafana"
} ,
"intervalSeconds" : 60 ,
"is_paused" : false ,
"version" : 3 ,
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-02-10 23:28:34 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "NoData" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
} ,
{
"expr" : "" ,
"for" : "0s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "0s" ,
2025-02-10 23:28:34 +08:00
"grafana_alert" : {
"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" ,
"updated_by" : {
"uid" : "uid" ,
"name" : "grafana"
} ,
"intervalSeconds" : 60 ,
"is_paused" : false ,
"version" : 3 ,
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-02-10 23:28:34 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "Alerting" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
}
]
}
]
} `
2025-01-25 03:49:05 +08:00
assert . JSONEq ( t , expectedGetNamespaceResponseBody , body )
}
// update the first rule and completely remove the other
{
forValue , err := model . ParseDuration ( "30s" )
require . NoError ( t , err )
2025-03-18 18:24:48 +08:00
keepFiringForValue , err := model . ParseDuration ( "5s" )
require . NoError ( t , err )
2025-01-25 03:49:05 +08:00
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
{
ApiRuleNode : & apimodels . ApiRuleNode {
2025-03-18 18:24:48 +08:00
For : & forValue ,
KeepFiringFor : & keepFiringForValue ,
2025-01-25 03:49:05 +08:00
Labels : map [ string ] string {
// delete foo label
"label1" : "val1" , // update label value
"label2" : "val2" , // new label
} ,
Annotations : map [ string ] string {
// delete foo annotation
"annotation1" : "val1" , // update annotation value
"annotation2" : "val2" , // new annotation
} ,
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
UID : ruleUID , // Including the UID in the payload makes the endpoint update the existing rule.
Title : "AlwaysNormal" ,
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 ) ,
} ,
} ,
} ,
Interval : interval ,
}
2025-03-15 04:14:06 +08:00
respModel , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
require . Equal ( t , respModel . Updated , [ ] string { ruleUID } )
require . Len ( t , respModel . Deleted , 1 )
// let's make sure that rule definitions are updated correctly.
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , resp . StatusCode , 202 )
body , m := rulesNamespaceWithoutVariableValues ( t , b )
returnedUIDs , ok := m [ "default,arulegroup" ]
assert . True ( t , ok )
assert . Equal ( t , 1 , len ( returnedUIDs ) )
assert . Equal ( t , ruleUID , returnedUIDs [ 0 ] )
assert . JSONEq ( t , `
{
"default" : [
{
"name" : "arulegroup" ,
"interval" : "1m" ,
"rules" : [
{
"annotations" : {
"annotation1" : "val1" ,
"annotation2" : "val2"
} ,
"expr" : "" ,
"for" : "30s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "5s" ,
2025-01-25 03:49:05 +08:00
"labels" : {
"label1" : "val1" ,
"label2" : "val2"
} ,
"grafana_alert" : {
"title" : "AlwaysNormal" ,
"condition" : "A" ,
"data" : [
{
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"expression" : "2 + 3 \u003C 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
}
] ,
"updated" : "2021-02-21T01:10:30Z" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
2025-02-08 00:24:28 +08:00
"uid" : "uid" ,
2025-01-28 03:31:40 +08:00
"name" : "grafana"
} ,
2025-01-25 03:49:05 +08:00
"intervalSeconds" : 60 ,
"is_paused" : false ,
2025-02-10 23:28:34 +08:00
"version" : 4 ,
2025-01-25 03:49:05 +08:00
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-01-25 03:49:05 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "Alerting" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
}
]
}
]
} ` , body )
}
// update the rule; delete labels and annotations
{
forValue , err := model . ParseDuration ( "30s" )
require . NoError ( t , err )
2025-03-18 18:24:48 +08:00
keepFiringForValue , err := model . ParseDuration ( "15s" )
require . NoError ( t , err )
2025-01-25 03:49:05 +08:00
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
{
ApiRuleNode : & apimodels . ApiRuleNode {
2025-03-18 18:24:48 +08:00
For : & forValue ,
KeepFiringFor : & keepFiringForValue ,
2025-01-25 03:49:05 +08:00
} ,
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
UID : ruleUID , // Including the UID in the payload makes the endpoint update the existing rule.
Title : "AlwaysNormal" ,
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 ) ,
} ,
} ,
} ,
Interval : interval ,
}
2025-03-15 04:14:06 +08:00
respModel , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
require . Equal ( t , respModel . Updated , [ ] string { ruleUID } )
// let's make sure that rule definitions are updated correctly.
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , resp . StatusCode , 202 )
body , m := rulesNamespaceWithoutVariableValues ( t , b )
returnedUIDs , ok := m [ "default,arulegroup" ]
assert . True ( t , ok )
assert . Equal ( t , 1 , len ( returnedUIDs ) )
assert . Equal ( t , ruleUID , returnedUIDs [ 0 ] )
assert . JSONEq ( t , `
{
"default" : [
{
"name" : "arulegroup" ,
"interval" : "1m" ,
"rules" : [
{
"expr" : "" ,
"for" : "30s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "15s" ,
2025-01-25 03:49:05 +08:00
"grafana_alert" : {
"title" : "AlwaysNormal" ,
"condition" : "A" ,
"data" : [
{
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"expression" : "2 + 3 \u003C 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
}
] ,
"updated" : "2021-02-21T01:10:30Z" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
2025-02-08 00:24:28 +08:00
"uid" : "uid" ,
2025-01-28 03:31:40 +08:00
"name" : "grafana"
} ,
2025-01-25 03:49:05 +08:00
"intervalSeconds" : 60 ,
"is_paused" : false ,
2025-02-10 23:28:34 +08:00
"version" : 5 ,
2025-01-25 03:49:05 +08:00
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-01-25 03:49:05 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "Alerting" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
}
]
}
]
} ` , body )
}
// update the rule; keep title, condition, no data state, error state, queries and expressions if not provided. should be noop
{
rules := apimodels . PostableRuleGroupConfig {
Name : "arulegroup" ,
Rules : [ ] apimodels . PostableExtendedRuleNode {
{
GrafanaManagedAlert : & apimodels . PostableGrafanaRule {
UID : ruleUID , // Including the UID in the payload makes the endpoint update the existing rule.
} ,
} ,
} ,
Interval : interval ,
}
2025-03-15 04:14:06 +08:00
respModel , status , _ := apiClient . PostRulesGroupWithStatus ( t , "default" , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
require . Equal ( t , "no changes detected in the rule group" , respModel . Message )
assert . Empty ( t , respModel . Created )
assert . Empty ( t , respModel . Updated )
assert . Empty ( t , respModel . Deleted )
// let's make sure that rule definitions are updated correctly.
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default" , grafanaListedAddr )
// nolint:gosec
resp , err := http . Get ( u )
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 )
assert . Equal ( t , resp . StatusCode , 202 )
body , m := rulesNamespaceWithoutVariableValues ( t , b )
returnedUIDs , ok := m [ "default,arulegroup" ]
assert . True ( t , ok )
assert . Equal ( t , 1 , len ( returnedUIDs ) )
assert . Equal ( t , ruleUID , returnedUIDs [ 0 ] )
assert . JSONEq ( t , `
{
"default" : [
{
"name" : "arulegroup" ,
"interval" : "1m" ,
"rules" : [
{
"expr" : "" ,
"for" : "30s" ,
2025-03-18 18:24:48 +08:00
"keep_firing_for" : "15s" ,
2025-01-25 03:49:05 +08:00
"grafana_alert" : {
"title" : "AlwaysNormal" ,
"condition" : "A" ,
"data" : [
{
"refId" : "A" ,
"queryType" : "" ,
"relativeTimeRange" : {
"from" : 18000 ,
"to" : 10800
} ,
"datasourceUid" : "__expr__" ,
"model" : {
"expression" : "2 + 3 \u003C 1" ,
"intervalMs" : 1000 ,
"maxDataPoints" : 43200 ,
"type" : "math"
}
}
] ,
"updated" : "2021-02-21T01:10:30Z" ,
2025-01-28 03:31:40 +08:00
"updated_by" : {
2025-02-08 00:24:28 +08:00
"uid" : "uid" ,
2025-01-28 03:31:40 +08:00
"name" : "grafana"
} ,
2025-01-25 03:49:05 +08:00
"intervalSeconds" : 60 ,
"is_paused" : false ,
2025-02-10 23:28:34 +08:00
"version" : 5 ,
2025-01-25 03:49:05 +08:00
"uid" : "uid" ,
2025-03-15 04:14:06 +08:00
"guid" : "guid" ,
2025-01-25 03:49:05 +08:00
"namespace_uid" : "nsuid" ,
"rule_group" : "arulegroup" ,
"no_data_state" : "Alerting" ,
"exec_err_state" : "Alerting" ,
"metadata" : {
"editor_settings" : {
"simplified_query_and_expressions_section" : false ,
"simplified_notifications_section" : false
}
}
}
}
]
}
]
} ` , body )
}
client := & http . Client { }
// Finally, make sure we can delete it.
{
t . Run ( "succeed if the rule group name does not exists" , func ( t * testing . T ) {
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default/groupnotexist" , grafanaListedAddr )
req , err := http . NewRequest ( http . MethodDelete , u , nil )
require . NoError ( t , err )
resp , err := client . Do ( req )
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 , http . StatusAccepted , resp . StatusCode )
var res map [ string ] any
require . NoError ( t , json . Unmarshal ( b , & res ) )
require . Equal ( t , "rules deleted" , res [ "message" ] )
} )
t . Run ( "succeed if the rule group name does exist" , func ( t * testing . T ) {
u := fmt . Sprintf ( "http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default/arulegroup" , grafanaListedAddr )
req , err := http . NewRequest ( http . MethodDelete , u , nil )
require . NoError ( t , err )
resp , err := client . Do ( req )
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 , http . StatusAccepted , resp . StatusCode )
require . JSONEq ( t , ` { "message":"rules deleted"} ` , string ( b ) )
} )
}
}
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 , env := testinfra . StartGrafanaEnv ( t , dir , path )
// Create a user to make authenticated requests
createUser ( t , env . SQLStore , env . Cfg , 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
2025-03-15 04:14:06 +08:00
resp , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2025-01-25 03:49:05 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
require . Len ( t , resp . Created , 1 )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
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
2025-03-15 04:14:06 +08:00
resp , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2025-01-25 03:49:05 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
require . Len ( t , resp . Created , 1 )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to get rule group. Response: %s" , body )
2023-02-01 20:15:03 +08:00
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
2025-03-15 04:14:06 +08:00
resp , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-02-01 20:15:03 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2023-10-07 06:11:24 +08:00
require . Len ( t , resp . Created , 1 )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
2023-02-01 20:15:03 +08:00
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
2025-03-15 04:14:06 +08:00
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-02-01 20:15:03 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
2023-02-01 20:15:03 +08:00
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
2025-03-15 04:14:06 +08:00
_ , status , body = client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2023-02-01 20:15:03 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2023-02-01 20:15:03 +08:00
require . Equal ( t , tc . expectedIsPausedInDb , getGroup . Rules [ 0 ] . GrafanaManagedAlert . IsPaused )
} )
}
}
2024-01-05 00:47:13 +08:00
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 ,
2025-04-24 23:58:17 +08:00
EnableFeatureToggles : [ ] string { featuremgmt . FlagConfigurableSchedulerTick } ,
2024-01-05 00:47:13 +08:00
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
2024-01-05 00:47:13 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2024-01-05 00:47:13 +08:00
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 )
}
}
2025-03-15 04:14:06 +08:00
changes , status , body := apiClient . PostRulesGroupWithStatus ( t , folder , & postData , false )
2024-01-05 00:47:13 +08:00
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 )
}
2024-02-15 22:45:10 +08:00
func TestIntegrationRuleNotificationSettings ( 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 . FlagAlertingSimplifiedRouting } ,
} )
2024-04-04 21:04:47 +08:00
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
2024-02-15 22:45:10 +08:00
// Create a user to make authenticated requests
2024-04-04 21:04:47 +08:00
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
2024-02-15 22:45:10 +08:00
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "password" ,
Login : "grafana" ,
} )
apiClient := newAlertingApiClient ( grafanaListedAddr , "grafana" , "password" )
folder := "Test-Alerting"
apiClient . CreateFolder ( t , folder , folder )
testDataRaw , err := testData . ReadFile ( path . Join ( "test-data" , "rule-notification-settings-1-post.json" ) )
require . NoError ( t , err )
type testData struct {
RuleGroup apimodels . PostableRuleGroupConfig
Receiver apimodels . EmbeddedContactPoint
TimeInterval apimodels . MuteTimeInterval
}
var d testData
err = json . Unmarshal ( testDataRaw , & d )
require . NoError ( t , err )
apiClient . EnsureReceiver ( t , d . Receiver )
apiClient . EnsureMuteTiming ( t , d . TimeInterval )
t . Run ( "create should fail if receiver does not exist" , func ( t * testing . T ) {
var copyD testData
err = json . Unmarshal ( testDataRaw , & copyD )
group := copyD . RuleGroup
ns := group . Rules [ 0 ] . GrafanaManagedAlert . NotificationSettings
ns . Receiver = "random-receiver"
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , folder , & group , false )
2024-02-15 22:45:10 +08:00
require . Equalf ( t , http . StatusBadRequest , status , body )
t . Log ( body )
} )
t . Run ( "create should fail if mute timing does not exist" , func ( t * testing . T ) {
var copyD testData
err = json . Unmarshal ( testDataRaw , & copyD )
group := copyD . RuleGroup
ns := group . Rules [ 0 ] . GrafanaManagedAlert . NotificationSettings
ns . MuteTimeIntervals = [ ] string { "random-time-interval" }
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , folder , & group , false )
2024-02-15 22:45:10 +08:00
require . Equalf ( t , http . StatusBadRequest , status , body )
t . Log ( body )
} )
2024-04-17 00:14:39 +08:00
t . Run ( "create should not fail if group_by is missing required labels but they should still be used" , func ( t * testing . T ) {
2024-02-15 22:45:10 +08:00
var copyD testData
err = json . Unmarshal ( testDataRaw , & copyD )
group := copyD . RuleGroup
ns := group . Rules [ 0 ] . GrafanaManagedAlert . NotificationSettings
ns . GroupBy = [ ] string { "label1" }
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , folder , & group , false )
2024-04-17 00:14:39 +08:00
require . Equalf ( t , http . StatusAccepted , status , body )
cfg , status , body := apiClient . GetAlertmanagerConfigWithStatus ( t )
if ! assert . Equalf ( t , http . StatusOK , status , body ) {
return
}
// Ensure that the group by contains the default required labels.
autogenRoute := cfg . AlertmanagerConfig . Route . Routes [ 0 ]
receiverRoute := autogenRoute . Routes [ 0 ]
ruleRoute := receiverRoute . Routes [ 0 ]
assert . Equal ( t , [ ] model . LabelName { ngmodels . FolderTitleLabel , model . AlertNameLabel , "label1" } , ruleRoute . GroupBy )
2024-02-15 22:45:10 +08:00
t . Log ( body )
} )
t . Run ( "should create rule and generate route" , func ( t * testing . T ) {
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , folder , & d . RuleGroup , false )
2024-02-15 22:45:10 +08:00
require . Equalf ( t , http . StatusAccepted , status , body )
notificationSettings := d . RuleGroup . Rules [ 0 ] . GrafanaManagedAlert . NotificationSettings
var routeBody string
if ! assert . EventuallyWithT ( t , func ( c * assert . CollectT ) {
amConfig , status , body := apiClient . GetAlertmanagerConfigWithStatus ( t )
routeBody = body
if ! assert . Equalf ( t , http . StatusOK , status , body ) {
return
}
route := amConfig . AlertmanagerConfig . Route
if ! assert . Len ( c , route . Routes , 1 ) {
return
}
// Check that we are in the auto-generated root
autogenRoute := route . Routes [ 0 ]
if ! assert . Len ( c , autogenRoute . ObjectMatchers , 1 ) {
return
}
canContinue := assert . Equal ( c , ngmodels . AutogeneratedRouteLabel , autogenRoute . ObjectMatchers [ 0 ] . Name )
assert . Equal ( c , labels . MatchEqual , autogenRoute . ObjectMatchers [ 0 ] . Type )
assert . Equal ( c , "true" , autogenRoute . ObjectMatchers [ 0 ] . Value )
assert . Equalf ( c , route . Receiver , autogenRoute . Receiver , "Autogenerated root receiver must be the default one" )
assert . Nil ( c , autogenRoute . GroupWait )
assert . Nil ( c , autogenRoute . GroupInterval )
assert . Nil ( c , autogenRoute . RepeatInterval )
assert . Empty ( c , autogenRoute . MuteTimeIntervals )
assert . Empty ( c , autogenRoute . GroupBy )
if ! canContinue {
return
}
// Now check that the second level is route for receivers
if ! assert . NotEmpty ( c , autogenRoute . Routes ) {
return
}
// There can be many routes, for all receivers
idx := slices . IndexFunc ( autogenRoute . Routes , func ( route * apimodels . Route ) bool {
return route . Receiver == notificationSettings . Receiver
} )
if ! assert . GreaterOrEqual ( t , idx , 0 ) {
return
}
receiverRoute := autogenRoute . Routes [ idx ]
if ! assert . Len ( c , receiverRoute . ObjectMatchers , 1 ) {
return
}
canContinue = assert . Equal ( c , ngmodels . AutogeneratedRouteReceiverNameLabel , receiverRoute . ObjectMatchers [ 0 ] . Name )
assert . Equal ( c , labels . MatchEqual , receiverRoute . ObjectMatchers [ 0 ] . Type )
assert . Equal ( c , notificationSettings . Receiver , receiverRoute . ObjectMatchers [ 0 ] . Value )
assert . Equal ( c , notificationSettings . Receiver , receiverRoute . Receiver )
assert . Nil ( c , receiverRoute . GroupWait )
assert . Nil ( c , receiverRoute . GroupInterval )
assert . Nil ( c , receiverRoute . RepeatInterval )
assert . Empty ( c , receiverRoute . MuteTimeIntervals )
var groupBy [ ] string
for _ , name := range receiverRoute . GroupBy {
groupBy = append ( groupBy , string ( name ) )
}
slices . Sort ( groupBy )
assert . EqualValues ( c , [ ] string { "alertname" , "grafana_folder" } , groupBy )
if ! canContinue {
return
}
// Now check that we created the 3rd level for specific combination of settings
if ! assert . Lenf ( c , receiverRoute . Routes , 1 , "Receiver route should contain one options route" ) {
return
}
optionsRoute := receiverRoute . Routes [ 0 ]
if ! assert . Len ( c , optionsRoute . ObjectMatchers , 1 ) {
return
}
assert . Equal ( c , ngmodels . AutogeneratedRouteSettingsHashLabel , optionsRoute . ObjectMatchers [ 0 ] . Name )
assert . Equal ( c , labels . MatchEqual , optionsRoute . ObjectMatchers [ 0 ] . Type )
assert . EqualValues ( c , notificationSettings . GroupWait , optionsRoute . GroupWait )
assert . EqualValues ( c , notificationSettings . GroupInterval , optionsRoute . GroupInterval )
assert . EqualValues ( c , notificationSettings . RepeatInterval , optionsRoute . RepeatInterval )
assert . EqualValues ( c , notificationSettings . MuteTimeIntervals , optionsRoute . MuteTimeIntervals )
groupBy = nil
for _ , name := range optionsRoute . GroupBy {
groupBy = append ( groupBy , string ( name ) )
}
assert . EqualValues ( c , notificationSettings . GroupBy , groupBy )
} , 10 * time . Second , 1 * time . Second ) {
t . Logf ( "config: %s" , routeBody )
}
} )
t . Run ( "should correctly create alerts" , func ( t * testing . T ) {
var response string
if ! assert . EventuallyWithT ( t , func ( c * assert . CollectT ) {
groups , status , body := apiClient . GetActiveAlertsWithStatus ( t )
require . Equalf ( t , http . StatusOK , status , body )
response = body
if len ( groups ) == 0 {
return
}
g := groups [ 0 ]
alert := g . Alerts [ 0 ]
assert . Contains ( c , alert . Labels , ngmodels . AutogeneratedRouteLabel )
assert . Equal ( c , "true" , alert . Labels [ ngmodels . AutogeneratedRouteLabel ] )
assert . Contains ( c , alert . Labels , ngmodels . AutogeneratedRouteReceiverNameLabel )
assert . Equal ( c , d . Receiver . Name , alert . Labels [ ngmodels . AutogeneratedRouteReceiverNameLabel ] )
assert . Contains ( c , alert . Labels , ngmodels . AutogeneratedRouteSettingsHashLabel )
assert . NotEmpty ( c , alert . Labels [ ngmodels . AutogeneratedRouteSettingsHashLabel ] )
} , 10 * time . Second , 1 * time . Second ) {
t . Logf ( "response: %s" , response )
}
} )
t . Run ( "should update rule with empty settings and delete route" , func ( t * testing . T ) {
var copyD testData
err = json . Unmarshal ( testDataRaw , & copyD )
group := copyD . RuleGroup
notificationSettings := group . Rules [ 0 ] . GrafanaManagedAlert . NotificationSettings
group . Rules [ 0 ] . GrafanaManagedAlert . NotificationSettings = nil
2025-03-15 04:14:06 +08:00
_ , status , body := apiClient . PostRulesGroupWithStatus ( t , folder , & group , false )
2024-02-15 22:45:10 +08:00
require . Equalf ( t , http . StatusAccepted , status , body )
var routeBody string
if ! assert . EventuallyWithT ( t , func ( c * assert . CollectT ) {
amConfig , status , body := apiClient . GetAlertmanagerConfigWithStatus ( t )
routeBody = body
if ! assert . Equalf ( t , http . StatusOK , status , body ) {
return
}
route := amConfig . AlertmanagerConfig . Route
if ! assert . Len ( c , route . Routes , 1 ) {
return
}
// Check that we are in the auto-generated root
autogenRoute := route . Routes [ 0 ]
if ! assert . Len ( c , autogenRoute . ObjectMatchers , 1 ) {
return
}
if ! assert . Equal ( c , ngmodels . AutogeneratedRouteLabel , autogenRoute . ObjectMatchers [ 0 ] . Name ) {
return
}
// Now check that the second level is route for receivers
if ! assert . NotEmpty ( c , autogenRoute . Routes ) {
return
}
// There can be many routes, for all receivers
idx := slices . IndexFunc ( autogenRoute . Routes , func ( route * apimodels . Route ) bool {
return route . Receiver == notificationSettings . Receiver
} )
if ! assert . GreaterOrEqual ( t , idx , 0 ) {
return
}
receiverRoute := autogenRoute . Routes [ idx ]
if ! assert . Empty ( c , receiverRoute . Routes ) {
return
}
} , 10 * time . Second , 1 * time . Second ) {
t . Logf ( "config: %s" , routeBody )
}
} )
}
2024-06-11 07:05:47 +08:00
func TestIntegrationRuleUpdateAllDatabases ( t * testing . T ) {
// Setup Grafana and its Database
dir , path := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
DisableAnonymous : true ,
AppModeProduction : true ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , path )
// Create a user to make authenticated requests
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "admin" ,
Login : "admin" ,
} )
client := newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" )
folderUID := util . GenerateShortUID ( )
client . CreateFolder ( t , folderUID , "folder1" )
t . Run ( "group renamed followed by delete for case-only changes should not delete both groups" , func ( t * testing . T ) { // Regression test.
group := generateAlertRuleGroup ( 3 , alertRuleGen ( ) )
groupName := group . Name
2025-03-15 04:14:06 +08:00
_ , status , body := client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2024-06-11 07:05:47 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status := client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2024-06-11 07:05:47 +08:00
require . Lenf ( t , getGroup . Rules , 3 , "expected 3 rules in group" )
require . Equal ( t , groupName , getGroup . Rules [ 0 ] . GrafanaManagedAlert . RuleGroup )
group = convertGettableRuleGroupToPostable ( getGroup . GettableRuleGroupConfig )
newGroup := strings . ToUpper ( group . Name )
group . Name = newGroup
2025-03-15 04:14:06 +08:00
_ , status , body = client . PostRulesGroupWithStatus ( t , folderUID , & group , false )
2024-06-11 07:05:47 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post rule group. Response: %s" , body )
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , group . Name )
require . Equal ( t , http . StatusAccepted , status )
2024-06-11 07:05:47 +08:00
require . Lenf ( t , getGroup . Rules , 3 , "expected 3 rules in group" )
require . Equal ( t , newGroup , getGroup . Rules [ 0 ] . GrafanaManagedAlert . RuleGroup )
2025-03-15 04:14:06 +08:00
status , body = client . DeleteRulesGroup ( t , folderUID , groupName , false )
2024-06-11 07:05:47 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to post noop rule group. Response: %s" , body )
// Old group is gone.
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , groupName )
require . Equal ( t , http . StatusNotFound , status )
2024-06-11 07:05:47 +08:00
// New group still exists.
2025-02-08 00:24:28 +08:00
getGroup , status = client . GetRulesGroup ( t , folderUID , newGroup )
require . Equal ( t , http . StatusAccepted , status )
2024-06-11 07:05:47 +08:00
require . Lenf ( t , getGroup . Rules , 3 , "expected 3 rules in group" )
require . Equal ( t , newGroup , getGroup . Rules [ 0 ] . GrafanaManagedAlert . RuleGroup )
} )
}
2025-01-25 03:49:05 +08:00
2025-02-04 02:26:18 +08:00
func TestIntegrationRuleVersions ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
// Setup Grafana and its Database
dir , p := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
EnableQuota : true ,
DisableAnonymous : true ,
AppModeProduction : true ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
createUser ( t , env . SQLStore , env . Cfg , 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" )
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
2025-03-15 04:14:06 +08:00
response := apiClient . PostRulesGroup ( t , "folder1" , & group1 , false )
2025-02-04 02:26:18 +08:00
require . NotEmptyf ( t , response . Created , "Expected created to be set" )
uid := response . Created [ 0 ]
ruleV1 := apiClient . GetRuleByUID ( t , uid )
t . Run ( "should return 1 version right after creation" , func ( t * testing . T ) {
versions , status , raw := apiClient . GetRuleVersionsWithStatus ( t , uid )
require . Equalf ( t , http . StatusOK , status , "Expected status 200, got %d: %s" , status , raw )
require . Lenf ( t , versions , 1 , "Expected 1 version, got %d" , len ( versions ) )
assert . Equal ( t , ruleV1 , versions [ 0 ] )
} )
2025-02-08 00:24:28 +08:00
group1Gettable , status := apiClient . GetRulesGroup ( t , "folder1" , group1 . Name )
require . Equal ( t , http . StatusAccepted , status )
2025-02-04 02:26:18 +08:00
group1 = convertGettableRuleGroupToPostable ( group1Gettable . GettableRuleGroupConfig )
group1 . Rules [ 0 ] . Annotations [ util . GenerateShortUID ( ) ] = util . GenerateShortUID ( )
2025-03-15 04:14:06 +08:00
_ = apiClient . PostRulesGroup ( t , "folder1" , & group1 , false )
2025-02-04 02:26:18 +08:00
ruleV2 := apiClient . GetRuleByUID ( t , uid )
t . Run ( "should return previous versions after update" , func ( t * testing . T ) {
versions , status , raw := apiClient . GetRuleVersionsWithStatus ( t , uid )
require . Equalf ( t , http . StatusOK , status , "Expected status 200, got %d: %s" , status , raw )
require . Lenf ( t , versions , 2 , "Expected 2 versions, got %d" , len ( versions ) )
pathsToIgnore := [ ] string {
"GrafanaManagedAlert.ID" , // In versions ID has different value
}
// compare expected and actual and ignore the dynamic fields
diff := cmp . Diff ( apimodels . GettableRuleVersions { ruleV2 , ruleV1 } , versions , cmp . FilterPath ( func ( path cmp . Path ) bool {
for _ , s := range pathsToIgnore {
if strings . Contains ( path . String ( ) , s ) {
return true
}
}
return false
} , cmp . Ignore ( ) ) )
assert . Empty ( t , diff )
} )
2025-03-15 04:14:06 +08:00
_ = apiClient . PostRulesGroup ( t , "folder1" , & group1 , false ) // Noop update
2025-02-04 02:26:18 +08:00
t . Run ( "should not add new version if rule was not changed" , func ( t * testing . T ) {
versions , status , raw := apiClient . GetRuleVersionsWithStatus ( t , uid )
require . Equalf ( t , http . StatusOK , status , "Expected status 200, got %d: %s" , status , raw )
require . Lenf ( t , versions , 2 , "Expected 2 versions, got %d" , len ( versions ) )
} )
2025-03-15 04:14:06 +08:00
apiClient . DeleteRulesGroup ( t , "folder1" , group1 . Name , false )
2025-02-04 02:26:18 +08:00
t . Run ( "should NotFound after rule was deleted" , func ( t * testing . T ) {
_ , status , raw := apiClient . GetRuleVersionsWithStatus ( t , uid )
require . Equalf ( t , http . StatusNotFound , status , "Expected status 404, got %d: %s" , status , raw )
} )
}
2025-03-12 00:40:44 +08:00
func TestIntegrationRuleSoftDelete ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
// Setup Grafana and its Database
dir , p := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
EnableQuota : true ,
DisableAnonymous : true ,
AppModeProduction : true ,
EnableFeatureToggles : [ ] string { featuremgmt . FlagAlertRuleRestore } ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "admin" ,
Login : "admin" ,
} )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleEditor ) ,
Password : "password" ,
Login : "editor" ,
} )
adminClient := newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" )
editorClient := newAlertingApiClient ( grafanaListedAddr , "editor" , "password" )
deleted , status , data := adminClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusOK , status , data )
require . Emptyf ( t , deleted , "Expected empty list of deleted rules, got %v" , deleted )
// Create the namespace we'll save our alerts to.
adminClient . CreateFolder ( t , "folder1" , "folder1" )
var group apimodels . RuleGroupConfigResponse
{ // create rules and some history
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
2025-03-15 04:14:06 +08:00
response := adminClient . PostRulesGroup ( t , "folder1" , & group1 , false )
2025-03-12 00:40:44 +08:00
require . NotEmptyf ( t , response . Created , "Expected created to be set" )
// create some versions of the rule
for i := 0 ; i < 3 ; i ++ {
groups , status := adminClient . GetRulesGroup ( t , "folder1" , group1 . Name )
require . Equal ( t , http . StatusAccepted , status )
group1 = convertGettableRuleGroupToPostable ( groups . GettableRuleGroupConfig )
group1 . Rules [ 0 ] . Annotations [ util . GenerateShortUID ( ) ] = util . GenerateShortUID ( )
2025-03-15 04:14:06 +08:00
_ = adminClient . PostRulesGroup ( t , "folder1" , & group1 , false )
2025-03-12 00:40:44 +08:00
}
group , status = adminClient . GetRulesGroup ( t , "folder1" , group1 . Name )
require . Equal ( t , http . StatusAccepted , status )
}
// deleting group by using editor user
2025-03-15 04:14:06 +08:00
status , body := editorClient . DeleteRulesGroup ( t , "folder1" , group . Name , false )
2025-03-12 00:40:44 +08:00
require . Equalf ( t , http . StatusAccepted , status , "failed to delete group. Response: %s" , body )
t . Run ( "should see deleted rules" , func ( t * testing . T ) {
rules , status , raw := adminClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusOK , status , raw )
require . Containsf ( t , rules , "" , "All rules should be in empty folder but got %v" , slices . Collect ( maps . Keys ( rules ) ) )
require . Lenf ( t , rules [ "" ] , 1 , "All deleted rules should be in single group but got %d" , len ( rules [ "" ] ) )
require . Equalf ( t , "" , rules [ "" ] [ 0 ] . Name , "All deleted rules should be in empty group but got %v" , rules [ "" ] [ 0 ] . Name )
require . Len ( t , rules [ "" ] [ 0 ] . Rules , len ( group . Rules ) )
require . Empty ( t , cmp . Diff ( group . Rules , rules [ "" ] [ 0 ] . Rules , cmpopts . IgnoreFields ( apimodels . GettableGrafanaRule { } , "UID" , "Version" , "Updated" , "UpdatedBy" ) ) )
rule := rules [ "" ] [ 0 ] . Rules [ 0 ]
require . Equalf ( t , "editor" , rule . GrafanaManagedAlert . UpdatedBy . Name , "Field 'UpdatedBy' should be set by editor but got %v " , rule . GrafanaManagedAlert . UpdatedBy )
} )
t . Run ( "only admin should be able to see deleted rules" , func ( t * testing . T ) {
t . Run ( "editor" , func ( t * testing . T ) {
_ , status , raw := editorClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusForbidden , status , raw )
} )
t . Run ( "viewer" , func ( t * testing . T ) {
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleViewer ) ,
Password : "password" ,
Login : "viewer" ,
} )
client := newAlertingApiClient ( grafanaListedAddr , "viewer" , "password" )
_ , status , raw := client . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusForbidden , status , raw )
} )
} )
2025-03-15 04:14:06 +08:00
t . Run ( "permanently delete rule from deleted rules" , func ( t * testing . T ) {
rules , status , raw := adminClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusOK , status , raw )
require . NotEmpty ( t , rules [ "" ] [ 0 ] . Rules )
ruleGUID := rules [ "" ] [ 0 ] . Rules [ 0 ] . GrafanaManagedAlert . GUID
t . Run ( "non-admins should not be able to do it" , func ( t * testing . T ) {
status , raw := editorClient . DeleteRuleFromTrashByGUID ( t , ruleGUID )
requireStatusCode ( t , http . StatusForbidden , status , raw )
} )
status , raw = adminClient . DeleteRuleFromTrashByGUID ( t , ruleGUID )
requireStatusCode ( t , http . StatusOK , status , raw )
rules , status , raw = adminClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusOK , status , raw )
idx := slices . IndexFunc ( rules [ "" ] [ 0 ] . Rules , func ( node apimodels . GettableExtendedRuleNode ) bool {
return node . GrafanaManagedAlert . GUID == ruleGUID
} )
require . Equalf ( t , - 1 , idx , "rule is expected to be deleted but it was returned by list operation" )
} )
}
func TestIntegrationRulePermanentlyDelete ( t * testing . T ) {
testinfra . SQLiteIntegrationTest ( t )
// Setup Grafana and its Database
dir , p := testinfra . CreateGrafDir ( t , testinfra . GrafanaOpts {
DisableLegacyAlerting : true ,
EnableUnifiedAlerting : true ,
EnableQuota : true ,
DisableAnonymous : true ,
AppModeProduction : true ,
EnableFeatureToggles : [ ] string { featuremgmt . FlagAlertRuleRestore } ,
} )
grafanaListedAddr , env := testinfra . StartGrafanaEnv ( t , dir , p )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleAdmin ) ,
Password : "admin" ,
Login : "admin" ,
} )
createUser ( t , env . SQLStore , env . Cfg , user . CreateUserCommand {
DefaultOrgRole : string ( org . RoleEditor ) ,
Password : "password" ,
Login : "editor" ,
} )
adminClient := newAlertingApiClient ( grafanaListedAddr , "admin" , "admin" )
editorClient := newAlertingApiClient ( grafanaListedAddr , "editor" , "password" )
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 ) )
require . Greaterf ( t , len ( group1 . Rules ) , 1 , "group should contain at least 2 rules" )
// Create the namespace we'll save our alerts to.
adminClient . CreateFolder ( t , "folder1" , "folder1" )
// Create rule under folder1
response := adminClient . PostRulesGroup ( t , "folder1" , & group1 , false )
require . NotEmptyf ( t , response . Created , "Expected created to be set" )
deleted , status , raw := adminClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusOK , status , raw )
require . Emptyf ( t , deleted , "Expected empty list of deleted rules, got %v" , deleted )
t . Run ( "delete rule in group permanently" , func ( t * testing . T ) {
group1Before , _ := adminClient . GetRulesGroup ( t , "folder1" , group1 . Name )
group1 = convertGettableRuleGroupToPostable ( group1Before . GettableRuleGroupConfig )
group1 . Rules = group1 . Rules [ : 1 ] // remove one rule
t . Run ( "denied to non-admin" , func ( t * testing . T ) {
_ , status , raw := editorClient . PostRulesGroupWithStatus ( t , "folder1" , & group1 , true )
require . Equalf ( t , http . StatusForbidden , status , "got unexpected response: %s" , raw )
g , _ := editorClient . GetRulesGroup ( t , "folder1" , group1 . Name )
require . Len ( t , g . Rules , len ( group1Before . Rules ) )
} )
t . Run ( "allowed to admin" , func ( t * testing . T ) {
_ , status , raw := adminClient . PostRulesGroupWithStatus ( t , "folder1" , & group1 , true )
require . Equalf ( t , http . StatusAccepted , status , "got unexpected response: %s" , raw )
g , _ := adminClient . GetRulesGroup ( t , "folder1" , group1 . Name )
require . Len ( t , g . Rules , len ( group1 . Rules ) )
deleted , status , raw := adminClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusOK , status , raw )
require . Emptyf ( t , deleted , "Expected empty list of deleted rules, got %v" , deleted )
} )
} )
t . Run ( "delete group permanently" , func ( t * testing . T ) {
group1 , status , raw := adminClient . GetRulesGroupWithStatus ( t , "folder1" , group1 . Name )
require . Equalf ( t , http . StatusAccepted , status , "got unexpected response: %s" , raw )
t . Run ( "denied to non-admin" , func ( t * testing . T ) {
status , raw := editorClient . DeleteRulesGroup ( t , "folder1" , group1 . Name , true )
require . Equalf ( t , http . StatusForbidden , status , "got unexpected response: %s" , raw )
g , _ := editorClient . GetRulesGroup ( t , "folder1" , group1 . Name )
require . Len ( t , g . Rules , len ( group1 . Rules ) )
} )
t . Run ( "allowed to admin" , func ( t * testing . T ) {
status , raw := adminClient . DeleteRulesGroup ( t , "folder1" , group1 . Name , true )
require . Equalf ( t , http . StatusAccepted , status , "got unexpected response: %s" , raw )
_ , status , rawb := adminClient . GetRulesGroupWithStatus ( t , "folder1" , group1 . Name )
require . Equalf ( t , http . StatusNotFound , status , "got unexpected response: %s" , string ( rawb ) )
deleted , status , raw := adminClient . GetDeletedRulesWithStatus ( t )
requireStatusCode ( t , http . StatusOK , status , raw )
require . Emptyf ( t , deleted , "Expected empty list of deleted rules, got %v" , deleted )
} )
} )
2025-03-12 00:40:44 +08:00
}
2025-01-25 03:49:05 +08:00
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 ,
} ,
}
}
// rulesNamespaceWithoutVariableValues takes a apimodels.NamespaceConfigResponse JSON-based input and makes the dynamic fields static e.g. uid, dates, etc.
// it returns a map of the modified rule UIDs with the namespace,rule_group as a key
func rulesNamespaceWithoutVariableValues ( t * testing . T , b [ ] byte ) ( string , map [ string ] [ ] string ) {
t . Helper ( )
var r apimodels . NamespaceConfigResponse
require . NoError ( t , json . Unmarshal ( b , & r ) )
// create a map holding the created rule UIDs per namespace/group
m := make ( map [ string ] [ ] string )
for namespace , nodes := range r {
for _ , node := range nodes {
compositeKey := strings . Join ( [ ] string { namespace , node . Name } , "," )
_ , ok := m [ compositeKey ]
if ! ok {
m [ compositeKey ] = make ( [ ] string , 0 , len ( node . Rules ) )
}
for _ , rule := range node . Rules {
m [ compositeKey ] = append ( m [ compositeKey ] , rule . GrafanaManagedAlert . UID )
rule . GrafanaManagedAlert . UID = "uid"
rule . GrafanaManagedAlert . NamespaceUID = "nsuid"
rule . GrafanaManagedAlert . Updated = time . Date ( 2021 , time . Month ( 2 ) , 21 , 1 , 10 , 30 , 0 , time . UTC )
2025-01-28 03:31:40 +08:00
rule . GrafanaManagedAlert . UpdatedBy . UID = "uid"
2025-03-15 04:14:06 +08:00
rule . GrafanaManagedAlert . GUID = "guid"
2025-01-25 03:49:05 +08:00
}
}
}
json , err := json . Marshal ( & r )
require . NoError ( t , err )
return string ( json ) , m
}
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"
} ` ) ,
} ,
} ,
} ,
} ,
} ,
}
2025-03-15 04:14:06 +08:00
resp , status , _ := client . PostRulesGroupWithStatus ( t , folder , & rules , false )
2025-02-08 00:24:28 +08:00
require . Equal ( t , http . StatusAccepted , status )
2025-01-25 03:49:05 +08:00
require . Len ( t , resp . Created , 1 )
return rules , resp . Created [ 0 ]
}
func getLongString ( t * testing . T , n int ) string {
t . Helper ( )
b := make ( [ ] rune , n )
for i := range b {
b [ i ] = 'a'
}
return string ( b )
}