github.com/sequix/cortex@v1.1.6/pkg/ruler/api_test.go (about) 1 package ruler 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "strings" 12 "testing" 13 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 17 "github.com/sequix/cortex/pkg/configs" 18 "github.com/sequix/cortex/pkg/configs/api" 19 "github.com/sequix/cortex/pkg/configs/client" 20 "github.com/sequix/cortex/pkg/configs/db" 21 "github.com/sequix/cortex/pkg/configs/db/dbtest" 22 "github.com/weaveworks/common/user" 23 ) 24 25 const ( 26 endpoint = "/api/prom/rules" 27 ) 28 29 var ( 30 app *API 31 database db.DB 32 counter int 33 privateAPI client.Client 34 ) 35 36 // setup sets up the environment for the tests. 37 func setup(t *testing.T) { 38 database = dbtest.Setup(t) 39 app = NewAPI(database) 40 counter = 0 41 var err error 42 privateAPI, err = client.New(client.Config{ 43 DBConfig: db.Config{ 44 URI: "mock", // trigger client.NewConfigClient to use the mock DB. 45 Mock: database, 46 }, 47 }) 48 require.NoError(t, err) 49 } 50 51 // cleanup cleans up the environment after a test. 52 func cleanup(t *testing.T) { 53 dbtest.Cleanup(t, database) 54 } 55 56 // request makes a request to the configs API. 57 func request(t *testing.T, handler http.Handler, method, urlStr string, body io.Reader) *httptest.ResponseRecorder { 58 w := httptest.NewRecorder() 59 r, err := http.NewRequest(method, urlStr, body) 60 require.NoError(t, err) 61 handler.ServeHTTP(w, r) 62 return w 63 } 64 65 // requestAsUser makes a request to the configs API as the given user. 66 func requestAsUser(t *testing.T, handler http.Handler, userID string, method, urlStr string, body io.Reader) *httptest.ResponseRecorder { 67 w := httptest.NewRecorder() 68 r, err := http.NewRequest(method, urlStr, body) 69 require.NoError(t, err) 70 r = r.WithContext(user.InjectOrgID(r.Context(), userID)) 71 user.InjectOrgIDIntoHTTPRequest(r.Context(), r) 72 handler.ServeHTTP(w, r) 73 return w 74 } 75 76 // makeString makes a string, guaranteed to be unique within a test. 77 func makeString(pattern string) string { 78 counter++ 79 return fmt.Sprintf(pattern, counter) 80 } 81 82 // makeUserID makes an arbitrary user ID. Guaranteed to be unique within a test. 83 func makeUserID() string { 84 return makeString("user%d") 85 } 86 87 // makeRulerConfig makes an arbitrary ruler config 88 func makeRulerConfig(rfv configs.RuleFormatVersion) configs.RulesConfig { 89 switch rfv { 90 case configs.RuleFormatV1: 91 return configs.RulesConfig{ 92 Files: map[string]string{ 93 "filename.rules": makeString(` 94 # Config no. %d. 95 ALERT ScrapeFailed 96 IF up != 1 97 FOR 10m 98 LABELS { severity="warning" } 99 ANNOTATIONS { 100 summary = "Scrape of {{$labels.job}} (pod: {{$labels.instance}}) failed.", 101 description = "Prometheus cannot reach the /metrics page on the {{$labels.instance}} pod.", 102 impact = "We have no monitoring data for {{$labels.job}} - {{$labels.instance}}. At worst, it's completely down. At best, we cannot reliably respond to operational issues.", 103 dashboardURL = "$${base_url}/admin/prometheus/targets", 104 } 105 `), 106 }, 107 FormatVersion: configs.RuleFormatV1, 108 } 109 case configs.RuleFormatV2: 110 return configs.RulesConfig{ 111 Files: map[string]string{ 112 "filename.rules": makeString(` 113 # Config no. %d. 114 groups: 115 - name: example 116 rules: 117 - alert: ScrapeFailed 118 expr: 'up != 1' 119 for: 10m 120 labels: 121 severity: warning 122 annotations: 123 summary: "Scrape of {{$labels.job}} (pod: {{$labels.instance}}) failed." 124 description: "Prometheus cannot reach the /metrics page on the {{$labels.instance}} pod." 125 impact: "We have no monitoring data for {{$labels.job}} - {{$labels.instance}}. At worst, it's completely down. At best, we cannot reliably respond to operational issues." 126 dashboardURL: "$${base_url}/admin/prometheus/targets" 127 `), 128 }, 129 FormatVersion: configs.RuleFormatV2, 130 } 131 default: 132 panic("unknown rule format") 133 } 134 } 135 136 // parseVersionedRulesConfig parses a configs.VersionedRulesConfig from JSON. 137 func parseVersionedRulesConfig(t *testing.T, b []byte) configs.VersionedRulesConfig { 138 var x configs.VersionedRulesConfig 139 err := json.Unmarshal(b, &x) 140 require.NoError(t, err, "Could not unmarshal JSON: %v", string(b)) 141 return x 142 } 143 144 // post a config 145 func post(t *testing.T, userID string, oldConfig configs.RulesConfig, newConfig configs.RulesConfig) configs.VersionedRulesConfig { 146 updateRequest := configUpdateRequest{ 147 OldConfig: oldConfig, 148 NewConfig: newConfig, 149 } 150 b, err := json.Marshal(updateRequest) 151 require.NoError(t, err) 152 reader := bytes.NewReader(b) 153 w := requestAsUser(t, app, userID, "POST", endpoint, reader) 154 require.Equal(t, http.StatusNoContent, w.Code) 155 return get(t, userID) 156 } 157 158 // get a config 159 func get(t *testing.T, userID string) configs.VersionedRulesConfig { 160 w := requestAsUser(t, app, userID, "GET", endpoint, nil) 161 return parseVersionedRulesConfig(t, w.Body.Bytes()) 162 } 163 164 // configs returns 404 if no config has been created yet. 165 func Test_GetConfig_NotFound(t *testing.T) { 166 setup(t) 167 defer cleanup(t) 168 169 userID := makeUserID() 170 w := requestAsUser(t, app, userID, "GET", endpoint, nil) 171 assert.Equal(t, http.StatusNotFound, w.Code) 172 } 173 174 // configs returns 401 to requests without authentication. 175 func Test_PostConfig_Anonymous(t *testing.T) { 176 setup(t) 177 defer cleanup(t) 178 179 w := request(t, app, "POST", endpoint, nil) 180 assert.Equal(t, http.StatusUnauthorized, w.Code) 181 } 182 183 // Posting to a configuration sets it so that you can get it again. 184 func Test_PostConfig_CreatesConfig(t *testing.T) { 185 setup(t) 186 defer cleanup(t) 187 188 userID := makeUserID() 189 config := makeRulerConfig(configs.RuleFormatV2) 190 result := post(t, userID, configs.RulesConfig{}, config) 191 assert.Equal(t, config, result.Config) 192 } 193 194 // Posting an invalid config when there's none set returns an error and leaves the config unset. 195 func Test_PostConfig_InvalidNewConfig(t *testing.T) { 196 setup(t) 197 defer cleanup(t) 198 199 userID := makeUserID() 200 invalidConfig := configs.RulesConfig{ 201 Files: map[string]string{ 202 "some.rules": "invalid config", 203 }, 204 FormatVersion: configs.RuleFormatV2, 205 } 206 updateRequest := configUpdateRequest{ 207 OldConfig: configs.RulesConfig{}, 208 NewConfig: invalidConfig, 209 } 210 b, err := json.Marshal(updateRequest) 211 require.NoError(t, err) 212 reader := bytes.NewReader(b) 213 { 214 w := requestAsUser(t, app, userID, "POST", endpoint, reader) 215 require.Equal(t, http.StatusBadRequest, w.Code) 216 } 217 { 218 w := requestAsUser(t, app, userID, "GET", endpoint, nil) 219 require.Equal(t, http.StatusNotFound, w.Code) 220 } 221 } 222 223 // Posting a v1 rule format configuration sets it so that you can get it again. 224 func Test_PostConfig_UpdatesConfig_V1RuleFormat(t *testing.T) { 225 setup(t) 226 app = NewAPI(database) 227 defer cleanup(t) 228 229 userID := makeUserID() 230 config1 := makeRulerConfig(configs.RuleFormatV1) 231 view1 := post(t, userID, configs.RulesConfig{}, config1) 232 config2 := makeRulerConfig(configs.RuleFormatV1) 233 view2 := post(t, userID, config1, config2) 234 assert.True(t, view2.ID > view1.ID, "%v > %v", view2.ID, view1.ID) 235 assert.Equal(t, config2, view2.Config) 236 } 237 238 // Posting an invalid v1 rule format config when there's one already set returns an error and leaves the config as is. 239 func Test_PostConfig_InvalidChangedConfig_V1RuleFormat(t *testing.T) { 240 setup(t) 241 app = NewAPI(database) 242 defer cleanup(t) 243 244 userID := makeUserID() 245 config := makeRulerConfig(configs.RuleFormatV1) 246 post(t, userID, configs.RulesConfig{}, config) 247 invalidConfig := configs.RulesConfig{ 248 Files: map[string]string{ 249 "some.rules": "invalid config", 250 }, 251 FormatVersion: configs.RuleFormatV1, 252 } 253 updateRequest := configUpdateRequest{ 254 OldConfig: configs.RulesConfig{}, 255 NewConfig: invalidConfig, 256 } 257 b, err := json.Marshal(updateRequest) 258 require.NoError(t, err) 259 reader := bytes.NewReader(b) 260 { 261 w := requestAsUser(t, app, userID, "POST", endpoint, reader) 262 require.Equal(t, http.StatusBadRequest, w.Code) 263 } 264 result := get(t, userID) 265 assert.Equal(t, config, result.Config) 266 } 267 268 // Posting a v2 rule format configuration sets it so that you can get it again. 269 func Test_PostConfig_UpdatesConfig_V2RuleFormat(t *testing.T) { 270 setup(t) 271 defer cleanup(t) 272 273 userID := makeUserID() 274 config1 := makeRulerConfig(configs.RuleFormatV2) 275 view1 := post(t, userID, configs.RulesConfig{}, config1) 276 config2 := makeRulerConfig(configs.RuleFormatV2) 277 view2 := post(t, userID, config1, config2) 278 assert.True(t, view2.ID > view1.ID, "%v > %v", view2.ID, view1.ID) 279 assert.Equal(t, config2, view2.Config) 280 } 281 282 // Posting an invalid v2 rule format config when there's one already set returns an error and leaves the config as is. 283 func Test_PostConfig_InvalidChangedConfig_V2RuleFormat(t *testing.T) { 284 setup(t) 285 defer cleanup(t) 286 287 userID := makeUserID() 288 config := makeRulerConfig(configs.RuleFormatV2) 289 post(t, userID, configs.RulesConfig{}, config) 290 invalidConfig := configs.RulesConfig{ 291 Files: map[string]string{ 292 "some.rules": "invalid config", 293 }, 294 } 295 updateRequest := configUpdateRequest{ 296 OldConfig: configs.RulesConfig{}, 297 NewConfig: invalidConfig, 298 } 299 b, err := json.Marshal(updateRequest) 300 require.NoError(t, err) 301 reader := bytes.NewReader(b) 302 { 303 w := requestAsUser(t, app, userID, "POST", endpoint, reader) 304 require.Equal(t, http.StatusBadRequest, w.Code) 305 } 306 result := get(t, userID) 307 assert.Equal(t, config, result.Config) 308 } 309 310 // Posting a config with an invalid rule format version returns an error and leaves the config as is. 311 func Test_PostConfig_InvalidChangedConfig_InvalidRuleFormat(t *testing.T) { 312 setup(t) 313 defer cleanup(t) 314 315 userID := makeUserID() 316 config := makeRulerConfig(configs.RuleFormatV2) 317 post(t, userID, configs.RulesConfig{}, config) 318 319 // We have to provide the marshaled JSON manually here because json.Marshal() would error 320 // on a bad rule format version. 321 reader := strings.NewReader(`{"old_config":{"format_version":"1","files":null},"new_config":{"format_version":"<unknown>","files":{"filename.rules":"# Empty."}}}`) 322 { 323 w := requestAsUser(t, app, userID, "POST", endpoint, reader) 324 require.Equal(t, http.StatusBadRequest, w.Code) 325 } 326 result := get(t, userID) 327 assert.Equal(t, config, result.Config) 328 } 329 330 // Different users can have different configurations. 331 func Test_PostConfig_MultipleUsers(t *testing.T) { 332 setup(t) 333 defer cleanup(t) 334 335 userID1 := makeUserID() 336 userID2 := makeUserID() 337 config1 := post(t, userID1, configs.RulesConfig{}, makeRulerConfig(configs.RuleFormatV2)) 338 config2 := post(t, userID2, configs.RulesConfig{}, makeRulerConfig(configs.RuleFormatV2)) 339 foundConfig1 := get(t, userID1) 340 assert.Equal(t, config1, foundConfig1) 341 foundConfig2 := get(t, userID2) 342 assert.Equal(t, config2, foundConfig2) 343 assert.True(t, config2.ID > config1.ID, "%v > %v", config2.ID, config1.ID) 344 } 345 346 // GetAllConfigs returns an empty list of configs if there aren't any. 347 func Test_GetAllConfigs_Empty(t *testing.T) { 348 setup(t) 349 defer cleanup(t) 350 351 configs, err := privateAPI.GetRules(context.Background(), 0) 352 assert.NoError(t, err, "error getting configs") 353 assert.Equal(t, 0, len(configs)) 354 } 355 356 // GetAllConfigs returns all created configs. 357 func Test_GetAllConfigs(t *testing.T) { 358 setup(t) 359 defer cleanup(t) 360 361 userID := makeUserID() 362 config := makeRulerConfig(configs.RuleFormatV2) 363 view := post(t, userID, configs.RulesConfig{}, config) 364 365 found, err := privateAPI.GetRules(context.Background(), 0) 366 assert.NoError(t, err, "error getting configs") 367 assert.Equal(t, map[string]configs.VersionedRulesConfig{ 368 userID: view, 369 }, found) 370 } 371 372 // GetAllConfigs returns the *newest* versions of all created configs. 373 func Test_GetAllConfigs_Newest(t *testing.T) { 374 setup(t) 375 defer cleanup(t) 376 377 userID := makeUserID() 378 379 config1 := post(t, userID, configs.RulesConfig{}, makeRulerConfig(configs.RuleFormatV2)) 380 config2 := post(t, userID, config1.Config, makeRulerConfig(configs.RuleFormatV2)) 381 lastCreated := post(t, userID, config2.Config, makeRulerConfig(configs.RuleFormatV2)) 382 383 found, err := privateAPI.GetRules(context.Background(), 0) 384 assert.NoError(t, err, "error getting configs") 385 assert.Equal(t, map[string]configs.VersionedRulesConfig{ 386 userID: lastCreated, 387 }, found) 388 } 389 390 func Test_GetConfigs_IncludesNewerConfigsAndExcludesOlder(t *testing.T) { 391 setup(t) 392 defer cleanup(t) 393 394 post(t, makeUserID(), configs.RulesConfig{}, makeRulerConfig(configs.RuleFormatV2)) 395 config2 := post(t, makeUserID(), configs.RulesConfig{}, makeRulerConfig(configs.RuleFormatV2)) 396 userID3 := makeUserID() 397 config3 := post(t, userID3, configs.RulesConfig{}, makeRulerConfig(configs.RuleFormatV2)) 398 399 found, err := privateAPI.GetRules(context.Background(), config2.ID) 400 assert.NoError(t, err, "error getting configs") 401 assert.Equal(t, map[string]configs.VersionedRulesConfig{ 402 userID3: config3, 403 }, found) 404 } 405 406 // postAlertmanagerConfig posts an alertmanager config to the alertmanager configs API. 407 func postAlertmanagerConfig(t *testing.T, userID, configFile string) { 408 config := configs.Config{ 409 AlertmanagerConfig: configFile, 410 RulesConfig: configs.RulesConfig{}, 411 } 412 b, err := json.Marshal(config) 413 require.NoError(t, err) 414 reader := bytes.NewReader(b) 415 configsAPI := api.New(database) 416 w := requestAsUser(t, configsAPI, userID, "POST", "/api/prom/configs/alertmanager", reader) 417 require.Equal(t, http.StatusNoContent, w.Code) 418 } 419 420 // getAlertmanagerConfig posts an alertmanager config to the alertmanager configs API. 421 func getAlertmanagerConfig(t *testing.T, userID string) string { 422 w := requestAsUser(t, api.New(database), userID, "GET", "/api/prom/configs/alertmanager", nil) 423 var x configs.View 424 b := w.Body.Bytes() 425 err := json.Unmarshal(b, &x) 426 require.NoError(t, err, "Could not unmarshal JSON: %v", string(b)) 427 return x.Config.AlertmanagerConfig 428 } 429 430 // If a user has only got alertmanager config set, then we learn nothing about them via GetConfigs. 431 func Test_AlertmanagerConfig_NotInAllConfigs(t *testing.T) { 432 setup(t) 433 defer cleanup(t) 434 435 config := makeString(` 436 # Config no. %d. 437 route: 438 receiver: noop 439 440 receivers: 441 - name: noop`) 442 postAlertmanagerConfig(t, makeUserID(), config) 443 444 found, err := privateAPI.GetRules(context.Background(), 0) 445 assert.NoError(t, err, "error getting configs") 446 assert.Equal(t, map[string]configs.VersionedRulesConfig{}, found) 447 } 448 449 // Setting a ruler config doesn't change alertmanager config. 450 func Test_AlertmanagerConfig_RulerConfigDoesntChangeIt(t *testing.T) { 451 setup(t) 452 defer cleanup(t) 453 454 userID := makeUserID() 455 alertmanagerConfig := makeString(` 456 # Config no. %d. 457 route: 458 receiver: noop 459 460 receivers: 461 - name: noop`) 462 postAlertmanagerConfig(t, userID, alertmanagerConfig) 463 464 rulerConfig := makeRulerConfig(configs.RuleFormatV2) 465 post(t, userID, configs.RulesConfig{}, rulerConfig) 466 467 newAlertmanagerConfig := getAlertmanagerConfig(t, userID) 468 assert.Equal(t, alertmanagerConfig, newAlertmanagerConfig) 469 }