go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cli/add.go (about) 1 // Copyright 2016 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 cli 16 17 import ( 18 "context" 19 "fmt" 20 "os" 21 "strings" 22 "sync/atomic" 23 24 "google.golang.org/genproto/protobuf/field_mask" 25 26 "github.com/golang/protobuf/proto" 27 "github.com/google/uuid" 28 "github.com/maruel/subcommands" 29 "google.golang.org/grpc/metadata" 30 31 bb "go.chromium.org/luci/buildbucket" 32 "go.chromium.org/luci/buildbucket/protoutil" 33 "go.chromium.org/luci/common/cli" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/common/logging" 36 37 "google.golang.org/protobuf/types/known/structpb" 38 39 pb "go.chromium.org/luci/buildbucket/proto" 40 ) 41 42 func cmdAdd(p Params) *subcommands.Command { 43 return &subcommands.Command{ 44 UsageLine: `add [flags] [BUILDER [[BUILDER...]]`, 45 ShortDesc: "add builds", 46 LongDesc: doc(` 47 Add a build for each BUILDER argument. 48 49 A BUILDER must have format "<project>/<bucket>/<builder>", for 50 example "chromium/try/linux-rel". 51 If no builders were specified on the command line, they are read 52 from stdin. 53 54 Example: add linux-rel and mac-rel builds to chromium/ci bucket using Shell expansion. 55 bb add chromium/ci/{linux-rel,mac-rel} 56 `), 57 CommandRun: func() subcommands.CommandRun { 58 r := &addRun{} 59 r.RegisterDefaultFlags(p) 60 61 r.clsFlag.Register(&r.Flags, doc(` 62 CL URL as input for the builds. Can be specified multiple times. 63 64 Example: add a linux-rel tryjob for CL 1539021 65 bb add -cl https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/1539021/1 chromium/try/linux-rel 66 `)) 67 r.commitFlag.Register(&r.Flags, doc(` 68 Commit URL as input to the builds. 69 70 Example: build a specific revision 71 bb add -commit https://chromium.googlesource.com/chromium/src/+/7dab11d0e282bfa1d6f65cc52195f9602921d5b9 chromium/ci/linux-rel 72 73 Example: build latest chromium/src revision 74 bb add -commit https://chromium.googlesource.com/chromium/src/+/master chromium/ci/linux-rel 75 `)) 76 r.Flags.StringVar(&r.ref, "ref", "refs/heads/master", "Git ref for the -commit that specifies a commit hash.") 77 r.tagsFlag.Register(&r.Flags, doc(` 78 Build tags. Can be specified multiple times. 79 80 Example: add a build with tags "a:1" and "b:2". 81 bb add -t a:1 -t b:2 chromium/try/linux-rel 82 `)) 83 r.Flags.BoolVar(&r.experimental, "exp", false, doc(` 84 (deprecated) Mark the builds as experimental. 85 86 Identical and lower precedence to the preferred: 87 -ex +`+bb.ExperimentNonProduction+` 88 `)) 89 r.experimentsFlag.Register(&r.Flags, doc(` 90 Adds or removes an experiment from the build. 91 92 Must have the form `+"`[+-]experiment_name`"+`. 93 * +experiment_name adds the experiment to the build. 94 * -experiment_name prevents the experiment from being set on the build. 95 96 Well-known experiments: 97 * `+bb.ExperimentNonProduction+` 98 * `+bb.ExperimentBackendAlt+` 99 * `+bb.ExperimentBackendGo+` 100 * `+bb.ExperimentBBCanarySoftware+` 101 * `+bb.ExperimentBBAgent+` 102 * `+bb.ExperimentBBAgentDownloadCipd+` 103 * `+bb.ExperimentBBAgentGetBuild+` 104 `)) 105 r.Flags.Var(PropertiesFlag(&r.properties), "p", doc(` 106 Input properties for the build. 107 108 If a flag value starts with @, properties are read from the JSON file at the 109 path that follows @. Example: 110 bb add -p @my_properties.json chromium/try/linux-rel 111 This form can be used only in the first flag value. 112 113 Otherwise, a flag value must have name=value form. 114 If the property value is valid JSON, then it is parsed as JSON; 115 otherwise treated as a string. Example: 116 bb add -p foo=1 -p 'bar={"a": 2}' chromium/try/linux-rel 117 Different property names can be specified multiple times. 118 `)) 119 r.Flags.BoolVar(&r.canary, "canary", false, doc(` 120 (deprecated) Force the build to use canary infrastructure. 121 122 Identical and lower precedence to the preferred: 123 -ex +`+bb.ExperimentBBCanarySoftware+` 124 `)) 125 r.Flags.BoolVar(&r.noCanary, "nocanary", false, doc(` 126 (deprecated) Force the build to NOT use canary infrastructure. 127 128 Identical and lower precedence to the preferred: 129 -ex -`+bb.ExperimentBBCanarySoftware+` 130 `)) 131 r.Flags.StringVar(&r.swarmingParentRunID, "swarming-parent-run-id", "", doc(` 132 Establish parent->child relationship between provided swarming task (parent) 133 and the build to be triggered (child). 134 135 Provided value must be an ID of the swarming task sharing the same 136 swarming server as the build being created. If parent task completes 137 before the newly created build does, then swarming server will 138 forcefully terminate the build. 139 140 This makes the child build lifetime bounded by the lifetime of the given swarming task. 141 `)) 142 r.Flags.StringVar(&r.canOutliveParent, "can-outlive-parent", "", doc(` 143 Flag to indicate if the build to be triggered (child) can outlive its 144 parent build, which is discovered in the luci context. 145 146 Can be "yes" or "no". 147 148 * If "yes", the triggered build can keep running after its parent ends. 149 * swarming-parent-run-id must be empty in this case. 150 151 * If "no", the triggered build will be canceled if its parent ends. 152 153 * If unspecified, the value would be determined by the presence of 154 swarming-parent-run-id: "no" if swarming-parent-run-id is provided, 155 otherwise "yes". 156 `)) 157 return r 158 }, 159 } 160 } 161 162 type addRun struct { 163 printRun 164 clsFlag 165 commitFlag 166 tagsFlag 167 experimentsFlag 168 169 ref string 170 experimental bool 171 canary, noCanary bool 172 properties structpb.Struct 173 swarmingParentRunID string 174 canOutliveParent string 175 } 176 177 func (r *addRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { 178 if r.canary && r.noCanary { 179 fmt.Fprintf(os.Stderr, "-canary and -nocanary are mutually exclusive\n") 180 return 1 181 } 182 if err := r.validateCanOutliveParent(); err != nil { 183 fmt.Fprintln(os.Stderr, fmt.Sprint(err)) 184 return 1 185 } 186 ctx := cli.GetContext(a, r, env) 187 if err := r.initClients(ctx, nil); err != nil { 188 return r.done(ctx, err) 189 } 190 191 baseReq, err := r.prepareBaseRequest(ctx) 192 if err != nil { 193 return r.done(ctx, err) 194 } 195 196 if r.scheduleBuildToken != "" && r.scheduleBuildToken != bb.DummyBuildbucketToken { 197 ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs(bb.BuildbucketTokenHeader, r.scheduleBuildToken)) 198 } 199 200 i := int32(0) 201 return r.PrintAndDone(ctx, args, argOrder, func(ctx context.Context, builder string) (*pb.Build, error) { 202 req := proto.Clone(baseReq).(*pb.ScheduleBuildRequest) 203 204 // PrintAndDone callback is executed concurrently. 205 req.RequestId += fmt.Sprintf("-%d", atomic.AddInt32(&i, 1)) 206 207 var err error 208 req.Builder, err = protoutil.ParseBuilderID(builder) 209 if err != nil { 210 return nil, err 211 } 212 return r.buildsClient.ScheduleBuild(ctx, req, expectedCodeRPCOption) 213 }) 214 } 215 216 func (r *addRun) validateCanOutliveParent() error { 217 r.canOutliveParent = strings.ToLower(r.canOutliveParent) 218 if r.canOutliveParent != "" && r.canOutliveParent != "yes" && r.canOutliveParent != "no" { 219 return errors.New(`-can-outlive-parent can only be "yes", "no" or unspecified`) 220 } 221 if r.canOutliveParent == "yes" && r.swarmingParentRunID != "" { 222 return errors.New(`-can-outlive-parent can only be "no" or unspecified if -swarming-parent-run-id is set`) 223 } 224 if r.canOutliveParent == "" { 225 if r.swarmingParentRunID != "" { 226 r.canOutliveParent = "no" 227 } else { 228 r.canOutliveParent = "yes" 229 } 230 } 231 return nil 232 } 233 234 func (r *addRun) prepareBaseRequest(ctx context.Context) (*pb.ScheduleBuildRequest, error) { 235 ret := &pb.ScheduleBuildRequest{ 236 RequestId: uuid.New().String(), 237 Tags: r.Tags(), 238 Fields: &field_mask.FieldMask{Paths: []string{"*"}}, 239 Properties: &r.properties, 240 Swarming: &pb.ScheduleBuildRequest_Swarming{ParentRunId: r.swarmingParentRunID}, 241 Experiments: r.experiments, 242 } 243 244 switch { 245 case r.canary: 246 ret.Experiments[bb.ExperimentBBCanarySoftware] = true 247 logging.Warningf(ctx, "-canary is deprecated, setting experiment +%s", bb.ExperimentBBCanarySoftware) 248 case r.noCanary: 249 ret.Experiments[bb.ExperimentBBCanarySoftware] = false 250 logging.Warningf(ctx, "-canary is deprecated, setting experiment -%s", bb.ExperimentBBCanarySoftware) 251 } 252 if r.experimental { 253 ret.Experiments[bb.ExperimentNonProduction] = true 254 logging.Warningf(ctx, "-exp is deprecated, setting experiment +%s", bb.ExperimentNonProduction) 255 } 256 257 var err error 258 if ret.GerritChanges, err = r.retrieveCLs(ctx, r.httpClient, !kRequirePatchset); err != nil { 259 return nil, err 260 } 261 262 if ret.GitilesCommit, err = r.retrieveCommit(ctx, r.httpClient); err != nil { 263 return nil, err 264 } 265 if ret.GitilesCommit != nil && ret.GitilesCommit.Ref == "" { 266 ret.GitilesCommit.Ref = r.ref 267 } 268 269 // Only set ret.CanOutliveParent when triggering a child build for a real 270 // Buildbucket build. 271 if r.scheduleBuildToken != "" && r.scheduleBuildToken != bb.DummyBuildbucketToken { 272 switch r.canOutliveParent { 273 case "yes": 274 ret.CanOutliveParent = pb.Trinary_YES 275 case "no": 276 ret.CanOutliveParent = pb.Trinary_NO 277 default: 278 return nil, errors.New(fmt.Sprintf(`invalid value of -can-outlive-parent: %s, should be "yes"" or "no"`, r.canOutliveParent)) 279 } 280 } 281 return ret, nil 282 }