add more control over request metadata; add message templates (#17)

* add more control over request metadata between reflection calls and main RPC invocation
* add flag to print a message template (when describing message types)
* relax some command-line argument issues to warnings
This commit is contained in:
Joshua Humphries 2018-03-08 11:29:06 -05:00 committed by GitHub
parent 99a3346b94
commit f7eb17819d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 103 additions and 7 deletions

View File

@ -14,7 +14,9 @@ import (
"github.com/golang/protobuf/jsonpb" "github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
"github.com/jhump/protoreflect/desc" "github.com/jhump/protoreflect/desc"
"github.com/jhump/protoreflect/dynamic"
"github.com/jhump/protoreflect/grpcreflect" "github.com/jhump/protoreflect/grpcreflect"
"golang.org/x/net/context" "golang.org/x/net/context"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -49,6 +51,8 @@ var (
with -plaintext option. Must also provide -cert option.`) with -plaintext option. Must also provide -cert option.`)
protoset multiString protoset multiString
addlHeaders multiString addlHeaders multiString
rpcHeaders multiString
reflHeaders multiString
data = flag.String("d", "", data = flag.String("d", "",
`JSON request contents. If the value is '@' then the request contents are `JSON request contents. If the value is '@' then the request contents are
read from stdin. For calls that accept a stream of requests, the read from stdin. For calls that accept a stream of requests, the
@ -68,16 +72,26 @@ var (
network links or due to incorrect stream method usage.`) network links or due to incorrect stream method usage.`)
emitDefaults = flag.Bool("emit-defaults", false, emitDefaults = flag.Bool("emit-defaults", false,
`Emit default values from JSON-encoded responses.`) `Emit default values from JSON-encoded responses.`)
msgTemplate = flag.Bool("msg-template", false,
`When describing messages, show a JSON template for the message type.`)
verbose = flag.Bool("v", false, verbose = flag.Bool("v", false,
`Enable verbose output.`) `Enable verbose output.`)
) )
func init() { func init() {
// TODO: Allow separate headers for relflection/invocation
flag.Var(&addlHeaders, "H", flag.Var(&addlHeaders, "H",
`Additional request headers in 'name: value' format. May specify more `Additional headers in 'name: value' format. May specify more than one
than one via multiple -H flags. These headers will also be included in via multiple flags. These headers will also be included in reflection
reflection requests to a server.`) requests requests to a server.`)
flag.Var(&rpcHeaders, "rpc-header",
`Additional RPC headers in 'name: value' format. May specify more than
one via multiple flags. These headers will *only* be used when invoking
the requested RPC method. They are excluded from reflection requests.`)
flag.Var(&reflHeaders, "reflect-header",
`Additional reflection headers in 'name: value' format. May specify more
than one via multiple flags. These headers will only be used during
reflection requests and will be excluded when invoking the requested RPC
method.`)
flag.Var(&protoset, "protoset", flag.Var(&protoset, "protoset",
`The name of a file containing an encoded FileDescriptorSet. This file's `The name of a file containing an encoded FileDescriptorSet. This file's
contents will be used to determine the RPC schema instead of querying contents will be used to determine the RPC schema instead of querying
@ -155,7 +169,10 @@ func main() {
args = args[1:] args = args[1:]
} else { } else {
if *data != "" { if *data != "" {
fail(nil, "The -d argument is not used with 'list' or 'describe' verb.") warn("The -d argument is not used with 'list' or 'describe' verb.")
}
if len(rpcHeaders) > 0 {
warn("The -rpc-header argument is not used with 'list' or 'describe' verb.")
} }
if len(args) > 0 { if len(args) > 0 {
symbol = args[0] symbol = args[0]
@ -172,6 +189,9 @@ func main() {
if len(protoset) == 0 && target == "" { if len(protoset) == 0 && target == "" {
fail(nil, "No host:port specified and no protoset specified.") fail(nil, "No host:port specified and no protoset specified.")
} }
if len(protoset) > 0 && len(reflHeaders) > 0 {
warn("The -reflect-header argument is not used when -protoset files are used ")
}
ctx := context.Background() ctx := context.Background()
if *maxTime != "" { if *maxTime != "" {
@ -231,7 +251,7 @@ func main() {
fail(err, "Failed to process proto descriptor sets") fail(err, "Failed to process proto descriptor sets")
} }
} else { } else {
md := grpcurl.MetadataFromHeaders(addlHeaders) md := grpcurl.MetadataFromHeaders(append(addlHeaders, reflHeaders...))
refCtx := metadata.NewOutgoingContext(ctx, md) refCtx := metadata.NewOutgoingContext(ctx, md)
cc = dial() cc = dial()
refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(cc)) refClient = grpcreflect.NewClient(refCtx, reflectpb.NewServerReflectionClient(cc))
@ -329,6 +349,16 @@ func main() {
fail(err, "Failed to describe symbol %q", s) fail(err, "Failed to describe symbol %q", s)
} }
fmt.Println(txt) fmt.Println(txt)
if dsc, ok := dsc.(*desc.MessageDescriptor); ok && *msgTemplate {
// for messages, also show a template in JSON, to make it easier to
// create a request to invoke an RPC
tmpl := makeTemplate(dynamic.NewMessage(dsc))
fmt.Println("\nMessage template:")
jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true}
jsm.Marshal(os.Stdout, tmpl)
fmt.Println()
}
} }
} else { } else {
@ -344,7 +374,7 @@ func main() {
} }
h := &handler{dec: dec, descSource: descSource} h := &handler{dec: dec, descSource: descSource}
err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, addlHeaders, h, h.getRequestData) err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, h.getRequestData)
if err != nil { if err != nil {
fail(err, "Error invoking method %q", symbol) fail(err, "Error invoking method %q", symbol)
} }
@ -389,6 +419,11 @@ the method's request type will be sent.
} }
func warn(msg string, args ...interface{}) {
msg = fmt.Sprintf("Warning: %s\n", msg)
fmt.Fprintf(os.Stderr, msg, args...)
}
func fail(err error, msg string, args ...interface{}) { func fail(err error, msg string, args ...interface{}) {
if err != nil { if err != nil {
msg += ": %v" msg += ": %v"
@ -466,3 +501,64 @@ func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) {
fmt.Printf("\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md)) fmt.Printf("\nResponse trailers received:\n%s\n", grpcurl.MetadataToString(md))
} }
} }
// makeTemplate fleshes out the given message so that it is a suitable template for creating
// an instance of that message in JSON. In particular, it ensures that any repeated fields
// (which include map fields) are not empty, so they will render with a single element (to
// show the types and optionally nested fields). It also ensures that nested messages are
// not nil by setting them to a message that is also fleshed out as a template message.
func makeTemplate(msg proto.Message) proto.Message {
dm, ok := msg.(*dynamic.Message)
if !ok {
return msg
}
// for repeated fields, add a single element with default value
// and for message fields, add a message with all default fields
// that also has non-nil message and non-empty repeated fields
for _, fd := range dm.GetMessageDescriptor().GetFields() {
if fd.IsRepeated() {
switch fd.GetType() {
case descpb.FieldDescriptorProto_TYPE_FIXED32,
descpb.FieldDescriptorProto_TYPE_UINT32:
dm.AddRepeatedField(fd, uint32(0))
case descpb.FieldDescriptorProto_TYPE_SFIXED32,
descpb.FieldDescriptorProto_TYPE_SINT32,
descpb.FieldDescriptorProto_TYPE_INT32,
descpb.FieldDescriptorProto_TYPE_ENUM:
dm.AddRepeatedField(fd, int32(0))
case descpb.FieldDescriptorProto_TYPE_FIXED64,
descpb.FieldDescriptorProto_TYPE_UINT64:
dm.AddRepeatedField(fd, uint64(0))
case descpb.FieldDescriptorProto_TYPE_SFIXED64,
descpb.FieldDescriptorProto_TYPE_SINT64,
descpb.FieldDescriptorProto_TYPE_INT64:
dm.AddRepeatedField(fd, int64(0))
case descpb.FieldDescriptorProto_TYPE_STRING:
dm.AddRepeatedField(fd, "")
case descpb.FieldDescriptorProto_TYPE_BYTES:
dm.AddRepeatedField(fd, []byte{})
case descpb.FieldDescriptorProto_TYPE_BOOL:
dm.AddRepeatedField(fd, false)
case descpb.FieldDescriptorProto_TYPE_FLOAT:
dm.AddRepeatedField(fd, float32(0))
case descpb.FieldDescriptorProto_TYPE_DOUBLE:
dm.AddRepeatedField(fd, float64(0))
case descpb.FieldDescriptorProto_TYPE_MESSAGE,
descpb.FieldDescriptorProto_TYPE_GROUP:
dm.AddRepeatedField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType())))
}
} else if fd.GetMessageType() != nil {
dm.SetField(fd, makeTemplate(dynamic.NewMessage(fd.GetMessageType())))
}
}
return dm
}