go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/rpc/list_config_sets.go (about) 1 // Copyright 2023 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 "google.golang.org/grpc/codes" 21 "google.golang.org/grpc/status" 22 23 "go.chromium.org/luci/common/logging" 24 "go.chromium.org/luci/common/proto/mask" 25 "go.chromium.org/luci/config" 26 "go.chromium.org/luci/gae/service/datastore" 27 "go.chromium.org/luci/server/auth" 28 29 "go.chromium.org/luci/config_service/internal/acl" 30 "go.chromium.org/luci/config_service/internal/model" 31 pb "go.chromium.org/luci/config_service/proto" 32 ) 33 34 // ListConfigSets returns a list of config sets. Implements pb.ConfigsServer. 35 func (c Configs) ListConfigSets(ctx context.Context, req *pb.ListConfigSetsRequest) (*pb.ListConfigSetsResponse, error) { 36 // Validate fields mask 37 m, err := toConfigSetMask(req.GetFields()) 38 switch { 39 case err != nil: 40 return nil, status.Errorf(codes.InvalidArgument, "invalid fields mask: %s", err) 41 case m.MustIncludes("file_paths") != mask.Exclude: 42 return nil, status.Errorf(codes.InvalidArgument, ` 43 'file_paths' is not supported in fields mask. Must specify a config set in 44 order to include file paths (via GetConfigSet rpc).`) 45 case m.MustIncludes("configs") != mask.Exclude: 46 return nil, status.Errorf(codes.InvalidArgument, ` 47 'configs' is not supported in fields mask. Must specify a config set in 48 order to include file paths (via GetConfigSet rpc).`) 49 } 50 51 // Fetch config sets 52 query := datastore.NewQuery(model.ConfigSetKind) 53 switch req.GetDomain() { 54 case pb.ListConfigSetsRequest_SERVICE: 55 // Range query for config sets which start with "services/". 56 // Use Lt("__key__", "serivces0") because '0' is the next char of "/". 57 query = query.Gt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ServiceDomain)+"/")). 58 Lt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ServiceDomain)+"0")) 59 case pb.ListConfigSetsRequest_PROJECT: 60 query = query.Gt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ProjectDomain)+"/")). 61 Lt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ProjectDomain)+"0")) 62 } 63 var cfgSets []*model.ConfigSet 64 err = datastore.Run(ctx, query, func(cs *model.ConfigSet) error { 65 switch hasPerm, err := acl.CanReadConfigSet(ctx, cs.ID); { 66 case err != nil: 67 logging.Errorf(ctx, "cannot check %q read access for %q: %s", cs.ID, auth.CurrentIdentity(ctx), err) 68 return err 69 case hasPerm: 70 cfgSets = append(cfgSets, cs) 71 } 72 return nil 73 }) 74 if err != nil { 75 logging.Errorf(ctx, "error when querying %s config sets: %s", req.Domain.String(), err) 76 return nil, status.Errorf(codes.Internal, "error while fetching config sets") 77 } 78 79 cfgSetsPb := make([]*pb.ConfigSet, len(cfgSets)) 80 for i, cs := range cfgSets { 81 cfgSetsPb[i] = toConfigSetPb(cs) 82 } 83 84 // Fetch last_import_attempt if needed. 85 // TODO(crbug.com/1465995): Might be inconsistent between ConfigSet and ImportAttempt 86 // if the ConfigSet gets updated during the two fetches. But it’s rare and costs an 87 // extra call every time. So not putting it in a transaction for now. 88 if m.MustIncludes("last_import_attempt") != mask.Exclude { 89 attempts := make([]*model.ImportAttempt, len(cfgSets)) 90 for i, cs := range cfgSets { 91 attempts[i] = &model.ImportAttempt{ 92 ConfigSet: datastore.KeyForObj(ctx, cs), 93 } 94 } 95 if err := datastore.Get(ctx, attempts); err != nil { 96 logging.Errorf(ctx, "failed to fetch last import attempts: %s", err) 97 return nil, status.Errorf(codes.Internal, "error while fetching last import attempts") 98 } 99 for i, attempt := range attempts { 100 cfgSetsPb[i].LastImportAttempt = toImportAttempt(attempt) 101 } 102 } 103 104 // Trim ConfigSet proto. 105 for _, cfgSetPb := range cfgSetsPb { 106 if err := m.Trim(cfgSetPb); err != nil { 107 logging.Errorf(ctx, "cannot trim ConfigSet (%q) proto: %s", cfgSetPb.Name, err) 108 return nil, status.Errorf(codes.Internal, "error while constructing the response") 109 } 110 } 111 return &pb.ListConfigSetsResponse{ConfigSets: cfgSetsPb}, nil 112 }