mirror of
https://github.com/fullstorydev/grpcurl.git
synced 2026-06-11 05:21:44 +03:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4054d1d115 | ||
|
|
5631bba117 | ||
|
|
80425d1b17 | ||
|
|
7e4045565f | ||
|
|
e5b4fc6cc0 | ||
|
|
09c3d1d69e | ||
|
|
5d6316f470 | ||
|
|
f0723c6273 | ||
|
|
fe97274a1b | ||
|
|
1bbf8dae71 | ||
|
|
0fcd3253f6 | ||
|
|
4c9c82cec3 | ||
|
|
5082a1dc68 | ||
|
|
d641a66208 | ||
|
|
ce84976d3c | ||
|
|
b292d5aef8 | ||
|
|
5516a45602 | ||
|
|
4a329f3b13 | ||
|
|
1c6532c060 | ||
|
|
0f9e76c978 | ||
|
|
9fa2fce63b | ||
|
|
70e9bba1b8 | ||
|
|
d86529bb4f | ||
|
|
0dea37ee70 | ||
|
|
dfa06f4410 | ||
|
|
22ce2f04fd | ||
|
|
1e8e50f4f8 | ||
|
|
7cabe7a9d0 | ||
|
|
9a4bbacdd6 | ||
|
|
69ea782936 | ||
|
|
58cd93280e | ||
|
|
a337c1afcf | ||
|
|
e00ef3eb7c | ||
|
|
397a8c18ca | ||
|
|
554e69be2c | ||
|
|
2dd771c49e | ||
|
|
79a550b858 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
VERSION
|
||||||
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
|
||||||
13
.travis.yml
13
.travis.yml
@@ -3,16 +3,19 @@ sudo: false
|
|||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- go: "1.7"
|
|
||||||
- go: "1.8"
|
|
||||||
- go: "1.9"
|
- go: "1.9"
|
||||||
- go: "1.10"
|
- go: "1.10"
|
||||||
env: VET=1
|
|
||||||
- go: "1.11"
|
- go: "1.11"
|
||||||
|
env:
|
||||||
|
- GO111MODULE=off
|
||||||
|
- VET=1
|
||||||
|
- go: "1.11"
|
||||||
|
env: GO111MODULE=on
|
||||||
|
- go: "1.12"
|
||||||
env: GO111MODULE=off
|
env: GO111MODULE=off
|
||||||
- go: "1.11"
|
- go: "1.12"
|
||||||
env: GO111MODULE=on
|
env: GO111MODULE=on
|
||||||
- go: tip
|
- go: tip
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- if [[ "$VET" = 1 ]]; then make; else make deps test; fi
|
- if [[ "$VET" = 1 ]]; then make ci; else make deps test; fi
|
||||||
|
|||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
FROM golang:1.11.10-alpine as builder
|
||||||
|
MAINTAINER FullStory Engineering
|
||||||
|
|
||||||
|
# currently, a module build requires gcc (so Go tool can build
|
||||||
|
# module-aware versions of std library; it ships only w/ the
|
||||||
|
# non-module versions)
|
||||||
|
RUN apk update && apk add --no-cache ca-certificates git gcc g++ libc-dev
|
||||||
|
# create non-privileged group and user
|
||||||
|
RUN addgroup -S grpcurl && adduser -S grpcurl -G grpcurl
|
||||||
|
|
||||||
|
WORKDIR /tmp/fullstorydev/grpcurl
|
||||||
|
# copy just the files/sources we need to build grpcurl
|
||||||
|
COPY VERSION *.go go.* /tmp/fullstorydev/grpcurl/
|
||||||
|
COPY cmd /tmp/fullstorydev/grpcurl/cmd
|
||||||
|
# and build a completely static binary (so we can use
|
||||||
|
# scratch as basis for the final image)
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
ENV GOOS=linux
|
||||||
|
ENV GOARCH=amd64
|
||||||
|
ENV GO111MODULE=on
|
||||||
|
RUN go build -o /grpcurl \
|
||||||
|
-ldflags "-w -extldflags \"-static\" -X \"main.version=$(cat VERSION)\"" \
|
||||||
|
./cmd/grpcurl
|
||||||
|
|
||||||
|
# New FROM so we have a nice'n'tiny image
|
||||||
|
FROM scratch
|
||||||
|
WORKDIR /
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
COPY --from=builder /etc/passwd /etc/passwd
|
||||||
|
COPY --from=builder /grpcurl /bin/grpcurl
|
||||||
|
USER grpcurl
|
||||||
|
|
||||||
|
ENTRYPOINT ["/bin/grpcurl"]
|
||||||
34
Makefile
34
Makefile
@@ -1,10 +1,12 @@
|
|||||||
|
dev_build_version=$(shell git describe --tags --always --dirty)
|
||||||
|
|
||||||
# TODO: run golint and errcheck, but only to catch *new* violations and
|
# 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
|
# 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
|
# violations already in the code). They can be useful to catch errors, but
|
||||||
# they are just too noisy to be a requirement for a CI -- we don't even *want*
|
# they are just too noisy to be a requirement for a CI -- we don't even *want*
|
||||||
# to fix some of the things they consider to be violations.
|
# to fix some of the things they consider to be violations.
|
||||||
.PHONY: ci
|
.PHONY: ci
|
||||||
ci: deps checkgofmt vet staticcheck unused ineffassign predeclared test
|
ci: deps checkgofmt vet staticcheck ineffassign predeclared test
|
||||||
|
|
||||||
.PHONY: deps
|
.PHONY: deps
|
||||||
deps:
|
deps:
|
||||||
@@ -16,7 +18,18 @@ updatedeps:
|
|||||||
|
|
||||||
.PHONY: install
|
.PHONY: install
|
||||||
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: docker
|
||||||
|
docker:
|
||||||
|
@echo $(dev_build_version) > VERSION
|
||||||
|
docker build -t fullstorydev/grpcurl:$(dev_build_version) .
|
||||||
|
@rm VERSION
|
||||||
|
|
||||||
.PHONY: checkgofmt
|
.PHONY: checkgofmt
|
||||||
checkgofmt:
|
checkgofmt:
|
||||||
@@ -29,16 +42,19 @@ checkgofmt:
|
|||||||
vet:
|
vet:
|
||||||
go 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
|
.PHONY: staticcheck
|
||||||
staticcheck:
|
staticcheck:
|
||||||
@go get honnef.co/go/tools/cmd/staticcheck
|
@go get honnef.co/go/tools/cmd/staticcheck
|
||||||
staticcheck ./...
|
staticcheck ./...
|
||||||
|
|
||||||
.PHONY: unused
|
|
||||||
unused:
|
|
||||||
@go get honnef.co/go/tools/cmd/unused
|
|
||||||
unused ./...
|
|
||||||
|
|
||||||
.PHONY: ineffassign
|
.PHONY: ineffassign
|
||||||
ineffassign:
|
ineffassign:
|
||||||
@go get github.com/gordonklaus/ineffassign
|
@go get github.com/gordonklaus/ineffassign
|
||||||
@@ -52,11 +68,11 @@ predeclared:
|
|||||||
# Intentionally omitted from CI, but target here for ad-hoc reports.
|
# Intentionally omitted from CI, but target here for ad-hoc reports.
|
||||||
.PHONY: golint
|
.PHONY: golint
|
||||||
golint:
|
golint:
|
||||||
@go get github.com/golang/lint/golint
|
@go get golang.org/x/lint/golint
|
||||||
golint -min_confidence 0.9 -set_exit_status ./...
|
golint -min_confidence 0.9 -set_exit_status ./...
|
||||||
|
|
||||||
# Intentionally omitted from CI, but target here for ad-hoc reports.
|
# Intentionally omitted from CI, but target here for ad-hoc reports.
|
||||||
.PHONY: errchack
|
.PHONY: errcheck
|
||||||
errcheck:
|
errcheck:
|
||||||
@go get github.com/kisielk/errcheck
|
@go get github.com/kisielk/errcheck
|
||||||
errcheck ./...
|
errcheck ./...
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -14,22 +14,22 @@ This program accepts messages using JSON encoding, which is much more friendly f
|
|||||||
humans and scripts.
|
humans and scripts.
|
||||||
|
|
||||||
With this tool you can also browse the schema for gRPC services, either by querying
|
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),
|
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
|
by reading proto source files, or by loading in compiled "protoset" files (files that contain
|
||||||
[descriptor protos](https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto)).
|
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
|
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
|
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
|
reflection, you will either need the proto source files that define the service or need
|
||||||
protoset files that `grpcurl` can use.
|
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
|
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
|
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
|
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
|
the [protoreflect](https://godoc.org/github.com/jhump/protoreflect) library, and shows
|
||||||
off what they can do.
|
off what they can do.
|
||||||
|
|
||||||
|
See also the [`grpcurl` talk at GopherCon 2018](https://www.youtube.com/watch?v=dDr-8kbMnaw).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
`grpcurl` supports all kinds of RPC methods, including streaming methods. You can even
|
`grpcurl` supports all kinds of RPC methods, including streaming methods. You can even
|
||||||
operate bi-directional streaming methods interactively by running `grpcurl` from an
|
operate bi-directional streaming methods interactively by running `grpcurl` from an
|
||||||
@@ -44,6 +44,17 @@ service. If not, you can supply the `.proto` source files or you can supply prot
|
|||||||
files (containing compiled descriptors, produced by `protoc`) to `grpcurl`.
|
files (containing compiled descriptors, produced by `protoc`) to `grpcurl`.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
### Binaries
|
||||||
|
|
||||||
|
Download the binary from the [releases](https://github.com/fullstorydev/grpcurl/releases) page.
|
||||||
|
|
||||||
|
On macOS, `grpcurl` is available via Homebrew:
|
||||||
|
```shell
|
||||||
|
brew install grpcurl
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Source
|
||||||
You can use the `go` tool to install `grpcurl`:
|
You can use the `go` tool to install `grpcurl`:
|
||||||
```shell
|
```shell
|
||||||
go get github.com/fullstorydev/grpcurl
|
go get github.com/fullstorydev/grpcurl
|
||||||
@@ -59,7 +70,11 @@ If you have already pulled down this repo to a location that is not in your
|
|||||||
run `make install`.
|
run `make install`.
|
||||||
|
|
||||||
If you encounter compile errors, you could have out-dated versions of `grpcurl`'s
|
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
|
## Usage
|
||||||
The usage doc for the tool explains the numerous options:
|
The usage doc for the tool explains the numerous options:
|
||||||
@@ -72,7 +87,7 @@ In the sections below, you will find numerous examples demonstrating how to use
|
|||||||
|
|
||||||
### Invoking RPCs
|
### Invoking RPCs
|
||||||
Invoking an RPC on a trusted server (e.g. TLS without self-signed key or custom CA)
|
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:
|
do with `grpcurl`. This minimal invocation sends an empty request body:
|
||||||
```shell
|
```shell
|
||||||
grpcurl grpc.server.com:443 my.custom.server.Service/Method
|
grpcurl grpc.server.com:443 my.custom.server.Service/Method
|
||||||
@@ -92,7 +107,7 @@ If you want to include `grpcurl` in a command pipeline, such as when using `jq`
|
|||||||
create a request body, you can use `-d @`, which tells `grpcurl` to read the actual
|
create a request body, you can use `-d @`, which tells `grpcurl` to read the actual
|
||||||
request body from stdin:
|
request body from stdin:
|
||||||
```shell
|
```shell
|
||||||
grpcurl -d @ grpc.server.com:443 my.custom.server.Service/Method <<<EOM
|
grpcurl -d @ grpc.server.com:443 my.custom.server.Service/Method <<EOM
|
||||||
{
|
{
|
||||||
"id": 1234,
|
"id": 1234,
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -125,8 +140,10 @@ grpcurl localhost:8787 list my.custom.server.Service
|
|||||||
|
|
||||||
### Describing Elements
|
### Describing Elements
|
||||||
The "describe" verb will print the type of any symbol that the server knows about
|
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
|
or that is found in a given protoset file. It also prints a description of that
|
||||||
symbol, in JSON.
|
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
|
```shell
|
||||||
# Server supports reflection
|
# Server supports reflection
|
||||||
grpcurl localhost:8787 describe my.custom.server.Service.MethodOne
|
grpcurl localhost:8787 describe my.custom.server.Service.MethodOne
|
||||||
@@ -138,7 +155,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
|
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
|
To use `grpcurl` on servers that do not support reflection, you can use `.proto` source
|
||||||
files.
|
files.
|
||||||
|
|
||||||
@@ -151,14 +185,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`
|
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.
|
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
|
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
|
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
|
by using protoset files (since it skips the parsing and compilation steps with each
|
||||||
invocation).
|
invocation).
|
||||||
|
|
||||||
Protoset files contain binary encoded `google.protobuf.FileDescriptorSet` protos. To create
|
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
|
```shell
|
||||||
protoc --proto_path=. \
|
protoc --proto_path=. \
|
||||||
--descriptor_set_out=myservice.protoset \
|
--descriptor_set_out=myservice.protoset \
|
||||||
@@ -169,3 +207,8 @@ protoc --proto_path=. \
|
|||||||
The `--descriptor_set_out` argument is what tells `protoc` to produce a protoset,
|
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
|
and the `--include_imports` argument is necessary for the protoset to contain
|
||||||
everything that `grpcurl` needs to process and understand the schema.
|
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
|
// Command grpcurl makes gRPC requests (a la cURL, but HTTP/2). It can use a supplied descriptor
|
||||||
// service reflection to translate JSON request data into the appropriate protobuf request data and vice
|
// file, protobuf sources, or service reflection to translate JSON or text request data into the
|
||||||
// versa for presenting the response contents.
|
// appropriate protobuf messages and vice versa for presenting the response contents.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/protobuf/jsonpb"
|
"github.com/fullstorydev/grpcurl"
|
||||||
"github.com/golang/protobuf/proto"
|
|
||||||
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||||
"github.com/jhump/protoreflect/desc"
|
"github.com/jhump/protoreflect/desc"
|
||||||
"github.com/jhump/protoreflect/dynamic"
|
|
||||||
"github.com/jhump/protoreflect/grpcreflect"
|
"github.com/jhump/protoreflect/grpcreflect"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
@@ -25,107 +23,124 @@ import (
|
|||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
|
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 (
|
var (
|
||||||
exit = os.Exit
|
exit = os.Exit
|
||||||
|
|
||||||
isUnixSocket func() bool // nil when run on non-unix platform
|
isUnixSocket func() bool // nil when run on non-unix platform
|
||||||
|
|
||||||
help = flag.Bool("help", false,
|
flags = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||||
`Print usage instructions and exit.`)
|
|
||||||
plaintext = flag.Bool("plaintext", false,
|
help = flags.Bool("help", false, prettify(`
|
||||||
`Use plain-text HTTP/2 when connecting to server (no TLS).`)
|
Print usage instructions and exit.`))
|
||||||
insecure = flag.Bool("insecure", false,
|
printVersion = flags.Bool("version", false, prettify(`
|
||||||
`Skip server certificate and domain verification. (NOT SECURE!). Not
|
Print version.`))
|
||||||
valid with -plaintext option.`)
|
plaintext = flags.Bool("plaintext", false, prettify(`
|
||||||
cacert = flag.String("cacert", "",
|
Use plain-text HTTP/2 when connecting to server (no TLS).`))
|
||||||
`File containing trusted root certificates for verifying the server.
|
insecure = flags.Bool("insecure", false, prettify(`
|
||||||
Ignored if -insecure is specified.`)
|
Skip server certificate and domain verification. (NOT SECURE!) Not
|
||||||
cert = flag.String("cert", "",
|
valid with -plaintext option.`))
|
||||||
`File containing client certificate (public key), to present to the
|
cacert = flags.String("cacert", "", prettify(`
|
||||||
server. Not valid with -plaintext option. Must also provide -key option.`)
|
File containing trusted root certificates for verifying the server.
|
||||||
key = flag.String("key", "",
|
Ignored if -insecure is specified.`))
|
||||||
`File containing client private key, to present to the server. Not valid
|
cert = flags.String("cert", "", prettify(`
|
||||||
with -plaintext option. Must also provide -cert option.`)
|
File containing client certificate (public key), to present to the
|
||||||
|
server. Not valid with -plaintext option. Must also provide -key option.`))
|
||||||
|
key = flags.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
|
protoset multiString
|
||||||
protoFiles multiString
|
protoFiles multiString
|
||||||
importPaths multiString
|
importPaths multiString
|
||||||
addlHeaders multiString
|
addlHeaders multiString
|
||||||
rpcHeaders multiString
|
rpcHeaders multiString
|
||||||
reflHeaders multiString
|
reflHeaders multiString
|
||||||
authority = flag.String("authority", "",
|
authority = flags.String("authority", "", prettify(`
|
||||||
":authority pseudo header value to be passed along with underlying HTTP/2 requests. It defaults to `host [ \":\" port ]` part of the target url.")
|
Value of :authority pseudo-header to be use with underlying HTTP/2
|
||||||
data = flag.String("d", "",
|
requests. It defaults to the given address.`))
|
||||||
`JSON request contents. If the value is '@' then the request contents are
|
data = flags.String("d", "", prettify(`
|
||||||
read from stdin. For calls that accept a stream of requests, the
|
Data for request contents. If the value is '@' then the request contents
|
||||||
contents should include all such request messages concatenated together
|
are read from stdin. For calls that accept a stream of requests, the
|
||||||
(optionally separated by whitespace).`)
|
contents should include all such request messages concatenated together
|
||||||
connectTimeout = flag.String("connect-timeout", "",
|
(possibly delimited; see -format).`))
|
||||||
`The maximum time, in seconds, to wait for connection to be established.
|
format = flags.String("format", "json", prettify(`
|
||||||
Defaults to 10 seconds.`)
|
The format of request data. The allowed values are 'json' or 'text'. For
|
||||||
keepaliveTime = flag.String("keepalive-time", "",
|
'json', the input data must be in JSON format. Multiple request values
|
||||||
`If present, the maximum idle time in seconds, after which a keepalive
|
may be concatenated (messages with a JSON representation other than
|
||||||
probe is sent. If the connection remains idle and no keepalive response
|
object must be separated by whitespace, such as a newline). For 'text',
|
||||||
is received for this same period then the connection is closed and the
|
the input data must be in the protobuf text format, in which case
|
||||||
operation fails.`)
|
multiple request values must be separated by the "record separator"
|
||||||
maxTime = flag.String("max-time", "",
|
ASCII character: 0x1E. The stream should not end in a record separator.
|
||||||
`The maximum total time the operation can take. This is useful for
|
If it does, it will be interpreted as a final, blank message after the
|
||||||
preventing batch jobs that use grpcurl from hanging due to slow or bad
|
separator.`))
|
||||||
network links or due to incorrect stream method usage.`)
|
connectTimeout = flags.Float64("connect-timeout", 0, prettify(`
|
||||||
emitDefaults = flag.Bool("emit-defaults", false,
|
The maximum time, in seconds, to wait for connection to be established.
|
||||||
`Emit default values from JSON-encoded responses.`)
|
Defaults to 10 seconds.`))
|
||||||
msgTemplate = flag.Bool("msg-template", false,
|
keepaliveTime = flags.Float64("keepalive-time", 0, prettify(`
|
||||||
`When describing messages, show a JSON template for the message type.`)
|
If present, the maximum idle time in seconds, after which a keepalive
|
||||||
verbose = flag.Bool("v", false,
|
probe is sent. If the connection remains idle and no keepalive response
|
||||||
`Enable verbose output.`)
|
is received for this same period then the connection is closed and the
|
||||||
serverName = flag.String("servername", "", "Override servername when validating TLS certificate.")
|
operation fails.`))
|
||||||
|
maxTime = flags.Float64("max-time", 0, prettify(`
|
||||||
|
The maximum total time the operation can take, in seconds. 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.`))
|
||||||
|
maxMsgSz = flags.Int("max-msg-sz", 0, prettify(`
|
||||||
|
The maximum encoded size of a response message, in bytes, that grpcurl
|
||||||
|
will accept. If not specified, defaults to 4,194,304 (4 megabytes).`))
|
||||||
|
emitDefaults = flags.Bool("emit-defaults", false, prettify(`
|
||||||
|
Emit default values for JSON-encoded responses.`))
|
||||||
|
msgTemplate = flags.Bool("msg-template", false, prettify(`
|
||||||
|
When describing messages, show a template of input data.`))
|
||||||
|
verbose = flags.Bool("v", false, prettify(`
|
||||||
|
Enable verbose output.`))
|
||||||
|
serverName = flags.String("servername", "", prettify(`
|
||||||
|
Override server name when validating TLS certificate.`))
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
flag.Var(&addlHeaders, "H",
|
flags.Var(&addlHeaders, "H", prettify(`
|
||||||
`Additional headers in 'name: value' format. May specify more than one
|
Additional headers in 'name: value' format. May specify more than one
|
||||||
via multiple flags. These headers will also be included in reflection
|
via multiple flags. These headers will also be included in reflection
|
||||||
requests requests to a server.`)
|
requests requests to a server.`))
|
||||||
flag.Var(&rpcHeaders, "rpc-header",
|
flags.Var(&rpcHeaders, "rpc-header", prettify(`
|
||||||
`Additional RPC headers in 'name: value' format. May specify more than
|
Additional RPC headers in 'name: value' format. May specify more than
|
||||||
one via multiple flags. These headers will *only* be used when invoking
|
one via multiple flags. These headers will *only* be used when invoking
|
||||||
the requested RPC method. They are excluded from reflection requests.`)
|
the requested RPC method. They are excluded from reflection requests.`))
|
||||||
flag.Var(&reflHeaders, "reflect-header",
|
flags.Var(&reflHeaders, "reflect-header", prettify(`
|
||||||
`Additional reflection headers in 'name: value' format. May specify more
|
Additional reflection headers in 'name: value' format. May specify more
|
||||||
than one via multiple flags. These headers will only be used during
|
than one via multiple flags. These headers will *only* be used during
|
||||||
reflection requests and will be excluded when invoking the requested RPC
|
reflection requests and will be excluded when invoking the requested RPC
|
||||||
method.`)
|
method.`))
|
||||||
flag.Var(&protoset, "protoset",
|
flags.Var(&protoset, "protoset", prettify(`
|
||||||
`The name of a file containing an encoded FileDescriptorSet. This file's
|
The name of a file containing an encoded FileDescriptorSet. This file's
|
||||||
contents will be used to determine the RPC schema instead of querying
|
contents will be used to determine the RPC schema instead of querying
|
||||||
for it from the remote server via the GRPC reflection API. When set: the
|
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.
|
'list' action lists the services found in the given descriptors (vs.
|
||||||
those exposed by the remote server), and the 'describe' action describes
|
those exposed by the remote server), and the 'describe' action describes
|
||||||
symbols found in the given descriptors. May specify more than one via
|
symbols found in the given descriptors. May specify more than one via
|
||||||
multiple -protoset flags. It is an error to use both -protoset and
|
multiple -protoset flags. It is an error to use both -protoset and
|
||||||
-proto flags.`)
|
-proto flags.`))
|
||||||
flag.Var(&protoFiles, "proto",
|
flags.Var(&protoFiles, "proto", prettify(`
|
||||||
`The name of a proto source file. Source files given will be used to
|
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
|
determine the RPC schema instead of querying for it from the remote
|
||||||
server via the GRPC reflection API. When set: the 'list' action lists
|
server via the gRPC reflection API. When set: the 'list' action lists
|
||||||
the services found in the given files and their imports (vs. those
|
the services found in the given files and their imports (vs. those
|
||||||
exposed by the remote server), and the 'describe' action describes
|
exposed by the remote server), and the 'describe' action describes
|
||||||
symbols found in the given files. May specify more than one via
|
symbols found in the given files. May specify more than one via multiple
|
||||||
multiple -proto flags. Imports will be resolved using the given
|
-proto flags. Imports will be resolved using the given -import-path
|
||||||
-import-path flags. Multiple proto files can be specified by specifying
|
flags. Multiple proto files can be specified by specifying multiple
|
||||||
multiple -proto flags. It is an error to use both -protoset and -proto
|
-proto flags. It is an error to use both -protoset and -proto flags.`))
|
||||||
flags.`)
|
flags.Var(&importPaths, "import-path", prettify(`
|
||||||
flag.Var(&importPaths, "import-path",
|
The path to a directory from which proto sources can be imported, for
|
||||||
`The path to a directory from which proto sources can be imported,
|
use with -proto flags. Multiple import paths can be configured by
|
||||||
for use with -proto flags. Multiple import paths can be configured by
|
specifying multiple -import-path flags. Paths will be searched in the
|
||||||
specifying multiple -import-path flags. Paths will be searched in the
|
order given. If no import paths are given, all files (including all
|
||||||
order given. If no import paths are given, all files (including all
|
imports) must be provided as -proto flags, and grpcurl will attempt to
|
||||||
imports) must be provided as -proto flags, and grpcurl will attempt to
|
resolve all import statements from the set of file names given.`))
|
||||||
resolve all import statements from the set of file names given.`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type multiString []string
|
type multiString []string
|
||||||
@@ -140,14 +155,30 @@ func (s *multiString) Set(value string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.CommandLine.Usage = usage
|
flags.Usage = usage
|
||||||
flag.Parse()
|
flags.Parse(os.Args[1:])
|
||||||
if *help {
|
if *help {
|
||||||
usage()
|
usage()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
if *printVersion {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s %s\n", filepath.Base(os.Args[0]), version)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
// Do extra validation on arguments and figure out what user asked us to do.
|
// Do extra validation on arguments and figure out what user asked us to do.
|
||||||
|
if *connectTimeout < 0 {
|
||||||
|
fail(nil, "The -connect-timeout argument must not be negative.")
|
||||||
|
}
|
||||||
|
if *keepaliveTime < 0 {
|
||||||
|
fail(nil, "The -keepalive-time argument must not be negative.")
|
||||||
|
}
|
||||||
|
if *maxTime < 0 {
|
||||||
|
fail(nil, "The -max-time argument must not be negative.")
|
||||||
|
}
|
||||||
|
if *maxMsgSz < 0 {
|
||||||
|
fail(nil, "The -max-msg-sz argument must not be negative.")
|
||||||
|
}
|
||||||
if *plaintext && *insecure {
|
if *plaintext && *insecure {
|
||||||
fail(nil, "The -plaintext and -insecure arguments are mutually exclusive.")
|
fail(nil, "The -plaintext and -insecure arguments are mutually exclusive.")
|
||||||
}
|
}
|
||||||
@@ -160,8 +191,14 @@ func main() {
|
|||||||
if (*key == "") != (*cert == "") {
|
if (*key == "") != (*cert == "") {
|
||||||
fail(nil, "The -cert and -key arguments must be used together and both be present.")
|
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()
|
args := flags.Args()
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
fail(nil, "Too few arguments.")
|
fail(nil, "Too few arguments.")
|
||||||
@@ -226,38 +263,29 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if *maxTime != "" {
|
if *maxTime > 0 {
|
||||||
t, err := strconv.ParseFloat(*maxTime, 64)
|
timeout := time.Duration(*maxTime * float64(time.Second))
|
||||||
if err != nil {
|
|
||||||
fail(nil, "The -max-time argument must be a valid number.")
|
|
||||||
}
|
|
||||||
timeout := time.Duration(t * float64(time.Second))
|
|
||||||
ctx, _ = context.WithTimeout(ctx, timeout)
|
ctx, _ = context.WithTimeout(ctx, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
dial := func() *grpc.ClientConn {
|
dial := func() *grpc.ClientConn {
|
||||||
dialTime := 10 * time.Second
|
dialTime := 10 * time.Second
|
||||||
if *connectTimeout != "" {
|
if *connectTimeout > 0 {
|
||||||
t, err := strconv.ParseFloat(*connectTimeout, 64)
|
dialTime = time.Duration(*connectTimeout * float64(time.Second))
|
||||||
if err != nil {
|
|
||||||
fail(nil, "The -connect-timeout argument must be a valid number.")
|
|
||||||
}
|
|
||||||
dialTime = time.Duration(t * float64(time.Second))
|
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithTimeout(ctx, dialTime)
|
ctx, cancel := context.WithTimeout(ctx, dialTime)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
var opts []grpc.DialOption
|
var opts []grpc.DialOption
|
||||||
if *keepaliveTime != "" {
|
if *keepaliveTime > 0 {
|
||||||
t, err := strconv.ParseFloat(*keepaliveTime, 64)
|
timeout := time.Duration(*keepaliveTime * float64(time.Second))
|
||||||
if err != nil {
|
|
||||||
fail(nil, "The -keepalive-time argument must be a valid number.")
|
|
||||||
}
|
|
||||||
timeout := time.Duration(t * float64(time.Second))
|
|
||||||
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
opts = append(opts, grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||||
Time: timeout,
|
Time: timeout,
|
||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
if *maxMsgSz > 0 {
|
||||||
|
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(*maxMsgSz)))
|
||||||
|
}
|
||||||
if *authority != "" {
|
if *authority != "" {
|
||||||
opts = append(opts, grpc.WithAuthority(*authority))
|
opts = append(opts, grpc.WithAuthority(*authority))
|
||||||
}
|
}
|
||||||
@@ -378,43 +406,77 @@ func main() {
|
|||||||
fail(err, "Failed to resolve symbol %q", s)
|
fail(err, "Failed to resolve symbol %q", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
txt, err := grpcurl.GetDescriptorText(dsc, descSource)
|
fqn := dsc.GetFullyQualifiedName()
|
||||||
if err != nil {
|
var elementType string
|
||||||
fail(err, "Failed to describe symbol %q", s)
|
switch d := dsc.(type) {
|
||||||
}
|
|
||||||
|
|
||||||
switch dsc.(type) {
|
|
||||||
case *desc.MessageDescriptor:
|
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:
|
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:
|
case *desc.OneOfDescriptor:
|
||||||
fmt.Printf("%s is a one-of:\n", dsc.GetFullyQualifiedName())
|
elementType = "a one-of"
|
||||||
case *desc.EnumDescriptor:
|
case *desc.EnumDescriptor:
|
||||||
fmt.Printf("%s is an enum:\n", dsc.GetFullyQualifiedName())
|
elementType = "an enum"
|
||||||
case *desc.EnumValueDescriptor:
|
case *desc.EnumValueDescriptor:
|
||||||
fmt.Printf("%s is an enum value:\n", dsc.GetFullyQualifiedName())
|
elementType = "an enum value"
|
||||||
case *desc.ServiceDescriptor:
|
case *desc.ServiceDescriptor:
|
||||||
fmt.Printf("%s is a service:\n", dsc.GetFullyQualifiedName())
|
elementType = "a service"
|
||||||
case *desc.MethodDescriptor:
|
case *desc.MethodDescriptor:
|
||||||
fmt.Printf("%s is a method:\n", dsc.GetFullyQualifiedName())
|
elementType = "a method"
|
||||||
default:
|
default:
|
||||||
err = fmt.Errorf("descriptor has unrecognized type %T", dsc)
|
err = fmt.Errorf("descriptor has unrecognized type %T", dsc)
|
||||||
fail(err, "Failed to describe symbol %q", s)
|
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)
|
fmt.Println(txt)
|
||||||
|
|
||||||
if dsc, ok := dsc.(*desc.MessageDescriptor); ok && *msgTemplate {
|
if dsc, ok := dsc.(*desc.MessageDescriptor); ok && *msgTemplate {
|
||||||
// for messages, also show a template in JSON, to make it easier to
|
// for messages, also show a template in JSON, to make it easier to
|
||||||
// create a request to invoke an RPC
|
// create a request to invoke an RPC
|
||||||
tmpl := makeTemplate(dynamic.NewMessage(dsc))
|
tmpl := grpcurl.MakeTemplate(dsc)
|
||||||
fmt.Println("\nMessage template:")
|
_, formatter, err := grpcurl.RequestParserAndFormatterFor(grpcurl.Format(*format), descSource, true, false, nil)
|
||||||
jsm := jsonpb.Marshaler{Indent: " ", EmitDefaults: true}
|
if err != nil {
|
||||||
err := jsm.Marshal(os.Stdout, tmpl)
|
fail(err, "Failed to construct formatter for %q", *format)
|
||||||
|
}
|
||||||
|
str, err := formatter(tmpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fail(err, "Failed to print template for message %s", s)
|
fail(err, "Failed to print template for message %s", s)
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println("\nMessage template:")
|
||||||
|
fmt.Println(str)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,31 +485,41 @@ func main() {
|
|||||||
if cc == nil {
|
if cc == nil {
|
||||||
cc = dial()
|
cc = dial()
|
||||||
}
|
}
|
||||||
var dec *json.Decoder
|
var in io.Reader
|
||||||
if *data == "@" {
|
if *data == "@" {
|
||||||
dec = json.NewDecoder(os.Stdin)
|
in = os.Stdin
|
||||||
} else {
|
} else {
|
||||||
dec = json.NewDecoder(strings.NewReader(*data))
|
in = strings.NewReader(*data)
|
||||||
}
|
}
|
||||||
|
|
||||||
h := &handler{dec: dec, descSource: descSource}
|
// if not verbose output, then also include record delimiters
|
||||||
err := grpcurl.InvokeRpc(ctx, descSource, cc, symbol, append(addlHeaders, rpcHeaders...), h, h.getRequestData)
|
// 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 {
|
if err != nil {
|
||||||
fail(err, "Error invoking method %q", symbol)
|
fail(err, "Error invoking method %q", symbol)
|
||||||
}
|
}
|
||||||
reqSuffix := ""
|
reqSuffix := ""
|
||||||
respSuffix := ""
|
respSuffix := ""
|
||||||
if h.reqCount != 1 {
|
reqCount := rf.NumRequests()
|
||||||
|
if reqCount != 1 {
|
||||||
reqSuffix = "s"
|
reqSuffix = "s"
|
||||||
}
|
}
|
||||||
if h.respCount != 1 {
|
if h.NumResponses != 1 {
|
||||||
respSuffix = "s"
|
respSuffix = "s"
|
||||||
}
|
}
|
||||||
if *verbose {
|
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 {
|
if h.Status.Code() != codes.OK {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR:\n Code: %s\n Message: %s\n", h.stat.Code().String(), h.stat.Message())
|
grpcurl.PrintStatus(os.Stderr, h.Status, formatter)
|
||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -457,8 +529,8 @@ func usage() {
|
|||||||
fmt.Fprintf(os.Stderr, `Usage:
|
fmt.Fprintf(os.Stderr, `Usage:
|
||||||
%s [flags] [address] [list|describe] [symbol]
|
%s [flags] [address] [list|describe] [symbol]
|
||||||
|
|
||||||
The 'host:port' is only optional when used with 'list' or 'describe' and a
|
The 'address' is only optional when used with 'list' or 'describe' and a
|
||||||
protoset flag is provided.
|
protoset or proto flag is provided.
|
||||||
|
|
||||||
If 'list' is indicated, the symbol (if present) should be a fully-qualified
|
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
|
service name. If present, all methods of that service are listed. If not
|
||||||
@@ -470,17 +542,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
|
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
|
'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
|
be used to invoke the named method. If no body is given but one is required
|
||||||
the method's request type will be sent.
|
(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
|
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 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
|
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
|
Unix variants, if a -unix=true flag is present, then the address must be the
|
||||||
path to the domain socket.
|
path to the domain socket.
|
||||||
`, os.Args[0])
|
|
||||||
flag.PrintDefaults()
|
|
||||||
|
|
||||||
|
Available flags:
|
||||||
|
`, os.Args[0])
|
||||||
|
flags.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{}) {
|
func warn(msg string, args ...interface{}) {
|
||||||
@@ -503,125 +595,3 @@ func fail(err error, msg string, args ...interface{}) {
|
|||||||
exit(2)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "flag"
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
unix = flag.Bool("unix", false,
|
unix = flags.Bool("unix", false, prettify(`
|
||||||
`Indicates that the server address is the path to a Unix domain socket.`)
|
Indicates that the server address is the path to a Unix domain socket.`))
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|||||||
253
desc_source.go
Normal file
253
desc_source.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
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) {
|
||||||
|
fileNames, err := protoparse.ResolveFilenames(importPaths, fileNames...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := protoparse.Parser{
|
||||||
|
ImportPaths: importPaths,
|
||||||
|
InferImportPaths: len(importPaths) == 0,
|
||||||
|
IncludeSourceCodeInfo: true,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// DescriptorSourceFromFileDescriptors 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
|
||||||
|
}
|
||||||
469
format.go
Normal file
469
format.go
Normal file
@@ -0,0 +1,469 @@
|
|||||||
|
package grpcurl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/jsonpb"
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
"github.com/jhump/protoreflect/desc"
|
||||||
|
"github.com/jhump/protoreflect/dynamic"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
var protoTextMarshaler = proto.TextMarshaler{ExpandAny: true}
|
||||||
|
|
||||||
|
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 := protoTextMarshaler.Marshal(&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")
|
||||||
|
)
|
||||||
|
|
||||||
|
// AnyResolverFromDescriptorSource returns an AnyResolver that will search for
|
||||||
|
// types using the given descriptor source.
|
||||||
|
func AnyResolverFromDescriptorSource(source DescriptorSource) jsonpb.AnyResolver {
|
||||||
|
return &anyResolver{source: source}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnyResolverFromDescriptorSourceWithFallback returns an AnyResolver that will
|
||||||
|
// search for types using the given descriptor source and then fallback to a
|
||||||
|
// special message if the type is not found. The fallback type will render to
|
||||||
|
// JSON with a "@type" property, just like an Any message, but also with a
|
||||||
|
// custom "@value" property that includes the binary encoded payload.
|
||||||
|
func AnyResolverFromDescriptorSourceWithFallback(source DescriptorSource) jsonpb.AnyResolver {
|
||||||
|
res := anyResolver{source: source}
|
||||||
|
return &anyResolverWithFallback{AnyResolver: &res}
|
||||||
|
}
|
||||||
|
|
||||||
|
type anyResolver struct {
|
||||||
|
source DescriptorSource
|
||||||
|
|
||||||
|
er dynamic.ExtensionRegistry
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
mf *dynamic.MessageFactory
|
||||||
|
resolved map[string]func() proto.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *anyResolver) Resolve(typeUrl string) (proto.Message, error) {
|
||||||
|
mname := typeUrl
|
||||||
|
if slash := strings.LastIndex(mname, "/"); slash >= 0 {
|
||||||
|
mname = mname[slash+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.RLock()
|
||||||
|
factory := r.resolved[mname]
|
||||||
|
r.mu.RUnlock()
|
||||||
|
|
||||||
|
// already resolved?
|
||||||
|
if factory != nil {
|
||||||
|
return factory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
// double-check, in case we were racing with another goroutine
|
||||||
|
// that resolved this one
|
||||||
|
factory = r.resolved[mname]
|
||||||
|
if factory != nil {
|
||||||
|
return factory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// use descriptor source to resolve message type
|
||||||
|
d, err := r.source.FindSymbol(mname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
md, ok := d.(*desc.MessageDescriptor)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown message: %s", typeUrl)
|
||||||
|
}
|
||||||
|
// populate any extensions for this message, too
|
||||||
|
if exts, err := r.source.AllExtensionsForType(mname); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if err := r.er.AddExtension(exts...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.mf == nil {
|
||||||
|
r.mf = dynamic.NewMessageFactoryWithExtensionRegistry(&r.er)
|
||||||
|
}
|
||||||
|
|
||||||
|
factory = func() proto.Message {
|
||||||
|
return r.mf.NewMessage(md)
|
||||||
|
}
|
||||||
|
if r.resolved == nil {
|
||||||
|
r.resolved = map[string]func() proto.Message{}
|
||||||
|
}
|
||||||
|
r.resolved[mname] = factory
|
||||||
|
return factory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// anyResolverWithFallback can provide a fallback value for unknown
|
||||||
|
// messages that will format itself to JSON using an "@value" field
|
||||||
|
// that has the base64-encoded data for the unknown message value.
|
||||||
|
type anyResolverWithFallback struct {
|
||||||
|
jsonpb.AnyResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r anyResolverWithFallback) Resolve(typeUrl string) (proto.Message, error) {
|
||||||
|
msg, err := r.AnyResolver.Resolve(typeUrl)
|
||||||
|
if err == nil {
|
||||||
|
return msg, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try "default" resolution logic. This mirrors the default behavior
|
||||||
|
// of jsonpb, which checks to see if the given message name is registered
|
||||||
|
// in the proto package.
|
||||||
|
mname := typeUrl
|
||||||
|
if slash := strings.LastIndex(mname, "/"); slash >= 0 {
|
||||||
|
mname = mname[slash+1:]
|
||||||
|
}
|
||||||
|
mt := proto.MessageType(mname)
|
||||||
|
if mt != nil {
|
||||||
|
return reflect.New(mt.Elem()).Interface().(proto.Message), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, fallback to a special placeholder that can marshal itself
|
||||||
|
// to JSON using a special "@value" property to show base64-encoded
|
||||||
|
// data for the embedded message
|
||||||
|
return &unknownAny{TypeUrl: typeUrl, Error: fmt.Sprintf("%s is not recognized; see @value for raw binary message data", mname)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type unknownAny struct {
|
||||||
|
TypeUrl string `json:"@type"`
|
||||||
|
Error string `json:"@error"`
|
||||||
|
Value string `json:"@value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) MarshalJSONPB(jsm *jsonpb.Marshaler) ([]byte, error) {
|
||||||
|
if jsm.Indent != "" {
|
||||||
|
return json.MarshalIndent(a, "", jsm.Indent)
|
||||||
|
}
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) Unmarshal(b []byte) error {
|
||||||
|
a.Value = base64.StdEncoding.EncodeToString(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) Reset() {
|
||||||
|
a.Value = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) String() string {
|
||||||
|
b, err := a.MarshalJSONPB(&jsonpb.Marshaler{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("ERROR: %v", err.Error())
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *unknownAny) ProtoMessage() {
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ proto.Message = (*unknownAny)(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 := AnyResolverFromDescriptorSource(descSource)
|
||||||
|
return NewJSONRequestParser(in, resolver), NewJSONFormatter(emitJSONDefaultFields, anyResolverWithFallback{AnyResolver: 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintStatus prints details about the given status to the given writer. The given
|
||||||
|
// formatter is used to print any detail messages that may be included in the status.
|
||||||
|
// If the given status has a code of OK, "OK" is printed and that is all. Otherwise,
|
||||||
|
// "ERROR:" is printed along with a line showing the code, one showing the message
|
||||||
|
// string, and each detail message if any are present. The detail messages will be
|
||||||
|
// printed as proto text format or JSON, depending on the given formatter.
|
||||||
|
func PrintStatus(w io.Writer, stat *status.Status, formatter Formatter) {
|
||||||
|
if stat.Code() == codes.OK {
|
||||||
|
fmt.Fprintln(w, "OK")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "ERROR:\n Code: %s\n Message: %s\n", stat.Code().String(), stat.Message())
|
||||||
|
|
||||||
|
statpb := stat.Proto()
|
||||||
|
if len(statpb.Details) > 0 {
|
||||||
|
fmt.Fprintf(w, " Details:\n")
|
||||||
|
for i, det := range statpb.Details {
|
||||||
|
prefix := fmt.Sprintf(" %d)", i+1)
|
||||||
|
fmt.Fprintf(w, "%s\t", prefix)
|
||||||
|
prefix = strings.Repeat(" ", len(prefix)) + "\t"
|
||||||
|
|
||||||
|
output, err := formatter(det)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(w, "Error parsing detail message: %v\n", err)
|
||||||
|
} else {
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if i == 0 {
|
||||||
|
// first line is already indented
|
||||||
|
fmt.Fprintf(w, "%s\n", line)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "%s%s\n", prefix, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)
|
||||||
8
go.mod
8
go.mod
@@ -1,8 +1,8 @@
|
|||||||
module github.com/fullstorydev/grpcurl
|
module github.com/fullstorydev/grpcurl
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/golang/protobuf v1.1.0
|
github.com/golang/protobuf v1.3.1
|
||||||
github.com/jhump/protoreflect v1.0.0
|
github.com/jhump/protoreflect v1.5.0
|
||||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a
|
||||||
google.golang.org/grpc v1.12.0
|
google.golang.org/grpc v1.21.0
|
||||||
)
|
)
|
||||||
|
|||||||
35
go.sum
35
go.sum
@@ -1,13 +1,30 @@
|
|||||||
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/jhump/protoreflect v1.0.0 h1:l94KtQ6gRI3ouKVcXNdofCQJWoHATzcI6tDizOgUaf0=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/jhump/protoreflect v1.0.0/go.mod h1:kG/zRVeS2M91gYaCvvUbPkMjjtFQS4qqjcPFzFkh2zE=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022 h1:MVYFTUmVD3/+ERcvRRI+P/C2+WOUimXh+Pd8LVsklZ4=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/jhump/protoreflect v1.5.0 h1:NgpVT+dX71c8hZnxHof2M7QDK7QtohIJ7DYycjnkyfc=
|
||||||
|
github.com/jhump/protoreflect v1.5.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
google.golang.org/genproto v0.0.0-20170818100345-ee236bd376b0 h1:jgaHBfsPDMBDKsth1hPtI1HcOyecWndWOFSGW21VgaM=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
google.golang.org/genproto v0.0.0-20170818100345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
|
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
|
||||||
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.12.0 h1:Mm8atZtkT+P6R43n/dqNDWkPPu5BwRVu/1rJnJCeZH8=
|
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
||||||
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
842
grpcurl.go
842
grpcurl.go
@@ -13,247 +13,25 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/golang/protobuf/jsonpb"
|
|
||||||
"github.com/golang/protobuf/proto"
|
"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"
|
||||||
"github.com/jhump/protoreflect/desc/protoparse"
|
"github.com/jhump/protoreflect/desc/protoprint"
|
||||||
"github.com/jhump/protoreflect/dynamic"
|
"github.com/jhump/protoreflect/dynamic"
|
||||||
"github.com/jhump/protoreflect/dynamic/grpcdynamic"
|
|
||||||
"github.com/jhump/protoreflect/grpcreflect"
|
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/metadata"
|
"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
|
// ListServices uses the given descriptor source to return a sorted list of fully-qualified
|
||||||
// service names.
|
// service names.
|
||||||
func ListServices(source DescriptorSource) ([]string, error) {
|
func ListServices(source DescriptorSource) ([]string, error) {
|
||||||
@@ -265,6 +43,78 @@ func ListServices(source DescriptorSource) ([]string, error) {
|
|||||||
return svcs, nil
|
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
|
// ListMethods uses the given descriptor source to return a sorted list of method names
|
||||||
// for the specified fully-qualified service name.
|
// for the specified fully-qualified service name.
|
||||||
func ListMethods(source DescriptorSource, serviceName string) ([]string, error) {
|
func ListMethods(source DescriptorSource, serviceName string) ([]string, error) {
|
||||||
@@ -277,365 +127,13 @@ func ListMethods(source DescriptorSource, serviceName string) ([]string, error)
|
|||||||
} else {
|
} else {
|
||||||
methods := make([]string, 0, len(sd.GetMethods()))
|
methods := make([]string, 0, len(sd.GetMethods()))
|
||||||
for _, method := range sd.GetMethods() {
|
for _, method := range sd.GetMethods() {
|
||||||
methods = append(methods, method.GetName())
|
methods = append(methods, method.GetFullyQualifiedName())
|
||||||
}
|
}
|
||||||
sort.Strings(methods)
|
sort.Strings(methods)
|
||||||
return methods, nil
|
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
|
// MetadataFromHeaders converts a list of header strings (each string in
|
||||||
// "Header-Name: Header-Value" form) into metadata. If a string has a header
|
// "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
|
// name without a value (e.g. does not contain a colon), the value is assumed
|
||||||
@@ -683,42 +181,61 @@ func decode(val string) (string, error) {
|
|||||||
return "", firstErr
|
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
|
// MetadataToString returns a string representation of the given metadata, for
|
||||||
// displaying to users.
|
// displaying to users.
|
||||||
func MetadataToString(md metadata.MD) string {
|
func MetadataToString(md metadata.MD) string {
|
||||||
if len(md) == 0 {
|
if len(md) == 0 {
|
||||||
return "(empty)"
|
return "(empty)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(md))
|
||||||
|
for k := range md {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
for k, vs := range md {
|
first := true
|
||||||
|
for _, k := range keys {
|
||||||
|
vs := md[k]
|
||||||
for _, v := range vs {
|
for _, v := range vs {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
b.WriteString(k)
|
b.WriteString(k)
|
||||||
b.WriteString(": ")
|
b.WriteString(": ")
|
||||||
if strings.HasSuffix(k, "-bin") {
|
if strings.HasSuffix(k, "-bin") {
|
||||||
v = base64.StdEncoding.EncodeToString([]byte(v))
|
v = base64.StdEncoding.EncodeToString([]byte(v))
|
||||||
}
|
}
|
||||||
b.WriteString(v)
|
b.WriteString(v)
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return b.String()
|
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.
|
// GetDescriptorText returns a string representation of the given descriptor.
|
||||||
func GetDescriptorText(dsc desc.Descriptor, descSource DescriptorSource) (string, error) {
|
// This returns a snippet of proto source that describes the given element.
|
||||||
dscProto := EnsureExtensions(descSource, dsc.AsProto())
|
func GetDescriptorText(dsc desc.Descriptor, _ DescriptorSource) (string, error) {
|
||||||
return (&jsonpb.Marshaler{Indent: " "}).MarshalToString(dscProto)
|
// 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
|
// EnsureExtensions uses the given descriptor source to download extensions for
|
||||||
@@ -840,7 +357,124 @@ func fullyConvertToDynamic(msgFact *dynamic.MessageFactory, msg proto.Message) (
|
|||||||
return dm, nil
|
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
|
// 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
|
// 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.
|
// certificate. If clientCertFile is not blank then clientKeyFile must not be blank.
|
||||||
@@ -877,13 +511,16 @@ func ClientTransportCredentials(insecureSkipVerify bool, cacertFile, clientCertF
|
|||||||
return credentials.NewTLS(&tlsConf), nil
|
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
|
// given properties. If cacertFile is blank, the server will not request client certs
|
||||||
// unless requireClientCerts is true. When requireClientCerts is false and cacertFile is
|
// 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
|
// 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.
|
// client certs. The serverCertFile and serverKeyFile must both not be blank.
|
||||||
func ServerTransportCredentials(cacertFile, serverCertFile, serverKeyFile string, requireClientCerts bool) (credentials.TransportCredentials, error) {
|
func ServerTransportCredentials(cacertFile, serverCertFile, serverKeyFile string, requireClientCerts bool) (credentials.TransportCredentials, error) {
|
||||||
var tlsConf tls.Config
|
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
|
// Load the server certificates from disk
|
||||||
certificate, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile)
|
certificate, err := tls.LoadX509KeyPair(serverCertFile, serverKeyFile)
|
||||||
@@ -936,11 +573,8 @@ func BlockingDial(ctx context.Context, network, address string, creds credential
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialer := func(address string, timeout time.Duration) (net.Conn, error) {
|
dialer := func(ctx context.Context, address string) (net.Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
conn, err := (&net.Dialer{}).DialContext(ctx, network, address)
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
conn, err := (&net.Dialer{Cancel: ctx.Done()}).Dial(network, address)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeResult(err)
|
writeResult(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -963,7 +597,7 @@ func BlockingDial(ctx context.Context, network, address string, creds credential
|
|||||||
opts = append(opts,
|
opts = append(opts,
|
||||||
grpc.WithBlock(),
|
grpc.WithBlock(),
|
||||||
grpc.FailOnNonTempDialError(true),
|
grpc.FailOnNonTempDialError(true),
|
||||||
grpc.WithDialer(dialer),
|
grpc.WithContextDialer(dialer),
|
||||||
grpc.WithInsecure(), // we are handling TLS, so tell grpc not to
|
grpc.WithInsecure(), // we are handling TLS, so tell grpc not to
|
||||||
)
|
)
|
||||||
conn, err := grpc.DialContext(ctx, address, opts...)
|
conn, err := grpc.DialContext(ctx, address, opts...)
|
||||||
|
|||||||
134
grpcurl_test.go
134
grpcurl_test.go
@@ -1,6 +1,7 @@
|
|||||||
package grpcurl_test
|
package grpcurl_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang/protobuf/jsonpb"
|
"github.com/golang/protobuf/jsonpb"
|
||||||
|
jsonpbtest "github.com/golang/protobuf/jsonpb/jsonpb_test_proto"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
"github.com/jhump/protoreflect/desc"
|
"github.com/jhump/protoreflect/desc"
|
||||||
"github.com/jhump/protoreflect/grpcreflect"
|
"github.com/jhump/protoreflect/grpcreflect"
|
||||||
@@ -44,6 +46,10 @@ type descSourceCase struct {
|
|||||||
includeRefl bool
|
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) {
|
func TestMain(m *testing.M) {
|
||||||
var err error
|
var err error
|
||||||
sourceProtoset, err = DescriptorSourceFromProtoSets("testing/test.protoset")
|
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)
|
t.Fatalf("failed to list methods for TestService: %v", err)
|
||||||
}
|
}
|
||||||
expected := []string{
|
expected := []string{
|
||||||
"EmptyCall",
|
"grpc.testing.TestService.EmptyCall",
|
||||||
"FullDuplexCall",
|
"grpc.testing.TestService.FullDuplexCall",
|
||||||
"HalfDuplexCall",
|
"grpc.testing.TestService.HalfDuplexCall",
|
||||||
"StreamingInputCall",
|
"grpc.testing.TestService.StreamingInputCall",
|
||||||
"StreamingOutputCall",
|
"grpc.testing.TestService.StreamingOutputCall",
|
||||||
"UnaryCall",
|
"grpc.testing.TestService.UnaryCall",
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(expected, names) {
|
if !reflect.DeepEqual(expected, names) {
|
||||||
t.Errorf("ListMethods returned wrong results: wanted %v, got %v", 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 {
|
if err != nil {
|
||||||
t.Fatalf("failed to list methods for ServerReflection: %v", err)
|
t.Fatalf("failed to list methods for ServerReflection: %v", err)
|
||||||
}
|
}
|
||||||
expected = []string{"ServerReflectionInfo"}
|
expected = []string{"grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo"}
|
||||||
} else {
|
} else {
|
||||||
// without reflection, we see all services defined in the same test.proto file, which is the
|
// without reflection, we see all services defined in the same test.proto file, which is the
|
||||||
// TestService as well as UnimplementedService
|
// TestService as well as UnimplementedService
|
||||||
@@ -222,7 +228,7 @@ func doTestListMethods(t *testing.T, source DescriptorSource, includeReflection
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to list methods for ServerReflection: %v", err)
|
t.Fatalf("failed to list methods for ServerReflection: %v", err)
|
||||||
}
|
}
|
||||||
expected = []string{"UnimplementedCall"}
|
expected = []string{"grpc.testing.UnimplementedService.UnimplementedCall"}
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(expected, names) {
|
if !reflect.DeepEqual(expected, names) {
|
||||||
t.Errorf("ListMethods returned wrong results: wanted %v, got %v", 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) {
|
func TestDescribe(t *testing.T) {
|
||||||
for _, ds := range descSources {
|
for _, ds := range descSources {
|
||||||
t.Run(ds.name, func(t *testing.T) {
|
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)
|
||||||
|
cancel()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
# Run this script to generate files used by tests.
|
||||||
|
|
||||||
echo "Creating protosets..."
|
echo "Creating protosets..."
|
||||||
protoc ../../../google.golang.org/grpc/interop/grpc_testing/test.proto \
|
protoc testing/test.proto \
|
||||||
-I../../../ --include_imports \
|
--include_imports \
|
||||||
--descriptor_set_out=testing/test.protoset
|
--descriptor_set_out=testing/test.protoset
|
||||||
|
|
||||||
protoc testing/example.proto \
|
protoc testing/example.proto \
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func (a *accounts) openAccount(customer string, accountType Account_Type, initia
|
|||||||
a.AccountNumbersByCustomer[customer] = accountNums
|
a.AccountNumbersByCustomer[customer] = accountNums
|
||||||
var acct account
|
var acct account
|
||||||
acct.AccountNumber = num
|
acct.AccountNumber = num
|
||||||
|
acct.Type = accountType
|
||||||
acct.BalanceCents = initialBalanceCents
|
acct.BalanceCents = initialBalanceCents
|
||||||
acct.Transactions = append(acct.Transactions, &Transaction{
|
acct.Transactions = append(acct.Transactions, &Transaction{
|
||||||
AccountNumber: num,
|
AccountNumber: num,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func unaryLogger(ctx context.Context, req interface{}, info *grpc.UnaryServerInf
|
|||||||
} else {
|
} else {
|
||||||
code = codes.Unknown
|
code = codes.Unknown
|
||||||
}
|
}
|
||||||
grpclog.Infof("completed <%d>: %v (%d) %v\n", i, code, code, time.Now().Sub(start))
|
grpclog.Infof("completed <%d>: %v (%d) %v\n", i, code, code, time.Since(start))
|
||||||
return rsp, err
|
return rsp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ func streamLogger(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServer
|
|||||||
} else {
|
} else {
|
||||||
code = codes.Unknown
|
code = codes.Unknown
|
||||||
}
|
}
|
||||||
grpclog.Infof("completed <%d>: %v(%d) %v\n", i, code, code, time.Now().Sub(start))
|
grpclog.Infof("completed <%d>: %v(%d) %v\n", i, code, code, time.Since(start))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ syntax = "proto3";
|
|||||||
import "google/protobuf/descriptor.proto";
|
import "google/protobuf/descriptor.proto";
|
||||||
import "google/protobuf/empty.proto";
|
import "google/protobuf/empty.proto";
|
||||||
import "google/protobuf/timestamp.proto";
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "testing/example2.proto";
|
||||||
|
|
||||||
message TestRequest {
|
message TestRequest {
|
||||||
repeated string file_names = 1;
|
repeated string file_names = 1;
|
||||||
|
repeated Extension extensions = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TestResponse {
|
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.
@@ -109,24 +109,42 @@ func TestBrokenTLS_ClientPlainText(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// client connection (usually) succeeds since client is not waiting for TLS handshake
|
// client connection (usually) succeeds since client is not waiting for TLS handshake
|
||||||
e, err := createTestServerAndClient(serverCreds, nil)
|
// (we try several times, but if we never get a connection and the error message is
|
||||||
if err != nil {
|
// a known/expected possibility, we'll just bail)
|
||||||
if strings.Contains(err.Error(), "deadline exceeded") {
|
var e testEnv
|
||||||
// It is possible that connection never becomes healthy:
|
failCount := 0
|
||||||
|
for {
|
||||||
|
e, err = createTestServerAndClient(serverCreds, nil)
|
||||||
|
if err == nil {
|
||||||
|
// success!
|
||||||
|
defer e.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), "deadline exceeded") ||
|
||||||
|
strings.Contains(err.Error(), "use of closed network connection") {
|
||||||
|
// It is possible that the connection never becomes healthy:
|
||||||
// 1) grpc connects successfully
|
// 1) grpc connects successfully
|
||||||
// 2) grpc client tries to send HTTP/2 preface and settings frame
|
// 2) grpc client tries to send HTTP/2 preface and settings frame
|
||||||
// 3) server, expecting handshake, closes the connection
|
// 3) server, expecting handshake, closes the connection
|
||||||
// 4) in the client, the write fails, so the connection never
|
// 4) in the client, the write fails, so the connection never
|
||||||
// becomes ready
|
// becomes ready
|
||||||
// More often than not, the connection becomes ready (presumably
|
// The client will attempt to reconnect on transient errors, so
|
||||||
// the write to the socket succeeds before the server closes the
|
// may eventually bump into the connect time limit. This used to
|
||||||
// connection). But when it does not, it is possible to observe
|
// result in a "deadline exceeded" error, but more recent versions
|
||||||
// timeouts when setting up the connection.
|
// of the grpc library report any underlying I/O error instead, so
|
||||||
return
|
// we also check for "use of closed network connection".
|
||||||
|
failCount++
|
||||||
|
if failCount > 5 {
|
||||||
|
return // bail...
|
||||||
|
}
|
||||||
|
// we'll try again
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// some other error occurred, so we'll consider that a test failure
|
||||||
|
t.Fatalf("failed to setup server and client: %v", err)
|
||||||
}
|
}
|
||||||
t.Fatalf("failed to setup server and client: %v", err)
|
|
||||||
}
|
}
|
||||||
defer e.Close()
|
|
||||||
|
|
||||||
// but request fails because server closes connection upon seeing request
|
// but request fails because server closes connection upon seeing request
|
||||||
// bytes that are not a TLS handshake
|
// bytes that are not a TLS handshake
|
||||||
@@ -285,7 +303,7 @@ func simpleTest(t *testing.T, cc *grpc.ClientConn) {
|
|||||||
cl := grpc_testing.NewTestServiceClient(cc)
|
cl := grpc_testing.NewTestServiceClient(cc)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, err := cl.UnaryCall(ctx, &grpc_testing.SimpleRequest{}, grpc.FailFast(false))
|
_, err := cl.UnaryCall(ctx, &grpc_testing.SimpleRequest{}, grpc.WaitForReady(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("simple RPC failed: %v", err)
|
t.Errorf("simple RPC failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user