github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/ruler/base/api_test.go (about)

     1  package base
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/http/httptest"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/gorilla/mux"
    16  	"github.com/grafana/dskit/services"
    17  	"github.com/stretchr/testify/require"
    18  	"github.com/weaveworks/common/user"
    19  
    20  	"github.com/grafana/loki/pkg/ruler/rulespb"
    21  )
    22  
    23  func TestRuler_rules(t *testing.T) {
    24  	cfg := defaultRulerConfig(t, newMockRuleStore(mockRules))
    25  
    26  	r := newTestRuler(t, cfg)
    27  	defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck
    28  
    29  	a := NewAPI(r, r.store, log.NewNopLogger())
    30  
    31  	req := requestFor(t, "GET", "https://localhost:8080/api/prom/api/v1/rules", nil, "user1")
    32  	w := httptest.NewRecorder()
    33  	a.PrometheusRules(w, req)
    34  
    35  	resp := w.Result()
    36  	body, _ := ioutil.ReadAll(resp.Body)
    37  
    38  	// Check status code and status response
    39  	responseJSON := response{}
    40  	err := json.Unmarshal(body, &responseJSON)
    41  	require.NoError(t, err)
    42  	require.Equal(t, http.StatusOK, resp.StatusCode)
    43  	require.Equal(t, responseJSON.Status, "success")
    44  
    45  	// Testing the running rules for user1 in the mock store
    46  	expectedResponse, _ := json.Marshal(response{
    47  		Status: "success",
    48  		Data: &RuleDiscovery{
    49  			RuleGroups: []*RuleGroup{
    50  				{
    51  					Name: "group1",
    52  					File: "namespace1",
    53  					Rules: []rule{
    54  						&recordingRule{
    55  							Name:   "UP_RULE",
    56  							Query:  "up",
    57  							Health: "unknown",
    58  							Type:   "recording",
    59  						},
    60  						&alertingRule{
    61  							Name:   "UP_ALERT",
    62  							Query:  "up < 1",
    63  							State:  "inactive",
    64  							Health: "unknown",
    65  							Type:   "alerting",
    66  							Alerts: []*Alert{},
    67  						},
    68  					},
    69  					Interval: 60,
    70  				},
    71  			},
    72  		},
    73  	})
    74  
    75  	require.Equal(t, string(expectedResponse), string(body))
    76  }
    77  
    78  func TestRuler_rules_special_characters(t *testing.T) {
    79  	cfg := defaultRulerConfig(t, newMockRuleStore(mockSpecialCharRules))
    80  
    81  	r := newTestRuler(t, cfg)
    82  	defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck
    83  
    84  	a := NewAPI(r, r.store, log.NewNopLogger())
    85  
    86  	req := requestFor(t, http.MethodGet, "https://localhost:8080/api/prom/api/v1/rules", nil, "user1")
    87  	w := httptest.NewRecorder()
    88  	a.PrometheusRules(w, req)
    89  
    90  	resp := w.Result()
    91  	body, _ := ioutil.ReadAll(resp.Body)
    92  
    93  	// Check status code and status response
    94  	responseJSON := response{}
    95  	err := json.Unmarshal(body, &responseJSON)
    96  	require.NoError(t, err)
    97  	require.Equal(t, http.StatusOK, resp.StatusCode)
    98  	require.Equal(t, responseJSON.Status, "success")
    99  
   100  	// Testing the running rules for user1 in the mock store
   101  	expectedResponse, _ := json.Marshal(response{
   102  		Status: "success",
   103  		Data: &RuleDiscovery{
   104  			RuleGroups: []*RuleGroup{
   105  				{
   106  					Name: ")(_+?/|group1+/?",
   107  					File: ")(_+?/|namespace1+/?",
   108  					Rules: []rule{
   109  						&recordingRule{
   110  							Name:   "UP_RULE",
   111  							Query:  "up",
   112  							Health: "unknown",
   113  							Type:   "recording",
   114  						},
   115  						&alertingRule{
   116  							Name:   "UP_ALERT",
   117  							Query:  "up < 1",
   118  							State:  "inactive",
   119  							Health: "unknown",
   120  							Type:   "alerting",
   121  							Alerts: []*Alert{},
   122  						},
   123  					},
   124  					Interval: 60,
   125  				},
   126  			},
   127  		},
   128  	})
   129  
   130  	require.Equal(t, string(expectedResponse), string(body))
   131  }
   132  
   133  func TestRuler_alerts(t *testing.T) {
   134  	cfg := defaultRulerConfig(t, newMockRuleStore(mockRules))
   135  
   136  	r := newTestRuler(t, cfg)
   137  	defer r.StopAsync()
   138  
   139  	a := NewAPI(r, r.store, log.NewNopLogger())
   140  
   141  	req := requestFor(t, http.MethodGet, "https://localhost:8080/api/prom/api/v1/alerts", nil, "user1")
   142  	w := httptest.NewRecorder()
   143  	a.PrometheusAlerts(w, req)
   144  
   145  	resp := w.Result()
   146  	body, _ := ioutil.ReadAll(resp.Body)
   147  
   148  	// Check status code and status response
   149  	responseJSON := response{}
   150  	err := json.Unmarshal(body, &responseJSON)
   151  	require.NoError(t, err)
   152  	require.Equal(t, http.StatusOK, resp.StatusCode)
   153  	require.Equal(t, responseJSON.Status, "success")
   154  
   155  	// Currently there is not an easy way to mock firing alerts. The empty
   156  	// response case is tested instead.
   157  	expectedResponse, _ := json.Marshal(response{
   158  		Status: "success",
   159  		Data: &AlertDiscovery{
   160  			Alerts: []*Alert{},
   161  		},
   162  	})
   163  
   164  	require.Equal(t, string(expectedResponse), string(body))
   165  }
   166  
   167  func TestRuler_Create(t *testing.T) {
   168  	cfg := defaultRulerConfig(t, newMockRuleStore(make(map[string]rulespb.RuleGroupList)))
   169  
   170  	r := newTestRuler(t, cfg)
   171  	defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck
   172  
   173  	a := NewAPI(r, r.store, log.NewNopLogger())
   174  
   175  	tc := []struct {
   176  		name   string
   177  		input  string
   178  		output string
   179  		err    error
   180  		status int
   181  	}{
   182  		{
   183  			name:   "with an empty payload",
   184  			input:  "",
   185  			status: 400,
   186  			err:    errors.New("invalid rules config: rule group name must not be empty"),
   187  		},
   188  		{
   189  			name: "with no rule group name",
   190  			input: `
   191  interval: 15s
   192  rules:
   193  - record: up_rule
   194    expr: up
   195  `,
   196  			status: 400,
   197  			err:    errors.New("invalid rules config: rule group name must not be empty"),
   198  		},
   199  		{
   200  			name: "with no rules",
   201  			input: `
   202  name: rg_name
   203  interval: 15s
   204  `,
   205  			status: 400,
   206  			err:    errors.New("invalid rules config: rule group 'rg_name' has no rules"),
   207  		},
   208  		{
   209  			name:   "with a a valid rules file",
   210  			status: 202,
   211  			input: `
   212  name: test
   213  interval: 15s
   214  rules:
   215  - record: up_rule
   216    expr: up{}
   217  - alert: up_alert
   218    expr: sum(up{}) > 1
   219    for: 30s
   220    annotations:
   221      test: test
   222    labels:
   223      test: test
   224  `,
   225  			output: "name: test\ninterval: 15s\nrules:\n    - record: up_rule\n      expr: up{}\n    - alert: up_alert\n      expr: sum(up{}) > 1\n      for: 30s\n      labels:\n        test: test\n      annotations:\n        test: test\n",
   226  		},
   227  	}
   228  
   229  	for _, tt := range tc {
   230  		t.Run(tt.name, func(t *testing.T) {
   231  			router := mux.NewRouter()
   232  			router.Path("/api/v1/rules/{namespace}").Methods("POST").HandlerFunc(a.CreateRuleGroup)
   233  			router.Path("/api/v1/rules/{namespace}/{groupName}").Methods("GET").HandlerFunc(a.GetRuleGroup)
   234  			// POST
   235  			req := requestFor(t, http.MethodPost, "https://localhost:8080/api/v1/rules/namespace", strings.NewReader(tt.input), "user1")
   236  			w := httptest.NewRecorder()
   237  
   238  			router.ServeHTTP(w, req)
   239  			require.Equal(t, tt.status, w.Code)
   240  
   241  			if tt.err == nil {
   242  				// GET
   243  				req = requestFor(t, http.MethodGet, "https://localhost:8080/api/v1/rules/namespace/test", nil, "user1")
   244  				w = httptest.NewRecorder()
   245  
   246  				router.ServeHTTP(w, req)
   247  				require.Equal(t, 200, w.Code)
   248  				require.Equal(t, tt.output, w.Body.String())
   249  			} else {
   250  				require.Equal(t, tt.err.Error()+"\n", w.Body.String())
   251  			}
   252  		})
   253  	}
   254  }
   255  
   256  func TestRuler_DeleteNamespace(t *testing.T) {
   257  	cfg := defaultRulerConfig(t, newMockRuleStore(mockRulesNamespaces))
   258  
   259  	r := newTestRuler(t, cfg)
   260  	defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck
   261  
   262  	a := NewAPI(r, r.store, log.NewNopLogger())
   263  
   264  	router := mux.NewRouter()
   265  	router.Path("/api/v1/rules/{namespace}").Methods(http.MethodDelete).HandlerFunc(a.DeleteNamespace)
   266  	router.Path("/api/v1/rules/{namespace}/{groupName}").Methods(http.MethodGet).HandlerFunc(a.GetRuleGroup)
   267  
   268  	// Verify namespace1 rules are there.
   269  	req := requestFor(t, http.MethodGet, "https://localhost:8080/api/v1/rules/namespace1/group1", nil, "user1")
   270  	w := httptest.NewRecorder()
   271  
   272  	router.ServeHTTP(w, req)
   273  	require.Equal(t, http.StatusOK, w.Code)
   274  	require.Equal(t, "name: group1\ninterval: 1m\nrules:\n    - record: UP_RULE\n      expr: up\n    - alert: UP_ALERT\n      expr: up < 1\n", w.Body.String())
   275  
   276  	// Delete namespace1
   277  	req = requestFor(t, http.MethodDelete, "https://localhost:8080/api/v1/rules/namespace1", nil, "user1")
   278  	w = httptest.NewRecorder()
   279  
   280  	router.ServeHTTP(w, req)
   281  	require.Equal(t, http.StatusAccepted, w.Code)
   282  	require.Equal(t, "{\"status\":\"success\",\"data\":null,\"errorType\":\"\",\"error\":\"\"}", w.Body.String())
   283  
   284  	// On Partial failures
   285  	req = requestFor(t, http.MethodDelete, "https://localhost:8080/api/v1/rules/namespace2", nil, "user1")
   286  	w = httptest.NewRecorder()
   287  
   288  	router.ServeHTTP(w, req)
   289  	require.Equal(t, http.StatusInternalServerError, w.Code)
   290  	require.Equal(t, "{\"status\":\"error\",\"data\":null,\"errorType\":\"server_error\",\"error\":\"unable to delete rg\"}", w.Body.String())
   291  }
   292  
   293  func TestRuler_LimitsPerGroup(t *testing.T) {
   294  	cfg := defaultRulerConfig(t, newMockRuleStore(make(map[string]rulespb.RuleGroupList)))
   295  
   296  	r := newTestRuler(t, cfg)
   297  	defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck
   298  
   299  	r.limits = ruleLimits{maxRuleGroups: 1, maxRulesPerRuleGroup: 1}
   300  
   301  	a := NewAPI(r, r.store, log.NewNopLogger())
   302  
   303  	tc := []struct {
   304  		name   string
   305  		input  string
   306  		output string
   307  		err    error
   308  		status int
   309  	}{
   310  		{
   311  			name:   "when exceeding the rules per rule group limit",
   312  			status: 400,
   313  			input: `
   314  name: test
   315  interval: 15s
   316  rules:
   317  - record: up_rule
   318    expr: up{}
   319  - alert: up_alert
   320    expr: sum(up{}) > 1
   321    for: 30s
   322    annotations:
   323      test: test
   324    labels:
   325      test: test
   326  `,
   327  			output: "per-user rules per rule group limit (limit: 1 actual: 2) exceeded\n",
   328  		},
   329  	}
   330  
   331  	for _, tt := range tc {
   332  		t.Run(tt.name, func(t *testing.T) {
   333  			router := mux.NewRouter()
   334  			router.Path("/api/v1/rules/{namespace}").Methods("POST").HandlerFunc(a.CreateRuleGroup)
   335  			// POST
   336  			req := requestFor(t, http.MethodPost, "https://localhost:8080/api/v1/rules/namespace", strings.NewReader(tt.input), "user1")
   337  			w := httptest.NewRecorder()
   338  
   339  			router.ServeHTTP(w, req)
   340  			require.Equal(t, tt.status, w.Code)
   341  			require.Equal(t, tt.output, w.Body.String())
   342  		})
   343  	}
   344  }
   345  
   346  func TestRuler_RulerGroupLimits(t *testing.T) {
   347  	cfg := defaultRulerConfig(t, newMockRuleStore(make(map[string]rulespb.RuleGroupList)))
   348  
   349  	r := newTestRuler(t, cfg)
   350  	defer services.StopAndAwaitTerminated(context.Background(), r) //nolint:errcheck
   351  
   352  	r.limits = ruleLimits{maxRuleGroups: 1, maxRulesPerRuleGroup: 1}
   353  
   354  	a := NewAPI(r, r.store, log.NewNopLogger())
   355  
   356  	tc := []struct {
   357  		name   string
   358  		input  string
   359  		output string
   360  		err    error
   361  		status int
   362  	}{
   363  		{
   364  			name:   "when pushing the first group within bounds of the limit",
   365  			status: 202,
   366  			input: `
   367  name: test_first_group_will_succeed
   368  interval: 15s
   369  rules:
   370  - record: up_rule
   371    expr: up{}
   372  `,
   373  			output: "{\"status\":\"success\",\"data\":null,\"errorType\":\"\",\"error\":\"\"}",
   374  		},
   375  		{
   376  			name:   "when exceeding the rule group limit after sending the first group",
   377  			status: 400,
   378  			input: `
   379  name: test_second_group_will_fail
   380  interval: 15s
   381  rules:
   382  - record: up_rule
   383    expr: up{}
   384  `,
   385  			output: "per-user rule groups limit (limit: 1 actual: 2) exceeded\n",
   386  		},
   387  	}
   388  
   389  	// define once so the requests build on each other so the number of rules can be tested
   390  	router := mux.NewRouter()
   391  	router.Path("/api/v1/rules/{namespace}").Methods("POST").HandlerFunc(a.CreateRuleGroup)
   392  
   393  	for _, tt := range tc {
   394  		t.Run(tt.name, func(t *testing.T) {
   395  			// POST
   396  			req := requestFor(t, http.MethodPost, "https://localhost:8080/api/v1/rules/namespace", strings.NewReader(tt.input), "user1")
   397  			w := httptest.NewRecorder()
   398  
   399  			router.ServeHTTP(w, req)
   400  			require.Equal(t, tt.status, w.Code)
   401  			require.Equal(t, tt.output, w.Body.String())
   402  		})
   403  	}
   404  }
   405  
   406  func requestFor(t *testing.T, method string, url string, body io.Reader, userID string) *http.Request {
   407  	t.Helper()
   408  
   409  	req := httptest.NewRequest(method, url, body)
   410  	ctx := user.InjectOrgID(req.Context(), userID)
   411  
   412  	return req.WithContext(ctx)
   413  }