go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/appengine/rpc/synthesize_build.go (about) 1 // Copyright 2022 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/protobuf/types/known/structpb" 21 22 "go.chromium.org/luci/common/errors" 23 "go.chromium.org/luci/gae/service/datastore" 24 "go.chromium.org/luci/grpc/appstatus" 25 26 "go.chromium.org/luci/buildbucket/appengine/internal/config" 27 "go.chromium.org/luci/buildbucket/appengine/internal/perm" 28 "go.chromium.org/luci/buildbucket/appengine/model" 29 "go.chromium.org/luci/buildbucket/bbperms" 30 pb "go.chromium.org/luci/buildbucket/proto" 31 "go.chromium.org/luci/buildbucket/protoutil" 32 ) 33 34 func validateSynthesize(req *pb.SynthesizeBuildRequest) error { 35 if req.GetBuilder() == nil && req.GetTemplateBuildId() == 0 { 36 return errors.Reason("builder or template_build_id is required").Err() 37 } 38 if req.GetBuilder() != nil && req.GetTemplateBuildId() != 0 { 39 return errors.Reason("builder and template_build_id are mutually exclusive").Err() 40 } 41 if req.GetBuilder() != nil { 42 if err := protoutil.ValidateRequiredBuilderID(req.Builder); err != nil { 43 return errors.Annotate(err, "builder").Err() 44 } 45 } 46 return nil 47 } 48 49 func synthesizeBuild(ctx context.Context, schReq *pb.ScheduleBuildRequest) (*pb.Build, error) { 50 builder := schReq.GetBuilder() 51 if builder == nil { 52 return nil, errors.Reason("builder must be specified").Err() 53 } 54 if err := perm.HasInBuilder(ctx, bbperms.BuildersGet, builder); err != nil { 55 return nil, err 56 } 57 globalCfg, err := config.GetSettingsCfg(ctx) 58 if err != nil { 59 return nil, errors.Annotate(err, "error fetching service config").Err() 60 } 61 62 bktCfg := &model.Bucket{ 63 Parent: model.ProjectKey(ctx, builder.Project), 64 ID: builder.Bucket, 65 } 66 bldrCfg := &model.Builder{ 67 Parent: model.BucketKey(ctx, builder.Project, builder.Bucket), 68 ID: builder.Builder, 69 } 70 switch err := datastore.Get(ctx, bktCfg, bldrCfg); { 71 case errors.Contains(err, datastore.ErrNoSuchEntity): 72 switch { 73 case bktCfg == nil: 74 // Bucket not found. 75 return nil, perm.NotFoundErr(ctx) 76 case len(bktCfg.Shadows) > 0: 77 // This is a shadow bucket. Synthesizing a build from shadow bucket 78 // is not supported. 79 return nil, appstatus.BadRequest(errors.Reason("Synthesizing a build from a shadow bucket is not supported").Err()) 80 default: 81 // Builder not found. 82 return nil, perm.NotFoundErr(ctx) 83 } 84 case err != nil: 85 return nil, errors.Annotate(err, "failed to get builder config").Err() 86 default: 87 bld := scheduleShadowBuild(ctx, schReq, nil, bktCfg.Proto.Shadow, globalCfg, bldrCfg.Config) 88 return bld, nil 89 } 90 } 91 92 func scheduleShadowBuild(ctx context.Context, schReq *pb.ScheduleBuildRequest, ancestors []int64, shadowBucket string, globalCfg *pb.SettingsCfg, cfg *pb.BuilderConfig) *pb.Build { 93 origBucket := schReq.Builder.Bucket 94 95 cfgCopy := cfg 96 if shadowBucket != "" && shadowBucket != origBucket { 97 cfgCopy = applyShadowAdjustment(cfg) 98 } 99 100 bld := buildFromScheduleRequest(ctx, schReq, ancestors, "", cfgCopy, globalCfg) 101 102 if shadowBucket != "" && shadowBucket != origBucket { 103 bld.Infra.Led = &pb.BuildInfra_Led{ 104 ShadowedBucket: origBucket, 105 } 106 bld.Input.Properties.Fields["$recipe_engine/led"] = &structpb.Value{ 107 Kind: &structpb.Value_StructValue{ 108 StructValue: &structpb.Struct{ 109 Fields: map[string]*structpb.Value{ 110 "shadowed_bucket": { 111 Kind: &structpb.Value_StringValue{ 112 StringValue: origBucket, 113 }, 114 }, 115 }, 116 }, 117 }, 118 } 119 bld.Builder.Bucket = shadowBucket 120 } 121 return bld 122 } 123 124 // synthesizeBuildFromTemplate returns a request with fields populated by the 125 // given template_build_id if there is one. Fields set in the request override 126 // fields populated from the template. Does not modify the incoming request. 127 func synthesizeBuildFromTemplate(ctx context.Context, req *pb.SynthesizeBuildRequest) (*pb.Build, error) { 128 ret, err := scheduleRequestFromBuildID(ctx, req.TemplateBuildId, false) 129 if err != nil { 130 return nil, err 131 } 132 133 if len(req.GetExperiments()) > 0 { 134 ret.Experiments = req.Experiments 135 } else { 136 ret.Experiments = map[string]bool{} 137 } 138 return synthesizeBuild(ctx, ret) 139 } 140 141 // SynthesizeBuild handles a request to synthesize a build. Implements pb.BuildsServer. 142 func (*Builds) SynthesizeBuild(ctx context.Context, req *pb.SynthesizeBuildRequest) (*pb.Build, error) { 143 if err := validateSynthesize(req); err != nil { 144 return nil, appstatus.BadRequest(err) 145 } 146 147 if req.GetTemplateBuildId() != 0 { 148 return synthesizeBuildFromTemplate(ctx, req) 149 } 150 151 exps := map[string]bool{} 152 if len(req.GetExperiments()) > 0 { 153 exps = req.Experiments 154 } 155 return synthesizeBuild(ctx, &pb.ScheduleBuildRequest{ 156 Builder: req.GetBuilder(), 157 Experiments: exps, 158 }) 159 }