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 := &quotapb.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 = &quotapb.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  }