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  }