github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/ruler/rulestore/objectclient/rule_store.go (about) 1 package objectclient 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/base64" 7 "fmt" 8 "io/ioutil" 9 "strings" 10 11 "github.com/go-kit/log" 12 "github.com/go-kit/log/level" 13 "github.com/gogo/protobuf/proto" 14 "github.com/pkg/errors" 15 "golang.org/x/sync/errgroup" 16 17 "github.com/cortexproject/cortex/pkg/chunk" 18 "github.com/cortexproject/cortex/pkg/ruler/rulespb" 19 "github.com/cortexproject/cortex/pkg/ruler/rulestore" 20 ) 21 22 // Object Rule Storage Schema 23 // ======================= 24 // Object Name: "rules/<user_id>/<base64 URL Encoded: namespace>/<base64 URL Encoded: group_name>" 25 // Storage Format: Encoded RuleGroupDesc 26 // 27 // Prometheus Rule Groups can include a large number of characters that are not valid object names 28 // in common object storage systems. A URL Base64 encoding allows for generic consistent naming 29 // across all backends 30 31 const ( 32 delim = "/" 33 rulePrefix = "rules" + delim 34 ) 35 36 // RuleStore allows cortex rules to be stored using an object store backend. 37 type RuleStore struct { 38 client chunk.ObjectClient 39 loadConcurrency int 40 41 logger log.Logger 42 } 43 44 // NewRuleStore returns a new RuleStore 45 func NewRuleStore(client chunk.ObjectClient, loadConcurrency int, logger log.Logger) *RuleStore { 46 return &RuleStore{ 47 client: client, 48 loadConcurrency: loadConcurrency, 49 logger: logger, 50 } 51 } 52 53 // If existing rule group is supplied, it is Reset and reused. If nil, new RuleGroupDesc is allocated. 54 func (o *RuleStore) getRuleGroup(ctx context.Context, objectKey string, rg *rulespb.RuleGroupDesc) (*rulespb.RuleGroupDesc, error) { 55 reader, err := o.client.GetObject(ctx, objectKey) 56 if err == chunk.ErrStorageObjectNotFound { 57 level.Debug(o.logger).Log("msg", "rule group does not exist", "name", objectKey) 58 return nil, errors.Wrapf(rulestore.ErrGroupNotFound, "get rule group user=%q, namespace=%q, name=%q", rg.GetUser(), rg.GetNamespace(), rg.GetName()) 59 } 60 61 if err != nil { 62 return nil, errors.Wrapf(err, "failed to get rule group %s", objectKey) 63 } 64 defer func() { _ = reader.Close() }() 65 66 buf, err := ioutil.ReadAll(reader) 67 if err != nil { 68 return nil, errors.Wrapf(err, "failed to read rule group %s", objectKey) 69 } 70 71 if rg == nil { 72 rg = &rulespb.RuleGroupDesc{} 73 } else { 74 rg.Reset() 75 } 76 77 err = proto.Unmarshal(buf, rg) 78 if err != nil { 79 return nil, errors.Wrapf(err, "failed to unmarshal rule group %s", objectKey) 80 } 81 82 return rg, nil 83 } 84 85 func (o *RuleStore) ListAllUsers(ctx context.Context) ([]string, error) { 86 _, prefixes, err := o.client.List(ctx, rulePrefix, delim) 87 if err != nil { 88 return nil, err 89 } 90 91 var result []string 92 for _, p := range prefixes { 93 s := string(p) 94 95 s = strings.TrimPrefix(s, rulePrefix) 96 s = strings.TrimSuffix(s, delim) 97 98 if s != "" { 99 result = append(result, s) 100 } 101 } 102 103 return result, nil 104 } 105 106 // ListAllRuleGroups implements rules.RuleStore. 107 func (o *RuleStore) ListAllRuleGroups(ctx context.Context) (map[string]rulespb.RuleGroupList, error) { 108 // No delimiter to get *all* rule groups for all users and namespaces. 109 ruleGroupObjects, _, err := o.client.List(ctx, rulePrefix, "") 110 if err != nil { 111 return nil, err 112 } 113 114 return convertRuleGroupObjectsToMap(ruleGroupObjects), nil 115 } 116 117 func (o *RuleStore) ListRuleGroupsForUserAndNamespace(ctx context.Context, userID, namespace string) (rulespb.RuleGroupList, error) { 118 ruleGroupObjects, _, err := o.client.List(ctx, generateRuleObjectKey(userID, namespace, ""), "") 119 if err != nil { 120 return nil, err 121 } 122 123 return convertRuleGroupObjectsToMap(ruleGroupObjects)[userID], nil 124 } 125 126 func (o *RuleStore) LoadRuleGroups(ctx context.Context, groupsToLoad map[string]rulespb.RuleGroupList) error { 127 ch := make(chan *rulespb.RuleGroupDesc) 128 129 // Given we store one file per rule group. With this, we create a pool of workers that will 130 // download all rule groups in parallel. We limit the number of workers to avoid a 131 // particular user having too many rule groups rate limiting us with the object storage. 132 g, gCtx := errgroup.WithContext(ctx) 133 for i := 0; i < o.loadConcurrency; i++ { 134 g.Go(func() error { 135 for gr := range ch { 136 if gr == nil { 137 continue 138 } 139 140 user, namespace, group := gr.GetUser(), gr.GetNamespace(), gr.GetName() 141 if user == "" || namespace == "" || group == "" { 142 return fmt.Errorf("invalid rule group: user=%q, namespace=%q, group=%q", user, namespace, group) 143 } 144 145 key := generateRuleObjectKey(user, namespace, group) 146 147 level.Debug(o.logger).Log("msg", "loading rule group", "key", key, "user", user) 148 gr, err := o.getRuleGroup(gCtx, key, gr) // reuse group pointer from the map. 149 if err != nil { 150 level.Error(o.logger).Log("msg", "failed to get rule group", "key", key, "user", user) 151 return err 152 } 153 154 if user != gr.User || namespace != gr.Namespace || group != gr.Name { 155 return fmt.Errorf("mismatch between requested rule group and loaded rule group, requested: user=%q, namespace=%q, group=%q, loaded: user=%q, namespace=%q, group=%q", user, namespace, group, gr.User, gr.Namespace, gr.Name) 156 } 157 } 158 159 return nil 160 }) 161 } 162 163 outer: 164 for _, gs := range groupsToLoad { 165 for _, g := range gs { 166 select { 167 case <-gCtx.Done(): 168 break outer 169 case ch <- g: 170 // ok 171 } 172 } 173 } 174 close(ch) 175 176 return g.Wait() 177 } 178 179 func convertRuleGroupObjectsToMap(ruleGroupObjects []chunk.StorageObject) map[string]rulespb.RuleGroupList { 180 result := map[string]rulespb.RuleGroupList{} 181 for _, rg := range ruleGroupObjects { 182 user, namespace, group := decomposeRuleObjectKey(rg.Key) 183 if user == "" || namespace == "" || group == "" { 184 continue 185 } 186 187 result[user] = append(result[user], &rulespb.RuleGroupDesc{ 188 User: user, 189 Namespace: namespace, 190 Name: group, 191 }) 192 } 193 return result 194 } 195 196 // GetRuleGroup returns the requested rule group 197 func (o *RuleStore) GetRuleGroup(ctx context.Context, userID string, namespace string, grp string) (*rulespb.RuleGroupDesc, error) { 198 handle := generateRuleObjectKey(userID, namespace, grp) 199 return o.getRuleGroup(ctx, handle, nil) 200 } 201 202 // SetRuleGroup sets provided rule group 203 func (o *RuleStore) SetRuleGroup(ctx context.Context, userID string, namespace string, group *rulespb.RuleGroupDesc) error { 204 data, err := proto.Marshal(group) 205 if err != nil { 206 return err 207 } 208 209 objectKey := generateRuleObjectKey(userID, namespace, group.Name) 210 return o.client.PutObject(ctx, objectKey, bytes.NewReader(data)) 211 } 212 213 // DeleteRuleGroup deletes the specified rule group 214 func (o *RuleStore) DeleteRuleGroup(ctx context.Context, userID string, namespace string, groupName string) error { 215 objectKey := generateRuleObjectKey(userID, namespace, groupName) 216 err := o.client.DeleteObject(ctx, objectKey) 217 if err == chunk.ErrStorageObjectNotFound { 218 return rulestore.ErrGroupNotFound 219 } 220 return err 221 } 222 223 // DeleteNamespace deletes all the rule groups in the specified namespace 224 func (o *RuleStore) DeleteNamespace(ctx context.Context, userID, namespace string) error { 225 ruleGroupObjects, _, err := o.client.List(ctx, generateRuleObjectKey(userID, namespace, ""), "") 226 if err != nil { 227 return err 228 } 229 230 if len(ruleGroupObjects) == 0 { 231 return rulestore.ErrGroupNamespaceNotFound 232 } 233 234 for _, obj := range ruleGroupObjects { 235 if err := ctx.Err(); err != nil { 236 return err 237 } 238 239 level.Debug(o.logger).Log("msg", "deleting rule group", "namespace", namespace, "key", obj.Key) 240 err = o.client.DeleteObject(ctx, obj.Key) 241 if err != nil { 242 level.Error(o.logger).Log("msg", "unable to delete rule group from namespace", "err", err, "namespace", namespace, "key", obj.Key) 243 return err 244 } 245 } 246 247 return nil 248 } 249 250 func generateRuleObjectKey(userID, namespace, groupName string) string { 251 if userID == "" { 252 return rulePrefix 253 } 254 255 prefix := rulePrefix + userID + delim 256 if namespace == "" { 257 return prefix 258 } 259 260 ns := base64.URLEncoding.EncodeToString([]byte(namespace)) + delim 261 if groupName == "" { 262 return prefix + ns 263 } 264 265 return prefix + ns + base64.URLEncoding.EncodeToString([]byte(groupName)) 266 } 267 268 func decomposeRuleObjectKey(objectKey string) (userID, namespace, groupName string) { 269 if !strings.HasPrefix(objectKey, rulePrefix) { 270 return 271 } 272 273 components := strings.Split(objectKey, delim) 274 if len(components) != 4 { 275 return 276 } 277 278 ns, err := base64.URLEncoding.DecodeString(components[2]) 279 if err != nil { 280 return 281 } 282 283 gr, err := base64.URLEncoding.DecodeString(components[3]) 284 if err != nil { 285 return 286 } 287 288 return components[1], string(ns), string(gr) 289 }