go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/cmd/prpc/call.go (about)

     1  // Copyright 2016 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 main
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"sort"
    24  	"strings"
    25  
    26  	"github.com/maruel/subcommands"
    27  
    28  	"google.golang.org/grpc"
    29  	"google.golang.org/grpc/metadata"
    30  	"google.golang.org/grpc/status"
    31  
    32  	"go.chromium.org/luci/auth"
    33  	"go.chromium.org/luci/common/cli"
    34  	"go.chromium.org/luci/common/flag"
    35  	"go.chromium.org/luci/grpc/prpc"
    36  )
    37  
    38  const (
    39  	cmdCallUsage = `call [flags] <server> <service>.<method>
    40  
    41    server: host ("example.com") or port for localhost (":8080").
    42    service: full name of a service, e.g. "pkg.service"
    43    method: name of the method.
    44  `
    45  
    46  	cmdCallDesc = "calls a service method."
    47  )
    48  
    49  func cmdCall(defaultAuthOpts auth.Options) *subcommands.Command {
    50  	return &subcommands.Command{
    51  		UsageLine: cmdCallUsage,
    52  		ShortDesc: cmdCallDesc,
    53  		LongDesc: `Calls a service method.
    54  The input message is read from stdin (defaulting to JSONPB)`,
    55  		CommandRun: func() subcommands.CommandRun {
    56  			c := &callRun{
    57  				format:   formatFlagJSONPB,
    58  				metadata: metadata.MD{},
    59  			}
    60  			c.registerBaseFlags(defaultAuthOpts)
    61  			c.Flags.Var(&c.format, "format", fmt.Sprintf(
    62  				`Message format. Valid values: %s. Indicates both input and output format. The default is json.`,
    63  				formatFlagMap.Choices()))
    64  
    65  			c.Flags.Var(flag.GRPCMetadata(c.metadata), "metadata", "a key:value pair of request header metadata; may be specified multiple times")
    66  			return c
    67  		},
    68  	}
    69  }
    70  
    71  // callRun implements "call" subcommand.
    72  type callRun struct {
    73  	cmdRun
    74  	format   formatFlag
    75  	metadata metadata.MD
    76  }
    77  
    78  func (r *callRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
    79  	if len(args) < 2 {
    80  		return r.argErr(cmdCallDesc, cmdCallUsage, "")
    81  	}
    82  	host, target := args[0], args[1]
    83  	args = args[2:]
    84  
    85  	req := request{
    86  		format:       r.format,
    87  		message:      os.Stdin,
    88  		messageFlags: args,
    89  	}
    90  
    91  	var err error
    92  	req.service, req.method, err = splitServiceAndMethod(target)
    93  	if err != nil {
    94  		return r.argErr(cmdCallDesc, cmdCallUsage, "%s", err)
    95  	}
    96  
    97  	ctx := cli.GetContext(a, r, env)
    98  	client, err := r.authenticatedClient(ctx, host)
    99  	if err != nil {
   100  		return ecAuthenticatedClientError
   101  	}
   102  
   103  	// Insert outoging metadata.
   104  	ctx = metadata.NewOutgoingContext(ctx, r.metadata)
   105  
   106  	hmd, err := call(ctx, client, &req, os.Stdout)
   107  	if err != nil {
   108  		return r.done(err)
   109  	}
   110  
   111  	if r.verbose {
   112  		printMetadata(os.Stderr, "> ", hmd)
   113  	}
   114  
   115  	return 0
   116  }
   117  
   118  func splitServiceAndMethod(fullName string) (service string, method string, err error) {
   119  	lastDot := strings.LastIndex(fullName, ".")
   120  	if lastDot < 0 {
   121  		return "", "", fmt.Errorf("invalid full method name %q. It must contain a '.'", fullName)
   122  	}
   123  	service = fullName[:lastDot]
   124  	method = fullName[lastDot+1:]
   125  	return
   126  }
   127  
   128  // request is an RPC request.
   129  type request struct {
   130  	service      string
   131  	method       string
   132  	message      io.Reader
   133  	messageFlags []string
   134  	format       formatFlag
   135  }
   136  
   137  // call makes an RPC and writes response to out.
   138  func call(ctx context.Context, client *prpc.Client, req *request, out io.Writer) (hmd metadata.MD, err error) {
   139  	var inf, outf prpc.Format
   140  	var message []byte
   141  	switch req.format {
   142  
   143  	default:
   144  		var buf bytes.Buffer
   145  		if _, err := buf.ReadFrom(req.message); err != nil {
   146  			return nil, err
   147  		}
   148  		message = buf.Bytes()
   149  		inf = req.format.Format()
   150  		outf = inf
   151  	}
   152  
   153  	// Send the request.
   154  	res, err := client.CallWithFormats(ctx, req.service, req.method, message, inf, outf, grpc.Header(&hmd))
   155  	if err != nil {
   156  		return nil, &exitCode{err, int(status.Code(err))}
   157  	}
   158  
   159  	// Read response.
   160  	if _, err := out.Write(res); err != nil {
   161  		return nil, fmt.Errorf("failed to write response: %s", err)
   162  	}
   163  
   164  	return hmd, nil
   165  }
   166  
   167  func printMetadata(w io.Writer, prefix string, md metadata.MD) {
   168  	keys := make([]string, 0, len(md))
   169  	for k := range md {
   170  		keys = append(keys, k)
   171  	}
   172  	sort.Strings(keys)
   173  	for _, k := range keys {
   174  		for _, v := range md[k] {
   175  			fmt.Fprintf(w, "%s%s: %s\n", prefix, k, v)
   176  		}
   177  	}
   178  }