Skip to content

Commit ccc9007

Browse files
authored
add -protoset-out option (#120)
1 parent 9248ea0 commit ccc9007

File tree

3 files changed

+149
-7
lines changed

3 files changed

+149
-7
lines changed

cmd/grpcurl/grpcurl.go

+36-7
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,14 @@ var (
5959
rpcHeaders multiString
6060
reflHeaders multiString
6161
expandHeaders = flags.Bool("expand-headers", false, prettify(`
62-
If set, headers may use '${NAME}' syntax to reference environment variables.
63-
These will be expanded to the actual environment variable value before
64-
sending to the server. For example, if there is an environment variable
65-
defined like FOO=bar, then a header of 'key: ${FOO}' would expand to 'key: bar'.
66-
This applies to -H, -rpc-header, and -reflect-header options. No other
67-
expansion/escaping is performed. This can be used to supply
68-
credentials/secrets without having to put them in command-line arguments.`))
62+
If set, headers may use '${NAME}' syntax to reference environment
63+
variables. These will be expanded to the actual environment variable
64+
value before sending to the server. For example, if there is an
65+
environment variable defined like FOO=bar, then a header of
66+
'key: ${FOO}' would expand to 'key: bar'. This applies to -H,
67+
-rpc-header, and -reflect-header options. No other expansion/escaping is
68+
performed. This can be used to supply credentials/secrets without having
69+
to put them in command-line arguments.`))
6970
authority = flags.String("authority", "", prettify(`
7071
Value of :authority pseudo-header to be use with underlying HTTP/2
7172
requests. It defaults to the given address.`))
@@ -101,6 +102,13 @@ var (
101102
will accept. If not specified, defaults to 4,194,304 (4 megabytes).`))
102103
emitDefaults = flags.Bool("emit-defaults", false, prettify(`
103104
Emit default values for JSON-encoded responses.`))
105+
protosetOut = flags.String("protoset-out", "", prettify(`
106+
The name of a file to be written that will contain a FileDescriptorSet
107+
proto. With the list and describe verbs, the listed or described
108+
elements and their transitive dependencies will be written to the named
109+
file if this option is given. When invoking an RPC and this option is
110+
given, the method being invoked and its transitive dependencies will be
111+
included in the output file.`))
104112
msgTemplate = flags.Bool("msg-template", false, prettify(`
105113
When describing messages, show a template of input data.`))
106114
verbose = flags.Bool("v", false, prettify(`
@@ -391,6 +399,9 @@ func main() {
391399
fmt.Printf("%s\n", svc)
392400
}
393401
}
402+
if err := writeProtoset(descSource, svcs...); err != nil {
403+
fail(err, "Failed to write protoset to %s", *protosetOut)
404+
}
394405
} else {
395406
methods, err := grpcurl.ListMethods(descSource, symbol)
396407
if err != nil {
@@ -403,6 +414,9 @@ func main() {
403414
fmt.Printf("%s\n", m)
404415
}
405416
}
417+
if err := writeProtoset(descSource, symbol); err != nil {
418+
fail(err, "Failed to write protoset to %s", *protosetOut)
419+
}
406420
}
407421

408422
} else if describe {
@@ -503,6 +517,9 @@ func main() {
503517
fmt.Println(str)
504518
}
505519
}
520+
if err := writeProtoset(descSource, symbols...); err != nil {
521+
fail(err, "Failed to write protoset to %s", *protosetOut)
522+
}
506523

507524
} else {
508525
// Invoke an RPC
@@ -619,3 +636,15 @@ func fail(err error, msg string, args ...interface{}) {
619636
exit(2)
620637
}
621638
}
639+
640+
func writeProtoset(descSource grpcurl.DescriptorSource, symbols ...string) error {
641+
if *protosetOut == "" {
642+
return nil
643+
}
644+
f, err := os.Create(*protosetOut)
645+
if err != nil {
646+
return err
647+
}
648+
defer f.Close()
649+
return grpcurl.WriteProtoset(f, descSource, symbols...)
650+
}

desc_source.go

+51
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package grpcurl
33
import (
44
"errors"
55
"fmt"
6+
"io"
67
"io/ioutil"
78
"sync"
89

@@ -251,3 +252,53 @@ func reflectionSupport(err error) error {
251252
}
252253
return err
253254
}
255+
256+
// WriteProtoset will use the given descriptor source to resolve all of the given
257+
// symbols and write a proto file descriptor set with their definitions to the
258+
// given output. The output will include descriptors for all files in which the
259+
// symbols are defined as well as their transitive dependencies.
260+
func WriteProtoset(out io.Writer, descSource DescriptorSource, symbols ...string) error {
261+
// compute set of file descriptors
262+
filenames := make([]string, 0, len(symbols))
263+
fds := make(map[string]*desc.FileDescriptor, len(symbols))
264+
for _, sym := range symbols {
265+
d, err := descSource.FindSymbol(sym)
266+
if err != nil {
267+
return fmt.Errorf("failed to find descriptor for %q: %v", sym, err)
268+
}
269+
fd := d.GetFile()
270+
if _, ok := fds[fd.GetName()]; !ok {
271+
fds[fd.GetName()] = fd
272+
filenames = append(filenames, fd.GetName())
273+
}
274+
}
275+
// now expand that to include transitive dependencies in topologically sorted
276+
// order (such that file always appears after its dependencies)
277+
expandedFiles := make(map[string]struct{}, len(fds))
278+
allFilesSlice := make([]*descpb.FileDescriptorProto, 0, len(fds))
279+
for _, filename := range filenames {
280+
allFilesSlice = addFilesToSet(allFilesSlice, expandedFiles, fds[filename])
281+
}
282+
// now we can serialize to file
283+
b, err := proto.Marshal(&descpb.FileDescriptorSet{File: allFilesSlice})
284+
if err != nil {
285+
return fmt.Errorf("failed to serialize file descriptor set: %v", err)
286+
}
287+
if _, err := out.Write(b); err != nil {
288+
return fmt.Errorf("failed to write file descriptor set: %v", err)
289+
}
290+
return nil
291+
}
292+
293+
func addFilesToSet(allFiles []*descpb.FileDescriptorProto, expanded map[string]struct{}, fd *desc.FileDescriptor) []*descpb.FileDescriptorProto {
294+
if _, ok := expanded[fd.GetName()]; ok {
295+
// already seen this one
296+
return allFiles
297+
}
298+
expanded[fd.GetName()] = struct{}{}
299+
// add all dependencies first
300+
for _, dep := range fd.GetDependencies() {
301+
allFiles = addFilesToSet(allFiles, expanded, dep)
302+
}
303+
return append(allFiles, fd.AsFileDescriptorProto())
304+
}

desc_source_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package grpcurl
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"testing"
7+
8+
"github.com/golang/protobuf/proto"
9+
"github.com/golang/protobuf/protoc-gen-go/descriptor"
10+
)
11+
12+
func TestWriteProtoset(t *testing.T) {
13+
exampleProtoset, err := loadProtoset("./testing/example.protoset")
14+
if err != nil {
15+
t.Fatalf("failed to load example.protoset: %v", err)
16+
}
17+
testProtoset, err := loadProtoset("./testing/test.protoset")
18+
if err != nil {
19+
t.Fatalf("failed to load test.protoset: %v", err)
20+
}
21+
22+
mergedProtoset := &descriptor.FileDescriptorSet{
23+
File: append(exampleProtoset.File, testProtoset.File...),
24+
}
25+
26+
descSrc, err := DescriptorSourceFromFileDescriptorSet(mergedProtoset)
27+
if err != nil {
28+
t.Fatalf("failed to create descriptor source: %v", err)
29+
}
30+
31+
checkWriteProtoset(t, descSrc, exampleProtoset, "TestService")
32+
checkWriteProtoset(t, descSrc, testProtoset, "grpc.testing.TestService")
33+
checkWriteProtoset(t, descSrc, mergedProtoset, "TestService", "grpc.testing.TestService")
34+
}
35+
36+
func loadProtoset(path string) (*descriptor.FileDescriptorSet, error) {
37+
b, err := ioutil.ReadFile(path)
38+
if err != nil {
39+
return nil, err
40+
}
41+
var protoset descriptor.FileDescriptorSet
42+
if err := proto.Unmarshal(b, &protoset); err != nil {
43+
return nil, err
44+
}
45+
return &protoset, nil
46+
}
47+
48+
func checkWriteProtoset(t *testing.T, descSrc DescriptorSource, protoset *descriptor.FileDescriptorSet, symbols ...string) {
49+
var buf bytes.Buffer
50+
if err := WriteProtoset(&buf, descSrc, symbols...); err != nil {
51+
t.Fatalf("failed to write protoset: %v", err)
52+
}
53+
54+
var result descriptor.FileDescriptorSet
55+
if err := proto.Unmarshal(buf.Bytes(), &result); err != nil {
56+
t.Fatalf("failed to unmarshal written protoset: %v", err)
57+
}
58+
59+
if !proto.Equal(protoset, &result) {
60+
t.Fatalf("written protoset not equal to input:\nExpecting: %s\nActual: %s", protoset, &result)
61+
}
62+
}

0 commit comments

Comments
 (0)