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  }