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 }