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 }