go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/get_builder.go (about) 1 // Copyright 2020 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 "fmt" 20 "sort" 21 22 "google.golang.org/protobuf/encoding/protojson" 23 "google.golang.org/protobuf/types/known/durationpb" 24 "google.golang.org/protobuf/types/known/structpb" 25 26 "go.chromium.org/luci/common/errors" 27 "go.chromium.org/luci/common/proto/reflectutil" 28 "go.chromium.org/luci/gae/service/datastore" 29 "go.chromium.org/luci/grpc/appstatus" 30 31 "go.chromium.org/luci/buildbucket/appengine/internal/config" 32 "go.chromium.org/luci/buildbucket/appengine/internal/perm" 33 "go.chromium.org/luci/buildbucket/appengine/model" 34 "go.chromium.org/luci/buildbucket/bbperms" 35 pb "go.chromium.org/luci/buildbucket/proto" 36 "go.chromium.org/luci/buildbucket/protoutil" 37 ) 38 39 // validateGetBuilder validates the given request. 40 func validateGetBuilder(req *pb.GetBuilderRequest) error { 41 if err := protoutil.ValidateBuilderID(req.Id); err != nil { 42 return errors.Annotate(err, "id").Err() 43 } 44 45 return nil 46 } 47 48 func stringsToRequestedDimensions(strDims []string) (map[string][]*pb.RequestedDimension, []string) { 49 // key -> slice of dimensions (key, value, expiration) with matching keys. 50 dims := make(map[string][]*pb.RequestedDimension) 51 var empty []string 52 53 for _, d := range strDims { 54 exp, k, v := config.ParseDimension(d) 55 if v == "" { 56 empty = append(empty, k) 57 continue 58 } 59 dim := &pb.RequestedDimension{ 60 Key: k, 61 Value: v, 62 } 63 if exp > 0 { 64 dim.Expiration = &durationpb.Duration{ 65 Seconds: exp, 66 } 67 } 68 dims[k] = append(dims[k], dim) 69 } 70 return dims, empty 71 } 72 73 // applyShadowAdjustment makes a copy of the builder config then applies shadow 74 // builder adjustments to it. 75 func applyShadowAdjustment(cfg *pb.BuilderConfig) *pb.BuilderConfig { 76 rtnCfg := reflectutil.ShallowCopy(cfg).(*pb.BuilderConfig) 77 shadowBldrCfg := cfg.GetShadowBuilderAdjustments() 78 if shadowBldrCfg == nil { 79 return rtnCfg 80 } 81 if shadowBldrCfg.ServiceAccount != "" { 82 rtnCfg.ServiceAccount = shadowBldrCfg.ServiceAccount 83 } 84 85 if len(shadowBldrCfg.Dimensions) > 0 { 86 dims, _ := stringsToRequestedDimensions(rtnCfg.Dimensions) 87 shadowDims, empty := stringsToRequestedDimensions(shadowBldrCfg.Dimensions) 88 89 for k, d := range shadowDims { 90 dims[k] = d 91 } 92 for _, key := range empty { 93 delete(dims, key) 94 } 95 var updatedDims []string 96 for _, dims := range dims { 97 for _, dim := range dims { 98 dimStr := fmt.Sprintf("%s:%s", dim.Key, dim.Value) 99 if dim.Expiration != nil { 100 dimStr = fmt.Sprintf("%d:%s", dim.Expiration.Seconds, dimStr) 101 } 102 updatedDims = append(updatedDims, dimStr) 103 } 104 } 105 sort.Strings(updatedDims) 106 rtnCfg.Dimensions = updatedDims 107 } 108 109 if shadowBldrCfg.Properties != "" { 110 if rtnCfg.GetProperties() == "" { 111 rtnCfg.Properties = shadowBldrCfg.Properties 112 } else { 113 origProp := &structpb.Struct{} 114 shadowProp := &structpb.Struct{} 115 if err := protojson.Unmarshal([]byte(rtnCfg.Properties), origProp); err != nil { 116 // Builder config should have been validated already. 117 panic(errors.Annotate(err, "error unmarshaling builder properties for %q", rtnCfg.Name).Err()) 118 } 119 if err := protojson.Unmarshal([]byte(shadowBldrCfg.Properties), shadowProp); err != nil { 120 // Builder config should have been validated already. 121 panic(errors.Annotate(err, "error unmarshaling builder shadow properties for %q", rtnCfg.Name).Err()) 122 } 123 for k, v := range shadowProp.GetFields() { 124 origProp.Fields[k] = v 125 } 126 updatedProp, err := protojson.Marshal(origProp) 127 if err != nil { 128 panic(errors.Annotate(err, "error marshaling builder properties for %q", rtnCfg.Name).Err()) 129 } 130 rtnCfg.Properties = string(updatedProp) 131 } 132 } 133 rtnCfg.ShadowBuilderAdjustments = nil 134 return rtnCfg 135 } 136 137 func trySynthesizeFromShadowedBuilder(ctx context.Context, req *pb.GetBuilderRequest) (*pb.BuilderItem, error) { 138 reqBucket := &model.Bucket{ 139 Parent: model.ProjectKey(ctx, req.Id.Project), 140 ID: req.Id.Bucket, 141 } 142 switch err := datastore.Get(ctx, reqBucket); { 143 case err == datastore.ErrNoSuchEntity: 144 return nil, perm.NotFoundErr(ctx) 145 case err != nil: 146 return nil, err 147 } 148 if len(reqBucket.Shadows) == 0 { 149 // This bucket doesn't shadow any other buckets. 150 return nil, perm.NotFoundErr(ctx) 151 } 152 var builders []*model.Builder 153 for _, shadowedBkt := range reqBucket.Shadows { 154 builders = append(builders, &model.Builder{ 155 Parent: model.BucketKey(ctx, req.Id.Project, shadowedBkt), 156 ID: req.Id.Builder, 157 }) 158 } 159 if err := model.GetIgnoreMissing(ctx, builders); err != nil { 160 return nil, errors.Annotate(err, "failed to fetch entities").Err() 161 } 162 for _, bldr := range builders { 163 if bldr.Config != nil { 164 cfgCopy := applyShadowAdjustment(bldr.Config) 165 return &pb.BuilderItem{ 166 Id: req.Id, 167 Config: cfgCopy, 168 }, nil 169 } 170 } 171 return nil, perm.NotFoundErr(ctx) 172 } 173 174 // GetBuilder handles a request to retrieve a builder. Implements pb.BuildersServer. 175 func (*Builders) GetBuilder(ctx context.Context, req *pb.GetBuilderRequest) (*pb.BuilderItem, error) { 176 if err := validateGetBuilder(req); err != nil { 177 return nil, appstatus.BadRequest(err) 178 } 179 180 if err := perm.HasInBuilder(ctx, bbperms.BuildersGet, req.Id); err != nil { 181 return nil, err 182 } 183 184 builder := &model.Builder{ 185 Parent: model.BucketKey(ctx, req.Id.Project, req.Id.Bucket), 186 ID: req.Id.Builder, 187 } 188 switch err := datastore.Get(ctx, builder); { 189 case err == datastore.ErrNoSuchEntity: 190 return trySynthesizeFromShadowedBuilder(ctx, req) 191 case err != nil: 192 return nil, err 193 } 194 195 if req.Mask == nil { 196 req.Mask = &pb.BuilderMask{Type: pb.BuilderMask_CONFIG_ONLY} 197 } 198 199 response := &pb.BuilderItem{Id: req.Id} 200 201 switch req.Mask.Type { 202 case pb.BuilderMask_ALL: 203 response.Config = builder.Config 204 response.Metadata = builder.Metadata 205 case pb.BuilderMask_CONFIG_ONLY: 206 response.Config = builder.Config 207 case pb.BuilderMask_METADATA_ONLY: 208 response.Metadata = builder.Metadata 209 } 210 211 return response, nil 212 }