go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/quota/load_config.go (about) 1 // Copyright 2022 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package quota 16 17 import ( 18 "context" 19 "crypto/sha256" 20 "encoding/ascii85" 21 "time" 22 23 "github.com/gomodule/redigo/redis" 24 "google.golang.org/protobuf/types/known/timestamppb" 25 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/errors" 28 "go.chromium.org/luci/common/proto/msgpackpb" 29 30 "go.chromium.org/luci/server/quota/internal/quotakeys" 31 "go.chromium.org/luci/server/quota/quotapb" 32 ) 33 34 // LoadPoliciesManual ensures that the given policy config is uploaded at 35 // `version` for the given Application in `realm`. 36 // 37 // If a policy config already exists for `(cfg.id, realm, version)`, this 38 // returns immediately without checking its content. It is the application's 39 // responsibility to ensure that (namespace, version) always refers to the same 40 // `cfg` contents. 41 // 42 // Version must not contain "$" or "~". 43 func (a *Application) LoadPoliciesManual(ctx context.Context, realm string, version string, cfg *quotapb.PolicyConfig) (*quotapb.PolicyConfigID, error) { 44 cid := "apb.PolicyConfigID{ 45 AppId: a.id, 46 Realm: realm, 47 Version: version, 48 } 49 return cid, a.loadPolicies(ctx, cid, cfg) 50 } 51 52 // LoadPoliciesAuto ensures that the given policy config is ingested with 53 // a content-hash version for the given Application in `realm`. 54 // 55 // If a policy config already exists for `(cfg.id, realm, version)`, this 56 // returns immediately without checking its content. 57 // 58 // Returns the calculated version hash. 59 func (a *Application) LoadPoliciesAuto(ctx context.Context, realm string, cfg *quotapb.PolicyConfig) (cid *quotapb.PolicyConfigID, err error) { 60 h := sha256.New() 61 err = msgpackpb.MarshalStream(h, cfg, msgpackpb.Deterministic, msgpackpb.DisallowUnknownFields) 62 if err != nil { 63 return nil, errors.Annotate(err, "while computing PolicyConfig hash").Err() 64 } 65 dat := h.Sum(nil) 66 buf := make([]byte, ascii85.MaxEncodedLen(len(dat))) 67 buf = buf[:ascii85.Encode(buf, dat)] 68 69 cid = "apb.PolicyConfigID{ 70 AppId: a.id, 71 Realm: realm, 72 VersionScheme: 1, 73 Version: string(buf), 74 } 75 76 return cid, a.loadPolicies(ctx, cid, cfg) 77 } 78 79 const secondsInDay = uint32((time.Hour * 24) / time.Second) 80 81 func checkPolicy(p *quotapb.Policy) error { 82 if p.Default > p.Limit { 83 return errors.Reason("Default>Limit: %d > %d", p.Default, p.Limit).Err() 84 } 85 if r := p.Refill; r != nil { 86 if r.Interval == 0 && r.Units < 0 { 87 return errors.New("Refill: Interval=0 && Units<0") 88 } 89 if (secondsInDay % r.Interval) != 0 { 90 return errors.Reason("Interval does not cleanly divide day: %d", r.Interval).Err() 91 } 92 } 93 return nil 94 } 95 96 // loadPolicies ensures that the given policy config is loaded. 97 // 98 // If a version of the policy config already exists for this namespace+version 99 // pair, this returns immediately without checking its content. 100 // 101 // Returns the version string suitable for PolicyID and an error. If 102 // cid.VersionScheme is `Version` then this is the same value and can be ignored. 103 func (a *Application) loadPolicies(ctx context.Context, cid *quotapb.PolicyConfigID, cfg *quotapb.PolicyConfig) (err error) { 104 if a.id == "" { 105 return errors.New("invalid application") 106 } 107 108 if err = cfg.ValidateAll(); err != nil { 109 return err 110 } 111 112 for i, entry := range cfg.Policies { 113 if !a.resources.Has(entry.Key.ResourceType) { 114 return errors.Reason("cfg.Policies[%d].Key: unknown resource type: %s", i, entry.Key.ResourceType).Err() 115 } 116 if err = checkPolicy(entry.Policy); err != nil { 117 return errors.Annotate(err, "cfg.Policies[%d].Policy", i).Err() 118 } 119 } 120 121 cfgIDKey := quotakeys.PolicyConfigID(cid) 122 123 return withRedisConn(ctx, func(conn redis.Conn) error { 124 // If this thing exists, we're done. 125 exists, err := redis.Bool(conn.Do("EXISTS", cfgIDKey)) 126 if err != nil { 127 return errors.Annotate(err, "unable to check existance of policy config %q", cfgIDKey).Err() 128 } 129 if exists { 130 return nil 131 } 132 133 // At this point we'll call HSET; If we're racing, it's OK because the 134 // application has ensured us that (namespace, versionScheme, version) 135 // always maps to the same Config. 136 args := make(redis.Args, 0, 1+2+(len(cfg.Policies)*2)) 137 args = args.Add(cfgIDKey) 138 tsBytes, err := msgpackpb.Marshal(timestamppb.New(clock.Now(ctx)), msgpackpb.Deterministic) 139 if err != nil { 140 return errors.Annotate(err, "serializing timestamp").Err() 141 } 142 args = args.Add("~loaded_time", string(tsBytes)) 143 144 for i, entry := range cfg.Policies { 145 polBytes, err := msgpackpb.Marshal(entry.Policy, msgpackpb.Deterministic, msgpackpb.DisallowUnknownFields) 146 if err != nil { 147 return errors.Annotate(err, "serializing cfg.Policies[%d]", i).Err() 148 } 149 args = args.Add(quotakeys.PolicyKey(entry.Key), string(polBytes)) 150 } 151 152 _, err = conn.Do("HSET", args...) 153 return errors.Annotate(err, "unable to load policy config").Err() 154 }) 155 }