mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
	
	
		
			409 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
		
		
			
		
	
	
			409 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Go
		
	
	
	
|  | package alerting | ||
|  | 
 | ||
|  | import ( | ||
|  | 	"context" | ||
|  | 	"encoding/json" | ||
|  | 	"fmt" | ||
|  | 	"net/http" | ||
|  | 	"testing" | ||
|  | 	"time" | ||
|  | 
 | ||
|  | 	alertingModels "github.com/grafana/alerting/models" | ||
|  | 	amv2 "github.com/prometheus/alertmanager/api/v2/models" | ||
|  | 	"github.com/prometheus/common/model" | ||
|  | 	"github.com/stretchr/testify/require" | ||
|  | 
 | ||
|  | 	"github.com/grafana/grafana/pkg/expr" | ||
|  | 	"github.com/grafana/grafana/pkg/services/accesscontrol" | ||
|  | 	"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions" | ||
|  | 	"github.com/grafana/grafana/pkg/services/datasources" | ||
|  | 	apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" | ||
|  | 	ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" | ||
|  | 	"github.com/grafana/grafana/pkg/services/org" | ||
|  | 	"github.com/grafana/grafana/pkg/services/user" | ||
|  | 	"github.com/grafana/grafana/pkg/setting" | ||
|  | 	"github.com/grafana/grafana/pkg/tests/testinfra" | ||
|  | 	"github.com/grafana/grafana/pkg/util" | ||
|  | ) | ||
|  | 
 | ||
|  | const ( | ||
|  | 	TESTDATA_UID = "testdata" | ||
|  | ) | ||
|  | 
 | ||
|  | func TestGrafanaRuleConfig(t *testing.T) { | ||
|  | 	dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ | ||
|  | 		DisableLegacyAlerting: true, | ||
|  | 		EnableUnifiedAlerting: true, | ||
|  | 		DisableAnonymous:      true, | ||
|  | 		AppModeProduction:     true, | ||
|  | 		EnableFeatureToggles:  []string{}, | ||
|  | 		EnableLog:             false, | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path) | ||
|  | 
 | ||
|  | 	userId := createUser(t, env.SQLStore, user.CreateUserCommand{ | ||
|  | 		DefaultOrgRole: string(org.RoleAdmin), | ||
|  | 		Password:       "admin", | ||
|  | 		Login:          "admin", | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	apiCli := newAlertingApiClient(grafanaListedAddr, "admin", "admin") | ||
|  | 
 | ||
|  | 	dsCmd := &datasources.AddDataSourceCommand{ | ||
|  | 		Name:   "TestDatasource", | ||
|  | 		Type:   "testdata", | ||
|  | 		Access: datasources.DS_ACCESS_PROXY, | ||
|  | 		UID:    TESTDATA_UID, | ||
|  | 		UserID: userId, | ||
|  | 		OrgID:  1, | ||
|  | 	} | ||
|  | 	_, err := env.Server.HTTPServer.DataSourcesService.AddDataSource(context.Background(), dsCmd) | ||
|  | 	require.NoError(t, err) | ||
|  | 
 | ||
|  | 	dynamicLabels := []string{"GA", "FL", "AL", "AZ"} | ||
|  | 	dynamicLabelsJson, _ := json.Marshal(&dynamicLabels) | ||
|  | 	testdataQueryModel := json.RawMessage(fmt.Sprintf(`{ | ||
|  | 								  "refId": "A", | ||
|  | 								  "hide": false, | ||
|  | 								  "scenarioId": "usa", | ||
|  | 								  "usa": { | ||
|  | 									"mode": "timeseries", | ||
|  | 									"period": "1m", | ||
|  | 									"states": %s, | ||
|  | 									"fields": [ | ||
|  | 									  "baz" | ||
|  | 									] | ||
|  | 								  } | ||
|  | 								}`, string(dynamicLabelsJson))) | ||
|  | 
 | ||
|  | 	genRule := func(ruleGen func() apimodels.PostableExtendedRuleNode) apimodels.PostableExtendedRuleNodeExtended { | ||
|  | 		return apimodels.PostableExtendedRuleNodeExtended{ | ||
|  | 			Rule:           ruleGen(), | ||
|  | 			NamespaceUID:   "NamespaceUID", | ||
|  | 			NamespaceTitle: "NamespaceTitle", | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	t.Run("valid rule should accept request", func(t *testing.T) { | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, genRule(alertRuleGen())) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should return alerts in response", func(t *testing.T) { | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, genRule(alertRuleGen())) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 1) | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should return static annotations", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		rule.Rule.Annotations = map[string]string{ | ||
|  | 			"foo":  "bar", | ||
|  | 			"foo2": "bar2", | ||
|  | 		} | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for _, alert := range result { | ||
|  | 			require.Equal(t, "bar", alert.Annotations["foo"]) | ||
|  | 			require.Equal(t, "bar2", alert.Annotations["foo2"]) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should return static labels", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		rule.Rule.Labels = map[string]string{ | ||
|  | 			"foo":  "bar", | ||
|  | 			"foo2": "bar2", | ||
|  | 		} | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for _, alert := range result { | ||
|  | 			require.Equal(t, "bar", alert.Labels["foo"]) | ||
|  | 			require.Equal(t, "bar2", alert.Labels["foo2"]) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should return interpolated annotations", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		rule.Rule.Annotations = map[string]string{ | ||
|  | 			"value":    "{{ $value }}", | ||
|  | 			"values.B": "{{ $values.B }}", | ||
|  | 			"values.C": "{{ $values.C }}", | ||
|  | 		} | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for i, alert := range result { | ||
|  | 			require.NotEmpty(t, alert.Annotations["values.B"]) | ||
|  | 			require.NotEmpty(t, alert.Annotations["values.C"]) | ||
|  | 			valueB := fmt.Sprintf("[ var='B' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Annotations["values.B"]) | ||
|  | 			valueC := fmt.Sprintf("[ var='C' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Annotations["values.C"]) | ||
|  | 			require.Contains(t, alert.Annotations["value"], valueB) | ||
|  | 			require.Contains(t, alert.Annotations["value"], valueC) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should return interpolated labels", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		rule.Rule.Labels = map[string]string{ | ||
|  | 			"value":    "{{ $value }}", | ||
|  | 			"values.B": "{{ $values.B }}", | ||
|  | 			"values.C": "{{ $values.C }}", | ||
|  | 		} | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for i, alert := range result { | ||
|  | 			require.NotEmpty(t, alert.Labels["values.B"]) | ||
|  | 			require.NotEmpty(t, alert.Labels["values.C"]) | ||
|  | 			valueB := fmt.Sprintf("[ var='B' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Labels["values.B"]) | ||
|  | 			valueC := fmt.Sprintf("[ var='C' labels={state=%s} value=%s ]", dynamicLabels[i], alert.Labels["values.C"]) | ||
|  | 			require.Contains(t, alert.Labels["value"], valueB) | ||
|  | 			require.Contains(t, alert.Labels["value"], valueC) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should use functions with annotations", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		rule.Rule.Annotations = map[string]string{ | ||
|  | 			"externalURL": "{{ externalURL }}", | ||
|  | 			"humanize":    "{{ humanize 1000.0 }}", | ||
|  | 		} | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for _, alert := range result { | ||
|  | 			require.Equal(t, "http://localhost:3000/", alert.Annotations["externalURL"]) | ||
|  | 			require.Equal(t, "1k", alert.Annotations["humanize"]) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should use functions with labels", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		rule.Rule.Labels = map[string]string{ | ||
|  | 			"externalURL": "{{ externalURL }}", | ||
|  | 			"humanize":    "{{ humanize 1000.0 }}", | ||
|  | 		} | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for _, alert := range result { | ||
|  | 			require.Equal(t, "http://localhost:3000/", alert.Labels["externalURL"]) | ||
|  | 			require.Equal(t, "1k", alert.Labels["humanize"]) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should return dynamic labels", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for i, alert := range result { | ||
|  | 			require.Equal(t, dynamicLabels[i], alert.Labels["state"]) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("valid rule should return built-in labels", func(t *testing.T) { | ||
|  | 		rule := genRule(testdataRule(testdataQueryModel, nil, nil)) | ||
|  | 		status, body := apiCli.SubmitRuleForTesting(t, rule) | ||
|  | 		require.Equal(t, http.StatusOK, status) | ||
|  | 		var result []amv2.PostableAlert | ||
|  | 		require.NoErrorf(t, json.Unmarshal([]byte(body), &result), "cannot parse response to data frame") | ||
|  | 		require.Len(t, result, 4) | ||
|  | 		for _, alert := range result { | ||
|  | 			require.Equal(t, rule.Rule.GrafanaManagedAlert.Title, alert.Labels[model.AlertNameLabel]) | ||
|  | 			require.Equal(t, rule.NamespaceUID, alert.Labels[alertingModels.NamespaceUIDLabel]) | ||
|  | 			require.Equal(t, rule.NamespaceTitle, alert.Labels[ngmodels.FolderTitleLabel]) | ||
|  | 		} | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("invalid rule should reject request", func(t *testing.T) { | ||
|  | 		req := genRule(alertRuleGen()) | ||
|  | 		req.Rule = apimodels.PostableExtendedRuleNode{} | ||
|  | 		status, _ := apiCli.SubmitRuleForTesting(t, req) | ||
|  | 		require.Equal(t, http.StatusBadRequest, status) | ||
|  | 	}) | ||
|  | 
 | ||
|  | 	t.Run("authentication permissions", func(t *testing.T) { | ||
|  | 		if !setting.IsEnterprise { | ||
|  | 			t.Skip("Enterprise-only test") | ||
|  | 		} | ||
|  | 
 | ||
|  | 		testUserId := createUser(t, env.SQLStore, user.CreateUserCommand{ | ||
|  | 			DefaultOrgRole: "DOESNOTEXIST", // Needed so that the SignedInUser has OrgId=1. Otherwise, datasource will not be found.
 | ||
|  | 			Password:       "test", | ||
|  | 			Login:          "test", | ||
|  | 		}) | ||
|  | 
 | ||
|  | 		testUserApiCli := newAlertingApiClient(grafanaListedAddr, "test", "test") | ||
|  | 
 | ||
|  | 		t.Run("fail if can't read rules", func(t *testing.T) { | ||
|  | 			status, body := testUserApiCli.SubmitRuleForTesting(t, genRule(testdataRule(testdataQueryModel, nil, nil))) | ||
|  | 			require.Contains(t, body, accesscontrol.ActionAlertingRuleRead) | ||
|  | 			require.Equalf(t, http.StatusForbidden, status, "Response: %s", body) | ||
|  | 		}) | ||
|  | 
 | ||
|  | 		// access control permissions store
 | ||
|  | 		permissionsStore := resourcepermissions.NewStore(env.SQLStore) | ||
|  | 		_, err := permissionsStore.SetUserResourcePermission(context.Background(), | ||
|  | 			accesscontrol.GlobalOrgID, | ||
|  | 			accesscontrol.User{ID: testUserId}, | ||
|  | 			resourcepermissions.SetResourcePermissionCommand{ | ||
|  | 				Actions: []string{ | ||
|  | 					accesscontrol.ActionAlertingRuleRead, | ||
|  | 				}, | ||
|  | 				Resource:          "folders", | ||
|  | 				ResourceID:        "*", | ||
|  | 				ResourceAttribute: "uid", | ||
|  | 			}, nil) | ||
|  | 		require.NoError(t, err) | ||
|  | 		testUserApiCli.ReloadCachedPermissions(t) | ||
|  | 
 | ||
|  | 		t.Run("fail if can't query data sources", func(t *testing.T) { | ||
|  | 			status, body := testUserApiCli.SubmitRuleForTesting(t, genRule(testdataRule(testdataQueryModel, nil, nil))) | ||
|  | 			require.Contains(t, body, "user is not authorized to query one or many data sources used by the rule") | ||
|  | 			require.Equalf(t, http.StatusUnauthorized, status, "Response: %s", body) | ||
|  | 		}) | ||
|  | 
 | ||
|  | 		_, err = permissionsStore.SetUserResourcePermission(context.Background(), | ||
|  | 			accesscontrol.GlobalOrgID, | ||
|  | 			accesscontrol.User{ID: testUserId}, | ||
|  | 			resourcepermissions.SetResourcePermissionCommand{ | ||
|  | 				Actions: []string{ | ||
|  | 					datasources.ActionQuery, | ||
|  | 				}, | ||
|  | 				Resource:          "datasources", | ||
|  | 				ResourceID:        TESTDATA_UID, | ||
|  | 				ResourceAttribute: "uid", | ||
|  | 			}, nil) | ||
|  | 		require.NoError(t, err) | ||
|  | 		testUserApiCli.ReloadCachedPermissions(t) | ||
|  | 
 | ||
|  | 		t.Run("succeed if can query data sources", func(t *testing.T) { | ||
|  | 			status, body := testUserApiCli.SubmitRuleForTesting(t, genRule(testdataRule(testdataQueryModel, nil, nil))) | ||
|  | 			require.Equalf(t, http.StatusOK, status, "Response: %s", body) | ||
|  | 		}) | ||
|  | 	}) | ||
|  | } | ||
|  | 
 | ||
|  | func testdataRule(queryModel json.RawMessage, labels map[string]string, annotations map[string]string) func() apimodels.PostableExtendedRuleNode { | ||
|  | 	return func() apimodels.PostableExtendedRuleNode { | ||
|  | 		forDuration := model.Duration(10 * time.Second) | ||
|  | 		return apimodels.PostableExtendedRuleNode{ | ||
|  | 			ApiRuleNode: &apimodels.ApiRuleNode{ | ||
|  | 				For:         &forDuration, | ||
|  | 				Labels:      labels, | ||
|  | 				Annotations: annotations, | ||
|  | 			}, | ||
|  | 			GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ | ||
|  | 				Title:     fmt.Sprintf("rule-%s", util.GenerateShortUID()), | ||
|  | 				Condition: "C", | ||
|  | 				Data: []apimodels.AlertQuery{ | ||
|  | 					{ | ||
|  | 						RefID:             "A", | ||
|  | 						RelativeTimeRange: apimodels.RelativeTimeRange{From: 600, To: 0}, | ||
|  | 						DatasourceUID:     TESTDATA_UID, | ||
|  | 						Model:             queryModel, | ||
|  | 					}, | ||
|  | 					{ // Simple reduce last A.
 | ||
|  | 						RefID:             "B", | ||
|  | 						RelativeTimeRange: apimodels.RelativeTimeRange{From: 0, To: 0}, | ||
|  | 						DatasourceUID:     expr.DatasourceUID, | ||
|  | 						Model: json.RawMessage(`{ | ||
|  | 								  "refId": "B", | ||
|  | 								  "hide": false, | ||
|  | 								  "type": "reduce", | ||
|  | 								  "datasource": { | ||
|  | 									"uid": "__expr__", | ||
|  | 									"type": "__expr__" | ||
|  | 								  }, | ||
|  | 								  "conditions": [ | ||
|  | 									{ | ||
|  | 									  "type": "query", | ||
|  | 									  "evaluator": { | ||
|  | 										"params": [], | ||
|  | 										"type": "gt" | ||
|  | 									  }, | ||
|  | 									  "operator": { | ||
|  | 										"type": "and" | ||
|  | 									  }, | ||
|  | 									  "query": { | ||
|  | 										"params": [ | ||
|  | 										  "B" | ||
|  | 										] | ||
|  | 									  }, | ||
|  | 									  "reducer": { | ||
|  | 										"params": [], | ||
|  | 										"type": "last" | ||
|  | 									  } | ||
|  | 									} | ||
|  | 								  ], | ||
|  | 								  "reducer": "last", | ||
|  | 								  "expression": "A" | ||
|  | 								}`), | ||
|  | 					}, | ||
|  | 					{ // Threshold B > 0.
 | ||
|  | 						RefID:             "C", | ||
|  | 						RelativeTimeRange: apimodels.RelativeTimeRange{From: 0, To: 0}, | ||
|  | 						DatasourceUID:     expr.DatasourceUID, | ||
|  | 						Model: json.RawMessage(`{ | ||
|  | 							  "refId": "C", | ||
|  | 							  "hide": false, | ||
|  | 							  "type": "threshold", | ||
|  | 							  "datasource": { | ||
|  | 								"uid": "__expr__", | ||
|  | 								"type": "__expr__" | ||
|  | 							  }, | ||
|  | 							  "conditions": [ | ||
|  | 								{ | ||
|  | 								  "type": "query", | ||
|  | 								  "evaluator": { | ||
|  | 									"params": [ | ||
|  | 									  0 | ||
|  | 									], | ||
|  | 									"type": "gt" | ||
|  | 								  }, | ||
|  | 								  "operator": { | ||
|  | 									"type": "and" | ||
|  | 								  }, | ||
|  | 								  "query": { | ||
|  | 									"params": [ | ||
|  | 									  "C" | ||
|  | 									] | ||
|  | 								  }, | ||
|  | 								  "reducer": { | ||
|  | 									"params": [], | ||
|  | 									"type": "last" | ||
|  | 								  } | ||
|  | 								} | ||
|  | 							  ], | ||
|  | 							  "expression": "B" | ||
|  | 							}`), | ||
|  | 					}, | ||
|  | 				}, | ||
|  | 			}, | ||
|  | 		} | ||
|  | 	} | ||
|  | } |