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 }