go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/buildbucket/cli/ls.go (about)

     1  // Copyright 2019 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  	"flag"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"strings"
    24  	"time"
    25  
    26  	"github.com/golang/protobuf/proto"
    27  	"github.com/maruel/subcommands"
    28  	"google.golang.org/genproto/protobuf/field_mask"
    29  
    30  	bb "go.chromium.org/luci/buildbucket"
    31  	"go.chromium.org/luci/common/cli"
    32  	"go.chromium.org/luci/common/data/stringset"
    33  	"go.chromium.org/luci/common/system/pager"
    34  
    35  	luciflag "go.chromium.org/luci/common/flag"
    36  
    37  	"go.chromium.org/luci/buildbucket/protoutil"
    38  
    39  	pb "go.chromium.org/luci/buildbucket/proto"
    40  )
    41  
    42  func cmdLS(p Params) *subcommands.Command {
    43  	return &subcommands.Command{
    44  		UsageLine: `ls [flags] [PATH [PATH...]]`,
    45  		ShortDesc: "lists builds",
    46  		LongDesc: doc(`
    47  			Lists builds.
    48  
    49  			Listed builds are sorted by creation time, descending.
    50  
    51  			A PATH argument can be one of
    52  				- "<project>"
    53  				- "<project>/<bucket>"
    54  				- "<project>/<bucket>/<builder>"
    55  
    56  			Multiple PATHs are connected with logical OR.
    57  			Printed builds are deduplicated.
    58  		`),
    59  		CommandRun: func() subcommands.CommandRun {
    60  			r := &lsRun{}
    61  			r.RegisterDefaultFlags(p)
    62  			r.RegisterIDFlag()
    63  			r.RegisterFieldFlags()
    64  			r.clsFlag.Register(&r.Flags, doc(`
    65  				CL URLs that builds must be associated with.
    66  				This flag is mutually exclusive with flag: -predicate.
    67  
    68  				Example: list builds of CL 1539021.
    69  					bb ls -cl https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/1539021/1
    70  			`))
    71  			r.tagsFlag.Register(&r.Flags, doc(`
    72  				Tags that builds must have. Can be specified multiple times.
    73  				All tags must be present.
    74  				This flag is mutually exclusive with flag: -predicate.
    75  
    76  				Example: list builds with tags "a:1" and "b:2".
    77  					bb ls -t a:1 -t b:2
    78  			`))
    79  			r.Flags.BoolVar(&r.includeExperimental, "exp", false, doc(`
    80  				Print experimental builds too.
    81  				This flag is mutually exclusive with flag: -predicate.
    82  			`))
    83  			r.experimentsFlag.Register(&r.Flags, doc(`
    84  				Experiments that the builds must (or must not) have.
    85  				This flag is mutually exclusive with flag: -predicate
    86  
    87  				Can be specified multiple times. All provided values must match.
    88  
    89  				Each value is in the form of `+"`[+-]experiment_name`"+`, where "+" indicates "must have"
    90  				and "-" indicates "must not have".
    91  
    92  				As a special case, "+luci.non_production" implies "-exp=true".
    93  
    94  				Example: list builds with expirement luci.non_production:
    95  				  bb ls -ex +luci.non_production
    96  
    97  				Well-known experiments:
    98  				  * `+bb.ExperimentNonProduction+`
    99  				  * `+bb.ExperimentBackendAlt+`
   100  					* `+bb.ExperimentBackendGo+`
   101  				  * `+bb.ExperimentBBCanarySoftware+`
   102  				  * `+bb.ExperimentBBAgent+`
   103  					* `+bb.ExperimentBBAgentDownloadCipd+`
   104  				  * `+bb.ExperimentBBAgentGetBuild+`
   105  			`))
   106  			r.Flags.Var(StatusFlag(&r.status), "status",
   107  				fmt.Sprintf("Build status. Valid values: %s.\n"+
   108  					"This flag is mutually exclusive with flag: -predicate.",
   109  					strings.Join(statusFlagValuesName, ", ")))
   110  			r.Flags.IntVar(&r.limit, "n", 0, doc(`
   111  				Limit the number of builds to print. If 0, then unlimited.
   112  				Can be passed as "-<number>", e.g. "ls -10".
   113  			`))
   114  			r.Flags.BoolVar(&r.noPager, "nopage", false, doc(`
   115  				Disable paging.
   116  			`))
   117  			r.Flags.Var(luciflag.MessageSliceFlag(&r.predicates), "predicate", doc(`
   118  				BuildPredicate that all builds in the response should match.
   119  
   120  				Predicate is expressed in JSON format of BuildPredicate proto message:
   121  					https://bit.ly/2RUjloG
   122  
   123  				Multiple predicates are supported and connected with logical OR.
   124  				This flag is mutally exclusive with -cl, -t, -exp, -status flags and
   125  				any PATH arguments
   126  
   127  				Example: list builds that is either with tag "a:1" or of builder "a/b/c"
   128  					bb ls \
   129  					-predicate '{"tags":[{"key":"a","value":"1"}]}' \
   130  					-predicate '{"builder":{"project":"a","bucket":"b","builder":"c"}}'`))
   131  			return r
   132  		},
   133  	}
   134  }
   135  
   136  // flagsMEWithPredicate defines a set of flags that are mutually exclusive with predicate flag
   137  var flagsMEWithPredicate = stringset.NewFromSlice("cl", "exp", "status", "t")
   138  
   139  type lsRun struct {
   140  	printRun
   141  	clsFlag
   142  	tagsFlag
   143  	experimentsFlag
   144  
   145  	predicates []*pb.BuildPredicate
   146  	status     pb.Status
   147  
   148  	includeExperimental bool
   149  	limit               int
   150  	noPager             bool
   151  }
   152  
   153  func (r *lsRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   154  	ctx := cli.GetContext(a, r, env)
   155  
   156  	if err := r.initClients(ctx, nil); err != nil {
   157  		return r.done(ctx, err)
   158  	}
   159  
   160  	if r.limit < 0 {
   161  		return r.done(ctx, fmt.Errorf("-n value must be non-negative"))
   162  	}
   163  
   164  	reqs, err := r.parseSearchRequests(ctx, args)
   165  	if err != nil {
   166  		return r.done(ctx, err)
   167  	}
   168  
   169  	disableColors := r.noColor || shouldDisableColors()
   170  
   171  	listBuilds := func(ctx context.Context, out io.WriteCloser) int {
   172  		buildC := make(chan *pb.Build)
   173  		errC := make(chan error)
   174  		go func() {
   175  			err := protoutil.Search(ctx, buildC, r.buildsClient, reqs...)
   176  			close(buildC)
   177  			errC <- err
   178  		}()
   179  
   180  		p := newPrinter(out, disableColors, time.Now)
   181  		count := 0
   182  		for b := range buildC {
   183  			r.printBuild(p, b, count == 0)
   184  			count++
   185  			if count == r.limit {
   186  				return 0
   187  			}
   188  		}
   189  
   190  		if err := <-errC; err != nil && err != context.Canceled {
   191  			return r.done(ctx, err)
   192  		}
   193  		return 0
   194  	}
   195  
   196  	if r.noPager {
   197  		return listBuilds(ctx, os.Stdout)
   198  	}
   199  	return pager.Main(ctx, listBuilds)
   200  }
   201  
   202  // parseSearchRequests converts flags and arguments to search requests.
   203  func (r *lsRun) parseSearchRequests(ctx context.Context, args []string) ([]*pb.SearchBuildsRequest, error) {
   204  	predicates, err := r.parseBuildPredicates(ctx, args)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	var pageSize int
   210  	if pageSize = defaultPageSize; r.limit > 0 && r.limit < pageSize {
   211  		pageSize = r.limit
   212  	}
   213  
   214  	fields, err := r.FieldMask()
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	for i, p := range fields.Paths {
   219  		fields.Paths[i] = "builds.*." + p
   220  	}
   221  
   222  	ret := make([]*pb.SearchBuildsRequest, len(predicates))
   223  	for i, predicate := range predicates {
   224  		ret[i] = &pb.SearchBuildsRequest{
   225  			Predicate: predicate,
   226  			PageSize:  int32(pageSize),
   227  			// Creating defensive copy. Search method do mutate the field mask
   228  			// append next_page_token
   229  			Fields: proto.Clone(fields).(*field_mask.FieldMask),
   230  		}
   231  	}
   232  	return ret, nil
   233  }
   234  
   235  // parseBuildPredicates converts flags and args to a slice of pb.BuildPredicate.
   236  func (r *lsRun) parseBuildPredicates(ctx context.Context, args []string) ([]*pb.BuildPredicate, error) {
   237  	if len(r.predicates) > 0 {
   238  		// Get all flags that have been set with value provided on the command line.
   239  		setFlags := stringset.New(r.Flags.NFlag())
   240  		r.Flags.Visit(func(flag *flag.Flag) {
   241  			setFlags.Add(flag.Name)
   242  		})
   243  		switch invalidFlags := setFlags.Intersect(flagsMEWithPredicate); {
   244  		case invalidFlags.Len() > 0:
   245  			return nil, fmt.Errorf("-predicate flag is mutually exclusive with flags: %q", invalidFlags.ToSortedSlice())
   246  		case len(args) > 0:
   247  			return nil, fmt.Errorf("-predicate flag is mutually exclusive with positional arguments")
   248  		default:
   249  			return r.predicates, nil
   250  		}
   251  	}
   252  
   253  	basePredicate := &pb.BuildPredicate{
   254  		Tags:                r.Tags(),
   255  		Status:              r.status,
   256  		IncludeExperimental: r.includeExperimental,
   257  		Experiments:         r.experimentsFlag.experimentsFlat(),
   258  	}
   259  
   260  	if r.experimentsFlag.experiments[bb.ExperimentNonProduction] {
   261  		basePredicate.IncludeExperimental = true
   262  	}
   263  
   264  	var err error
   265  	if basePredicate.GerritChanges, err = r.clsFlag.retrieveCLs(ctx, r.httpClient, kRequirePatchset); err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	if len(args) == 0 {
   270  		return []*pb.BuildPredicate{basePredicate}, nil
   271  	}
   272  
   273  	// PATH arguments
   274  	predicates := make([]*pb.BuildPredicate, len(args))
   275  	for i, path := range args {
   276  		predicates[i] = proto.Clone(basePredicate).(*pb.BuildPredicate)
   277  		if predicates[i].Builder, err = r.parsePath(path); err != nil {
   278  			return nil, fmt.Errorf("invalid path %q: %s", path, err)
   279  		}
   280  	}
   281  	return predicates, nil
   282  }
   283  
   284  func (r *lsRun) parsePath(path string) (*pb.BuilderID, error) {
   285  	bid := &pb.BuilderID{}
   286  	switch parts := strings.Split(path, "/"); len(parts) {
   287  	case 3:
   288  		bid.Builder = parts[2]
   289  		fallthrough
   290  	case 2:
   291  		bid.Bucket = parts[1]
   292  		fallthrough
   293  	case 1:
   294  		bid.Project = parts[0]
   295  	default:
   296  		return nil, fmt.Errorf("got %d components, want 1-3", len(parts))
   297  	}
   298  	return bid, nil
   299  }