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

     1  package bucketclient
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"sort"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/go-kit/log"
    12  	"github.com/pkg/errors"
    13  	"github.com/prometheus/common/model"
    14  	"github.com/prometheus/prometheus/pkg/rulefmt"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/thanos-io/thanos/pkg/objstore"
    18  
    19  	"github.com/cortexproject/cortex/pkg/chunk"
    20  	"github.com/cortexproject/cortex/pkg/cortexpb"
    21  	"github.com/cortexproject/cortex/pkg/ruler/rulespb"
    22  	"github.com/cortexproject/cortex/pkg/ruler/rulestore"
    23  	"github.com/cortexproject/cortex/pkg/ruler/rulestore/objectclient"
    24  )
    25  
    26  type testGroup struct {
    27  	user, namespace string
    28  	ruleGroup       rulefmt.RuleGroup
    29  }
    30  
    31  func TestListRules(t *testing.T) {
    32  	runForEachRuleStore(t, func(t *testing.T, rs rulestore.RuleStore, _ interface{}) {
    33  		groups := []testGroup{
    34  			{user: "user1", namespace: "hello", ruleGroup: rulefmt.RuleGroup{Name: "first testGroup"}},
    35  			{user: "user1", namespace: "hello", ruleGroup: rulefmt.RuleGroup{Name: "second testGroup"}},
    36  			{user: "user1", namespace: "world", ruleGroup: rulefmt.RuleGroup{Name: "another namespace testGroup"}},
    37  			{user: "user2", namespace: "+-!@#$%. ", ruleGroup: rulefmt.RuleGroup{Name: "different user"}},
    38  		}
    39  
    40  		for _, g := range groups {
    41  			desc := rulespb.ToProto(g.user, g.namespace, g.ruleGroup)
    42  			require.NoError(t, rs.SetRuleGroup(context.Background(), g.user, g.namespace, desc))
    43  		}
    44  
    45  		{
    46  			users, err := rs.ListAllUsers(context.Background())
    47  			require.NoError(t, err)
    48  			require.ElementsMatch(t, []string{"user1", "user2"}, users)
    49  		}
    50  
    51  		{
    52  			allGroupsMap, err := rs.ListAllRuleGroups(context.Background())
    53  			require.NoError(t, err)
    54  			require.Len(t, allGroupsMap, 2)
    55  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
    56  				{User: "user1", Namespace: "hello", Name: "first testGroup"},
    57  				{User: "user1", Namespace: "hello", Name: "second testGroup"},
    58  				{User: "user1", Namespace: "world", Name: "another namespace testGroup"},
    59  			}, allGroupsMap["user1"])
    60  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
    61  				{User: "user2", Namespace: "+-!@#$%. ", Name: "different user"},
    62  			}, allGroupsMap["user2"])
    63  		}
    64  
    65  		{
    66  			user1Groups, err := rs.ListRuleGroupsForUserAndNamespace(context.Background(), "user1", "")
    67  			require.NoError(t, err)
    68  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
    69  				{User: "user1", Namespace: "hello", Name: "first testGroup"},
    70  				{User: "user1", Namespace: "hello", Name: "second testGroup"},
    71  				{User: "user1", Namespace: "world", Name: "another namespace testGroup"},
    72  			}, user1Groups)
    73  		}
    74  
    75  		{
    76  			helloGroups, err := rs.ListRuleGroupsForUserAndNamespace(context.Background(), "user1", "hello")
    77  			require.NoError(t, err)
    78  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
    79  				{User: "user1", Namespace: "hello", Name: "first testGroup"},
    80  				{User: "user1", Namespace: "hello", Name: "second testGroup"},
    81  			}, helloGroups)
    82  		}
    83  
    84  		{
    85  			invalidUserGroups, err := rs.ListRuleGroupsForUserAndNamespace(context.Background(), "invalid", "")
    86  			require.NoError(t, err)
    87  			require.Empty(t, invalidUserGroups)
    88  		}
    89  
    90  		{
    91  			invalidNamespaceGroups, err := rs.ListRuleGroupsForUserAndNamespace(context.Background(), "user1", "invalid")
    92  			require.NoError(t, err)
    93  			require.Empty(t, invalidNamespaceGroups)
    94  		}
    95  
    96  		{
    97  			user2Groups, err := rs.ListRuleGroupsForUserAndNamespace(context.Background(), "user2", "")
    98  			require.NoError(t, err)
    99  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
   100  				{User: "user2", Namespace: "+-!@#$%. ", Name: "different user"},
   101  			}, user2Groups)
   102  		}
   103  	})
   104  }
   105  
   106  func TestLoadRules(t *testing.T) {
   107  	runForEachRuleStore(t, func(t *testing.T, rs rulestore.RuleStore, _ interface{}) {
   108  		groups := []testGroup{
   109  			{user: "user1", namespace: "hello", ruleGroup: rulefmt.RuleGroup{Name: "first testGroup", Interval: model.Duration(time.Minute), Rules: []rulefmt.RuleNode{{
   110  				For:    model.Duration(5 * time.Minute),
   111  				Labels: map[string]string{"label1": "value1"},
   112  			}}}},
   113  			{user: "user1", namespace: "hello", ruleGroup: rulefmt.RuleGroup{Name: "second testGroup", Interval: model.Duration(2 * time.Minute)}},
   114  			{user: "user1", namespace: "world", ruleGroup: rulefmt.RuleGroup{Name: "another namespace testGroup", Interval: model.Duration(1 * time.Hour)}},
   115  			{user: "user2", namespace: "+-!@#$%. ", ruleGroup: rulefmt.RuleGroup{Name: "different user", Interval: model.Duration(5 * time.Minute)}},
   116  		}
   117  
   118  		for _, g := range groups {
   119  			desc := rulespb.ToProto(g.user, g.namespace, g.ruleGroup)
   120  			require.NoError(t, rs.SetRuleGroup(context.Background(), g.user, g.namespace, desc))
   121  		}
   122  
   123  		allGroupsMap, err := rs.ListAllRuleGroups(context.Background())
   124  
   125  		// Before load, rules are not loaded
   126  		{
   127  			require.NoError(t, err)
   128  			require.Len(t, allGroupsMap, 2)
   129  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
   130  				{User: "user1", Namespace: "hello", Name: "first testGroup"},
   131  				{User: "user1", Namespace: "hello", Name: "second testGroup"},
   132  				{User: "user1", Namespace: "world", Name: "another namespace testGroup"},
   133  			}, allGroupsMap["user1"])
   134  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
   135  				{User: "user2", Namespace: "+-!@#$%. ", Name: "different user"},
   136  			}, allGroupsMap["user2"])
   137  		}
   138  
   139  		err = rs.LoadRuleGroups(context.Background(), allGroupsMap)
   140  		require.NoError(t, err)
   141  
   142  		// After load, rules are loaded.
   143  		{
   144  			require.NoError(t, err)
   145  			require.Len(t, allGroupsMap, 2)
   146  
   147  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
   148  				{User: "user1", Namespace: "hello", Name: "first testGroup", Interval: time.Minute, Rules: []*rulespb.RuleDesc{
   149  					{
   150  						For:    5 * time.Minute,
   151  						Labels: []cortexpb.LabelAdapter{{Name: "label1", Value: "value1"}},
   152  					},
   153  				}},
   154  				{User: "user1", Namespace: "hello", Name: "second testGroup", Interval: 2 * time.Minute},
   155  				{User: "user1", Namespace: "world", Name: "another namespace testGroup", Interval: 1 * time.Hour},
   156  			}, allGroupsMap["user1"])
   157  
   158  			require.ElementsMatch(t, []*rulespb.RuleGroupDesc{
   159  				{User: "user2", Namespace: "+-!@#$%. ", Name: "different user", Interval: 5 * time.Minute},
   160  			}, allGroupsMap["user2"])
   161  		}
   162  
   163  		// Loading group with mismatched info fails.
   164  		require.NoError(t, rs.SetRuleGroup(context.Background(), "user1", "hello", &rulespb.RuleGroupDesc{User: "user2", Namespace: "world", Name: "first testGroup"}))
   165  		require.EqualError(t, rs.LoadRuleGroups(context.Background(), allGroupsMap), "mismatch between requested rule group and loaded rule group, requested: user=\"user1\", namespace=\"hello\", group=\"first testGroup\", loaded: user=\"user2\", namespace=\"world\", group=\"first testGroup\"")
   166  
   167  		// Load with missing rule groups fails.
   168  		require.NoError(t, rs.DeleteRuleGroup(context.Background(), "user1", "hello", "first testGroup"))
   169  		require.EqualError(t, rs.LoadRuleGroups(context.Background(), allGroupsMap), "get rule group user=\"user2\", namespace=\"world\", name=\"first testGroup\": group does not exist")
   170  	})
   171  }
   172  
   173  func TestDelete(t *testing.T) {
   174  	runForEachRuleStore(t, func(t *testing.T, rs rulestore.RuleStore, bucketClient interface{}) {
   175  		groups := []testGroup{
   176  			{user: "user1", namespace: "A", ruleGroup: rulefmt.RuleGroup{Name: "1"}},
   177  			{user: "user1", namespace: "A", ruleGroup: rulefmt.RuleGroup{Name: "2"}},
   178  			{user: "user1", namespace: "B", ruleGroup: rulefmt.RuleGroup{Name: "3"}},
   179  			{user: "user1", namespace: "C", ruleGroup: rulefmt.RuleGroup{Name: "4"}},
   180  			{user: "user2", namespace: "second", ruleGroup: rulefmt.RuleGroup{Name: "group"}},
   181  			{user: "user3", namespace: "third", ruleGroup: rulefmt.RuleGroup{Name: "group"}},
   182  		}
   183  
   184  		for _, g := range groups {
   185  			desc := rulespb.ToProto(g.user, g.namespace, g.ruleGroup)
   186  			require.NoError(t, rs.SetRuleGroup(context.Background(), g.user, g.namespace, desc))
   187  		}
   188  
   189  		// Verify that nothing was deleted, because we used canceled context.
   190  		{
   191  			canceled, cancelFn := context.WithCancel(context.Background())
   192  			cancelFn()
   193  
   194  			require.Error(t, rs.DeleteNamespace(canceled, "user1", ""))
   195  
   196  			require.Equal(t, []string{
   197  				"rules/user1/" + getRuleGroupObjectKey("A", "1"),
   198  				"rules/user1/" + getRuleGroupObjectKey("A", "2"),
   199  				"rules/user1/" + getRuleGroupObjectKey("B", "3"),
   200  				"rules/user1/" + getRuleGroupObjectKey("C", "4"),
   201  				"rules/user2/" + getRuleGroupObjectKey("second", "group"),
   202  				"rules/user3/" + getRuleGroupObjectKey("third", "group"),
   203  			}, getSortedObjectKeys(bucketClient))
   204  		}
   205  
   206  		// Verify that we can delete individual rule group, or entire namespace.
   207  		{
   208  			require.NoError(t, rs.DeleteRuleGroup(context.Background(), "user2", "second", "group"))
   209  			require.NoError(t, rs.DeleteNamespace(context.Background(), "user1", "A"))
   210  
   211  			require.Equal(t, []string{
   212  				"rules/user1/" + getRuleGroupObjectKey("B", "3"),
   213  				"rules/user1/" + getRuleGroupObjectKey("C", "4"),
   214  				"rules/user3/" + getRuleGroupObjectKey("third", "group"),
   215  			}, getSortedObjectKeys(bucketClient))
   216  		}
   217  
   218  		// Verify that we can delete all remaining namespaces for user1.
   219  		{
   220  			require.NoError(t, rs.DeleteNamespace(context.Background(), "user1", ""))
   221  
   222  			require.Equal(t, []string{
   223  				"rules/user3/" + getRuleGroupObjectKey("third", "group"),
   224  			}, getSortedObjectKeys(bucketClient))
   225  		}
   226  
   227  		{
   228  			// Trying to delete empty namespace again will result in error.
   229  			require.Equal(t, rulestore.ErrGroupNamespaceNotFound, rs.DeleteNamespace(context.Background(), "user1", ""))
   230  		}
   231  	})
   232  }
   233  
   234  func runForEachRuleStore(t *testing.T, testFn func(t *testing.T, store rulestore.RuleStore, bucketClient interface{})) {
   235  	legacyClient := chunk.NewMockStorage()
   236  	legacyStore := objectclient.NewRuleStore(legacyClient, 5, log.NewNopLogger())
   237  
   238  	bucketClient := objstore.NewInMemBucket()
   239  	bucketStore := NewBucketRuleStore(bucketClient, nil, log.NewNopLogger())
   240  
   241  	stores := map[string]struct {
   242  		store  rulestore.RuleStore
   243  		client interface{}
   244  	}{
   245  		"legacy": {store: legacyStore, client: legacyClient},
   246  		"bucket": {store: bucketStore, client: bucketClient},
   247  	}
   248  
   249  	for name, data := range stores {
   250  		t.Run(name, func(t *testing.T) {
   251  			testFn(t, data.store, data.client)
   252  		})
   253  	}
   254  }
   255  
   256  func getSortedObjectKeys(bucketClient interface{}) []string {
   257  	if typed, ok := bucketClient.(*chunk.MockStorage); ok {
   258  		return typed.GetSortedObjectKeys()
   259  	}
   260  
   261  	if typed, ok := bucketClient.(*objstore.InMemBucket); ok {
   262  		var keys []string
   263  		for key := range typed.Objects() {
   264  			keys = append(keys, key)
   265  		}
   266  		sort.Strings(keys)
   267  		return keys
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  func TestParseRuleGroupObjectKey(t *testing.T) {
   274  	decodedNamespace := "my-namespace"
   275  	encodedNamespace := base64.URLEncoding.EncodeToString([]byte(decodedNamespace))
   276  
   277  	decodedGroup := "my-group"
   278  	encodedGroup := base64.URLEncoding.EncodeToString([]byte(decodedGroup))
   279  
   280  	tests := map[string]struct {
   281  		key               string
   282  		expectedErr       error
   283  		expectedNamespace string
   284  		expectedGroup     string
   285  	}{
   286  		"empty object key": {
   287  			key:         "",
   288  			expectedErr: errInvalidRuleGroupKey,
   289  		},
   290  		"invalid object key pattern": {
   291  			key:         "way/too/long",
   292  			expectedErr: errInvalidRuleGroupKey,
   293  		},
   294  		"empty namespace": {
   295  			key:         fmt.Sprintf("/%s", encodedGroup),
   296  			expectedErr: errEmptyNamespace,
   297  		},
   298  		"invalid namespace encoding": {
   299  			key:         fmt.Sprintf("invalid/%s", encodedGroup),
   300  			expectedErr: errors.New("illegal base64 data at input byte 4"),
   301  		},
   302  		"empty group": {
   303  			key:         fmt.Sprintf("%s/", encodedNamespace),
   304  			expectedErr: errEmptyGroupName,
   305  		},
   306  		"invalid group encoding": {
   307  			key:         fmt.Sprintf("%s/invalid", encodedNamespace),
   308  			expectedErr: errors.New("illegal base64 data at input byte 4"),
   309  		},
   310  		"valid object key": {
   311  			key:               fmt.Sprintf("%s/%s", encodedNamespace, encodedGroup),
   312  			expectedNamespace: decodedNamespace,
   313  			expectedGroup:     decodedGroup,
   314  		},
   315  	}
   316  
   317  	for testName, testData := range tests {
   318  		t.Run(testName, func(t *testing.T) {
   319  			namespace, group, err := parseRuleGroupObjectKey(testData.key)
   320  
   321  			if testData.expectedErr != nil {
   322  				assert.EqualError(t, err, testData.expectedErr.Error())
   323  			} else {
   324  				require.NoError(t, err)
   325  				assert.Equal(t, testData.expectedNamespace, namespace)
   326  				assert.Equal(t, testData.expectedGroup, group)
   327  			}
   328  		})
   329  	}
   330  }
   331  
   332  func TestParseRuleGroupObjectKeyWithUser(t *testing.T) {
   333  	decodedNamespace := "my-namespace"
   334  	encodedNamespace := base64.URLEncoding.EncodeToString([]byte(decodedNamespace))
   335  
   336  	decodedGroup := "my-group"
   337  	encodedGroup := base64.URLEncoding.EncodeToString([]byte(decodedGroup))
   338  
   339  	tests := map[string]struct {
   340  		key               string
   341  		expectedErr       error
   342  		expectedUser      string
   343  		expectedNamespace string
   344  		expectedGroup     string
   345  	}{
   346  		"empty object key": {
   347  			key:         "",
   348  			expectedErr: errInvalidRuleGroupKey,
   349  		},
   350  		"invalid object key pattern": {
   351  			key:         "way/too/much/long",
   352  			expectedErr: errInvalidRuleGroupKey,
   353  		},
   354  		"empty user": {
   355  			key:         fmt.Sprintf("/%s/%s", encodedNamespace, encodedGroup),
   356  			expectedErr: errEmptyUser,
   357  		},
   358  		"empty namespace": {
   359  			key:         fmt.Sprintf("user-1//%s", encodedGroup),
   360  			expectedErr: errEmptyNamespace,
   361  		},
   362  		"invalid namespace encoding": {
   363  			key:         fmt.Sprintf("user-1/invalid/%s", encodedGroup),
   364  			expectedErr: errors.New("illegal base64 data at input byte 4"),
   365  		},
   366  		"empty group name": {
   367  			key:         fmt.Sprintf("user-1/%s/", encodedNamespace),
   368  			expectedErr: errEmptyGroupName,
   369  		},
   370  		"invalid group encoding": {
   371  			key:         fmt.Sprintf("user-1/%s/invalid", encodedNamespace),
   372  			expectedErr: errors.New("illegal base64 data at input byte 4"),
   373  		},
   374  		"valid object key": {
   375  			key:               fmt.Sprintf("user-1/%s/%s", encodedNamespace, encodedGroup),
   376  			expectedUser:      "user-1",
   377  			expectedNamespace: decodedNamespace,
   378  			expectedGroup:     decodedGroup,
   379  		},
   380  	}
   381  
   382  	for testName, testData := range tests {
   383  		t.Run(testName, func(t *testing.T) {
   384  			user, namespace, group, err := parseRuleGroupObjectKeyWithUser(testData.key)
   385  
   386  			if testData.expectedErr != nil {
   387  				assert.EqualError(t, err, testData.expectedErr.Error())
   388  			} else {
   389  				require.NoError(t, err)
   390  				assert.Equal(t, testData.expectedUser, user)
   391  				assert.Equal(t, testData.expectedNamespace, namespace)
   392  				assert.Equal(t, testData.expectedGroup, group)
   393  			}
   394  		})
   395  	}
   396  }
   397  
   398  func TestListAllRuleGroupsWithNoNamespaceOrGroup(t *testing.T) {
   399  	obj := mockBucket{
   400  		names: []string{
   401  			"rules/",
   402  			"rules/user1/",
   403  			"rules/user2/bnM=/",         // namespace "ns", ends with '/'
   404  			"rules/user3/bnM=/Z3JvdXAx", // namespace "ns", group "group1"
   405  		},
   406  	}
   407  
   408  	s := NewBucketRuleStore(obj, nil, log.NewNopLogger())
   409  	out, err := s.ListAllRuleGroups(context.Background())
   410  	require.NoError(t, err)
   411  
   412  	require.Equal(t, 1, len(out))                    // one user
   413  	require.Equal(t, 1, len(out["user3"]))           // one group
   414  	require.Equal(t, "group1", out["user3"][0].Name) // one group
   415  }
   416  
   417  type mockBucket struct {
   418  	objstore.Bucket
   419  
   420  	names []string
   421  }
   422  
   423  func (mb mockBucket) Iter(_ context.Context, dir string, f func(string) error, options ...objstore.IterOption) error {
   424  	for _, n := range mb.names {
   425  		if err := f(n); err != nil {
   426  			return err
   427  		}
   428  	}
   429  	return nil
   430  }