mirror of
https://github.com/fullstorydev/grpcurl.git
synced 2026-05-25 13:11:46 +03:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f9e76c978 | ||
|
|
9fa2fce63b | ||
|
|
70e9bba1b8 | ||
|
|
d86529bb4f | ||
|
|
0dea37ee70 | ||
|
|
dfa06f4410 | ||
|
|
22ce2f04fd | ||
|
|
1e8e50f4f8 | ||
|
|
7cabe7a9d0 | ||
|
|
9a4bbacdd6 | ||
|
|
69ea782936 | ||
|
|
58cd93280e | ||
|
|
a337c1afcf | ||
|
|
e00ef3eb7c | ||
|
|
397a8c18ca | ||
|
|
554e69be2c | ||
|
|
2dd771c49e | ||
|
|
79a550b858 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
dist/
|
||||
24
.goreleaser.yml
Normal file
24
.goreleaser.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
builds:
|
||||
- binary: grpcurl
|
||||
main: ./cmd/grpcurl
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
ldflags:
|
||||
- -s -w -X main.version=v{{.Version}}
|
||||
|
||||
archive:
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
replacements:
|
||||
amd64: x86_64
|
||||
386: x86_32
|
||||
darwin: osx
|
||||
files:
|
||||
- LICENSE
|
||||
@@ -3,8 +3,6 @@ sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: "1.7"
|
||||
- go: "1.8"
|
||||
- go: "1.9"
|
||||
- go: "1.10"
|
||||
env: VET=1
|
||||
|
||||
23
Makefile
23
Makefile
@@ -1,3 +1,5 @@
|
||||
dev_build_version=$(shell git describe --tags --always --dirty)
|
||||
|
||||
# TODO: run golint and errcheck, but only to catch *new* violations and
|
||||
# decide whether to change code or not (e.g. we need to be able to whitelist
|
||||
# violations already in the code). They can be useful to catch errors, but
|
||||
@@ -16,7 +18,12 @@ updatedeps:
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
go install ./...
|
||||
go install -ldflags '-X "main.version=dev build $(dev_build_version)"' ./...
|
||||
|
||||
.PHONY: release
|
||||
release:
|
||||
@GO111MODULE=off go get github.com/goreleaser/goreleaser
|
||||
goreleaser --rm-dist
|
||||
|
||||
.PHONY: checkgofmt
|
||||
checkgofmt:
|
||||
@@ -29,10 +36,18 @@ checkgofmt:
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
# TODO: remove the ignored check; need it for now because it
|
||||
# is complaining about a deprecated comment added to grpc,
|
||||
# but it's not yet released. Once the new (non-deprecated)
|
||||
# API is included in a release, we can move to that new
|
||||
# version and fix the call site to no longer use deprecated
|
||||
# method.
|
||||
# This all works fine with Go modules, but without modules,
|
||||
# CI is just getting latest master for dependencies like grpc.
|
||||
.PHONY: staticcheck
|
||||
staticcheck:
|
||||
@go get honnef.co/go/tools/cmd/staticcheck
|
||||
staticcheck ./...
|
||||
staticcheck -ignore github.com/fullstorydev/grpcurl/tls_settings_test.go:SA1019 ./...
|
||||
|
||||
.PHONY: unused
|
||||
unused:
|
||||
@@ -52,11 +67,11 @@ predeclared:
|
||||
# Intentionally omitted from CI, but target here for ad-hoc reports.
|
||||
.PHONY: golint
|
||||
golint:
|
||||
@go get github.com/golang/lint/golint
|
||||
@go get golang.org/x/lint/golint
|
||||
golint -min_confidence 0.9 -set_exit_status ./...
|
||||
|
||||
# Intentionally omitted from CI, but target here for ad-hoc reports.
|
||||
.PHONY: errchack
|
||||
.PHONY: errcheck
|
||||
errcheck:
|
||||
@go get github.com/kisielk/errcheck
|
||||
errcheck ./...
|
||||
|
||||
62
README.md
62
README.md
@@ -14,22 +14,22 @@ This program accepts messages using JSON encoding, which is much more friendly f
|
||||
humans and scripts.
|
||||
|
||||
With this tool you can also browse the schema for gRPC services, either by querying
|
||||
a server that supports [service reflection](https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto),
|
||||
by reading proto source files, or by loading in compiled "protoset" files (files that contain encoded file
|
||||
[descriptor protos](https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto)).
|
||||
a server that supports [server reflection](https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto),
|
||||
by reading proto source files, or by loading in compiled "protoset" files (files that contain
|
||||
encoded file [descriptor protos](https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto)).
|
||||
In fact, the way the tool transforms JSON request data into a binary encoded protobuf
|
||||
is using that very same schema. So, if the server you interact with does not support
|
||||
reflection, you will either need the proto source files that define the service or need
|
||||
protoset files that `grpcurl` can use.
|
||||
|
||||
[Examples for how to set up server reflection can be found here.](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md#known-implementations)
|
||||
|
||||
This repo also provides a library package, `github.com/fullstorydev/grpcurl`, that has
|
||||
functions for simplifying the construction of other command-line tools that dynamically
|
||||
invoke gRPC endpoints. This code is a great example of how to use the various packages of
|
||||
the [protoreflect](https://godoc.org/github.com/jhump/protoreflect) library, and shows
|
||||
off what they can do.
|
||||
|
||||
See also the [`grpcurl` talk at GopherCon 2018](https://www.youtube.com/watch?v=dDr-8kbMnaw).
|
||||
|
||||
## Features
|
||||
`grpcurl` supports all kinds of RPC methods, including streaming methods. You can even
|
||||
operate bi-directional streaming methods interactively by running `grpcurl` from an
|
||||
@@ -44,6 +44,12 @@ service. If not, you can supply the `.proto` source files or you can supply prot
|
||||
files (containing compiled descriptors, produced by `protoc`) to `grpcurl`.
|
||||
|
||||
## Installation
|
||||
|
||||
### Binaries
|
||||
|
||||
Download the binary from the [releases](https://github.com/fullstorydev/grpcurl/releases) page.
|
||||
|
||||
### From Source
|
||||
You can use the `go` tool to install `grpcurl`:
|
||||
```shell
|
||||
go get github.com/fullstorydev/grpcurl
|
||||
@@ -59,7 +65,11 @@ If you have already pulled down this repo to a location that is not in your
|
||||
run `make install`.
|
||||
|
||||
If you encounter compile errors, you could have out-dated versions of `grpcurl`'s
|
||||
dependencies. You can update the dependencies by running `make updatedeps`.
|
||||
dependencies. You can update the dependencies by running `make updatedeps`. You can
|
||||
also use [`vgo`](https://github.com/golang/vgo) to install, which will use the right
|
||||
versions of dependencies. Or, if you are using Go 1.11, you can add `GO111MODULE=on`
|
||||
as a prefix to the commands above, which will also build using the right versions of
|
||||
dependencies (vs. whatever you may already in your `GOPATH`).
|
||||
|
||||
## Usage
|
||||
The usage doc for the tool explains the numerous options:
|
||||
@@ -72,7 +82,7 @@ In the sections below, you will find numerous examples demonstrating how to use
|
||||
|
||||
### Invoking RPCs
|
||||
Invoking an RPC on a trusted server (e.g. TLS without self-signed key or custom CA)
|
||||
that requires no client certs and supports service reflection is the simplest thing to
|
||||
that requires no client certs and supports server reflection is the simplest thing to
|
||||
do with `grpcurl`. This minimal invocation sends an empty request body:
|
||||
```shell
|
||||
grpcurl grpc.server.com:443 my.custom.server.Service/Method
|
||||
@@ -125,8 +135,10 @@ grpcurl localhost:8787 list my.custom.server.Service
|
||||
|
||||
### Describing Elements
|
||||
The "describe" verb will print the type of any symbol that the server knows about
|
||||
or that is found in a given protoset file and also print the full descriptor for the
|
||||
symbol, in JSON.
|
||||
or that is found in a given protoset file. It also prints a description of that
|
||||
symbol, in the form of snippets of proto source. It won't necessarily be the
|
||||
original source that defined the element, but it will be equivalent.
|
||||
|
||||
```shell
|
||||
# Server supports reflection
|
||||
grpcurl localhost:8787 describe my.custom.server.Service.MethodOne
|
||||
@@ -138,7 +150,24 @@ grpcurl -protoset my-protos.bin describe my.custom.server.Service.MethodOne
|
||||
grpcurl -import-path ../protos -proto my-stuff.proto describe my.custom.server.Service.MethodOne
|
||||
```
|
||||
|
||||
## Proto Source Files
|
||||
## Descriptor Sources
|
||||
The `grpcurl` tool can operate on a variety of sources for descriptors. The descriptors
|
||||
are required, in order for `grpcurl` to understand the RPC schema, translate inputs
|
||||
into the protobuf binary format as well as translate responses from the binary format
|
||||
into text. The sections below document the supported sources and what command-line flags
|
||||
are needed to use them.
|
||||
|
||||
### Server Reflection
|
||||
|
||||
Without any additional command-line flags, `grpcurl` will try to use [server reflection](https://github.com/grpc/grpc/blob/master/src/proto/grpc/reflection/v1alpha/reflection.proto).
|
||||
|
||||
Examples for how to set up server reflection can be found [here](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md#known-implementations).
|
||||
|
||||
When using reflection, the server address (host:port or path to Unix socket) is required
|
||||
even for "list" and "describe" operations, so that `grpcurl` can connect to the server
|
||||
and ask it for its descriptors.
|
||||
|
||||
### Proto Source Files
|
||||
To use `grpcurl` on servers that do not support reflection, you can use `.proto` source
|
||||
files.
|
||||
|
||||
@@ -151,14 +180,18 @@ location of the standard protos included with `protoc` (which contain various "w
|
||||
types" with a package definition of `google.protobuf`). These files are "known" by `grpcurl`
|
||||
as a snapshot of their descriptors is built into the `grpcurl` binary.
|
||||
|
||||
## Protoset Files
|
||||
When using proto sources, you can omit the server address (host:port or path to Unix socket)
|
||||
when using the "list" and "describe" operations since they only need to consult the proto
|
||||
source files.
|
||||
|
||||
### Protoset Files
|
||||
You can also use compiled protoset files with `grpcurl`. If you are scripting `grpcurl` and
|
||||
need to re-use the same proto sources for many invocations, you will see better performance
|
||||
by using protoset files (since it skips the parsing and compilation steps with each
|
||||
invocation).
|
||||
|
||||
Protoset files contain binary encoded `google.protobuf.FileDescriptorSet` protos. To create
|
||||
a protoset file, invoke `protoc` with the `*.proto` files that describe the service:
|
||||
a protoset file, invoke `protoc` with the `*.proto` files that define the service:
|
||||
```shell
|
||||
protoc --proto_path=. \
|
||||
--descriptor_set_out=myservice.protoset \
|
||||
@@ -169,3 +202,8 @@ protoc --proto_path=. \
|
||||
The `--descriptor_set_out` argument is what tells `protoc` to produce a protoset,
|
||||
and the `--include_imports` argument is necessary for the protoset to contain
|
||||
everything that `grpcurl` needs to process and understand the schema.
|
||||
|
||||
When using protosets, you can omit the server address (host:port or path to Unix socket)
|
||||
when using the "list" and "describe" operations since they only need to consult the
|
||||
protoset files.
|
||||
|
||||
|
||||
9
cmd/grpcurl/go1_10.go
Normal file
9
cmd/grpcurl/go1_10.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build go1.10
|
||||
|
||||
package main
|
||||
|
||||
func indent() string {
|
||||
// In Go 1.10 and up, the flag package automatically
|
||||
// adds the right indentation.
|
||||
return ""
|
||||
}
|
||||
9
cmd/grpcurl/go1_9.go
Normal file
9
cmd/grpcurl/go1_9.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// +build !go1.10
|
||||
|
||||
package main
|
||||
|
||||
func indent() string {
|
||||
// In Go 1.9 and older, we need to add indentation
|
||||
// after newlines in the flag doc strings.
|
||||
return " \t"
|
||||
}
|
||||
@@ -1,22 +1,20 @@
|
||||
// Command grpcurl makes GRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor file or
|
||||
// service reflection to translate JSON request data into the appropriate protobuf request data and vice
|
||||
// versa for presenting the response contents.
|
||||
// Command grpcurl makes gRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor
|
||||
// file, protobuf sources, or service reflection to translate JSON or text request data into the
|
||||
// appropriate protobuf messages and vice versa for presenting the response contents.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/fullstorydev/grpcurl"
|
||||
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"github.com/jhump/protoreflect/dynamic"
|
||||
"github.com/jhump/protoreflect/grpcreflect"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
@@ -25,107 +23,119 @@ import (
|
||||
"google.golang.org/grpc/keepalive"
|
||||
"google.golang.org/grpc/metadata"
|
||||
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/fullstorydev/grpcurl"
|
||||
)
|
||||
|
||||
var version = "dev build <no version set>"
|
||||
|
||||
var (
|
||||
exit = os.Exit
|
||||
|
||||
isUnixSocket func() bool // nil when run on non-unix platform
|
||||
|
||||
help = flag.Bool("help", false,
|
||||
`Print usage instructions and exit.`)
|
||||
plaintext = flag.Bool("plaintext", false,
|
||||
`Use plain-text HTTP/2 when connecting to server (no TLS).`)
|
||||
insecure = flag.Bool("insecure", false,
|
||||
`Skip server certificate and domain verification. (NOT SECURE!). Not
|
||||
valid with -plaintext option.`)
|
||||
cacert = flag.String("cacert", "",
|
||||
`File containing trusted root certificates for verifying the server.
|
||||
Ignored if -insecure is specified.`)
|
||||
cert = flag.String("cert", "",
|
||||
`File containing client certificate (public key), to present to the
|
||||
server. Not valid with -plaintext option. Must also provide -key option.`)
|
||||
key = flag.String("key", "",
|
||||
`File containing client private key, to present to the server. Not valid
|
||||
with -plaintext option. Must also provide -cert option.`)
|
||||
help = flag.Bool("help", false, prettify(`
|
||||
Print usage instructions and exit.`))
|
||||
printVersion = flag.Bool("version", false, prettify(`
|
||||
Print version.`))
|
||||
plaintext = flag.Bool("plaintext", false, prettify(`
|
||||
Use plain-text HTTP/2 when connecting to server (no TLS).`))
|
||||
insecure = flag.Bool("insecure", false, prettify(`
|
||||
Skip server certificate and domain verification. (NOT SECURE!) Not
|
||||
valid with -plaintext option.`))
|
||||
cacert = flag.String("cacert", "", prettify(`
|
||||
File containing trusted root certificates for verifying the server.
|
||||
Ignored if -insecure is specified.`))
|
||||
cert = flag.String("cert", "", prettify(`
|
||||
File containing client certificate (public key), to present to the
|
||||
server. Not valid with -plaintext option. Must also provide -key option.`))
|
||||
key = flag.String("key", "", prettify(`
|
||||
File containing client private key, to present to the server. Not valid
|
||||
with -plaintext option. Must also provide -cert option.`))
|
||||
protoset multiString
|
||||
protoFiles multiString
|
||||
importPaths multiString
|
||||
addlHeaders multiString
|
||||
rpcHeaders multiString
|
||||
reflHeaders multiString
|
||||
authority = flag.String("authority", "",
|
||||
":authority pseudo header value to be passed along with underlying HTTP/2 requests. It defaults to `host [ \":\" port ]` part of the target url.")
|
||||
data = flag.String("d", "",
|
||||
`JSON request contents. If the value is '@' then the request contents are
|
||||
read from stdin. For calls that accept a stream of requests, the
|
||||
contents should include all such request messages concatenated together
|
||||
(optionally separated by whitespace).`)
|
||||
connectTimeout = flag.String("connect-timeout", "",
|
||||
`The maximum time, in seconds, to wait for connection to be established.
|
||||
Defaults to 10 seconds.`)
|
||||
keepaliveTime = flag.String("keepalive-time", "",
|
||||
`If present, the maximum idle time in seconds, after which a keepalive
|
||||
probe is sent. If the connection remains idle and no keepalive response
|
||||
is received for this same period then the connection is closed and the
|
||||
operation fails.`)
|
||||
maxTime = flag.String("max-time", "",
|
||||
`The maximum total time the operation can take. This is useful for
|
||||
preventing batch jobs that use grpcurl from hanging due to slow or bad
|
||||
network links or due to incorrect stream method usage.`)
|
||||
emitDefaults = flag.Bool("emit-defaults", false,
|
||||
`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,
|
||||
`Enable verbose output.`)
|
||||
serverName = flag.String("servername", "", "Override servername when validating TLS certificate.")
|
||||
authority = flag.String("authority", "", prettify(`
|
||||
Value of :authority pseudo-header to be use with underlying HTTP/2
|
||||
requests. It defaults to the given address.`))
|
||||
data = flag.String("d", "", prettify(`
|
||||
Data for request contents. If the value is '@' then the request contents
|
||||
are read from stdin. For calls that accept a stream of requests, the
|
||||
contents should include all such request messages concatenated together
|
||||
(possibly delimited; see -format).`))
|
||||
format = flag.String("format", "json", prettify(`
|
||||
The format of request data. The allowed values are 'json' or 'text'. For
|
||||
'json', the input data must be in JSON format. Multiple request values
|
||||
may be concatenated (messages with a JSON representation other than
|
||||
object must be separated by whitespace, such as a newline). For 'text',
|
||||
the input data must be in the protobuf text format, in which case
|
||||
multiple request values must be separated by the "record separator"
|
||||
ASCII character: 0x1E. The stream should not end in a record separator.
|
||||
If it does, it will be interpreted as a final, blank message after the
|
||||
separator.`))
|
||||
connectTimeout = flag.String("connect-timeout", "", prettify(`
|
||||
The maximum time, in seconds, to wait for connection to be established.
|
||||
Defaults to 10 seconds.`))
|
||||
keepaliveTime = flag.String("keepalive-time", "", prettify(`
|
||||
If present, the maximum idle time in seconds, after which a keepalive
|
||||
probe is sent. If the connection remains idle and no keepalive response
|
||||
is received for this same period then the connection is closed and the
|
||||
operation fails.`))
|
||||
maxTime = flag.String("max-time", "", prettify(`
|
||||
The maximum total time the operation can take. This is useful for
|
||||
preventing batch jobs that use grpcurl from hanging due to slow or bad
|
||||
network links or due to incorrect stream method usage.`))
|
||||
emitDefaults = flag.Bool("emit-defaults", false, prettify(`
|
||||
Emit default values for JSON-encoded responses.`))
|
||||
msgTemplate = flag.Bool("msg-template", false, prettify(`
|
||||
When describing messages, show a template of input data.`))
|
||||
verbose = flag.Bool("v", false, prettify(`
|
||||
Enable verbose output.`))
|
||||
serverName = flag.String("servername", "", prettify(`
|
||||
Override server name when validating TLS certificate.`))
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.Var(&addlHeaders, "H",
|
||||
`Additional headers in 'name: value' format. May specify more than one
|
||||
via multiple flags. These headers will also be included in reflection
|
||||
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",
|
||||
`The name of a file containing an encoded FileDescriptorSet. This file's
|
||||
contents will be used to determine the RPC schema instead of querying
|
||||
for it from the remote server via the GRPC reflection API. When set: the
|
||||
'list' action lists the services found in the given descriptors (vs.
|
||||
those exposed by the remote server), and the 'describe' action describes
|
||||
symbols found in the given descriptors. May specify more than one via
|
||||
multiple -protoset flags. It is an error to use both -protoset and
|
||||
-proto flags.`)
|
||||
flag.Var(&protoFiles, "proto",
|
||||
`The name of a proto source file. Source files given will be used to
|
||||
determine the RPC schema instead of querying for it from the remote
|
||||
server via the GRPC reflection API. When set: the 'list' action lists
|
||||
the services found in the given files and their imports (vs. those
|
||||
exposed by the remote server), and the 'describe' action describes
|
||||
symbols found in the given files. May specify more than one via
|
||||
multiple -proto flags. Imports will be resolved using the given
|
||||
-import-path flags. Multiple proto files can be specified by specifying
|
||||
multiple -proto flags. It is an error to use both -protoset and -proto
|
||||
flags.`)
|
||||
flag.Var(&importPaths, "import-path",
|
||||
`The path to a directory from which proto sources can be imported,
|
||||
for use with -proto flags. Multiple import paths can be configured by
|
||||
specifying multiple -import-path flags. Paths will be searched in the
|
||||
order given. If no import paths are given, all files (including all
|
||||
imports) must be provided as -proto flags, and grpcurl will attempt to
|
||||
resolve all import statements from the set of file names given.`)
|
||||
flag.Var(&addlHeaders, "H", prettify(`
|
||||
Additional headers in 'name: value' format. May specify more than one
|
||||
via multiple flags. These headers will also be included in reflection
|
||||
requests requests to a server.`))
|
||||
flag.Var(&rpcHeaders, "rpc-header", prettify(`
|
||||
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", prettify(`
|
||||
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", prettify(`
|
||||
The name of a file containing an encoded FileDescriptorSet. This file's
|
||||
contents will be used to determine the RPC schema instead of querying
|
||||
for it from the remote server via the gRPC reflection API. When set: the
|
||||
'list' action lists the services found in the given descriptors (vs.
|
||||
those exposed by the remote server), and the 'describe' action describes
|
||||
symbols found in the given descriptors. May specify more than one via
|
||||
multiple -protoset flags. It is an error to use both -protoset and
|
||||
-proto flags.`))
|
||||
flag.Var(&protoFiles, "proto", prettify(`
|
||||
The name of a proto source file. Source files given will be used to
|
||||
determine the RPC schema instead of querying for it from the remote
|
||||
server via the gRPC reflection API. When set: the 'list' action lists
|
||||
the services found in the given files and their imports (vs. those
|
||||
exposed by the remote server), and the 'describe' action describes
|
||||
symbols found in the given files. May specify more than one via multiple
|
||||
-proto flags. Imports will be resolved using the given -import-path
|
||||
flags. Multiple proto files can be specified by specifying multiple
|
||||
-proto flags. It is an error to use both -protoset and -proto flags.`))
|
||||
flag.Var(&importPaths, "import-path", prettify(`
|
||||
The path to a directory from which proto sources can be imported, for
|
||||
use with -proto flags. Multiple import paths can be configured by
|
||||
specifying multiple -import-path flags. Paths will be searched in the
|
||||
order given. If no import paths are given, all files (including all
|
||||
imports) must be provided as -proto flags, and grpcurl will attempt to
|
||||
resolve all import statements from the set of file names given.`))
|
||||
}
|
||||
|
||||
type multiString []string
|
||||
@@ -146,6 +156,10 @@ func main() {
|
||||
usage()
|
||||
os.Exit(0)
|
||||
}
|
||||
if *printVersion {
|
||||
fmt.Fprintf(os.Stderr, "%s %s\n", os.Args[0], version)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Do extra validation on arguments and figure out what user asked us to do.
|
||||
if *plaintext && *insecure {
|
||||
@@ -160,6 +174,12 @@ func main() {
|
||||
if (*key == "") != (*cert == "") {
|
||||
fail(nil, "The -cert and -key arguments must be used together and both be present.")
|
||||
}
|
||||
if *format != "json" && *format != "text" {
|
||||
fail(nil, "The -format option must be 'json' or 'text.")
|
||||
}
|
||||
if *emitDefaults && *format != "json" {
|
||||
warn("The -emit-defaults is only used when using json format.")
|
||||
}
|
||||
|
||||
args := flag.Args()
|
||||
|
||||
@@ -378,43 +398,77 @@ func main() {
|
||||
fail(err, "Failed to resolve symbol %q", s)
|
||||
}
|
||||
|
||||
txt, err := grpcurl.GetDescriptorText(dsc, descSource)
|
||||
if err != nil {
|
||||
fail(err, "Failed to describe symbol %q", s)
|
||||
}
|
||||
|
||||
switch dsc.(type) {
|
||||
fqn := dsc.GetFullyQualifiedName()
|
||||
var elementType string
|
||||
switch d := dsc.(type) {
|
||||
case *desc.MessageDescriptor:
|
||||
fmt.Printf("%s is a message:\n", dsc.GetFullyQualifiedName())
|
||||
elementType = "a message"
|
||||
parent, ok := d.GetParent().(*desc.MessageDescriptor)
|
||||
if ok {
|
||||
if d.IsMapEntry() {
|
||||
for _, f := range parent.GetFields() {
|
||||
if f.IsMap() && f.GetMessageType() == d {
|
||||
// found it: describe the map field instead
|
||||
elementType = "the entry type for a map field"
|
||||
dsc = f
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// see if it's a group
|
||||
for _, f := range parent.GetFields() {
|
||||
if f.GetType() == descpb.FieldDescriptorProto_TYPE_GROUP && f.GetMessageType() == d {
|
||||
// found it: describe the map field instead
|
||||
elementType = "the type of a group field"
|
||||
dsc = f
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case *desc.FieldDescriptor:
|
||||
fmt.Printf("%s is a field:\n", dsc.GetFullyQualifiedName())
|
||||
elementType = "a field"
|
||||
if d.GetType() == descpb.FieldDescriptorProto_TYPE_GROUP {
|
||||
elementType = "a group field"
|
||||
} else if d.IsExtension() {
|
||||
elementType = "an extension"
|
||||
}
|
||||
case *desc.OneOfDescriptor:
|
||||
fmt.Printf("%s is a one-of:\n", dsc.GetFullyQualifiedName())
|
||||
elementType = "a one-of"
|
||||
case *desc.EnumDescriptor:
|
||||
fmt.Printf("%s is an enum:\n", dsc.GetFullyQualifiedName())
|
||||
elementType = "an enum"
|
||||
case *desc.EnumValueDescriptor:
|
||||
fmt.Printf("%s is an enum value:\n", dsc.GetFullyQualifiedName())
|
||||
elementType = "an enum value"
|
||||
case *desc.ServiceDescriptor:
|
||||
fmt.Printf("%s is a service:\n", dsc.GetFullyQualifiedName())
|
||||
elementType = "a service"
|
||||
case *desc.MethodDescriptor:
|
||||
fmt.Printf("%s is a method:\n", dsc.GetFullyQualifiedName())
|
||||
elementType = "a method"
|
||||
default:
|
||||
err = fmt.Errorf("descriptor has unrecognized type %T", dsc)
|
||||
fail(err, "Failed to describe symbol %q", s)
|
||||
}
|
||||
|
||||
txt, err := grpcurl.GetDescriptorText(dsc, descSource)
|
||||
if err != nil {
|
||||
fail(err, "Failed to describe symbol %q", s)
|
||||
}
|
||||
fmt.Printf("%s is %s:\n", fqn, elementType)
|
||||
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}
|
||||
err := jsm.Marshal(os.Stdout, tmpl)
|
||||
tmpl := grpcurl.MakeTemplate(dsc)
|
||||
_, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, true, false, nil)
|
||||
if err != nil {
|
||||
fail(err, "Failed to construct formatter for %q", *format)
|
||||
}
|
||||
str, err := formatter(tmpl)
|
||||
if err != nil {
|
||||
fail(err, "Failed to print template for message %s", s)
|
||||
}
|
||||
fmt.Println()
|
||||
fmt.Println("\nMessage template:")
|
||||
fmt.Println(str)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,31 +477,41 @@ func main() {
|
||||
if cc == nil {
|
||||
cc = dial()
|
||||
}
|
||||
var dec *json.Decoder
|
||||
var in io.Reader
|
||||
if *data == "@" {
|
||||
dec = json.NewDecoder(os.Stdin)
|
||||
in = os.Stdin
|
||||
} else {
|
||||
dec = json.NewDecoder(strings.NewReader(*data))
|
||||
in = strings.NewReader(*data)
|
||||
}
|
||||
|
||||
h := &handler{dec: dec, descSource: descSource}
|
||||
err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, h.getRequestData)
|
||||
// if not verbose output, then also include record delimiters
|
||||
// between each message, so output could potentially be piped
|
||||
// to another grpcurl process
|
||||
includeSeparators := !*verbose
|
||||
rf, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, *emitDefaults, includeSeparators, in)
|
||||
if err != nil {
|
||||
fail(err, "Failed to construct request parser and formatter for %q", *format)
|
||||
}
|
||||
h := grpcurl.NewDefaultEventHandler(os.Stdout, descSource, formatter, *verbose)
|
||||
|
||||
err = grpcurl.InvokeRPC(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, rf.Next)
|
||||
if err != nil {
|
||||
fail(err, "Error invoking method %q", symbol)
|
||||
}
|
||||
reqSuffix := ""
|
||||
respSuffix := ""
|
||||
if h.reqCount != 1 {
|
||||
reqCount := rf.NumRequests()
|
||||
if reqCount != 1 {
|
||||
reqSuffix = "s"
|
||||
}
|
||||
if h.respCount != 1 {
|
||||
if h.NumResponses != 1 {
|
||||
respSuffix = "s"
|
||||
}
|
||||
if *verbose {
|
||||
fmt.Printf("Sent %d request%s and received %d response%s\n", h.reqCount, reqSuffix, h.respCount, respSuffix)
|
||||
fmt.Printf("Sent %d request%s and received %d response%s\n", reqCount, reqSuffix, h.NumResponses, respSuffix)
|
||||
}
|
||||
if h.stat.Code() != codes.OK {
|
||||
fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.stat.Code().String(), h.stat.Message())
|
||||
if h.Status.Code() != codes.OK {
|
||||
fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.Status.Code().String(), h.Status.Message())
|
||||
exit(1)
|
||||
}
|
||||
}
|
||||
@@ -457,8 +521,8 @@ func usage() {
|
||||
fmt.Fprintf(os.Stderr, `Usage:
|
||||
%s [flags] [address] [list|describe] [symbol]
|
||||
|
||||
The 'host:port' is only optional when used with 'list' or 'describe' and a
|
||||
protoset flag is provided.
|
||||
The 'address' is only optional when used with 'list' or 'describe' and a
|
||||
protoset or proto flag is provided.
|
||||
|
||||
If 'list' is indicated, the symbol (if present) should be a fully-qualified
|
||||
service name. If present, all methods of that service are listed. If not
|
||||
@@ -470,17 +534,37 @@ is given then the descriptors for all exposed or known services are shown.
|
||||
|
||||
If neither verb is present, the symbol must be a fully-qualified method name in
|
||||
'service/method' or 'service.method' format. In this case, the request body will
|
||||
be used to invoke the named method. If no body is given, an empty instance of
|
||||
the method's request type will be sent.
|
||||
be used to invoke the named method. If no body is given but one is required
|
||||
(i.e. the method is unary or server-streaming), an empty instance of the
|
||||
method's request type will be sent.
|
||||
|
||||
The address will typically be in the form "host:port" where host can be an IP
|
||||
address or a hostname and port is a numeric port or service name. If an IPv6
|
||||
address is given, it must be surrounded by brackets, like "[2001:db8::1]". For
|
||||
Unix variants, if a -unix=true flag is present, then the address must be the
|
||||
path to the domain socket.
|
||||
|
||||
Available flags:
|
||||
`, os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func prettify(docString string) string {
|
||||
parts := strings.Split(docString, "\n")
|
||||
|
||||
// cull empty lines and also remove trailing and leading spaces
|
||||
// from each line in the doc string
|
||||
j := 0
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
parts[j] = part
|
||||
j++
|
||||
}
|
||||
|
||||
return strings.Join(parts[:j], "\n"+indent())
|
||||
}
|
||||
|
||||
func warn(msg string, args ...interface{}) {
|
||||
@@ -503,125 +587,3 @@ func fail(err error, msg string, args ...interface{}) {
|
||||
exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
dec *json.Decoder
|
||||
descSource grpcurl.DescriptorSource
|
||||
reqCount int
|
||||
respCount int
|
||||
stat *status.Status
|
||||
}
|
||||
|
||||
func (h *handler) OnResolveMethod(md *desc.MethodDescriptor) {
|
||||
if *verbose {
|
||||
txt, err := grpcurl.GetDescriptorText(md, h.descSource)
|
||||
if err == nil {
|
||||
fmt.Printf("\nResolved method descriptor:\n%s\n", txt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (*handler) OnSendHeaders(md metadata.MD) {
|
||||
if *verbose {
|
||||
fmt.Printf("\nRequest metadata to send:\n%s\n", grpcurl.MetadataToString(md))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) getRequestData() ([]byte, error) {
|
||||
// we don't use a mutex, though this methods will be called from different goroutine
|
||||
// than other methods for bidi calls, because this method does not share any state
|
||||
// with the other methods.
|
||||
var msg json.RawMessage
|
||||
if err := h.dec.Decode(&msg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.reqCount++
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (*handler) OnReceiveHeaders(md metadata.MD) {
|
||||
if *verbose {
|
||||
fmt.Printf("\nResponse headers received:\n%s\n", grpcurl.MetadataToString(md))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) OnReceiveResponse(resp proto.Message) {
|
||||
h.respCount++
|
||||
if *verbose {
|
||||
fmt.Print("\nResponse contents:\n")
|
||||
}
|
||||
jsm := jsonpb.Marshaler{EmitDefaults: *emitDefaults, Indent: " "}
|
||||
respStr, err := jsm.MarshalToString(resp)
|
||||
if err != nil {
|
||||
fail(err, "failed to generate JSON form of response message")
|
||||
}
|
||||
fmt.Println(respStr)
|
||||
}
|
||||
|
||||
func (h *handler) OnReceiveTrailers(stat *status.Status, md metadata.MD) {
|
||||
h.stat = stat
|
||||
if *verbose {
|
||||
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
|
||||
}
|
||||
|
||||
46
cmd/grpcurl/indent_test.go
Normal file
46
cmd/grpcurl/indent_test.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFlagDocIndent(t *testing.T) {
|
||||
// Tests the prettify() and indent() function. The indent() function
|
||||
// differs by Go version, due to differences in "flags" package across
|
||||
// versions. Run with multiple versions of Go to ensure that doc output
|
||||
// is properly indented, regardless of Go version.
|
||||
|
||||
var fs flag.FlagSet
|
||||
var buf bytes.Buffer
|
||||
fs.SetOutput(&buf)
|
||||
|
||||
fs.String("foo", "", prettify(`
|
||||
This is a flag doc string.
|
||||
It has multiple lines.
|
||||
More than two, actually.`))
|
||||
fs.Int("bar", 100, prettify(`This is a simple flag doc string.`))
|
||||
fs.Bool("baz", false, prettify(`
|
||||
This is another long doc string.
|
||||
It also has multiple lines. But not as long as the first one.`))
|
||||
|
||||
fs.PrintDefaults()
|
||||
|
||||
expected :=
|
||||
` -bar int
|
||||
This is a simple flag doc string. (default 100)
|
||||
-baz
|
||||
This is another long doc string.
|
||||
It also has multiple lines. But not as long as the first one.
|
||||
-foo string
|
||||
This is a flag doc string.
|
||||
It has multiple lines.
|
||||
More than two, actually.
|
||||
`
|
||||
|
||||
actual := buf.String()
|
||||
if actual != expected {
|
||||
t.Errorf("Flag output had wrong indentation.\nExpecting:\n%s\nGot:\n%s", expected, actual)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,8 @@ package main
|
||||
import "flag"
|
||||
|
||||
var (
|
||||
unix = flag.Bool("unix", false,
|
||||
`Indicates that the server address is the path to a Unix domain socket.`)
|
||||
unix = flag.Bool("unix", false, prettify(`
|
||||
Indicates that the server address is the path to a Unix domain socket.`))
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
248
desc_source.go
Normal file
248
desc_source.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package grpcurl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"github.com/jhump/protoreflect/desc/protoparse"
|
||||
"github.com/jhump/protoreflect/dynamic"
|
||||
"github.com/jhump/protoreflect/grpcreflect"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// ErrReflectionNotSupported is returned by DescriptorSource operations that
|
||||
// rely on interacting with the reflection service when the source does not
|
||||
// actually expose the reflection service. When this occurs, an alternate source
|
||||
// (like file descriptor sets) must be used.
|
||||
var ErrReflectionNotSupported = errors.New("server does not support the reflection API")
|
||||
|
||||
// DescriptorSource is a source of protobuf descriptor information. It can be backed by a FileDescriptorSet
|
||||
// proto (like a file generated by protoc) or a remote server that supports the reflection API.
|
||||
type DescriptorSource interface {
|
||||
// ListServices returns a list of fully-qualified service names. It will be all services in a set of
|
||||
// descriptor files or the set of all services exposed by a gRPC server.
|
||||
ListServices() ([]string, error)
|
||||
// FindSymbol returns a descriptor for the given fully-qualified symbol name.
|
||||
FindSymbol(fullyQualifiedName string) (desc.Descriptor, error)
|
||||
// AllExtensionsForType returns all known extension fields that extend the given message type name.
|
||||
AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error)
|
||||
}
|
||||
|
||||
// DescriptorSourceFromProtoSets creates a DescriptorSource that is backed by the named files, whose contents
|
||||
// are encoded FileDescriptorSet protos.
|
||||
func DescriptorSourceFromProtoSets(fileNames ...string) (DescriptorSource, error) {
|
||||
files := &descpb.FileDescriptorSet{}
|
||||
for _, fileName := range fileNames {
|
||||
b, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load protoset file %q: %v", fileName, err)
|
||||
}
|
||||
var fs descpb.FileDescriptorSet
|
||||
err = proto.Unmarshal(b, &fs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse contents of protoset file %q: %v", fileName, err)
|
||||
}
|
||||
files.File = append(files.File, fs.File...)
|
||||
}
|
||||
return DescriptorSourceFromFileDescriptorSet(files)
|
||||
}
|
||||
|
||||
// DescriptorSourceFromProtoFiles creates a DescriptorSource that is backed by the named files,
|
||||
// whose contents are Protocol Buffer source files. The given importPaths are used to locate
|
||||
// any imported files.
|
||||
func DescriptorSourceFromProtoFiles(importPaths []string, fileNames ...string) (DescriptorSource, error) {
|
||||
p := protoparse.Parser{
|
||||
ImportPaths: importPaths,
|
||||
InferImportPaths: len(importPaths) == 0,
|
||||
}
|
||||
fds, err := p.ParseFiles(fileNames...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse given files: %v", err)
|
||||
}
|
||||
return DescriptorSourceFromFileDescriptors(fds...)
|
||||
}
|
||||
|
||||
// DescriptorSourceFromFileDescriptorSet creates a DescriptorSource that is backed by the FileDescriptorSet.
|
||||
func DescriptorSourceFromFileDescriptorSet(files *descpb.FileDescriptorSet) (DescriptorSource, error) {
|
||||
unresolved := map[string]*descpb.FileDescriptorProto{}
|
||||
for _, fd := range files.File {
|
||||
unresolved[fd.GetName()] = fd
|
||||
}
|
||||
resolved := map[string]*desc.FileDescriptor{}
|
||||
for _, fd := range files.File {
|
||||
_, err := resolveFileDescriptor(unresolved, resolved, fd.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &fileSource{files: resolved}, nil
|
||||
}
|
||||
|
||||
func resolveFileDescriptor(unresolved map[string]*descpb.FileDescriptorProto, resolved map[string]*desc.FileDescriptor, filename string) (*desc.FileDescriptor, error) {
|
||||
if r, ok := resolved[filename]; ok {
|
||||
return r, nil
|
||||
}
|
||||
fd, ok := unresolved[filename]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no descriptor found for %q", filename)
|
||||
}
|
||||
deps := make([]*desc.FileDescriptor, 0, len(fd.GetDependency()))
|
||||
for _, dep := range fd.GetDependency() {
|
||||
depFd, err := resolveFileDescriptor(unresolved, resolved, dep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deps = append(deps, depFd)
|
||||
}
|
||||
result, err := desc.CreateFileDescriptor(fd, deps...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved[filename] = result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DescriptorSourceFromFileDescriptorSet creates a DescriptorSource that is backed by the given
|
||||
// file descriptors
|
||||
func DescriptorSourceFromFileDescriptors(files ...*desc.FileDescriptor) (DescriptorSource, error) {
|
||||
fds := map[string]*desc.FileDescriptor{}
|
||||
for _, fd := range files {
|
||||
if err := addFile(fd, fds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &fileSource{files: fds}, nil
|
||||
}
|
||||
|
||||
func addFile(fd *desc.FileDescriptor, fds map[string]*desc.FileDescriptor) error {
|
||||
name := fd.GetName()
|
||||
if existing, ok := fds[name]; ok {
|
||||
// already added this file
|
||||
if existing != fd {
|
||||
// doh! duplicate files provided
|
||||
return fmt.Errorf("given files include multiple copies of %q", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
fds[name] = fd
|
||||
for _, dep := range fd.GetDependencies() {
|
||||
if err := addFile(dep, fds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileSource struct {
|
||||
files map[string]*desc.FileDescriptor
|
||||
er *dynamic.ExtensionRegistry
|
||||
erInit sync.Once
|
||||
}
|
||||
|
||||
func (fs *fileSource) ListServices() ([]string, error) {
|
||||
set := map[string]bool{}
|
||||
for _, fd := range fs.files {
|
||||
for _, svc := range fd.GetServices() {
|
||||
set[svc.GetFullyQualifiedName()] = true
|
||||
}
|
||||
}
|
||||
sl := make([]string, 0, len(set))
|
||||
for svc := range set {
|
||||
sl = append(sl, svc)
|
||||
}
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
// GetAllFiles returns all of the underlying file descriptors. This is
|
||||
// more thorough and more efficient than the fallback strategy used by
|
||||
// the GetAllFiles package method, for enumerating all files from a
|
||||
// descriptor source.
|
||||
func (fs *fileSource) GetAllFiles() ([]*desc.FileDescriptor, error) {
|
||||
files := make([]*desc.FileDescriptor, len(fs.files))
|
||||
i := 0
|
||||
for _, fd := range fs.files {
|
||||
files[i] = fd
|
||||
i++
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (fs *fileSource) FindSymbol(fullyQualifiedName string) (desc.Descriptor, error) {
|
||||
for _, fd := range fs.files {
|
||||
if dsc := fd.FindSymbol(fullyQualifiedName); dsc != nil {
|
||||
return dsc, nil
|
||||
}
|
||||
}
|
||||
return nil, notFound("Symbol", fullyQualifiedName)
|
||||
}
|
||||
|
||||
func (fs *fileSource) AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error) {
|
||||
fs.erInit.Do(func() {
|
||||
fs.er = &dynamic.ExtensionRegistry{}
|
||||
for _, fd := range fs.files {
|
||||
fs.er.AddExtensionsFromFile(fd)
|
||||
}
|
||||
})
|
||||
return fs.er.AllExtensionsForType(typeName), nil
|
||||
}
|
||||
|
||||
// DescriptorSourceFromServer creates a DescriptorSource that uses the given gRPC reflection client
|
||||
// to interrogate a server for descriptor information. If the server does not support the reflection
|
||||
// API then the various DescriptorSource methods will return ErrReflectionNotSupported
|
||||
func DescriptorSourceFromServer(_ context.Context, refClient *grpcreflect.Client) DescriptorSource {
|
||||
return serverSource{client: refClient}
|
||||
}
|
||||
|
||||
type serverSource struct {
|
||||
client *grpcreflect.Client
|
||||
}
|
||||
|
||||
func (ss serverSource) ListServices() ([]string, error) {
|
||||
svcs, err := ss.client.ListServices()
|
||||
return svcs, reflectionSupport(err)
|
||||
}
|
||||
|
||||
func (ss serverSource) FindSymbol(fullyQualifiedName string) (desc.Descriptor, error) {
|
||||
file, err := ss.client.FileContainingSymbol(fullyQualifiedName)
|
||||
if err != nil {
|
||||
return nil, reflectionSupport(err)
|
||||
}
|
||||
d := file.FindSymbol(fullyQualifiedName)
|
||||
if d == nil {
|
||||
return nil, notFound("Symbol", fullyQualifiedName)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (ss serverSource) AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error) {
|
||||
var exts []*desc.FieldDescriptor
|
||||
nums, err := ss.client.AllExtensionNumbersForType(typeName)
|
||||
if err != nil {
|
||||
return nil, reflectionSupport(err)
|
||||
}
|
||||
for _, fieldNum := range nums {
|
||||
ext, err := ss.client.ResolveExtension(typeName, fieldNum)
|
||||
if err != nil {
|
||||
return nil, reflectionSupport(err)
|
||||
}
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
return exts, nil
|
||||
}
|
||||
|
||||
func reflectionSupport(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if stat, ok := status.FromError(err); ok && stat.Code() == codes.Unimplemented {
|
||||
return ErrReflectionNotSupported
|
||||
}
|
||||
return err
|
||||
}
|
||||
297
format.go
Normal file
297
format.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package grpcurl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"github.com/jhump/protoreflect/dynamic"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// RequestParser processes input into messages.
|
||||
type RequestParser interface {
|
||||
// Next parses input data into the given request message. If called after
|
||||
// input is exhausted, it returns io.EOF. If the caller re-uses the same
|
||||
// instance in multiple calls to Next, it should call msg.Reset() in between
|
||||
// each call.
|
||||
Next(msg proto.Message) error
|
||||
// NumRequests returns the number of messages that have been parsed and
|
||||
// returned by a call to Next.
|
||||
NumRequests() int
|
||||
}
|
||||
|
||||
type jsonRequestParser struct {
|
||||
dec *json.Decoder
|
||||
unmarshaler jsonpb.Unmarshaler
|
||||
requestCount int
|
||||
}
|
||||
|
||||
// NewJSONRequestParser returns a RequestParser that reads data in JSON format
|
||||
// from the given reader. The given resolver is used to assist with decoding of
|
||||
// google.protobuf.Any messages.
|
||||
//
|
||||
// Input data that contains more than one message should just include all
|
||||
// messages concatenated (though whitespace is necessary to separate some kinds
|
||||
// of values in JSON).
|
||||
//
|
||||
// If the given reader has no data, the returned parser will return io.EOF on
|
||||
// the very first call.
|
||||
func NewJSONRequestParser(in io.Reader, resolver jsonpb.AnyResolver) RequestParser {
|
||||
return &jsonRequestParser{
|
||||
dec: json.NewDecoder(in),
|
||||
unmarshaler: jsonpb.Unmarshaler{AnyResolver: resolver},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *jsonRequestParser) Next(m proto.Message) error {
|
||||
var msg json.RawMessage
|
||||
if err := f.dec.Decode(&msg); err != nil {
|
||||
return err
|
||||
}
|
||||
f.requestCount++
|
||||
return f.unmarshaler.Unmarshal(bytes.NewReader(msg), m)
|
||||
}
|
||||
|
||||
func (f *jsonRequestParser) NumRequests() int {
|
||||
return f.requestCount
|
||||
}
|
||||
|
||||
const (
|
||||
textSeparatorChar = 0x1e
|
||||
)
|
||||
|
||||
type textRequestParser struct {
|
||||
r *bufio.Reader
|
||||
err error
|
||||
requestCount int
|
||||
}
|
||||
|
||||
// NewTextRequestParser returns a RequestParser that reads data in the protobuf
|
||||
// text format from the given reader.
|
||||
//
|
||||
// Input data that contains more than one message should include an ASCII
|
||||
// 'Record Separator' character (0x1E) between each message.
|
||||
//
|
||||
// Empty text is a valid text format and represents an empty message. So if the
|
||||
// given reader has no data, the returned parser will yield an empty message
|
||||
// for the first call to Next and then return io.EOF thereafter. This also means
|
||||
// that if the input data ends with a record separator, then a final empty
|
||||
// message will be parsed *after* the separator.
|
||||
func NewTextRequestParser(in io.Reader) RequestParser {
|
||||
return &textRequestParser{r: bufio.NewReader(in)}
|
||||
}
|
||||
|
||||
func (f *textRequestParser) Next(m proto.Message) error {
|
||||
if f.err != nil {
|
||||
return f.err
|
||||
}
|
||||
|
||||
var b []byte
|
||||
b, f.err = f.r.ReadBytes(textSeparatorChar)
|
||||
if f.err != nil && f.err != io.EOF {
|
||||
return f.err
|
||||
}
|
||||
// remove delimiter
|
||||
if len(b) > 0 && b[len(b)-1] == textSeparatorChar {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
|
||||
f.requestCount++
|
||||
|
||||
return proto.UnmarshalText(string(b), m)
|
||||
}
|
||||
|
||||
func (f *textRequestParser) NumRequests() int {
|
||||
return f.requestCount
|
||||
}
|
||||
|
||||
// Formatter translates messages into string representations.
|
||||
type Formatter func(proto.Message) (string, error)
|
||||
|
||||
// NewJSONFormatter returns a formatter that returns JSON strings. The JSON will
|
||||
// include empty/default values (instead of just omitted them) if emitDefaults
|
||||
// is true. The given resolver is used to assist with encoding of
|
||||
// google.protobuf.Any messages.
|
||||
func NewJSONFormatter(emitDefaults bool, resolver jsonpb.AnyResolver) Formatter {
|
||||
marshaler := jsonpb.Marshaler{
|
||||
EmitDefaults: emitDefaults,
|
||||
Indent: " ",
|
||||
AnyResolver: resolver,
|
||||
}
|
||||
return marshaler.MarshalToString
|
||||
}
|
||||
|
||||
// NewTextFormatter returns a formatter that returns strings in the protobuf
|
||||
// text format. If includeSeparator is true then, when invoked to format
|
||||
// multiple messages, all messages after the first one will be prefixed with the
|
||||
// ASCII 'Record Separator' character (0x1E).
|
||||
func NewTextFormatter(includeSeparator bool) Formatter {
|
||||
tf := textFormatter{useSeparator: includeSeparator}
|
||||
return tf.format
|
||||
}
|
||||
|
||||
type textFormatter struct {
|
||||
useSeparator bool
|
||||
numFormatted int
|
||||
}
|
||||
|
||||
func (tf *textFormatter) format(m proto.Message) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
if tf.useSeparator && tf.numFormatted > 0 {
|
||||
if err := buf.WriteByte(textSeparatorChar); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// If message implements MarshalText method (such as a *dynamic.Message),
|
||||
// it won't get details about whether or not to format to text compactly
|
||||
// or with indentation. So first see if the message also implements a
|
||||
// MarshalTextIndent method and use that instead if available.
|
||||
type indentMarshaler interface {
|
||||
MarshalTextIndent() ([]byte, error)
|
||||
}
|
||||
|
||||
if indenter, ok := m.(indentMarshaler); ok {
|
||||
b, err := indenter.MarshalTextIndent()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := buf.Write(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else if err := proto.MarshalText(&buf, m); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// no trailing newline needed
|
||||
str := buf.String()
|
||||
if str[len(str)-1] == '\n' {
|
||||
str = str[:len(str)-1]
|
||||
}
|
||||
|
||||
tf.numFormatted++
|
||||
|
||||
return str, nil
|
||||
}
|
||||
|
||||
type Format string
|
||||
|
||||
const (
|
||||
FormatJSON = Format("json")
|
||||
FormatText = Format("text")
|
||||
)
|
||||
|
||||
func anyResolver(source DescriptorSource) (jsonpb.AnyResolver, error) {
|
||||
// TODO: instead of pro-actively downloading file descriptors to
|
||||
// build a dynamic resolver, it would be better if the resolver
|
||||
// impl was lazy, and simply downloaded the descriptors as needed
|
||||
// when asked to resolve a particular type URL
|
||||
|
||||
// best effort: build resolver with whatever files we can
|
||||
// load, ignoring any errors
|
||||
files, _ := GetAllFiles(source)
|
||||
|
||||
var er dynamic.ExtensionRegistry
|
||||
for _, fd := range files {
|
||||
er.AddExtensionsFromFile(fd)
|
||||
}
|
||||
mf := dynamic.NewMessageFactoryWithExtensionRegistry(&er)
|
||||
return dynamic.AnyResolver(mf, files...), nil
|
||||
}
|
||||
|
||||
// RequestParserAndFormatterFor returns a request parser and formatter for the
|
||||
// given format. The given descriptor source may be used for parsing message
|
||||
// data (if needed by the format). The flags emitJSONDefaultFields and
|
||||
// includeTextSeparator are options for JSON and protobuf text formats,
|
||||
// respectively. Requests will be parsed from the given in.
|
||||
func RequestParserAndFormatterFor(format Format, descSource DescriptorSource, emitJSONDefaultFields, includeTextSeparator bool, in io.Reader) (RequestParser, Formatter, error) {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
resolver, err := anyResolver(descSource)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("error creating message resolver: %v", err)
|
||||
}
|
||||
return NewJSONRequestParser(in, resolver), NewJSONFormatter(emitJSONDefaultFields, resolver), nil
|
||||
case FormatText:
|
||||
return NewTextRequestParser(in), NewTextFormatter(includeTextSeparator), nil
|
||||
default:
|
||||
return nil, nil, fmt.Errorf("unknown format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultEventHandler logs events to a writer. This is not thread-safe, but is
|
||||
// safe for use with InvokeRPC as long as NumResponses and Status are not read
|
||||
// until the call to InvokeRPC completes.
|
||||
type DefaultEventHandler struct {
|
||||
out io.Writer
|
||||
descSource DescriptorSource
|
||||
formatter func(proto.Message) (string, error)
|
||||
verbose bool
|
||||
|
||||
// NumResponses is the number of responses that have been received.
|
||||
NumResponses int
|
||||
// Status is the status that was received at the end of an RPC. It is
|
||||
// nil if the RPC is still in progress.
|
||||
Status *status.Status
|
||||
}
|
||||
|
||||
// NewDefaultEventHandler returns an InvocationEventHandler that logs events to
|
||||
// the given output. If verbose is true, all events are logged. Otherwise, only
|
||||
// response messages are logged.
|
||||
func NewDefaultEventHandler(out io.Writer, descSource DescriptorSource, formatter Formatter, verbose bool) *DefaultEventHandler {
|
||||
return &DefaultEventHandler{
|
||||
out: out,
|
||||
descSource: descSource,
|
||||
formatter: formatter,
|
||||
verbose: verbose,
|
||||
}
|
||||
}
|
||||
|
||||
var _ InvocationEventHandler = (*DefaultEventHandler)(nil)
|
||||
|
||||
func (h *DefaultEventHandler) OnResolveMethod(md *desc.MethodDescriptor) {
|
||||
if h.verbose {
|
||||
txt, err := GetDescriptorText(md, h.descSource)
|
||||
if err == nil {
|
||||
fmt.Fprintf(h.out, "\nResolved method descriptor:\n%s\n", txt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DefaultEventHandler) OnSendHeaders(md metadata.MD) {
|
||||
if h.verbose {
|
||||
fmt.Fprintf(h.out, "\nRequest metadata to send:\n%s\n", MetadataToString(md))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DefaultEventHandler) OnReceiveHeaders(md metadata.MD) {
|
||||
if h.verbose {
|
||||
fmt.Fprintf(h.out, "\nResponse headers received:\n%s\n", MetadataToString(md))
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DefaultEventHandler) OnReceiveResponse(resp proto.Message) {
|
||||
h.NumResponses++
|
||||
if h.verbose {
|
||||
fmt.Fprint(h.out, "\nResponse contents:\n")
|
||||
}
|
||||
if respStr, err := h.formatter(resp); err != nil {
|
||||
fmt.Fprintf(h.out, "Failed to format response message %d: %v\n", h.NumResponses, err)
|
||||
} else {
|
||||
fmt.Fprintln(h.out, respStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DefaultEventHandler) OnReceiveTrailers(stat *status.Status, md metadata.MD) {
|
||||
h.Status = stat
|
||||
if h.verbose {
|
||||
fmt.Fprintf(h.out, "\nResponse trailers received:\n%s\n", MetadataToString(md))
|
||||
}
|
||||
}
|
||||
297
format_test.go
Normal file
297
format_test.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package grpcurl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes/struct"
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestRequestParser(t *testing.T) {
|
||||
source, err := DescriptorSourceFromProtoSets("testing/example.protoset")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create descriptor source: %v", err)
|
||||
}
|
||||
|
||||
msg, err := makeProto()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create message: %v", err)
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
format Format
|
||||
input string
|
||||
expectedOutput []proto.Message
|
||||
}{
|
||||
{
|
||||
format: FormatJSON,
|
||||
input: "",
|
||||
},
|
||||
{
|
||||
format: FormatJSON,
|
||||
input: messageAsJSON,
|
||||
expectedOutput: []proto.Message{msg},
|
||||
},
|
||||
{
|
||||
format: FormatJSON,
|
||||
input: messageAsJSON + messageAsJSON + messageAsJSON,
|
||||
expectedOutput: []proto.Message{msg, msg, msg},
|
||||
},
|
||||
{
|
||||
// unlike JSON, empty input yields one empty message (vs. zero messages)
|
||||
format: FormatText,
|
||||
input: "",
|
||||
expectedOutput: []proto.Message{&structpb.Value{}},
|
||||
},
|
||||
{
|
||||
format: FormatText,
|
||||
input: messageAsText,
|
||||
expectedOutput: []proto.Message{msg},
|
||||
},
|
||||
{
|
||||
format: FormatText,
|
||||
input: messageAsText + string(textSeparatorChar),
|
||||
expectedOutput: []proto.Message{msg, &structpb.Value{}},
|
||||
},
|
||||
{
|
||||
format: FormatText,
|
||||
input: messageAsText + string(textSeparatorChar) + messageAsText + string(textSeparatorChar) + messageAsText,
|
||||
expectedOutput: []proto.Message{msg, msg, msg},
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
name := fmt.Sprintf("#%d, %s, %d message(s)", i+1, tc.format, len(tc.expectedOutput))
|
||||
rf, _, err := RequestParserAndFormatterFor(tc.format, source, false, false, strings.NewReader(tc.input))
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create parser and formatter: %v", err)
|
||||
continue
|
||||
}
|
||||
numReqs := 0
|
||||
for {
|
||||
var req structpb.Value
|
||||
err := rf.Next(&req)
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Errorf("%s, msg %d: unexpected error: %v", name, numReqs, err)
|
||||
}
|
||||
if !proto.Equal(&req, tc.expectedOutput[numReqs]) {
|
||||
t.Errorf("%s, msg %d: incorrect message;\nexpecting:\n%v\ngot:\n%v", name, numReqs, tc.expectedOutput[numReqs], &req)
|
||||
}
|
||||
numReqs++
|
||||
}
|
||||
if rf.NumRequests() != numReqs {
|
||||
t.Errorf("%s: factory reported wrong number of requests: expecting %d, got %d", name, numReqs, rf.NumRequests())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler prints response data (and headers/trailers in verbose mode).
|
||||
// This verifies that we get the right output in both JSON and proto text modes.
|
||||
func TestHandler(t *testing.T) {
|
||||
source, err := DescriptorSourceFromProtoSets("testing/example.protoset")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create descriptor source: %v", err)
|
||||
}
|
||||
d, err := source.FindSymbol("TestService.GetFiles")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to find method 'TestService.GetFiles': %v", err)
|
||||
}
|
||||
md, ok := d.(*desc.MethodDescriptor)
|
||||
if !ok {
|
||||
t.Fatalf("wrong kind of descriptor found: %T", d)
|
||||
}
|
||||
|
||||
reqHeaders := metadata.Pairs("foo", "123", "bar", "456")
|
||||
respHeaders := metadata.Pairs("foo", "abc", "bar", "def", "baz", "xyz")
|
||||
respTrailers := metadata.Pairs("a", "1", "b", "2", "c", "3")
|
||||
rsp, err := makeProto()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create response message: %v", err)
|
||||
}
|
||||
|
||||
for _, format := range []Format{FormatJSON, FormatText} {
|
||||
for _, numMessages := range []int{1, 3} {
|
||||
for _, verbose := range []bool{true, false} {
|
||||
name := fmt.Sprintf("%s, %d message(s)", format, numMessages)
|
||||
if verbose {
|
||||
name += ", verbose"
|
||||
}
|
||||
|
||||
_, formatter, err := RequestParserAndFormatterFor(format, source, false, !verbose, nil)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to create parser and formatter: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
h := NewDefaultEventHandler(&buf, source, formatter, verbose)
|
||||
|
||||
h.OnResolveMethod(md)
|
||||
h.OnSendHeaders(reqHeaders)
|
||||
h.OnReceiveHeaders(respHeaders)
|
||||
for i := 0; i < numMessages; i++ {
|
||||
h.OnReceiveResponse(rsp)
|
||||
}
|
||||
h.OnReceiveTrailers(nil, respTrailers)
|
||||
|
||||
expectedOutput := ""
|
||||
if verbose {
|
||||
expectedOutput += verbosePrefix
|
||||
}
|
||||
for i := 0; i < numMessages; i++ {
|
||||
if verbose {
|
||||
expectedOutput += verboseResponseHeader
|
||||
}
|
||||
if format == "json" {
|
||||
expectedOutput += messageAsJSON
|
||||
} else {
|
||||
if i > 0 && !verbose {
|
||||
expectedOutput += string(textSeparatorChar)
|
||||
}
|
||||
expectedOutput += messageAsText
|
||||
}
|
||||
}
|
||||
if verbose {
|
||||
expectedOutput += verboseSuffix
|
||||
}
|
||||
|
||||
out := buf.String()
|
||||
if !compare(out, expectedOutput) {
|
||||
t.Errorf("%s: Incorrect output. Expected:\n%s\nGot:\n%s", name, expectedOutput, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// compare checks that actual and expected are equal, returning true if so.
|
||||
// A simple equality check (==) does not suffice because jsonpb formats
|
||||
// structpb.Value strangely. So if that formatting gets fixed, we don't
|
||||
// want this test in grpcurl to suddenly start failing. So we check each
|
||||
// line and compare the lines after stripping whitespace (which removes
|
||||
// the jsonpb format anomalies).
|
||||
func compare(actual, expected string) bool {
|
||||
actualLines := strings.Split(actual, "\n")
|
||||
expectedLines := strings.Split(expected, "\n")
|
||||
if len(actualLines) != len(expectedLines) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(actualLines); i++ {
|
||||
if strings.TrimSpace(actualLines[i]) != strings.TrimSpace(expectedLines[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func makeProto() (proto.Message, error) {
|
||||
var rsp structpb.Value
|
||||
err := jsonpb.UnmarshalString(`{
|
||||
"foo": ["abc", "def", "ghi"],
|
||||
"bar": { "a": 1, "b": 2 },
|
||||
"baz": true,
|
||||
"null": null
|
||||
}`, &rsp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rsp, nil
|
||||
}
|
||||
|
||||
var (
|
||||
verbosePrefix = `
|
||||
Resolved method descriptor:
|
||||
rpc GetFiles ( .TestRequest ) returns ( .TestResponse );
|
||||
|
||||
Request metadata to send:
|
||||
bar: 456
|
||||
foo: 123
|
||||
|
||||
Response headers received:
|
||||
bar: def
|
||||
baz: xyz
|
||||
foo: abc
|
||||
`
|
||||
verboseSuffix = `
|
||||
Response trailers received:
|
||||
a: 1
|
||||
b: 2
|
||||
c: 3
|
||||
`
|
||||
verboseResponseHeader = `
|
||||
Response contents:
|
||||
`
|
||||
messageAsJSON = `{
|
||||
"bar": {
|
||||
"a": 1,
|
||||
"b": 2
|
||||
},
|
||||
"baz": true,
|
||||
"foo": [
|
||||
"abc",
|
||||
"def",
|
||||
"ghi"
|
||||
],
|
||||
"null": null
|
||||
}
|
||||
`
|
||||
messageAsText = `struct_value: <
|
||||
fields: <
|
||||
key: "bar"
|
||||
value: <
|
||||
struct_value: <
|
||||
fields: <
|
||||
key: "a"
|
||||
value: <
|
||||
number_value: 1
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "b"
|
||||
value: <
|
||||
number_value: 2
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "baz"
|
||||
value: <
|
||||
bool_value: true
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "foo"
|
||||
value: <
|
||||
list_value: <
|
||||
values: <
|
||||
string_value: "abc"
|
||||
>
|
||||
values: <
|
||||
string_value: "def"
|
||||
>
|
||||
values: <
|
||||
string_value: "ghi"
|
||||
>
|
||||
>
|
||||
>
|
||||
>
|
||||
fields: <
|
||||
key: "null"
|
||||
value: <
|
||||
null_value: NULL_VALUE
|
||||
>
|
||||
>
|
||||
>
|
||||
`
|
||||
)
|
||||
2
go.mod
2
go.mod
@@ -2,7 +2,7 @@ module github.com/fullstorydev/grpcurl
|
||||
|
||||
require (
|
||||
github.com/golang/protobuf v1.1.0
|
||||
github.com/jhump/protoreflect v1.0.0
|
||||
github.com/jhump/protoreflect v1.1.0
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022
|
||||
google.golang.org/grpc v1.12.0
|
||||
)
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,7 +1,7 @@
|
||||
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/jhump/protoreflect v1.0.0 h1:l94KtQ6gRI3ouKVcXNdofCQJWoHATzcI6tDizOgUaf0=
|
||||
github.com/jhump/protoreflect v1.0.0/go.mod h1:kG/zRVeS2M91gYaCvvUbPkMjjtFQS4qqjcPFzFkh2zE=
|
||||
github.com/jhump/protoreflect v1.1.0 h1:h+zsMrsiq0vIl7yWmeowmd8e8VtnWk75U04GgXA2s6Y=
|
||||
github.com/jhump/protoreflect v1.1.0/go.mod h1:kG/zRVeS2M91gYaCvvUbPkMjjtFQS4qqjcPFzFkh2zE=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022 h1:MVYFTUmVD3/+ERcvRRI+P/C2+WOUimXh+Pd8LVsklZ4=
|
||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
|
||||
832
grpcurl.go
832
grpcurl.go
@@ -13,247 +13,26 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/golang/protobuf/ptypes/struct"
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"github.com/jhump/protoreflect/desc/protoparse"
|
||||
"github.com/jhump/protoreflect/desc/protoprint"
|
||||
"github.com/jhump/protoreflect/dynamic"
|
||||
"github.com/jhump/protoreflect/dynamic/grpcdynamic"
|
||||
"github.com/jhump/protoreflect/grpcreflect"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// ErrReflectionNotSupported is returned by DescriptorSource operations that
|
||||
// rely on interacting with the reflection service when the source does not
|
||||
// actually expose the reflection service. When this occurs, an alternate source
|
||||
// (like file descriptor sets) must be used.
|
||||
var ErrReflectionNotSupported = errors.New("server does not support the reflection API")
|
||||
|
||||
// DescriptorSource is a source of protobuf descriptor information. It can be backed by a FileDescriptorSet
|
||||
// proto (like a file generated by protoc) or a remote server that supports the reflection API.
|
||||
type DescriptorSource interface {
|
||||
// ListServices returns a list of fully-qualified service names. It will be all services in a set of
|
||||
// descriptor files or the set of all services exposed by a GRPC server.
|
||||
ListServices() ([]string, error)
|
||||
// FindSymbol returns a descriptor for the given fully-qualified symbol name.
|
||||
FindSymbol(fullyQualifiedName string) (desc.Descriptor, error)
|
||||
// AllExtensionsForType returns all known extension fields that extend the given message type name.
|
||||
AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error)
|
||||
}
|
||||
|
||||
// DescriptorSourceFromProtoSets creates a DescriptorSource that is backed by the named files, whose contents
|
||||
// are encoded FileDescriptorSet protos.
|
||||
func DescriptorSourceFromProtoSets(fileNames ...string) (DescriptorSource, error) {
|
||||
files := &descriptor.FileDescriptorSet{}
|
||||
for _, fileName := range fileNames {
|
||||
b, err := ioutil.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load protoset file %q: %v", fileName, err)
|
||||
}
|
||||
var fs descriptor.FileDescriptorSet
|
||||
err = proto.Unmarshal(b, &fs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse contents of protoset file %q: %v", fileName, err)
|
||||
}
|
||||
files.File = append(files.File, fs.File...)
|
||||
}
|
||||
return DescriptorSourceFromFileDescriptorSet(files)
|
||||
}
|
||||
|
||||
// DescriptorSourceFromProtoFiles creates a DescriptorSource that is backed by the named files,
|
||||
// whose contents are Protocol Buffer source files. The given importPaths are used to locate
|
||||
// any imported files.
|
||||
func DescriptorSourceFromProtoFiles(importPaths []string, fileNames ...string) (DescriptorSource, error) {
|
||||
p := protoparse.Parser{
|
||||
ImportPaths: importPaths,
|
||||
InferImportPaths: len(importPaths) == 0,
|
||||
}
|
||||
fds, err := p.ParseFiles(fileNames...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse given files: %v", err)
|
||||
}
|
||||
return DescriptorSourceFromFileDescriptors(fds...)
|
||||
}
|
||||
|
||||
// DescriptorSourceFromFileDescriptorSet creates a DescriptorSource that is backed by the FileDescriptorSet.
|
||||
func DescriptorSourceFromFileDescriptorSet(files *descriptor.FileDescriptorSet) (DescriptorSource, error) {
|
||||
unresolved := map[string]*descriptor.FileDescriptorProto{}
|
||||
for _, fd := range files.File {
|
||||
unresolved[fd.GetName()] = fd
|
||||
}
|
||||
resolved := map[string]*desc.FileDescriptor{}
|
||||
for _, fd := range files.File {
|
||||
_, err := resolveFileDescriptor(unresolved, resolved, fd.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &fileSource{files: resolved}, nil
|
||||
}
|
||||
|
||||
func resolveFileDescriptor(unresolved map[string]*descriptor.FileDescriptorProto, resolved map[string]*desc.FileDescriptor, filename string) (*desc.FileDescriptor, error) {
|
||||
if r, ok := resolved[filename]; ok {
|
||||
return r, nil
|
||||
}
|
||||
fd, ok := unresolved[filename]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no descriptor found for %q", filename)
|
||||
}
|
||||
deps := make([]*desc.FileDescriptor, 0, len(fd.GetDependency()))
|
||||
for _, dep := range fd.GetDependency() {
|
||||
depFd, err := resolveFileDescriptor(unresolved, resolved, dep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deps = append(deps, depFd)
|
||||
}
|
||||
result, err := desc.CreateFileDescriptor(fd, deps...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved[filename] = result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DescriptorSourceFromFileDescriptorSet creates a DescriptorSource that is backed by the given
|
||||
// file descriptors
|
||||
func DescriptorSourceFromFileDescriptors(files ...*desc.FileDescriptor) (DescriptorSource, error) {
|
||||
fds := map[string]*desc.FileDescriptor{}
|
||||
for _, fd := range files {
|
||||
if err := addFile(fd, fds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &fileSource{files: fds}, nil
|
||||
}
|
||||
|
||||
func addFile(fd *desc.FileDescriptor, fds map[string]*desc.FileDescriptor) error {
|
||||
name := fd.GetName()
|
||||
if existing, ok := fds[name]; ok {
|
||||
// already added this file
|
||||
if existing != fd {
|
||||
// doh! duplicate files provided
|
||||
return fmt.Errorf("given files include multiple copies of %q", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
fds[name] = fd
|
||||
for _, dep := range fd.GetDependencies() {
|
||||
if err := addFile(dep, fds); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileSource struct {
|
||||
files map[string]*desc.FileDescriptor
|
||||
er *dynamic.ExtensionRegistry
|
||||
erInit sync.Once
|
||||
}
|
||||
|
||||
func (fs *fileSource) ListServices() ([]string, error) {
|
||||
set := map[string]bool{}
|
||||
for _, fd := range fs.files {
|
||||
for _, svc := range fd.GetServices() {
|
||||
set[svc.GetFullyQualifiedName()] = true
|
||||
}
|
||||
}
|
||||
sl := make([]string, 0, len(set))
|
||||
for svc := range set {
|
||||
sl = append(sl, svc)
|
||||
}
|
||||
return sl, nil
|
||||
}
|
||||
|
||||
func (fs *fileSource) FindSymbol(fullyQualifiedName string) (desc.Descriptor, error) {
|
||||
for _, fd := range fs.files {
|
||||
if dsc := fd.FindSymbol(fullyQualifiedName); dsc != nil {
|
||||
return dsc, nil
|
||||
}
|
||||
}
|
||||
return nil, notFound("Symbol", fullyQualifiedName)
|
||||
}
|
||||
|
||||
func (fs *fileSource) AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error) {
|
||||
fs.erInit.Do(func() {
|
||||
fs.er = &dynamic.ExtensionRegistry{}
|
||||
for _, fd := range fs.files {
|
||||
fs.er.AddExtensionsFromFile(fd)
|
||||
}
|
||||
})
|
||||
return fs.er.AllExtensionsForType(typeName), nil
|
||||
}
|
||||
|
||||
// DescriptorSourceFromServer creates a DescriptorSource that uses the given GRPC reflection client
|
||||
// to interrogate a server for descriptor information. If the server does not support the reflection
|
||||
// API then the various DescriptorSource methods will return ErrReflectionNotSupported
|
||||
func DescriptorSourceFromServer(ctx context.Context, refClient *grpcreflect.Client) DescriptorSource {
|
||||
return serverSource{client: refClient}
|
||||
}
|
||||
|
||||
type serverSource struct {
|
||||
client *grpcreflect.Client
|
||||
}
|
||||
|
||||
func (ss serverSource) ListServices() ([]string, error) {
|
||||
svcs, err := ss.client.ListServices()
|
||||
return svcs, reflectionSupport(err)
|
||||
}
|
||||
|
||||
func (ss serverSource) FindSymbol(fullyQualifiedName string) (desc.Descriptor, error) {
|
||||
file, err := ss.client.FileContainingSymbol(fullyQualifiedName)
|
||||
if err != nil {
|
||||
return nil, reflectionSupport(err)
|
||||
}
|
||||
d := file.FindSymbol(fullyQualifiedName)
|
||||
if d == nil {
|
||||
return nil, notFound("Symbol", fullyQualifiedName)
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (ss serverSource) AllExtensionsForType(typeName string) ([]*desc.FieldDescriptor, error) {
|
||||
var exts []*desc.FieldDescriptor
|
||||
nums, err := ss.client.AllExtensionNumbersForType(typeName)
|
||||
if err != nil {
|
||||
return nil, reflectionSupport(err)
|
||||
}
|
||||
for _, fieldNum := range nums {
|
||||
ext, err := ss.client.ResolveExtension(typeName, fieldNum)
|
||||
if err != nil {
|
||||
return nil, reflectionSupport(err)
|
||||
}
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
return exts, nil
|
||||
}
|
||||
|
||||
func reflectionSupport(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if stat, ok := status.FromError(err); ok && stat.Code() == codes.Unimplemented {
|
||||
return ErrReflectionNotSupported
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ListServices uses the given descriptor source to return a sorted list of fully-qualified
|
||||
// service names.
|
||||
func ListServices(source DescriptorSource) ([]string, error) {
|
||||
@@ -265,6 +44,78 @@ func ListServices(source DescriptorSource) ([]string, error) {
|
||||
return svcs, nil
|
||||
}
|
||||
|
||||
type sourceWithFiles interface {
|
||||
GetAllFiles() ([]*desc.FileDescriptor, error)
|
||||
}
|
||||
|
||||
var _ sourceWithFiles = (*fileSource)(nil)
|
||||
|
||||
// GetAllFiles uses the given descriptor source to return a list of file descriptors.
|
||||
func GetAllFiles(source DescriptorSource) ([]*desc.FileDescriptor, error) {
|
||||
var files []*desc.FileDescriptor
|
||||
srcFiles, ok := source.(sourceWithFiles)
|
||||
|
||||
// If an error occurs, we still try to load as many files as we can, so that
|
||||
// caller can decide whether to ignore error or not.
|
||||
var firstError error
|
||||
if ok {
|
||||
files, firstError = srcFiles.GetAllFiles()
|
||||
} else {
|
||||
// Source does not implement GetAllFiles method, so use ListServices
|
||||
// and grab files from there.
|
||||
svcNames, err := source.ListServices()
|
||||
if err != nil {
|
||||
firstError = err
|
||||
} else {
|
||||
allFiles := map[string]*desc.FileDescriptor{}
|
||||
for _, name := range svcNames {
|
||||
d, err := source.FindSymbol(name)
|
||||
if err != nil {
|
||||
if firstError == nil {
|
||||
firstError = err
|
||||
}
|
||||
} else {
|
||||
addAllFilesToSet(d.GetFile(), allFiles)
|
||||
}
|
||||
}
|
||||
files = make([]*desc.FileDescriptor, len(allFiles))
|
||||
i := 0
|
||||
for _, fd := range allFiles {
|
||||
files[i] = fd
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(filesByName(files))
|
||||
return files, firstError
|
||||
}
|
||||
|
||||
type filesByName []*desc.FileDescriptor
|
||||
|
||||
func (f filesByName) Len() int {
|
||||
return len(f)
|
||||
}
|
||||
|
||||
func (f filesByName) Less(i, j int) bool {
|
||||
return f[i].GetName() < f[j].GetName()
|
||||
}
|
||||
|
||||
func (f filesByName) Swap(i, j int) {
|
||||
f[i], f[j] = f[j], f[i]
|
||||
}
|
||||
|
||||
func addAllFilesToSet(fd *desc.FileDescriptor, all map[string]*desc.FileDescriptor) {
|
||||
if _, ok := all[fd.GetName()]; ok {
|
||||
// already added
|
||||
return
|
||||
}
|
||||
all[fd.GetName()] = fd
|
||||
for _, dep := range fd.GetDependencies() {
|
||||
addAllFilesToSet(dep, all)
|
||||
}
|
||||
}
|
||||
|
||||
// ListMethods uses the given descriptor source to return a sorted list of method names
|
||||
// for the specified fully-qualified service name.
|
||||
func ListMethods(source DescriptorSource, serviceName string) ([]string, error) {
|
||||
@@ -277,365 +128,13 @@ func ListMethods(source DescriptorSource, serviceName string) ([]string, error)
|
||||
} else {
|
||||
methods := make([]string, 0, len(sd.GetMethods()))
|
||||
for _, method := range sd.GetMethods() {
|
||||
methods = append(methods, method.GetName())
|
||||
methods = append(methods, method.GetFullyQualifiedName())
|
||||
}
|
||||
sort.Strings(methods)
|
||||
return methods, nil
|
||||
}
|
||||
}
|
||||
|
||||
type notFoundError string
|
||||
|
||||
func notFound(kind, name string) error {
|
||||
return notFoundError(fmt.Sprintf("%s not found: %s", kind, name))
|
||||
}
|
||||
|
||||
func (e notFoundError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func isNotFoundError(err error) bool {
|
||||
if grpcreflect.IsElementNotFoundError(err) {
|
||||
return true
|
||||
}
|
||||
_, ok := err.(notFoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// InvocationEventHandler is a bag of callbacks for handling events that occur in the course
|
||||
// of invoking an RPC. The handler also provides request data that is sent. The callbacks are
|
||||
// generally called in the order they are listed below.
|
||||
type InvocationEventHandler interface {
|
||||
// OnResolveMethod is called with a descriptor of the method that is being invoked.
|
||||
OnResolveMethod(*desc.MethodDescriptor)
|
||||
// OnSendHeaders is called with the request metadata that is being sent.
|
||||
OnSendHeaders(metadata.MD)
|
||||
// OnReceiveHeaders is called when response headers have been received.
|
||||
OnReceiveHeaders(metadata.MD)
|
||||
// OnReceiveResponse is called for each response message received.
|
||||
OnReceiveResponse(proto.Message)
|
||||
// OnReceiveTrailers is called when response trailers and final RPC status have been received.
|
||||
OnReceiveTrailers(*status.Status, metadata.MD)
|
||||
}
|
||||
|
||||
// RequestMessageSupplier is a function that is called to retrieve request
|
||||
// messages for a GRPC operation. The message contents must be valid JSON. If
|
||||
// the supplier has no more messages, it should return nil, io.EOF.
|
||||
type RequestMessageSupplier func() ([]byte, error)
|
||||
|
||||
// InvokeRpc uses te given GRPC connection to invoke the given method. The given descriptor source
|
||||
// is used to determine the type of method and the type of request and response message. The given
|
||||
// headers are sent as request metadata. Methods on the given event handler are called as the
|
||||
// invocation proceeds.
|
||||
//
|
||||
// The given requestData function supplies the actual data to send. It should return io.EOF when
|
||||
// there is no more request data. If it returns a nil error then the returned JSON message should
|
||||
// not be blank. If the method being invoked is a unary or server-streaming RPC (e.g. exactly one
|
||||
// request message) and there is no request data (e.g. the first invocation of the function returns
|
||||
// io.EOF), then a blank request message is sent, as if the request data were an empty object: "{}".
|
||||
//
|
||||
// If the requestData function and the given event handler coordinate or share any state, they should
|
||||
// be thread-safe. This is because the requestData function may be called from a different goroutine
|
||||
// than the one invoking event callbacks. (This only happens for bi-directional streaming RPCs, where
|
||||
// one goroutine sends request messages and another consumes the response messages).
|
||||
func InvokeRpc(ctx context.Context, source DescriptorSource, cc *grpc.ClientConn, methodName string,
|
||||
headers []string, handler InvocationEventHandler, requestData RequestMessageSupplier) error {
|
||||
|
||||
md := MetadataFromHeaders(headers)
|
||||
|
||||
svc, mth := parseSymbol(methodName)
|
||||
if svc == "" || mth == "" {
|
||||
return fmt.Errorf("given method name %q is not in expected format: 'service/method' or 'service.method'", methodName)
|
||||
}
|
||||
dsc, err := source.FindSymbol(svc)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return fmt.Errorf("target server does not expose service %q", svc)
|
||||
}
|
||||
return fmt.Errorf("failed to query for service descriptor %q: %v", svc, err)
|
||||
}
|
||||
sd, ok := dsc.(*desc.ServiceDescriptor)
|
||||
if !ok {
|
||||
return fmt.Errorf("target server does not expose service %q", svc)
|
||||
}
|
||||
mtd := sd.FindMethodByName(mth)
|
||||
if mtd == nil {
|
||||
return fmt.Errorf("service %q does not include a method named %q", svc, mth)
|
||||
}
|
||||
|
||||
handler.OnResolveMethod(mtd)
|
||||
|
||||
// we also download any applicable extensions so we can provide full support for parsing user-provided data
|
||||
var ext dynamic.ExtensionRegistry
|
||||
alreadyFetched := map[string]bool{}
|
||||
if err = fetchAllExtensions(source, &ext, mtd.GetInputType(), alreadyFetched); err != nil {
|
||||
return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetInputType().GetFullyQualifiedName(), err)
|
||||
}
|
||||
if err = fetchAllExtensions(source, &ext, mtd.GetOutputType(), alreadyFetched); err != nil {
|
||||
return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetOutputType().GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
msgFactory := dynamic.NewMessageFactoryWithExtensionRegistry(&ext)
|
||||
req := msgFactory.NewMessage(mtd.GetInputType())
|
||||
|
||||
handler.OnSendHeaders(md)
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
stub := grpcdynamic.NewStubWithMessageFactory(cc, msgFactory)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
if mtd.IsClientStreaming() && mtd.IsServerStreaming() {
|
||||
return invokeBidi(ctx, stub, mtd, handler, requestData, req)
|
||||
} else if mtd.IsClientStreaming() {
|
||||
return invokeClientStream(ctx, stub, mtd, handler, requestData, req)
|
||||
} else if mtd.IsServerStreaming() {
|
||||
return invokeServerStream(ctx, stub, mtd, handler, requestData, req)
|
||||
} else {
|
||||
return invokeUnary(ctx, stub, mtd, handler, requestData, req)
|
||||
}
|
||||
}
|
||||
|
||||
func invokeUnary(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestMessageSupplier, req proto.Message) error {
|
||||
|
||||
data, err := requestData()
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
if len(data) != 0 {
|
||||
err = jsonpb.UnmarshalString(string(data), req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err)
|
||||
}
|
||||
}
|
||||
if err != io.EOF {
|
||||
// verify there is no second message, which is a usage error
|
||||
_, err := requestData()
|
||||
if err == nil {
|
||||
return fmt.Errorf("method %q is a unary RPC, but request data contained more than 1 message", md.GetFullyQualifiedName())
|
||||
} else if err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can actually invoke the RPC!
|
||||
var respHeaders metadata.MD
|
||||
var respTrailers metadata.MD
|
||||
resp, err := stub.InvokeRpc(ctx, md, req, grpc.Trailer(&respTrailers), grpc.Header(&respHeaders))
|
||||
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
|
||||
if stat.Code() == codes.OK {
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, respTrailers)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeClientStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestMessageSupplier, req proto.Message) error {
|
||||
|
||||
// invoke the RPC!
|
||||
str, err := stub.InvokeRpcClientStream(ctx, md)
|
||||
|
||||
// Upload each request message in the stream
|
||||
var resp proto.Message
|
||||
for err == nil {
|
||||
var data []byte
|
||||
data, err = requestData()
|
||||
if err == io.EOF {
|
||||
resp, err = str.CloseAndReceive()
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
if len(data) != 0 {
|
||||
err = jsonpb.UnmarshalString(string(data), req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err)
|
||||
}
|
||||
}
|
||||
|
||||
err = str.SendMsg(req)
|
||||
if err == io.EOF {
|
||||
// We get EOF on send if the server says "go away"
|
||||
// We have to use CloseAndReceive to get the actual code
|
||||
resp, err = str.CloseAndReceive()
|
||||
break
|
||||
}
|
||||
|
||||
req.Reset()
|
||||
}
|
||||
|
||||
// finally, process response data
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
if respHeaders, err := str.Header(); err == nil {
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
}
|
||||
|
||||
if stat.Code() == codes.OK {
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, str.Trailer())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeServerStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestMessageSupplier, req proto.Message) error {
|
||||
|
||||
data, err := requestData()
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
if len(data) != 0 {
|
||||
err = jsonpb.UnmarshalString(string(data), req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err)
|
||||
}
|
||||
}
|
||||
if err != io.EOF {
|
||||
// verify there is no second message, which is a usage error
|
||||
_, err := requestData()
|
||||
if err == nil {
|
||||
return fmt.Errorf("method %q is a server-streaming RPC, but request data contained more than 1 message", md.GetFullyQualifiedName())
|
||||
} else if err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can actually invoke the RPC!
|
||||
str, err := stub.InvokeRpcServerStream(ctx, md, req)
|
||||
|
||||
if respHeaders, err := str.Header(); err == nil {
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
}
|
||||
|
||||
// Download each response message
|
||||
for err == nil {
|
||||
var resp proto.Message
|
||||
resp, err = str.RecvMsg()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, str.Trailer())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeBidi(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestMessageSupplier, req proto.Message) error {
|
||||
|
||||
// invoke the RPC!
|
||||
str, err := stub.InvokeRpcBidiStream(ctx, md)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var sendErr atomic.Value
|
||||
|
||||
defer wg.Wait()
|
||||
|
||||
if err == nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Concurrently upload each request message in the stream
|
||||
var err error
|
||||
var data []byte
|
||||
for err == nil {
|
||||
data, err = requestData()
|
||||
|
||||
if err == io.EOF {
|
||||
err = str.CloseSend()
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error getting request data: %v", err)
|
||||
break
|
||||
}
|
||||
if len(data) != 0 {
|
||||
err = jsonpb.UnmarshalString(string(data), req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could not parse given request body as message of type %q: %v", md.GetInputType().GetFullyQualifiedName(), err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = str.SendMsg(req)
|
||||
|
||||
req.Reset()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sendErr.Store(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if respHeaders, err := str.Header(); err == nil {
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
}
|
||||
|
||||
// Download each response message
|
||||
for err == nil {
|
||||
var resp proto.Message
|
||||
resp, err = str.RecvMsg()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
if se, ok := sendErr.Load().(error); ok && se != io.EOF {
|
||||
err = se
|
||||
}
|
||||
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, str.Trailer())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MetadataFromHeaders converts a list of header strings (each string in
|
||||
// "Header-Name: Header-Value" form) into metadata. If a string has a header
|
||||
// name without a value (e.g. does not contain a colon), the value is assumed
|
||||
@@ -683,42 +182,61 @@ func decode(val string) (string, error) {
|
||||
return "", firstErr
|
||||
}
|
||||
|
||||
func parseSymbol(svcAndMethod string) (string, string) {
|
||||
pos := strings.LastIndex(svcAndMethod, "/")
|
||||
if pos < 0 {
|
||||
pos = strings.LastIndex(svcAndMethod, ".")
|
||||
if pos < 0 {
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return svcAndMethod[:pos], svcAndMethod[pos+1:]
|
||||
}
|
||||
|
||||
// MetadataToString returns a string representation of the given metadata, for
|
||||
// displaying to users.
|
||||
func MetadataToString(md metadata.MD) string {
|
||||
if len(md) == 0 {
|
||||
return "(empty)"
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(md))
|
||||
for k := range md {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var b bytes.Buffer
|
||||
for k, vs := range md {
|
||||
first := true
|
||||
for _, k := range keys {
|
||||
vs := md[k]
|
||||
for _, v := range vs {
|
||||
if first {
|
||||
first = false
|
||||
} else {
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString(k)
|
||||
b.WriteString(": ")
|
||||
if strings.HasSuffix(k, "-bin") {
|
||||
v = base64.StdEncoding.EncodeToString([]byte(v))
|
||||
}
|
||||
b.WriteString(v)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
var printer = &protoprint.Printer{
|
||||
Compact: true,
|
||||
OmitComments: protoprint.CommentsNonDoc,
|
||||
SortElements: true,
|
||||
ForceFullyQualifiedNames: true,
|
||||
}
|
||||
|
||||
// GetDescriptorText returns a string representation of the given descriptor.
|
||||
func GetDescriptorText(dsc desc.Descriptor, descSource DescriptorSource) (string, error) {
|
||||
dscProto := EnsureExtensions(descSource, dsc.AsProto())
|
||||
return (&jsonpb.Marshaler{Indent: " "}).MarshalToString(dscProto)
|
||||
// This returns a snippet of proto source that describes the given element.
|
||||
func GetDescriptorText(dsc desc.Descriptor, _ DescriptorSource) (string, error) {
|
||||
// Note: DescriptorSource is not used, but remains an argument for backwards
|
||||
// compatibility with previous implementation.
|
||||
txt, err := printer.PrintProtoToString(dsc)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// callers don't expect trailing newlines
|
||||
if txt[len(txt)-1] == '\n' {
|
||||
txt = txt[:len(txt)-1]
|
||||
}
|
||||
return txt, nil
|
||||
}
|
||||
|
||||
// EnsureExtensions uses the given descriptor source to download extensions for
|
||||
@@ -840,7 +358,124 @@ func fullyConvertToDynamic(msgFact *dynamic.MessageFactory, msg proto.Message) (
|
||||
return dm, nil
|
||||
}
|
||||
|
||||
// ClientTransportCredentials builds transport credentials for a GRPC client using the
|
||||
// MakeTemplate returns a message instance for the given descriptor that 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(md *desc.MessageDescriptor) proto.Message {
|
||||
return makeTemplate(md, nil)
|
||||
}
|
||||
|
||||
func makeTemplate(md *desc.MessageDescriptor, path []*desc.MessageDescriptor) proto.Message {
|
||||
switch md.GetFullyQualifiedName() {
|
||||
case "google.protobuf.Any":
|
||||
// empty type URL is not allowed by JSON representation
|
||||
// so we must give it a dummy type
|
||||
msg, _ := ptypes.MarshalAny(&empty.Empty{})
|
||||
return msg
|
||||
case "google.protobuf.Value":
|
||||
// unset kind is not allowed by JSON representation
|
||||
// so we must give it something
|
||||
return &structpb.Value{
|
||||
Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"google.protobuf.Value": {Kind: &structpb.Value_StringValue{
|
||||
StringValue: "supports arbitrary JSON",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
case "google.protobuf.ListValue":
|
||||
return &structpb.ListValue{
|
||||
Values: []*structpb.Value{
|
||||
{
|
||||
Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"google.protobuf.ListValue": {Kind: &structpb.Value_StringValue{
|
||||
StringValue: "is an array of arbitrary JSON values",
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
case "google.protobuf.Struct":
|
||||
return &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"google.protobuf.Struct": {Kind: &structpb.Value_StringValue{
|
||||
StringValue: "supports arbitrary JSON objects",
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
dm := dynamic.NewMessage(md)
|
||||
|
||||
// if the message is a recursive structure, we don't want to blow the stack
|
||||
for _, seen := range path {
|
||||
if seen == md {
|
||||
// already visited this type; avoid infinite recursion
|
||||
return dm
|
||||
}
|
||||
}
|
||||
path = append(path, dm.GetMessageDescriptor())
|
||||
|
||||
// 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(fd.GetMessageType(), path))
|
||||
}
|
||||
} else if fd.GetMessageType() != nil {
|
||||
dm.SetField(fd, makeTemplate(fd.GetMessageType(), path))
|
||||
}
|
||||
}
|
||||
return dm
|
||||
}
|
||||
|
||||
// ClientTransportCredentials builds transport credentials for a gRPC client using the
|
||||
// given properties. If cacertFile is blank, only standard trusted certs are used to
|
||||
// verify the server certs. If clientCertFile is blank, the client will not use a client
|
||||
// certificate. If clientCertFile is not blank then clientKeyFile must not be blank.
|
||||
@@ -877,13 +512,16 @@ func ClientTransportCredentials(insecureSkipVerify bool, cacertFile, clientCertF
|
||||
return credentials.NewTLS(&tlsConf), nil
|
||||
}
|
||||
|
||||
// ServerTransportCredentials builds transport credentials for a GRPC server using the
|
||||
// ServerTransportCredentials builds transport credentials for a gRPC server using the
|
||||
// given properties. If cacertFile is blank, the server will not request client certs
|
||||
// unless requireClientCerts is true. When requireClientCerts is false and cacertFile is
|
||||
// not blank, the server will verify client certs when presented, but will not require
|
||||
// client certs. The serverCertFile and serverKeyFile must both not be blank.
|
||||
func ServerTransportCredentials(cacertFile, serverCertFile, serverKeyFile string, requireClientCerts bool) (credentials.TransportCredentials, error) {
|
||||
var tlsConf tls.Config
|
||||
// TODO(jh): Remove this line once https://github.com/golang/go/issues/28779 is fixed
|
||||
// in Go tip. Until then, the recently merged TLS 1.3 support breaks the TLS tests.
|
||||
tlsConf.MaxVersion = tls.VersionTLS12
|
||||
|
||||
// Load the server certificates from disk
|
||||
certificate, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile)
|
||||
|
||||
134
grpcurl_test.go
134
grpcurl_test.go
@@ -1,6 +1,7 @@
|
||||
package grpcurl_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
jsonpbtest "github.com/golang/protobuf/jsonpb/jsonpb_test_proto"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"github.com/jhump/protoreflect/grpcreflect"
|
||||
@@ -44,6 +46,10 @@ type descSourceCase struct {
|
||||
includeRefl bool
|
||||
}
|
||||
|
||||
// NB: These tests intentionally use the deprecated InvokeRpc since that
|
||||
// calls the other (non-deprecated InvokeRPC). That allows the tests to
|
||||
// easily exercise both functions.
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var err error
|
||||
sourceProtoset, err = DescriptorSourceFromProtoSets("testing/test.protoset")
|
||||
@@ -197,12 +203,12 @@ func doTestListMethods(t *testing.T, source DescriptorSource, includeReflection
|
||||
t.Fatalf("failed to list methods for TestService: %v", err)
|
||||
}
|
||||
expected := []string{
|
||||
"EmptyCall",
|
||||
"FullDuplexCall",
|
||||
"HalfDuplexCall",
|
||||
"StreamingInputCall",
|
||||
"StreamingOutputCall",
|
||||
"UnaryCall",
|
||||
"grpc.testing.TestService.EmptyCall",
|
||||
"grpc.testing.TestService.FullDuplexCall",
|
||||
"grpc.testing.TestService.HalfDuplexCall",
|
||||
"grpc.testing.TestService.StreamingInputCall",
|
||||
"grpc.testing.TestService.StreamingOutputCall",
|
||||
"grpc.testing.TestService.UnaryCall",
|
||||
}
|
||||
if !reflect.DeepEqual(expected, names) {
|
||||
t.Errorf("ListMethods returned wrong results: wanted %v, got %v", expected, names)
|
||||
@@ -214,7 +220,7 @@ func doTestListMethods(t *testing.T, source DescriptorSource, includeReflection
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list methods for ServerReflection: %v", err)
|
||||
}
|
||||
expected = []string{"ServerReflectionInfo"}
|
||||
expected = []string{"grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo"}
|
||||
} else {
|
||||
// without reflection, we see all services defined in the same test.proto file, which is the
|
||||
// TestService as well as UnimplementedService
|
||||
@@ -222,7 +228,7 @@ func doTestListMethods(t *testing.T, source DescriptorSource, includeReflection
|
||||
if err != nil {
|
||||
t.Fatalf("failed to list methods for ServerReflection: %v", err)
|
||||
}
|
||||
expected = []string{"UnimplementedCall"}
|
||||
expected = []string{"grpc.testing.UnimplementedService.UnimplementedCall"}
|
||||
}
|
||||
if !reflect.DeepEqual(expected, names) {
|
||||
t.Errorf("ListMethods returned wrong results: wanted %v, got %v", expected, names)
|
||||
@@ -235,6 +241,118 @@ func doTestListMethods(t *testing.T, source DescriptorSource, includeReflection
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllFiles(t *testing.T) {
|
||||
expectedFiles := []string{"testing/test.proto"}
|
||||
// server reflection picks up filename from linked in Go package,
|
||||
// which indicates "grpc_testing/test.proto", not our local copy.
|
||||
expectedFilesWithReflection := []string{"grpc_reflection_v1alpha/reflection.proto", "grpc_testing/test.proto"}
|
||||
|
||||
for _, ds := range descSources {
|
||||
t.Run(ds.name, func(t *testing.T) {
|
||||
files, err := GetAllFiles(ds.source)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get all files: %v", err)
|
||||
}
|
||||
names := fileNames(files)
|
||||
expected := expectedFiles
|
||||
if ds.includeRefl {
|
||||
expected = expectedFilesWithReflection
|
||||
}
|
||||
if !reflect.DeepEqual(expected, names) {
|
||||
t.Errorf("GetAllFiles returned wrong results: wanted %v, got %v", expected, names)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// try cases with more complicated set of files
|
||||
otherSourceProtoset, err := DescriptorSourceFromProtoSets("testing/test.protoset", "testing/example.protoset")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
otherSourceProtoFiles, err := DescriptorSourceFromProtoFiles(nil, "testing/test.proto", "testing/example.proto")
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
otherDescSources := []descSourceCase{
|
||||
{"protoset[b]", otherSourceProtoset, false},
|
||||
{"proto[b]", otherSourceProtoFiles, false},
|
||||
}
|
||||
expectedFiles = []string{
|
||||
"google/protobuf/any.proto",
|
||||
"google/protobuf/descriptor.proto",
|
||||
"google/protobuf/empty.proto",
|
||||
"google/protobuf/timestamp.proto",
|
||||
"testing/example.proto",
|
||||
"testing/example2.proto",
|
||||
"testing/test.proto",
|
||||
}
|
||||
for _, ds := range otherDescSources {
|
||||
t.Run(ds.name, func(t *testing.T) {
|
||||
files, err := GetAllFiles(ds.source)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get all files: %v", err)
|
||||
}
|
||||
names := fileNames(files)
|
||||
if !reflect.DeepEqual(expectedFiles, names) {
|
||||
t.Errorf("GetAllFiles returned wrong results: wanted %v, got %v", expectedFiles, names)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func fileNames(files []*desc.FileDescriptor) []string {
|
||||
names := make([]string, len(files))
|
||||
for i, f := range files {
|
||||
names[i] = f.GetName()
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
const expectKnownType = `{
|
||||
"dur": "0s",
|
||||
"ts": "1970-01-01T00:00:00Z",
|
||||
"dbl": 0,
|
||||
"flt": 0,
|
||||
"i64": "0",
|
||||
"u64": "0",
|
||||
"i32": 0,
|
||||
"u32": 0,
|
||||
"bool": false,
|
||||
"str": "",
|
||||
"bytes": null,
|
||||
"st": {"google.protobuf.Struct": "supports arbitrary JSON objects"},
|
||||
"an": {"@type": "type.googleapis.com/google.protobuf.Empty", "value": {}},
|
||||
"lv": [{"google.protobuf.ListValue": "is an array of arbitrary JSON values"}],
|
||||
"val": {"google.protobuf.Value": "supports arbitrary JSON"}
|
||||
}`
|
||||
|
||||
func TestMakeTemplateKnownTypes(t *testing.T) {
|
||||
descriptor, err := desc.LoadMessageDescriptorForMessage((*jsonpbtest.KnownTypes)(nil))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load descriptor: %v", err)
|
||||
}
|
||||
message := MakeTemplate(descriptor)
|
||||
|
||||
jsm := jsonpb.Marshaler{EmitDefaults: true}
|
||||
out, err := jsm.MarshalToString(message)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal to JSON: %v", err)
|
||||
}
|
||||
|
||||
// make sure template JSON matches expected
|
||||
var actual, expected interface{}
|
||||
if err := json.Unmarshal([]byte(out), &actual); err != nil {
|
||||
t.Fatalf("failed to parse actual JSON: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(expectKnownType), &expected); err != nil {
|
||||
t.Fatalf("failed to parse expected JSON: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Errorf("template message is not as expected; want:\n%s\ngot:\n%s", expectKnownType, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribe(t *testing.T) {
|
||||
for _, ds := range descSources {
|
||||
t.Run(ds.name, func(t *testing.T) {
|
||||
|
||||
389
invoke.go
Normal file
389
invoke.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package grpcurl
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/golang/protobuf/jsonpb"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/jhump/protoreflect/desc"
|
||||
"github.com/jhump/protoreflect/dynamic"
|
||||
"github.com/jhump/protoreflect/dynamic/grpcdynamic"
|
||||
"github.com/jhump/protoreflect/grpcreflect"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// InvocationEventHandler is a bag of callbacks for handling events that occur in the course
|
||||
// of invoking an RPC. The handler also provides request data that is sent. The callbacks are
|
||||
// generally called in the order they are listed below.
|
||||
type InvocationEventHandler interface {
|
||||
// OnResolveMethod is called with a descriptor of the method that is being invoked.
|
||||
OnResolveMethod(*desc.MethodDescriptor)
|
||||
// OnSendHeaders is called with the request metadata that is being sent.
|
||||
OnSendHeaders(metadata.MD)
|
||||
// OnReceiveHeaders is called when response headers have been received.
|
||||
OnReceiveHeaders(metadata.MD)
|
||||
// OnReceiveResponse is called for each response message received.
|
||||
OnReceiveResponse(proto.Message)
|
||||
// OnReceiveTrailers is called when response trailers and final RPC status have been received.
|
||||
OnReceiveTrailers(*status.Status, metadata.MD)
|
||||
}
|
||||
|
||||
// RequestMessageSupplier is a function that is called to retrieve request
|
||||
// messages for a GRPC operation. This type is deprecated and will be removed in
|
||||
// a future release.
|
||||
//
|
||||
// Deprecated: This is only used with the deprecated InvokeRpc. Instead, use
|
||||
// RequestSupplier with InvokeRPC.
|
||||
type RequestMessageSupplier func() ([]byte, error)
|
||||
|
||||
// InvokeRpc uses the given gRPC connection to invoke the given method. This function is deprecated
|
||||
// and will be removed in a future release. It just delegates to the similarly named InvokeRPC
|
||||
// method, whose signature is only slightly different.
|
||||
//
|
||||
// Deprecated: use InvokeRPC instead.
|
||||
func InvokeRpc(ctx context.Context, source DescriptorSource, cc *grpc.ClientConn, methodName string,
|
||||
headers []string, handler InvocationEventHandler, requestData RequestMessageSupplier) error {
|
||||
|
||||
return InvokeRPC(ctx, source, cc, methodName, headers, handler, func(m proto.Message) error {
|
||||
// New function is almost identical, but the request supplier function works differently.
|
||||
// So we adapt the logic here to maintain compatibility.
|
||||
data, err := requestData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return jsonpb.Unmarshal(bytes.NewReader(data), m)
|
||||
})
|
||||
}
|
||||
|
||||
// RequestSupplier is a function that is called to populate messages for a gRPC operation. The
|
||||
// function should populate the given message or return a non-nil error. If the supplier has no
|
||||
// more messages, it should return io.EOF. When it returns io.EOF, it should not in any way
|
||||
// modify the given message argument.
|
||||
type RequestSupplier func(proto.Message) error
|
||||
|
||||
// InvokeRPC uses the given gRPC channel to invoke the given method. The given descriptor source
|
||||
// is used to determine the type of method and the type of request and response message. The given
|
||||
// headers are sent as request metadata. Methods on the given event handler are called as the
|
||||
// invocation proceeds.
|
||||
//
|
||||
// The given requestData function supplies the actual data to send. It should return io.EOF when
|
||||
// there is no more request data. If the method being invoked is a unary or server-streaming RPC
|
||||
// (e.g. exactly one request message) and there is no request data (e.g. the first invocation of
|
||||
// the function returns io.EOF), then an empty request message is sent.
|
||||
//
|
||||
// If the requestData function and the given event handler coordinate or share any state, they should
|
||||
// be thread-safe. This is because the requestData function may be called from a different goroutine
|
||||
// than the one invoking event callbacks. (This only happens for bi-directional streaming RPCs, where
|
||||
// one goroutine sends request messages and another consumes the response messages).
|
||||
func InvokeRPC(ctx context.Context, source DescriptorSource, ch grpcdynamic.Channel, methodName string,
|
||||
headers []string, handler InvocationEventHandler, requestData RequestSupplier) error {
|
||||
|
||||
md := MetadataFromHeaders(headers)
|
||||
|
||||
svc, mth := parseSymbol(methodName)
|
||||
if svc == "" || mth == "" {
|
||||
return fmt.Errorf("given method name %q is not in expected format: 'service/method' or 'service.method'", methodName)
|
||||
}
|
||||
dsc, err := source.FindSymbol(svc)
|
||||
if err != nil {
|
||||
if isNotFoundError(err) {
|
||||
return fmt.Errorf("target server does not expose service %q", svc)
|
||||
}
|
||||
return fmt.Errorf("failed to query for service descriptor %q: %v", svc, err)
|
||||
}
|
||||
sd, ok := dsc.(*desc.ServiceDescriptor)
|
||||
if !ok {
|
||||
return fmt.Errorf("target server does not expose service %q", svc)
|
||||
}
|
||||
mtd := sd.FindMethodByName(mth)
|
||||
if mtd == nil {
|
||||
return fmt.Errorf("service %q does not include a method named %q", svc, mth)
|
||||
}
|
||||
|
||||
handler.OnResolveMethod(mtd)
|
||||
|
||||
// we also download any applicable extensions so we can provide full support for parsing user-provided data
|
||||
var ext dynamic.ExtensionRegistry
|
||||
alreadyFetched := map[string]bool{}
|
||||
if err = fetchAllExtensions(source, &ext, mtd.GetInputType(), alreadyFetched); err != nil {
|
||||
return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetInputType().GetFullyQualifiedName(), err)
|
||||
}
|
||||
if err = fetchAllExtensions(source, &ext, mtd.GetOutputType(), alreadyFetched); err != nil {
|
||||
return fmt.Errorf("error resolving server extensions for message %s: %v", mtd.GetOutputType().GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
msgFactory := dynamic.NewMessageFactoryWithExtensionRegistry(&ext)
|
||||
req := msgFactory.NewMessage(mtd.GetInputType())
|
||||
|
||||
handler.OnSendHeaders(md)
|
||||
ctx = metadata.NewOutgoingContext(ctx, md)
|
||||
|
||||
stub := grpcdynamic.NewStubWithMessageFactory(ch, msgFactory)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
if mtd.IsClientStreaming() && mtd.IsServerStreaming() {
|
||||
return invokeBidi(ctx, stub, mtd, handler, requestData, req)
|
||||
} else if mtd.IsClientStreaming() {
|
||||
return invokeClientStream(ctx, stub, mtd, handler, requestData, req)
|
||||
} else if mtd.IsServerStreaming() {
|
||||
return invokeServerStream(ctx, stub, mtd, handler, requestData, req)
|
||||
} else {
|
||||
return invokeUnary(ctx, stub, mtd, handler, requestData, req)
|
||||
}
|
||||
}
|
||||
|
||||
func invokeUnary(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestSupplier, req proto.Message) error {
|
||||
|
||||
err := requestData(req)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
if err != io.EOF {
|
||||
// verify there is no second message, which is a usage error
|
||||
err := requestData(req)
|
||||
if err == nil {
|
||||
return fmt.Errorf("method %q is a unary RPC, but request data contained more than 1 message", md.GetFullyQualifiedName())
|
||||
} else if err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can actually invoke the RPC!
|
||||
var respHeaders metadata.MD
|
||||
var respTrailers metadata.MD
|
||||
resp, err := stub.InvokeRpc(ctx, md, req, grpc.Trailer(&respTrailers), grpc.Header(&respHeaders))
|
||||
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
|
||||
if stat.Code() == codes.OK {
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, respTrailers)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeClientStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestSupplier, req proto.Message) error {
|
||||
|
||||
// invoke the RPC!
|
||||
str, err := stub.InvokeRpcClientStream(ctx, md)
|
||||
|
||||
// Upload each request message in the stream
|
||||
var resp proto.Message
|
||||
for err == nil {
|
||||
err = requestData(req)
|
||||
if err == io.EOF {
|
||||
resp, err = str.CloseAndReceive()
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
|
||||
err = str.SendMsg(req)
|
||||
if err == io.EOF {
|
||||
// We get EOF on send if the server says "go away"
|
||||
// We have to use CloseAndReceive to get the actual code
|
||||
resp, err = str.CloseAndReceive()
|
||||
break
|
||||
}
|
||||
|
||||
req.Reset()
|
||||
}
|
||||
|
||||
// finally, process response data
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
if respHeaders, err := str.Header(); err == nil {
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
}
|
||||
|
||||
if stat.Code() == codes.OK {
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, str.Trailer())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeServerStream(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestSupplier, req proto.Message) error {
|
||||
|
||||
err := requestData(req)
|
||||
if err != nil && err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
if err != io.EOF {
|
||||
// verify there is no second message, which is a usage error
|
||||
err := requestData(req)
|
||||
if err == nil {
|
||||
return fmt.Errorf("method %q is a server-streaming RPC, but request data contained more than 1 message", md.GetFullyQualifiedName())
|
||||
} else if err != io.EOF {
|
||||
return fmt.Errorf("error getting request data: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Now we can actually invoke the RPC!
|
||||
str, err := stub.InvokeRpcServerStream(ctx, md, req)
|
||||
|
||||
if respHeaders, err := str.Header(); err == nil {
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
}
|
||||
|
||||
// Download each response message
|
||||
for err == nil {
|
||||
var resp proto.Message
|
||||
resp, err = str.RecvMsg()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, str.Trailer())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func invokeBidi(ctx context.Context, stub grpcdynamic.Stub, md *desc.MethodDescriptor, handler InvocationEventHandler,
|
||||
requestData RequestSupplier, req proto.Message) error {
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// invoke the RPC!
|
||||
str, err := stub.InvokeRpcBidiStream(ctx, md)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var sendErr atomic.Value
|
||||
|
||||
defer wg.Wait()
|
||||
|
||||
if err == nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Concurrently upload each request message in the stream
|
||||
var err error
|
||||
for err == nil {
|
||||
err = requestData(req)
|
||||
|
||||
if err == io.EOF {
|
||||
err = str.CloseSend()
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error getting request data: %v", err)
|
||||
break
|
||||
}
|
||||
|
||||
err = str.SendMsg(req)
|
||||
|
||||
req.Reset()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
sendErr.Store(err)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if respHeaders, err := str.Header(); err == nil {
|
||||
handler.OnReceiveHeaders(respHeaders)
|
||||
}
|
||||
|
||||
// Download each response message
|
||||
for err == nil {
|
||||
var resp proto.Message
|
||||
resp, err = str.RecvMsg()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
handler.OnReceiveResponse(resp)
|
||||
}
|
||||
|
||||
if se, ok := sendErr.Load().(error); ok && se != io.EOF {
|
||||
err = se
|
||||
}
|
||||
|
||||
stat, ok := status.FromError(err)
|
||||
if !ok {
|
||||
// Error codes sent from the server will get printed differently below.
|
||||
// So just bail for other kinds of errors here.
|
||||
return fmt.Errorf("grpc call for %q failed: %v", md.GetFullyQualifiedName(), err)
|
||||
}
|
||||
|
||||
handler.OnReceiveTrailers(stat, str.Trailer())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type notFoundError string
|
||||
|
||||
func notFound(kind, name string) error {
|
||||
return notFoundError(fmt.Sprintf("%s not found: %s", kind, name))
|
||||
}
|
||||
|
||||
func (e notFoundError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func isNotFoundError(err error) bool {
|
||||
if grpcreflect.IsElementNotFoundError(err) {
|
||||
return true
|
||||
}
|
||||
_, ok := err.(notFoundError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func parseSymbol(svcAndMethod string) (string, string) {
|
||||
pos := strings.LastIndex(svcAndMethod, "/")
|
||||
if pos < 0 {
|
||||
pos = strings.LastIndex(svcAndMethod, ".")
|
||||
if pos < 0 {
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
return svcAndMethod[:pos], svcAndMethod[pos+1:]
|
||||
}
|
||||
@@ -7,8 +7,8 @@ cd "$(dirname $0)"
|
||||
# Run this script to generate files used by tests.
|
||||
|
||||
echo "Creating protosets..."
|
||||
protoc ../../../google.golang.org/grpc/interop/grpc_testing/test.proto \
|
||||
-I../../../ --include_imports \
|
||||
protoc testing/test.proto \
|
||||
--include_imports \
|
||||
--descriptor_set_out=testing/test.protoset
|
||||
|
||||
protoc testing/example.proto \
|
||||
|
||||
@@ -38,6 +38,7 @@ func (a *accounts) openAccount(customer string, accountType Account_Type, initia
|
||||
a.AccountNumbersByCustomer[customer] = accountNums
|
||||
var acct account
|
||||
acct.AccountNumber = num
|
||||
acct.Type = accountType
|
||||
acct.BalanceCents = initialBalanceCents
|
||||
acct.Transactions = append(acct.Transactions, &Transaction{
|
||||
AccountNumber: num,
|
||||
|
||||
@@ -3,9 +3,11 @@ syntax = "proto3";
|
||||
import "google/protobuf/descriptor.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "testing/example2.proto";
|
||||
|
||||
message TestRequest {
|
||||
repeated string file_names = 1;
|
||||
repeated Extension extensions = 2;
|
||||
}
|
||||
|
||||
message TestResponse {
|
||||
|
||||
Binary file not shown.
8
testing/example2.proto
Normal file
8
testing/example2.proto
Normal file
@@ -0,0 +1,8 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
message Extension {
|
||||
uint64 id = 1;
|
||||
google.protobuf.Any data = 2;
|
||||
}
|
||||
Binary file not shown.
Reference in New Issue
Block a user