go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/led/ledcli/util.go (about) 1 // Copyright 2020 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 ledcli 16 17 import ( 18 "context" 19 "encoding/json" 20 "fmt" 21 "net/http" 22 "os" 23 "strings" 24 "time" 25 26 "github.com/maruel/subcommands" 27 28 "github.com/golang/protobuf/jsonpb" 29 "github.com/golang/protobuf/proto" 30 "go.chromium.org/luci/auth" 31 "go.chromium.org/luci/auth/client/authcli" 32 "go.chromium.org/luci/common/cli" 33 "go.chromium.org/luci/common/clock" 34 "go.chromium.org/luci/common/errors" 35 "go.chromium.org/luci/common/flag/stringmapflag" 36 "go.chromium.org/luci/common/logging" 37 job "go.chromium.org/luci/led/job" 38 ) 39 40 // TODO(iannucci): the 'subcommands' library is a mess, use something better. 41 42 type command interface { 43 subcommands.CommandRun 44 45 initFlags(opts cmdBaseOptions) 46 47 jobInput() bool 48 positionalRange() (min, max int) 49 50 validateFlags(ctx context.Context, positionals []string, env subcommands.Env) error 51 execute(ctx context.Context, authClient *http.Client, authOpts auth.Options, inJob *job.Definition) (output any, err error) 52 } 53 54 type cmdBaseOptions struct { 55 authOpts auth.Options 56 kitchenSupport job.KitchenSupport 57 } 58 59 type cmdBase struct { 60 subcommands.CommandRunBase 61 62 logFlags logging.Config 63 authFlags authcli.Flags 64 65 kitchenSupport job.KitchenSupport 66 67 authenticator *auth.Authenticator 68 } 69 70 func (c *cmdBase) initFlags(opts cmdBaseOptions) { 71 c.kitchenSupport = opts.kitchenSupport 72 c.logFlags.Level = logging.Info 73 c.logFlags.AddFlags(&c.Flags) 74 c.authFlags.Register(&c.Flags, opts.authOpts) 75 } 76 77 func readJobDefinition(ctx context.Context) (*job.Definition, error) { 78 readErr := make(chan error) 79 80 jd := &job.Definition{} 81 go func() { 82 defer close(readErr) 83 readErr <- jsonpb.Unmarshal(os.Stdin, jd) 84 }() 85 86 var err error 87 select { 88 case err = <-readErr: 89 // we read it before the timeout 90 case <-clock.After(ctx, time.Second): 91 logging.Warningf(ctx, "waiting for JobDefinition on stdin...") 92 err = <-readErr 93 } 94 95 return jd, errors.Annotate(err, "decoding job Definition").Err() 96 } 97 98 func (c *cmdBase) doContextExecute(a subcommands.Application, cmd command, args []string, env subcommands.Env) int { 99 ctx := c.logFlags.Set(cli.GetContext(a, cmd, env)) 100 authOpts, err := c.authFlags.Options() 101 authOpts.Transport = auth.NewModifyingTransport(http.DefaultTransport, func(req *http.Request) error { 102 req.Header.Set("User-Agent", userAgent) 103 return nil 104 }) 105 if err != nil { 106 logging.Errorf(ctx, "bad auth arguments: %s\n\n", err) 107 c.GetFlags().Usage() 108 return 1 109 } 110 c.authenticator = auth.NewAuthenticator(ctx, auth.SilentLogin, authOpts) 111 authClient, err := c.authenticator.Client() 112 if err == auth.ErrLoginRequired { 113 fmt.Fprintln(os.Stderr, "Login required: run `led auth-login`.") 114 return 1 115 } 116 117 //positional 118 min, max := cmd.positionalRange() 119 if len(args) < min { 120 logging.Errorf(ctx, "expected at least %d positional arguments, got %d", min, len(args)) 121 c.GetFlags().Usage() 122 return 1 123 } 124 if len(args) > max { 125 logging.Errorf(ctx, "expected at most %d positional arguments, got %d", max, len(args)) 126 c.GetFlags().Usage() 127 return 1 128 } 129 130 if err = cmd.validateFlags(ctx, args, env); err != nil { 131 logging.Errorf(ctx, "bad arguments: %s\n\n", err) 132 c.GetFlags().Usage() 133 return 1 134 } 135 136 var inJob *job.Definition 137 if cmd.jobInput() { 138 if inJob, err = readJobDefinition(ctx); err != nil { 139 errors.Log(ctx, err) 140 return 1 141 } 142 } 143 144 output, err := cmd.execute(ctx, authClient, authOpts, inJob) 145 if err != nil { 146 errors.Log(ctx, err) 147 return 1 148 } 149 150 if output != nil { 151 switch x := output.(type) { 152 case proto.Message: 153 err = (&jsonpb.Marshaler{ 154 OrigName: true, 155 Indent: " ", 156 }).Marshal(os.Stdout, x) 157 158 default: 159 enc := json.NewEncoder(os.Stdout) 160 enc.SetIndent("", " ") 161 err = enc.Encode(output) 162 } 163 if err != nil { 164 errors.Log(ctx, errors.Annotate(err, "encoding output").Err()) 165 return 1 166 } 167 } 168 169 return 0 170 } 171 172 func pingHost(host string) error { 173 rsp, err := http.Get("https://" + host) 174 if err != nil { 175 return errors.Annotate(err, "%q", host).Err() 176 } 177 defer rsp.Body.Close() 178 if rsp.StatusCode != 200 { 179 return errors.Reason("%q: bad status %d", host, rsp.StatusCode).Err() 180 } 181 return nil 182 } 183 184 func processExperiments(experiments stringmapflag.Value) (map[string]bool, error) { 185 processed := make(map[string]bool, len(experiments)) 186 for k, v := range experiments { 187 lower := strings.ToLower(v) 188 if lower != "true" && lower != "false" { 189 return nil, errors.Reason("bad -experiment %s=...: the value should be `true` or `false`, got %q", k, v).Err() 190 } 191 processed[k] = lower == "true" 192 } 193 return processed, nil 194 }