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  }