From 0dff23d9f4922040cb370ec4f5f0bf41ef5461e5 Mon Sep 17 00:00:00 2001 From: Sri Krishna Vempati Date: Mon, 16 Dec 2024 15:18:40 -0800 Subject: [PATCH 1/2] Add support for Any resolution and fetching metadata for all messages known by the backend into the web form. --- cmd/grpcui/grpcui.go | 35 ++++++++++++++++++++++++++++++++++- handlers.go | 36 +++++++++++++++++++++++++++++------- standalone/standalone.go | 17 ++++++++++++----- 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/cmd/grpcui/grpcui.go b/cmd/grpcui/grpcui.go index 3a184dd5..53fc8414 100644 --- a/cmd/grpcui/grpcui.go +++ b/cmd/grpcui/grpcui.go @@ -306,6 +306,29 @@ type compositeSource struct { file grpcurl.DescriptorSource } +func Union(slice1, slice2 []*desc.FileDescriptor) []*desc.FileDescriptor { + // Create a map to track unique elements + elementMap := make(map[string]*desc.FileDescriptor) + + // Add elements from the first slice to the map + for _, elem := range slice1 { + elementMap[elem.GetName()] = elem + } + + // Add elements from the second slice to the map + for _, elem := range slice2 { + elementMap[elem.GetName()] = elem + } + + // Create a slice to store the union + var union []*desc.FileDescriptor + for elem := range elementMap { + union = append(union, elementMap[elem]) + } + + return union +} + func (cs compositeSource) ListServices() ([]string, error) { return cs.reflection.ListServices() } @@ -598,6 +621,16 @@ func main() { fail(err, "Failed to compute set of methods to expose") } allFiles, err := grpcurl.GetAllFiles(descSource) + // grpcurl doesn't handle the case of composite source, so we need to handle it. + if fileSource != nil { + fileOnlyFiles, err := grpcurl.GetAllFiles(fileSource) + if err != nil { + fail(err, "Failed to enumerate all proto files from fileSource") + } + allFiles = Union(allFiles, fileOnlyFiles) + } + // Get additional Files + if err != nil { fail(err, "Failed to enumerate all proto files") } @@ -632,7 +665,7 @@ func main() { handlerOpts = append(handlerOpts, configureJSandCSS(extraCSS, standalone.AddCSSFile)...) handlerOpts = append(handlerOpts, configureAssets(otherAssets)...) - handler := standalone.Handler(cc, target, methods, allFiles, handlerOpts...) + handler := standalone.HandlerWithDescriptorSource(cc, target, methods, allFiles, descSource, handlerOpts...) if *maxTime > 0 { timeout := time.Duration(*maxTime * float64(time.Second)) // enforce the timeout by wrapping the handler and inserting a diff --git a/handlers.go b/handlers.go index 2bb3b864..9ac804d6 100644 --- a/handlers.go +++ b/handlers.go @@ -66,6 +66,8 @@ type InvokeOptions struct { // of a bool "verbose" flag, so that additional logs may be added in the // future and the caller control how detailed those logs will be. Verbosity int + + AdditionalDescriptorSource grpcurl.DescriptorSource } // RPCInvokeHandlerWithOptions is the same as RPCInvokeHandler except that it @@ -91,11 +93,18 @@ func RPCInvokeHandlerWithOptions(ch grpc.ClientConnInterface, descs []*desc.Meth for _, md := range descs { if md.GetFullyQualifiedName() == method { - descSource, err := grpcurl.DescriptorSourceFromFileDescriptors(md.GetFile()) - if err != nil { - http.Error(w, "Failed to create descriptor source: "+err.Error(), http.StatusInternalServerError) - return + var descSource grpcurl.DescriptorSource + if options.AdditionalDescriptorSource == nil { + currDescSource, err := grpcurl.DescriptorSourceFromFileDescriptors(md.GetFile()) + if err != nil { + http.Error(w, "Failed to create descriptor source: "+err.Error(), http.StatusInternalServerError) + return + } + descSource = currDescSource + } else { + descSource = options.AdditionalDescriptorSource } + results, err := invokeRPC(r.Context(), method, ch, descSource, r.Header, r.Body, &options) if err != nil { if _, ok := err.(errReadFail); ok { @@ -138,6 +147,7 @@ func RPCMetadataHandler(methods []*desc.MethodDescriptor, files []*desc.FileDesc method := r.URL.Query().Get("method") var results *schema + if method == "*" { // This means gather *all* message types. This is used to // provide a drop-down for Any messages. @@ -145,7 +155,7 @@ func RPCMetadataHandler(methods []*desc.MethodDescriptor, files []*desc.FileDesc } else { for _, md := range methods { if md.GetFullyQualifiedName() == method { - r, err := gatherMetadataForMethod(md) + r, err := gatherMetadataForMethod(md, files) if err != nil { http.Error(w, "Failed to gather metadata for RPC Method", http.StatusUnprocessableEntity) return @@ -258,7 +268,8 @@ func gatherAllMessages(msgs []*desc.MessageDescriptor, result *schema) { } } -func gatherMetadataForMethod(md *desc.MethodDescriptor) (*schema, error) { +func gatherMetadataForMethod(md *desc.MethodDescriptor, + files []*desc.FileDescriptor) (*schema, error) { msg := md.GetInputType() result := &schema{ RequestType: msg.GetFullyQualifiedName(), @@ -268,6 +279,9 @@ func gatherMetadataForMethod(md *desc.MethodDescriptor) (*schema, error) { } result.visitMessage(msg) + for _, fd := range files { + gatherAllMessages(fd.GetMessageTypes(), result) + } return result, nil } @@ -435,7 +449,15 @@ func invokeRPC(ctx context.Context, methodName string, ch grpc.ClientConnInterfa reqStats.Sent++ req := input.Data[0] input.Data = input.Data[1:] - if err := jsonpb.Unmarshal(bytes.NewReader(req), m); err != nil { + var jsonUnmarshaler jsonpb.Unmarshaler + if descSource != nil { + anyResolver := grpcurl.AnyResolverFromDescriptorSource(descSource) + jsonUnmarshaler = jsonpb.Unmarshaler{AnyResolver: anyResolver} + } else { + jsonUnmarshaler = jsonpb.Unmarshaler{} + } + + if err := jsonUnmarshaler.Unmarshal(bytes.NewReader(req), m); err != nil { return status.Errorf(codes.InvalidArgument, err.Error()) } return nil diff --git a/standalone/standalone.go b/standalone/standalone.go index 4fa6a024..e6dbb5d7 100644 --- a/standalone/standalone.go +++ b/standalone/standalone.go @@ -7,6 +7,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "github.com/fullstorydev/grpcurl" "html/template" "io" "mime" @@ -42,7 +43,8 @@ const csrfHeaderName = "x-grpcui-csrf-token" // // The returned handler expects to serve resources from "/". If it will instead // be handling a sub-path (e.g. handling "/rpc-ui/") then use http.StripPrefix. -func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescriptor, files []*desc.FileDescriptor, opts ...HandlerOption) http.Handler { +func HandlerWithDescriptorSource(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescriptor, files []*desc.FileDescriptor, + descSource grpcurl.DescriptorSource, opts ...HandlerOption) http.Handler { uiOpts := &handlerOptions{ indexTmpl: defaultIndexTemplate, css: grpcui.WebFormSampleCSS(), @@ -91,10 +93,11 @@ func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescri }) invokeOpts := grpcui.InvokeOptions{ - ExtraMetadata: uiOpts.extraMetadata, - PreserveHeaders: uiOpts.preserveHeaders, - EmitDefaults: uiOpts.emitDefaults, - Verbosity: uiOpts.invokeVerbosity, + ExtraMetadata: uiOpts.extraMetadata, + PreserveHeaders: uiOpts.preserveHeaders, + EmitDefaults: uiOpts.emitDefaults, + Verbosity: uiOpts.invokeVerbosity, + AdditionalDescriptorSource: descSource, } rpcInvokeHandler := http.StripPrefix("/invoke", grpcui.RPCInvokeHandlerWithOptions(ch, methods, invokeOpts)) mux.HandleFunc("/invoke/", func(w http.ResponseWriter, r *http.Request) { @@ -140,6 +143,10 @@ func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescri }) } +func Handler(ch grpcdynamic.Channel, target string, methods []*desc.MethodDescriptor, files []*desc.FileDescriptor, opts ...HandlerOption) http.Handler { + return HandlerWithDescriptorSource(ch, target, methods, files, nil, opts...) +} + var defaultIndexTemplate = template.Must(template.New("index.html").Parse(string(standalone.IndexTemplate()))) func getIndexContents(tmpl *template.Template, target string, webFormHTML []byte, addlResources []*resource) []byte { From 4330243c876d9816812e6a74b3cde5de90cf5c18 Mon Sep 17 00:00:00 2001 From: Sri Krishna Vempati Date: Mon, 16 Dec 2024 18:57:06 -0800 Subject: [PATCH 2/2] Cleanup --- cmd/grpcui/grpcui.go | 1 - handlers.go | 1 - 2 files changed, 2 deletions(-) diff --git a/cmd/grpcui/grpcui.go b/cmd/grpcui/grpcui.go index 53fc8414..694f98f2 100644 --- a/cmd/grpcui/grpcui.go +++ b/cmd/grpcui/grpcui.go @@ -629,7 +629,6 @@ func main() { } allFiles = Union(allFiles, fileOnlyFiles) } - // Get additional Files if err != nil { fail(err, "Failed to enumerate all proto files") diff --git a/handlers.go b/handlers.go index 9ac804d6..69fab085 100644 --- a/handlers.go +++ b/handlers.go @@ -147,7 +147,6 @@ func RPCMetadataHandler(methods []*desc.MethodDescriptor, files []*desc.FileDesc method := r.URL.Query().Get("method") var results *schema - if method == "*" { // This means gather *all* message types. This is used to // provide a drop-down for Any messages.