github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/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/model/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/grafana/loki/pkg/logproto" 20 "github.com/grafana/loki/pkg/ruler/rulespb" 21 "github.com/grafana/loki/pkg/ruler/rulestore" 22 "github.com/grafana/loki/pkg/ruler/rulestore/objectclient" 23 "github.com/grafana/loki/pkg/storage/chunk/client/testutils" 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: []logproto.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 := testutils.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 testutils.ResetMockStorage() 252 testFn(t, data.store, data.client) 253 }) 254 } 255 } 256 257 func getSortedObjectKeys(bucketClient interface{}) []string { 258 if typed, ok := bucketClient.(*testutils.MockStorage); ok { 259 return typed.GetSortedObjectKeys() 260 } 261 262 if typed, ok := bucketClient.(*objstore.InMemBucket); ok { 263 var keys []string 264 for key := range typed.Objects() { 265 keys = append(keys, key) 266 } 267 sort.Strings(keys) 268 return keys 269 } 270 271 return nil 272 } 273 274 func TestParseRuleGroupObjectKey(t *testing.T) { 275 decodedNamespace := "my-namespace" 276 encodedNamespace := base64.URLEncoding.EncodeToString([]byte(decodedNamespace)) 277 278 decodedGroup := "my-group" 279 encodedGroup := base64.URLEncoding.EncodeToString([]byte(decodedGroup)) 280 281 tests := map[string]struct { 282 key string 283 expectedErr error 284 expectedNamespace string 285 expectedGroup string 286 }{ 287 "empty object key": { 288 key: "", 289 expectedErr: errInvalidRuleGroupKey, 290 }, 291 "invalid object key pattern": { 292 key: "way/too/long", 293 expectedErr: errInvalidRuleGroupKey, 294 }, 295 "empty namespace": { 296 key: fmt.Sprintf("/%s", encodedGroup), 297 expectedErr: errEmptyNamespace, 298 }, 299 "invalid namespace encoding": { 300 key: fmt.Sprintf("invalid/%s", encodedGroup), 301 expectedErr: errors.New("illegal base64 data at input byte 4"), 302 }, 303 "empty group": { 304 key: fmt.Sprintf("%s/", encodedNamespace), 305 expectedErr: errEmptyGroupName, 306 }, 307 "invalid group encoding": { 308 key: fmt.Sprintf("%s/invalid", encodedNamespace), 309 expectedErr: errors.New("illegal base64 data at input byte 4"), 310 }, 311 "valid object key": { 312 key: fmt.Sprintf("%s/%s", encodedNamespace, encodedGroup), 313 expectedNamespace: decodedNamespace, 314 expectedGroup: decodedGroup, 315 }, 316 } 317 318 for testName, testData := range tests { 319 t.Run(testName, func(t *testing.T) { 320 namespace, group, err := parseRuleGroupObjectKey(testData.key) 321 322 if testData.expectedErr != nil { 323 assert.EqualError(t, err, testData.expectedErr.Error()) 324 } else { 325 require.NoError(t, err) 326 assert.Equal(t, testData.expectedNamespace, namespace) 327 assert.Equal(t, testData.expectedGroup, group) 328 } 329 }) 330 } 331 } 332 333 func TestParseRuleGroupObjectKeyWithUser(t *testing.T) { 334 decodedNamespace := "my-namespace" 335 encodedNamespace := base64.URLEncoding.EncodeToString([]byte(decodedNamespace)) 336 337 decodedGroup := "my-group" 338 encodedGroup := base64.URLEncoding.EncodeToString([]byte(decodedGroup)) 339 340 tests := map[string]struct { 341 key string 342 expectedErr error 343 expectedUser string 344 expectedNamespace string 345 expectedGroup string 346 }{ 347 "empty object key": { 348 key: "", 349 expectedErr: errInvalidRuleGroupKey, 350 }, 351 "invalid object key pattern": { 352 key: "way/too/much/long", 353 expectedErr: errInvalidRuleGroupKey, 354 }, 355 "empty user": { 356 key: fmt.Sprintf("/%s/%s", encodedNamespace, encodedGroup), 357 expectedErr: errEmptyUser, 358 }, 359 "empty namespace": { 360 key: fmt.Sprintf("user-1//%s", encodedGroup), 361 expectedErr: errEmptyNamespace, 362 }, 363 "invalid namespace encoding": { 364 key: fmt.Sprintf("user-1/invalid/%s", encodedGroup), 365 expectedErr: errors.New("illegal base64 data at input byte 4"), 366 }, 367 "empty group name": { 368 key: fmt.Sprintf("user-1/%s/", encodedNamespace), 369 expectedErr: errEmptyGroupName, 370 }, 371 "invalid group encoding": { 372 key: fmt.Sprintf("user-1/%s/invalid", encodedNamespace), 373 expectedErr: errors.New("illegal base64 data at input byte 4"), 374 }, 375 "valid object key": { 376 key: fmt.Sprintf("user-1/%s/%s", encodedNamespace, encodedGroup), 377 expectedUser: "user-1", 378 expectedNamespace: decodedNamespace, 379 expectedGroup: decodedGroup, 380 }, 381 } 382 383 for testName, testData := range tests { 384 t.Run(testName, func(t *testing.T) { 385 user, namespace, group, err := parseRuleGroupObjectKeyWithUser(testData.key) 386 387 if testData.expectedErr != nil { 388 assert.EqualError(t, err, testData.expectedErr.Error()) 389 } else { 390 require.NoError(t, err) 391 assert.Equal(t, testData.expectedUser, user) 392 assert.Equal(t, testData.expectedNamespace, namespace) 393 assert.Equal(t, testData.expectedGroup, group) 394 } 395 }) 396 } 397 } 398 399 func TestListAllRuleGroupsWithNoNamespaceOrGroup(t *testing.T) { 400 obj := mockBucket{ 401 names: []string{ 402 "rules/", 403 "rules/user1/", 404 "rules/user2/bnM=/", // namespace "ns", ends with '/' 405 "rules/user3/bnM=/Z3JvdXAx", // namespace "ns", group "group1" 406 }, 407 } 408 409 s := NewBucketRuleStore(obj, nil, log.NewNopLogger()) 410 out, err := s.ListAllRuleGroups(context.Background()) 411 require.NoError(t, err) 412 413 require.Equal(t, 1, len(out)) // one user 414 require.Equal(t, 1, len(out["user3"])) // one group 415 require.Equal(t, "group1", out["user3"][0].Name) // one group 416 } 417 418 type mockBucket struct { 419 objstore.Bucket 420 421 names []string 422 } 423 424 func (mb mockBucket) Iter(_ context.Context, dir string, f func(string) error, options ...objstore.IterOption) error { 425 for _, n := range mb.names { 426 if err := f(n); err != nil { 427 return err 428 } 429 } 430 return nil 431 }