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 }