go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/config_service/rpc/get_project_configs.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 "net/http" 20 "slices" 21 22 "google.golang.org/grpc/codes" 23 "google.golang.org/grpc/status" 24 25 "go.chromium.org/luci/common/errors" 26 "go.chromium.org/luci/common/gcloud/gs" 27 "go.chromium.org/luci/common/logging" 28 "go.chromium.org/luci/common/proto/mask" 29 "go.chromium.org/luci/config" 30 "go.chromium.org/luci/gae/service/datastore" 31 "go.chromium.org/luci/server/auth" 32 33 "go.chromium.org/luci/config_service/internal/acl" 34 "go.chromium.org/luci/config_service/internal/clients" 35 "go.chromium.org/luci/config_service/internal/common" 36 "go.chromium.org/luci/config_service/internal/model" 37 pb "go.chromium.org/luci/config_service/proto" 38 ) 39 40 // maxProjConfigsResSize limits the sum of small configs size GetProjectConfigs 41 // rpc can return. In the current situation, it most likely won't happen. Treat 42 // this as a defensive coding, which can: 43 // 1. Prevent accidentally fetching hundreds of small config which causes the 44 // Config Server OOM. 45 // 2. In the future, when this rpc expand to support regex files fetching, the 46 // response is more likely large. It should limit the response size and use 47 // some other ways to return the entire results (e.g pagination.) 48 // 49 // Note: Make it a var instead of a const to facilitate the unit test. 50 var maxProjConfigsResSize = 200 * 1024 * 1024 51 52 // GetProjectConfigs gets the specified project configs from all projects. 53 // Implement pb.ConfigsServer. 54 func (c Configs) GetProjectConfigs(ctx context.Context, req *pb.GetProjectConfigsRequest) (*pb.GetProjectConfigsResponse, error) { 55 if err := validatePath(req.GetPath()); err != nil { 56 return nil, status.Errorf(codes.InvalidArgument, "invalid path - %q: %s", req.GetPath(), err) 57 } 58 m, err := toConfigMask(req.Fields) 59 if err != nil { 60 return nil, status.Errorf(codes.InvalidArgument, "invalid fields mask: %s", err) 61 } 62 63 var files []*model.File 64 // Query all config sets which start with "projects/". Use 65 // Lt("__key__", string(config.ProjectDomain)+"0"), because '0' is the next 66 // char of "/" in ASCII table. 67 // The query only fetches keys with `latest_revision.id` field. 68 query := datastore.NewQuery(model.ConfigSetKind). 69 Project("latest_revision.id"). 70 Gt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ProjectDomain)+"/")). 71 Lt("__key__", datastore.MakeKey(ctx, model.ConfigSetKind, string(config.ProjectDomain)+"0")) 72 err = datastore.Run(ctx, query, func(cs *model.ConfigSet) error { 73 switch hasPerm, err := acl.CanReadConfigSet(ctx, cs.ID); { 74 case err != nil: 75 logging.Errorf(ctx, "cannot check %q read access for %q: %s", cs.ID, auth.CurrentIdentity(ctx), err) 76 return err 77 case hasPerm: 78 files = append(files, &model.File{ 79 Path: req.Path, 80 Revision: datastore.MakeKey(ctx, model.ConfigSetKind, string(cs.ID), model.RevisionKind, cs.LatestRevision.ID), 81 }) 82 } 83 return nil 84 }) 85 if err != nil { 86 logging.Errorf(ctx, "error when querying project config sets: %s", err) 87 return nil, status.Errorf(codes.Internal, "error while fetching project configs") 88 } 89 90 // Get latest revision files from Datastore. 91 if err := errors.Filter(datastore.Get(ctx, files), datastore.ErrNoSuchEntity); err != nil { 92 logging.Errorf(ctx, "failed to fetch config files from Datastore: %s", err) 93 return nil, status.Errorf(codes.Internal, "error while fetching project configs") 94 } 95 var configs []*pb.Config 96 totalSize := 0 97 // Sort the files from smallest to largest so that the response will include 98 // as many smaller configs as possible. 99 slices.SortFunc(files, func(a, b *model.File) int { 100 return int(a.Size - b.Size) 101 }) 102 for _, f := range files { 103 cfgPb := &pb.Config{ 104 ConfigSet: f.Revision.Root().StringID(), 105 Path: f.Path, 106 ContentSha256: f.ContentSHA256, 107 Size: f.Size, 108 Revision: f.Revision.StringID(), 109 Url: common.GitilesURL(f.Location.GetGitilesLocation()), 110 } 111 totalSize += int(f.Size) 112 switch { 113 case len(f.Content) == 0 && f.GcsURI == "": 114 // This file is not found. 115 continue 116 case len(f.Content) != 0 && f.Size < int64(maxRawContentSize) && totalSize < maxProjConfigsResSize && m.MustIncludes("raw_content") != mask.Exclude: 117 rawContent, err := f.GetRawContent(ctx) 118 if err != nil { 119 logging.Errorf(ctx, "failed to get the raw content of the config for %s-%s: %s", cfgPb.ConfigSet, cfgPb.Path, err) 120 return nil, status.Errorf(codes.Internal, "error while getting raw content") 121 } 122 cfgPb.Content = &pb.Config_RawContent{RawContent: rawContent} 123 case f.GcsURI != "" && m.MustIncludes("signed_url") != mask.Exclude: 124 urls, err := common.CreateSignedURLs(ctx, clients.GetGsClient(ctx), []gs.Path{f.GcsURI}, http.MethodGet, nil) 125 if err != nil { 126 logging.Errorf(ctx, "cannot generate a signed url for GCS object %s: %s", f.GcsURI, err) 127 return nil, status.Errorf(codes.Internal, "error while generating the config signed url") 128 } 129 cfgPb.Content = &pb.Config_SignedUrl{SignedUrl: urls[0]} 130 } 131 132 // Trim config proto by the Fields mask. 133 if err := m.Trim(cfgPb); err != nil { 134 logging.Errorf(ctx, "cannot trim the config proto in config set %q: %s", cfgPb.ConfigSet, err) 135 return nil, status.Errorf(codes.Internal, "error while constructing the response") 136 } 137 configs = append(configs, cfgPb) 138 } 139 return &pb.GetProjectConfigsResponse{Configs: configs}, nil 140 }