go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/cli/matchconfig.go (about) 1 // Copyright 2021 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 23 "github.com/maruel/subcommands" 24 25 "google.golang.org/protobuf/encoding/prototext" 26 27 "go.chromium.org/luci/auth" 28 "go.chromium.org/luci/auth/client/authcli" 29 "go.chromium.org/luci/common/api/gerrit" 30 "go.chromium.org/luci/common/cli" 31 "go.chromium.org/luci/common/data/text" 32 "go.chromium.org/luci/common/errors" 33 gerritpb "go.chromium.org/luci/common/proto/gerrit" 34 "go.chromium.org/luci/common/sync/parallel" 35 lucivalidation "go.chromium.org/luci/config/validation" 36 37 cfgpb "go.chromium.org/luci/cv/api/config/v2" 38 "go.chromium.org/luci/cv/internal/configs/prjcfg" 39 "go.chromium.org/luci/cv/internal/configs/validation" 40 "go.chromium.org/luci/cv/internal/gerrit/cfgmatcher" 41 ) 42 43 func cmdMatchConfig(p Params) *subcommands.Command { 44 return &subcommands.Command{ 45 UsageLine: "match-config [flags] CFG_PATH CL1 [CL2 ...] ", 46 ShortDesc: "Match given CL(s) against given config.", 47 LongDesc: text.Doc(` 48 With a given configuration file, validate it, and determine the configuration 49 that would apply to the given CL(s). 50 51 CFG_PATH must be the path to a generated "commit-queue.cfg" file. 52 CL1, CL2, etc. must be given as URLs to Gerrit CLs e.g.: 53 "https://chromium-review.googlesource.com/c/infra/luci/luci-go/+/3198992" 54 "https://crrev.com/c/3198992" 55 `), 56 CommandRun: func() subcommands.CommandRun { 57 r := &matchConfigRun{} 58 r.authFlags.Register(&r.Flags, p.Auth) 59 return r 60 }, 61 } 62 } 63 64 type matchConfigRun struct { 65 subcommands.CommandRunBase 66 authFlags authcli.Flags 67 } 68 69 func (r *matchConfigRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { 70 ctx := cli.GetContext(a, r, env) 71 72 if err := r.validateArgs(ctx, args); err != nil { 73 return r.done(badArgsTag.Apply(err)) 74 } 75 76 config, err := loadAndValidateConfig(ctx, args[0]) 77 if err != nil { 78 return r.done(err) 79 } 80 81 // cfgmatcher works with CV's storage-layer prjcfg.ConfigGroups, 82 // which includes more than just the group name. 83 // So, provide cfgmatcher config groups with empty hash as it doesn't matter 84 // for CV CLI use case. 85 prjCfgGroups := make([]*prjcfg.ConfigGroup, len(config.ConfigGroups)) 86 for i, cg := range config.ConfigGroups { 87 prjCfgGroups[i] = &prjcfg.ConfigGroup{Content: cg, ID: prjcfg.MakeConfigGroupID("", cg.GetName())} 88 } 89 90 clURLs := args[1:] 91 results := make([]matchResult, len(clURLs)) 92 err = parallel.FanOutIn(func(work chan<- func() error) { 93 for i, clURL := range clURLs { 94 i, clURL := i, clURL 95 matcher := cfgmatcher.LoadMatcherFromConfigGroups(ctx, prjCfgGroups, nil) 96 work <- func() error { 97 results[i] = r.match(ctx, clURL, matcher) 98 return nil 99 } 100 } 101 }) 102 if err != nil { 103 panic("impossible: workpool returned error") 104 } 105 errs := errors.MultiError(nil) 106 for i, mr := range results { 107 fmt.Printf("\n%s:\n", clURLs[i]) 108 fmt.Printf(" Location: Host: %s, Repo: %s, Ref: %s\n", mr.Host, mr.Repo, mr.Ref) 109 if len(mr.Names) != 0 { 110 fmt.Printf(" Matched: %s\n", strings.Join(mr.Names, ", ")) 111 } 112 if mr.Error != nil { 113 fmt.Printf(" Error: %s\n", mr.Error) 114 errs.MaybeAdd(mr.Error) 115 } 116 } 117 return r.done(errs.AsError()) 118 } 119 120 type matchResult struct { 121 Host, Repo, Ref string 122 Names []string 123 Error error 124 } 125 126 func (r *matchConfigRun) match(ctx context.Context, url string, matcher *cfgmatcher.Matcher) matchResult { 127 ret := matchResult{} 128 129 host, change, err := gerrit.FuzzyParseURL(url) 130 if err != nil { 131 ret.Error = err 132 return ret 133 } 134 135 // We use a new client for each CL because their hosts may be different. 136 client, err := r.newGerritClient(ctx, host) 137 if err != nil { 138 ret.Error = err 139 return ret 140 } 141 142 info, err := client.GetChange(ctx, &gerritpb.GetChangeRequest{Number: change}) 143 if err != nil { 144 ret.Error = err 145 return ret 146 } 147 148 ret.Host, ret.Repo, ret.Ref = host, info.GetProject(), info.GetRef() 149 150 ids := matcher.Match(ret.Host, ret.Repo, ret.Ref) 151 for _, id := range ids { 152 ret.Names = append(ret.Names, id.Name()) 153 } 154 155 if len(ret.Names) == 0 { 156 ret.Error = errors.Reason("the CL did not match any config groups").Err() 157 } 158 if len(ret.Names) > 1 { 159 ret.Error = errors.Reason("the CL matched multiple config groups").Err() 160 } 161 return ret 162 } 163 164 func (r *matchConfigRun) validateArgs(ctx context.Context, args []string) error { 165 if len(args) < 2 { 166 return errors.Reason("At least 2 arguments are required").Err() 167 } 168 for i, arg := range args { 169 if i == 0 { 170 // Ensure cfg file exists. 171 _, err := os.Stat(arg) 172 if err != nil { 173 return err 174 } 175 } else { 176 // Ensure CL URLs are valid. 177 _, _, err := gerrit.FuzzyParseURL(arg) 178 if err != nil { 179 return err 180 } 181 } 182 } 183 return nil 184 } 185 186 func loadAndValidateConfig(ctx context.Context, cfgPath string) (*cfgpb.Config, error) { 187 in, err := os.ReadFile(cfgPath) 188 if err != nil { 189 return nil, err 190 } 191 ret := &cfgpb.Config{} 192 err = prototext.Unmarshal(in, ret) 193 if err != nil { 194 return nil, err 195 } 196 vctx := &lucivalidation.Context{Context: ctx} 197 if err := validation.ValidateProjectConfig(vctx, ret); err != nil { 198 return nil, err 199 } 200 return ret, vctx.Finalize() 201 } 202 203 func (r *matchConfigRun) newGerritClient(ctx context.Context, host string) (gerritpb.GerritClient, error) { 204 authOpts, err := r.authFlags.Options() 205 if err != nil { 206 return nil, err 207 } 208 c, err := auth.NewAuthenticator(ctx, auth.SilentLogin, authOpts).Client() 209 switch { 210 case err == auth.ErrLoginRequired: 211 return nil, errors.New("Login required: run `luci-cv auth-login`") 212 case err != nil: 213 return nil, err 214 } 215 return gerrit.NewRESTClient(c, host, true) 216 } 217 218 func (r *matchConfigRun) done(err error) int { 219 if err == nil { 220 return 0 221 } 222 fmt.Fprintln(os.Stderr, err) 223 _, badArgs := errors.TagValueIn(badArgsTag.Key, err) 224 if badArgs { 225 return 2 226 } 227 return 1 228 }