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  }