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 }