├── bin
├── hermit.hcl
├── go
├── gofmt
├── .go-1.19.3.pkg
├── .golangci-lint-1.50.1.pkg
├── golangci-lint
├── README.hermit.md
├── activate-hermit
└── hermit
├── .gitignore
├── examplectl.gif
├── generate.go
├── keys.go
├── doc.go
├── internal
├── reflection
│ ├── buf.yaml
│ ├── buf.gen.yaml
│ ├── gen
│ │ └── go
│ │ │ ├── v1
│ │ │ └── grpc_reflection_v1alphaconnect
│ │ │ │ └── reflection.connect.go
│ │ │ └── v1alpha1
│ │ │ └── grpc_reflection_v1alphaconnect
│ │ │ └── reflection.connect.go
│ ├── v1alpha1
│ │ └── reflection.proto
│ └── v1
│ │ └── reflection.proto
├── testing
│ ├── generate.go
│ ├── cmd
│ │ └── example
│ │ │ └── main.go
│ ├── proto
│ │ ├── api.proto
│ │ └── examplepb
│ │ │ ├── api_grpc.pb.go
│ │ │ └── api.pb.go
│ └── pkg
│ │ └── example
│ │ └── server.go
├── descriptors
│ ├── descriptors.go
│ ├── type.go
│ └── proto.go
└── grpc
│ ├── grpc.go
│ └── connect.go
├── CONTRIBUTING.md
├── cmd
├── grpctl
│ ├── main.go
│ └── docs
│ │ ├── main.go
│ │ ├── grpctl.md
│ │ └── go.mod
└── billingctl
│ └── main.go
├── util.go
├── reflection.go
├── .github
└── workflows
│ └── ci.yaml
├── .golangci.yaml
├── go.mod
├── design
├── 2-reflection.md
├── 1-grpctl.md
└── 3-customizabiliity.md
├── config.go
├── grpc.go
├── README.md
├── go.sum
├── opts.go
├── command.go
├── LICENSE
├── command_test.go
└── grpctl.svg
/bin/hermit.hcl:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/bin/go:
--------------------------------------------------------------------------------
1 | .go-1.19.3.pkg
--------------------------------------------------------------------------------
/bin/gofmt:
--------------------------------------------------------------------------------
1 | .go-1.19.3.pkg
--------------------------------------------------------------------------------
/bin/.go-1.19.3.pkg:
--------------------------------------------------------------------------------
1 | hermit
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .hermit/go/bin/*
2 |
--------------------------------------------------------------------------------
/bin/.golangci-lint-1.50.1.pkg:
--------------------------------------------------------------------------------
1 | hermit
--------------------------------------------------------------------------------
/bin/golangci-lint:
--------------------------------------------------------------------------------
1 | .golangci-lint-1.50.1.pkg
--------------------------------------------------------------------------------
/examplectl.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joshcarp/grpctl/HEAD/examplectl.gif
--------------------------------------------------------------------------------
/generate.go:
--------------------------------------------------------------------------------
1 | //go:generate cd cmd/grpctl/docs && go generate
2 | package grpctl
3 |
--------------------------------------------------------------------------------
/keys.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | type (
4 | methodDescriptorKey struct{}
5 | )
6 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package grpctl provides to build cobra commands from proto descriptors.
2 | package grpctl
3 |
--------------------------------------------------------------------------------
/internal/reflection/buf.yaml:
--------------------------------------------------------------------------------
1 | version: v1
2 | breaking:
3 | use:
4 | - FILE
5 | lint:
6 | use:
7 | - DEFAULT
8 |
--------------------------------------------------------------------------------
/internal/reflection/buf.gen.yaml:
--------------------------------------------------------------------------------
1 | version: v1
2 | managed:
3 | enabled: false
4 |
5 | plugins:
6 | - name: connect-go
7 | out: gen/go
8 | opt: paths=source_relative
--------------------------------------------------------------------------------
/internal/testing/generate.go:
--------------------------------------------------------------------------------
1 | package testing
2 |
3 | //go:generate protoc -I proto --go_out=paths=source_relative:proto/examplepb --go-grpc_out=paths=source_relative:proto/examplepb proto/api.proto
4 |
--------------------------------------------------------------------------------
/bin/README.hermit.md:
--------------------------------------------------------------------------------
1 | # Hermit environment
2 |
3 | This is a [Hermit](https://github.com/cashapp/hermit) bin directory.
4 |
5 | The symlinks in this directory are managed by Hermit and will automatically
6 | download and install Hermit itself as well as packages. These packages are
7 | local to this environment.
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | I should probably add more information here
4 |
5 | ## Prerequisites
6 | - [hermit](https://github.com/cashapp/hermit) is used for tool version management.
7 | - [embedmd](https://github.com/campoy/embedmd) this isn't on hermit yet, but when it is it won't need to be installed separately.
8 |
--------------------------------------------------------------------------------
/cmd/grpctl/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 |
7 | "github.com/joshcarp/grpctl"
8 | )
9 |
10 | func main() {
11 | cmd, err := grpctl.ReflectionCommand()
12 | if err != nil {
13 | log.Fatal(err)
14 | }
15 | if err := cmd.ExecuteContext(context.Background()); err != nil {
16 | log.Fatal(err)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/cmd/grpctl/docs/main.go:
--------------------------------------------------------------------------------
1 | //go:generate go run .
2 | package main
3 |
4 | import (
5 | "log"
6 |
7 | "github.com/joshcarp/grpctl"
8 |
9 | "github.com/spf13/cobra/doc"
10 | )
11 |
12 | func main() {
13 | cmd, err := grpctl.ReflectionCommand()
14 | if err != nil {
15 | log.Fatal(err)
16 | }
17 | err = doc.GenMarkdownTree(cmd, ".")
18 | if err != nil {
19 | log.Fatal(err)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | func recusiveParentPreRun(cmd *cobra.Command, args []string) error {
8 | for cmd != nil {
9 | this := cmd
10 | if cmd.PersistentPreRunE != nil {
11 | err := cmd.PersistentPreRunE(this, args)
12 | if err != nil {
13 | return err
14 | }
15 | }
16 | if !this.HasParent() {
17 | break
18 | }
19 | cmd = this.Parent()
20 | }
21 | return nil
22 | }
23 |
--------------------------------------------------------------------------------
/cmd/grpctl/docs/grpctl.md:
--------------------------------------------------------------------------------
1 | ## grpctl
2 |
3 | an intuitive grpc cli
4 |
5 | ### Options
6 |
7 | ```
8 | -a, --address string Address in form 'host:port'
9 | --config string Config file (default is $HOME/.grpctl.yaml)
10 | -H, --header stringArray Header in form 'key: value'
11 | -h, --help help for grpctl
12 | -p, --plaintext Dial grpc.WithInsecure
13 | ```
14 |
15 | ###### Auto generated by spf13/cobra on 10-Dec-2021
16 |
--------------------------------------------------------------------------------
/reflection.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/spf13/cobra"
7 | )
8 |
9 | // ReflectionCommand returns the grpctl command that is used in the grpctl binary.
10 | func ReflectionCommand() (*cobra.Command, error) {
11 | cmd := &cobra.Command{
12 | Use: "grpctl",
13 | Short: "an intuitive grpc cli",
14 | }
15 | err := BuildCommand(cmd, WithArgs(os.Args), WithReflection(os.Args), WithCompletion())
16 | if err != nil {
17 | return nil, err
18 | }
19 | return cmd, nil
20 | }
21 |
--------------------------------------------------------------------------------
/internal/testing/cmd/example/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 |
7 | "github.com/joshcarp/grpctl/internal/testing/pkg/example"
8 | "github.com/joshcarp/grpctl/internal/testing/proto/examplepb"
9 | "google.golang.org/grpc"
10 | "google.golang.org/grpc/reflection"
11 | )
12 |
13 | func main() {
14 | ln, err := net.Listen("tcp", "127.0.0.1:8081")
15 | if err != nil {
16 | log.Fatal(err)
17 | }
18 | srv := grpc.NewServer()
19 | examplepb.RegisterFooAPIServer(srv, example.FooServer{})
20 | examplepb.RegisterBarAPIServer(srv, example.BarServer{})
21 | reflection.Register(srv)
22 | err = srv.Serve(ln)
23 | if err != nil {
24 | log.Fatal(err)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/bin/activate-hermit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # This file must be used with "source bin/activate-hermit" from bash or zsh.
3 | # You cannot run it directly
4 | #
5 | # THIS FILE IS GENERATED; DO NOT MODIFY
6 |
7 | if [ "${BASH_SOURCE-}" = "$0" ]; then
8 | echo "You must source this script: \$ source $0" >&2
9 | exit 33
10 | fi
11 |
12 | BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
13 | if "${BIN_DIR}/hermit" noop > /dev/null; then
14 | eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"
15 |
16 | if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
17 | hash -r 2>/dev/null
18 | fi
19 |
20 | echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
21 | fi
22 |
--------------------------------------------------------------------------------
/internal/testing/proto/api.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | option go_package = "github.com/joshcarp/grpctl/internal/testing/proto/examplepb;examplepb";
4 | option java_multiple_files = true;
5 | option java_package = "com.joshcarp.example";
6 | option java_outer_classname = "example";
7 |
8 | package example;
9 |
10 | service FooAPI {
11 | rpc Hello(exampleRequest) returns (exampleResponse);
12 | }
13 |
14 | service BarAPI {
15 | rpc ListBars(BarRequest) returns (BarResponse);
16 | }
17 |
18 | message BarRequest {
19 | string message = 1;
20 | repeated string foos = 2;
21 | }
22 |
23 | message BarResponse {
24 | string message = 1;
25 | }
26 |
27 | message exampleRequest {
28 | string message = 1;
29 | }
30 |
31 | message exampleResponse {
32 | string message = 1;
33 | }
34 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | # Adapted from https://github.com/cashapp/hermit/blob/196cebb641d93a47e1f60b07f1b386e882c9b40d/.github/workflows/ci.yml#L1
2 | on:
3 | push:
4 | paths-ignore:
5 | - 'docs/**'
6 | branches:
7 | pull_request:
8 | paths-ignore:
9 | - 'docs/**'
10 | name: CI
11 | jobs:
12 | test:
13 | name: Test
14 | runs-on: ubuntu-20.04
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v2
18 | - name: Init Hermit
19 | run: ./bin/hermit env -r >> $GITHUB_ENV
20 | - name: Test
21 | run: go test ./...
22 | lint:
23 | name: Lint
24 | runs-on: ubuntu-20.04
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v2
28 | - name: Init Hermit
29 | run: ./bin/hermit env -r >> $GITHUB_ENV
30 | - name: golangci-lint
31 | run: golangci-lint run
32 |
--------------------------------------------------------------------------------
/cmd/billingctl/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "log"
6 | "os"
7 |
8 | billing "cloud.google.com/go/billing/apiv1/billingpb"
9 | "github.com/joshcarp/grpctl"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | // Example call:
14 | // billingctl -H="Authorization: Bearer $(gcloud auth application-default print-access-token)" CloudBilling ListBillingAccounts.
15 | func main() {
16 | cmd := &cobra.Command{
17 | Use: "billingctl",
18 | Short: "an example cli tool for the gcp billing api",
19 | }
20 | err := grpctl.BuildCommand(cmd,
21 | grpctl.WithArgs(os.Args),
22 | grpctl.WithFileDescriptors(
23 | billing.File_google_cloud_billing_v1_cloud_billing_proto,
24 | billing.File_google_cloud_billing_v1_cloud_catalog_proto,
25 | ),
26 | )
27 | if err != nil {
28 | log.Print(err)
29 | }
30 | if err := cmd.ExecuteContext(context.Background()); err != nil {
31 | log.Print(err)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | run:
2 | timeout: 10m
3 | linters:
4 | disable-all: true
5 | enable:
6 | - bodyclose
7 | - deadcode
8 | - depguard
9 | - dogsled
10 | - dupl
11 | - errcheck
12 | - gochecknoinits
13 | - goconst
14 | - gocritic
15 | - gocyclo
16 | - gofmt
17 | - goimports
18 | - gosec
19 | - gosimple
20 | - govet
21 | - ineffassign
22 | - lll
23 | - misspell
24 | - nakedret
25 | - exportloopref
26 | - staticcheck
27 | - structcheck
28 | - stylecheck
29 | - typecheck
30 | - unconvert
31 | - unused
32 | - varcheck
33 | - whitespace
34 | - gofumpt
35 | - gochecknoglobals
36 | - gochecknoinits
37 | - errname
38 | - forbidigo
39 | - forcetypeassert
40 | - paralleltest
41 | - wastedassign
42 | - unparam
43 | - godot
44 | linters-settings:
45 | lll:
46 | line-length: 180
47 | errcheck:
48 | check-blank: true
49 |
--------------------------------------------------------------------------------
/internal/descriptors/descriptors.go:
--------------------------------------------------------------------------------
1 | package descriptors
2 |
3 | import (
4 | "fmt"
5 |
6 | "google.golang.org/protobuf/reflect/protoreflect"
7 | )
8 |
9 | func FullMethod(c protoreflect.MethodDescriptor) string {
10 | return fmt.Sprintf("/%s/%s", c.Parent().FullName(), c.Name())
11 | }
12 |
13 | func Command(descriptor protoreflect.Descriptor) string {
14 | return string(descriptor.Name())
15 | }
16 |
17 | func ServicesFromFileDescriptor(c protoreflect.FileDescriptor) []protoreflect.ServiceDescriptor {
18 | var objs []protoreflect.ServiceDescriptor
19 | for i := 0; i < c.Services().Len(); i++ {
20 | service := c.Services().Get(i)
21 | objs = append(objs, service)
22 | }
23 | return objs
24 | }
25 |
26 | func MethodsFromServiceDescriptor(c protoreflect.ServiceDescriptor) []protoreflect.MethodDescriptor {
27 | var objs []protoreflect.MethodDescriptor
28 | for j := 0; j < c.Methods().Len(); j++ {
29 | method := c.Methods().Get(j)
30 | objs = append(objs, method)
31 | }
32 | return objs
33 | }
34 |
--------------------------------------------------------------------------------
/cmd/grpctl/docs/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/joshcarp/grpctl/cmd/doc
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/joshcarp/grpctl v0.0.0-20211210041557-d008f3f7f22e
7 | github.com/spf13/cobra v1.4.1-0.20220318100158-f848943afd72
8 | )
9 |
10 | require (
11 | github.com/bufbuild/connect-go v1.1.0 // indirect
12 | github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
13 | github.com/golang/protobuf v1.5.2 // indirect
14 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
15 | github.com/russross/blackfriday/v2 v2.1.0 // indirect
16 | github.com/spf13/pflag v1.0.5 // indirect
17 | golang.org/x/net v0.7.0 // indirect
18 | golang.org/x/sys v0.5.0 // indirect
19 | golang.org/x/text v0.7.0 // indirect
20 | google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e // indirect
21 | google.golang.org/grpc v1.50.1 // indirect
22 | google.golang.org/protobuf v1.28.1 // indirect
23 | gopkg.in/yaml.v2 v2.4.0 // indirect
24 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
25 | )
26 |
27 | replace github.com/joshcarp/grpctl => ../../../
28 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/joshcarp/grpctl
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/bufbuild/connect-go v1.1.0
7 | github.com/googleapis/gax-go/v2 v2.6.0
8 | github.com/spf13/cobra v1.4.1-0.20220318100158-f848943afd72
9 | github.com/stretchr/testify v1.7.0
10 | golang.org/x/net v0.2.0
11 | google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e
12 | google.golang.org/grpc v1.50.1
13 | google.golang.org/protobuf v1.28.1
14 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
15 | )
16 |
17 | require (
18 | cloud.google.com/go/billing v1.7.0 // indirect
19 | github.com/davecgh/go-spew v1.1.1 // indirect
20 | github.com/golang/protobuf v1.5.2 // indirect
21 | github.com/inconshreveable/mousetrap v1.0.0 // indirect
22 | github.com/kr/text v0.2.0 // indirect
23 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
24 | github.com/pmezard/go-difflib v1.0.0 // indirect
25 | github.com/spf13/pflag v1.0.5 // indirect
26 | golang.org/x/sys v0.2.0 // indirect
27 | golang.org/x/text v0.4.0 // indirect
28 | google.golang.org/api v0.102.0 // indirect
29 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect
30 | )
31 |
--------------------------------------------------------------------------------
/design/2-reflection.md:
--------------------------------------------------------------------------------
1 | # grpctl support for reflection
2 |
3 | ## Requirements
4 | - Ability to interact with reflection apis.
5 | - No config, and no paging input. This means that any command written can be shared without any external configuration.
6 | - Support for tab completion
7 |
8 | ## Ideas
9 |
10 | reflection is just another way of getting hold of the `protodescriptor.FileDescriptor` as opposed to explicitly importing the `pb.File_foo` from the generated go code.
11 |
12 | In order to meet the requirement of no config with tab completion, the cli tool would need to accomplish gRPC server reflection within the tab completion stage.
13 | For example once the following is written, and then `tab` is pressed:
14 | ```bash
15 | grpctl --address=localhost:8081 --plaintext=true [tab-tab]
16 | ```
17 |
18 | grpctl would go out to the grpc server located at `localhost:8081` and hit its server reflection api.
19 |
20 | After this initial part, the `protodescriptor.FileDescriptor` would be cached locally so the next time tab completion is needed it does not need to use grpc reflection.
21 |
22 | After this stage the cli is identical to using the static `protodescriptor.FileDescriptor`.
23 |
24 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | import (
4 | "encoding/base64"
5 | "os"
6 | "time"
7 |
8 | "gopkg.in/yaml.v3"
9 | )
10 |
11 | type config struct {
12 | Entries map[string]entry
13 | }
14 |
15 | type entry struct {
16 | Descriptor string
17 | Expiry time.Time
18 | }
19 |
20 | func (e entry) decodeDescriptor() ([]byte, error) {
21 | return base64.StdEncoding.DecodeString(e.Descriptor)
22 | }
23 |
24 | func loadConfig(filename string) (config, error) {
25 | f, err := os.ReadFile(filename)
26 | if err != nil {
27 | a := config{}.save(filename)
28 | return config{}, a
29 | }
30 | var c config
31 | err = yaml.Unmarshal(f, &c)
32 | if err != nil {
33 | return config{}, err
34 | }
35 | c = c.prune()
36 | if err = c.save(filename); err != nil {
37 | return config{}, err
38 | }
39 | return c, nil
40 | }
41 |
42 | func (c config) add(filename string, target string, descriptor []byte, dur time.Duration) error {
43 | c.Entries[target] = entry{
44 | Descriptor: base64.StdEncoding.EncodeToString(descriptor),
45 | Expiry: time.Now().Add(dur),
46 | }
47 | return c.save(filename)
48 | }
49 |
50 | func (c config) save(filename string) error {
51 | b, err := yaml.Marshal(c)
52 | if err != nil {
53 | return err
54 | }
55 | return os.WriteFile(filename, b, os.ModePerm)
56 | }
57 |
58 | func (c config) prune() config {
59 | newEntries := make(map[string]entry, len(c.Entries))
60 | for target, val := range c.Entries {
61 | if val.Expiry.Before(time.Now()) {
62 | continue
63 | }
64 | newEntries[target] = val
65 | }
66 | c.Entries = newEntries
67 | return c
68 | }
69 |
--------------------------------------------------------------------------------
/bin/hermit:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # THIS FILE IS GENERATED; DO NOT MODIFY
4 |
5 | set -eo pipefail
6 |
7 | export HERMIT_USER_HOME=~
8 |
9 | if [ -z "${HERMIT_STATE_DIR}" ]; then
10 | case "$(uname -s)" in
11 | Darwin)
12 | export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit"
13 | ;;
14 | Linux)
15 | export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit"
16 | ;;
17 | esac
18 | fi
19 |
20 | export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}"
21 | HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
22 | export HERMIT_CHANNEL
23 | export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}
24 |
25 | if [ ! -x "${HERMIT_EXE}" ]; then
26 | echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
27 | INSTALL_SCRIPT="$(mktemp)"
28 | # This value must match that of the install script
29 | INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38"
30 | if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then
31 | curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}"
32 | else
33 | # Install script is versioned by its sha256sum value
34 | curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}"
35 | # Verify install script's sha256sum
36 | openssl dgst -sha256 "${INSTALL_SCRIPT}" | \
37 | awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \
38 | '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}'
39 | fi
40 | /bin/bash "${INSTALL_SCRIPT}" 1>&2
41 | fi
42 |
43 | exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"
44 |
--------------------------------------------------------------------------------
/grpc.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | import (
4 | "os"
5 | "path"
6 | "time"
7 |
8 | "github.com/joshcarp/grpctl/internal/grpc"
9 |
10 | "github.com/spf13/cobra"
11 |
12 | "google.golang.org/protobuf/proto"
13 | "google.golang.org/protobuf/reflect/protoreflect"
14 | "google.golang.org/protobuf/types/descriptorpb"
15 | )
16 |
17 | func reflectFileDesc(flags []string) ([]protoreflect.FileDescriptor, error) {
18 | cmd := cobra.Command{
19 | FParseErrWhitelist: cobra.FParseErrWhitelist{
20 | UnknownFlags: true,
21 | },
22 | }
23 | err := persistentFlags(&cmd, "")
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | if len(flags) > 0 && flags[0] == "__complete" {
29 | flags = flags[1:]
30 | }
31 | cmd.SetArgs(flags)
32 | var fds []protoreflect.FileDescriptor
33 | cmd.RunE = func(cmd *cobra.Command, args []string) error {
34 | addr, err := cmd.Flags().GetString("address")
35 | if err != nil {
36 | return err
37 | }
38 | if addr == "" {
39 | return nil
40 | }
41 | cfgFile, err := cmd.Flags().GetString("config")
42 | if err != nil {
43 | return err
44 | }
45 | if cfgFile == "" {
46 | home, err := os.UserHomeDir()
47 | if err != nil {
48 | return err
49 | }
50 | cfgFile = path.Join(home, ".grpctl.yaml")
51 | if _, err := os.Stat(cfgFile); os.IsNotExist(err) {
52 | err = config{}.save(cfgFile)
53 | if err != nil {
54 | return err
55 | }
56 | }
57 | }
58 | cfg, err := loadConfig(cfgFile)
59 | if err != nil {
60 | return err
61 | }
62 | desc, err := cfg.Entries[addr].decodeDescriptor()
63 | if err != nil {
64 | return err
65 | }
66 | if len(desc) != 0 {
67 | spb := &descriptorpb.FileDescriptorSet{}
68 | err = proto.Unmarshal(desc, spb)
69 | if err != nil {
70 | return err
71 | }
72 | fds, err = grpc.ConvertToProtoReflectDesc(spb)
73 | if err != nil {
74 | return err
75 | }
76 | return nil
77 | }
78 | fdset, err := grpc.Reflect(cmd.Root().Context(), addr)
79 | if err != nil {
80 | return err
81 | }
82 | fds, err = grpc.ConvertToProtoReflectDesc(fdset)
83 | if err != nil {
84 | return err
85 | }
86 | b, err := proto.Marshal(fdset)
87 | if err != nil {
88 | return err
89 | }
90 | if err := cfg.add(cfgFile, addr, b, time.Minute*15); err != nil {
91 | return err
92 | }
93 | return nil
94 | }
95 |
96 | err = cmd.Execute()
97 | return fds, err
98 | }
99 |
--------------------------------------------------------------------------------
/design/1-grpctl.md:
--------------------------------------------------------------------------------
1 | # grpctl
2 |
3 | ## Requirements
4 | - Ability to interact with grpc apis without needing to consult any documentation.
5 | - Ability to customize behaviour for a specific use case if needed.
6 | - Ability to create custom binary for a specific teams grpc services.
7 |
8 | ## Nice to haves
9 |
10 | - No config: grpctl should be able to have no config/paperwork for the user to use the tool.
11 | - Server reflection: can be used like grpcurl, but would support tab completion.
12 |
13 | ## Inspirations
14 | - [grpcurl](https://github.com/fullstorydev/grpcurl)
15 | - Universal, doesn't allow to build custom cli.
16 | - [protoc-gen-cobra](https://github.com/fiorix/protoc-gen-cobra)
17 | - Works for a lot of usecases, but adding code generators have an overhead.
18 | - [gWhisper](https://github.com/IBM/gWhisper)
19 | - supports tab completion, doesn't support creating a custom cli.
20 |
21 | ## Ideas
22 |
23 | `grpctl` should be a package that allows an engineer to create a custom cli for their teams gRPC API's.
24 | It should use as much information that is possible from the `protodescriptor.FileDescriptor` that is possible. This `protoreflect.FileDescriptor` can come from the generated go grpc code.
25 | This approach means that there is no need to manually compile `.proto` files to get the `protodescriptor.FileDescriptor`.
26 |
27 | The conversion of `protodescriptor.FileDescriptor` to cobra command would look like this:
28 | - protoreflect.ServiceDescriptor -> top level command (eg `fooctl FooAPI`)
29 | - protoreflect.MethodDescriptor -> second level command (eg `fooctl FooAPI ListBar`)
30 | - protoreflect.MessageDescriptor -> flags (eg `fooctl FooAPI ListBar --field1="string"`)
31 |
32 | ### Creating a new cli tool
33 |
34 | Example of how a new grpctl cli should be created:
35 | ```golang
36 | // Example call: billingctl -H="Authorization: Bearer $(gcloud auth application-default print-access-token)" CloudBilling ListBillingAccounts
37 | func main() {
38 | cmd := &cobra.Command{
39 | Use: "billingctl",
40 | Short: "an example cli tool for the gcp billing api",
41 | }
42 | err := grpctl.BuildCommand(cmd,
43 | grpctl.WithFileDescriptors( // This specifies that we want to
44 | billing.File_google_cloud_billing_v1_cloud_billing_proto,
45 | billing.File_google_cloud_billing_v1_cloud_catalog_proto,
46 | ),
47 | )
48 | if err != nil {
49 | log.Print(err)
50 | }
51 | if err := grpctl.RunCommand(cmd, context.Background()); err != nil {
52 | log.Print(err)
53 | }
54 | }
55 | ```
56 |
57 | `grpctl.WithFileDescriptors` allows you to specify the proto descriptors from the generated go code, and in this specific example these FileDescriptors are found [here](https://github.com/googleapis/go-genproto/blob/3a66f561d7aa4010d9715ecf4c19b19e81e19f3c/googleapis/cloud/billing/v1/cloud_billing.pb.go#L767) which are generated from the proto source [here](https://github.com/googleapis/googleapis/blob/987192dfddeb79d3262b9f9f7dbf092827f931ac/google/cloud/billing/v1/cloud_billing.proto).
58 |
59 |
60 |
--------------------------------------------------------------------------------
/internal/testing/pkg/example/server.go:
--------------------------------------------------------------------------------
1 | package example
2 |
3 | import (
4 | "context"
5 | "crypto/x509"
6 | "fmt"
7 | "log"
8 | "net"
9 | "time"
10 |
11 | "google.golang.org/grpc/reflection"
12 |
13 | "github.com/googleapis/gax-go/v2"
14 | "github.com/joshcarp/grpctl/internal/testing/proto/examplepb"
15 | "google.golang.org/grpc"
16 | "google.golang.org/grpc/credentials"
17 | "google.golang.org/grpc/metadata"
18 | )
19 |
20 | type FooServer struct {
21 | examplepb.UnimplementedFooAPIServer
22 | }
23 |
24 | type Logger func(format string, args ...interface{})
25 |
26 | func (f FooServer) Hello(ctx context.Context, example *examplepb.ExampleRequest) (*examplepb.ExampleResponse, error) {
27 | md, _ := metadata.FromIncomingContext(ctx)
28 | return &examplepb.ExampleResponse{
29 | Message: fmt.Sprintf("Incoming Message: %s \n Metadata: %s", example.Message, md),
30 | }, nil
31 | }
32 |
33 | type BarServer struct {
34 | examplepb.UnimplementedBarAPIServer
35 | }
36 |
37 | func (f BarServer) ListBars(ctx context.Context, example *examplepb.BarRequest) (*examplepb.BarResponse, error) {
38 | md, _ := metadata.FromIncomingContext(ctx)
39 | return &examplepb.BarResponse{
40 | Message: fmt.Sprintf("Incoming Message: %s \n Metadata: %s", example.Message, md),
41 | }, nil
42 | }
43 |
44 | func ServeLis(ctx context.Context, log Logger, ln net.Listener, r ...func(*grpc.Server)) (err error) {
45 | srv := grpc.NewServer()
46 | for _, rr := range r {
47 | rr(srv)
48 | reflection.Register(srv)
49 | }
50 | go func() {
51 | err := srv.Serve(ln)
52 | log("error serving: %v", err)
53 | }()
54 | go func() {
55 | <-ctx.Done()
56 | srv.Stop()
57 | err := ln.Close()
58 | log("error closing: %v", err)
59 | }()
60 |
61 | bo := gax.Backoff{
62 | Initial: time.Second,
63 | Multiplier: 2,
64 | Max: 10 * time.Second,
65 | }
66 | for {
67 | _, err := setup(context.Background(), true, fmt.Sprintf("localhost:%d", ln.Addr().(*net.TCPAddr).Port))
68 | if err != nil {
69 | if err := gax.Sleep(ctx, bo.Pause()); err != nil {
70 | return err
71 | }
72 | continue
73 | }
74 | return nil
75 | }
76 | }
77 |
78 | func ServeRand(ctx context.Context, r ...func(*grpc.Server)) (int, error) {
79 | ln, err := net.Listen("tcp", "127.0.0.1:0")
80 | if err != nil {
81 | return 0, err
82 | }
83 | tcpAddr, _ := ln.Addr().(*net.TCPAddr)
84 | return tcpAddr.Port, ServeLis(ctx, log.Printf, ln, r...)
85 | }
86 |
87 | func setup(ctx context.Context, plaintext bool, targetURL string) (*grpc.ClientConn, error) {
88 | opts := []grpc.DialOption{
89 | grpc.WithBlock(),
90 | grpc.WithInsecure(), //nolint
91 | }
92 | if !plaintext {
93 | cp, err := x509.SystemCertPool()
94 | if err != nil {
95 | return nil, err
96 | }
97 | opts = []grpc.DialOption{
98 | grpc.WithBlock(),
99 | grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(cp, "")),
100 | }
101 | }
102 | cc, err := grpc.DialContext(ctx, targetURL, opts...)
103 | if err != nil {
104 | return nil, fmt.Errorf("%v: failed to connect to server", err)
105 | }
106 | return cc, nil
107 | }
108 |
--------------------------------------------------------------------------------
/design/3-customizabiliity.md:
--------------------------------------------------------------------------------
1 | # customizability in grpctl
2 |
3 | ## Requirements
4 |
5 | - Ability to customize grpctl and add small units of functionality based on the use case.
6 |
7 | ## Nice to haves
8 |
9 | - Should work for a substantial project; something like googleapis.
10 | - Avoid all global state.
11 |
12 | ## Inspirations
13 |
14 | - [functional options](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)
15 |
16 | ## Ideas
17 |
18 | functional options can be used to reduce the complexity of the discrete units of logic. The `cobra.Command` already has
19 | a lot of functionality that can be achieved through `cobra.Command.PersistentPreRun` and `cobra.Command.PreRun`.
20 |
21 | One issue that does exist is that the cobra.Command does not have the ability to set the `cobra.Context()`, which means
22 | that customizing behaviour would be limited.
23 |
24 | ### workaround for lack of setting ability for cobra.Command.Context()
25 |
26 | Instead of using the context directly a "custom context" struct can be added
27 | in `cobra.Command.ExecuteContext(context.Contetx)` with a pointer to a mutable context.
28 |
29 | Execution would look like this:
30 |
31 | ```go
32 | func RunCommand(cmd *cobra.Command, ctx context.Context) error {
33 | customCtx := customContext{
34 | ctx: &ctx,
35 | }
36 | return cmd.ExecuteContext(context.WithValue(context.Background(), configKey, &customCtx))
37 | }
38 | ```
39 |
40 | because `customCtx` stores a pointer to `ctx` one can modify what is in that position, essentially allowing for a
41 | context to be modified as if `cobra.Command.SetContext(context.Context)` existed.
42 |
43 | This allows the following to be possible:
44 |
45 | ```go
46 | &cobra.Command{
47 | Use: "foobar",
48 | PreRunE: func(cmd *cobra.Command, args []string) error {
49 | custCtx, ctx, ok := getContext(cmd) // unwrap the "custom context"
50 | if !ok {
51 | return nil
52 | }
53 | custCtx.setContext(context.WithValue(ctx, "foo", "bar")) // replace the context that the cusomContext.ctx points to
54 | return nil
55 | },
56 | RunE: func(cmd *cobra.Command, args []string) error {
57 | _, ctx, ok := getContext(cmd)
58 | require.True(t, ok, "custom context not found")
59 | require.Equal(t, "bar", ctx.Value("foo")) // The value is reflected here
60 | return nil
61 | },
62 | }
63 |
64 | ```
65 |
66 | ## Functional options that edit the context
67 |
68 | Now that the context can be edited from prerun hooks Authentication becomes easier, and will not halt autocompletion because PreRun hooks are not executed in the completion stage.
69 |
70 | Now the following functional option is possible:
71 | ```go
72 | WithContextFunc(func(ctx context.Context, cmd *cobra.Command) (context.Context, error) {
73 | return metadata.AppendToOutgoingContext(ctx, "fookey", "fooval"), nil
74 | })
75 | ```
76 | This can be used for setting grpc and authentication headers.
77 |
78 | ## Behaviour that can customize command
79 |
80 | The existing grpctl options can be found in [opts.go](../opts.go)
81 |
82 | If more flags are required for example a `--user` flag which is to modify the grpc context, one can add the flag definition and autocomplete before or after `grpctl.BuildCommand`
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/internal/descriptors/type.go:
--------------------------------------------------------------------------------
1 | package descriptors
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "reflect"
7 | "strconv"
8 | "strings"
9 |
10 | "google.golang.org/protobuf/reflect/protoreflect"
11 | )
12 |
13 | type DataValue struct {
14 | Kind protoreflect.Kind `json:"-"`
15 | Proto bool `json:"-"`
16 | Value interface{} `json:"value"`
17 | Empty bool `json:"-"`
18 | }
19 |
20 | type DataMap map[string]*DataValue
21 |
22 | func (d DataMap) ToJSON() ([]byte, error) {
23 | jsonVal := d.ToInterfaceMap()
24 | return json.Marshal(jsonVal)
25 | }
26 |
27 | func (d DataMap) ToInterfaceMap() map[string]interface{} {
28 | jsonVal := map[string]interface{}{}
29 | for key, val := range d {
30 | if val.Empty {
31 | continue
32 | }
33 | jsonVal[key] = val.Value
34 | }
35 | return jsonVal
36 | }
37 |
38 | func ToInterfaceMap(v interface{}) (map[string]interface{}, error) {
39 | marshal, err := json.Marshal(v)
40 | if err != nil {
41 | return nil, err
42 | }
43 | m := map[string]interface{}{}
44 | err = json.Unmarshal(marshal, &m)
45 | if err != nil {
46 | return nil, err
47 | }
48 | return m, nil
49 | }
50 |
51 | func (v *DataValue) String() string {
52 | return fmt.Sprintf("%v", v.Value)
53 | }
54 |
55 | func (v *DataValue) Set(val string) error {
56 | var err error
57 | if !v.Proto {
58 | switch reflect.TypeOf(v.Value).Kind() {
59 | case reflect.Map:
60 | vals := strings.Split(val, "=")
61 | if len(vals) != 2 {
62 | return fmt.Errorf("map type should be length of 2")
63 | }
64 | v.Value = map[string]interface{}{vals[0]: vals[1]}
65 | v.Empty = false
66 | return nil
67 | case reflect.Bool:
68 | v.Value, err = strconv.ParseBool(val)
69 | v.Empty = false
70 | return err
71 | }
72 | m, err := ToInterfaceMap(DataValue{Value: val})
73 | if err != nil {
74 | return nil
75 | }
76 | marshal, err := json.Marshal(m)
77 | if err != nil {
78 | return err
79 | }
80 | err = json.Unmarshal(marshal, &v)
81 | if err != nil {
82 | return err
83 | }
84 | v.Empty = false
85 | return nil
86 | }
87 | switch v.Kind {
88 | case protoreflect.BoolKind:
89 | v.Value, err = strconv.ParseBool(val)
90 | case protoreflect.EnumKind:
91 | v.Value, err = strconv.ParseInt(val, 10, 64)
92 | case protoreflect.Int32Kind:
93 | v.Value, err = strconv.ParseInt(val, 10, 32)
94 | case protoreflect.Sint32Kind:
95 | v.Value, err = strconv.ParseInt(val, 10, 32)
96 | case protoreflect.Uint32Kind:
97 | v.Value, err = strconv.ParseInt(val, 10, 32)
98 | case protoreflect.Int64Kind:
99 | v.Value, err = strconv.ParseInt(val, 10, 64)
100 | case protoreflect.Sint64Kind:
101 | v.Value, err = strconv.ParseInt(val, 10, 64)
102 | case protoreflect.Uint64Kind:
103 | v.Value, err = strconv.ParseInt(val, 10, 64)
104 | case protoreflect.Sfixed32Kind:
105 | v.Value, err = strconv.ParseInt(val, 10, 32)
106 | case protoreflect.Fixed32Kind:
107 | v.Value, err = strconv.ParseInt(val, 10, 32)
108 | case protoreflect.FloatKind:
109 | v.Value, err = strconv.ParseFloat(val, 64)
110 | case protoreflect.Sfixed64Kind:
111 | v.Value, err = strconv.ParseInt(val, 10, 64)
112 | case protoreflect.Fixed64Kind:
113 | v.Value, err = strconv.ParseInt(val, 10, 64)
114 | case protoreflect.DoubleKind:
115 | v.Value, err = strconv.ParseFloat(val, 64)
116 | case protoreflect.StringKind:
117 | v.Value = val
118 | case protoreflect.BytesKind, protoreflect.GroupKind, protoreflect.MessageKind:
119 | v.Value = val
120 | }
121 | return err
122 | }
123 |
124 | func (v *DataValue) Type() string {
125 | return v.Kind.String()
126 | }
127 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
grpctl
2 |
3 |
4 |
5 | []()
6 | [](https://github.com/joshcarp/grpctl/issues)
7 | [](https://github.com/joshcarp/grpctl/pulls)
8 | [](/LICENSE)
9 |
10 |
11 |
12 | A golang package for easily creating custom cli tools from FileDescriptors, or through the gRPC reflection API.
13 |
14 | # 📖 Table of contents
15 |
16 | - [Reflection cli mode](#reflection-cli-mode)
17 | - [File descriptor mode](#file-descriptor-mode)
18 | - [Autocompletion](#autocompletion)
19 | - [Flags](#flags)
20 | - [Design](#design)
21 | - [Contributing](#contributing)
22 | - [License](#license)
23 |
24 | ## 🪞 Reflection cli mode
25 |
26 | To be used like `grpcurl` against reflection APIs but with tab completion.
27 |
28 | 
29 |
30 | ### 📥 Install
31 |
32 | ```bash
33 | go get github.com/joshcarp/grpctl/cmd/grpctl
34 | grpctl --help
35 | ```
36 |
37 | [embedmd]:# (cmd/grpctl/docs/grpctl.md bash / -a/ /WithInsecure/)
38 | ```bash
39 | -a, --address string Address in form 'host:port'
40 | --config string Config file (default is $HOME/.grpctl.yaml)
41 | -H, --header stringArray Header in form 'key: value'
42 | -h, --help help for grpctl
43 | -p, --plaintext Dial grpc.WithInsecure
44 | ```
45 |
46 | ## 🗄️ File descriptor mode
47 |
48 | To easily create a cli tool for your grpc APIs using the code generated `protoreflect.FileDescriptor`
49 | To view all options that can be used, see [opts.go](opts.go).
50 |
51 | 
52 |
53 | ### 📥 Install
54 |
55 | [embedmd]:# (cmd/billingctl/main.go go /func main/ $)
56 | ```go
57 | func main() {
58 | cmd := &cobra.Command{
59 | Use: "billingctl",
60 | Short: "an example cli tool for the gcp billing api",
61 | }
62 | err := grpctl.BuildCommand(cmd,
63 | grpctl.WithArgs(os.Args),
64 | grpctl.WithFileDescriptors(
65 | billing.File_google_cloud_billing_v1_cloud_billing_proto,
66 | billing.File_google_cloud_billing_v1_cloud_catalog_proto,
67 | ),
68 | )
69 | if err != nil {
70 | log.Print(err)
71 | }
72 | if err := grpctl.RunCommand(cmd, context.Background()); err != nil {
73 | log.Print(err)
74 | }
75 | }
76 | ```
77 |
78 | ## 🤖 Autocompletion
79 |
80 | run `grpctl completion --help` and do what it says
81 |
82 | ## 🏳️🌈 Flags
83 |
84 | - `--address`
85 | ```bash
86 | grpctl --address=
87 | ```
88 | - it is important that the `=` is used with flags, otherwise the value will be interpreted as a command which does not exist.
89 |
90 | - `--header`
91 | ```bash
92 | grpctl --address= -H="Foo:Bar" -H="Bar: Foo"
93 | ```
94 | - Any white spaces at the start of the value will be stripped
95 |
96 | - `--protocol`
97 | ```bash
98 | grpctl --address= --protocol=
99 | ```
100 | - Specifies which rpc protocol to use, default=grpc
101 |
102 | - `--http1`
103 | ```bash
104 | grpctl --address= --http1
105 | ```
106 | - Use a http1.1 client instead of http2
107 |
108 | # 🧠 Design
109 |
110 | Design documents (more like a stream of consciousness) can be found in [./design](./design).
111 |
112 | # 🔧 Contributing
113 |
114 | This project is still in an alpha state, any contributions are welcome see [CONTRIBUTING.md](CONTRIBUTING.md).
115 |
116 | There is also a slack channel on gophers slack: [#grpctl](https://gophers.slack.com/archives/C02CAH9NP7H)
117 |
118 | # 🖋️ License
119 |
120 | See [LICENSE](LICENSE) for more details.
121 |
122 | ## 🎉 Acknowledgements
123 | - [@dancantos](https://github.com/dancantos)/[@anzboi](https://github.com/anzboi) and I were talking about [protoc-gen-cobra](https://github.com/fiorix/protoc-gen-cobra) when dan came up with the idea of using the proto descriptors to generate cobra commands on the fly.
124 |
125 |
--------------------------------------------------------------------------------
/internal/reflection/gen/go/v1/grpc_reflection_v1alphaconnect/reflection.connect.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-connect-go. DO NOT EDIT.
2 | //
3 | // Source: v1/reflection.proto
4 |
5 | package grpc_reflection_v1alphaconnect
6 |
7 | import (
8 | context "context"
9 | errors "errors"
10 | connect_go "github.com/bufbuild/connect-go"
11 | grpc_reflection_v1alpha "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
12 | http "net/http"
13 | strings "strings"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file and the connect package are
17 | // compatible. If you get a compiler error that this constant is not defined, this code was
18 | // generated with a version of connect newer than the one compiled into your binary. You can fix the
19 | // problem by either regenerating this code with an older version of connect or updating the connect
20 | // version compiled into your binary.
21 | const _ = connect_go.IsAtLeastVersion0_1_0
22 |
23 | const (
24 | // ServerReflectionName is the fully-qualified name of the ServerReflection service.
25 | ServerReflectionName = "grpc.reflection.v1.ServerReflection"
26 | )
27 |
28 | // ServerReflectionClient is a client for the grpc.reflection.v1.ServerReflection service.
29 | type ServerReflectionClient interface {
30 | // The reflection service is structured as a bidirectional stream, ensuring
31 | // all related requests go to a single server.
32 | ServerReflectionInfo(context.Context) *connect_go.BidiStreamForClient[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]
33 | }
34 |
35 | // NewServerReflectionClient constructs a client for the grpc.reflection.v1.ServerReflection
36 | // service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
37 | // gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
38 | // the connect.WithGRPC() or connect.WithGRPCWeb() options.
39 | //
40 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example,
41 | // http://api.acme.com or https://acme.com/grpc).
42 | func NewServerReflectionClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) ServerReflectionClient {
43 | baseURL = strings.TrimRight(baseURL, "/")
44 | return &serverReflectionClient{
45 | serverReflectionInfo: connect_go.NewClient[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse](
46 | httpClient,
47 | baseURL+"/grpc.reflection.v1.ServerReflection/ServerReflectionInfo",
48 | opts...,
49 | ),
50 | }
51 | }
52 |
53 | // serverReflectionClient implements ServerReflectionClient.
54 | type serverReflectionClient struct {
55 | serverReflectionInfo *connect_go.Client[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]
56 | }
57 |
58 | // ServerReflectionInfo calls grpc.reflection.v1.ServerReflection.ServerReflectionInfo.
59 | func (c *serverReflectionClient) ServerReflectionInfo(ctx context.Context) *connect_go.BidiStreamForClient[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse] {
60 | return c.serverReflectionInfo.CallBidiStream(ctx)
61 | }
62 |
63 | // ServerReflectionHandler is an implementation of the grpc.reflection.v1.ServerReflection service.
64 | type ServerReflectionHandler interface {
65 | // The reflection service is structured as a bidirectional stream, ensuring
66 | // all related requests go to a single server.
67 | ServerReflectionInfo(context.Context, *connect_go.BidiStream[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]) error
68 | }
69 |
70 | // NewServerReflectionHandler builds an HTTP handler from the service implementation. It returns the
71 | // path on which to mount the handler and the handler itself.
72 | //
73 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
74 | // and JSON codecs. They also support gzip compression.
75 | func NewServerReflectionHandler(svc ServerReflectionHandler, opts ...connect_go.HandlerOption) (string, http.Handler) {
76 | mux := http.NewServeMux()
77 | mux.Handle("/grpc.reflection.v1.ServerReflection/ServerReflectionInfo", connect_go.NewBidiStreamHandler(
78 | "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo",
79 | svc.ServerReflectionInfo,
80 | opts...,
81 | ))
82 | return "/grpc.reflection.v1.ServerReflection/", mux
83 | }
84 |
85 | // UnimplementedServerReflectionHandler returns CodeUnimplemented from all methods.
86 | type UnimplementedServerReflectionHandler struct{}
87 |
88 | func (UnimplementedServerReflectionHandler) ServerReflectionInfo(context.Context, *connect_go.BidiStream[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]) error {
89 | return connect_go.NewError(connect_go.CodeUnimplemented, errors.New("grpc.reflection.v1.ServerReflection.ServerReflectionInfo is not implemented"))
90 | }
91 |
--------------------------------------------------------------------------------
/internal/reflection/gen/go/v1alpha1/grpc_reflection_v1alphaconnect/reflection.connect.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-connect-go. DO NOT EDIT.
2 | //
3 | // v1alpha1/reflection.proto is a deprecated file.
4 |
5 | package grpc_reflection_v1alphaconnect
6 |
7 | import (
8 | context "context"
9 | errors "errors"
10 | connect_go "github.com/bufbuild/connect-go"
11 | grpc_reflection_v1alpha "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
12 | http "net/http"
13 | strings "strings"
14 | )
15 |
16 | // This is a compile-time assertion to ensure that this generated file and the connect package are
17 | // compatible. If you get a compiler error that this constant is not defined, this code was
18 | // generated with a version of connect newer than the one compiled into your binary. You can fix the
19 | // problem by either regenerating this code with an older version of connect or updating the connect
20 | // version compiled into your binary.
21 | const _ = connect_go.IsAtLeastVersion0_1_0
22 |
23 | const (
24 | // ServerReflectionName is the fully-qualified name of the ServerReflection service.
25 | ServerReflectionName = "grpc.reflection.v1alpha.ServerReflection"
26 | )
27 |
28 | // ServerReflectionClient is a client for the grpc.reflection.v1alpha.ServerReflection service.
29 | type ServerReflectionClient interface {
30 | // The reflection service is structured as a bidirectional stream, ensuring
31 | // all related requests go to a single server.
32 | ServerReflectionInfo(context.Context) *connect_go.BidiStreamForClient[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]
33 | }
34 |
35 | // NewServerReflectionClient constructs a client for the grpc.reflection.v1alpha.ServerReflection
36 | // service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for
37 | // gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply
38 | // the connect.WithGRPC() or connect.WithGRPCWeb() options.
39 | //
40 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example,
41 | // http://api.acme.com or https://acme.com/grpc).
42 | func NewServerReflectionClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) ServerReflectionClient {
43 | baseURL = strings.TrimRight(baseURL, "/")
44 | return &serverReflectionClient{
45 | serverReflectionInfo: connect_go.NewClient[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse](
46 | httpClient,
47 | baseURL+"/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
48 | opts...,
49 | ),
50 | }
51 | }
52 |
53 | // serverReflectionClient implements ServerReflectionClient.
54 | type serverReflectionClient struct {
55 | serverReflectionInfo *connect_go.Client[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]
56 | }
57 |
58 | // ServerReflectionInfo calls grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo.
59 | func (c *serverReflectionClient) ServerReflectionInfo(ctx context.Context) *connect_go.BidiStreamForClient[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse] {
60 | return c.serverReflectionInfo.CallBidiStream(ctx)
61 | }
62 |
63 | // ServerReflectionHandler is an implementation of the grpc.reflection.v1alpha.ServerReflection
64 | // service.
65 | type ServerReflectionHandler interface {
66 | // The reflection service is structured as a bidirectional stream, ensuring
67 | // all related requests go to a single server.
68 | ServerReflectionInfo(context.Context, *connect_go.BidiStream[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]) error
69 | }
70 |
71 | // NewServerReflectionHandler builds an HTTP handler from the service implementation. It returns the
72 | // path on which to mount the handler and the handler itself.
73 | //
74 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
75 | // and JSON codecs. They also support gzip compression.
76 | func NewServerReflectionHandler(svc ServerReflectionHandler, opts ...connect_go.HandlerOption) (string, http.Handler) {
77 | mux := http.NewServeMux()
78 | mux.Handle("/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo", connect_go.NewBidiStreamHandler(
79 | "/grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
80 | svc.ServerReflectionInfo,
81 | opts...,
82 | ))
83 | return "/grpc.reflection.v1alpha.ServerReflection/", mux
84 | }
85 |
86 | // UnimplementedServerReflectionHandler returns CodeUnimplemented from all methods.
87 | type UnimplementedServerReflectionHandler struct{}
88 |
89 | func (UnimplementedServerReflectionHandler) ServerReflectionInfo(context.Context, *connect_go.BidiStream[grpc_reflection_v1alpha.ServerReflectionRequest, grpc_reflection_v1alpha.ServerReflectionResponse]) error {
90 | return connect_go.NewError(connect_go.CodeUnimplemented, errors.New("grpc.reflection.v1alpha.ServerReflection.ServerReflectionInfo is not implemented"))
91 | }
92 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y=
2 | cloud.google.com/go/billing v1.7.0 h1:Xkii76HWELHwBtkQVZvqmSo9GTr0O+tIbRNnMcGdlg4=
3 | cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y=
4 | cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs=
5 | github.com/bufbuild/connect-go v1.1.0 h1:AUgqqO2ePdOJSpPOep6BPYz5v2moW1Lb8sQh0EeRzQ8=
6 | github.com/bufbuild/connect-go v1.1.0/go.mod h1:9iNvh/NOsfhNBUH5CtvXeVUskQO1xsrEviH7ZArwZ3I=
7 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
8 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
13 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
14 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
15 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
16 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
17 | github.com/googleapis/gax-go/v2 v2.6.0 h1:SXk3ABtQYDT/OH8jAyvEOQ58mgawq5C4o/4/89qN2ZU=
18 | github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY=
19 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
20 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
22 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
25 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
26 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
27 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
28 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
30 | github.com/spf13/cobra v1.4.1-0.20220318100158-f848943afd72 h1:oFdTNlgEciJdN6i4bYLqzT54yoPtXOt8JNN2B7Er944=
31 | github.com/spf13/cobra v1.4.1-0.20220318100158-f848943afd72/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g=
32 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
33 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
34 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
35 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
36 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
37 | golang.org/x/net v0.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
38 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
39 | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
40 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
41 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
42 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
43 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
44 | google.golang.org/api v0.102.0 h1:JxJl2qQ85fRMPNvlZY/enexbxpCjLwGhZUtgfGeQ51I=
45 | google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo=
46 | google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e h1:azcyH5lGzGy7pkLCbhPe0KkKxsM7c6UA/FZIXImKE7M=
47 | google.golang.org/genproto v0.0.0-20221111202108-142d8a6fa32e/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
48 | google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
49 | google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
50 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
51 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
52 | google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
53 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
55 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
56 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
57 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
58 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
59 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
60 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
61 |
--------------------------------------------------------------------------------
/internal/grpc/grpc.go:
--------------------------------------------------------------------------------
1 | package grpc
2 |
3 | import (
4 | "context"
5 | "crypto/tls"
6 | "fmt"
7 | "net"
8 | "net/http"
9 | "strings"
10 |
11 | "github.com/bufbuild/connect-go"
12 | reflectconnectv1 "github.com/joshcarp/grpctl/internal/reflection/gen/go/v1/grpc_reflection_v1alphaconnect"
13 | reflectconnect "github.com/joshcarp/grpctl/internal/reflection/gen/go/v1alpha1/grpc_reflection_v1alphaconnect"
14 | "golang.org/x/net/http2"
15 | reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
16 |
17 | "google.golang.org/protobuf/proto"
18 | "google.golang.org/protobuf/reflect/protodesc"
19 | "google.golang.org/protobuf/reflect/protoreflect"
20 | "google.golang.org/protobuf/types/descriptorpb"
21 | )
22 |
23 | func plaintext(url string) bool {
24 | return strings.Contains(url, "http://")
25 | }
26 |
27 | func client(enablehttp1 bool, plaintext bool) *http.Client {
28 | if enablehttp1 {
29 | return http.DefaultClient
30 | }
31 | return &http.Client{
32 | Transport: &http2.Transport{
33 | AllowHTTP: true,
34 | DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
35 | if plaintext {
36 | return net.Dial(network, addr)
37 | }
38 | return tls.Dial(network, addr, cfg)
39 | },
40 | },
41 | }
42 | }
43 |
44 | func Reflect(ctx context.Context, baseurl string) (*descriptorpb.FileDescriptorSet, error) {
45 | fdset, err := ReflectV1alpha1(ctx, baseurl)
46 | if connect.CodeOf(err) == connect.CodeUnimplemented {
47 | return ReflectV1(ctx, baseurl)
48 | }
49 | return fdset, err
50 | }
51 |
52 | // nolint: dupl
53 | func ReflectV1alpha1(ctx context.Context, baseurl string) (*descriptorpb.FileDescriptorSet, error) {
54 | client := reflectconnect.NewServerReflectionClient(client(false, plaintext(baseurl)), baseurl, connect.WithGRPC())
55 | stream := client.ServerReflectionInfo(ctx)
56 | req := &reflectpb.ServerReflectionRequest{MessageRequest: &reflectpb.ServerReflectionRequest_ListServices{}}
57 | if err := stream.Send(req); err != nil {
58 | return nil, err
59 | }
60 | resp, err := stream.Receive()
61 | if err != nil {
62 | return nil, err
63 | }
64 | listResp := resp.GetListServicesResponse()
65 | if listResp == nil {
66 | return nil, fmt.Errorf("can't list services")
67 | }
68 | fds := &descriptorpb.FileDescriptorSet{}
69 | seen := make(map[string]bool)
70 | for _, service := range listResp.GetService() {
71 | req = &reflectpb.ServerReflectionRequest{
72 | MessageRequest: &reflectpb.ServerReflectionRequest_FileContainingSymbol{
73 | FileContainingSymbol: service.GetName(),
74 | },
75 | }
76 | if err = stream.Send(req); err != nil {
77 | return nil, err
78 | }
79 | resp, err = stream.Receive()
80 | if err != nil {
81 | return nil, fmt.Errorf("error listing methods on '%s': %w", service, err)
82 | }
83 | fdResp := resp.GetFileDescriptorResponse()
84 | for _, f := range fdResp.GetFileDescriptorProto() {
85 | a := &descriptorpb.FileDescriptorProto{}
86 | if err = proto.Unmarshal(f, a); err != nil {
87 | return nil, err
88 | }
89 | if seen[a.GetName()] {
90 | continue
91 | }
92 | seen[a.GetName()] = true
93 | fds.File = append(fds.File, a)
94 | }
95 | }
96 | return fds, nil
97 | }
98 |
99 | // nolint: dupl
100 | func ReflectV1(ctx context.Context, baseurl string) (*descriptorpb.FileDescriptorSet, error) {
101 | client := reflectconnectv1.NewServerReflectionClient(client(false, plaintext(baseurl)), baseurl, connect.WithGRPC())
102 | stream := client.ServerReflectionInfo(ctx)
103 | req := &reflectpb.ServerReflectionRequest{MessageRequest: &reflectpb.ServerReflectionRequest_ListServices{}}
104 | if err := stream.Send(req); err != nil {
105 | return nil, err
106 | }
107 | resp, err := stream.Receive()
108 | if err != nil {
109 | return nil, err
110 | }
111 | listResp := resp.GetListServicesResponse()
112 | if listResp == nil {
113 | return nil, fmt.Errorf("can't list services")
114 | }
115 | fds := &descriptorpb.FileDescriptorSet{}
116 | seen := make(map[string]bool)
117 | for _, service := range listResp.GetService() {
118 | req = &reflectpb.ServerReflectionRequest{
119 | MessageRequest: &reflectpb.ServerReflectionRequest_FileContainingSymbol{
120 | FileContainingSymbol: service.GetName(),
121 | },
122 | }
123 | if err = stream.Send(req); err != nil {
124 | return nil, err
125 | }
126 | resp, err = stream.Receive()
127 | if err != nil {
128 | return nil, fmt.Errorf("error listing methods on '%s': %w", service, err)
129 | }
130 | fdResp := resp.GetFileDescriptorResponse()
131 | for _, f := range fdResp.GetFileDescriptorProto() {
132 | a := &descriptorpb.FileDescriptorProto{}
133 | if err = proto.Unmarshal(f, a); err != nil {
134 | return nil, err
135 | }
136 | if seen[a.GetName()] {
137 | continue
138 | }
139 | seen[a.GetName()] = true
140 | fds.File = append(fds.File, a)
141 | }
142 | }
143 | return fds, nil
144 | }
145 |
146 | func ConvertToProtoReflectDesc(fds *descriptorpb.FileDescriptorSet) ([]protoreflect.FileDescriptor, error) {
147 | files, err := protodesc.NewFiles(fds)
148 | if err != nil {
149 | return nil, err
150 | }
151 | var reflectds []protoreflect.FileDescriptor
152 | for _, fd := range fds.File {
153 | reflectfile, err := protodesc.NewFile(fd, files)
154 | if err != nil {
155 | return nil, err
156 | }
157 | reflectds = append(reflectds, reflectfile)
158 | }
159 | return reflectds, nil
160 | }
161 |
--------------------------------------------------------------------------------
/opts.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 |
8 | "github.com/spf13/cobra"
9 | "google.golang.org/protobuf/reflect/protoreflect"
10 | )
11 |
12 | // WithContextFunc will add commands to the cobra command through the file descriptors provided.
13 | func WithFileDescriptors(descriptors ...protoreflect.FileDescriptor) CommandOption {
14 | return func(cmd *cobra.Command) error {
15 | err := CommandFromFileDescriptors(cmd, descriptors...)
16 | if err != nil {
17 | return err
18 | }
19 | return nil
20 | }
21 | }
22 |
23 | // WithContextFunc will modify the context before the main command is run but not in the completion stage.
24 | func WithContextFunc(f func(context.Context, *cobra.Command) (context.Context, error)) CommandOption {
25 | return func(cmd *cobra.Command) error {
26 | existingPreRun := cmd.PersistentPreRunE
27 | cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
28 | if existingPreRun != nil {
29 | err := existingPreRun(cmd, args)
30 | if err != nil {
31 | return err
32 | }
33 | }
34 | ctx, err := f(cmd.Root().Context(), cmd)
35 | if err != nil {
36 | return err
37 | }
38 | cmd.Root().SetContext(ctx)
39 | return nil
40 | }
41 | return nil
42 | }
43 | }
44 |
45 | // WithContextDescriptorFunc will modify the context before the main command is run but not in the completion stage.
46 | func WithContextDescriptorFunc(f func(context.Context, *cobra.Command, protoreflect.MethodDescriptor) (context.Context, error)) CommandOption {
47 | return func(cmd *cobra.Command) error {
48 | existingPreRun := cmd.PersistentPreRunE
49 | cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
50 | if existingPreRun != nil {
51 | err := existingPreRun(cmd, args)
52 | if err != nil {
53 | return err
54 | }
55 | }
56 |
57 | a := cmd.Root().Context().Value(methodDescriptorKey{})
58 | method, ok := a.(protoreflect.MethodDescriptor)
59 | if !ok {
60 | return nil
61 | }
62 | ctx, err := f(cmd.Root().Context(), cmd, method)
63 | if err != nil {
64 | return err
65 | }
66 | cmd.Root().SetContext(ctx)
67 | return nil
68 | }
69 | return nil
70 | }
71 | }
72 |
73 | // WithArgs will set the args of the command as args[1:].
74 | func WithArgs(args []string) CommandOption {
75 | return func(cmd *cobra.Command) error {
76 | cmd.SetArgs(args[1:])
77 | return nil
78 | }
79 | }
80 |
81 | // WithReflection will enable grpc reflection on the command. Use this as an alternative to WithFileDescriptors.
82 | func WithReflection(args []string) CommandOption {
83 | return func(cmd *cobra.Command) error {
84 | var err error
85 | cmd.ValidArgsFunction = func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
86 | fds, err2 := reflectFileDesc(args)
87 | if err2 != nil {
88 | err = err2
89 | return nil, cobra.ShellCompDirectiveNoFileComp
90 | }
91 | var opts []string
92 | err2 = CommandFromFileDescriptors(cmd, fds...)
93 | if err2 != nil {
94 | err = err2
95 | return nil, cobra.ShellCompDirectiveNoFileComp
96 | }
97 | return opts, cobra.ShellCompDirectiveNoFileComp
98 | }
99 | fds, err := reflectFileDesc(args[1:])
100 | if err != nil {
101 | return err
102 | }
103 | if err = persistentFlags(cmd, ""); err != nil {
104 | return err
105 | }
106 | err = CommandFromFileDescriptors(cmd, fds...)
107 | if err != nil {
108 | return err
109 | }
110 | return nil
111 | }
112 | }
113 |
114 | func WithCompletion() CommandOption {
115 | return func(command *cobra.Command) error {
116 | cmd := &cobra.Command{
117 | Use: "completion [bash|zsh|fish|powershell]",
118 | Short: "Generate completion script",
119 | Long: fmt.Sprintf(`To load completions:
120 |
121 | Bash:
122 |
123 | $ source <(%[1]s completion bash)
124 |
125 | # To load completions for each session, execute once:
126 | # Linux:
127 | $ %[1]s completion bash > /etc/bash_completion.d/%[1]s
128 | # macOS:
129 | $ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s
130 |
131 | Zsh:
132 |
133 | # If shell completion is not already enabled in your environment,
134 | # you will need to enable it. You can execute the following once:
135 |
136 | $ echo "autoload -U compinit; compinit" >> ~/.zshrc
137 |
138 | # To load completions for each session, execute once:
139 | $ %[1]s completion zsh > "${fpath[1]}/_%[1]s"
140 |
141 | # You will need to start a new shell for this setup to take effect.
142 |
143 | fish:
144 |
145 | $ %[1]s completion fish | source
146 |
147 | # To load completions for each session, execute once:
148 | $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish
149 |
150 | PowerShell:
151 |
152 | PS> %[1]s completion powershell | Out-String | Invoke-Expression
153 |
154 | # To load completions for every new session, run:
155 | PS> %[1]s completion powershell > %[1]s.ps1
156 | # and source this file from your PowerShell profile.
157 | `, command.Root().Name()),
158 | DisableFlagsInUseLine: true,
159 | ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
160 | Args: cobra.ExactValidArgs(1),
161 | RunE: func(cmd *cobra.Command, args []string) error {
162 | var err error
163 | switch args[0] {
164 | case "bash":
165 | err = cmd.Root().GenBashCompletion(os.Stdout)
166 | case "zsh":
167 | err = cmd.Root().GenZshCompletion(os.Stdout)
168 | case "fish":
169 | err = cmd.Root().GenFishCompletion(os.Stdout, true)
170 | case "powershell":
171 | err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
172 | }
173 | return err
174 | },
175 | }
176 | command.AddCommand(cmd)
177 | return nil
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/internal/reflection/v1alpha1/reflection.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The gRPC Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | // Service exported by server reflection
15 |
16 |
17 | // Warning: this entire file is deprecated. Use this instead:
18 | // https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto
19 |
20 | syntax = "proto3";
21 |
22 | package grpc.reflection.v1alpha;
23 | option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1alpha";
24 | option deprecated = true;
25 | option java_multiple_files = true;
26 | option java_package = "io.grpc.reflection.v1alpha";
27 | option java_outer_classname = "ServerReflectionProto";
28 |
29 | service ServerReflection {
30 | // The reflection service is structured as a bidirectional stream, ensuring
31 | // all related requests go to a single server.
32 | rpc ServerReflectionInfo(stream ServerReflectionRequest)
33 | returns (stream ServerReflectionResponse);
34 | }
35 |
36 | // The message sent by the client when calling ServerReflectionInfo method.
37 | message ServerReflectionRequest {
38 | string host = 1;
39 | // To use reflection service, the client should set one of the following
40 | // fields in message_request. The server distinguishes requests by their
41 | // defined field and then handles them using corresponding methods.
42 | oneof message_request {
43 | // Find a proto file by the file name.
44 | string file_by_filename = 3;
45 |
46 | // Find the proto file that declares the given fully-qualified symbol name.
47 | // This field should be a fully-qualified symbol name
48 | // (e.g. .[.] or .).
49 | string file_containing_symbol = 4;
50 |
51 | // Find the proto file which defines an extension extending the given
52 | // message type with the given field number.
53 | ExtensionRequest file_containing_extension = 5;
54 |
55 | // Finds the tag numbers used by all known extensions of extendee_type, and
56 | // appends them to ExtensionNumberResponse in an undefined order.
57 | // Its corresponding method is best-effort: it's not guaranteed that the
58 | // reflection service will implement this method, and it's not guaranteed
59 | // that this method will provide all extensions. Returns
60 | // StatusCode::UNIMPLEMENTED if it's not implemented.
61 | // This field should be a fully-qualified type name. The format is
62 | // .
63 | string all_extension_numbers_of_type = 6;
64 |
65 | // List the full names of registered services. The content will not be
66 | // checked.
67 | string list_services = 7;
68 | }
69 | }
70 |
71 | // The type name and extension number sent by the client when requesting
72 | // file_containing_extension.
73 | message ExtensionRequest {
74 | // Fully-qualified type name. The format should be .
75 | string containing_type = 1;
76 | int32 extension_number = 2;
77 | }
78 |
79 | // The message sent by the server to answer ServerReflectionInfo method.
80 | message ServerReflectionResponse {
81 | string valid_host = 1;
82 | ServerReflectionRequest original_request = 2;
83 | // The server set one of the following fields according to the message_request
84 | // in the request.
85 | oneof message_response {
86 | // This message is used to answer file_by_filename, file_containing_symbol,
87 | // file_containing_extension requests with transitive dependencies. As
88 | // the repeated label is not allowed in oneof fields, we use a
89 | // FileDescriptorResponse message to encapsulate the repeated fields.
90 | // The reflection service is allowed to avoid sending FileDescriptorProtos
91 | // that were previously sent in response to earlier requests in the stream.
92 | FileDescriptorResponse file_descriptor_response = 4;
93 |
94 | // This message is used to answer all_extension_numbers_of_type requst.
95 | ExtensionNumberResponse all_extension_numbers_response = 5;
96 |
97 | // This message is used to answer list_services request.
98 | ListServiceResponse list_services_response = 6;
99 |
100 | // This message is used when an error occurs.
101 | ErrorResponse error_response = 7;
102 | }
103 | }
104 |
105 | // Serialized FileDescriptorProto messages sent by the server answering
106 | // a file_by_filename, file_containing_symbol, or file_containing_extension
107 | // request.
108 | message FileDescriptorResponse {
109 | // Serialized FileDescriptorProto messages. We avoid taking a dependency on
110 | // descriptor.proto, which uses proto2 only features, by making them opaque
111 | // bytes instead.
112 | repeated bytes file_descriptor_proto = 1;
113 | }
114 |
115 | // A list of extension numbers sent by the server answering
116 | // all_extension_numbers_of_type request.
117 | message ExtensionNumberResponse {
118 | // Full name of the base type, including the package name. The format
119 | // is .
120 | string base_type_name = 1;
121 | repeated int32 extension_number = 2;
122 | }
123 |
124 | // A list of ServiceResponse sent by the server answering list_services request.
125 | message ListServiceResponse {
126 | // The information of each service may be expanded in the future, so we use
127 | // ServiceResponse message to encapsulate it.
128 | repeated ServiceResponse service = 1;
129 | }
130 |
131 | // The information of a single service used by ListServiceResponse to answer
132 | // list_services request.
133 | message ServiceResponse {
134 | // Full name of a registered service, including its package name. The format
135 | // is .
136 | string name = 1;
137 | }
138 |
139 | // The error code and error message sent by the server when an error occurs.
140 | message ErrorResponse {
141 | // This field uses the error codes defined in grpc::StatusCode.
142 | int32 error_code = 1;
143 | string error_message = 2;
144 | }
--------------------------------------------------------------------------------
/internal/reflection/v1/reflection.proto:
--------------------------------------------------------------------------------
1 | // Copyright 2016 The gRPC Authors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | // Service exported by server reflection. A more complete description of how
16 | // server reflection works can be found at
17 | // https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
18 | //
19 | // The canonical version of this proto can be found at
20 | // https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto
21 |
22 | syntax = "proto3";
23 |
24 | package grpc.reflection.v1;
25 | option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1alpha";
26 | option java_multiple_files = true;
27 | option java_package = "io.grpc.reflection.v1";
28 | option java_outer_classname = "ServerReflectionProto";
29 |
30 | service ServerReflection {
31 | // The reflection service is structured as a bidirectional stream, ensuring
32 | // all related requests go to a single server.
33 | rpc ServerReflectionInfo(stream ServerReflectionRequest)
34 | returns (stream ServerReflectionResponse);
35 | }
36 |
37 | // The message sent by the client when calling ServerReflectionInfo method.
38 | message ServerReflectionRequest {
39 | string host = 1;
40 | // To use reflection service, the client should set one of the following
41 | // fields in message_request. The server distinguishes requests by their
42 | // defined field and then handles them using corresponding methods.
43 | oneof message_request {
44 | // Find a proto file by the file name.
45 | string file_by_filename = 3;
46 |
47 | // Find the proto file that declares the given fully-qualified symbol name.
48 | // This field should be a fully-qualified symbol name
49 | // (e.g. .[.] or .).
50 | string file_containing_symbol = 4;
51 |
52 | // Find the proto file which defines an extension extending the given
53 | // message type with the given field number.
54 | ExtensionRequest file_containing_extension = 5;
55 |
56 | // Finds the tag numbers used by all known extensions of the given message
57 | // type, and appends them to ExtensionNumberResponse in an undefined order.
58 | // Its corresponding method is best-effort: it's not guaranteed that the
59 | // reflection service will implement this method, and it's not guaranteed
60 | // that this method will provide all extensions. Returns
61 | // StatusCode::UNIMPLEMENTED if it's not implemented.
62 | // This field should be a fully-qualified type name. The format is
63 | // .
64 | string all_extension_numbers_of_type = 6;
65 |
66 | // List the full names of registered services. The content will not be
67 | // checked.
68 | string list_services = 7;
69 | }
70 | }
71 |
72 | // The type name and extension number sent by the client when requesting
73 | // file_containing_extension.
74 | message ExtensionRequest {
75 | // Fully-qualified type name. The format should be .
76 | string containing_type = 1;
77 | int32 extension_number = 2;
78 | }
79 |
80 | // The message sent by the server to answer ServerReflectionInfo method.
81 | message ServerReflectionResponse {
82 | string valid_host = 1;
83 | ServerReflectionRequest original_request = 2;
84 | // The server sets one of the following fields according to the message_request
85 | // in the request.
86 | oneof message_response {
87 | // This message is used to answer file_by_filename, file_containing_symbol,
88 | // file_containing_extension requests with transitive dependencies.
89 | // As the repeated label is not allowed in oneof fields, we use a
90 | // FileDescriptorResponse message to encapsulate the repeated fields.
91 | // The reflection service is allowed to avoid sending FileDescriptorProtos
92 | // that were previously sent in response to earlier requests in the stream.
93 | FileDescriptorResponse file_descriptor_response = 4;
94 |
95 | // This message is used to answer all_extension_numbers_of_type requests.
96 | ExtensionNumberResponse all_extension_numbers_response = 5;
97 |
98 | // This message is used to answer list_services requests.
99 | ListServiceResponse list_services_response = 6;
100 |
101 | // This message is used when an error occurs.
102 | ErrorResponse error_response = 7;
103 | }
104 | }
105 |
106 | // Serialized FileDescriptorProto messages sent by the server answering
107 | // a file_by_filename, file_containing_symbol, or file_containing_extension
108 | // request.
109 | message FileDescriptorResponse {
110 | // Serialized FileDescriptorProto messages. We avoid taking a dependency on
111 | // descriptor.proto, which uses proto2 only features, by making them opaque
112 | // bytes instead.
113 | repeated bytes file_descriptor_proto = 1;
114 | }
115 |
116 | // A list of extension numbers sent by the server answering
117 | // all_extension_numbers_of_type request.
118 | message ExtensionNumberResponse {
119 | // Full name of the base type, including the package name. The format
120 | // is .
121 | string base_type_name = 1;
122 | repeated int32 extension_number = 2;
123 | }
124 |
125 | // A list of ServiceResponse sent by the server answering list_services request.
126 | message ListServiceResponse {
127 | // The information of each service may be expanded in the future, so we use
128 | // ServiceResponse message to encapsulate it.
129 | repeated ServiceResponse service = 1;
130 | }
131 |
132 | // The information of a single service used by ListServiceResponse to answer
133 | // list_services request.
134 | message ServiceResponse {
135 | // Full name of a registered service, including its package name. The format
136 | // is .
137 | string name = 1;
138 | }
139 |
140 | // The error code and error message sent by the server when an error occurs.
141 | message ErrorResponse {
142 | // This field uses the error codes defined in grpc::StatusCode.
143 | int32 error_code = 1;
144 | string error_message = 2;
145 | }
146 |
--------------------------------------------------------------------------------
/internal/descriptors/proto.go:
--------------------------------------------------------------------------------
1 | //nolint
2 | package descriptors
3 |
4 | import (
5 | "encoding/json"
6 |
7 | "google.golang.org/protobuf/encoding/protojson"
8 | "google.golang.org/protobuf/proto"
9 | "google.golang.org/protobuf/reflect/protoreflect"
10 | "google.golang.org/protobuf/types/dynamicpb"
11 | "google.golang.org/protobuf/types/known/anypb"
12 | "google.golang.org/protobuf/types/known/emptypb"
13 | "google.golang.org/protobuf/types/known/structpb"
14 | )
15 |
16 | // Adapted from https://github.com/fullstorydev/grpcurl/blob/de25c898228e36e8539862ed08de69598e64cb76/grpcurl.go#L400
17 | func MakeJSONTemplate(md protoreflect.MessageDescriptor) (map[string]interface{}, string) {
18 | toString, err := protojson.Marshal(MakeTemplate(md, nil))
19 | if err != nil {
20 | return nil, ""
21 | }
22 | var m map[string]interface{}
23 | err = json.Unmarshal(toString, &m)
24 | if err != nil {
25 | return nil, ""
26 | }
27 | return m, string(toString)
28 | }
29 |
30 | func MakeTemplate(md protoreflect.MessageDescriptor, path []protoreflect.MessageDescriptor) proto.Message {
31 | switch md.FullName() {
32 | case "google.protobuf.Any":
33 | var any anypb.Any
34 | _ = anypb.MarshalFrom(&any, &emptypb.Empty{}, proto.MarshalOptions{})
35 | return &any
36 | case "google.protobuf.Value":
37 | return &structpb.Value{
38 | Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
39 | Fields: map[string]*structpb.Value{
40 | "google.protobuf.Value": {Kind: &structpb.Value_StringValue{
41 | StringValue: "supports arbitrary JSON",
42 | }},
43 | },
44 | }},
45 | }
46 | case "google.protobuf.ListValue":
47 | return &structpb.ListValue{
48 | Values: []*structpb.Value{
49 | {
50 | Kind: &structpb.Value_StructValue{StructValue: &structpb.Struct{
51 | Fields: map[string]*structpb.Value{
52 | "google.protobuf.ListValue": {Kind: &structpb.Value_StringValue{
53 | StringValue: "is an array of arbitrary JSON values",
54 | }},
55 | },
56 | }},
57 | },
58 | },
59 | }
60 | case "google.protobuf.Struct":
61 | return &structpb.Struct{
62 | Fields: map[string]*structpb.Value{
63 | "google.protobuf.Struct": {Kind: &structpb.Value_StringValue{
64 | StringValue: "supports arbitrary JSON objects",
65 | }},
66 | },
67 | }
68 | }
69 | dm := dynamicpb.NewMessage(md)
70 | for _, seen := range path {
71 | if seen == md {
72 | return dm
73 | }
74 | }
75 | for i := 0; i < dm.Descriptor().Fields().Len(); i++ {
76 | fd := dm.Descriptor().Fields().Get(i)
77 | var val protoreflect.Value
78 | switch fd.Kind() {
79 | case protoreflect.BoolKind:
80 | val = protoreflect.ValueOfBool(true)
81 | case protoreflect.EnumKind:
82 | val = protoreflect.ValueOfEnum(1)
83 | case protoreflect.Int32Kind:
84 | val = protoreflect.ValueOfInt32(1)
85 | case protoreflect.Sint32Kind:
86 | val = protoreflect.ValueOfInt32(1)
87 | case protoreflect.Uint32Kind:
88 | val = protoreflect.ValueOfInt32(1)
89 | case protoreflect.Int64Kind:
90 | val = protoreflect.ValueOfInt64(1)
91 | case protoreflect.Sint64Kind:
92 | val = protoreflect.ValueOfInt64(1)
93 | case protoreflect.Uint64Kind:
94 | val = protoreflect.ValueOfInt64(1)
95 | case protoreflect.Sfixed32Kind:
96 | val = protoreflect.ValueOfInt32(1)
97 | case protoreflect.Fixed32Kind:
98 | val = protoreflect.ValueOfFloat32(1.1)
99 | case protoreflect.FloatKind:
100 | val = protoreflect.ValueOfFloat32(1.1)
101 | case protoreflect.Sfixed64Kind:
102 | val = protoreflect.ValueOfInt64(1)
103 | case protoreflect.Fixed64Kind:
104 | val = protoreflect.ValueOfFloat64(1.1)
105 | case protoreflect.DoubleKind:
106 | val = protoreflect.ValueOfFloat64(1.1)
107 | case protoreflect.StringKind:
108 | val = protoreflect.ValueOfString("string")
109 | case protoreflect.BytesKind:
110 | val = protoreflect.ValueOfBytes([]byte(fd.JSONName()))
111 | case protoreflect.MessageKind:
112 | val = protoreflect.ValueOfMessage(MakeTemplate(fd.Message(), nil).ProtoReflect())
113 | default:
114 | return dm
115 | }
116 | if fd.Cardinality() == protoreflect.Repeated {
117 | val = protoreflect.ValueOfList(&List{vals: []protoreflect.Value{val}})
118 | continue
119 | }
120 |
121 | // TODO: this is a bug in the billingctl example
122 | if fd.JSONName() == "crc32c" {
123 | return dm
124 | }
125 | dm.Set(fd, val)
126 | }
127 | return dm
128 | }
129 |
130 | type List struct {
131 | vals []protoreflect.Value
132 | }
133 |
134 | // Len reports the number of entries in the List.
135 | // Get, Set, and Truncate panic with out of bound indexes.
136 | func (l *List) Len() int {
137 | return len(l.vals)
138 | }
139 |
140 | // Get retrieves the value at the given index.
141 | // It never returns an invalid value.
142 | func (l *List) Get(i int) protoreflect.Value {
143 | return l.vals[i]
144 | }
145 |
146 | // Set stores a value for the given index.
147 | // When setting a composite type, it is unspecified whether the set
148 | // value aliases the source's memory in any way.
149 | //
150 | // Set is a mutating operation and unsafe for concurrent use.
151 | func (l *List) Set(i int, val protoreflect.Value) {
152 | l.vals[i] = val
153 | }
154 |
155 | // Append appends the provided value to the end of the list.
156 | // When appending a composite type, it is unspecified whether the appended
157 | // value aliases the source's memory in any way.
158 | //
159 | // Append is a mutating operation and unsafe for concurrent use.
160 | func (l *List) Append(v protoreflect.Value) {
161 | l.vals = append(l.vals, v)
162 | }
163 |
164 | // AppendMutable appends a new, empty, mutable message value to the end
165 | // of the list and returns it.
166 | // It panics if the list does not contain a message type.
167 | func (l *List) AppendMutable() protoreflect.Value {
168 | return protoreflect.ValueOfMessage(nil)
169 | }
170 |
171 | // Truncate truncates the list to a smaller length.
172 | //
173 | // Truncate is a mutating operation and unsafe for concurrent use.
174 | func (l *List) Truncate(i int) {
175 | l.vals = l.vals[0:i]
176 | }
177 |
178 | // NewElement returns a new value for a list element.
179 | // For enums, this returns the first enum value.
180 | // For other scalars, this returns the zero value.
181 | // For messages, this returns a new, empty, mutable value.
182 | func (l *List) NewElement() protoreflect.Value {
183 | return protoreflect.ValueOfMessage(nil)
184 | }
185 |
186 | // IsValid reports whether the list is valid.
187 | //
188 | // An invalid list is an empty, read-only value.
189 | //
190 | // Validity is not part of the protobuf data model, and may not
191 | // be preserved in marshaling or other operations.
192 | func (l *List) IsValid() bool {
193 | return true
194 | }
195 |
--------------------------------------------------------------------------------
/internal/grpc/connect.go:
--------------------------------------------------------------------------------
1 | package grpc
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "io"
7 |
8 | "github.com/bufbuild/connect-go"
9 | "github.com/joshcarp/grpctl/internal/descriptors"
10 | "google.golang.org/grpc/metadata"
11 | "google.golang.org/protobuf/encoding/protojson"
12 | "google.golang.org/protobuf/proto"
13 | "google.golang.org/protobuf/reflect/protoreflect"
14 | "google.golang.org/protobuf/reflect/protoregistry"
15 | "google.golang.org/protobuf/types/dynamicpb"
16 | "google.golang.org/protobuf/types/known/emptypb"
17 | )
18 |
19 | func CallUnary(ctx context.Context, addr string, method protoreflect.MethodDescriptor, inputData []byte, protocol string, http1 bool) ([]byte, error) {
20 | dynamicRequest := dynamicpb.NewMessage(method.Input())
21 | err := protojson.Unmarshal(inputData, dynamicRequest)
22 | if err != nil {
23 | return nil, err
24 | }
25 | requestBytes, err := proto.Marshal(dynamicRequest)
26 | if err != nil {
27 | return nil, err
28 | }
29 | request := &emptypb.Empty{}
30 | if err := proto.Unmarshal(requestBytes, request); err != nil {
31 | return nil, err
32 | }
33 | connectReq := connect.NewRequest(request)
34 | if md, ok := metadata.FromOutgoingContext(ctx); ok {
35 | for key, val := range md {
36 | connectReq.Header().Set(key, val[0])
37 | }
38 | }
39 | fqnAddr := addr + descriptors.FullMethod(method)
40 | var clientOpts []connect.ClientOption
41 | switch protocol {
42 | case "grpc":
43 | clientOpts = append(clientOpts, connect.WithGRPC())
44 | case "grpcweb":
45 | clientOpts = append(clientOpts, connect.WithGRPCWeb())
46 | case "connect":
47 | default:
48 | }
49 | client := connect.NewClient[emptypb.Empty, emptypb.Empty](client(http1, plaintext(addr)), fqnAddr, clientOpts...)
50 | var registry protoregistry.Types
51 | if err := registry.RegisterMessage(dynamicpb.NewMessageType(method.Output())); err != nil {
52 | return nil, err
53 | }
54 | if err := registry.RegisterMessage(dynamicpb.NewMessageType(method.Input())); err != nil {
55 | return nil, err
56 | }
57 | response, err := client.CallUnary(ctx, connectReq)
58 | if err != nil {
59 | return nil, err
60 | }
61 | responseBytes, err := proto.Marshal(response.Msg)
62 | if err != nil {
63 | return nil, err
64 | }
65 | dynamicResponse := dynamicpb.NewMessage(method.Output())
66 | if err := proto.Unmarshal(responseBytes, dynamicResponse); err != nil {
67 | return nil, err
68 | }
69 | return protojson.MarshalOptions{Resolver: ®istry, Multiline: true, Indent: " "}.Marshal(dynamicResponse)
70 | }
71 |
72 | func ParseMessage(inputJSON []byte, messageDesc protoreflect.MessageDescriptor) (*emptypb.Empty, error) {
73 | dynamicRequest := dynamicpb.NewMessage(messageDesc)
74 | err := protojson.Unmarshal(inputJSON, dynamicRequest)
75 | if err != nil {
76 | return nil, err
77 | }
78 | requestBytes, err := proto.Marshal(dynamicRequest)
79 | if err != nil {
80 | return nil, err
81 | }
82 | request := &emptypb.Empty{}
83 | if err := proto.Unmarshal(requestBytes, request); err != nil {
84 | return nil, err
85 | }
86 | return request, nil
87 | }
88 |
89 | func Send(inputJSON chan []byte, messageDescriptor protoreflect.MessageDescriptor, f func(*emptypb.Empty) error) error {
90 | for inputs := range inputJSON {
91 | request, err := ParseMessage(inputs, messageDescriptor)
92 | if err != nil {
93 | return err
94 | }
95 | err = f(request)
96 | if err != nil {
97 | return err
98 | }
99 | }
100 | return nil
101 | }
102 |
103 | func Receive(outputJSON chan []byte, method protoreflect.MethodDescriptor, f func() (*emptypb.Empty, error)) error {
104 | defer close(outputJSON)
105 | for {
106 | msg, err := f()
107 | if errors.Is(err, io.EOF) {
108 | break
109 | }
110 | if err != nil {
111 | return err
112 | }
113 | if msg == nil {
114 | break
115 | }
116 | responseBytes, err := proto.Marshal(msg)
117 | if err != nil {
118 | return nil
119 | }
120 | dynamicResponse := dynamicpb.NewMessage(method.Output())
121 | if err := proto.Unmarshal(responseBytes, dynamicResponse); err != nil {
122 | return err
123 | }
124 | reg, err := registry(method)
125 | if err != nil {
126 | return err
127 | }
128 | b, err := protojson.MarshalOptions{Resolver: ®, Multiline: true, Indent: " "}.Marshal(dynamicResponse)
129 | if err != nil {
130 | return err
131 | }
132 | outputJSON <- b
133 | }
134 | return nil
135 | }
136 |
137 | func CallStreaming(ctx context.Context, addr string, method protoreflect.MethodDescriptor, protocol string, http1 bool, inputJSON, outputJSON chan []byte) error {
138 | client := getClient(addr, method, protocol, http1)
139 | if method.IsStreamingClient() && method.IsStreamingServer() { //nolint:gocritic
140 | stream := client.CallBidiStream(ctx)
141 | if err := Send(inputJSON, method.Input(), stream.Send); err != nil {
142 | return err
143 | }
144 | if err := Receive(outputJSON, method, stream.Receive); err != nil {
145 | return err
146 | }
147 | } else if method.IsStreamingClient() {
148 | stream := client.CallClientStream(ctx)
149 | if err := Send(inputJSON, method.Input(), stream.Send); err != nil {
150 | return err
151 | }
152 | err := Receive(outputJSON, method, func() (*emptypb.Empty, error) {
153 | resp, err := stream.CloseAndReceive()
154 | if err != nil {
155 | return nil, err
156 | }
157 | return resp.Msg, err
158 | })
159 | if err != nil {
160 | return err
161 | }
162 | } else if method.IsStreamingServer() {
163 | req, err := ParseMessage(<-inputJSON, method.Input())
164 | if err != nil {
165 | return err
166 | }
167 | stream, err := client.CallServerStream(ctx, connect.NewRequest(req))
168 | if err != nil {
169 | return err
170 | }
171 | err = Receive(outputJSON, method, func() (*emptypb.Empty, error) {
172 | if stream.Receive() {
173 | return stream.Msg(), nil
174 | }
175 | return nil, nil
176 | })
177 | if err != nil {
178 | return err
179 | }
180 | }
181 | return nil
182 | }
183 |
184 | func registry(method protoreflect.MethodDescriptor) (protoregistry.Types, error) {
185 | var registry protoregistry.Types
186 | if err := registry.RegisterMessage(dynamicpb.NewMessageType(method.Output())); err != nil {
187 | return protoregistry.Types{}, err
188 | }
189 | if err := registry.RegisterMessage(dynamicpb.NewMessageType(method.Input())); err != nil {
190 | return protoregistry.Types{}, err
191 | }
192 | return registry, nil
193 | }
194 |
195 | func getClient(addr string, method protoreflect.MethodDescriptor, protocol string, http1 bool) *connect.Client[emptypb.Empty, emptypb.Empty] {
196 | fqnAddr := addr + descriptors.FullMethod(method)
197 | var clientOpts []connect.ClientOption
198 | switch protocol {
199 | case "grpc":
200 | clientOpts = append(clientOpts, connect.WithGRPC())
201 | case "grpcweb":
202 | clientOpts = append(clientOpts, connect.WithGRPCWeb())
203 | case "connect":
204 | default:
205 | }
206 | return connect.NewClient[emptypb.Empty, emptypb.Empty](client(http1, plaintext(addr)), fqnAddr, clientOpts...)
207 | }
208 |
--------------------------------------------------------------------------------
/internal/testing/proto/examplepb/api_grpc.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT.
2 |
3 | package examplepb
4 |
5 | import (
6 | context "context"
7 | grpc "google.golang.org/grpc"
8 | codes "google.golang.org/grpc/codes"
9 | status "google.golang.org/grpc/status"
10 | )
11 |
12 | // This is a compile-time assertion to ensure that this generated file
13 | // is compatible with the grpc package it is being compiled against.
14 | // Requires gRPC-Go v1.32.0 or later.
15 | const _ = grpc.SupportPackageIsVersion7
16 |
17 | // FooAPIClient is the client API for FooAPI service.
18 | //
19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
20 | type FooAPIClient interface {
21 | Hello(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error)
22 | }
23 |
24 | type fooAPIClient struct {
25 | cc grpc.ClientConnInterface
26 | }
27 |
28 | func NewFooAPIClient(cc grpc.ClientConnInterface) FooAPIClient {
29 | return &fooAPIClient{cc}
30 | }
31 |
32 | func (c *fooAPIClient) Hello(ctx context.Context, in *ExampleRequest, opts ...grpc.CallOption) (*ExampleResponse, error) {
33 | out := new(ExampleResponse)
34 | err := c.cc.Invoke(ctx, "/example.FooAPI/Hello", in, out, opts...)
35 | if err != nil {
36 | return nil, err
37 | }
38 | return out, nil
39 | }
40 |
41 | // FooAPIServer is the server API for FooAPI service.
42 | // All implementations must embed UnimplementedFooAPIServer
43 | // for forward compatibility
44 | type FooAPIServer interface {
45 | Hello(context.Context, *ExampleRequest) (*ExampleResponse, error)
46 | mustEmbedUnimplementedFooAPIServer()
47 | }
48 |
49 | // UnimplementedFooAPIServer must be embedded to have forward compatible implementations.
50 | type UnimplementedFooAPIServer struct {
51 | }
52 |
53 | func (UnimplementedFooAPIServer) Hello(context.Context, *ExampleRequest) (*ExampleResponse, error) {
54 | return nil, status.Errorf(codes.Unimplemented, "method Hello not implemented")
55 | }
56 | func (UnimplementedFooAPIServer) mustEmbedUnimplementedFooAPIServer() {}
57 |
58 | // UnsafeFooAPIServer may be embedded to opt out of forward compatibility for this service.
59 | // Use of this interface is not recommended, as added methods to FooAPIServer will
60 | // result in compilation errors.
61 | type UnsafeFooAPIServer interface {
62 | mustEmbedUnimplementedFooAPIServer()
63 | }
64 |
65 | func RegisterFooAPIServer(s grpc.ServiceRegistrar, srv FooAPIServer) {
66 | s.RegisterService(&FooAPI_ServiceDesc, srv)
67 | }
68 |
69 | func _FooAPI_Hello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
70 | in := new(ExampleRequest)
71 | if err := dec(in); err != nil {
72 | return nil, err
73 | }
74 | if interceptor == nil {
75 | return srv.(FooAPIServer).Hello(ctx, in)
76 | }
77 | info := &grpc.UnaryServerInfo{
78 | Server: srv,
79 | FullMethod: "/example.FooAPI/Hello",
80 | }
81 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
82 | return srv.(FooAPIServer).Hello(ctx, req.(*ExampleRequest))
83 | }
84 | return interceptor(ctx, in, info, handler)
85 | }
86 |
87 | // FooAPI_ServiceDesc is the grpc.ServiceDesc for FooAPI service.
88 | // It's only intended for direct use with grpc.RegisterService,
89 | // and not to be introspected or modified (even as a copy)
90 | var FooAPI_ServiceDesc = grpc.ServiceDesc{
91 | ServiceName: "example.FooAPI",
92 | HandlerType: (*FooAPIServer)(nil),
93 | Methods: []grpc.MethodDesc{
94 | {
95 | MethodName: "Hello",
96 | Handler: _FooAPI_Hello_Handler,
97 | },
98 | },
99 | Streams: []grpc.StreamDesc{},
100 | Metadata: "api.proto",
101 | }
102 |
103 | // BarAPIClient is the client API for BarAPI service.
104 | //
105 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
106 | type BarAPIClient interface {
107 | ListBars(ctx context.Context, in *BarRequest, opts ...grpc.CallOption) (*BarResponse, error)
108 | }
109 |
110 | type barAPIClient struct {
111 | cc grpc.ClientConnInterface
112 | }
113 |
114 | func NewBarAPIClient(cc grpc.ClientConnInterface) BarAPIClient {
115 | return &barAPIClient{cc}
116 | }
117 |
118 | func (c *barAPIClient) ListBars(ctx context.Context, in *BarRequest, opts ...grpc.CallOption) (*BarResponse, error) {
119 | out := new(BarResponse)
120 | err := c.cc.Invoke(ctx, "/example.BarAPI/ListBars", in, out, opts...)
121 | if err != nil {
122 | return nil, err
123 | }
124 | return out, nil
125 | }
126 |
127 | // BarAPIServer is the server API for BarAPI service.
128 | // All implementations must embed UnimplementedBarAPIServer
129 | // for forward compatibility
130 | type BarAPIServer interface {
131 | ListBars(context.Context, *BarRequest) (*BarResponse, error)
132 | mustEmbedUnimplementedBarAPIServer()
133 | }
134 |
135 | // UnimplementedBarAPIServer must be embedded to have forward compatible implementations.
136 | type UnimplementedBarAPIServer struct {
137 | }
138 |
139 | func (UnimplementedBarAPIServer) ListBars(context.Context, *BarRequest) (*BarResponse, error) {
140 | return nil, status.Errorf(codes.Unimplemented, "method ListBars not implemented")
141 | }
142 | func (UnimplementedBarAPIServer) mustEmbedUnimplementedBarAPIServer() {}
143 |
144 | // UnsafeBarAPIServer may be embedded to opt out of forward compatibility for this service.
145 | // Use of this interface is not recommended, as added methods to BarAPIServer will
146 | // result in compilation errors.
147 | type UnsafeBarAPIServer interface {
148 | mustEmbedUnimplementedBarAPIServer()
149 | }
150 |
151 | func RegisterBarAPIServer(s grpc.ServiceRegistrar, srv BarAPIServer) {
152 | s.RegisterService(&BarAPI_ServiceDesc, srv)
153 | }
154 |
155 | func _BarAPI_ListBars_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
156 | in := new(BarRequest)
157 | if err := dec(in); err != nil {
158 | return nil, err
159 | }
160 | if interceptor == nil {
161 | return srv.(BarAPIServer).ListBars(ctx, in)
162 | }
163 | info := &grpc.UnaryServerInfo{
164 | Server: srv,
165 | FullMethod: "/example.BarAPI/ListBars",
166 | }
167 | handler := func(ctx context.Context, req interface{}) (interface{}, error) {
168 | return srv.(BarAPIServer).ListBars(ctx, req.(*BarRequest))
169 | }
170 | return interceptor(ctx, in, info, handler)
171 | }
172 |
173 | // BarAPI_ServiceDesc is the grpc.ServiceDesc for BarAPI service.
174 | // It's only intended for direct use with grpc.RegisterService,
175 | // and not to be introspected or modified (even as a copy)
176 | var BarAPI_ServiceDesc = grpc.ServiceDesc{
177 | ServiceName: "example.BarAPI",
178 | HandlerType: (*BarAPIServer)(nil),
179 | Methods: []grpc.MethodDesc{
180 | {
181 | MethodName: "ListBars",
182 | Handler: _BarAPI_ListBars_Handler,
183 | },
184 | },
185 | Streams: []grpc.StreamDesc{},
186 | Metadata: "api.proto",
187 | }
188 |
--------------------------------------------------------------------------------
/command.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "strings"
9 |
10 | "github.com/joshcarp/grpctl/internal/grpc"
11 | "google.golang.org/grpc/metadata"
12 |
13 | "google.golang.org/genproto/googleapis/api/annotations"
14 | "google.golang.org/protobuf/proto"
15 |
16 | "github.com/joshcarp/grpctl/internal/descriptors"
17 | "github.com/spf13/cobra"
18 | "google.golang.org/protobuf/reflect/protoreflect"
19 | )
20 |
21 | // CommandOption are options to customize the grpctl cobra command.
22 | type CommandOption func(*cobra.Command) error
23 |
24 | func WithStdin(stdin io.Reader) func(cmd *cobra.Command) error {
25 | return func(cmd *cobra.Command) error {
26 | cmd.SetIn(stdin)
27 | return nil
28 | }
29 | }
30 |
31 | // BuildCommand builds a grpctl command from a list of GrpctlOption.
32 | func BuildCommand(cmd *cobra.Command, opts ...CommandOption) error {
33 | for _, f := range opts {
34 | err := f(cmd)
35 | if err != nil {
36 | return err
37 | }
38 | }
39 | return nil
40 | }
41 |
42 | func persistentFlags(cmd *cobra.Command, defaultHosts ...string) error {
43 | var addr, cfgFile, defaultHost, protocol string
44 | var http1enabled bool
45 | cmd.PersistentFlags().BoolVar(&http1enabled, "http1", false, "use http1.1 instead of http2")
46 | cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "grpc", "protocol to use: [connect, grpc, grpcweb]")
47 | err := cmd.RegisterFlagCompletionFunc("protocol", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
48 | return []string{"grpc", "connect", "grpcweb"}, cobra.ShellCompDirectiveNoFileComp
49 | })
50 | if err != nil {
51 | return err
52 | }
53 | if len(defaultHosts) > 0 {
54 | defaultHost = defaultHosts[0]
55 | }
56 | cmd.PersistentFlags().StringVarP(&addr, "address", "a", defaultHost, "Address in form 'scheme://host:port'")
57 | if len(defaultHosts) > 0 {
58 | err = cmd.RegisterFlagCompletionFunc("address", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
59 | return defaultHosts, cobra.ShellCompDirectiveNoFileComp
60 | })
61 | }
62 |
63 | if err != nil {
64 | return err
65 | }
66 | cmd.PersistentFlags().StringArrayP("header", "H", []string{}, "Header in form 'key: value'")
67 | err = cmd.RegisterFlagCompletionFunc("header", cobra.NoFileCompletions)
68 | if err != nil {
69 | return err
70 | }
71 | cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "Config file (default is $HOME/.grpctl.yaml)")
72 | cmd.PersistentFlags().Lookup("config").Hidden = true
73 | return nil
74 | }
75 |
76 | // CommandFromFileDescriptors adds commands to cmd from FileDescriptors.
77 | func CommandFromFileDescriptors(cmd *cobra.Command, descriptors ...protoreflect.FileDescriptor) error {
78 | for _, desc := range descriptors {
79 | err := CommandFromFileDescriptor(cmd, desc)
80 | if err != nil {
81 | return err
82 | }
83 | }
84 | return nil
85 | }
86 |
87 | // CommandFromFileDescriptor adds commands to cmd from a single FileDescriptor.
88 | func CommandFromFileDescriptor(cmd *cobra.Command, methods protoreflect.FileDescriptor) error {
89 | seen := map[string]bool{}
90 | for _, service := range descriptors.ServicesFromFileDescriptor(methods) {
91 | command := descriptors.Command(service)
92 | if seen[command] {
93 | return fmt.Errorf("duplicate service name: %s in %s", command, methods.Name())
94 | }
95 | seen[command] = true
96 | err := CommandFromServiceDescriptor(cmd, service)
97 | if err != nil {
98 | return err
99 | }
100 | }
101 | return nil
102 | }
103 |
104 | // CommandFromServiceDescriptor adds commands to cmd from a ServiceDescriptor.
105 | // Commands added through this will have two levels: the ServiceDescriptor name as level 1 commands
106 | // And the MethodDescriptors as level 2 commands.
107 | func CommandFromServiceDescriptor(cmd *cobra.Command, service protoreflect.ServiceDescriptor) error {
108 | command := descriptors.Command(service)
109 | serviceCmd := cobra.Command{
110 | Use: command,
111 | Short: fmt.Sprintf("%s as defined in %s", command, service.ParentFile().Path()),
112 | }
113 | for _, method := range descriptors.MethodsFromServiceDescriptor(service) {
114 | err := CommandFromMethodDescriptor(&serviceCmd, method)
115 | if err != nil {
116 | return err
117 | }
118 | }
119 | cmd.AddCommand(&serviceCmd)
120 | defaulthost := proto.GetExtension(service.Options(), annotations.E_DefaultHost)
121 | serviceCmd.Parent().ResetFlags()
122 | if defaulthost != "" {
123 | return persistentFlags(serviceCmd.Parent(), fmt.Sprintf("%v:443", defaulthost))
124 | }
125 | return persistentFlags(serviceCmd.Parent())
126 | }
127 |
128 | func endpointType(method protoreflect.MethodDescriptor) string {
129 | if method.IsStreamingClient() && method.IsStreamingServer() {
130 | return "Bidirectional Streaming"
131 | }
132 | if method.IsStreamingServer() {
133 | return "Server Streaming"
134 | }
135 | if method.IsStreamingClient() {
136 | return "Client Streaming"
137 | }
138 | return "Unary"
139 | }
140 |
141 | // CommandFromMethodDescriptor adds commands to cmd from a MethodDescriptor.
142 | // Commands added through this will have one level from the MethodDescriptors name.
143 | func CommandFromMethodDescriptor(cmd *cobra.Command, method protoreflect.MethodDescriptor) error {
144 | dataMap := make(descriptors.DataMap)
145 | for fieldNum := 0; fieldNum < method.Input().Fields().Len(); fieldNum++ {
146 | field := method.Input().Fields().Get(fieldNum)
147 | jsonName := field.JSONName()
148 | field.Default()
149 | field.Kind()
150 | dataMap[jsonName] = &descriptors.DataValue{Kind: field.Kind(), Value: field.Default().Interface(), Proto: true}
151 | }
152 | var inputData, data string
153 | methodCmdName := descriptors.Command(method)
154 | methodCmd := cobra.Command{
155 | Use: methodCmdName,
156 | Short: fmt.Sprintf("%s (%s) as defined in %s", methodCmdName, endpointType(method), method.ParentFile().Path()),
157 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
158 | cmd.Root().SetContext(context.WithValue(cmd.Root().Context(), methodDescriptorKey{}, method))
159 | return recusiveParentPreRun(cmd.Parent(), args)
160 | },
161 | RunE: func(cmd *cobra.Command, args []string) error {
162 | protocol, err := cmd.Flags().GetString("protocol")
163 | if err != nil {
164 | return err
165 | }
166 | http1, err := cmd.Flags().GetBool("http1")
167 | if err != nil {
168 | return err
169 | }
170 | headers, err := cmd.Flags().GetStringArray("header")
171 | if err != nil {
172 | return err
173 | }
174 | addr, err := cmd.Flags().GetString("address")
175 | if err != nil {
176 | return err
177 | }
178 | if addr == "" {
179 | return nil
180 | }
181 | for _, header := range headers {
182 | keyval := strings.Split(header, ":")
183 | if len(keyval) != 2 {
184 | return fmt.Errorf("headers need to be in form -H=Foo:Bar")
185 | }
186 | cmd.Root().SetContext(metadata.AppendToOutgoingContext(cmd.Root().Context(), keyval[0], strings.TrimLeft(keyval[1], " ")))
187 | }
188 | if err != nil {
189 | return err
190 | }
191 | switch data {
192 | case "":
193 | b, err := dataMap.ToJSON()
194 | if err != nil {
195 | return err
196 | }
197 | inputData = string(b)
198 | default:
199 | inputData = data
200 | }
201 | if method.IsStreamingClient() || method.IsStreamingServer() {
202 | return handleStreaming(cmd, method, addr, protocol, http1)
203 | }
204 | return handleUnary(cmd, addr, method, inputData, protocol, http1)
205 | },
206 | }
207 | methodCmd.Flags().StringVar(&data, "json-data", "", "JSON data input that will be used as a request")
208 | defaults, templ := descriptors.MakeJSONTemplate(method.Input())
209 | err := methodCmd.RegisterFlagCompletionFunc("json-data", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
210 | return []string{templ}, cobra.ShellCompDirectiveDefault
211 | })
212 | if err != nil {
213 | return err
214 | }
215 | for key, val := range dataMap {
216 | methodCmd.Flags().Var(val, key, "")
217 | err := methodCmd.RegisterFlagCompletionFunc(key, func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
218 | return []string{fmt.Sprintf("%v", defaults[key])}, cobra.ShellCompDirectiveDefault
219 | })
220 | if err != nil {
221 | return err
222 | }
223 | }
224 | methodCmd.ValidArgsFunction = cobra.NoFileCompletions
225 | cmd.AddCommand(&methodCmd)
226 | return nil
227 | }
228 |
229 | func handleUnary(cmd *cobra.Command, addr string, method protoreflect.MethodDescriptor, inputData string, protocol string, http1 bool) error {
230 | marshallerm, err := grpc.CallUnary(cmd.Root().Context(), addr, method, []byte(inputData), protocol, http1)
231 | if err != nil {
232 | return err
233 | }
234 | res := string(marshallerm)
235 | if err != nil {
236 | _, err = cmd.OutOrStderr().Write([]byte(err.Error()))
237 | if err != nil {
238 | return err
239 | }
240 | return err
241 | }
242 | _, err = cmd.OutOrStdout().Write([]byte(res))
243 | return err
244 | }
245 |
246 | func handleStreaming(cmd *cobra.Command, method protoreflect.MethodDescriptor, addr, protocol string, http1 bool) (err error) {
247 | inputJSON, outputJSON := make(chan []byte), make(chan []byte)
248 | go func() {
249 | reterr := grpc.CallStreaming(cmd.Root().Context(), addr, method, protocol, http1, inputJSON, outputJSON)
250 | if reterr != nil {
251 | err = reterr
252 | return
253 | }
254 | }()
255 | b, err := io.ReadAll(cmd.InOrStdin())
256 | if err != nil {
257 | return err
258 | }
259 | msgArr := make([]map[string]any, 0)
260 | if err := json.Unmarshal(b, &msgArr); err != nil {
261 | return err
262 | }
263 | for _, msg := range msgArr {
264 | byteMsg, err := json.Marshal(msg)
265 | if err != nil {
266 | return err
267 | }
268 | inputJSON <- byteMsg
269 | }
270 | close(inputJSON)
271 | for marshallerm := range outputJSON {
272 | res := string(marshallerm)
273 | if err != nil {
274 | _, err = cmd.OutOrStderr().Write([]byte(err.Error()))
275 | if err != nil {
276 | return err
277 | }
278 | return err
279 | }
280 | _, err = cmd.OutOrStdout().Write([]byte(res))
281 | if err != nil {
282 | return err
283 | }
284 | }
285 | return nil
286 | }
287 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2021 Joshua Carpeggiani
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/command_test.go:
--------------------------------------------------------------------------------
1 | package grpctl
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "runtime"
8 | "testing"
9 |
10 | "google.golang.org/genproto/googleapis/api/annotations"
11 | "google.golang.org/protobuf/proto"
12 | "google.golang.org/protobuf/reflect/protoreflect"
13 |
14 | "google.golang.org/grpc/metadata"
15 |
16 | "github.com/joshcarp/grpctl/internal/testing/pkg/example"
17 | "github.com/joshcarp/grpctl/internal/testing/proto/examplepb"
18 | "github.com/stretchr/testify/require"
19 | "google.golang.org/grpc"
20 |
21 | "github.com/spf13/cobra"
22 | )
23 |
24 | func TestBuildCommand(t *testing.T) {
25 | t.Parallel()
26 | port, err := example.ServeRand(
27 | context.Background(),
28 | func(server *grpc.Server) {
29 | examplepb.RegisterFooAPIServer(server, &example.FooServer{})
30 | })
31 | require.NoError(t, err)
32 | addr := fmt.Sprintf("localhost:%d", port)
33 | tests := []struct {
34 | name string
35 | args []string
36 | want string
37 | opts func([]string) []CommandOption
38 | json string
39 | wantErr bool
40 | }{
41 | {
42 | name: "basic",
43 | args: []string{
44 | "grpctl",
45 | "--address=http://" + addr,
46 | "FooAPI",
47 | "Hello",
48 | "--message",
49 | "blah",
50 | },
51 | opts: func(args []string) []CommandOption {
52 | return []CommandOption{
53 | WithArgs(args),
54 | WithReflection(args),
55 | }
56 | },
57 | json: fmt.Sprintf("{\n \"message\": \"Incoming Message: blah \\n "+
58 | "Metadata: map[:authority:[%s] accept-encoding:[identity] "+
59 | "content-type:[application/grpc+proto] grpc-accept-encoding:[gzip] "+
60 | "user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
61 | },
62 | {
63 | name: "completion_enabled",
64 | args: []string{
65 | "root",
66 | },
67 | opts: func(args []string) []CommandOption {
68 | return []CommandOption{
69 | WithArgs(args),
70 | WithReflection(args),
71 | WithCompletion(),
72 | }
73 | },
74 | want: `Usage:
75 | root [command]
76 |
77 | Available Commands:
78 | completion Generate completion script
79 | help Help about any command
80 |
81 | Flags:
82 | -a, --address string Address in form 'scheme://host:port'
83 | -H, --header stringArray Header in form 'key: value'
84 | -h, --help help for root
85 | --http1 use http1.1 instead of http2
86 | -p, --protocol string protocol to use: [connect, grpc, grpcweb] (default "grpc")
87 |
88 | Use "root [command] --help" for more information about a command.
89 | `,
90 | },
91 | {
92 | name: "__complete_empty_string",
93 | args: []string{"grpctl", "__complete", "--address=http://" + addr, ""},
94 | opts: func(args []string) []CommandOption {
95 | return []CommandOption{
96 | WithArgs(args),
97 | WithReflection(args),
98 | }
99 | },
100 | want: `BarAPI BarAPI as defined in api.proto
101 | FooAPI FooAPI as defined in api.proto
102 | ServerReflection ServerReflection as defined in reflection/grpc_reflection_v1alpha/reflection.proto
103 | completion Generate the autocompletion script for the specified shell
104 | help Help about any command
105 | :4
106 | `,
107 | },
108 | {
109 | name: "__complete_empty",
110 | args: []string{"grpctl", "__complete", "--address=http://" + addr},
111 | opts: func(args []string) []CommandOption {
112 | return []CommandOption{
113 | WithArgs(args),
114 | WithReflection(args),
115 | }
116 | },
117 | want: `:0
118 | `,
119 | },
120 | {
121 | name: "__complete_BarAPI",
122 | args: []string{"grpctl", "__complete", "--address=http://" + addr, "BarAPI", ""},
123 | opts: func(args []string) []CommandOption {
124 | return []CommandOption{
125 | WithArgs(args),
126 | WithReflection(args),
127 | }
128 | },
129 | want: `ListBars ListBars (Unary) as defined in api.proto
130 | :4
131 | `,
132 | },
133 | {
134 | name: "header",
135 | args: []string{
136 | "grpctl",
137 | "--address=http://" + addr,
138 | "-H=Foo:Bar",
139 | "FooAPI",
140 | "Hello",
141 | "--message",
142 | "blah",
143 | },
144 | opts: func(args []string) []CommandOption {
145 | return []CommandOption{
146 | WithArgs(args),
147 | WithReflection(args),
148 | }
149 | },
150 | json: fmt.Sprintf("{\n \"message\": \"Incoming Message: blah \\n "+
151 | "Metadata: map[:authority:[%s] accept-encoding:[identity] "+
152 | "content-type:[application/grpc+proto] foo:[Bar] grpc-accept-encoding:[gzip] "+
153 | "user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
154 | },
155 | {
156 | name: "headers",
157 | args: []string{
158 | "grpctl",
159 | "--address=http://" + addr,
160 | "-H=Foo:Bar",
161 | "-H=Foo2:Bar2",
162 | "FooAPI",
163 | "Hello",
164 | "--message",
165 | "blah",
166 | },
167 | opts: func(args []string) []CommandOption {
168 | return []CommandOption{
169 | WithArgs(args),
170 | WithReflection(args),
171 | }
172 | },
173 | json: fmt.Sprintf("{\n \"message\": \"Incoming Message: blah \\n "+
174 | "Metadata: map[:authority:[%s] accept-encoding:[identity] content-type:[application/grpc+proto] "+
175 | "foo:[Bar] foo2:[Bar2] grpc-accept-encoding:[gzip] "+
176 | "user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
177 | },
178 | {
179 | name: "WithContextFunc-No-Change",
180 | args: []string{
181 | "grpctl",
182 | "--address=http://" + addr,
183 | "-H=Foo:Bar",
184 | "-H=Foo2:Bar2",
185 | "FooAPI",
186 | "Hello",
187 | "--message",
188 | "blah",
189 | },
190 | opts: func(args []string) []CommandOption {
191 | return []CommandOption{
192 | WithContextFunc(func(ctx context.Context, cmd *cobra.Command) (context.Context, error) {
193 | return ctx, nil
194 | }),
195 | WithArgs(args),
196 | WithReflection(args),
197 | }
198 | },
199 | json: fmt.Sprintf("{\n \"message\": \"Incoming Message: blah \\n "+
200 | "Metadata: map[:authority:[%s] accept-encoding:[identity] content-type:[application/grpc+proto] "+
201 | "foo:[Bar] foo2:[Bar2] grpc-accept-encoding:[gzip] "+
202 | "user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
203 | },
204 | {
205 | name: "WithContextFunc-No-Change",
206 | args: []string{
207 | "grpctl",
208 | "--address=http://" + addr,
209 | "-H=Foo:Bar",
210 | "-H=Foo2:Bar2",
211 | "FooAPI",
212 | "Hello",
213 | "--message",
214 | "blah",
215 | },
216 | opts: func(args []string) []CommandOption {
217 | return []CommandOption{
218 | WithContextFunc(func(ctx context.Context, cmd *cobra.Command) (context.Context, error) {
219 | return ctx, nil
220 | }),
221 | WithArgs(args),
222 | WithReflection(args),
223 | }
224 | },
225 | json: fmt.Sprintf("{\n \"message\": \"Incoming Message: blah \\n "+
226 | "Metadata: map[:authority:[%s] accept-encoding:[identity] content-type:[application/grpc+proto] "+
227 | "foo:[Bar] foo2:[Bar2] grpc-accept-encoding:[gzip] "+
228 | "user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
229 | },
230 | {
231 | name: "WithContextFunc",
232 | args: []string{
233 | "grpctl",
234 | "--address=http://" + addr,
235 | "-H=Foo:Bar",
236 | "-H=Foo2:Bar2",
237 | "FooAPI",
238 | "Hello",
239 | "--message",
240 | "blah",
241 | },
242 | opts: func(args []string) []CommandOption {
243 | return []CommandOption{
244 | WithContextFunc(func(ctx context.Context, _ *cobra.Command) (context.Context, error) {
245 | return metadata.AppendToOutgoingContext(ctx, "fookey", "fooval"), nil
246 | }),
247 | WithArgs(args),
248 | WithReflection(args),
249 | }
250 | },
251 | json: fmt.Sprintf("{\n \"message\": \"Incoming Message: blah \\n "+
252 | "Metadata: map[:authority:[%s] accept-encoding:[identity] content-type:[application/grpc+proto] "+
253 | "foo:[Bar] foo2:[Bar2] fookey:[fooval] grpc-accept-encoding:[gzip] "+
254 | "user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
255 | },
256 | {
257 | name: "WithDescriptorContextFuncSimple",
258 | args: []string{
259 | "grpctl",
260 | "--address=http://" + addr,
261 | "-H=Foo:Bar",
262 | "-H=Foo2:Bar2",
263 | "FooAPI",
264 | "Hello",
265 | "--message",
266 | "blah",
267 | },
268 | opts: func(args []string) []CommandOption {
269 | return []CommandOption{
270 | WithContextDescriptorFunc(func(ctx context.Context, _ *cobra.Command, _ protoreflect.MethodDescriptor) (context.Context, error) {
271 | return metadata.AppendToOutgoingContext(ctx, "fookey", "fooval"), nil
272 | }),
273 | WithArgs(args),
274 | WithReflection(args),
275 | }
276 | },
277 | json: fmt.Sprintf("{\n \"message\": \"Incoming Message: "+
278 | "blah \\n Metadata: map[:authority:[%s] accept-encoding:[identity] "+
279 | "content-type:[application/grpc+proto] foo:[Bar] foo2:[Bar2] fookey:[fooval] "+
280 | "grpc-accept-encoding:[gzip] user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
281 | },
282 | {
283 | name: "WithDescriptorContextFuncMethodDescriptorsUsed",
284 | args: []string{
285 | "grpctl",
286 | "--address=http://" + addr,
287 | "-H=Foo:Bar",
288 | "-H=Foo2:Bar2",
289 | "FooAPI",
290 | "Hello",
291 | "--message",
292 | "blah",
293 | },
294 | opts: func(args []string) []CommandOption {
295 | return []CommandOption{
296 | WithContextDescriptorFunc(func(ctx context.Context, _ *cobra.Command, descriptor protoreflect.MethodDescriptor) (context.Context, error) {
297 | serviceDesc := descriptor.Parent()
298 | service, ok := serviceDesc.(protoreflect.ServiceDescriptor)
299 | require.True(t, ok)
300 | b := proto.GetExtension(service.Options(), annotations.E_DefaultHost)
301 | bstr, _ := b.(string)
302 | return metadata.AppendToOutgoingContext(ctx, "fookey", bstr), nil
303 | }),
304 | WithArgs(args),
305 | WithReflection(args),
306 | }
307 | },
308 | json: fmt.Sprintf(
309 | "{\n \"message\": \"Incoming Message: "+
310 | "blah \\n Metadata: map[:authority:[%s] "+
311 | "accept-encoding:[identity] content-type:[application/grpc+proto] "+
312 | "foo:[Bar] foo2:[Bar2] fookey:[] grpc-accept-encoding:[gzip] "+
313 | "user-agent:[grpc-go-connect/1.1.0 (%s)]]\"\n}", addr, runtime.Version()),
314 | },
315 | }
316 | for _, tt := range tests {
317 | tt := tt
318 | t.Run(tt.name, func(t *testing.T) {
319 | t.Parallel()
320 | cmd := &cobra.Command{
321 | Use: "root",
322 | }
323 | var b bytes.Buffer
324 | cmd.SetOut(&b)
325 | if err := BuildCommand(cmd, tt.opts(tt.args)...); (err != nil) != tt.wantErr {
326 | t.Errorf("ExecuteReflect() error = %v, wantErr %v", err, tt.wantErr)
327 | }
328 | if err := cmd.ExecuteContext(context.Background()); err != nil {
329 | t.Errorf("ExecuteReflect() error = %v, wantErr %v", err, tt.wantErr)
330 | }
331 | bs := b.String()
332 | if tt.json != "" {
333 | require.JSONEq(t, tt.json, bs)
334 | return
335 | }
336 | require.Equal(t, tt.want, bs)
337 | })
338 | }
339 | }
340 |
341 | func TestRunCommand(t *testing.T) {
342 | t.Parallel()
343 | type contextkey struct{}
344 | tests := []struct {
345 | name string
346 | args *cobra.Command
347 | wantErr bool
348 | }{
349 | {
350 | name: "",
351 | args: &cobra.Command{
352 | Use: "foobar",
353 | PreRunE: func(cmd *cobra.Command, args []string) error {
354 | cmd.Root().SetContext(context.WithValue(cmd.Root().Context(), contextkey{}, "bar"))
355 | return nil
356 | },
357 | RunE: func(cmd *cobra.Command, args []string) error {
358 | require.Equal(t, "bar", cmd.Root().Context().Value(contextkey{}))
359 | return nil
360 | },
361 | },
362 | wantErr: false,
363 | },
364 | }
365 | for _, tt := range tests {
366 | tt := tt
367 | t.Run(tt.name, func(t *testing.T) {
368 | t.Parallel()
369 | err := tt.args.ExecuteContext(context.Background())
370 | require.NoError(t, err)
371 | })
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/internal/testing/proto/examplepb/api.pb.go:
--------------------------------------------------------------------------------
1 | // Code generated by protoc-gen-go. DO NOT EDIT.
2 | // versions:
3 | // protoc-gen-go v1.27.1
4 | // protoc v3.17.3
5 | // source: api.proto
6 |
7 | package examplepb
8 |
9 | import (
10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect"
11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl"
12 | reflect "reflect"
13 | sync "sync"
14 | )
15 |
16 | const (
17 | // Verify that this generated code is sufficiently up-to-date.
18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
19 | // Verify that runtime/protoimpl is sufficiently up-to-date.
20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
21 | )
22 |
23 | type BarRequest struct {
24 | state protoimpl.MessageState
25 | sizeCache protoimpl.SizeCache
26 | unknownFields protoimpl.UnknownFields
27 |
28 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
29 | Foos []string `protobuf:"bytes,2,rep,name=foos,proto3" json:"foos,omitempty"`
30 | }
31 |
32 | func (x *BarRequest) Reset() {
33 | *x = BarRequest{}
34 | if protoimpl.UnsafeEnabled {
35 | mi := &file_api_proto_msgTypes[0]
36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
37 | ms.StoreMessageInfo(mi)
38 | }
39 | }
40 |
41 | func (x *BarRequest) String() string {
42 | return protoimpl.X.MessageStringOf(x)
43 | }
44 |
45 | func (*BarRequest) ProtoMessage() {}
46 |
47 | func (x *BarRequest) ProtoReflect() protoreflect.Message {
48 | mi := &file_api_proto_msgTypes[0]
49 | if protoimpl.UnsafeEnabled && x != nil {
50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
51 | if ms.LoadMessageInfo() == nil {
52 | ms.StoreMessageInfo(mi)
53 | }
54 | return ms
55 | }
56 | return mi.MessageOf(x)
57 | }
58 |
59 | // Deprecated: Use BarRequest.ProtoReflect.Descriptor instead.
60 | func (*BarRequest) Descriptor() ([]byte, []int) {
61 | return file_api_proto_rawDescGZIP(), []int{0}
62 | }
63 |
64 | func (x *BarRequest) GetMessage() string {
65 | if x != nil {
66 | return x.Message
67 | }
68 | return ""
69 | }
70 |
71 | func (x *BarRequest) GetFoos() []string {
72 | if x != nil {
73 | return x.Foos
74 | }
75 | return nil
76 | }
77 |
78 | type BarResponse struct {
79 | state protoimpl.MessageState
80 | sizeCache protoimpl.SizeCache
81 | unknownFields protoimpl.UnknownFields
82 |
83 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
84 | }
85 |
86 | func (x *BarResponse) Reset() {
87 | *x = BarResponse{}
88 | if protoimpl.UnsafeEnabled {
89 | mi := &file_api_proto_msgTypes[1]
90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
91 | ms.StoreMessageInfo(mi)
92 | }
93 | }
94 |
95 | func (x *BarResponse) String() string {
96 | return protoimpl.X.MessageStringOf(x)
97 | }
98 |
99 | func (*BarResponse) ProtoMessage() {}
100 |
101 | func (x *BarResponse) ProtoReflect() protoreflect.Message {
102 | mi := &file_api_proto_msgTypes[1]
103 | if protoimpl.UnsafeEnabled && x != nil {
104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
105 | if ms.LoadMessageInfo() == nil {
106 | ms.StoreMessageInfo(mi)
107 | }
108 | return ms
109 | }
110 | return mi.MessageOf(x)
111 | }
112 |
113 | // Deprecated: Use BarResponse.ProtoReflect.Descriptor instead.
114 | func (*BarResponse) Descriptor() ([]byte, []int) {
115 | return file_api_proto_rawDescGZIP(), []int{1}
116 | }
117 |
118 | func (x *BarResponse) GetMessage() string {
119 | if x != nil {
120 | return x.Message
121 | }
122 | return ""
123 | }
124 |
125 | type ExampleRequest struct {
126 | state protoimpl.MessageState
127 | sizeCache protoimpl.SizeCache
128 | unknownFields protoimpl.UnknownFields
129 |
130 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
131 | }
132 |
133 | func (x *ExampleRequest) Reset() {
134 | *x = ExampleRequest{}
135 | if protoimpl.UnsafeEnabled {
136 | mi := &file_api_proto_msgTypes[2]
137 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
138 | ms.StoreMessageInfo(mi)
139 | }
140 | }
141 |
142 | func (x *ExampleRequest) String() string {
143 | return protoimpl.X.MessageStringOf(x)
144 | }
145 |
146 | func (*ExampleRequest) ProtoMessage() {}
147 |
148 | func (x *ExampleRequest) ProtoReflect() protoreflect.Message {
149 | mi := &file_api_proto_msgTypes[2]
150 | if protoimpl.UnsafeEnabled && x != nil {
151 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
152 | if ms.LoadMessageInfo() == nil {
153 | ms.StoreMessageInfo(mi)
154 | }
155 | return ms
156 | }
157 | return mi.MessageOf(x)
158 | }
159 |
160 | // Deprecated: Use ExampleRequest.ProtoReflect.Descriptor instead.
161 | func (*ExampleRequest) Descriptor() ([]byte, []int) {
162 | return file_api_proto_rawDescGZIP(), []int{2}
163 | }
164 |
165 | func (x *ExampleRequest) GetMessage() string {
166 | if x != nil {
167 | return x.Message
168 | }
169 | return ""
170 | }
171 |
172 | type ExampleResponse struct {
173 | state protoimpl.MessageState
174 | sizeCache protoimpl.SizeCache
175 | unknownFields protoimpl.UnknownFields
176 |
177 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
178 | }
179 |
180 | func (x *ExampleResponse) Reset() {
181 | *x = ExampleResponse{}
182 | if protoimpl.UnsafeEnabled {
183 | mi := &file_api_proto_msgTypes[3]
184 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
185 | ms.StoreMessageInfo(mi)
186 | }
187 | }
188 |
189 | func (x *ExampleResponse) String() string {
190 | return protoimpl.X.MessageStringOf(x)
191 | }
192 |
193 | func (*ExampleResponse) ProtoMessage() {}
194 |
195 | func (x *ExampleResponse) ProtoReflect() protoreflect.Message {
196 | mi := &file_api_proto_msgTypes[3]
197 | if protoimpl.UnsafeEnabled && x != nil {
198 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
199 | if ms.LoadMessageInfo() == nil {
200 | ms.StoreMessageInfo(mi)
201 | }
202 | return ms
203 | }
204 | return mi.MessageOf(x)
205 | }
206 |
207 | // Deprecated: Use ExampleResponse.ProtoReflect.Descriptor instead.
208 | func (*ExampleResponse) Descriptor() ([]byte, []int) {
209 | return file_api_proto_rawDescGZIP(), []int{3}
210 | }
211 |
212 | func (x *ExampleResponse) GetMessage() string {
213 | if x != nil {
214 | return x.Message
215 | }
216 | return ""
217 | }
218 |
219 | var File_api_proto protoreflect.FileDescriptor
220 |
221 | var file_api_proto_rawDesc = []byte{
222 | 0x0a, 0x09, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x65, 0x78, 0x61,
223 | 0x6d, 0x70, 0x6c, 0x65, 0x22, 0x3a, 0x0a, 0x0a, 0x42, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
224 | 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20,
225 | 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04,
226 | 0x66, 0x6f, 0x6f, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x66, 0x6f, 0x6f, 0x73,
227 | 0x22, 0x27, 0x0a, 0x0b, 0x42, 0x61, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
228 | 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
229 | 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2a, 0x0a, 0x0e, 0x65, 0x78, 0x61,
230 | 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d,
231 | 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65,
232 | 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x2b, 0x0a, 0x0f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
233 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73,
234 | 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
235 | 0x67, 0x65, 0x32, 0x44, 0x0a, 0x06, 0x46, 0x6f, 0x6f, 0x41, 0x50, 0x49, 0x12, 0x3a, 0x0a, 0x05,
236 | 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x17, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e,
237 | 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18,
238 | 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
239 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x3f, 0x0a, 0x06, 0x42, 0x61, 0x72, 0x41,
240 | 0x50, 0x49, 0x12, 0x35, 0x0a, 0x08, 0x4c, 0x69, 0x73, 0x74, 0x42, 0x61, 0x72, 0x73, 0x12, 0x13,
241 | 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x42, 0x61, 0x72, 0x52, 0x65, 0x71, 0x75,
242 | 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x42, 0x61,
243 | 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x68, 0x0a, 0x14, 0x63, 0x6f, 0x6d,
244 | 0x2e, 0x6a, 0x6f, 0x73, 0x68, 0x63, 0x61, 0x72, 0x70, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c,
245 | 0x65, 0x42, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x50, 0x01, 0x5a, 0x45, 0x67, 0x69,
246 | 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x6f, 0x73, 0x68, 0x63, 0x61, 0x72,
247 | 0x70, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x74, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
248 | 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f,
249 | 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, 0x3b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c,
250 | 0x65, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
251 | }
252 |
253 | var (
254 | file_api_proto_rawDescOnce sync.Once
255 | file_api_proto_rawDescData = file_api_proto_rawDesc
256 | )
257 |
258 | func file_api_proto_rawDescGZIP() []byte {
259 | file_api_proto_rawDescOnce.Do(func() {
260 | file_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_rawDescData)
261 | })
262 | return file_api_proto_rawDescData
263 | }
264 |
265 | var file_api_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
266 | var file_api_proto_goTypes = []interface{}{
267 | (*BarRequest)(nil), // 0: example.BarRequest
268 | (*BarResponse)(nil), // 1: example.BarResponse
269 | (*ExampleRequest)(nil), // 2: example.exampleRequest
270 | (*ExampleResponse)(nil), // 3: example.exampleResponse
271 | }
272 | var file_api_proto_depIdxs = []int32{
273 | 2, // 0: example.FooAPI.Hello:input_type -> example.exampleRequest
274 | 0, // 1: example.BarAPI.ListBars:input_type -> example.BarRequest
275 | 3, // 2: example.FooAPI.Hello:output_type -> example.exampleResponse
276 | 1, // 3: example.BarAPI.ListBars:output_type -> example.BarResponse
277 | 2, // [2:4] is the sub-list for method output_type
278 | 0, // [0:2] is the sub-list for method input_type
279 | 0, // [0:0] is the sub-list for extension type_name
280 | 0, // [0:0] is the sub-list for extension extendee
281 | 0, // [0:0] is the sub-list for field type_name
282 | }
283 |
284 | func init() { file_api_proto_init() }
285 | func file_api_proto_init() {
286 | if File_api_proto != nil {
287 | return
288 | }
289 | if !protoimpl.UnsafeEnabled {
290 | file_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
291 | switch v := v.(*BarRequest); i {
292 | case 0:
293 | return &v.state
294 | case 1:
295 | return &v.sizeCache
296 | case 2:
297 | return &v.unknownFields
298 | default:
299 | return nil
300 | }
301 | }
302 | file_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
303 | switch v := v.(*BarResponse); i {
304 | case 0:
305 | return &v.state
306 | case 1:
307 | return &v.sizeCache
308 | case 2:
309 | return &v.unknownFields
310 | default:
311 | return nil
312 | }
313 | }
314 | file_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
315 | switch v := v.(*ExampleRequest); i {
316 | case 0:
317 | return &v.state
318 | case 1:
319 | return &v.sizeCache
320 | case 2:
321 | return &v.unknownFields
322 | default:
323 | return nil
324 | }
325 | }
326 | file_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
327 | switch v := v.(*ExampleResponse); i {
328 | case 0:
329 | return &v.state
330 | case 1:
331 | return &v.sizeCache
332 | case 2:
333 | return &v.unknownFields
334 | default:
335 | return nil
336 | }
337 | }
338 | }
339 | type x struct{}
340 | out := protoimpl.TypeBuilder{
341 | File: protoimpl.DescBuilder{
342 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
343 | RawDescriptor: file_api_proto_rawDesc,
344 | NumEnums: 0,
345 | NumMessages: 4,
346 | NumExtensions: 0,
347 | NumServices: 2,
348 | },
349 | GoTypes: file_api_proto_goTypes,
350 | DependencyIndexes: file_api_proto_depIdxs,
351 | MessageInfos: file_api_proto_msgTypes,
352 | }.Build()
353 | File_api_proto = out.File
354 | file_api_proto_rawDesc = nil
355 | file_api_proto_goTypes = nil
356 | file_api_proto_depIdxs = nil
357 | }
358 |
--------------------------------------------------------------------------------
/grpctl.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------