go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/quotabeta/quotaconfig/configservice/configservice.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 configservice provides an implementation of quotaconfig.Interface
    16  // which fetches *pb.Policy configs stored with the LUCI Config service. Create
    17  // an instance suitable for use with the quota library using New.
    18  package configservice
    19  
    20  import (
    21  	"context"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  
    25  	"go.chromium.org/luci/common/errors"
    26  	"go.chromium.org/luci/config"
    27  	"go.chromium.org/luci/config/cfgclient"
    28  	"go.chromium.org/luci/config/validation"
    29  
    30  	"go.chromium.org/luci/server/caching"
    31  	pb "go.chromium.org/luci/server/quotabeta/proto"
    32  	"go.chromium.org/luci/server/quotabeta/quotaconfig"
    33  )
    34  
    35  // Ensure configService implements Interface at compile-time.
    36  var _ quotaconfig.Interface = &configService{}
    37  
    38  // configService fetches known *pb.Policy protos from the config service.
    39  // Implements Interface. Safe for concurrent use as long as the cache is
    40  // also safe for concurrent use.
    41  type configService struct {
    42  	cache  caching.BlobCache
    43  	cfgSet config.Set
    44  	path   string
    45  }
    46  
    47  // New returns a quotaconfig.Interface which fetches known *pb.Policy protos at
    48  // the given path for the given config set known to the LUCI Config service.
    49  // Suitable for use with the quota library by calling
    50  // quota.WithConfig(ctx, configservice.New(ctx, cfgSet, path)).
    51  // Panics if caching.GlobalCache() returns nil, which shouldn't happen if the
    52  // redisconn module (required by the quota library) has been initialized.
    53  func New(ctx context.Context, cfgSet config.Set, path string) quotaconfig.Interface {
    54  	cache := caching.GlobalCache(ctx, "quota.configservice")
    55  	if cache == nil {
    56  		// Shouldn't happen, since the quota library depends on redisconn which
    57  		// installs a redis-based cache into the context on module initialization.
    58  		panic(errors.New("no global cache available"))
    59  	}
    60  	return &configService{
    61  		cache:  cache,
    62  		cfgSet: cfgSet,
    63  		path:   path,
    64  	}
    65  }
    66  
    67  // Get returns a cached copy of the named *pb.Policy if it exists, or
    68  // quotaconfig.ErrNotFound if it doesn't. Errs if the context doesn't contain a
    69  // cfgclient.Interface (usually installed by config/service/cfgmodule).
    70  func (c *configService) Get(ctx context.Context, name string) (*pb.Policy, error) {
    71  	b, err := c.cache.Get(ctx, name)
    72  	switch {
    73  	case err == caching.ErrCacheMiss:
    74  		return nil, quotaconfig.ErrNotFound
    75  	case err != nil:
    76  		return nil, errors.Annotate(err, "retrieving cached policy %q", name).Err()
    77  	}
    78  	p := &pb.Policy{}
    79  	if err := proto.Unmarshal(b, p); err != nil {
    80  		return nil, errors.Annotate(err, "unmarshalling cached policy %q", name).Err()
    81  	}
    82  	return p, nil
    83  }
    84  
    85  // Refresh fetches all *pb.Policies from the LUCI Config service.
    86  func (c *configService) Refresh(ctx context.Context) error {
    87  	s := &pb.Config{}
    88  	if err := cfgclient.Get(ctx, c.cfgSet, c.path, cfgclient.ProtoText(s), nil); err != nil {
    89  		return errors.Annotate(err, "fetching policy config %q for config set %q", c.path, c.cfgSet).Err()
    90  	}
    91  	v := &validation.Context{
    92  		Context: ctx,
    93  	}
    94  	for i, p := range s.GetPolicy() {
    95  		v.Enter("policy %d", i)
    96  		quotaconfig.ValidatePolicy(v, p)
    97  		v.Exit()
    98  	}
    99  	if err := v.Finalize(); err != nil {
   100  		return errors.Annotate(err, "policy config %q for config set %q did not pass validation", c.path, c.cfgSet).Err()
   101  	}
   102  	for _, p := range s.GetPolicy() {
   103  		b, err := proto.Marshal(p)
   104  		if err != nil {
   105  			return errors.Annotate(err, "marshalling policy %q", p.Name).Err()
   106  		}
   107  		if err := c.cache.Set(ctx, p.Name, b, 0); err != nil {
   108  			return errors.Annotate(err, "caching policy %q", p.Name).Err()
   109  		}
   110  	}
   111  	return nil
   112  }