go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gce/appengine/rpc/config.go (about)

     1  // Copyright 2018 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 rpc
    16  
    17  import (
    18  	"context"
    19  
    20  	"github.com/golang/protobuf/proto"
    21  
    22  	"google.golang.org/grpc/codes"
    23  	"google.golang.org/grpc/status"
    24  	"google.golang.org/protobuf/types/known/emptypb"
    25  
    26  	"go.chromium.org/luci/common/clock"
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/common/proto/paged"
    30  	"go.chromium.org/luci/gae/service/datastore"
    31  	"go.chromium.org/luci/server/auth"
    32  
    33  	"go.chromium.org/luci/gce/api/config/v1"
    34  	"go.chromium.org/luci/gce/appengine/model"
    35  )
    36  
    37  // Config implements config.ConfigurationServer.
    38  type Config struct {
    39  }
    40  
    41  // Ensure Config implements config.ConfigurationServer.
    42  var _ config.ConfigurationServer = &Config{}
    43  
    44  // Delete handles a request to delete a config.
    45  // For app-internal use only.
    46  func (*Config) Delete(c context.Context, req *config.DeleteRequest) (*emptypb.Empty, error) {
    47  	if req.GetId() == "" {
    48  		return nil, status.Errorf(codes.InvalidArgument, "ID is required")
    49  	}
    50  	if err := datastore.Delete(c, &model.Config{ID: req.Id}); err != nil {
    51  		return nil, errors.Annotate(err, "failed to delete config").Err()
    52  	}
    53  	return &emptypb.Empty{}, nil
    54  }
    55  
    56  // Ensure handles a request to create or update a config.
    57  // For app-internal use only.
    58  func (*Config) Ensure(c context.Context, req *config.EnsureRequest) (*config.Config, error) {
    59  	if req.GetId() == "" {
    60  		return nil, status.Errorf(codes.InvalidArgument, "ID is required")
    61  	}
    62  	cfg := &model.Config{
    63  		ID: req.Id,
    64  	}
    65  	if err := datastore.RunInTransaction(c, func(c context.Context) error {
    66  		var priorAmount int32
    67  		switch err := datastore.Get(c, cfg); {
    68  		case err == nil:
    69  			// Don't forget potentially custom amount set via Update RPC.
    70  			priorAmount = cfg.Config.CurrentAmount
    71  		case err == datastore.ErrNoSuchEntity:
    72  			priorAmount = 0
    73  		default:
    74  			return errors.Annotate(err, "failed to fetch config").Err()
    75  		}
    76  		cfg.Config = req.Config
    77  		cfg.Config.CurrentAmount = priorAmount
    78  		if err := datastore.Put(c, cfg); err != nil {
    79  			return errors.Annotate(err, "failed to store config").Err()
    80  		}
    81  		return nil
    82  	}, nil); err != nil {
    83  		return nil, err
    84  	}
    85  	return cfg.Config, nil
    86  }
    87  
    88  // Get handles a request to get a config.
    89  func (*Config) Get(c context.Context, req *config.GetRequest) (*config.Config, error) {
    90  	if req.GetId() == "" {
    91  		return nil, status.Errorf(codes.InvalidArgument, "ID is required")
    92  	}
    93  
    94  	cfg, err := getConfigByID(c, req.Id)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	return cfg.Config, nil
   100  }
   101  
   102  // List handles a request to list all configs.
   103  func (*Config) List(c context.Context, req *config.ListRequest) (*config.ListResponse, error) {
   104  	rsp := &config.ListResponse{}
   105  	if err := paged.Query(c, req.GetPageSize(), req.GetPageToken(), rsp, datastore.NewQuery(model.ConfigKind), func(cfg *model.Config) error {
   106  		rsp.Configs = append(rsp.Configs, cfg.Config)
   107  		return nil
   108  	}); err != nil {
   109  		return nil, err
   110  	}
   111  	return rsp, nil
   112  }
   113  
   114  // Update handles a request to update a config.
   115  func (*Config) Update(c context.Context, req *config.UpdateRequest) (*config.Config, error) {
   116  	switch {
   117  	case req.GetId() == "":
   118  		return nil, status.Errorf(codes.InvalidArgument, "ID is required")
   119  	case len(req.UpdateMask.GetPaths()) == 0:
   120  		return nil, status.Errorf(codes.InvalidArgument, "update mask is required")
   121  	}
   122  	for _, p := range req.UpdateMask.Paths {
   123  		switch p {
   124  		case "config.current_amount":
   125  			ret, err := updateAmount(c, req)
   126  			return ret, err
   127  		case "config.duts":
   128  			ret, err := updateDUTs(c, req)
   129  			return ret, err
   130  		default:
   131  			return nil, status.Errorf(codes.InvalidArgument, "field %q is invalid or immutable", p)
   132  		}
   133  	}
   134  	return nil, nil
   135  }
   136  
   137  // updateDUTs updates config.Duts.
   138  // This is used for go/cloudbots.
   139  func updateDUTs(c context.Context, req *config.UpdateRequest) (*config.Config, error) {
   140  	logging.Debugf(c, "update config %s duts to %d", req.GetId(), req.GetConfig().GetDuts())
   141  
   142  	var ret *config.Config
   143  
   144  	if err := datastore.RunInTransaction(c, func(c context.Context) error {
   145  		cfg, err := getConfigByID(c, req.Id)
   146  		if err != nil {
   147  			return err
   148  		}
   149  		ret = cfg.Config
   150  		duts := req.Config.GetDuts()
   151  		if dutsEqual(ret.GetDuts(), duts) {
   152  			return nil
   153  		}
   154  		cfg.Config.Duts = duts
   155  		// Config.CurrentAmount serves as a second source of truth.
   156  		// The first source of truth being Config.Duts.
   157  		cfg.Config.CurrentAmount = int32(len(duts))
   158  		if err := datastore.Put(c, cfg); err != nil {
   159  			return errors.Annotate(err, "failed to store config").Err()
   160  		}
   161  		return nil
   162  	}, nil); err != nil {
   163  		return nil, err
   164  	}
   165  	return ret, nil
   166  }
   167  
   168  // updateAmount updates config.CurrentAmount.
   169  func updateAmount(c context.Context, req *config.UpdateRequest) (*config.Config, error) {
   170  	logging.Debugf(c, "update config %s current_amount to %d", req.GetId(), req.GetConfig().GetCurrentAmount())
   171  
   172  	var ret *config.Config
   173  
   174  	if err := datastore.RunInTransaction(c, func(c context.Context) error {
   175  		cfg, err := getConfigByID(c, req.Id)
   176  		if err != nil {
   177  			return err
   178  		}
   179  		ret = cfg.Config
   180  
   181  		amt, err := cfg.Config.ComputeAmount(req.Config.GetCurrentAmount(), clock.Now(c))
   182  		switch {
   183  		case err != nil:
   184  			return errors.Annotate(err, "failed to parse amount").Err()
   185  		case amt == cfg.Config.CurrentAmount:
   186  			return nil
   187  		default:
   188  			cfg.Config.CurrentAmount = amt
   189  			if err := datastore.Put(c, cfg); err != nil {
   190  				return errors.Annotate(err, "failed to store config").Err()
   191  			}
   192  			return nil
   193  		}
   194  	}, nil); err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	return ret, nil
   199  }
   200  
   201  // configPrelude ensures the user is authorized to use the config API.
   202  func configPrelude(c context.Context, methodName string, req proto.Message) (context.Context, error) {
   203  	if methodName == "Update" || methodName == "Get" {
   204  		// Update performs its own authorization checks, so allow all callers through.
   205  		logging.Debugf(c, "%s called %q:\n%s", auth.CurrentIdentity(c), methodName, req)
   206  		return c, nil
   207  	}
   208  	if !isReadOnly(methodName) {
   209  		return c, status.Errorf(codes.PermissionDenied, "unauthorized user")
   210  	}
   211  	switch is, err := auth.IsMember(c, admins, writers, readers); {
   212  	case err != nil:
   213  		return c, err
   214  	case is:
   215  		logging.Debugf(c, "%s called %q:\n%s", auth.CurrentIdentity(c), methodName, req)
   216  		return c, nil
   217  	}
   218  	return c, status.Errorf(codes.PermissionDenied, "unauthorized user")
   219  }
   220  
   221  // NewConfigurationServer returns a new configuration server.
   222  func NewConfigurationServer() config.ConfigurationServer {
   223  	return &config.DecoratedConfiguration{
   224  		Prelude:  configPrelude,
   225  		Service:  &Config{},
   226  		Postlude: gRPCifyAndLogErr,
   227  	}
   228  }
   229  
   230  func getConfigByID(c context.Context, id string) (*model.Config, error) {
   231  	cfg := &model.Config{
   232  		ID: id,
   233  	}
   234  	switch err := datastore.Get(c, cfg); err {
   235  	case nil:
   236  	case datastore.ErrNoSuchEntity:
   237  		return nil, notFoundErr(id)
   238  	default:
   239  		return nil, errors.Annotate(err, "failed to fetch config").Err()
   240  	}
   241  
   242  	switch is, err := auth.IsMember(c, cfg.Config.GetOwner()...); {
   243  	case err != nil:
   244  		return nil, err
   245  	case !is:
   246  		return nil, notFoundErr(id)
   247  	}
   248  	return cfg, nil
   249  }
   250  
   251  func notFoundErr(id string) error {
   252  	// To avoid revealing information about config existence to unauthorized users,
   253  	// not found and permission denied responses should be ambiguous.
   254  	return status.Errorf(codes.NotFound, "no config found with ID %q or unauthorized user", id)
   255  }
   256  
   257  // dutsEqual test equality between two sets of DUTs.
   258  // The sets are equal if they have the same length and
   259  // if the same keys are present in both sets.
   260  func dutsEqual(x, y map[string]*emptypb.Empty) bool {
   261  	if len(x) != len(y) {
   262  		return false
   263  	}
   264  	for k := range x {
   265  		if _, ok := y[k]; !ok {
   266  			return false
   267  		}
   268  	}
   269  	return true
   270  }