go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/quotabeta/quotaconfig/quotaconfig.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 quotaconfig exports the interface required by the quota library to 16 // read *pb.Policy configs. Provides an in-memory implementation of the 17 // interface suitable for testing. See quotaconfig subpackages for other 18 // implementations. 19 package quotaconfig 20 21 import ( 22 "context" 23 "regexp" 24 "sync" 25 26 "google.golang.org/protobuf/proto" 27 28 "go.chromium.org/luci/common/errors" 29 "go.chromium.org/luci/config/validation" 30 31 pb "go.chromium.org/luci/server/quotabeta/proto" 32 ) 33 34 // ErrNotFound must be returned by Interface.Get implementations when the named 35 // *pb.Policy is not found. 36 var ErrNotFound = errors.New("policy not found") 37 38 // Interface encapsulates the functionality needed to implement a configuration 39 // layer usable by the quota library. Implementations should ensure returned 40 // *pb.Policies are valid (see ValidatePolicy). 41 type Interface interface { 42 // Get returns the named *pb.Policy or ErrNotFound if it doesn't exist. 43 // 44 // Called by the quota library every time quota is manipulated, 45 // so implementations should return relatively quickly. 46 Get(context.Context, string) (*pb.Policy, error) 47 48 // Refresh fetches all *pb.Policies. 49 // 50 // Implementations should validate (see ValidatePolicy) and cache configs so 51 // future Get calls return relatively quickly. 52 Refresh(context.Context) error 53 } 54 55 // Ensure Memory implements Interface at compile-time. 56 var _ Interface = &Memory{} 57 58 // Memory holds known *pb.Policy protos in memory. 59 // Implements Interface. Safe for concurrent use. 60 type Memory struct { 61 // lock is a mutex governing reads/writes to policies. 62 lock sync.RWMutex 63 64 // policies is a map of policy name to *pb.Policy with that name. 65 policies map[string]*pb.Policy 66 } 67 68 // Get returns a copy of the named *pb.Policy if it exists, or else ErrNotFound. 69 func (m *Memory) Get(ctx context.Context, name string) (*pb.Policy, error) { 70 m.lock.RLock() 71 defer m.lock.RUnlock() 72 p, ok := m.policies[name] 73 if !ok { 74 return nil, ErrNotFound 75 } 76 return proto.Clone(p).(*pb.Policy), nil 77 } 78 79 // Refresh fetches all *pb.Policies. 80 func (m *Memory) Refresh(ctx context.Context) error { 81 return nil 82 } 83 84 // NewMemory returns a new Memory initialized with the given policies. 85 func NewMemory(ctx context.Context, policies []*pb.Policy) (Interface, error) { 86 m := &Memory{ 87 policies: make(map[string]*pb.Policy, len(policies)), 88 } 89 v := &validation.Context{ 90 Context: ctx, 91 } 92 for i, p := range policies { 93 p = proto.Clone(p).(*pb.Policy) 94 v.Enter("policy %d", i) 95 ValidatePolicy(v, p) 96 m.policies[p.Name] = p 97 v.Exit() 98 } 99 if err := v.Finalize(); err != nil { 100 return nil, errors.Annotate(err, "policies did not pass validation").Err() 101 } 102 return m, nil 103 } 104 105 // policyName is a *regexp.Regexp which policy names must match. 106 // Must start with a letter. Allowed characters (no spaces): A-Z a-z 0-9 - _ / 107 // The special substring ${user} is allowed. Must not exceed 64 characters. 108 var policyName = regexp.MustCompile(`^[A-Za-z]([A-Za-z0-9-_/]|(\$\{user\}))*$`) 109 110 // ValidatePolicy validates the given *pb.Policy. 111 func ValidatePolicy(ctx *validation.Context, p *pb.Policy) { 112 if !policyName.MatchString(p.GetName()) { 113 ctx.Errorf("name must match %q", policyName.String()) 114 } 115 if len(p.GetName()) > 64 { 116 ctx.Errorf("name must not exceed 64 characters") 117 } 118 if p.GetResources() < 0 { 119 ctx.Errorf("resources must not be negative") 120 } 121 if p.GetReplenishment() < 0 { 122 ctx.Errorf("replenishment must not be negative") 123 } 124 }