github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/ruler/api_test.go (about)

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