├── .github ├── CODEOWNERS ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── test.yaml ├── .gitignore ├── .go-version ├── CHANGELOG.md ├── LICENSE ├── README.md ├── buf.gen.yaml ├── buf.yaml ├── client.go ├── client_posix_test.go ├── client_test.go ├── client_unix_test.go ├── constants.go ├── discover.go ├── docs ├── README.md ├── extensive-go-plugin-tutorial.md ├── guide-plugin-write-non-go.md └── internals.md ├── error.go ├── error_test.go ├── examples ├── basic │ ├── .gitignore │ ├── README.md │ ├── main.go │ ├── plugin │ │ └── greeter_impl.go │ └── shared │ │ └── greeter_interface.go ├── bidirectional │ ├── README.md │ ├── buf.gen.yaml │ ├── buf.yaml │ ├── main.go │ ├── plugin-go-grpc │ │ └── main.go │ ├── proto │ │ ├── kv.pb.go │ │ ├── kv.proto │ │ └── kv_grpc.pb.go │ └── shared │ │ ├── grpc.go │ │ └── interface.go ├── grpc │ ├── .gitignore │ ├── README.md │ ├── buf.gen.yaml │ ├── buf.yaml │ ├── main.go │ ├── plugin-go-grpc │ │ └── main.go │ ├── plugin-go-netrpc │ │ └── main.go │ ├── plugin-python │ │ ├── .gitignore │ │ ├── plugin.py │ │ ├── proto │ │ │ ├── kv_pb2.py │ │ │ └── kv_pb2_grpc.py │ │ └── requirements.txt │ ├── proto │ │ ├── kv.pb.go │ │ ├── kv.proto │ │ └── kv_grpc.pb.go │ └── shared │ │ ├── grpc.go │ │ ├── interface.go │ │ └── rpc.go └── negotiated │ ├── .gitignore │ ├── README.md │ ├── main.go │ └── plugin-go │ └── main.go ├── go.mod ├── go.sum ├── grpc_broker.go ├── grpc_client.go ├── grpc_client_test.go ├── grpc_controller.go ├── grpc_server.go ├── grpc_stdio.go ├── internal ├── cmdrunner │ ├── addr_translator.go │ ├── cmd_reattach.go │ ├── cmd_runner.go │ ├── cmd_runner_test.go │ ├── notes_unix.go │ ├── notes_windows.go │ ├── process.go │ ├── process_posix.go │ ├── process_windows.go │ └── testdata │ │ ├── .gitignore │ │ ├── Makefile │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ └── minimal.go ├── grpcmux │ ├── blocked_client_listener.go │ ├── blocked_server_listener.go │ ├── grpc_client_muxer.go │ ├── grpc_muxer.go │ └── grpc_server_muxer.go └── plugin │ ├── grpc_broker.pb.go │ ├── grpc_broker.proto │ ├── grpc_broker_grpc.pb.go │ ├── grpc_controller.pb.go │ ├── grpc_controller.proto │ ├── grpc_controller_grpc.pb.go │ ├── grpc_stdio.pb.go │ ├── grpc_stdio.proto │ └── grpc_stdio_grpc.pb.go ├── log_entry.go ├── mtls.go ├── mux_broker.go ├── plugin.go ├── plugin_test.go ├── process.go ├── protocol.go ├── rpc_client.go ├── rpc_client_test.go ├── rpc_server.go ├── runner └── runner.go ├── server.go ├── server_mux.go ├── server_test.go ├── server_unix_test.go ├── stream.go ├── test └── grpc │ ├── test.pb.go │ ├── test.proto │ └── test_grpc.pb.go └── testing.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 3 | 4 | # Default owner 5 | * @hashicorp/team-ip-compliance 6 | 7 | # Add override rules below. Each line is a file/folder pattern followed by one or more owners. 8 | # Being an owner means those groups or individuals will be added as reviewers to PRs affecting 9 | # those areas of the code. 10 | # Examples: 11 | # /docs/ @docs-team 12 | # *.js @js-team 13 | # *.go @go-team -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: 2 5 | 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | day: "sunday" 12 | commit-message: 13 | prefix: "[chore] : " 14 | groups: 15 | actions: 16 | patterns: 17 | - "*" 18 | 19 | - package-ecosystem: "gomod" 20 | directories: 21 | - "/" 22 | - "/internal/cmdrunner/testdata/" 23 | schedule: 24 | interval: "weekly" 25 | day: "sunday" 26 | commit-message: 27 | prefix: "[chore] : " 28 | groups: 29 | go: 30 | patterns: 31 | - "*" 32 | applies-to: "version-updates" 33 | go-security: 34 | patterns: 35 | - "*" 36 | applies-to: "security-updates" -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | <!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 --> 2 | ## Description 3 | 4 | <!-- Provide a summary of what the PR does and why it is being submitted. --> 5 | 6 | ## Related Issue 7 | 8 | <!-- If this PR is linked to any issue, provide the issue number or description here. Any related JIRA tickets can also be added here. --> 9 | 10 | ## How Has This Been Tested? 11 | 12 | <!-- Describe how the changes have been tested. Provide test instructions or details. --> 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - 'README.md' 6 | push: 7 | branches: 8 | - 'main' 9 | paths-ignore: 10 | - 'README.md' 11 | 12 | permissions: 13 | contents: read 14 | 15 | jobs: 16 | go-fmt-and-vet: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | - name: Setup Go 22 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 23 | with: 24 | go-version: '>=1.21' 25 | cache: true 26 | - name: Go formatting 27 | run: | 28 | files=$(go fmt ./...) 29 | if [ -n "$files" ]; then 30 | echo "The following file(s) do not conform to go fmt:" 31 | echo "$files" 32 | exit 1 33 | fi 34 | - name: Lint code 35 | uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 36 | go-test: 37 | needs: go-fmt-and-vet 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Checkout Code 41 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 42 | - name: Setup Go 43 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 44 | with: 45 | go-version: '>=1.21' 46 | cache: true 47 | - name: Run test and generate coverage report 48 | run: | 49 | go test -race ./... -v -coverprofile=coverage.out 50 | - name: Upload Coverage Report 51 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 52 | with: 53 | path: coverage.out 54 | name: Coverage-report 55 | - name: Display Coverage report 56 | run: go tool cover -func=coverage.out 57 | - name: Build Go 58 | run: go build ./... 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.24.1 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.6.2 2 | 3 | ENHANCEMENTS: 4 | 5 | * Added support for gRPC dial options to the `Dial` API [[GH-257](https://github.com/hashicorp/go-plugin/pull/257)] 6 | 7 | BUGS: 8 | 9 | * Fixed a bug where reattaching to a plugin that exits could kill an unrelated process [[GH-320](https://github.com/hashicorp/go-plugin/pull/320)] 10 | 11 | ## v1.6.1 12 | 13 | BUGS: 14 | 15 | * Suppress spurious `os.ErrClosed` on plugin shutdown [[GH-299](https://github.com/hashicorp/go-plugin/pull/299)] 16 | 17 | ENHANCEMENTS: 18 | 19 | * deps: bump google.golang.org/grpc to v1.58.3 [[GH-296](https://github.com/hashicorp/go-plugin/pull/296)] 20 | 21 | ## v1.6.0 22 | 23 | CHANGES: 24 | 25 | * plugin: Plugins written in other languages can optionally start to advertise whether they support gRPC broker multiplexing. 26 | If the environment variable `PLUGIN_MULTIPLEX_GRPC` is set, it is safe to include a seventh field containing a boolean 27 | value in the `|`-separated protocol negotiation line. 28 | 29 | ENHANCEMENTS: 30 | 31 | * Support muxing gRPC broker connections over a single listener [[GH-288](https://github.com/hashicorp/go-plugin/pull/288)] 32 | * client: Configurable buffer size for reading plugin log lines [[GH-265](https://github.com/hashicorp/go-plugin/pull/265)] 33 | * Use `buf` for proto generation [[GH-286](https://github.com/hashicorp/go-plugin/pull/286)] 34 | * deps: bump golang.org/x/net to v0.17.0 [[GH-285](https://github.com/hashicorp/go-plugin/pull/285)] 35 | * deps: bump golang.org/x/sys to v0.13.0 [[GH-285](https://github.com/hashicorp/go-plugin/pull/285)] 36 | * deps: bump golang.org/x/text to v0.13.0 [[GH-285](https://github.com/hashicorp/go-plugin/pull/285)] 37 | 38 | ## v1.5.2 39 | 40 | ENHANCEMENTS: 41 | 42 | client: New `UnixSocketConfig.TempDir` option allows setting the directory to use when creating plugin-specific Unix socket directories [[GH-282](https://github.com/hashicorp/go-plugin/pull/282)] 43 | 44 | ## v1.5.1 45 | 46 | BUGS: 47 | 48 | * server: `PLUGIN_UNIX_SOCKET_DIR` is consistently used for gRPC broker sockets as well as the initial socket [[GH-277](https://github.com/hashicorp/go-plugin/pull/277)] 49 | 50 | ENHANCEMENTS: 51 | 52 | * client: New `UnixSocketConfig` option in `ClientConfig` to support making the client's Unix sockets group-writable [[GH-277](https://github.com/hashicorp/go-plugin/pull/277)] 53 | 54 | ## v1.5.0 55 | 56 | ENHANCEMENTS: 57 | 58 | * client: New `runner.Runner` interface to support clients providing custom plugin command runner implementations [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)] 59 | * Accessible via new `ClientConfig` field `RunnerFunc`, which is mutually exclusive with `Cmd` and `Reattach` 60 | * Reattaching support via `ReattachConfig` field `ReattachFunc` 61 | * client: New `ClientConfig` field `SkipHostEnv` allows omitting the client process' own environment variables from the plugin command's environment [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)] 62 | * client: Add `ID()` method to `Client` for retrieving the pid or other unique ID of a running plugin [[GH-272](https://github.com/hashicorp/go-plugin/pull/272)] 63 | * server: Support setting the directory to create Unix sockets in with the env var `PLUGIN_UNIX_SOCKET_DIR` [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)] 64 | * server: Support setting group write permission and a custom group name or gid owner with the env var `PLUGIN_UNIX_SOCKET_GROUP` [[GH-270](https://github.com/hashicorp/go-plugin/pull/270)] 65 | 66 | ## v1.4.11-rc1 67 | 68 | ENHANCEMENTS: 69 | 70 | * deps: bump protoreflect to v1.15.1 [[GH-264](https://github.com/hashicorp/go-plugin/pull/264)] 71 | 72 | ## v1.4.10 73 | 74 | BUG FIXES: 75 | 76 | * additional notes: ensure to close files [[GH-241](https://github.com/hashicorp/go-plugin/pull/241)] 77 | 78 | ENHANCEMENTS: 79 | 80 | * deps: Remove direct dependency on golang.org/x/net [[GH-240](https://github.com/hashicorp/go-plugin/pull/240)] 81 | 82 | ## v1.4.9 83 | 84 | ENHANCEMENTS: 85 | 86 | * client: Remove log warning introduced in 1.4.5 when SecureConfig is nil. [[GH-238](https://github.com/hashicorp/go-plugin/pull/238)] 87 | 88 | ## v1.4.8 89 | 90 | BUG FIXES: 91 | 92 | * Fix windows build: [[GH-227](https://github.com/hashicorp/go-plugin/pull/227)] 93 | 94 | ## v1.4.7 95 | 96 | ENHANCEMENTS: 97 | 98 | * More detailed error message on plugin start failure: [[GH-223](https://github.com/hashicorp/go-plugin/pull/223)] 99 | 100 | ## v1.4.6 101 | 102 | BUG FIXES: 103 | 104 | * server: Prevent gRPC broker goroutine leak when using `GRPCServer` type `GracefulStop()` or `Stop()` methods [[GH-220](https://github.com/hashicorp/go-plugin/pull/220)] 105 | 106 | ## v1.4.5 107 | 108 | ENHANCEMENTS: 109 | 110 | * client: log warning when SecureConfig is nil [[GH-207](https://github.com/hashicorp/go-plugin/pull/207)] 111 | 112 | 113 | ## v1.4.4 114 | 115 | ENHANCEMENTS: 116 | 117 | * client: increase level of plugin exit logs [[GH-195](https://github.com/hashicorp/go-plugin/pull/195)] 118 | 119 | BUG FIXES: 120 | 121 | * Bidirectional communication: fix bidirectional communication when AutoMTLS is enabled [[GH-193](https://github.com/hashicorp/go-plugin/pull/193)] 122 | * RPC: Trim a spurious log message for plugins using RPC [[GH-186](https://github.com/hashicorp/go-plugin/pull/186)] 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Go Plugin System over RPC 2 | 3 | `go-plugin` is a Go (golang) plugin system over RPC. It is the plugin system 4 | that has been in use by HashiCorp tooling for over 4 years. While initially 5 | created for [Packer](https://www.packer.io), it is additionally in use by 6 | [Terraform](https://www.terraform.io), [Nomad](https://www.nomadproject.io), 7 | [Vault](https://www.vaultproject.io), 8 | [Boundary](https://www.boundaryproject.io), 9 | and [Waypoint](https://www.waypointproject.io). 10 | 11 | While the plugin system is over RPC, it is currently only designed to work 12 | over a local [reliable] network. Plugins over a real network are not supported 13 | and will lead to unexpected behavior. 14 | 15 | This plugin system has been used on millions of machines across many different 16 | projects and has proven to be battle hardened and ready for production use. 17 | 18 | ## Features 19 | 20 | The HashiCorp plugin system supports a number of features: 21 | 22 | **Plugins are Go interface implementations.** This makes writing and consuming 23 | plugins feel very natural. To a plugin author: you just implement an 24 | interface as if it were going to run in the same process. For a plugin user: 25 | you just use and call functions on an interface as if it were in the same 26 | process. This plugin system handles the communication in between. 27 | 28 | **Cross-language support.** Plugins can be written (and consumed) by 29 | almost every major language. This library supports serving plugins via 30 | [gRPC](http://www.grpc.io). gRPC-based plugins enable plugins to be written 31 | in any language. 32 | 33 | **Complex arguments and return values are supported.** This library 34 | provides APIs for handling complex arguments and return values such 35 | as interfaces, `io.Reader/Writer`, etc. We do this by giving you a library 36 | (`MuxBroker`) for creating new connections between the client/server to 37 | serve additional interfaces or transfer raw data. 38 | 39 | **Bidirectional communication.** Because the plugin system supports 40 | complex arguments, the host process can send it interface implementations 41 | and the plugin can call back into the host process. 42 | 43 | **Built-in Logging.** Any plugins that use the `log` standard library 44 | will have log data automatically sent to the host process. The host 45 | process will mirror this output prefixed with the path to the plugin 46 | binary. This makes debugging with plugins simple. If the host system 47 | uses [hclog](https://github.com/hashicorp/go-hclog) then the log data 48 | will be structured. If the plugin also uses hclog, logs from the plugin 49 | will be sent to the host hclog and be structured. 50 | 51 | **Protocol Versioning.** A very basic "protocol version" is supported that 52 | can be incremented to invalidate any previous plugins. This is useful when 53 | interface signatures are changing, protocol level changes are necessary, 54 | etc. When a protocol version is incompatible, a human friendly error 55 | message is shown to the end user. 56 | 57 | **Stdout/Stderr Syncing.** While plugins are subprocesses, they can continue 58 | to use stdout/stderr as usual and the output will get mirrored back to 59 | the host process. The host process can control what `io.Writer` these 60 | streams go to to prevent this from happening. 61 | 62 | **TTY Preservation.** Plugin subprocesses are connected to the identical 63 | stdin file descriptor as the host process, allowing software that requires 64 | a TTY to work. For example, a plugin can execute `ssh` and even though there 65 | are multiple subprocesses and RPC happening, it will look and act perfectly 66 | to the end user. 67 | 68 | **Host upgrade while a plugin is running.** Plugins can be "reattached" 69 | so that the host process can be upgraded while the plugin is still running. 70 | This requires the host/plugin to know this is possible and daemonize 71 | properly. `NewClient` takes a `ReattachConfig` to determine if and how to 72 | reattach. 73 | 74 | **Cryptographically Secure Plugins.** Plugins can be verified with an expected 75 | checksum and RPC communications can be configured to use TLS. The host process 76 | must be properly secured to protect this configuration. 77 | 78 | ## Architecture 79 | 80 | The HashiCorp plugin system works by launching subprocesses and communicating 81 | over RPC (using standard `net/rpc` or [gRPC](http://www.grpc.io)). A single 82 | connection is made between any plugin and the host process. For net/rpc-based 83 | plugins, we use a [connection multiplexing](https://github.com/hashicorp/yamux) 84 | library to multiplex any other connections on top. For gRPC-based plugins, 85 | the HTTP2 protocol handles multiplexing. 86 | 87 | This architecture has a number of benefits: 88 | 89 | * Plugins can't crash your host process: A panic in a plugin doesn't 90 | panic the plugin user. 91 | 92 | * Plugins are very easy to write: just write a Go application and `go build`. 93 | Or use any other language to write a gRPC server with a tiny amount of 94 | boilerplate to support go-plugin. 95 | 96 | * Plugins are very easy to install: just put the binary in a location where 97 | the host will find it (depends on the host but this library also provides 98 | helpers), and the plugin host handles the rest. 99 | 100 | * Plugins can be relatively secure: The plugin only has access to the 101 | interfaces and args given to it, not to the entire memory space of the 102 | process. Additionally, go-plugin can communicate with the plugin over 103 | TLS. 104 | 105 | ## Usage 106 | 107 | To use the plugin system, you must take the following steps. These are 108 | high-level steps that must be done. Examples are available in the 109 | `examples/` directory. 110 | 111 | 1. Choose the interface(s) you want to expose for plugins. 112 | 113 | 2. For each interface, implement an implementation of that interface 114 | that communicates over a `net/rpc` connection or over a 115 | [gRPC](http://www.grpc.io) connection or both. You'll have to implement 116 | both a client and server implementation. 117 | 118 | 3. Create a `Plugin` implementation that knows how to create the RPC 119 | client/server for a given plugin type. 120 | 121 | 4. Plugin authors call `plugin.Serve` to serve a plugin from the 122 | `main` function. 123 | 124 | 5. Plugin users use `plugin.Client` to launch a subprocess and request 125 | an interface implementation over RPC. 126 | 127 | That's it! In practice, step 2 is the most tedious and time consuming step. 128 | Even so, it isn't very difficult and you can see examples in the `examples/` 129 | directory as well as throughout our various open source projects. 130 | 131 | For complete API documentation, see [GoDoc](https://godoc.org/github.com/hashicorp/go-plugin). 132 | 133 | ## Roadmap 134 | 135 | Our plugin system is constantly evolving. As we use the plugin system for 136 | new projects or for new features in existing projects, we constantly find 137 | improvements we can make. 138 | 139 | At this point in time, the roadmap for the plugin system is: 140 | 141 | **Semantic Versioning.** Plugins will be able to implement a semantic version. 142 | This plugin system will give host processes a system for constraining 143 | versions. This is in addition to the protocol versioning already present 144 | which is more for larger underlying changes. 145 | 146 | ## What About Shared Libraries? 147 | 148 | When we started using plugins (late 2012, early 2013), plugins over RPC 149 | were the only option since Go didn't support dynamic library loading. Today, 150 | Go supports the [plugin](https://golang.org/pkg/plugin/) standard library with 151 | a number of limitations. Since 2012, our plugin system has stabilized 152 | from tens of millions of users using it, and has many benefits we've come to 153 | value greatly. 154 | 155 | For example, we use this plugin system in 156 | [Vault](https://www.vaultproject.io) where dynamic library loading is 157 | not acceptable for security reasons. That is an extreme 158 | example, but we believe our library system has more upsides than downsides 159 | over dynamic library loading and since we've had it built and tested for years, 160 | we'll continue to use it. 161 | 162 | Shared libraries have one major advantage over our system which is much 163 | higher performance. In real world scenarios across our various tools, 164 | we've never required any more performance out of our plugin system and it 165 | has seen very high throughput, so this isn't a concern for us at the moment. 166 | -------------------------------------------------------------------------------- /buf.gen.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: v1 5 | plugins: 6 | - plugin: buf.build/protocolbuffers/go 7 | out: . 8 | opt: 9 | - paths=source_relative 10 | - plugin: buf.build/grpc/go:v1.3.0 11 | out: . 12 | opt: 13 | - paths=source_relative 14 | - require_unimplemented_servers=false 15 | -------------------------------------------------------------------------------- /buf.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: v1 5 | build: 6 | excludes: 7 | - examples/ -------------------------------------------------------------------------------- /client_posix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package plugin 8 | 9 | import ( 10 | "os" 11 | "reflect" 12 | "syscall" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func TestClient_testInterfaceReattach(t *testing.T) { 18 | // Setup the process for daemonization 19 | process := helperProcess("test-interface-daemon") 20 | if process.SysProcAttr == nil { 21 | process.SysProcAttr = &syscall.SysProcAttr{} 22 | } 23 | process.SysProcAttr.Setsid = true 24 | syscall.Umask(0) 25 | 26 | c := NewClient(&ClientConfig{ 27 | Cmd: process, 28 | HandshakeConfig: testHandshake, 29 | Plugins: testPluginMap, 30 | }) 31 | 32 | // Start it so we can get the reattach info 33 | if _, err := c.Start(); err != nil { 34 | t.Fatalf("err should be nil, got %s", err) 35 | } 36 | 37 | // New client with reattach info 38 | reattach := c.ReattachConfig() 39 | if reattach == nil { 40 | c.Kill() 41 | t.Fatal("reattach config should be non-nil") 42 | return // Required for staticcheck SA5011 43 | } 44 | 45 | // Find the process and defer a kill so we know it is gone 46 | p, err := os.FindProcess(reattach.Pid) 47 | if err != nil { 48 | c.Kill() 49 | t.Fatalf("couldn't find process: %s", err) 50 | } 51 | defer func() { _ = p.Kill() }() 52 | 53 | // Reattach 54 | c = NewClient(&ClientConfig{ 55 | Reattach: reattach, 56 | HandshakeConfig: testHandshake, 57 | Plugins: testPluginMap, 58 | }) 59 | 60 | // Start shouldn't error 61 | if _, err := c.Start(); err != nil { 62 | t.Fatalf("err: %s", err) 63 | } 64 | 65 | // It should still be alive 66 | time.Sleep(1 * time.Second) 67 | if c.Exited() { 68 | t.Fatal("should not be exited") 69 | } 70 | 71 | // Grab the RPC client 72 | client, err := c.Client() 73 | if err != nil { 74 | t.Fatalf("err should be nil, got %s", err) 75 | } 76 | 77 | // Grab the impl 78 | raw, err := client.Dispense("test") 79 | if err != nil { 80 | t.Fatalf("err should be nil, got %s", err) 81 | } 82 | 83 | impl, ok := raw.(testInterface) 84 | if !ok { 85 | t.Fatalf("bad: %#v", raw) 86 | } 87 | 88 | result := impl.Double(21) 89 | if result != 42 { 90 | t.Fatalf("bad: %#v", result) 91 | } 92 | 93 | // Test the resulting reattach config 94 | reattach2 := c.ReattachConfig() 95 | if reattach2 == nil { 96 | t.Fatal("reattach from reattached should not be nil") 97 | } 98 | if !reflect.DeepEqual(reattach, reattach2) { 99 | t.Fatalf("bad: %#v", reattach) 100 | } 101 | 102 | // Kill it 103 | c.Kill() 104 | 105 | // Test that it knows it is exited 106 | if !c.Exited() { 107 | t.Fatal("should say client has exited") 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /client_unix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package plugin 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | "os/user" 14 | "path/filepath" 15 | "syscall" 16 | "testing" 17 | 18 | "github.com/hashicorp/go-hclog" 19 | "github.com/hashicorp/go-plugin/internal/cmdrunner" 20 | "github.com/hashicorp/go-plugin/runner" 21 | ) 22 | 23 | func TestSetGroup(t *testing.T) { 24 | group, err := user.LookupGroupId(fmt.Sprintf("%d", os.Getgid())) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | baseTempDir := t.TempDir() 29 | baseTempDir, err = filepath.EvalSymlinks(baseTempDir) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | for name, tc := range map[string]struct { 34 | group string 35 | }{ 36 | "as integer": {fmt.Sprintf("%d", os.Getgid())}, 37 | "as name": {group.Name}, 38 | } { 39 | t.Run(name, func(t *testing.T) { 40 | process := helperProcess("mock") 41 | c := NewClient(&ClientConfig{ 42 | HandshakeConfig: testHandshake, 43 | Plugins: testPluginMap, 44 | UnixSocketConfig: &UnixSocketConfig{ 45 | Group: tc.group, 46 | TempDir: baseTempDir, 47 | }, 48 | RunnerFunc: func(l hclog.Logger, cmd *exec.Cmd, tmpDir string) (runner.Runner, error) { 49 | // Run tests inside the RunnerFunc to ensure we don't race 50 | // with the code that deletes tmpDir when the client fails 51 | // to start properly. 52 | 53 | // Test that it creates a directory with the proper owners and permissions. 54 | if filepath.Dir(tmpDir) != baseTempDir { 55 | t.Errorf("Expected base TempDir to be %s, but tmpDir was %s", baseTempDir, tmpDir) 56 | } 57 | info, err := os.Lstat(tmpDir) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | if info.Mode()&os.ModePerm != 0o770 { 62 | t.Fatal(info.Mode()) 63 | } 64 | stat, ok := info.Sys().(*syscall.Stat_t) 65 | if !ok { 66 | t.Fatal() 67 | } 68 | if stat.Gid != uint32(os.Getgid()) { 69 | t.Fatalf("Expected %d, but got %d", os.Getgid(), stat.Gid) 70 | } 71 | 72 | // Check the correct environment variables were set to forward 73 | // Unix socket config onto the plugin. 74 | var foundUnixSocketDir, foundUnixSocketGroup bool 75 | for _, env := range cmd.Env { 76 | if env == fmt.Sprintf("%s=%s", EnvUnixSocketDir, tmpDir) { 77 | foundUnixSocketDir = true 78 | } 79 | if env == fmt.Sprintf("%s=%s", EnvUnixSocketGroup, tc.group) { 80 | foundUnixSocketGroup = true 81 | } 82 | } 83 | if !foundUnixSocketDir { 84 | t.Errorf("Did not find correct %s env in %v", EnvUnixSocketDir, cmd.Env) 85 | } 86 | if !foundUnixSocketGroup { 87 | t.Errorf("Did not find correct %s env in %v", EnvUnixSocketGroup, cmd.Env) 88 | } 89 | 90 | process.Env = append(process.Env, cmd.Env...) 91 | return cmdrunner.NewCmdRunner(l, process) 92 | }, 93 | }) 94 | defer c.Kill() 95 | 96 | _, err := c.Start() 97 | if err != nil { 98 | t.Fatalf("err should be nil, got %s", err) 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /constants.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | const ( 7 | // EnvUnixSocketDir specifies the directory that _plugins_ should create unix 8 | // sockets in. Does not affect client behavior. 9 | EnvUnixSocketDir = "PLUGIN_UNIX_SOCKET_DIR" 10 | 11 | // EnvUnixSocketGroup specifies the owning, writable group to set for Unix 12 | // sockets created by _plugins_. Does not affect client behavior. 13 | EnvUnixSocketGroup = "PLUGIN_UNIX_SOCKET_GROUP" 14 | 15 | envMultiplexGRPC = "PLUGIN_MULTIPLEX_GRPC" 16 | ) 17 | -------------------------------------------------------------------------------- /discover.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "path/filepath" 8 | ) 9 | 10 | // Discover discovers plugins that are in a given directory. 11 | // 12 | // The directory doesn't need to be absolute. For example, "." will work fine. 13 | // 14 | // This currently assumes any file matching the glob is a plugin. 15 | // In the future this may be smarter about checking that a file is 16 | // executable and so on. 17 | // 18 | // TODO: test 19 | func Discover(glob, dir string) ([]string, error) { 20 | var err error 21 | 22 | // Make the directory absolute if it isn't already 23 | if !filepath.IsAbs(dir) { 24 | dir, err = filepath.Abs(dir) 25 | if err != nil { 26 | return nil, err 27 | } 28 | } 29 | 30 | return filepath.Glob(filepath.Join(dir, glob)) 31 | } 32 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # go-plugin Documentation 2 | 3 | This directory contains documentation and guides for `go-plugin` and how 4 | to integrate it into your projects. It is assumed that you know _what_ 5 | go-plugin is and _why_ you would want to use it. If not, please see the 6 | [README](https://github.com/hashicorp/go-plugin/blob/master/README.md). 7 | 8 | ## Table of Contents 9 | 10 | **[Writing Plugins Without Go](https://github.com/hashicorp/go-plugin/blob/master/docs/guide-plugin-write-non-go.md).** 11 | This shows how to write a plugin using a programming language other than 12 | Go. 13 | 14 | **[Extensive Tutorial on Usage](https://github.com/hashicorp/go-plugin/blob/master/docs/extensive-go-plugin-tutorial.md).** 15 | A guide showing the usage and structure of go-plugin including a detailed 16 | walkthrough of setting up the plugins under the `examples` folder. -------------------------------------------------------------------------------- /docs/guide-plugin-write-non-go.md: -------------------------------------------------------------------------------- 1 | # Writing Plugins Without Go 2 | 3 | This guide explains how to write a go-plugin compatible plugin using 4 | a programming language other than Go. go-plugin supports plugins using 5 | [gRPC](http://www.grpc.io). This makes it relatively simple to write plugins 6 | using other languages! 7 | 8 | Minimal knowledge about gRPC is assumed. We recommend reading the 9 | [gRPC Go Tutorial](http://www.grpc.io/docs/tutorials/basic/go.html). This 10 | alone is enough gRPC knowledge to continue. 11 | 12 | This guide will implement the kv example in Python. 13 | Full source code for the examples present in this guide 14 | [is available in the examples/grpc folder](https://github.com/hashicorp/go-plugin/tree/master/examples/grpc). 15 | 16 | ## 1. Implement the Service 17 | 18 | The first step is to implement the gRPC server for the protocol buffers 19 | service that your plugin defines. This is a standard gRPC server. 20 | For the KV service, the service looks like this: 21 | 22 | ```proto 23 | service KV { 24 | rpc Get(GetRequest) returns (GetResponse); 25 | rpc Put(PutRequest) returns (Empty); 26 | } 27 | ``` 28 | 29 | We can implement that using Python as easily as: 30 | 31 | ```python 32 | class KVServicer(kv_pb2_grpc.KVServicer): 33 | """Implementation of KV service.""" 34 | 35 | def Get(self, request, context): 36 | filename = "kv_"+request.key 37 | with open(filename, 'r') as f: 38 | result = kv_pb2.GetResponse() 39 | result.value = f.read() 40 | return result 41 | 42 | def Put(self, request, context): 43 | filename = "kv_"+request.key 44 | value = "{0}\n\nWritten from plugin-python".format(request.value) 45 | with open(filename, 'w') as f: 46 | f.write(value) 47 | 48 | return kv_pb2.Empty() 49 | 50 | ``` 51 | 52 | Great! With that, we have a fully functioning implementation of the service. 53 | You can test this using standard gRPC testing mechanisms. 54 | 55 | ## 2. Serve the Service 56 | 57 | Next, we need to create a gRPC server and serve the service we just made. 58 | 59 | In Python: 60 | 61 | ```python 62 | # Make the server 63 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 64 | 65 | # Add our service 66 | kv_pb2_grpc.add_KVServicer_to_server(KVServicer(), server) 67 | 68 | # Listen on a port 69 | server.add_insecure_port(':1234') 70 | 71 | # Start 72 | server.start() 73 | ``` 74 | 75 | You can listen on any TCP address or Unix domain socket. go-plugin does 76 | assume that connections are reliable (local), so you should not serve 77 | your plugin across the network. 78 | 79 | ## 3. Add the gRPC Health Checking Service 80 | 81 | go-plugin requires the 82 | [gRPC Health Checking Service](https://github.com/grpc/grpc/blob/master/doc/health-checking.md) 83 | to be registered on your server. You must register the status of "plugin" to be SERVING. 84 | 85 | The health checking service is used by go-plugin to determine if everything 86 | is healthy with the connection. If you don't implement this service, your 87 | process may be abruptly restarted and your plugins are likely to be unreliable. 88 | 89 | ``` 90 | health = HealthServicer() 91 | health.set("plugin", health_pb2.HealthCheckResponse.ServingStatus.Value('SERVING')) 92 | health_pb2_grpc.add_HealthServicer_to_server(health, server) 93 | ``` 94 | 95 | ## 4. Output Handshake Information 96 | 97 | The final step is to output the handshake information to stdout. go-plugin 98 | reads a single line from stdout to determine how to connect to your plugin, 99 | what protocol it is using, etc. 100 | 101 | 102 | The structure is: 103 | 104 | ``` 105 | CORE-PROTOCOL-VERSION | APP-PROTOCOL-VERSION | NETWORK-TYPE | NETWORK-ADDR | PROTOCOL 106 | ``` 107 | 108 | Where: 109 | 110 | * `CORE-PROTOCOL-VERSION` is the protocol version for go-plugin itself. 111 | The current value is `1`. Please use this value. Any other value will 112 | cause your plugin to not load. 113 | 114 | * `APP-PROTOCOL-VERSION` is the protocol version for the application data. 115 | This is determined by the application. You must reference the documentation 116 | for your application to determine the desired value. 117 | 118 | * `NETWORK-TYPE` and `NETWORK-ADDR` are the networking information for 119 | connecting to this plugin. The type must be "unix" or "tcp". The address 120 | is a path to the Unix socket for "unix" and an IP address for "tcp". 121 | 122 | * `PROTOCOL` is the named protocol that the connection will use. If this 123 | is omitted (older versions), this is "netrpc" for Go net/rpc. This can 124 | also be "grpc". This is the protocol that the plugin wants to speak to 125 | the host process with. 126 | 127 | For our example that is: 128 | 129 | ``` 130 | 1|1|tcp|127.0.0.1:1234|grpc 131 | ``` 132 | 133 | The only element you'll have to be careful about is the second one (the 134 | `APP-PROTOCOL-VERISON`). This will depend on the application you're 135 | building a plugin for. Please reference their documentation for more 136 | information. 137 | 138 | ## 5. Done! 139 | 140 | And we're done! 141 | 142 | Configure the host application (the application you're writing a plugin 143 | for) to execute your Python application. Configuring plugins is specific 144 | to the host application. 145 | 146 | For our example, we used an environmental variable, and it looks like this: 147 | 148 | ```sh 149 | $ export KV_PLUGIN="python plugin.py" 150 | ``` 151 | -------------------------------------------------------------------------------- /docs/internals.md: -------------------------------------------------------------------------------- 1 | # go-plugin Internals 2 | 3 | This section discusses the internals of how go-plugin works. 4 | 5 | go-plugin operates by either _serving_ a plugin or being a _client_ 6 | connecting to a remote plugin. The "client" is the host process or the 7 | process that itself uses plugins. The "server" is the plugin process. 8 | 9 | For a server: 10 | 11 | 1. Output handshake to stdout 12 | 2. Wait for connection on control address 13 | 3. Serve plugins over control address 14 | 15 | For a client: 16 | 17 | 1. Launch a plugin binary 18 | 2. Read and verify handshake from plugin stdout 19 | 3. Connect to plugin control address using desired protocol 20 | 4. Dispense plugins using control connection 21 | 22 | ## Handshake 23 | 24 | The handshake is the initial communication between a plugin and a host 25 | process to determine how the host process can connect and communicate to 26 | the plugin. This handshake is done over the plugin process's stdout. 27 | 28 | The `go-plugin` library itself handles the handshake when using the 29 | `Server` to serve a plugin. **You do not need to understand the internals 30 | of the handshake,** unless you're building a go-plugin compatible plugin 31 | in another language. 32 | 33 | The handshake is a single line of data terminated with a newline character 34 | `\n`. It looks like the following: 35 | 36 | ``` 37 | 1|3|unix|/path/to/socket|grpc 38 | ``` 39 | 40 | The structure is: 41 | 42 | ``` 43 | CORE-PROTOCOL-VERSION | APP-PROTOCOL-VERSION | NETWORK-TYPE | NETWORK-ADDR | PROTOCOL 44 | ``` 45 | 46 | Where: 47 | 48 | * `CORE-PROTOCOL-VERSION` is the protocol version for go-plugin itself. 49 | The current value is `1`. Please use this value. Any other value will 50 | cause your plugin to not load. 51 | 52 | * `APP-PROTOCOL-VERSION` is the protocol version for the application data. 53 | This is determined by the application. You must reference the documentation 54 | for your application to determine the desired value. 55 | 56 | * `NETWORK-TYPE` and `NETWORK-ADDR` are the networking information for 57 | connecting to this plugin. The type must be "unix" or "tcp". The address 58 | is a path to the Unix socket for "unix" and an IP address for "tcp". 59 | 60 | * `PROTOCOL` is the named protocol that the connection will use. If this 61 | is omitted (older versions), this is "netrpc" for Go net/rpc. This can 62 | also be "grpc". This is the protocol that the plugin wants to speak to 63 | the host process with. 64 | 65 | ## Environment Variables 66 | 67 | When serving a plugin over TCP, the following environment variables can be 68 | specified to restrict the port that will be assigned to be from within a 69 | specific range. If not values are provided, the port will be randomly assigned 70 | by the operating system. 71 | 72 | * `PLUGIN_MIN_PORT`: Specifies the minimum port value that will be assigned to the listener. 73 | 74 | * `PLUGIN_MAX_PORT`: Specifies the maximum port value that will be assigned to the listener. 75 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | // This is a type that wraps error types so that they can be messaged 7 | // across RPC channels. Since "error" is an interface, we can't always 8 | // gob-encode the underlying structure. This is a valid error interface 9 | // implementer that we will push across. 10 | type BasicError struct { 11 | Message string 12 | } 13 | 14 | // NewBasicError is used to create a BasicError. 15 | // 16 | // err is allowed to be nil. 17 | func NewBasicError(err error) *BasicError { 18 | if err == nil { 19 | return nil 20 | } 21 | 22 | return &BasicError{err.Error()} 23 | } 24 | 25 | func (e *BasicError) Error() string { 26 | return e.Message 27 | } 28 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | ) 10 | 11 | func TestBasicError_ImplementsError(t *testing.T) { 12 | var _ error = new(BasicError) 13 | } 14 | 15 | func TestBasicError_MatchesMessage(t *testing.T) { 16 | err := errors.New("foo") 17 | wrapped := NewBasicError(err) 18 | 19 | if wrapped.Error() != err.Error() { 20 | t.Fatalf("bad: %#v", wrapped.Error()) 21 | } 22 | } 23 | 24 | func TestNewBasicError_nil(t *testing.T) { 25 | r := NewBasicError(nil) 26 | if r != nil { 27 | t.Fatalf("bad: %#v", r) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore binaries 2 | plugin/greeter 3 | basic -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | Plugin Example 2 | -------------- 3 | 4 | Compile the plugin itself via: 5 | 6 | go build -o ./plugin/greeter ./plugin/greeter_impl.go 7 | 8 | Compile this driver via: 9 | 10 | go build -o basic . 11 | 12 | You can then launch the plugin sample via: 13 | 14 | ./basic 15 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "os" 10 | "os/exec" 11 | 12 | hclog "github.com/hashicorp/go-hclog" 13 | "github.com/hashicorp/go-plugin" 14 | "github.com/hashicorp/go-plugin/examples/basic/shared" 15 | ) 16 | 17 | func main() { 18 | // Create an hclog.Logger 19 | logger := hclog.New(&hclog.LoggerOptions{ 20 | Name: "plugin", 21 | Output: os.Stdout, 22 | Level: hclog.Debug, 23 | }) 24 | 25 | // We're a host! Start by launching the plugin process. 26 | client := plugin.NewClient(&plugin.ClientConfig{ 27 | HandshakeConfig: handshakeConfig, 28 | Plugins: pluginMap, 29 | Cmd: exec.Command("./plugin/greeter"), 30 | Logger: logger, 31 | }) 32 | defer client.Kill() 33 | 34 | // Connect via RPC 35 | rpcClient, err := client.Client() 36 | if err != nil { 37 | log.Fatal(err) 38 | } 39 | 40 | // Request the plugin 41 | raw, err := rpcClient.Dispense("greeter") 42 | if err != nil { 43 | log.Fatal(err) 44 | } 45 | 46 | // We should have a Greeter now! This feels like a normal interface 47 | // implementation but is in fact over an RPC connection. 48 | greeter := raw.(shared.Greeter) 49 | fmt.Println(greeter.Greet()) 50 | } 51 | 52 | // handshakeConfigs are used to just do a basic handshake between 53 | // a plugin and host. If the handshake fails, a user friendly error is shown. 54 | // This prevents users from executing bad plugins or executing a plugin 55 | // directory. It is a UX feature, not a security feature. 56 | var handshakeConfig = plugin.HandshakeConfig{ 57 | ProtocolVersion: 1, 58 | MagicCookieKey: "BASIC_PLUGIN", 59 | MagicCookieValue: "hello", 60 | } 61 | 62 | // pluginMap is the map of plugins we can dispense. 63 | var pluginMap = map[string]plugin.Plugin{ 64 | "greeter": &shared.GreeterPlugin{}, 65 | } 66 | -------------------------------------------------------------------------------- /examples/basic/plugin/greeter_impl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | 9 | "github.com/hashicorp/go-hclog" 10 | "github.com/hashicorp/go-plugin" 11 | "github.com/hashicorp/go-plugin/examples/basic/shared" 12 | ) 13 | 14 | // Here is a real implementation of Greeter 15 | type GreeterHello struct { 16 | logger hclog.Logger 17 | } 18 | 19 | func (g *GreeterHello) Greet() string { 20 | g.logger.Debug("message from GreeterHello.Greet") 21 | return "Hello!" 22 | } 23 | 24 | // handshakeConfigs are used to just do a basic handshake between 25 | // a plugin and host. If the handshake fails, a user friendly error is shown. 26 | // This prevents users from executing bad plugins or executing a plugin 27 | // directory. It is a UX feature, not a security feature. 28 | var handshakeConfig = plugin.HandshakeConfig{ 29 | ProtocolVersion: 1, 30 | MagicCookieKey: "BASIC_PLUGIN", 31 | MagicCookieValue: "hello", 32 | } 33 | 34 | func main() { 35 | logger := hclog.New(&hclog.LoggerOptions{ 36 | Level: hclog.Trace, 37 | Output: os.Stderr, 38 | JSONFormat: true, 39 | }) 40 | 41 | greeter := &GreeterHello{ 42 | logger: logger, 43 | } 44 | // pluginMap is the map of plugins we can dispense. 45 | var pluginMap = map[string]plugin.Plugin{ 46 | "greeter": &shared.GreeterPlugin{Impl: greeter}, 47 | } 48 | 49 | logger.Debug("message from plugin", "foo", "bar") 50 | 51 | plugin.Serve(&plugin.ServeConfig{ 52 | HandshakeConfig: handshakeConfig, 53 | Plugins: pluginMap, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /examples/basic/shared/greeter_interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package shared 5 | 6 | import ( 7 | "net/rpc" 8 | 9 | "github.com/hashicorp/go-plugin" 10 | ) 11 | 12 | // Greeter is the interface that we're exposing as a plugin. 13 | type Greeter interface { 14 | Greet() string 15 | } 16 | 17 | // Here is an implementation that talks over RPC 18 | type GreeterRPC struct{ client *rpc.Client } 19 | 20 | func (g *GreeterRPC) Greet() string { 21 | var resp string 22 | err := g.client.Call("Plugin.Greet", new(interface{}), &resp) 23 | if err != nil { 24 | // You usually want your interfaces to return errors. If they don't, 25 | // there isn't much other choice here. 26 | panic(err) 27 | } 28 | 29 | return resp 30 | } 31 | 32 | // Here is the RPC server that GreeterRPC talks to, conforming to 33 | // the requirements of net/rpc 34 | type GreeterRPCServer struct { 35 | // This is the real implementation 36 | Impl Greeter 37 | } 38 | 39 | func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error { 40 | *resp = s.Impl.Greet() 41 | return nil 42 | } 43 | 44 | // This is the implementation of plugin.Plugin so we can serve/consume this 45 | // 46 | // This has two methods: Server must return an RPC server for this plugin 47 | // type. We construct a GreeterRPCServer for this. 48 | // 49 | // Client must return an implementation of our interface that communicates 50 | // over an RPC client. We return GreeterRPC for this. 51 | // 52 | // Ignore MuxBroker. That is used to create more multiplexed streams on our 53 | // plugin connection and is a more advanced use case. 54 | type GreeterPlugin struct { 55 | // Impl Injection 56 | Impl Greeter 57 | } 58 | 59 | func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) { 60 | return &GreeterRPCServer{Impl: p.Impl}, nil 61 | } 62 | 63 | func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { 64 | return &GreeterRPC{client: c}, nil 65 | } 66 | -------------------------------------------------------------------------------- /examples/bidirectional/README.md: -------------------------------------------------------------------------------- 1 | # Counter Example 2 | 3 | This example builds a simple key/counter store CLI where the mechanism 4 | for storing and retrieving keys is pluggable. However, in this example we don't 5 | trust the plugin to do the summation work. We use bi-directional plugins to 6 | call back into the main proccess to do the sum of two numbers. To build this example: 7 | 8 | ```sh 9 | # This builds the main CLI 10 | $ go build -o counter 11 | 12 | # This builds the plugin written in Go 13 | $ go build -o counter-go-grpc ./plugin-go-grpc 14 | 15 | # This tells the Counter binary to use the "counter-go-grpc" binary 16 | $ export COUNTER_PLUGIN="./counter-go-grpc" 17 | 18 | # Read and write 19 | $ ./counter put hello 1 20 | $ ./counter put hello 1 21 | 22 | $ ./counter get hello 23 | 2 24 | ``` 25 | 26 | ### Plugin: plugin-go-grpc 27 | 28 | This plugin uses gRPC to serve a plugin that is written in Go: 29 | 30 | ``` 31 | # This builds the plugin written in Go 32 | $ go build -o counter-go-grpc ./plugin-go-grpc 33 | 34 | # This tells the KV binary to use the "kv-go-grpc" binary 35 | $ export COUNTER_PLUGIN="./counter-go-grpc" 36 | ``` 37 | 38 | ## Updating the Protocol 39 | 40 | If you update the protocol buffers file, you can regenerate the file 41 | using the following command from this directory. You do not need to run 42 | this if you're just trying the example. 43 | 44 | ```sh 45 | $ buf generate 46 | ``` 47 | -------------------------------------------------------------------------------- /examples/bidirectional/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: v1 5 | plugins: 6 | - plugin: buf.build/protocolbuffers/go 7 | out: . 8 | opt: 9 | - paths=source_relative 10 | - plugin: buf.build/grpc/go:v1.3.0 11 | out: . 12 | opt: 13 | - paths=source_relative 14 | - require_unimplemented_servers=false 15 | -------------------------------------------------------------------------------- /examples/bidirectional/buf.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: v1 5 | -------------------------------------------------------------------------------- /examples/bidirectional/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "strconv" 13 | 14 | "github.com/hashicorp/go-plugin" 15 | "github.com/hashicorp/go-plugin/examples/bidirectional/shared" 16 | ) 17 | 18 | type addHelper struct{} 19 | 20 | func (*addHelper) Sum(a, b int64) (int64, error) { 21 | return a + b, nil 22 | } 23 | 24 | func main() { 25 | // We don't want to see the plugin logs. 26 | log.SetOutput(io.Discard) 27 | 28 | // We're a host. Start by launching the plugin process. 29 | client := plugin.NewClient(&plugin.ClientConfig{ 30 | HandshakeConfig: shared.Handshake, 31 | Plugins: shared.PluginMap, 32 | Cmd: exec.Command("sh", "-c", os.Getenv("COUNTER_PLUGIN")), 33 | AllowedProtocols: []plugin.Protocol{ 34 | plugin.ProtocolNetRPC, plugin.ProtocolGRPC}, 35 | }) 36 | defer client.Kill() 37 | 38 | // Connect via RPC 39 | rpcClient, err := client.Client() 40 | if err != nil { 41 | fmt.Println("Error:", err.Error()) 42 | os.Exit(1) 43 | } 44 | 45 | // Request the plugin 46 | raw, err := rpcClient.Dispense("counter") 47 | if err != nil { 48 | fmt.Println("Error:", err.Error()) 49 | os.Exit(1) 50 | } 51 | 52 | // We should have a Counter store now! This feels like a normal interface 53 | // implementation but is in fact over an RPC connection. 54 | counter := raw.(shared.Counter) 55 | 56 | os.Args = os.Args[1:] 57 | switch os.Args[0] { 58 | case "get": 59 | result, err := counter.Get(os.Args[1]) 60 | if err != nil { 61 | fmt.Println("Error:", err.Error()) 62 | os.Exit(1) 63 | } 64 | 65 | fmt.Println(result) 66 | 67 | case "put": 68 | i, err := strconv.Atoi(os.Args[2]) 69 | if err != nil { 70 | fmt.Println("Error:", err.Error()) 71 | os.Exit(1) 72 | } 73 | 74 | err = counter.Put(os.Args[1], int64(i), &addHelper{}) 75 | if err != nil { 76 | fmt.Println("Error:", err.Error()) 77 | os.Exit(1) 78 | } 79 | 80 | default: 81 | fmt.Println("Please only use 'get' or 'put'") 82 | os.Exit(1) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /examples/bidirectional/plugin-go-grpc/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "os" 9 | 10 | "github.com/hashicorp/go-plugin" 11 | "github.com/hashicorp/go-plugin/examples/bidirectional/shared" 12 | ) 13 | 14 | // Here is a real implementation of KV that writes to a local file with 15 | // the key name and the contents are the value of the key. 16 | type Counter struct { 17 | } 18 | 19 | type data struct { 20 | Value int64 21 | } 22 | 23 | func (k *Counter) Put(key string, value int64, a shared.AddHelper) error { 24 | v, _ := k.Get(key) 25 | 26 | r, err := a.Sum(v, value) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | buf, err := json.Marshal(&data{r}) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | return os.WriteFile("kv_"+key, buf, 0644) 37 | } 38 | 39 | func (k *Counter) Get(key string) (int64, error) { 40 | dataRaw, err := os.ReadFile("kv_" + key) 41 | if err != nil { 42 | return 0, err 43 | } 44 | 45 | data := &data{} 46 | err = json.Unmarshal(dataRaw, data) 47 | if err != nil { 48 | return 0, err 49 | } 50 | 51 | return data.Value, nil 52 | } 53 | 54 | func main() { 55 | plugin.Serve(&plugin.ServeConfig{ 56 | HandshakeConfig: shared.Handshake, 57 | Plugins: map[string]plugin.Plugin{ 58 | "counter": &shared.CounterPlugin{Impl: &Counter{}}, 59 | }, 60 | 61 | // A non-nil value here enables gRPC serving for this plugin... 62 | GRPCServer: plugin.DefaultGRPCServer, 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /examples/bidirectional/proto/kv.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | syntax = "proto3"; 5 | package proto; 6 | option go_package = "./proto"; 7 | 8 | message GetRequest { 9 | string key = 1; 10 | } 11 | 12 | message GetResponse { 13 | int64 value = 1; 14 | } 15 | 16 | message PutRequest { 17 | uint32 add_server = 1; 18 | string key = 2; 19 | int64 value = 3; 20 | } 21 | 22 | message Empty {} 23 | 24 | message SumRequest { 25 | int64 a = 1; 26 | int64 b = 2; 27 | } 28 | 29 | message SumResponse { 30 | int64 r = 1; 31 | } 32 | 33 | service Counter { 34 | rpc Get(GetRequest) returns (GetResponse); 35 | rpc Put(PutRequest) returns (Empty); 36 | } 37 | 38 | service AddHelper { 39 | rpc Sum(SumRequest) returns (SumResponse); 40 | } 41 | -------------------------------------------------------------------------------- /examples/bidirectional/proto/kv_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 5 | // versions: 6 | // - protoc-gen-go-grpc v1.3.0 7 | // - protoc (unknown) 8 | // source: proto/kv.proto 9 | 10 | package proto 11 | 12 | import ( 13 | context "context" 14 | grpc "google.golang.org/grpc" 15 | codes "google.golang.org/grpc/codes" 16 | status "google.golang.org/grpc/status" 17 | ) 18 | 19 | // This is a compile-time assertion to ensure that this generated file 20 | // is compatible with the grpc package it is being compiled against. 21 | // Requires gRPC-Go v1.32.0 or later. 22 | const _ = grpc.SupportPackageIsVersion7 23 | 24 | const ( 25 | Counter_Get_FullMethodName = "/proto.Counter/Get" 26 | Counter_Put_FullMethodName = "/proto.Counter/Put" 27 | ) 28 | 29 | // CounterClient is the client API for Counter service. 30 | // 31 | // 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. 32 | type CounterClient interface { 33 | Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) 34 | Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error) 35 | } 36 | 37 | type counterClient struct { 38 | cc grpc.ClientConnInterface 39 | } 40 | 41 | func NewCounterClient(cc grpc.ClientConnInterface) CounterClient { 42 | return &counterClient{cc} 43 | } 44 | 45 | func (c *counterClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) { 46 | out := new(GetResponse) 47 | err := c.cc.Invoke(ctx, Counter_Get_FullMethodName, in, out, opts...) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return out, nil 52 | } 53 | 54 | func (c *counterClient) Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error) { 55 | out := new(Empty) 56 | err := c.cc.Invoke(ctx, Counter_Put_FullMethodName, in, out, opts...) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return out, nil 61 | } 62 | 63 | // CounterServer is the server API for Counter service. 64 | // All implementations should embed UnimplementedCounterServer 65 | // for forward compatibility 66 | type CounterServer interface { 67 | Get(context.Context, *GetRequest) (*GetResponse, error) 68 | Put(context.Context, *PutRequest) (*Empty, error) 69 | } 70 | 71 | // UnimplementedCounterServer should be embedded to have forward compatible implementations. 72 | type UnimplementedCounterServer struct { 73 | } 74 | 75 | func (UnimplementedCounterServer) Get(context.Context, *GetRequest) (*GetResponse, error) { 76 | return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") 77 | } 78 | func (UnimplementedCounterServer) Put(context.Context, *PutRequest) (*Empty, error) { 79 | return nil, status.Errorf(codes.Unimplemented, "method Put not implemented") 80 | } 81 | 82 | // UnsafeCounterServer may be embedded to opt out of forward compatibility for this service. 83 | // Use of this interface is not recommended, as added methods to CounterServer will 84 | // result in compilation errors. 85 | type UnsafeCounterServer interface { 86 | mustEmbedUnimplementedCounterServer() 87 | } 88 | 89 | func RegisterCounterServer(s grpc.ServiceRegistrar, srv CounterServer) { 90 | s.RegisterService(&Counter_ServiceDesc, srv) 91 | } 92 | 93 | func _Counter_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 94 | in := new(GetRequest) 95 | if err := dec(in); err != nil { 96 | return nil, err 97 | } 98 | if interceptor == nil { 99 | return srv.(CounterServer).Get(ctx, in) 100 | } 101 | info := &grpc.UnaryServerInfo{ 102 | Server: srv, 103 | FullMethod: Counter_Get_FullMethodName, 104 | } 105 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 106 | return srv.(CounterServer).Get(ctx, req.(*GetRequest)) 107 | } 108 | return interceptor(ctx, in, info, handler) 109 | } 110 | 111 | func _Counter_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 112 | in := new(PutRequest) 113 | if err := dec(in); err != nil { 114 | return nil, err 115 | } 116 | if interceptor == nil { 117 | return srv.(CounterServer).Put(ctx, in) 118 | } 119 | info := &grpc.UnaryServerInfo{ 120 | Server: srv, 121 | FullMethod: Counter_Put_FullMethodName, 122 | } 123 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 124 | return srv.(CounterServer).Put(ctx, req.(*PutRequest)) 125 | } 126 | return interceptor(ctx, in, info, handler) 127 | } 128 | 129 | // Counter_ServiceDesc is the grpc.ServiceDesc for Counter service. 130 | // It's only intended for direct use with grpc.RegisterService, 131 | // and not to be introspected or modified (even as a copy) 132 | var Counter_ServiceDesc = grpc.ServiceDesc{ 133 | ServiceName: "proto.Counter", 134 | HandlerType: (*CounterServer)(nil), 135 | Methods: []grpc.MethodDesc{ 136 | { 137 | MethodName: "Get", 138 | Handler: _Counter_Get_Handler, 139 | }, 140 | { 141 | MethodName: "Put", 142 | Handler: _Counter_Put_Handler, 143 | }, 144 | }, 145 | Streams: []grpc.StreamDesc{}, 146 | Metadata: "proto/kv.proto", 147 | } 148 | 149 | const ( 150 | AddHelper_Sum_FullMethodName = "/proto.AddHelper/Sum" 151 | ) 152 | 153 | // AddHelperClient is the client API for AddHelper service. 154 | // 155 | // 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. 156 | type AddHelperClient interface { 157 | Sum(ctx context.Context, in *SumRequest, opts ...grpc.CallOption) (*SumResponse, error) 158 | } 159 | 160 | type addHelperClient struct { 161 | cc grpc.ClientConnInterface 162 | } 163 | 164 | func NewAddHelperClient(cc grpc.ClientConnInterface) AddHelperClient { 165 | return &addHelperClient{cc} 166 | } 167 | 168 | func (c *addHelperClient) Sum(ctx context.Context, in *SumRequest, opts ...grpc.CallOption) (*SumResponse, error) { 169 | out := new(SumResponse) 170 | err := c.cc.Invoke(ctx, AddHelper_Sum_FullMethodName, in, out, opts...) 171 | if err != nil { 172 | return nil, err 173 | } 174 | return out, nil 175 | } 176 | 177 | // AddHelperServer is the server API for AddHelper service. 178 | // All implementations should embed UnimplementedAddHelperServer 179 | // for forward compatibility 180 | type AddHelperServer interface { 181 | Sum(context.Context, *SumRequest) (*SumResponse, error) 182 | } 183 | 184 | // UnimplementedAddHelperServer should be embedded to have forward compatible implementations. 185 | type UnimplementedAddHelperServer struct { 186 | } 187 | 188 | func (UnimplementedAddHelperServer) Sum(context.Context, *SumRequest) (*SumResponse, error) { 189 | return nil, status.Errorf(codes.Unimplemented, "method Sum not implemented") 190 | } 191 | 192 | // UnsafeAddHelperServer may be embedded to opt out of forward compatibility for this service. 193 | // Use of this interface is not recommended, as added methods to AddHelperServer will 194 | // result in compilation errors. 195 | type UnsafeAddHelperServer interface { 196 | mustEmbedUnimplementedAddHelperServer() 197 | } 198 | 199 | func RegisterAddHelperServer(s grpc.ServiceRegistrar, srv AddHelperServer) { 200 | s.RegisterService(&AddHelper_ServiceDesc, srv) 201 | } 202 | 203 | func _AddHelper_Sum_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 204 | in := new(SumRequest) 205 | if err := dec(in); err != nil { 206 | return nil, err 207 | } 208 | if interceptor == nil { 209 | return srv.(AddHelperServer).Sum(ctx, in) 210 | } 211 | info := &grpc.UnaryServerInfo{ 212 | Server: srv, 213 | FullMethod: AddHelper_Sum_FullMethodName, 214 | } 215 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 216 | return srv.(AddHelperServer).Sum(ctx, req.(*SumRequest)) 217 | } 218 | return interceptor(ctx, in, info, handler) 219 | } 220 | 221 | // AddHelper_ServiceDesc is the grpc.ServiceDesc for AddHelper service. 222 | // It's only intended for direct use with grpc.RegisterService, 223 | // and not to be introspected or modified (even as a copy) 224 | var AddHelper_ServiceDesc = grpc.ServiceDesc{ 225 | ServiceName: "proto.AddHelper", 226 | HandlerType: (*AddHelperServer)(nil), 227 | Methods: []grpc.MethodDesc{ 228 | { 229 | MethodName: "Sum", 230 | Handler: _AddHelper_Sum_Handler, 231 | }, 232 | }, 233 | Streams: []grpc.StreamDesc{}, 234 | Metadata: "proto/kv.proto", 235 | } 236 | -------------------------------------------------------------------------------- /examples/bidirectional/shared/grpc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package shared 5 | 6 | import ( 7 | "context" 8 | 9 | hclog "github.com/hashicorp/go-hclog" 10 | plugin "github.com/hashicorp/go-plugin" 11 | "github.com/hashicorp/go-plugin/examples/bidirectional/proto" 12 | "google.golang.org/grpc" 13 | ) 14 | 15 | // GRPCClient is an implementation of KV that talks over RPC. 16 | type GRPCClient struct { 17 | broker *plugin.GRPCBroker 18 | client proto.CounterClient 19 | } 20 | 21 | func (m *GRPCClient) Put(key string, value int64, a AddHelper) error { 22 | addHelperServer := &GRPCAddHelperServer{Impl: a} 23 | 24 | var s *grpc.Server 25 | serverFunc := func(opts []grpc.ServerOption) *grpc.Server { 26 | s = grpc.NewServer(opts...) 27 | proto.RegisterAddHelperServer(s, addHelperServer) 28 | 29 | return s 30 | } 31 | 32 | brokerID := m.broker.NextId() 33 | go m.broker.AcceptAndServe(brokerID, serverFunc) 34 | 35 | _, err := m.client.Put(context.Background(), &proto.PutRequest{ 36 | AddServer: brokerID, 37 | Key: key, 38 | Value: value, 39 | }) 40 | 41 | s.Stop() 42 | return err 43 | } 44 | 45 | func (m *GRPCClient) Get(key string) (int64, error) { 46 | resp, err := m.client.Get(context.Background(), &proto.GetRequest{ 47 | Key: key, 48 | }) 49 | if err != nil { 50 | return 0, err 51 | } 52 | 53 | return resp.Value, nil 54 | } 55 | 56 | // Here is the gRPC server that GRPCClient talks to. 57 | type GRPCServer struct { 58 | // This is the real implementation 59 | Impl Counter 60 | 61 | broker *plugin.GRPCBroker 62 | } 63 | 64 | func (m *GRPCServer) Put(ctx context.Context, req *proto.PutRequest) (*proto.Empty, error) { 65 | conn, err := m.broker.Dial(req.AddServer) 66 | if err != nil { 67 | return nil, err 68 | } 69 | defer func() { _ = conn.Close() }() 70 | 71 | a := &GRPCAddHelperClient{proto.NewAddHelperClient(conn)} 72 | return &proto.Empty{}, m.Impl.Put(req.Key, req.Value, a) 73 | } 74 | 75 | func (m *GRPCServer) Get(ctx context.Context, req *proto.GetRequest) (*proto.GetResponse, error) { 76 | v, err := m.Impl.Get(req.Key) 77 | return &proto.GetResponse{Value: v}, err 78 | } 79 | 80 | // GRPCClient is an implementation of KV that talks over RPC. 81 | type GRPCAddHelperClient struct{ client proto.AddHelperClient } 82 | 83 | func (m *GRPCAddHelperClient) Sum(a, b int64) (int64, error) { 84 | resp, err := m.client.Sum(context.Background(), &proto.SumRequest{ 85 | A: a, 86 | B: b, 87 | }) 88 | if err != nil { 89 | hclog.Default().Info("add.Sum", "client", "start", "err", err) 90 | return 0, err 91 | } 92 | return resp.R, err 93 | } 94 | 95 | // Here is the gRPC server that GRPCClient talks to. 96 | type GRPCAddHelperServer struct { 97 | // This is the real implementation 98 | Impl AddHelper 99 | } 100 | 101 | func (m *GRPCAddHelperServer) Sum(ctx context.Context, req *proto.SumRequest) (resp *proto.SumResponse, err error) { 102 | r, err := m.Impl.Sum(req.A, req.B) 103 | if err != nil { 104 | return nil, err 105 | } 106 | return &proto.SumResponse{R: r}, err 107 | } 108 | -------------------------------------------------------------------------------- /examples/bidirectional/shared/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package shared contains shared data between the host and plugins. 5 | package shared 6 | 7 | import ( 8 | "context" 9 | 10 | "google.golang.org/grpc" 11 | 12 | "github.com/hashicorp/go-plugin" 13 | "github.com/hashicorp/go-plugin/examples/bidirectional/proto" 14 | ) 15 | 16 | // Handshake is a common handshake that is shared by plugin and host. 17 | var Handshake = plugin.HandshakeConfig{ 18 | ProtocolVersion: 1, 19 | MagicCookieKey: "BASIC_PLUGIN", 20 | MagicCookieValue: "hello", 21 | } 22 | 23 | // PluginMap is the map of plugins we can dispense. 24 | var PluginMap = map[string]plugin.Plugin{ 25 | "counter": &CounterPlugin{}, 26 | } 27 | 28 | type AddHelper interface { 29 | Sum(int64, int64) (int64, error) 30 | } 31 | 32 | // KV is the interface that we're exposing as a plugin. 33 | type Counter interface { 34 | Put(key string, value int64, a AddHelper) error 35 | Get(key string) (int64, error) 36 | } 37 | 38 | // This is the implementation of plugin.Plugin so we can serve/consume this. 39 | // We also implement GRPCPlugin so that this plugin can be served over 40 | // gRPC. 41 | type CounterPlugin struct { 42 | plugin.NetRPCUnsupportedPlugin 43 | // Concrete implementation, written in Go. This is only used for plugins 44 | // that are written in Go. 45 | Impl Counter 46 | } 47 | 48 | func (p *CounterPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 49 | proto.RegisterCounterServer(s, &GRPCServer{ 50 | Impl: p.Impl, 51 | broker: broker, 52 | }) 53 | return nil 54 | } 55 | 56 | func (p *CounterPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 57 | return &GRPCClient{ 58 | client: proto.NewCounterClient(c), 59 | broker: broker, 60 | }, nil 61 | } 62 | 63 | var _ plugin.GRPCPlugin = &CounterPlugin{} 64 | -------------------------------------------------------------------------------- /examples/grpc/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | /kv 3 | /kv-* 4 | /kv_* 5 | -------------------------------------------------------------------------------- /examples/grpc/README.md: -------------------------------------------------------------------------------- 1 | # KV Example 2 | 3 | This example builds a simple key/value store CLI where the mechanism 4 | for storing and retrieving keys is pluggable. To build this example: 5 | 6 | ```sh 7 | # This builds the main CLI 8 | $ go build -o kv 9 | 10 | # This builds the plugin written in Go 11 | $ go build -o kv-go-grpc ./plugin-go-grpc 12 | 13 | # This tells the KV binary to use the "kv-go-grpc" binary 14 | $ export KV_PLUGIN="./kv-go-grpc" 15 | 16 | # Read and write 17 | $ ./kv put hello world 18 | 19 | $ ./kv get hello 20 | world 21 | ``` 22 | 23 | ### Plugin: plugin-go-grpc 24 | 25 | This plugin uses gRPC to serve a plugin that is written in Go: 26 | 27 | ``` 28 | # This builds the plugin written in Go 29 | $ go build -o kv-go-grpc ./plugin-go-grpc 30 | 31 | # This tells the KV binary to use the "kv-go-grpc" binary 32 | $ export KV_PLUGIN="./kv-go-grpc" 33 | ``` 34 | 35 | ### Plugin: plugin-go-netrpc 36 | 37 | This plugin uses the builtin Go net/rpc mechanism to serve the plugin: 38 | 39 | ``` 40 | # This builds the plugin written in Go 41 | $ go build -o kv-go-netrpc ./plugin-go-netrpc 42 | 43 | # This tells the KV binary to use the "kv-go-netrpc" binary 44 | $ export KV_PLUGIN="./kv-go-netrpc" 45 | ``` 46 | 47 | ### Plugin: plugin-python 48 | 49 | This plugin is written in Python: 50 | 51 | ``` 52 | $ python -m venv plugin-python/.venv 53 | $ source plugin-python/.venv/bin/activate 54 | $ pip install -r plugin-python/requirements.txt 55 | $ export KV_PLUGIN="python plugin-python/plugin.py" 56 | ``` 57 | 58 | ## Updating the Protocol 59 | 60 | If you update the protocol buffers file, you can regenerate the file 61 | using the following command from this directory. You do not need to run 62 | this if you're just trying the example. 63 | 64 | ```sh 65 | $ buf generate 66 | ``` 67 | -------------------------------------------------------------------------------- /examples/grpc/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: v1 5 | plugins: 6 | - plugin: buf.build/protocolbuffers/go 7 | out: . 8 | opt: 9 | - paths=source_relative 10 | - plugin: buf.build/grpc/go:v1.3.0 11 | out: . 12 | opt: 13 | - paths=source_relative 14 | - require_unimplemented_servers=false 15 | - plugin: buf.build/protocolbuffers/python:v24.4 16 | out: plugin-python 17 | - plugin: buf.build/grpc/python:v1.58.1 18 | out: plugin-python 19 | -------------------------------------------------------------------------------- /examples/grpc/buf.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | version: v1 5 | -------------------------------------------------------------------------------- /examples/grpc/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/exec" 12 | 13 | "github.com/hashicorp/go-plugin" 14 | "github.com/hashicorp/go-plugin/examples/grpc/shared" 15 | ) 16 | 17 | func run() error { 18 | // We're a host. Start by launching the plugin process. 19 | client := plugin.NewClient(&plugin.ClientConfig{ 20 | HandshakeConfig: shared.Handshake, 21 | Plugins: shared.PluginMap, 22 | Cmd: exec.Command("sh", "-c", os.Getenv("KV_PLUGIN")), 23 | AllowedProtocols: []plugin.Protocol{ 24 | plugin.ProtocolNetRPC, plugin.ProtocolGRPC}, 25 | }) 26 | defer client.Kill() 27 | 28 | // Connect via RPC 29 | rpcClient, err := client.Client() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Request the plugin 35 | raw, err := rpcClient.Dispense("kv_grpc") 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // We should have a KV store now! This feels like a normal interface 41 | // implementation but is in fact over an RPC connection. 42 | kv := raw.(shared.KV) 43 | os.Args = os.Args[1:] 44 | switch os.Args[0] { 45 | case "get": 46 | result, err := kv.Get(os.Args[1]) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | fmt.Println(string(result)) 52 | 53 | case "put": 54 | err := kv.Put(os.Args[1], []byte(os.Args[2])) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | default: 60 | return fmt.Errorf("please only use 'get' or 'put', given: %q", os.Args[0]) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func main() { 67 | // We don't want to see the plugin logs. 68 | log.SetOutput(io.Discard) 69 | 70 | if err := run(); err != nil { 71 | fmt.Printf("error: %+v\n", err) 72 | os.Exit(1) 73 | } 74 | 75 | os.Exit(0) 76 | } 77 | -------------------------------------------------------------------------------- /examples/grpc/plugin-go-grpc/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/hashicorp/go-plugin" 11 | "github.com/hashicorp/go-plugin/examples/grpc/shared" 12 | ) 13 | 14 | // Here is a real implementation of KV that writes to a local file with 15 | // the key name and the contents are the value of the key. 16 | type KV struct{} 17 | 18 | func (KV) Put(key string, value []byte) error { 19 | value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-grpc", string(value))) 20 | return os.WriteFile("kv_"+key, value, 0644) 21 | } 22 | 23 | func (KV) Get(key string) ([]byte, error) { 24 | return os.ReadFile("kv_" + key) 25 | } 26 | 27 | func main() { 28 | plugin.Serve(&plugin.ServeConfig{ 29 | HandshakeConfig: shared.Handshake, 30 | Plugins: map[string]plugin.Plugin{ 31 | "kv": &shared.KVGRPCPlugin{Impl: &KV{}}, 32 | }, 33 | 34 | // A non-nil value here enables gRPC serving for this plugin... 35 | GRPCServer: plugin.DefaultGRPCServer, 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /examples/grpc/plugin-go-netrpc/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/hashicorp/go-plugin" 11 | "github.com/hashicorp/go-plugin/examples/grpc/shared" 12 | ) 13 | 14 | // Here is a real implementation of KV that writes to a local file with 15 | // the key name and the contents are the value of the key. 16 | type KV struct{} 17 | 18 | func (KV) Put(key string, value []byte) error { 19 | value = []byte(fmt.Sprintf("%s\n\nWritten from plugin-go-netrpc", string(value))) 20 | return os.WriteFile("kv_"+key, value, 0644) 21 | } 22 | 23 | func (KV) Get(key string) ([]byte, error) { 24 | return os.ReadFile("kv_" + key) 25 | } 26 | 27 | func main() { 28 | plugin.Serve(&plugin.ServeConfig{ 29 | HandshakeConfig: shared.Handshake, 30 | Plugins: map[string]plugin.Plugin{ 31 | "kv": &shared.KVPlugin{Impl: &KV{}}, 32 | }, 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /examples/grpc/plugin-python/.gitignore: -------------------------------------------------------------------------------- 1 | /.venv -------------------------------------------------------------------------------- /examples/grpc/plugin-python/plugin.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | from concurrent import futures 5 | import sys 6 | import time 7 | 8 | import grpc 9 | 10 | from proto import kv_pb2 11 | from proto import kv_pb2_grpc 12 | 13 | from grpc_health.v1.health import HealthServicer 14 | from grpc_health.v1 import health_pb2, health_pb2_grpc 15 | 16 | class KVServicer(kv_pb2_grpc.KVServicer): 17 | """Implementation of KV service.""" 18 | 19 | def Get(self, request, context): 20 | filename = "kv_"+request.key 21 | with open(filename, 'r+b') as f: 22 | result = kv_pb2.GetResponse() 23 | result.value = f.read() 24 | return result 25 | 26 | def Put(self, request, context): 27 | filename = "kv_"+request.key 28 | value = "{0}\n\nWritten from plugin-python".format(request.value) 29 | with open(filename, 'w') as f: 30 | f.write(value) 31 | 32 | return kv_pb2.Empty() 33 | 34 | def serve(): 35 | # We need to build a health service to work with go-plugin 36 | health = HealthServicer() 37 | health.set("plugin", health_pb2.HealthCheckResponse.ServingStatus.Value('SERVING')) 38 | 39 | # Start the server. 40 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) 41 | kv_pb2_grpc.add_KVServicer_to_server(KVServicer(), server) 42 | health_pb2_grpc.add_HealthServicer_to_server(health, server) 43 | server.add_insecure_port('127.0.0.1:1234') 44 | server.start() 45 | 46 | # Output information 47 | print("1|1|tcp|127.0.0.1:1234|grpc") 48 | sys.stdout.flush() 49 | 50 | try: 51 | while True: 52 | time.sleep(60 * 60 * 24) 53 | except KeyboardInterrupt: 54 | server.stop(0) 55 | 56 | if __name__ == '__main__': 57 | serve() 58 | -------------------------------------------------------------------------------- /examples/grpc/plugin-python/proto/kv_pb2.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # -*- coding: utf-8 -*- 5 | # Generated by the protocol buffer compiler. DO NOT EDIT! 6 | # source: proto/kv.proto 7 | """Generated protocol buffer code.""" 8 | from google.protobuf import descriptor as _descriptor 9 | from google.protobuf import descriptor_pool as _descriptor_pool 10 | from google.protobuf import symbol_database as _symbol_database 11 | from google.protobuf.internal import builder as _builder 12 | # @@protoc_insertion_point(imports) 13 | 14 | _sym_db = _symbol_database.Default() 15 | 16 | 17 | 18 | 19 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eproto/kv.proto\x12\x05proto\"\x1e\n\nGetRequest\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\"#\n\x0bGetResponse\x12\x14\n\x05value\x18\x01 \x01(\x0cR\x05value\"4\n\nPutRequest\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\x0cR\x05value\"\x07\n\x05\x45mpty2Z\n\x02KV\x12,\n\x03Get\x12\x11.proto.GetRequest\x1a\x12.proto.GetResponse\x12&\n\x03Put\x12\x11.proto.PutRequest\x1a\x0c.proto.EmptyB\tZ\x07./protob\x06proto3') 20 | 21 | _globals = globals() 22 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 23 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'proto.kv_pb2', _globals) 24 | if _descriptor._USE_C_DESCRIPTORS == False: 25 | _globals['DESCRIPTOR']._options = None 26 | _globals['DESCRIPTOR']._serialized_options = b'Z\007./proto' 27 | _globals['_GETREQUEST']._serialized_start=25 28 | _globals['_GETREQUEST']._serialized_end=55 29 | _globals['_GETRESPONSE']._serialized_start=57 30 | _globals['_GETRESPONSE']._serialized_end=92 31 | _globals['_PUTREQUEST']._serialized_start=94 32 | _globals['_PUTREQUEST']._serialized_end=146 33 | _globals['_EMPTY']._serialized_start=148 34 | _globals['_EMPTY']._serialized_end=155 35 | _globals['_KV']._serialized_start=157 36 | _globals['_KV']._serialized_end=247 37 | # @@protoc_insertion_point(module_scope) 38 | -------------------------------------------------------------------------------- /examples/grpc/plugin-python/proto/kv_pb2_grpc.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) HashiCorp, Inc. 2 | # SPDX-License-Identifier: MPL-2.0 3 | 4 | # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! 5 | """Client and server classes corresponding to protobuf-defined services.""" 6 | import grpc 7 | 8 | from proto import kv_pb2 as proto_dot_kv__pb2 9 | 10 | 11 | class KVStub(object): 12 | """Missing associated documentation comment in .proto file.""" 13 | 14 | def __init__(self, channel): 15 | """Constructor. 16 | 17 | Args: 18 | channel: A grpc.Channel. 19 | """ 20 | self.Get = channel.unary_unary( 21 | '/proto.KV/Get', 22 | request_serializer=proto_dot_kv__pb2.GetRequest.SerializeToString, 23 | response_deserializer=proto_dot_kv__pb2.GetResponse.FromString, 24 | ) 25 | self.Put = channel.unary_unary( 26 | '/proto.KV/Put', 27 | request_serializer=proto_dot_kv__pb2.PutRequest.SerializeToString, 28 | response_deserializer=proto_dot_kv__pb2.Empty.FromString, 29 | ) 30 | 31 | 32 | class KVServicer(object): 33 | """Missing associated documentation comment in .proto file.""" 34 | 35 | def Get(self, request, context): 36 | """Missing associated documentation comment in .proto file.""" 37 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 38 | context.set_details('Method not implemented!') 39 | raise NotImplementedError('Method not implemented!') 40 | 41 | def Put(self, request, context): 42 | """Missing associated documentation comment in .proto file.""" 43 | context.set_code(grpc.StatusCode.UNIMPLEMENTED) 44 | context.set_details('Method not implemented!') 45 | raise NotImplementedError('Method not implemented!') 46 | 47 | 48 | def add_KVServicer_to_server(servicer, server): 49 | rpc_method_handlers = { 50 | 'Get': grpc.unary_unary_rpc_method_handler( 51 | servicer.Get, 52 | request_deserializer=proto_dot_kv__pb2.GetRequest.FromString, 53 | response_serializer=proto_dot_kv__pb2.GetResponse.SerializeToString, 54 | ), 55 | 'Put': grpc.unary_unary_rpc_method_handler( 56 | servicer.Put, 57 | request_deserializer=proto_dot_kv__pb2.PutRequest.FromString, 58 | response_serializer=proto_dot_kv__pb2.Empty.SerializeToString, 59 | ), 60 | } 61 | generic_handler = grpc.method_handlers_generic_handler( 62 | 'proto.KV', rpc_method_handlers) 63 | server.add_generic_rpc_handlers((generic_handler,)) 64 | 65 | 66 | # This class is part of an EXPERIMENTAL API. 67 | class KV(object): 68 | """Missing associated documentation comment in .proto file.""" 69 | 70 | @staticmethod 71 | def Get(request, 72 | target, 73 | options=(), 74 | channel_credentials=None, 75 | call_credentials=None, 76 | insecure=False, 77 | compression=None, 78 | wait_for_ready=None, 79 | timeout=None, 80 | metadata=None): 81 | return grpc.experimental.unary_unary(request, target, '/proto.KV/Get', 82 | proto_dot_kv__pb2.GetRequest.SerializeToString, 83 | proto_dot_kv__pb2.GetResponse.FromString, 84 | options, channel_credentials, 85 | insecure, call_credentials, compression, wait_for_ready, timeout, metadata) 86 | 87 | @staticmethod 88 | def Put(request, 89 | target, 90 | options=(), 91 | channel_credentials=None, 92 | call_credentials=None, 93 | insecure=False, 94 | compression=None, 95 | wait_for_ready=None, 96 | timeout=None, 97 | metadata=None): 98 | return grpc.experimental.unary_unary(request, target, '/proto.KV/Put', 99 | proto_dot_kv__pb2.PutRequest.SerializeToString, 100 | proto_dot_kv__pb2.Empty.FromString, 101 | options, channel_credentials, 102 | insecure, call_credentials, compression, wait_for_ready, timeout, metadata) 103 | -------------------------------------------------------------------------------- /examples/grpc/plugin-python/requirements.txt: -------------------------------------------------------------------------------- 1 | grpcio==1.59.0 2 | grpcio-health-checking==1.59.0 3 | protobuf==4.25.8 4 | -------------------------------------------------------------------------------- /examples/grpc/proto/kv.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | syntax = "proto3"; 5 | package proto; 6 | option go_package = "./proto"; 7 | 8 | message GetRequest { 9 | string key = 1; 10 | } 11 | 12 | message GetResponse { 13 | bytes value = 1; 14 | } 15 | 16 | message PutRequest { 17 | string key = 1; 18 | bytes value = 2; 19 | } 20 | 21 | message Empty {} 22 | 23 | service KV { 24 | rpc Get(GetRequest) returns (GetResponse); 25 | rpc Put(PutRequest) returns (Empty); 26 | } 27 | -------------------------------------------------------------------------------- /examples/grpc/proto/kv_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 5 | // versions: 6 | // - protoc-gen-go-grpc v1.3.0 7 | // - protoc (unknown) 8 | // source: proto/kv.proto 9 | 10 | package proto 11 | 12 | import ( 13 | context "context" 14 | grpc "google.golang.org/grpc" 15 | codes "google.golang.org/grpc/codes" 16 | status "google.golang.org/grpc/status" 17 | ) 18 | 19 | // This is a compile-time assertion to ensure that this generated file 20 | // is compatible with the grpc package it is being compiled against. 21 | // Requires gRPC-Go v1.32.0 or later. 22 | const _ = grpc.SupportPackageIsVersion7 23 | 24 | const ( 25 | KV_Get_FullMethodName = "/proto.KV/Get" 26 | KV_Put_FullMethodName = "/proto.KV/Put" 27 | ) 28 | 29 | // KVClient is the client API for KV service. 30 | // 31 | // 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. 32 | type KVClient interface { 33 | Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) 34 | Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error) 35 | } 36 | 37 | type kVClient struct { 38 | cc grpc.ClientConnInterface 39 | } 40 | 41 | func NewKVClient(cc grpc.ClientConnInterface) KVClient { 42 | return &kVClient{cc} 43 | } 44 | 45 | func (c *kVClient) Get(ctx context.Context, in *GetRequest, opts ...grpc.CallOption) (*GetResponse, error) { 46 | out := new(GetResponse) 47 | err := c.cc.Invoke(ctx, KV_Get_FullMethodName, in, out, opts...) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return out, nil 52 | } 53 | 54 | func (c *kVClient) Put(ctx context.Context, in *PutRequest, opts ...grpc.CallOption) (*Empty, error) { 55 | out := new(Empty) 56 | err := c.cc.Invoke(ctx, KV_Put_FullMethodName, in, out, opts...) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return out, nil 61 | } 62 | 63 | // KVServer is the server API for KV service. 64 | // All implementations should embed UnimplementedKVServer 65 | // for forward compatibility 66 | type KVServer interface { 67 | Get(context.Context, *GetRequest) (*GetResponse, error) 68 | Put(context.Context, *PutRequest) (*Empty, error) 69 | } 70 | 71 | // UnimplementedKVServer should be embedded to have forward compatible implementations. 72 | type UnimplementedKVServer struct { 73 | } 74 | 75 | func (UnimplementedKVServer) Get(context.Context, *GetRequest) (*GetResponse, error) { 76 | return nil, status.Errorf(codes.Unimplemented, "method Get not implemented") 77 | } 78 | func (UnimplementedKVServer) Put(context.Context, *PutRequest) (*Empty, error) { 79 | return nil, status.Errorf(codes.Unimplemented, "method Put not implemented") 80 | } 81 | 82 | // UnsafeKVServer may be embedded to opt out of forward compatibility for this service. 83 | // Use of this interface is not recommended, as added methods to KVServer will 84 | // result in compilation errors. 85 | type UnsafeKVServer interface { 86 | mustEmbedUnimplementedKVServer() 87 | } 88 | 89 | func RegisterKVServer(s grpc.ServiceRegistrar, srv KVServer) { 90 | s.RegisterService(&KV_ServiceDesc, srv) 91 | } 92 | 93 | func _KV_Get_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 94 | in := new(GetRequest) 95 | if err := dec(in); err != nil { 96 | return nil, err 97 | } 98 | if interceptor == nil { 99 | return srv.(KVServer).Get(ctx, in) 100 | } 101 | info := &grpc.UnaryServerInfo{ 102 | Server: srv, 103 | FullMethod: KV_Get_FullMethodName, 104 | } 105 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 106 | return srv.(KVServer).Get(ctx, req.(*GetRequest)) 107 | } 108 | return interceptor(ctx, in, info, handler) 109 | } 110 | 111 | func _KV_Put_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 112 | in := new(PutRequest) 113 | if err := dec(in); err != nil { 114 | return nil, err 115 | } 116 | if interceptor == nil { 117 | return srv.(KVServer).Put(ctx, in) 118 | } 119 | info := &grpc.UnaryServerInfo{ 120 | Server: srv, 121 | FullMethod: KV_Put_FullMethodName, 122 | } 123 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 124 | return srv.(KVServer).Put(ctx, req.(*PutRequest)) 125 | } 126 | return interceptor(ctx, in, info, handler) 127 | } 128 | 129 | // KV_ServiceDesc is the grpc.ServiceDesc for KV service. 130 | // It's only intended for direct use with grpc.RegisterService, 131 | // and not to be introspected or modified (even as a copy) 132 | var KV_ServiceDesc = grpc.ServiceDesc{ 133 | ServiceName: "proto.KV", 134 | HandlerType: (*KVServer)(nil), 135 | Methods: []grpc.MethodDesc{ 136 | { 137 | MethodName: "Get", 138 | Handler: _KV_Get_Handler, 139 | }, 140 | { 141 | MethodName: "Put", 142 | Handler: _KV_Put_Handler, 143 | }, 144 | }, 145 | Streams: []grpc.StreamDesc{}, 146 | Metadata: "proto/kv.proto", 147 | } 148 | -------------------------------------------------------------------------------- /examples/grpc/shared/grpc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package shared 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-plugin/examples/grpc/proto" 10 | ) 11 | 12 | // GRPCClient is an implementation of KV that talks over RPC. 13 | type GRPCClient struct{ client proto.KVClient } 14 | 15 | func (m *GRPCClient) Put(key string, value []byte) error { 16 | _, err := m.client.Put(context.Background(), &proto.PutRequest{ 17 | Key: key, 18 | Value: value, 19 | }) 20 | return err 21 | } 22 | 23 | func (m *GRPCClient) Get(key string) ([]byte, error) { 24 | resp, err := m.client.Get(context.Background(), &proto.GetRequest{ 25 | Key: key, 26 | }) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return resp.Value, nil 32 | } 33 | 34 | // Here is the gRPC server that GRPCClient talks to. 35 | type GRPCServer struct { 36 | // This is the real implementation 37 | Impl KV 38 | } 39 | 40 | func (m *GRPCServer) Put( 41 | ctx context.Context, 42 | req *proto.PutRequest) (*proto.Empty, error) { 43 | return &proto.Empty{}, m.Impl.Put(req.Key, req.Value) 44 | } 45 | 46 | func (m *GRPCServer) Get( 47 | ctx context.Context, 48 | req *proto.GetRequest) (*proto.GetResponse, error) { 49 | v, err := m.Impl.Get(req.Key) 50 | return &proto.GetResponse{Value: v}, err 51 | } 52 | -------------------------------------------------------------------------------- /examples/grpc/shared/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Package shared contains shared data between the host and plugins. 5 | package shared 6 | 7 | import ( 8 | "context" 9 | "net/rpc" 10 | 11 | "google.golang.org/grpc" 12 | 13 | "github.com/hashicorp/go-plugin" 14 | "github.com/hashicorp/go-plugin/examples/grpc/proto" 15 | ) 16 | 17 | // Handshake is a common handshake that is shared by plugin and host. 18 | var Handshake = plugin.HandshakeConfig{ 19 | // This isn't required when using VersionedPlugins 20 | ProtocolVersion: 1, 21 | MagicCookieKey: "BASIC_PLUGIN", 22 | MagicCookieValue: "hello", 23 | } 24 | 25 | // PluginMap is the map of plugins we can dispense. 26 | var PluginMap = map[string]plugin.Plugin{ 27 | "kv_grpc": &KVGRPCPlugin{}, 28 | "kv": &KVPlugin{}, 29 | } 30 | 31 | // KV is the interface that we're exposing as a plugin. 32 | type KV interface { 33 | Put(key string, value []byte) error 34 | Get(key string) ([]byte, error) 35 | } 36 | 37 | // This is the implementation of plugin.Plugin so we can serve/consume this. 38 | type KVPlugin struct { 39 | // Concrete implementation, written in Go. This is only used for plugins 40 | // that are written in Go. 41 | Impl KV 42 | } 43 | 44 | func (p *KVPlugin) Server(*plugin.MuxBroker) (interface{}, error) { 45 | return &RPCServer{Impl: p.Impl}, nil 46 | } 47 | 48 | func (*KVPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { 49 | return &RPCClient{client: c}, nil 50 | } 51 | 52 | // This is the implementation of plugin.GRPCPlugin so we can serve/consume this. 53 | type KVGRPCPlugin struct { 54 | // GRPCPlugin must still implement the Plugin interface 55 | plugin.Plugin 56 | // Concrete implementation, written in Go. This is only used for plugins 57 | // that are written in Go. 58 | Impl KV 59 | } 60 | 61 | func (p *KVGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 62 | proto.RegisterKVServer(s, &GRPCServer{Impl: p.Impl}) 63 | return nil 64 | } 65 | 66 | func (p *KVGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 67 | return &GRPCClient{client: proto.NewKVClient(c)}, nil 68 | } 69 | -------------------------------------------------------------------------------- /examples/grpc/shared/rpc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package shared 5 | 6 | import ( 7 | "net/rpc" 8 | ) 9 | 10 | // RPCClient is an implementation of KV that talks over RPC. 11 | type RPCClient struct{ client *rpc.Client } 12 | 13 | func (m *RPCClient) Put(key string, value []byte) error { 14 | // We don't expect a response, so we can just use interface{} 15 | var resp interface{} 16 | 17 | // The args are just going to be a map. A struct could be better. 18 | return m.client.Call("Plugin.Put", map[string]interface{}{ 19 | "key": key, 20 | "value": value, 21 | }, &resp) 22 | } 23 | 24 | func (m *RPCClient) Get(key string) ([]byte, error) { 25 | var resp []byte 26 | err := m.client.Call("Plugin.Get", key, &resp) 27 | return resp, err 28 | } 29 | 30 | // Here is the RPC server that RPCClient talks to, conforming to 31 | // the requirements of net/rpc 32 | type RPCServer struct { 33 | // This is the real implementation 34 | Impl KV 35 | } 36 | 37 | func (m *RPCServer) Put(args map[string]interface{}, resp *interface{}) error { 38 | return m.Impl.Put(args["key"].(string), args["value"].([]byte)) 39 | } 40 | 41 | func (m *RPCServer) Get(key string, resp *[]byte) error { 42 | v, err := m.Impl.Get(key) 43 | *resp = v 44 | return err 45 | } 46 | -------------------------------------------------------------------------------- /examples/negotiated/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | kv 3 | kv-* 4 | kv_* 5 | !kv_*.py 6 | -------------------------------------------------------------------------------- /examples/negotiated/README.md: -------------------------------------------------------------------------------- 1 | # Negotiated version KV Example 2 | 3 | This example builds a simple key/value store CLI where the plugin version can 4 | be negotiated between client and server. 5 | 6 | ```sh 7 | # This builds the main CLI 8 | $ go build -o kv 9 | 10 | # This builds the plugin written in Go 11 | $ go build -o kv-plugin ./plugin-go 12 | 13 | # Write a value using proto version 3 and grpc 14 | $ KV_PROTO=grpc ./kv put hello world 15 | 16 | # Read it back using proto version 2 and netrpc 17 | $ KV_PROTO=netrpc ./kv get hello 18 | world 19 | 20 | Written from plugin version 3 21 | Read by plugin version 2 22 | ``` 23 | 24 | # Negotiated Protocol 25 | 26 | The Client sends the list of available plugin versions to the server. When 27 | presented with a list of plugin versions, the server iterates over them in 28 | reverse, and uses the highest numbered match to choose the plugins to execute. 29 | If a legacy client is used and no versions are sent to the server, the server 30 | will default to the oldest version in its configuration. 31 | -------------------------------------------------------------------------------- /examples/negotiated/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "os/exec" 12 | 13 | "github.com/hashicorp/go-plugin" 14 | "github.com/hashicorp/go-plugin/examples/grpc/shared" 15 | ) 16 | 17 | func main() { 18 | // We don't want to see the plugin logs. 19 | log.SetOutput(io.Discard) 20 | 21 | plugins := map[int]plugin.PluginSet{} 22 | 23 | // Both version can be supported, but switch the implementation to 24 | // demonstrate version negoation. 25 | switch os.Getenv("KV_PROTO") { 26 | case "netrpc": 27 | plugins[2] = plugin.PluginSet{ 28 | "kv": &shared.KVPlugin{}, 29 | } 30 | case "grpc": 31 | plugins[3] = plugin.PluginSet{ 32 | "kv": &shared.KVGRPCPlugin{}, 33 | } 34 | default: 35 | fmt.Println("must set KV_PROTO to netrpc or grpc") 36 | os.Exit(1) 37 | } 38 | 39 | // We're a host. Start by launching the plugin process. 40 | client := plugin.NewClient(&plugin.ClientConfig{ 41 | HandshakeConfig: shared.Handshake, 42 | VersionedPlugins: plugins, 43 | Cmd: exec.Command("./kv-plugin"), 44 | AllowedProtocols: []plugin.Protocol{ 45 | plugin.ProtocolNetRPC, plugin.ProtocolGRPC}, 46 | }) 47 | defer client.Kill() 48 | 49 | rpcClient, err := client.Client() 50 | if err != nil { 51 | fmt.Println("Error:", err.Error()) 52 | os.Exit(1) 53 | } 54 | 55 | // Request the plugin 56 | raw, err := rpcClient.Dispense("kv") 57 | if err != nil { 58 | fmt.Println("Error:", err.Error()) 59 | os.Exit(1) 60 | } 61 | 62 | // We should have a KV store now! This feels like a normal interface 63 | // implementation but is in fact over an RPC connection. 64 | kv := raw.(shared.KV) 65 | os.Args = os.Args[1:] 66 | switch os.Args[0] { 67 | case "get": 68 | result, err := kv.Get(os.Args[1]) 69 | if err != nil { 70 | fmt.Println("Error:", err.Error()) 71 | os.Exit(1) 72 | } 73 | 74 | fmt.Println(string(result)) 75 | 76 | case "put": 77 | err := kv.Put(os.Args[1], []byte(os.Args[2])) 78 | if err != nil { 79 | fmt.Println("Error:", err.Error()) 80 | os.Exit(1) 81 | } 82 | 83 | default: 84 | fmt.Println("Please only use 'get' or 'put'") 85 | os.Exit(1) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /examples/negotiated/plugin-go/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | 10 | "github.com/hashicorp/go-plugin" 11 | "github.com/hashicorp/go-plugin/examples/grpc/shared" 12 | ) 13 | 14 | // Here is a real implementation of KV that uses grpc and writes to a local 15 | // file with the key name and the contents are the value of the key. 16 | type KVGRPC struct{} 17 | 18 | func (KVGRPC) Put(key string, value []byte) error { 19 | value = []byte(fmt.Sprintf("%s\n\nWritten from plugin version 3\n", string(value))) 20 | return os.WriteFile("kv_"+key, value, 0644) 21 | } 22 | 23 | func (KVGRPC) Get(key string) ([]byte, error) { 24 | d, err := os.ReadFile("kv_" + key) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return append(d, []byte("Read by plugin version 3\n")...), nil 29 | } 30 | 31 | // Here is a real implementation of KV that writes to a local file with 32 | // the key name and the contents are the value of the key. 33 | type KV struct{} 34 | 35 | func (KV) Put(key string, value []byte) error { 36 | value = []byte(fmt.Sprintf("%s\n\nWritten from plugin version 2\n", string(value))) 37 | return os.WriteFile("kv_"+key, value, 0644) 38 | } 39 | 40 | func (KV) Get(key string) ([]byte, error) { 41 | d, err := os.ReadFile("kv_" + key) 42 | if err != nil { 43 | return nil, err 44 | } 45 | return append(d, []byte("Read by plugin version 2\n")...), nil 46 | } 47 | 48 | func main() { 49 | plugin.Serve(&plugin.ServeConfig{ 50 | HandshakeConfig: shared.Handshake, 51 | VersionedPlugins: map[int]plugin.PluginSet{ 52 | // Version 2 only uses NetRPC 53 | 2: { 54 | "kv": &shared.KVPlugin{Impl: &KV{}}, 55 | }, 56 | // Version 3 only uses GRPC 57 | 3: { 58 | "kv": &shared.KVGRPCPlugin{Impl: &KVGRPC{}}, 59 | }, 60 | }, 61 | 62 | // A non-nil value here enables gRPC serving for this plugin... 63 | GRPCServer: plugin.DefaultGRPCServer, 64 | }) 65 | } 66 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hashicorp/go-plugin 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/golang/protobuf v1.5.4 7 | github.com/hashicorp/go-hclog v1.6.3 8 | github.com/hashicorp/yamux v0.1.2 9 | github.com/jhump/protoreflect v1.17.0 10 | github.com/oklog/run v1.1.0 11 | google.golang.org/grpc v1.61.0 12 | google.golang.org/protobuf v1.36.6 13 | ) 14 | 15 | require ( 16 | github.com/bufbuild/protocompile v0.14.1 // indirect 17 | github.com/fatih/color v1.13.0 // indirect 18 | github.com/mattn/go-colorable v0.1.12 // indirect 19 | github.com/mattn/go-isatty v0.0.17 // indirect 20 | golang.org/x/net v0.38.0 // indirect 21 | golang.org/x/sys v0.31.0 // indirect 22 | golang.org/x/text v0.23.0 // indirect 23 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= 2 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 7 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 8 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 9 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 13 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 14 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= 15 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= 16 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= 17 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= 18 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 19 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 20 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 21 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 22 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 23 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 24 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 25 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 26 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 30 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 31 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 32 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 33 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 34 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 35 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 36 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 37 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 44 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 45 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 46 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 47 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14= 48 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA= 49 | google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= 50 | google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= 51 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 52 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 53 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 54 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 55 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 56 | -------------------------------------------------------------------------------- /grpc_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "context" 8 | "crypto/tls" 9 | "fmt" 10 | "math" 11 | "net" 12 | 13 | "github.com/hashicorp/go-plugin/internal/plugin" 14 | "google.golang.org/grpc" 15 | "google.golang.org/grpc/credentials" 16 | "google.golang.org/grpc/credentials/insecure" 17 | "google.golang.org/grpc/health/grpc_health_v1" 18 | ) 19 | 20 | func dialGRPCConn(tls *tls.Config, dialer func(context.Context, string) (net.Conn, error), dialOpts ...grpc.DialOption) (*grpc.ClientConn, error) { 21 | // Build dialing options. 22 | opts := make([]grpc.DialOption, 0) 23 | 24 | // We use a custom dialer so that we can connect over unix domain sockets. 25 | opts = append(opts, grpc.WithContextDialer(dialer)) 26 | 27 | // Fail right away 28 | opts = append(opts, grpc.FailOnNonTempDialError(true)) 29 | 30 | // If we have no TLS configuration set, we need to explicitly tell grpc 31 | // that we're connecting with an insecure connection. 32 | if tls == nil { 33 | opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) 34 | } else { 35 | opts = append(opts, grpc.WithTransportCredentials( 36 | credentials.NewTLS(tls))) 37 | } 38 | 39 | opts = append(opts, 40 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32)), 41 | grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(math.MaxInt32))) 42 | 43 | // Add our custom options if we have any 44 | opts = append(opts, dialOpts...) 45 | 46 | // Connect. Note the first parameter is unused because we use a custom 47 | // dialer that has the state to see the address. 48 | conn, err := grpc.Dial("unused", opts...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return conn, nil 54 | } 55 | 56 | // newGRPCClient creates a new GRPCClient. The Client argument is expected 57 | // to be successfully started already with a lock held. 58 | func newGRPCClient(doneCtx context.Context, c *Client) (*GRPCClient, error) { 59 | conn, err := dialGRPCConn(c.config.TLSConfig, c.dialer, c.config.GRPCDialOptions...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | muxer, err := c.getGRPCMuxer(c.address) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | // Start the broker. 70 | brokerGRPCClient := newGRPCBrokerClient(conn) 71 | broker := newGRPCBroker(brokerGRPCClient, c.config.TLSConfig, c.unixSocketCfg, c.runner, muxer) 72 | go broker.Run() 73 | go func() { _ = brokerGRPCClient.StartStream() }() 74 | 75 | // Start the stdio client 76 | stdioClient, err := newGRPCStdioClient(doneCtx, c.logger.Named("stdio"), conn) 77 | if err != nil { 78 | return nil, err 79 | } 80 | go stdioClient.Run(c.config.SyncStdout, c.config.SyncStderr) 81 | 82 | cl := &GRPCClient{ 83 | Conn: conn, 84 | Plugins: c.config.Plugins, 85 | doneCtx: doneCtx, 86 | broker: broker, 87 | controller: plugin.NewGRPCControllerClient(conn), 88 | } 89 | 90 | return cl, nil 91 | } 92 | 93 | // GRPCClient connects to a GRPCServer over gRPC to dispense plugin types. 94 | type GRPCClient struct { 95 | Conn *grpc.ClientConn 96 | Plugins map[string]Plugin 97 | 98 | doneCtx context.Context 99 | broker *GRPCBroker 100 | 101 | controller plugin.GRPCControllerClient 102 | } 103 | 104 | // ClientProtocol impl. 105 | func (c *GRPCClient) Close() error { 106 | _ = c.broker.Close() 107 | _, _ = c.controller.Shutdown(c.doneCtx, &plugin.Empty{}) 108 | return c.Conn.Close() 109 | } 110 | 111 | // ClientProtocol impl. 112 | func (c *GRPCClient) Dispense(name string) (interface{}, error) { 113 | raw, ok := c.Plugins[name] 114 | if !ok { 115 | return nil, fmt.Errorf("unknown plugin type: %s", name) 116 | } 117 | 118 | p, ok := raw.(GRPCPlugin) 119 | if !ok { 120 | return nil, fmt.Errorf("plugin %q doesn't support gRPC", name) 121 | } 122 | 123 | return p.GRPCClient(c.doneCtx, c.broker, c.Conn) 124 | } 125 | 126 | // ClientProtocol impl. 127 | func (c *GRPCClient) Ping() error { 128 | client := grpc_health_v1.NewHealthClient(c.Conn) 129 | _, err := client.Check(context.Background(), &grpc_health_v1.HealthCheckRequest{ 130 | Service: GRPCServiceName, 131 | }) 132 | 133 | return err 134 | } 135 | -------------------------------------------------------------------------------- /grpc_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "context" 8 | "reflect" 9 | "testing" 10 | 11 | grpctest "github.com/hashicorp/go-plugin/test/grpc" 12 | "github.com/jhump/protoreflect/grpcreflect" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | func TestGRPC_App(t *testing.T) { 17 | t.Run("default", func(t *testing.T) { 18 | testGRPCClientApp(t, false) 19 | }) 20 | t.Run("mux", func(t *testing.T) { 21 | testGRPCClientApp(t, true) 22 | }) 23 | } 24 | 25 | func testGRPCClientApp(t *testing.T, multiplex bool) { 26 | client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{ 27 | "test": new(testGRPCInterfacePlugin), 28 | }) 29 | defer func() { _ = client.Close() }() 30 | defer server.Stop() 31 | 32 | raw, err := client.Dispense("test") 33 | if err != nil { 34 | t.Fatalf("err: %s", err) 35 | } 36 | 37 | impl, ok := raw.(testInterface) 38 | if !ok { 39 | t.Fatalf("bad: %#v", raw) 40 | } 41 | 42 | result := impl.Double(21) 43 | if result != 42 { 44 | t.Fatalf("bad: %#v", result) 45 | } 46 | 47 | err = impl.Bidirectional() 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | } 52 | 53 | func TestGRPCConn_BidirectionalPing(t *testing.T) { 54 | conn, _ := TestGRPCConn(t, func(s *grpc.Server) { 55 | grpctest.RegisterPingPongServer(s, &pingPongServer{}) 56 | }) 57 | defer func() { _ = conn.Close() }() 58 | pingPongClient := grpctest.NewPingPongClient(conn) 59 | 60 | pResp, err := pingPongClient.Ping(context.Background(), &grpctest.PingRequest{}) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | if pResp.Msg != "pong" { 65 | t.Fatal("Bad PingPong") 66 | } 67 | } 68 | 69 | func TestGRPCC_Stream(t *testing.T) { 70 | t.Run("default", func(t *testing.T) { 71 | testGRPCStream(t, false) 72 | }) 73 | t.Run("mux", func(t *testing.T) { 74 | testGRPCStream(t, true) 75 | }) 76 | } 77 | 78 | func testGRPCStream(t *testing.T, multiplex bool) { 79 | client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{ 80 | "test": new(testGRPCInterfacePlugin), 81 | }) 82 | defer func() { _ = client.Close() }() 83 | defer server.Stop() 84 | 85 | raw, err := client.Dispense("test") 86 | if err != nil { 87 | t.Fatalf("err: %s", err) 88 | } 89 | 90 | impl, ok := raw.(testStreamer) 91 | if !ok { 92 | t.Fatalf("bad: %#v", raw) 93 | } 94 | 95 | expected := []int32{21, 22, 23, 24, 25, 26} 96 | result, err := impl.Stream(21, 27) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | 101 | if !reflect.DeepEqual(result, expected) { 102 | t.Fatalf("expected: %v\ngot: %v", expected, result) 103 | } 104 | } 105 | 106 | func TestGRPC_Ping(t *testing.T) { 107 | t.Run("default", func(t *testing.T) { 108 | testGRPCClientPing(t, false) 109 | }) 110 | t.Run("mux", func(t *testing.T) { 111 | testGRPCClientPing(t, true) 112 | }) 113 | } 114 | 115 | func testGRPCClientPing(t *testing.T, multiplex bool) { 116 | client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{ 117 | "test": new(testGRPCInterfacePlugin), 118 | }) 119 | defer func() { _ = client.Close() }() 120 | defer server.Stop() 121 | 122 | // Run a couple pings 123 | if err := client.Ping(); err != nil { 124 | t.Fatalf("err: %s", err) 125 | } 126 | if err := client.Ping(); err != nil { 127 | t.Fatalf("err: %s", err) 128 | } 129 | 130 | // Close the remote end 131 | server.server.Stop() 132 | 133 | // Test ping fails 134 | if err := client.Ping(); err == nil { 135 | t.Fatal("should error") 136 | } 137 | } 138 | 139 | func TestGRPC_Reflection(t *testing.T) { 140 | t.Run("default", func(t *testing.T) { 141 | testGRPCClientReflection(t, false) 142 | }) 143 | t.Run("mux", func(t *testing.T) { 144 | testGRPCClientReflection(t, true) 145 | }) 146 | } 147 | 148 | func testGRPCClientReflection(t *testing.T, multiplex bool) { 149 | ctx := context.Background() 150 | 151 | client, server := TestPluginGRPCConn(t, multiplex, map[string]Plugin{ 152 | "test": new(testGRPCInterfacePlugin), 153 | }) 154 | defer func() { _ = client.Close() }() 155 | defer server.Stop() 156 | 157 | refClient := grpcreflect.NewClientAuto(ctx, client.Conn) 158 | 159 | svcs, err := refClient.ListServices() 160 | if err != nil { 161 | t.Fatalf("err: %s", err) 162 | } 163 | 164 | // TODO: maybe only assert some specific services here to make test more resilient 165 | expectedSvcs := []string{"grpc.health.v1.Health", "grpc.reflection.v1.ServerReflection", "grpc.reflection.v1alpha.ServerReflection", "grpctest.Test", "plugin.GRPCBroker", "plugin.GRPCController", "plugin.GRPCStdio"} 166 | 167 | if !reflect.DeepEqual(svcs, expectedSvcs) { 168 | t.Fatalf("expected: %v\ngot: %v", expectedSvcs, svcs) 169 | } 170 | 171 | healthDesc, err := refClient.ResolveService("grpc.health.v1.Health") 172 | if err != nil { 173 | t.Fatalf("err: %s", err) 174 | } 175 | 176 | methods := healthDesc.GetMethods() 177 | var methodNames []string 178 | for _, m := range methods { 179 | methodNames = append(methodNames, m.GetName()) 180 | } 181 | 182 | expectedMethodNames := []string{"Check", "Watch"} 183 | 184 | if !reflect.DeepEqual(methodNames, expectedMethodNames) { 185 | t.Fatalf("expected: %v\ngot: %v", expectedMethodNames, methodNames) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /grpc_controller.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/hashicorp/go-plugin/internal/plugin" 10 | ) 11 | 12 | // GRPCControllerServer handles shutdown calls to terminate the server when the 13 | // plugin client is closed. 14 | type grpcControllerServer struct { 15 | server *GRPCServer 16 | } 17 | 18 | // Shutdown stops the grpc server. It first will attempt a graceful stop, then a 19 | // full stop on the server. 20 | func (s *grpcControllerServer) Shutdown(ctx context.Context, _ *plugin.Empty) (*plugin.Empty, error) { 21 | resp := &plugin.Empty{} 22 | 23 | // TODO: figure out why GracefullStop doesn't work. 24 | s.server.Stop() 25 | return resp, nil 26 | } 27 | -------------------------------------------------------------------------------- /grpc_server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "bytes" 8 | "crypto/tls" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net" 13 | 14 | hclog "github.com/hashicorp/go-hclog" 15 | "github.com/hashicorp/go-plugin/internal/grpcmux" 16 | "github.com/hashicorp/go-plugin/internal/plugin" 17 | "google.golang.org/grpc" 18 | "google.golang.org/grpc/credentials" 19 | "google.golang.org/grpc/health" 20 | "google.golang.org/grpc/health/grpc_health_v1" 21 | "google.golang.org/grpc/reflection" 22 | ) 23 | 24 | // GRPCServiceName is the name of the service that the health check should 25 | // return as passing. 26 | const GRPCServiceName = "plugin" 27 | 28 | // DefaultGRPCServer can be used with the "GRPCServer" field for Server 29 | // as a default factory method to create a gRPC server with no extra options. 30 | func DefaultGRPCServer(opts []grpc.ServerOption) *grpc.Server { 31 | return grpc.NewServer(opts...) 32 | } 33 | 34 | // GRPCServer is a ServerType implementation that serves plugins over 35 | // gRPC. This allows plugins to easily be written for other languages. 36 | // 37 | // The GRPCServer outputs a custom configuration as a base64-encoded 38 | // JSON structure represented by the GRPCServerConfig config structure. 39 | type GRPCServer struct { 40 | // Plugins are the list of plugins to serve. 41 | Plugins map[string]Plugin 42 | 43 | // Server is the actual server that will accept connections. This 44 | // will be used for plugin registration as well. 45 | Server func([]grpc.ServerOption) *grpc.Server 46 | 47 | // TLS should be the TLS configuration if available. If this is nil, 48 | // the connection will not have transport security. 49 | TLS *tls.Config 50 | 51 | // DoneCh is the channel that is closed when this server has exited. 52 | DoneCh chan struct{} 53 | 54 | // Stdout/StderrLis are the readers for stdout/stderr that will be copied 55 | // to the stdout/stderr connection that is output. 56 | Stdout io.Reader 57 | Stderr io.Reader 58 | 59 | config GRPCServerConfig 60 | server *grpc.Server 61 | broker *GRPCBroker 62 | stdioServer *grpcStdioServer 63 | 64 | logger hclog.Logger 65 | 66 | muxer *grpcmux.GRPCServerMuxer 67 | } 68 | 69 | // ServerProtocol impl. 70 | func (s *GRPCServer) Init() error { 71 | // Create our server 72 | var opts []grpc.ServerOption 73 | if s.TLS != nil { 74 | opts = append(opts, grpc.Creds(credentials.NewTLS(s.TLS))) 75 | } 76 | s.server = s.Server(opts) 77 | 78 | // Register the health service 79 | healthCheck := health.NewServer() 80 | healthCheck.SetServingStatus( 81 | GRPCServiceName, grpc_health_v1.HealthCheckResponse_SERVING) 82 | grpc_health_v1.RegisterHealthServer(s.server, healthCheck) 83 | 84 | // Register the reflection service 85 | reflection.Register(s.server) 86 | 87 | // Register the broker service 88 | brokerServer := newGRPCBrokerServer() 89 | plugin.RegisterGRPCBrokerServer(s.server, brokerServer) 90 | s.broker = newGRPCBroker(brokerServer, s.TLS, unixSocketConfigFromEnv(), nil, s.muxer) 91 | go s.broker.Run() 92 | 93 | // Register the controller 94 | controllerServer := &grpcControllerServer{server: s} 95 | plugin.RegisterGRPCControllerServer(s.server, controllerServer) 96 | 97 | // Register the stdio service 98 | s.stdioServer = newGRPCStdioServer(s.logger, s.Stdout, s.Stderr) 99 | plugin.RegisterGRPCStdioServer(s.server, s.stdioServer) 100 | 101 | // Register all our plugins onto the gRPC server. 102 | for k, raw := range s.Plugins { 103 | p, ok := raw.(GRPCPlugin) 104 | if !ok { 105 | return fmt.Errorf("%q is not a GRPC-compatible plugin", k) 106 | } 107 | 108 | if err := p.GRPCServer(s.broker, s.server); err != nil { 109 | return fmt.Errorf("error registering %q: %s", k, err) 110 | } 111 | } 112 | 113 | return nil 114 | } 115 | 116 | // Stop calls Stop on the underlying grpc.Server and Close on the underlying 117 | // grpc.Broker if present. 118 | func (s *GRPCServer) Stop() { 119 | s.server.Stop() 120 | 121 | if s.broker != nil { 122 | _ = s.broker.Close() 123 | s.broker = nil 124 | } 125 | } 126 | 127 | // GracefulStop calls GracefulStop on the underlying grpc.Server and Close on 128 | // the underlying grpc.Broker if present. 129 | func (s *GRPCServer) GracefulStop() { 130 | s.server.GracefulStop() 131 | 132 | if s.broker != nil { 133 | _ = s.broker.Close() 134 | s.broker = nil 135 | } 136 | } 137 | 138 | // Config is the GRPCServerConfig encoded as JSON then base64. 139 | func (s *GRPCServer) Config() string { 140 | // Create a buffer that will contain our final contents 141 | var buf bytes.Buffer 142 | 143 | // Wrap the base64 encoding with JSON encoding. 144 | if err := json.NewEncoder(&buf).Encode(s.config); err != nil { 145 | // We panic since ths shouldn't happen under any scenario. We 146 | // carefully control the structure being encoded here and it should 147 | // always be successful. 148 | panic(err) 149 | } 150 | 151 | return buf.String() 152 | } 153 | 154 | func (s *GRPCServer) Serve(lis net.Listener) { 155 | defer close(s.DoneCh) 156 | err := s.server.Serve(lis) 157 | if err != nil { 158 | s.logger.Error("grpc server", "error", err) 159 | } 160 | } 161 | 162 | // GRPCServerConfig is the extra configuration passed along for consumers 163 | // to facilitate using GRPC plugins. 164 | type GRPCServerConfig struct { 165 | StdoutAddr string `json:"stdout_addr"` 166 | StderrAddr string `json:"stderr_addr"` 167 | } 168 | -------------------------------------------------------------------------------- /grpc_stdio.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "bufio" 8 | "bytes" 9 | "context" 10 | "io" 11 | 12 | empty "github.com/golang/protobuf/ptypes/empty" 13 | hclog "github.com/hashicorp/go-hclog" 14 | "github.com/hashicorp/go-plugin/internal/plugin" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/codes" 17 | "google.golang.org/grpc/status" 18 | ) 19 | 20 | // grpcStdioBuffer is the buffer size we try to fill when sending a chunk of 21 | // stdio data. This is currently 1 KB for no reason other than that seems like 22 | // enough (stdio data isn't that common) and is fairly low. 23 | const grpcStdioBuffer = 1 * 1024 24 | 25 | // grpcStdioServer implements the Stdio service and streams stdiout/stderr. 26 | type grpcStdioServer struct { 27 | stdoutCh <-chan []byte 28 | stderrCh <-chan []byte 29 | } 30 | 31 | // newGRPCStdioServer creates a new grpcStdioServer and starts the stream 32 | // copying for the given out and err readers. 33 | // 34 | // This must only be called ONCE per srcOut, srcErr. 35 | func newGRPCStdioServer(log hclog.Logger, srcOut, srcErr io.Reader) *grpcStdioServer { 36 | stdoutCh := make(chan []byte) 37 | stderrCh := make(chan []byte) 38 | 39 | // Begin copying the streams 40 | go copyChan(log, stdoutCh, srcOut) 41 | go copyChan(log, stderrCh, srcErr) 42 | 43 | // Construct our server 44 | return &grpcStdioServer{ 45 | stdoutCh: stdoutCh, 46 | stderrCh: stderrCh, 47 | } 48 | } 49 | 50 | // StreamStdio streams our stdout/err as the response. 51 | func (s *grpcStdioServer) StreamStdio( 52 | _ *empty.Empty, 53 | srv plugin.GRPCStdio_StreamStdioServer, 54 | ) error { 55 | // Share the same data value between runs. Sending this over the wire 56 | // marshals it so we can reuse this. 57 | var data plugin.StdioData 58 | 59 | for { 60 | // Read our data 61 | select { 62 | case data.Data = <-s.stdoutCh: 63 | data.Channel = plugin.StdioData_STDOUT 64 | 65 | case data.Data = <-s.stderrCh: 66 | data.Channel = plugin.StdioData_STDERR 67 | 68 | case <-srv.Context().Done(): 69 | return nil 70 | } 71 | 72 | // Not sure if this is possible, but if we somehow got here and 73 | // we didn't populate any data at all, then just continue. 74 | if len(data.Data) == 0 { 75 | continue 76 | } 77 | 78 | // Send our data to the client. 79 | if err := srv.Send(&data); err != nil { 80 | return err 81 | } 82 | } 83 | } 84 | 85 | // grpcStdioClient wraps the stdio service as a client to copy 86 | // the stdio data to output writers. 87 | type grpcStdioClient struct { 88 | log hclog.Logger 89 | stdioClient plugin.GRPCStdio_StreamStdioClient 90 | } 91 | 92 | // newGRPCStdioClient creates a grpcStdioClient. This will perform the 93 | // initial connection to the stdio service. If the stdio service is unavailable 94 | // then this will be a no-op. This allows this to work without error for 95 | // plugins that don't support this. 96 | func newGRPCStdioClient( 97 | ctx context.Context, 98 | log hclog.Logger, 99 | conn *grpc.ClientConn, 100 | ) (*grpcStdioClient, error) { 101 | client := plugin.NewGRPCStdioClient(conn) 102 | 103 | // Connect immediately to the endpoint 104 | stdioClient, err := client.StreamStdio(ctx, &empty.Empty{}) 105 | 106 | // If we get an Unavailable or Unimplemented error, this means that the plugin isn't 107 | // updated and linking to the latest version of go-plugin that supports 108 | // this. We fall back to the previous behavior of just not syncing anything. 109 | if status.Code(err) == codes.Unavailable || status.Code(err) == codes.Unimplemented { 110 | log.Warn("stdio service not available, stdout/stderr syncing unavailable") 111 | stdioClient = nil 112 | err = nil 113 | } 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | return &grpcStdioClient{ 119 | log: log, 120 | stdioClient: stdioClient, 121 | }, nil 122 | } 123 | 124 | // Run starts the loop that receives stdio data and writes it to the given 125 | // writers. This blocks and should be run in a goroutine. 126 | func (c *grpcStdioClient) Run(stdout, stderr io.Writer) { 127 | // This will be nil if stdio is not supported by the plugin 128 | if c.stdioClient == nil { 129 | c.log.Warn("stdio service unavailable, run will do nothing") 130 | return 131 | } 132 | 133 | for { 134 | c.log.Trace("waiting for stdio data") 135 | data, err := c.stdioClient.Recv() 136 | if err != nil { 137 | if err == io.EOF || 138 | status.Code(err) == codes.Unavailable || 139 | status.Code(err) == codes.Canceled || 140 | status.Code(err) == codes.Unimplemented || 141 | err == context.Canceled { 142 | c.log.Debug("received EOF, stopping recv loop", "err", err) 143 | return 144 | } 145 | 146 | c.log.Error("error receiving data", "err", err) 147 | return 148 | } 149 | 150 | // Determine our output writer based on channel 151 | var w io.Writer 152 | switch data.Channel { 153 | case plugin.StdioData_STDOUT: 154 | w = stdout 155 | 156 | case plugin.StdioData_STDERR: 157 | w = stderr 158 | 159 | default: 160 | c.log.Warn("unknown channel, dropping", "channel", data.Channel) 161 | continue 162 | } 163 | 164 | // Write! In the event of an error we just continue. 165 | if c.log.IsTrace() { 166 | c.log.Trace("received data", "channel", data.Channel.String(), "len", len(data.Data)) 167 | } 168 | if _, err := io.Copy(w, bytes.NewReader(data.Data)); err != nil { 169 | c.log.Error("failed to copy all bytes", "err", err) 170 | } 171 | } 172 | } 173 | 174 | // copyChan copies an io.Reader into a channel. 175 | func copyChan(log hclog.Logger, dst chan<- []byte, src io.Reader) { 176 | bufsrc := bufio.NewReader(src) 177 | 178 | for { 179 | // Make our data buffer. We allocate a new one per loop iteration 180 | // so that we can send it over the channel. 181 | var data [grpcStdioBuffer]byte 182 | 183 | // Read the data, this will block until data is available 184 | n, err := bufsrc.Read(data[:]) 185 | 186 | // We have to check if we have data BEFORE err != nil. The bufio 187 | // docs guarantee n == 0 on EOF but its better to be safe here. 188 | if n > 0 { 189 | // We have data! Send it on the channel. This will block if there 190 | // is no reader on the other side. We expect that go-plugin will 191 | // connect immediately to the stdio server to drain this so we want 192 | // this block to happen for backpressure. 193 | dst <- data[:n] 194 | } 195 | 196 | // If we hit EOF we're done copying 197 | if err == io.EOF { 198 | log.Debug("stdio EOF, exiting copy loop") 199 | return 200 | } 201 | 202 | // Any other error we just exit the loop. We don't expect there to 203 | // be errors since our use case for this is reading/writing from 204 | // a in-process pipe (os.Pipe). 205 | if err != nil { 206 | log.Warn("error copying stdio data, stopping copy", "err", err) 207 | return 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /internal/cmdrunner/addr_translator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package cmdrunner 5 | 6 | // addrTranslator implements stateless identity functions, as the host and plugin 7 | // run in the same context wrt Unix and network addresses. 8 | type addrTranslator struct{} 9 | 10 | func (*addrTranslator) PluginToHost(pluginNet, pluginAddr string) (string, string, error) { 11 | return pluginNet, pluginAddr, nil 12 | } 13 | 14 | func (*addrTranslator) HostToPlugin(hostNet, hostAddr string) (string, string, error) { 15 | return hostNet, hostAddr, nil 16 | } 17 | -------------------------------------------------------------------------------- /internal/cmdrunner/cmd_reattach.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package cmdrunner 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "net" 10 | "os" 11 | 12 | "github.com/hashicorp/go-plugin/runner" 13 | ) 14 | 15 | // ReattachFunc returns a function that allows reattaching to a plugin running 16 | // as a plain process. The process may or may not be a child process. 17 | func ReattachFunc(pid int, addr net.Addr) runner.ReattachFunc { 18 | return func() (runner.AttachedRunner, error) { 19 | p, err := os.FindProcess(pid) 20 | if err != nil { 21 | // On Unix systems, FindProcess never returns an error. 22 | // On Windows, for non-existent pids it returns: 23 | // os.SyscallError - 'OpenProcess: the paremter is incorrect' 24 | return nil, ErrProcessNotFound 25 | } 26 | 27 | // Attempt to connect to the addr since on Unix systems FindProcess 28 | // doesn't actually return an error if it can't find the process. 29 | conn, err := net.Dial(addr.Network(), addr.String()) 30 | if err != nil { 31 | return nil, ErrProcessNotFound 32 | } 33 | _ = conn.Close() 34 | 35 | return &CmdAttachedRunner{ 36 | pid: pid, 37 | process: p, 38 | }, nil 39 | } 40 | } 41 | 42 | // CmdAttachedRunner is mostly a subset of CmdRunner, except the Wait function 43 | // does not assume the process is a child of the host process, and so uses a 44 | // different implementation to wait on the process. 45 | type CmdAttachedRunner struct { 46 | pid int 47 | process *os.Process 48 | 49 | addrTranslator 50 | } 51 | 52 | func (c *CmdAttachedRunner) Wait(_ context.Context) error { 53 | return pidWait(c.pid) 54 | } 55 | 56 | func (c *CmdAttachedRunner) Kill(_ context.Context) error { 57 | return c.process.Kill() 58 | } 59 | 60 | func (c *CmdAttachedRunner) ID() string { 61 | return fmt.Sprintf("%d", c.pid) 62 | } 63 | -------------------------------------------------------------------------------- /internal/cmdrunner/cmd_runner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package cmdrunner 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | 14 | "github.com/hashicorp/go-hclog" 15 | "github.com/hashicorp/go-plugin/runner" 16 | ) 17 | 18 | var ( 19 | _ runner.Runner = (*CmdRunner)(nil) 20 | 21 | // ErrProcessNotFound is returned when a client is instantiated to 22 | // reattach to an existing process and it isn't found. 23 | ErrProcessNotFound = errors.New("reattachment process not found") 24 | ) 25 | 26 | const unrecognizedRemotePluginMessage = `This usually means 27 | the plugin was not compiled for this architecture, 28 | the plugin is missing dynamic-link libraries necessary to run, 29 | the plugin is not executable by this process due to file permissions, or 30 | the plugin failed to negotiate the initial go-plugin protocol handshake 31 | %s` 32 | 33 | // CmdRunner implements the runner.Runner interface. It mostly just passes through 34 | // to exec.Cmd methods. 35 | type CmdRunner struct { 36 | logger hclog.Logger 37 | cmd *exec.Cmd 38 | 39 | stdout io.ReadCloser 40 | stderr io.ReadCloser 41 | 42 | // Cmd info is persisted early, since the process information will be removed 43 | // after Kill is called. 44 | path string 45 | pid int 46 | 47 | addrTranslator 48 | } 49 | 50 | // NewCmdRunner returns an implementation of runner.Runner for running a plugin 51 | // as a subprocess. It must be passed a cmd that hasn't yet been started. 52 | func NewCmdRunner(logger hclog.Logger, cmd *exec.Cmd) (*CmdRunner, error) { 53 | stdout, err := cmd.StdoutPipe() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | stderr, err := cmd.StderrPipe() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | return &CmdRunner{ 64 | logger: logger, 65 | cmd: cmd, 66 | stdout: stdout, 67 | stderr: stderr, 68 | path: cmd.Path, 69 | }, nil 70 | } 71 | 72 | func (c *CmdRunner) Start(_ context.Context) error { 73 | c.logger.Debug("starting plugin", "path", c.cmd.Path, "args", c.cmd.Args) 74 | err := c.cmd.Start() 75 | if err != nil { 76 | return err 77 | } 78 | 79 | c.pid = c.cmd.Process.Pid 80 | c.logger.Debug("plugin started", "path", c.path, "pid", c.pid) 81 | return nil 82 | } 83 | 84 | func (c *CmdRunner) Wait(_ context.Context) error { 85 | return c.cmd.Wait() 86 | } 87 | 88 | func (c *CmdRunner) Kill(_ context.Context) error { 89 | if c.cmd.Process != nil { 90 | err := c.cmd.Process.Kill() 91 | // Swallow ErrProcessDone, we support calling Kill multiple times. 92 | if !errors.Is(err, os.ErrProcessDone) { 93 | return err 94 | } 95 | return nil 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (c *CmdRunner) Stdout() io.ReadCloser { 102 | return c.stdout 103 | } 104 | 105 | func (c *CmdRunner) Stderr() io.ReadCloser { 106 | return c.stderr 107 | } 108 | 109 | func (c *CmdRunner) Name() string { 110 | return c.path 111 | } 112 | 113 | func (c *CmdRunner) ID() string { 114 | return fmt.Sprintf("%d", c.pid) 115 | } 116 | 117 | // peTypes is a list of Portable Executable (PE) machine types from https://learn.microsoft.com/en-us/windows/win32/debug/pe-format 118 | // mapped to GOARCH types. It is not comprehensive, and only includes machine types that Go supports. 119 | var peTypes = map[uint16]string{ 120 | 0x14c: "386", 121 | 0x1c0: "arm", 122 | 0x6264: "loong64", 123 | 0x8664: "amd64", 124 | 0xaa64: "arm64", 125 | } 126 | 127 | func (c *CmdRunner) Diagnose(_ context.Context) string { 128 | return fmt.Sprintf(unrecognizedRemotePluginMessage, additionalNotesAboutCommand(c.cmd.Path)) 129 | } 130 | -------------------------------------------------------------------------------- /internal/cmdrunner/cmd_runner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package cmdrunner 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | func TestAdditionalNotesAboutCommand(t *testing.T) { 14 | files := []string{ 15 | "windows-amd64.exe", 16 | "windows-386.exe", 17 | "linux-amd64", 18 | "darwin-amd64", 19 | "darwin-arm64", 20 | } 21 | for _, file := range files { 22 | fullFile := filepath.Join("testdata", file) 23 | if _, err := os.Stat(fullFile); os.IsNotExist(err) { 24 | t.Skipf("testdata executables not present; please run 'make' in testdata/ directory for this test") 25 | } 26 | 27 | notes := additionalNotesAboutCommand(fullFile) 28 | if strings.Contains(file, "windows") && !strings.Contains(notes, "PE") { 29 | t.Errorf("Expected notes to contain Windows information:\n%s", notes) 30 | } 31 | if strings.Contains(file, "linux") && !strings.Contains(notes, "ELF") { 32 | t.Errorf("Expected notes to contain Linux information:\n%s", notes) 33 | } 34 | if strings.Contains(file, "darwin") && !strings.Contains(notes, "MachO") { 35 | t.Errorf("Expected notes to contain macOS information:\n%s", notes) 36 | } 37 | 38 | if strings.Contains(file, "amd64") && !strings.Contains(notes, "amd64") && !strings.Contains(notes, "EM_X86_64") && !strings.Contains(notes, "CpuAmd64") { 39 | t.Errorf("Expected notes to contain amd64 information:\n%s", notes) 40 | } 41 | 42 | if strings.Contains(file, "arm64") && !strings.Contains(notes, "CpuArm64") { 43 | t.Errorf("Expected notes to contain arm64 information:\n%s", notes) 44 | } 45 | if strings.Contains(file, "386") && !strings.Contains(notes, "386") { 46 | t.Errorf("Expected notes to contain 386 information:\n%s", notes) 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /internal/cmdrunner/notes_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package cmdrunner 8 | 9 | import ( 10 | "debug/elf" 11 | "debug/macho" 12 | "debug/pe" 13 | "fmt" 14 | "os" 15 | "os/user" 16 | "runtime" 17 | "strconv" 18 | "syscall" 19 | ) 20 | 21 | // additionalNotesAboutCommand tries to get additional information about a command that might help diagnose 22 | // why it won't run correctly. It runs as a best effort only. 23 | func additionalNotesAboutCommand(path string) string { 24 | notes := "" 25 | stat, err := os.Stat(path) 26 | if err != nil { 27 | return notes 28 | } 29 | 30 | notes += "\nAdditional notes about plugin:\n" 31 | notes += fmt.Sprintf(" Path: %s\n", path) 32 | notes += fmt.Sprintf(" Mode: %s\n", stat.Mode()) 33 | statT, ok := stat.Sys().(*syscall.Stat_t) 34 | if ok { 35 | currentUsername := "?" 36 | if u, err := user.LookupId(strconv.FormatUint(uint64(os.Getuid()), 10)); err == nil { 37 | currentUsername = u.Username 38 | } 39 | currentGroup := "?" 40 | if g, err := user.LookupGroupId(strconv.FormatUint(uint64(os.Getgid()), 10)); err == nil { 41 | currentGroup = g.Name 42 | } 43 | username := "?" 44 | if u, err := user.LookupId(strconv.FormatUint(uint64(statT.Uid), 10)); err == nil { 45 | username = u.Username 46 | } 47 | group := "?" 48 | if g, err := user.LookupGroupId(strconv.FormatUint(uint64(statT.Gid), 10)); err == nil { 49 | group = g.Name 50 | } 51 | notes += fmt.Sprintf(" Owner: %d [%s] (current: %d [%s])\n", statT.Uid, username, os.Getuid(), currentUsername) 52 | notes += fmt.Sprintf(" Group: %d [%s] (current: %d [%s])\n", statT.Gid, group, os.Getgid(), currentGroup) 53 | } 54 | 55 | if elfFile, err := elf.Open(path); err == nil { 56 | defer func() { _ = elfFile.Close() }() 57 | notes += fmt.Sprintf(" ELF architecture: %s (current architecture: %s)\n", elfFile.Machine, runtime.GOARCH) 58 | } else if machoFile, err := macho.Open(path); err == nil { 59 | defer func() { _ = machoFile.Close() }() 60 | notes += fmt.Sprintf(" MachO architecture: %s (current architecture: %s)\n", machoFile.Cpu, runtime.GOARCH) 61 | } else if peFile, err := pe.Open(path); err == nil { 62 | defer func() { _ = peFile.Close() }() 63 | machine, ok := peTypes[peFile.Machine] 64 | if !ok { 65 | machine = "unknown" 66 | } 67 | notes += fmt.Sprintf(" PE architecture: %s (current architecture: %s)\n", machine, runtime.GOARCH) 68 | } 69 | return notes 70 | } 71 | -------------------------------------------------------------------------------- /internal/cmdrunner/notes_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build windows 5 | // +build windows 6 | 7 | package cmdrunner 8 | 9 | import ( 10 | "debug/elf" 11 | "debug/macho" 12 | "debug/pe" 13 | "fmt" 14 | "os" 15 | "runtime" 16 | ) 17 | 18 | // additionalNotesAboutCommand tries to get additional information about a command that might help diagnose 19 | // why it won't run correctly. It runs as a best effort only. 20 | func additionalNotesAboutCommand(path string) string { 21 | notes := "" 22 | stat, err := os.Stat(path) 23 | if err != nil { 24 | return notes 25 | } 26 | 27 | notes += "\nAdditional notes about plugin:\n" 28 | notes += fmt.Sprintf(" Path: %s\n", path) 29 | notes += fmt.Sprintf(" Mode: %s\n", stat.Mode()) 30 | 31 | if elfFile, err := elf.Open(path); err == nil { 32 | defer elfFile.Close() 33 | notes += fmt.Sprintf(" ELF architecture: %s (current architecture: %s)\n", elfFile.Machine, runtime.GOARCH) 34 | } else if machoFile, err := macho.Open(path); err == nil { 35 | defer machoFile.Close() 36 | notes += fmt.Sprintf(" MachO architecture: %s (current architecture: %s)\n", machoFile.Cpu, runtime.GOARCH) 37 | } else if peFile, err := pe.Open(path); err == nil { 38 | defer peFile.Close() 39 | machine, ok := peTypes[peFile.Machine] 40 | if !ok { 41 | machine = "unknown" 42 | } 43 | notes += fmt.Sprintf(" PE architecture: %s (current architecture: %s)\n", machine, runtime.GOARCH) 44 | } 45 | return notes 46 | } 47 | -------------------------------------------------------------------------------- /internal/cmdrunner/process.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package cmdrunner 5 | 6 | import "time" 7 | 8 | // pidAlive checks whether a pid is alive. 9 | func pidAlive(pid int) bool { 10 | return _pidAlive(pid) 11 | } 12 | 13 | // pidWait blocks for a process to exit. 14 | func pidWait(pid int) error { 15 | ticker := time.NewTicker(1 * time.Second) 16 | defer ticker.Stop() 17 | 18 | for range ticker.C { 19 | if !pidAlive(pid) { 20 | break 21 | } 22 | } 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /internal/cmdrunner/process_posix.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package cmdrunner 8 | 9 | import ( 10 | "os" 11 | "syscall" 12 | ) 13 | 14 | // _pidAlive tests whether a process is alive or not by sending it Signal 0, 15 | // since Go otherwise has no way to test this. 16 | func _pidAlive(pid int) bool { 17 | proc, err := os.FindProcess(pid) 18 | if err == nil { 19 | err = proc.Signal(syscall.Signal(0)) 20 | } 21 | 22 | return err == nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/cmdrunner/process_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package cmdrunner 5 | 6 | import ( 7 | "syscall" 8 | ) 9 | 10 | const ( 11 | // Weird name but matches the MSDN docs 12 | exit_STILL_ACTIVE = 259 13 | 14 | processDesiredAccess = syscall.STANDARD_RIGHTS_READ | 15 | syscall.PROCESS_QUERY_INFORMATION | 16 | syscall.SYNCHRONIZE 17 | ) 18 | 19 | // _pidAlive tests whether a process is alive or not 20 | func _pidAlive(pid int) bool { 21 | h, err := syscall.OpenProcess(processDesiredAccess, false, uint32(pid)) 22 | if err != nil { 23 | return false 24 | } 25 | defer syscall.CloseHandle(h) 26 | 27 | var ec uint32 28 | if e := syscall.GetExitCodeProcess(h, &ec); e != nil { 29 | return false 30 | } 31 | 32 | return ec == exit_STILL_ACTIVE 33 | } 34 | -------------------------------------------------------------------------------- /internal/cmdrunner/testdata/.gitignore: -------------------------------------------------------------------------------- 1 | windows-amd64.exe 2 | windows-386.exe 3 | linux-amd64 4 | darwin-amd64 5 | darwin-arm64 6 | -------------------------------------------------------------------------------- /internal/cmdrunner/testdata/Makefile: -------------------------------------------------------------------------------- 1 | default: all 2 | .PHONY: default 3 | 4 | .PHONY: clean 5 | clean: 6 | rm -f windows-amd64.exe windows-386.exe linux-amd64 darwin-amd64 darwin-arm64 7 | 8 | .PHONY: all 9 | all: windows-amd64.exe windows-386.exe linux-amd64 darwin-amd64 darwin-arm64 10 | 11 | .PHONY: windows-amd64.exe 12 | windows-amd64.exe: 13 | GOOS=windows GOARCH=amd64 go build -o $@ 14 | 15 | .PHONY: windows-386.exe 16 | windows-386.exe: 17 | GOOS=windows GOARCH=386 go build -o $@ 18 | 19 | .PHONY: linux-amd64 20 | linux-amd64: 21 | GOOS=linux GOARCH=amd64 go build -o $@ 22 | 23 | .PHONY: darwin-amd64 24 | darwin-amd64: 25 | GOOS=darwin GOARCH=amd64 go build -o $@ 26 | 27 | .PHONY: darwin-arm64 28 | darwin-arm64: 29 | GOOS=darwin GOARCH=arm64 go build -o $@ -------------------------------------------------------------------------------- /internal/cmdrunner/testdata/README.md: -------------------------------------------------------------------------------- 1 | This folder contains a minimal Go program so that we can obtain example binaries of programs for various architectures for use in tests. -------------------------------------------------------------------------------- /internal/cmdrunner/testdata/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/testdata 2 | 3 | go 1.24 4 | 5 | replace github.com/hashicorp/go-plugin => ../../../ 6 | 7 | require github.com/hashicorp/go-plugin v1.4.7 8 | 9 | require google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect 10 | 11 | require ( 12 | github.com/fatih/color v1.13.0 // indirect 13 | github.com/golang/protobuf v1.5.4 // indirect 14 | github.com/hashicorp/go-hclog v1.6.3 // indirect 15 | github.com/hashicorp/yamux v0.1.2 // indirect 16 | github.com/mattn/go-colorable v0.1.12 // indirect 17 | github.com/mattn/go-isatty v0.0.17 // indirect 18 | github.com/oklog/run v1.1.0 // indirect 19 | golang.org/x/net v0.38.0 // indirect 20 | golang.org/x/sys v0.31.0 // indirect 21 | golang.org/x/text v0.23.0 // indirect 22 | google.golang.org/grpc v1.73.0 // indirect 23 | google.golang.org/protobuf v1.36.6 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /internal/cmdrunner/testdata/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 2 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 7 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 8 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 9 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 10 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 11 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 12 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 13 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 14 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 15 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 16 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 17 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 18 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 19 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 20 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= 21 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= 22 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 23 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 24 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 25 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 26 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 27 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 28 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 29 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 30 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 31 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 32 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 36 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 37 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 38 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 39 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 40 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 41 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 42 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 43 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 44 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 45 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 46 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 47 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 48 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 49 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 50 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 51 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 52 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 53 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 55 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 57 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 60 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 61 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 62 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 63 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= 64 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 65 | google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= 66 | google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= 67 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 68 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 69 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 71 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | -------------------------------------------------------------------------------- /internal/cmdrunner/testdata/minimal.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package main 5 | 6 | import "github.com/hashicorp/go-plugin" 7 | 8 | func main() { 9 | plugin.Serve(nil) 10 | } 11 | -------------------------------------------------------------------------------- /internal/grpcmux/blocked_client_listener.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package grpcmux 5 | 6 | import ( 7 | "io" 8 | "net" 9 | 10 | "github.com/hashicorp/yamux" 11 | ) 12 | 13 | var _ net.Listener = (*blockedClientListener)(nil) 14 | 15 | // blockedClientListener accepts connections for a specific gRPC broker stream 16 | // ID on the client (host) side of the connection. 17 | type blockedClientListener struct { 18 | session *yamux.Session 19 | waitCh chan struct{} 20 | doneCh <-chan struct{} 21 | } 22 | 23 | func newBlockedClientListener(session *yamux.Session, doneCh <-chan struct{}) *blockedClientListener { 24 | return &blockedClientListener{ 25 | waitCh: make(chan struct{}, 1), 26 | doneCh: doneCh, 27 | session: session, 28 | } 29 | } 30 | 31 | func (b *blockedClientListener) Accept() (net.Conn, error) { 32 | select { 33 | case <-b.waitCh: 34 | return b.session.Accept() 35 | case <-b.doneCh: 36 | return nil, io.EOF 37 | } 38 | } 39 | 40 | func (b *blockedClientListener) Addr() net.Addr { 41 | return b.session.Addr() 42 | } 43 | 44 | func (b *blockedClientListener) Close() error { 45 | // We don't close the session, the client muxer is responsible for that. 46 | return nil 47 | } 48 | 49 | func (b *blockedClientListener) unblock() { 50 | b.waitCh <- struct{}{} 51 | } 52 | -------------------------------------------------------------------------------- /internal/grpcmux/blocked_server_listener.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package grpcmux 5 | 6 | import ( 7 | "io" 8 | "net" 9 | ) 10 | 11 | var _ net.Listener = (*blockedServerListener)(nil) 12 | 13 | // blockedServerListener accepts connections for a specific gRPC broker stream 14 | // ID on the server (plugin) side of the connection. 15 | type blockedServerListener struct { 16 | addr net.Addr 17 | acceptCh chan acceptResult 18 | doneCh <-chan struct{} 19 | } 20 | 21 | type acceptResult struct { 22 | conn net.Conn 23 | err error 24 | } 25 | 26 | func newBlockedServerListener(addr net.Addr, doneCh <-chan struct{}) *blockedServerListener { 27 | return &blockedServerListener{ 28 | addr: addr, 29 | acceptCh: make(chan acceptResult), 30 | doneCh: doneCh, 31 | } 32 | } 33 | 34 | func (b *blockedServerListener) Accept() (net.Conn, error) { 35 | select { 36 | case accept := <-b.acceptCh: 37 | return accept.conn, accept.err 38 | case <-b.doneCh: 39 | return nil, io.EOF 40 | } 41 | } 42 | 43 | func (b *blockedServerListener) Addr() net.Addr { 44 | return b.addr 45 | } 46 | 47 | func (b *blockedServerListener) Close() error { 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/grpcmux/grpc_client_muxer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package grpcmux 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "sync" 10 | 11 | "github.com/hashicorp/go-hclog" 12 | "github.com/hashicorp/yamux" 13 | ) 14 | 15 | var _ GRPCMuxer = (*GRPCClientMuxer)(nil) 16 | 17 | // GRPCClientMuxer implements the client (host) side of the gRPC broker's 18 | // GRPCMuxer interface for multiplexing multiple gRPC broker connections over 19 | // a single net.Conn. 20 | // 21 | // The client dials the initial net.Conn eagerly, and creates a yamux.Session 22 | // as the implementation for multiplexing any additional connections. 23 | // 24 | // Each net.Listener returned from Listener will block until the client receives 25 | // a knock that matches its gRPC broker stream ID. There is no default listener 26 | // on the client, as it is a client for the gRPC broker's control services. (See 27 | // GRPCServerMuxer for more details). 28 | type GRPCClientMuxer struct { 29 | logger hclog.Logger 30 | session *yamux.Session 31 | 32 | acceptMutex sync.Mutex 33 | acceptListeners map[uint32]*blockedClientListener 34 | } 35 | 36 | func NewGRPCClientMuxer(logger hclog.Logger, addr net.Addr) (*GRPCClientMuxer, error) { 37 | // Eagerly establish the underlying connection as early as possible. 38 | logger.Debug("making new client mux initial connection", "addr", addr) 39 | conn, err := net.Dial(addr.Network(), addr.String()) 40 | if err != nil { 41 | return nil, err 42 | } 43 | if tcpConn, ok := conn.(*net.TCPConn); ok { 44 | // Make sure to set keep alive so that the connection doesn't die 45 | _ = tcpConn.SetKeepAlive(true) 46 | } 47 | 48 | cfg := yamux.DefaultConfig() 49 | cfg.Logger = logger.Named("yamux").StandardLogger(&hclog.StandardLoggerOptions{ 50 | InferLevels: true, 51 | }) 52 | cfg.LogOutput = nil 53 | sess, err := yamux.Client(conn, cfg) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | logger.Debug("client muxer connected", "addr", addr) 59 | m := &GRPCClientMuxer{ 60 | logger: logger, 61 | session: sess, 62 | acceptListeners: make(map[uint32]*blockedClientListener), 63 | } 64 | 65 | return m, nil 66 | } 67 | 68 | func (m *GRPCClientMuxer) Enabled() bool { 69 | return m != nil 70 | } 71 | 72 | func (m *GRPCClientMuxer) Listener(id uint32, doneCh <-chan struct{}) (net.Listener, error) { 73 | ln := newBlockedClientListener(m.session, doneCh) 74 | 75 | m.acceptMutex.Lock() 76 | m.acceptListeners[id] = ln 77 | m.acceptMutex.Unlock() 78 | 79 | return ln, nil 80 | } 81 | 82 | func (m *GRPCClientMuxer) AcceptKnock(id uint32) error { 83 | m.acceptMutex.Lock() 84 | defer m.acceptMutex.Unlock() 85 | 86 | ln, ok := m.acceptListeners[id] 87 | if !ok { 88 | return fmt.Errorf("no listener for id %d", id) 89 | } 90 | ln.unblock() 91 | return nil 92 | } 93 | 94 | func (m *GRPCClientMuxer) Dial() (net.Conn, error) { 95 | stream, err := m.session.Open() 96 | if err != nil { 97 | return nil, fmt.Errorf("error dialling new client stream: %w", err) 98 | } 99 | 100 | return stream, nil 101 | } 102 | 103 | func (m *GRPCClientMuxer) Close() error { 104 | return m.session.Close() 105 | } 106 | -------------------------------------------------------------------------------- /internal/grpcmux/grpc_muxer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package grpcmux 5 | 6 | import ( 7 | "net" 8 | ) 9 | 10 | // GRPCMuxer enables multiple implementations of net.Listener to accept 11 | // connections over a single "main" multiplexed net.Conn, and dial multiple 12 | // client connections over the same multiplexed net.Conn. 13 | // 14 | // The first multiplexed connection is used to serve the gRPC broker's own 15 | // control services: plugin.GRPCBroker, plugin.GRPCController, plugin.GRPCStdio. 16 | // 17 | // Clients must "knock" before dialling, to tell the server side that the 18 | // next net.Conn should be accepted onto a specific stream ID. The knock is a 19 | // bidirectional streaming message on the plugin.GRPCBroker service. 20 | type GRPCMuxer interface { 21 | // Enabled determines whether multiplexing should be used. It saves users 22 | // of the interface from having to compare an interface with nil, which 23 | // is a bit awkward to do correctly. 24 | Enabled() bool 25 | 26 | // Listener returns a multiplexed listener that will wait until AcceptKnock 27 | // is called with a matching ID before its Accept function returns. 28 | Listener(id uint32, doneCh <-chan struct{}) (net.Listener, error) 29 | 30 | // AcceptKnock unblocks the listener with the matching ID, and returns an 31 | // error if it hasn't been created yet. 32 | AcceptKnock(id uint32) error 33 | 34 | // Dial makes a new multiplexed client connection. To dial a specific ID, 35 | // a knock must be sent first. 36 | Dial() (net.Conn, error) 37 | 38 | // Close closes connections and releases any resources associated with the 39 | // muxer. 40 | Close() error 41 | } 42 | -------------------------------------------------------------------------------- /internal/grpcmux/grpc_server_muxer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package grpcmux 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net" 10 | "sync" 11 | "time" 12 | 13 | "github.com/hashicorp/go-hclog" 14 | "github.com/hashicorp/yamux" 15 | ) 16 | 17 | var _ GRPCMuxer = (*GRPCServerMuxer)(nil) 18 | var _ net.Listener = (*GRPCServerMuxer)(nil) 19 | 20 | // GRPCServerMuxer implements the server (plugin) side of the gRPC broker's 21 | // GRPCMuxer interface for multiplexing multiple gRPC broker connections over 22 | // a single net.Conn. 23 | // 24 | // The server side needs a listener to serve the gRPC broker's control services, 25 | // which includes the service we will receive knocks on. That means we always 26 | // accept the first connection onto a "default" main listener, and if we accept 27 | // any further connections without receiving a knock first, they are also given 28 | // to the default listener. 29 | // 30 | // When creating additional multiplexed listeners for specific stream IDs, we 31 | // can't control the order in which gRPC servers will call Accept() on each 32 | // listener, but we do need to control which gRPC server accepts which connection. 33 | // As such, each multiplexed listener blocks waiting on a channel. It will be 34 | // unblocked when a knock is received for the matching stream ID. 35 | type GRPCServerMuxer struct { 36 | addr net.Addr 37 | logger hclog.Logger 38 | 39 | sessionErrCh chan error 40 | sess *yamux.Session 41 | 42 | knockCh chan uint32 43 | 44 | acceptMutex sync.Mutex 45 | acceptChannels map[uint32]chan acceptResult 46 | } 47 | 48 | func NewGRPCServerMuxer(logger hclog.Logger, ln net.Listener) *GRPCServerMuxer { 49 | m := &GRPCServerMuxer{ 50 | addr: ln.Addr(), 51 | logger: logger, 52 | 53 | sessionErrCh: make(chan error), 54 | 55 | knockCh: make(chan uint32, 1), 56 | acceptChannels: make(map[uint32]chan acceptResult), 57 | } 58 | 59 | go m.acceptSession(ln) 60 | 61 | return m 62 | } 63 | 64 | // acceptSessionAndMuxAccept is responsible for establishing the yamux session, 65 | // and then kicking off the acceptLoop function. 66 | func (m *GRPCServerMuxer) acceptSession(ln net.Listener) { 67 | defer close(m.sessionErrCh) 68 | 69 | m.logger.Debug("accepting initial connection", "addr", m.addr) 70 | conn, err := ln.Accept() 71 | if err != nil { 72 | m.sessionErrCh <- err 73 | return 74 | } 75 | 76 | m.logger.Debug("initial server connection accepted", "addr", m.addr) 77 | cfg := yamux.DefaultConfig() 78 | cfg.Logger = m.logger.Named("yamux").StandardLogger(&hclog.StandardLoggerOptions{ 79 | InferLevels: true, 80 | }) 81 | cfg.LogOutput = nil 82 | m.sess, err = yamux.Server(conn, cfg) 83 | if err != nil { 84 | m.sessionErrCh <- err 85 | return 86 | } 87 | } 88 | 89 | func (m *GRPCServerMuxer) session() (*yamux.Session, error) { 90 | select { 91 | case err := <-m.sessionErrCh: 92 | if err != nil { 93 | return nil, err 94 | } 95 | case <-time.After(5 * time.Second): 96 | return nil, errors.New("timed out waiting for connection to be established") 97 | } 98 | 99 | // Should never happen. 100 | if m.sess == nil { 101 | return nil, errors.New("no connection established and no error received") 102 | } 103 | 104 | return m.sess, nil 105 | } 106 | 107 | // Accept accepts all incoming connections and routes them to the correct 108 | // stream ID based on the most recent knock received. 109 | func (m *GRPCServerMuxer) Accept() (net.Conn, error) { 110 | session, err := m.session() 111 | if err != nil { 112 | return nil, fmt.Errorf("error establishing yamux session: %w", err) 113 | } 114 | 115 | for { 116 | conn, acceptErr := session.Accept() 117 | 118 | select { 119 | case id := <-m.knockCh: 120 | m.acceptMutex.Lock() 121 | acceptCh, ok := m.acceptChannels[id] 122 | m.acceptMutex.Unlock() 123 | 124 | if !ok { 125 | if conn != nil { 126 | _ = conn.Close() 127 | } 128 | return nil, fmt.Errorf("received knock on ID %d that doesn't have a listener", id) 129 | } 130 | m.logger.Debug("sending conn to brokered listener", "id", id) 131 | acceptCh <- acceptResult{ 132 | conn: conn, 133 | err: acceptErr, 134 | } 135 | default: 136 | m.logger.Debug("sending conn to default listener") 137 | return conn, acceptErr 138 | } 139 | } 140 | } 141 | 142 | func (m *GRPCServerMuxer) Addr() net.Addr { 143 | return m.addr 144 | } 145 | 146 | func (m *GRPCServerMuxer) Close() error { 147 | session, err := m.session() 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return session.Close() 153 | } 154 | 155 | func (m *GRPCServerMuxer) Enabled() bool { 156 | return m != nil 157 | } 158 | 159 | func (m *GRPCServerMuxer) Listener(id uint32, doneCh <-chan struct{}) (net.Listener, error) { 160 | sess, err := m.session() 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | ln := newBlockedServerListener(sess.Addr(), doneCh) 166 | m.acceptMutex.Lock() 167 | m.acceptChannels[id] = ln.acceptCh 168 | m.acceptMutex.Unlock() 169 | 170 | return ln, nil 171 | } 172 | 173 | func (m *GRPCServerMuxer) Dial() (net.Conn, error) { 174 | sess, err := m.session() 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | stream, err := sess.OpenStream() 180 | if err != nil { 181 | return nil, fmt.Errorf("error dialling new server stream: %w", err) 182 | } 183 | 184 | return stream, nil 185 | } 186 | 187 | func (m *GRPCServerMuxer) AcceptKnock(id uint32) error { 188 | m.knockCh <- id 189 | return nil 190 | } 191 | -------------------------------------------------------------------------------- /internal/plugin/grpc_broker.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | syntax = "proto3"; 5 | package plugin; 6 | option go_package = "./plugin"; 7 | 8 | message ConnInfo { 9 | uint32 service_id = 1; 10 | string network = 2; 11 | string address = 3; 12 | message Knock { 13 | bool knock = 1; 14 | bool ack = 2; 15 | string error = 3; 16 | } 17 | Knock knock = 4; 18 | } 19 | 20 | service GRPCBroker { 21 | rpc StartStream(stream ConnInfo) returns (stream ConnInfo); 22 | } 23 | -------------------------------------------------------------------------------- /internal/plugin/grpc_broker_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 5 | // versions: 6 | // - protoc-gen-go-grpc v1.3.0 7 | // - protoc (unknown) 8 | // source: internal/plugin/grpc_broker.proto 9 | 10 | package plugin 11 | 12 | import ( 13 | context "context" 14 | grpc "google.golang.org/grpc" 15 | codes "google.golang.org/grpc/codes" 16 | status "google.golang.org/grpc/status" 17 | ) 18 | 19 | // This is a compile-time assertion to ensure that this generated file 20 | // is compatible with the grpc package it is being compiled against. 21 | // Requires gRPC-Go v1.32.0 or later. 22 | const _ = grpc.SupportPackageIsVersion7 23 | 24 | const ( 25 | GRPCBroker_StartStream_FullMethodName = "/plugin.GRPCBroker/StartStream" 26 | ) 27 | 28 | // GRPCBrokerClient is the client API for GRPCBroker service. 29 | // 30 | // 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. 31 | type GRPCBrokerClient interface { 32 | StartStream(ctx context.Context, opts ...grpc.CallOption) (GRPCBroker_StartStreamClient, error) 33 | } 34 | 35 | type gRPCBrokerClient struct { 36 | cc grpc.ClientConnInterface 37 | } 38 | 39 | func NewGRPCBrokerClient(cc grpc.ClientConnInterface) GRPCBrokerClient { 40 | return &gRPCBrokerClient{cc} 41 | } 42 | 43 | func (c *gRPCBrokerClient) StartStream(ctx context.Context, opts ...grpc.CallOption) (GRPCBroker_StartStreamClient, error) { 44 | stream, err := c.cc.NewStream(ctx, &GRPCBroker_ServiceDesc.Streams[0], GRPCBroker_StartStream_FullMethodName, opts...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | x := &gRPCBrokerStartStreamClient{stream} 49 | return x, nil 50 | } 51 | 52 | type GRPCBroker_StartStreamClient interface { 53 | Send(*ConnInfo) error 54 | Recv() (*ConnInfo, error) 55 | grpc.ClientStream 56 | } 57 | 58 | type gRPCBrokerStartStreamClient struct { 59 | grpc.ClientStream 60 | } 61 | 62 | func (x *gRPCBrokerStartStreamClient) Send(m *ConnInfo) error { 63 | return x.ClientStream.SendMsg(m) 64 | } 65 | 66 | func (x *gRPCBrokerStartStreamClient) Recv() (*ConnInfo, error) { 67 | m := new(ConnInfo) 68 | if err := x.ClientStream.RecvMsg(m); err != nil { 69 | return nil, err 70 | } 71 | return m, nil 72 | } 73 | 74 | // GRPCBrokerServer is the server API for GRPCBroker service. 75 | // All implementations should embed UnimplementedGRPCBrokerServer 76 | // for forward compatibility 77 | type GRPCBrokerServer interface { 78 | StartStream(GRPCBroker_StartStreamServer) error 79 | } 80 | 81 | // UnimplementedGRPCBrokerServer should be embedded to have forward compatible implementations. 82 | type UnimplementedGRPCBrokerServer struct { 83 | } 84 | 85 | func (UnimplementedGRPCBrokerServer) StartStream(GRPCBroker_StartStreamServer) error { 86 | return status.Errorf(codes.Unimplemented, "method StartStream not implemented") 87 | } 88 | 89 | // UnsafeGRPCBrokerServer may be embedded to opt out of forward compatibility for this service. 90 | // Use of this interface is not recommended, as added methods to GRPCBrokerServer will 91 | // result in compilation errors. 92 | type UnsafeGRPCBrokerServer interface { 93 | mustEmbedUnimplementedGRPCBrokerServer() 94 | } 95 | 96 | func RegisterGRPCBrokerServer(s grpc.ServiceRegistrar, srv GRPCBrokerServer) { 97 | s.RegisterService(&GRPCBroker_ServiceDesc, srv) 98 | } 99 | 100 | func _GRPCBroker_StartStream_Handler(srv interface{}, stream grpc.ServerStream) error { 101 | return srv.(GRPCBrokerServer).StartStream(&gRPCBrokerStartStreamServer{stream}) 102 | } 103 | 104 | type GRPCBroker_StartStreamServer interface { 105 | Send(*ConnInfo) error 106 | Recv() (*ConnInfo, error) 107 | grpc.ServerStream 108 | } 109 | 110 | type gRPCBrokerStartStreamServer struct { 111 | grpc.ServerStream 112 | } 113 | 114 | func (x *gRPCBrokerStartStreamServer) Send(m *ConnInfo) error { 115 | return x.ServerStream.SendMsg(m) 116 | } 117 | 118 | func (x *gRPCBrokerStartStreamServer) Recv() (*ConnInfo, error) { 119 | m := new(ConnInfo) 120 | if err := x.ServerStream.RecvMsg(m); err != nil { 121 | return nil, err 122 | } 123 | return m, nil 124 | } 125 | 126 | // GRPCBroker_ServiceDesc is the grpc.ServiceDesc for GRPCBroker service. 127 | // It's only intended for direct use with grpc.RegisterService, 128 | // and not to be introspected or modified (even as a copy) 129 | var GRPCBroker_ServiceDesc = grpc.ServiceDesc{ 130 | ServiceName: "plugin.GRPCBroker", 131 | HandlerType: (*GRPCBrokerServer)(nil), 132 | Methods: []grpc.MethodDesc{}, 133 | Streams: []grpc.StreamDesc{ 134 | { 135 | StreamName: "StartStream", 136 | Handler: _GRPCBroker_StartStream_Handler, 137 | ServerStreams: true, 138 | ClientStreams: true, 139 | }, 140 | }, 141 | Metadata: "internal/plugin/grpc_broker.proto", 142 | } 143 | -------------------------------------------------------------------------------- /internal/plugin/grpc_controller.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by protoc-gen-go. DO NOT EDIT. 5 | // versions: 6 | // protoc-gen-go v1.31.0 7 | // protoc (unknown) 8 | // source: internal/plugin/grpc_controller.proto 9 | 10 | package plugin 11 | 12 | import ( 13 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 14 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 15 | reflect "reflect" 16 | sync "sync" 17 | ) 18 | 19 | const ( 20 | // Verify that this generated code is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 22 | // Verify that runtime/protoimpl is sufficiently up-to-date. 23 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 24 | ) 25 | 26 | type Empty struct { 27 | state protoimpl.MessageState 28 | sizeCache protoimpl.SizeCache 29 | unknownFields protoimpl.UnknownFields 30 | } 31 | 32 | func (x *Empty) Reset() { 33 | *x = Empty{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_internal_plugin_grpc_controller_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *Empty) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*Empty) ProtoMessage() {} 46 | 47 | func (x *Empty) ProtoReflect() protoreflect.Message { 48 | mi := &file_internal_plugin_grpc_controller_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 Empty.ProtoReflect.Descriptor instead. 60 | func (*Empty) Descriptor() ([]byte, []int) { 61 | return file_internal_plugin_grpc_controller_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | var File_internal_plugin_grpc_controller_proto protoreflect.FileDescriptor 65 | 66 | var file_internal_plugin_grpc_controller_proto_rawDesc = []byte{ 67 | 0x0a, 0x25, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 68 | 0x6e, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 69 | 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x06, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x22, 70 | 0x07, 0x0a, 0x05, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x32, 0x3a, 0x0a, 0x0e, 0x47, 0x52, 0x50, 0x43, 71 | 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x08, 0x53, 0x68, 72 | 0x75, 0x74, 0x64, 0x6f, 0x77, 0x6e, 0x12, 0x0d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 73 | 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x45, 74 | 0x6d, 0x70, 0x74, 0x79, 0x42, 0x0a, 0x5a, 0x08, 0x2e, 0x2f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 75 | 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 76 | } 77 | 78 | var ( 79 | file_internal_plugin_grpc_controller_proto_rawDescOnce sync.Once 80 | file_internal_plugin_grpc_controller_proto_rawDescData = file_internal_plugin_grpc_controller_proto_rawDesc 81 | ) 82 | 83 | func file_internal_plugin_grpc_controller_proto_rawDescGZIP() []byte { 84 | file_internal_plugin_grpc_controller_proto_rawDescOnce.Do(func() { 85 | file_internal_plugin_grpc_controller_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_plugin_grpc_controller_proto_rawDescData) 86 | }) 87 | return file_internal_plugin_grpc_controller_proto_rawDescData 88 | } 89 | 90 | var file_internal_plugin_grpc_controller_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 91 | var file_internal_plugin_grpc_controller_proto_goTypes = []interface{}{ 92 | (*Empty)(nil), // 0: plugin.Empty 93 | } 94 | var file_internal_plugin_grpc_controller_proto_depIdxs = []int32{ 95 | 0, // 0: plugin.GRPCController.Shutdown:input_type -> plugin.Empty 96 | 0, // 1: plugin.GRPCController.Shutdown:output_type -> plugin.Empty 97 | 1, // [1:2] is the sub-list for method output_type 98 | 0, // [0:1] is the sub-list for method input_type 99 | 0, // [0:0] is the sub-list for extension type_name 100 | 0, // [0:0] is the sub-list for extension extendee 101 | 0, // [0:0] is the sub-list for field type_name 102 | } 103 | 104 | func init() { file_internal_plugin_grpc_controller_proto_init() } 105 | func file_internal_plugin_grpc_controller_proto_init() { 106 | if File_internal_plugin_grpc_controller_proto != nil { 107 | return 108 | } 109 | if !protoimpl.UnsafeEnabled { 110 | file_internal_plugin_grpc_controller_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 111 | switch v := v.(*Empty); i { 112 | case 0: 113 | return &v.state 114 | case 1: 115 | return &v.sizeCache 116 | case 2: 117 | return &v.unknownFields 118 | default: 119 | return nil 120 | } 121 | } 122 | } 123 | type x struct{} 124 | out := protoimpl.TypeBuilder{ 125 | File: protoimpl.DescBuilder{ 126 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 127 | RawDescriptor: file_internal_plugin_grpc_controller_proto_rawDesc, 128 | NumEnums: 0, 129 | NumMessages: 1, 130 | NumExtensions: 0, 131 | NumServices: 1, 132 | }, 133 | GoTypes: file_internal_plugin_grpc_controller_proto_goTypes, 134 | DependencyIndexes: file_internal_plugin_grpc_controller_proto_depIdxs, 135 | MessageInfos: file_internal_plugin_grpc_controller_proto_msgTypes, 136 | }.Build() 137 | File_internal_plugin_grpc_controller_proto = out.File 138 | file_internal_plugin_grpc_controller_proto_rawDesc = nil 139 | file_internal_plugin_grpc_controller_proto_goTypes = nil 140 | file_internal_plugin_grpc_controller_proto_depIdxs = nil 141 | } 142 | -------------------------------------------------------------------------------- /internal/plugin/grpc_controller.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | syntax = "proto3"; 5 | package plugin; 6 | option go_package = "./plugin"; 7 | 8 | message Empty { 9 | } 10 | 11 | // The GRPCController is responsible for telling the plugin server to shutdown. 12 | service GRPCController { 13 | rpc Shutdown(Empty) returns (Empty); 14 | } 15 | -------------------------------------------------------------------------------- /internal/plugin/grpc_controller_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 5 | // versions: 6 | // - protoc-gen-go-grpc v1.3.0 7 | // - protoc (unknown) 8 | // source: internal/plugin/grpc_controller.proto 9 | 10 | package plugin 11 | 12 | import ( 13 | context "context" 14 | grpc "google.golang.org/grpc" 15 | codes "google.golang.org/grpc/codes" 16 | status "google.golang.org/grpc/status" 17 | ) 18 | 19 | // This is a compile-time assertion to ensure that this generated file 20 | // is compatible with the grpc package it is being compiled against. 21 | // Requires gRPC-Go v1.32.0 or later. 22 | const _ = grpc.SupportPackageIsVersion7 23 | 24 | const ( 25 | GRPCController_Shutdown_FullMethodName = "/plugin.GRPCController/Shutdown" 26 | ) 27 | 28 | // GRPCControllerClient is the client API for GRPCController service. 29 | // 30 | // 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. 31 | type GRPCControllerClient interface { 32 | Shutdown(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) 33 | } 34 | 35 | type gRPCControllerClient struct { 36 | cc grpc.ClientConnInterface 37 | } 38 | 39 | func NewGRPCControllerClient(cc grpc.ClientConnInterface) GRPCControllerClient { 40 | return &gRPCControllerClient{cc} 41 | } 42 | 43 | func (c *gRPCControllerClient) Shutdown(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*Empty, error) { 44 | out := new(Empty) 45 | err := c.cc.Invoke(ctx, GRPCController_Shutdown_FullMethodName, in, out, opts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | // GRPCControllerServer is the server API for GRPCController service. 53 | // All implementations should embed UnimplementedGRPCControllerServer 54 | // for forward compatibility 55 | type GRPCControllerServer interface { 56 | Shutdown(context.Context, *Empty) (*Empty, error) 57 | } 58 | 59 | // UnimplementedGRPCControllerServer should be embedded to have forward compatible implementations. 60 | type UnimplementedGRPCControllerServer struct { 61 | } 62 | 63 | func (UnimplementedGRPCControllerServer) Shutdown(context.Context, *Empty) (*Empty, error) { 64 | return nil, status.Errorf(codes.Unimplemented, "method Shutdown not implemented") 65 | } 66 | 67 | // UnsafeGRPCControllerServer may be embedded to opt out of forward compatibility for this service. 68 | // Use of this interface is not recommended, as added methods to GRPCControllerServer will 69 | // result in compilation errors. 70 | type UnsafeGRPCControllerServer interface { 71 | mustEmbedUnimplementedGRPCControllerServer() 72 | } 73 | 74 | func RegisterGRPCControllerServer(s grpc.ServiceRegistrar, srv GRPCControllerServer) { 75 | s.RegisterService(&GRPCController_ServiceDesc, srv) 76 | } 77 | 78 | func _GRPCController_Shutdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 79 | in := new(Empty) 80 | if err := dec(in); err != nil { 81 | return nil, err 82 | } 83 | if interceptor == nil { 84 | return srv.(GRPCControllerServer).Shutdown(ctx, in) 85 | } 86 | info := &grpc.UnaryServerInfo{ 87 | Server: srv, 88 | FullMethod: GRPCController_Shutdown_FullMethodName, 89 | } 90 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 91 | return srv.(GRPCControllerServer).Shutdown(ctx, req.(*Empty)) 92 | } 93 | return interceptor(ctx, in, info, handler) 94 | } 95 | 96 | // GRPCController_ServiceDesc is the grpc.ServiceDesc for GRPCController service. 97 | // It's only intended for direct use with grpc.RegisterService, 98 | // and not to be introspected or modified (even as a copy) 99 | var GRPCController_ServiceDesc = grpc.ServiceDesc{ 100 | ServiceName: "plugin.GRPCController", 101 | HandlerType: (*GRPCControllerServer)(nil), 102 | Methods: []grpc.MethodDesc{ 103 | { 104 | MethodName: "Shutdown", 105 | Handler: _GRPCController_Shutdown_Handler, 106 | }, 107 | }, 108 | Streams: []grpc.StreamDesc{}, 109 | Metadata: "internal/plugin/grpc_controller.proto", 110 | } 111 | -------------------------------------------------------------------------------- /internal/plugin/grpc_stdio.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | syntax = "proto3"; 5 | package plugin; 6 | option go_package = "./plugin"; 7 | 8 | import "google/protobuf/empty.proto"; 9 | 10 | // GRPCStdio is a service that is automatically run by the plugin process 11 | // to stream any stdout/err data so that it can be mirrored on the plugin 12 | // host side. 13 | service GRPCStdio { 14 | // StreamStdio returns a stream that contains all the stdout/stderr. 15 | // This RPC endpoint must only be called ONCE. Once stdio data is consumed 16 | // it is not sent again. 17 | // 18 | // Callers should connect early to prevent blocking on the plugin process. 19 | rpc StreamStdio(google.protobuf.Empty) returns (stream StdioData); 20 | } 21 | 22 | // StdioData is a single chunk of stdout or stderr data that is streamed 23 | // from GRPCStdio. 24 | message StdioData { 25 | enum Channel { 26 | INVALID = 0; 27 | STDOUT = 1; 28 | STDERR = 2; 29 | } 30 | 31 | Channel channel = 1; 32 | bytes data = 2; 33 | } 34 | -------------------------------------------------------------------------------- /internal/plugin/grpc_stdio_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 5 | // versions: 6 | // - protoc-gen-go-grpc v1.3.0 7 | // - protoc (unknown) 8 | // source: internal/plugin/grpc_stdio.proto 9 | 10 | package plugin 11 | 12 | import ( 13 | context "context" 14 | grpc "google.golang.org/grpc" 15 | codes "google.golang.org/grpc/codes" 16 | status "google.golang.org/grpc/status" 17 | emptypb "google.golang.org/protobuf/types/known/emptypb" 18 | ) 19 | 20 | // This is a compile-time assertion to ensure that this generated file 21 | // is compatible with the grpc package it is being compiled against. 22 | // Requires gRPC-Go v1.32.0 or later. 23 | const _ = grpc.SupportPackageIsVersion7 24 | 25 | const ( 26 | GRPCStdio_StreamStdio_FullMethodName = "/plugin.GRPCStdio/StreamStdio" 27 | ) 28 | 29 | // GRPCStdioClient is the client API for GRPCStdio service. 30 | // 31 | // 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. 32 | type GRPCStdioClient interface { 33 | // StreamStdio returns a stream that contains all the stdout/stderr. 34 | // This RPC endpoint must only be called ONCE. Once stdio data is consumed 35 | // it is not sent again. 36 | // 37 | // Callers should connect early to prevent blocking on the plugin process. 38 | StreamStdio(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (GRPCStdio_StreamStdioClient, error) 39 | } 40 | 41 | type gRPCStdioClient struct { 42 | cc grpc.ClientConnInterface 43 | } 44 | 45 | func NewGRPCStdioClient(cc grpc.ClientConnInterface) GRPCStdioClient { 46 | return &gRPCStdioClient{cc} 47 | } 48 | 49 | func (c *gRPCStdioClient) StreamStdio(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (GRPCStdio_StreamStdioClient, error) { 50 | stream, err := c.cc.NewStream(ctx, &GRPCStdio_ServiceDesc.Streams[0], GRPCStdio_StreamStdio_FullMethodName, opts...) 51 | if err != nil { 52 | return nil, err 53 | } 54 | x := &gRPCStdioStreamStdioClient{stream} 55 | if err := x.ClientStream.SendMsg(in); err != nil { 56 | return nil, err 57 | } 58 | if err := x.ClientStream.CloseSend(); err != nil { 59 | return nil, err 60 | } 61 | return x, nil 62 | } 63 | 64 | type GRPCStdio_StreamStdioClient interface { 65 | Recv() (*StdioData, error) 66 | grpc.ClientStream 67 | } 68 | 69 | type gRPCStdioStreamStdioClient struct { 70 | grpc.ClientStream 71 | } 72 | 73 | func (x *gRPCStdioStreamStdioClient) Recv() (*StdioData, error) { 74 | m := new(StdioData) 75 | if err := x.ClientStream.RecvMsg(m); err != nil { 76 | return nil, err 77 | } 78 | return m, nil 79 | } 80 | 81 | // GRPCStdioServer is the server API for GRPCStdio service. 82 | // All implementations should embed UnimplementedGRPCStdioServer 83 | // for forward compatibility 84 | type GRPCStdioServer interface { 85 | // StreamStdio returns a stream that contains all the stdout/stderr. 86 | // This RPC endpoint must only be called ONCE. Once stdio data is consumed 87 | // it is not sent again. 88 | // 89 | // Callers should connect early to prevent blocking on the plugin process. 90 | StreamStdio(*emptypb.Empty, GRPCStdio_StreamStdioServer) error 91 | } 92 | 93 | // UnimplementedGRPCStdioServer should be embedded to have forward compatible implementations. 94 | type UnimplementedGRPCStdioServer struct { 95 | } 96 | 97 | func (UnimplementedGRPCStdioServer) StreamStdio(*emptypb.Empty, GRPCStdio_StreamStdioServer) error { 98 | return status.Errorf(codes.Unimplemented, "method StreamStdio not implemented") 99 | } 100 | 101 | // UnsafeGRPCStdioServer may be embedded to opt out of forward compatibility for this service. 102 | // Use of this interface is not recommended, as added methods to GRPCStdioServer will 103 | // result in compilation errors. 104 | type UnsafeGRPCStdioServer interface { 105 | mustEmbedUnimplementedGRPCStdioServer() 106 | } 107 | 108 | func RegisterGRPCStdioServer(s grpc.ServiceRegistrar, srv GRPCStdioServer) { 109 | s.RegisterService(&GRPCStdio_ServiceDesc, srv) 110 | } 111 | 112 | func _GRPCStdio_StreamStdio_Handler(srv interface{}, stream grpc.ServerStream) error { 113 | m := new(emptypb.Empty) 114 | if err := stream.RecvMsg(m); err != nil { 115 | return err 116 | } 117 | return srv.(GRPCStdioServer).StreamStdio(m, &gRPCStdioStreamStdioServer{stream}) 118 | } 119 | 120 | type GRPCStdio_StreamStdioServer interface { 121 | Send(*StdioData) error 122 | grpc.ServerStream 123 | } 124 | 125 | type gRPCStdioStreamStdioServer struct { 126 | grpc.ServerStream 127 | } 128 | 129 | func (x *gRPCStdioStreamStdioServer) Send(m *StdioData) error { 130 | return x.ServerStream.SendMsg(m) 131 | } 132 | 133 | // GRPCStdio_ServiceDesc is the grpc.ServiceDesc for GRPCStdio service. 134 | // It's only intended for direct use with grpc.RegisterService, 135 | // and not to be introspected or modified (even as a copy) 136 | var GRPCStdio_ServiceDesc = grpc.ServiceDesc{ 137 | ServiceName: "plugin.GRPCStdio", 138 | HandlerType: (*GRPCStdioServer)(nil), 139 | Methods: []grpc.MethodDesc{}, 140 | Streams: []grpc.StreamDesc{ 141 | { 142 | StreamName: "StreamStdio", 143 | Handler: _GRPCStdio_StreamStdio_Handler, 144 | ServerStreams: true, 145 | }, 146 | }, 147 | Metadata: "internal/plugin/grpc_stdio.proto", 148 | } 149 | -------------------------------------------------------------------------------- /log_entry.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "encoding/json" 8 | "time" 9 | ) 10 | 11 | // logEntry is the JSON payload that gets sent to Stderr from the plugin to the host 12 | type logEntry struct { 13 | Message string `json:"@message"` 14 | Level string `json:"@level"` 15 | Timestamp time.Time `json:"timestamp"` 16 | KVPairs []*logEntryKV `json:"kv_pairs"` 17 | } 18 | 19 | // logEntryKV is a key value pair within the Output payload 20 | type logEntryKV struct { 21 | Key string `json:"key"` 22 | Value interface{} `json:"value"` 23 | } 24 | 25 | // flattenKVPairs is used to flatten KVPair slice into []interface{} 26 | // for hclog consumption. 27 | func flattenKVPairs(kvs []*logEntryKV) []interface{} { 28 | var result []interface{} 29 | for _, kv := range kvs { 30 | result = append(result, kv.Key) 31 | result = append(result, kv.Value) 32 | } 33 | 34 | return result 35 | } 36 | 37 | // parseJSON handles parsing JSON output 38 | func parseJSON(input []byte) (*logEntry, error) { 39 | var raw map[string]interface{} 40 | entry := &logEntry{} 41 | 42 | err := json.Unmarshal(input, &raw) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // Parse hclog-specific objects 48 | if v, ok := raw["@message"]; ok { 49 | entry.Message = v.(string) 50 | delete(raw, "@message") 51 | } 52 | 53 | if v, ok := raw["@level"]; ok { 54 | entry.Level = v.(string) 55 | delete(raw, "@level") 56 | } 57 | 58 | if v, ok := raw["@timestamp"]; ok { 59 | t, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", v.(string)) 60 | if err != nil { 61 | return nil, err 62 | } 63 | entry.Timestamp = t 64 | delete(raw, "@timestamp") 65 | } 66 | 67 | // Parse dynamic KV args from the hclog payload. 68 | for k, v := range raw { 69 | entry.KVPairs = append(entry.KVPairs, &logEntryKV{ 70 | Key: k, 71 | Value: v, 72 | }) 73 | } 74 | 75 | return entry, nil 76 | } 77 | -------------------------------------------------------------------------------- /mtls.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "bytes" 8 | "crypto/ecdsa" 9 | "crypto/elliptic" 10 | "crypto/rand" 11 | "crypto/x509" 12 | "crypto/x509/pkix" 13 | "encoding/pem" 14 | "math/big" 15 | "time" 16 | ) 17 | 18 | // generateCert generates a temporary certificate for plugin authentication. The 19 | // certificate and private key are returns in PEM format. 20 | func generateCert() (cert []byte, privateKey []byte, err error) { 21 | key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) 22 | if err != nil { 23 | return nil, nil, err 24 | } 25 | 26 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 27 | sn, err := rand.Int(rand.Reader, serialNumberLimit) 28 | if err != nil { 29 | return nil, nil, err 30 | } 31 | 32 | host := "localhost" 33 | 34 | template := &x509.Certificate{ 35 | Subject: pkix.Name{ 36 | CommonName: host, 37 | Organization: []string{"HashiCorp"}, 38 | }, 39 | DNSNames: []string{host}, 40 | ExtKeyUsage: []x509.ExtKeyUsage{ 41 | x509.ExtKeyUsageClientAuth, 42 | x509.ExtKeyUsageServerAuth, 43 | }, 44 | KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign, 45 | BasicConstraintsValid: true, 46 | SerialNumber: sn, 47 | NotBefore: time.Now().Add(-30 * time.Second), 48 | NotAfter: time.Now().Add(262980 * time.Hour), 49 | IsCA: true, 50 | } 51 | 52 | der, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) 53 | if err != nil { 54 | return nil, nil, err 55 | } 56 | 57 | var certOut bytes.Buffer 58 | if err := pem.Encode(&certOut, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil { 59 | return nil, nil, err 60 | } 61 | 62 | keyBytes, err := x509.MarshalECPrivateKey(key) 63 | if err != nil { 64 | return nil, nil, err 65 | } 66 | 67 | var keyOut bytes.Buffer 68 | if err := pem.Encode(&keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}); err != nil { 69 | return nil, nil, err 70 | } 71 | 72 | cert = certOut.Bytes() 73 | privateKey = keyOut.Bytes() 74 | 75 | return cert, privateKey, nil 76 | } 77 | -------------------------------------------------------------------------------- /mux_broker.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "encoding/binary" 8 | "fmt" 9 | "log" 10 | "net" 11 | "sync" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/hashicorp/yamux" 16 | ) 17 | 18 | // MuxBroker is responsible for brokering multiplexed connections by unique ID. 19 | // 20 | // It is used by plugins to multiplex multiple RPC connections and data 21 | // streams on top of a single connection between the plugin process and the 22 | // host process. 23 | // 24 | // This allows a plugin to request a channel with a specific ID to connect to 25 | // or accept a connection from, and the broker handles the details of 26 | // holding these channels open while they're being negotiated. 27 | // 28 | // The Plugin interface has access to these for both Server and Client. 29 | // The broker can be used by either (optionally) to reserve and connect to 30 | // new multiplexed streams. This is useful for complex args and return values, 31 | // or anything else you might need a data stream for. 32 | type MuxBroker struct { 33 | nextId uint32 34 | session *yamux.Session 35 | streams map[uint32]*muxBrokerPending 36 | 37 | sync.Mutex 38 | } 39 | 40 | type muxBrokerPending struct { 41 | ch chan net.Conn 42 | doneCh chan struct{} 43 | } 44 | 45 | func newMuxBroker(s *yamux.Session) *MuxBroker { 46 | return &MuxBroker{ 47 | session: s, 48 | streams: make(map[uint32]*muxBrokerPending), 49 | } 50 | } 51 | 52 | // Accept accepts a connection by ID. 53 | // 54 | // This should not be called multiple times with the same ID at one time. 55 | func (m *MuxBroker) Accept(id uint32) (net.Conn, error) { 56 | var c net.Conn 57 | p := m.getStream(id) 58 | select { 59 | case c = <-p.ch: 60 | close(p.doneCh) 61 | case <-time.After(5 * time.Second): 62 | m.Lock() 63 | defer m.Unlock() 64 | delete(m.streams, id) 65 | 66 | return nil, fmt.Errorf("timeout waiting for accept") 67 | } 68 | 69 | // Ack our connection 70 | if err := binary.Write(c, binary.LittleEndian, id); err != nil { 71 | _ = c.Close() 72 | return nil, err 73 | } 74 | 75 | return c, nil 76 | } 77 | 78 | // AcceptAndServe is used to accept a specific stream ID and immediately 79 | // serve an RPC server on that stream ID. This is used to easily serve 80 | // complex arguments. 81 | // 82 | // The served interface is always registered to the "Plugin" name. 83 | func (m *MuxBroker) AcceptAndServe(id uint32, v interface{}) { 84 | conn, err := m.Accept(id) 85 | if err != nil { 86 | log.Printf("[ERR] plugin: plugin acceptAndServe error: %s", err) 87 | return 88 | } 89 | 90 | serve(conn, "Plugin", v) 91 | } 92 | 93 | // Close closes the connection and all sub-connections. 94 | func (m *MuxBroker) Close() error { 95 | return m.session.Close() 96 | } 97 | 98 | // Dial opens a connection by ID. 99 | func (m *MuxBroker) Dial(id uint32) (net.Conn, error) { 100 | // Open the stream 101 | stream, err := m.session.OpenStream() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | // Write the stream ID onto the wire. 107 | if err := binary.Write(stream, binary.LittleEndian, id); err != nil { 108 | _ = stream.Close() 109 | return nil, err 110 | } 111 | 112 | // Read the ack that we connected. Then we're off! 113 | var ack uint32 114 | if err := binary.Read(stream, binary.LittleEndian, &ack); err != nil { 115 | _ = stream.Close() 116 | return nil, err 117 | } 118 | if ack != id { 119 | _ = stream.Close() 120 | return nil, fmt.Errorf("bad ack: %d (expected %d)", ack, id) 121 | } 122 | 123 | return stream, nil 124 | } 125 | 126 | // NextId returns a unique ID to use next. 127 | // 128 | // It is possible for very long-running plugin hosts to wrap this value, 129 | // though it would require a very large amount of RPC calls. In practice 130 | // we've never seen it happen. 131 | func (m *MuxBroker) NextId() uint32 { 132 | return atomic.AddUint32(&m.nextId, 1) 133 | } 134 | 135 | // Run starts the brokering and should be executed in a goroutine, since it 136 | // blocks forever, or until the session closes. 137 | // 138 | // Uses of MuxBroker never need to call this. It is called internally by 139 | // the plugin host/client. 140 | func (m *MuxBroker) Run() { 141 | for { 142 | stream, err := m.session.AcceptStream() 143 | if err != nil { 144 | // Once we receive an error, just exit 145 | break 146 | } 147 | 148 | // Read the stream ID from the stream 149 | var id uint32 150 | if err := binary.Read(stream, binary.LittleEndian, &id); err != nil { 151 | _ = stream.Close() 152 | continue 153 | } 154 | 155 | // Initialize the waiter 156 | p := m.getStream(id) 157 | select { 158 | case p.ch <- stream: 159 | default: 160 | } 161 | 162 | // Wait for a timeout 163 | go m.timeoutWait(id, p) 164 | } 165 | } 166 | 167 | func (m *MuxBroker) getStream(id uint32) *muxBrokerPending { 168 | m.Lock() 169 | defer m.Unlock() 170 | 171 | p, ok := m.streams[id] 172 | if ok { 173 | return p 174 | } 175 | 176 | m.streams[id] = &muxBrokerPending{ 177 | ch: make(chan net.Conn, 1), 178 | doneCh: make(chan struct{}), 179 | } 180 | return m.streams[id] 181 | } 182 | 183 | func (m *MuxBroker) timeoutWait(id uint32, p *muxBrokerPending) { 184 | // Wait for the stream to either be picked up and connected, or 185 | // for a timeout. 186 | timeout := false 187 | select { 188 | case <-p.doneCh: 189 | case <-time.After(5 * time.Second): 190 | timeout = true 191 | } 192 | 193 | m.Lock() 194 | defer m.Unlock() 195 | 196 | // Delete the stream so no one else can grab it 197 | delete(m.streams, id) 198 | 199 | // If we timed out, then check if we have a channel in the buffer, 200 | // and if so, close it. 201 | if timeout { 202 | s := <-p.ch 203 | _ = s.Close() 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | // The plugin package exposes functions and helpers for communicating to 5 | // plugins which are implemented as standalone binary applications. 6 | // 7 | // plugin.Client fully manages the lifecycle of executing the application, 8 | // connecting to it, and returning the RPC client for dispensing plugins. 9 | // 10 | // plugin.Serve fully manages listeners to expose an RPC server from a binary 11 | // that plugin.Client can connect to. 12 | package plugin 13 | 14 | import ( 15 | "context" 16 | "errors" 17 | "net/rpc" 18 | 19 | "google.golang.org/grpc" 20 | ) 21 | 22 | // Plugin is the interface that is implemented to serve/connect to an 23 | // inteface implementation. 24 | type Plugin interface { 25 | // Server should return the RPC server compatible struct to serve 26 | // the methods that the Client calls over net/rpc. 27 | Server(*MuxBroker) (interface{}, error) 28 | 29 | // Client returns an interface implementation for the plugin you're 30 | // serving that communicates to the server end of the plugin. 31 | Client(*MuxBroker, *rpc.Client) (interface{}, error) 32 | } 33 | 34 | // GRPCPlugin is the interface that is implemented to serve/connect to 35 | // a plugin over gRPC. 36 | type GRPCPlugin interface { 37 | // GRPCServer should register this plugin for serving with the 38 | // given GRPCServer. Unlike Plugin.Server, this is only called once 39 | // since gRPC plugins serve singletons. 40 | GRPCServer(*GRPCBroker, *grpc.Server) error 41 | 42 | // GRPCClient should return the interface implementation for the plugin 43 | // you're serving via gRPC. The provided context will be canceled by 44 | // go-plugin in the event of the plugin process exiting. 45 | GRPCClient(context.Context, *GRPCBroker, *grpc.ClientConn) (interface{}, error) 46 | } 47 | 48 | // NetRPCUnsupportedPlugin implements Plugin but returns errors for the 49 | // Server and Client functions. This will effectively disable support for 50 | // net/rpc based plugins. 51 | // 52 | // This struct can be embedded in your struct. 53 | type NetRPCUnsupportedPlugin struct{} 54 | 55 | func (p NetRPCUnsupportedPlugin) Server(*MuxBroker) (interface{}, error) { 56 | return nil, errors.New("net/rpc plugin protocol not supported") 57 | } 58 | 59 | func (p NetRPCUnsupportedPlugin) Client(*MuxBroker, *rpc.Client) (interface{}, error) { 60 | return nil, errors.New("net/rpc plugin protocol not supported") 61 | } 62 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | -------------------------------------------------------------------------------- /protocol.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "io" 8 | "net" 9 | ) 10 | 11 | // Protocol is an enum representing the types of protocols. 12 | type Protocol string 13 | 14 | const ( 15 | ProtocolInvalid Protocol = "" 16 | ProtocolNetRPC Protocol = "netrpc" 17 | ProtocolGRPC Protocol = "grpc" 18 | ) 19 | 20 | // ServerProtocol is an interface that must be implemented for new plugin 21 | // protocols to be servers. 22 | type ServerProtocol interface { 23 | // Init is called once to configure and initialize the protocol, but 24 | // not start listening. This is the point at which all validation should 25 | // be done and errors returned. 26 | Init() error 27 | 28 | // Config is extra configuration to be outputted to stdout. This will 29 | // be automatically base64 encoded to ensure it can be parsed properly. 30 | // This can be an empty string if additional configuration is not needed. 31 | Config() string 32 | 33 | // Serve is called to serve connections on the given listener. This should 34 | // continue until the listener is closed. 35 | Serve(net.Listener) 36 | } 37 | 38 | // ClientProtocol is an interface that must be implemented for new plugin 39 | // protocols to be clients. 40 | type ClientProtocol interface { 41 | io.Closer 42 | 43 | // Dispense dispenses a new instance of the plugin with the given name. 44 | Dispense(string) (interface{}, error) 45 | 46 | // Ping checks that the client connection is still healthy. 47 | Ping() error 48 | } 49 | -------------------------------------------------------------------------------- /rpc_client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "crypto/tls" 8 | "fmt" 9 | "io" 10 | "net" 11 | "net/rpc" 12 | 13 | "github.com/hashicorp/yamux" 14 | ) 15 | 16 | // RPCClient connects to an RPCServer over net/rpc to dispense plugin types. 17 | type RPCClient struct { 18 | broker *MuxBroker 19 | control *rpc.Client 20 | plugins map[string]Plugin 21 | 22 | // These are the streams used for the various stdout/err overrides 23 | stdout, stderr net.Conn 24 | } 25 | 26 | // newRPCClient creates a new RPCClient. The Client argument is expected 27 | // to be successfully started already with a lock held. 28 | func newRPCClient(c *Client) (*RPCClient, error) { 29 | // Connect to the client 30 | conn, err := net.Dial(c.address.Network(), c.address.String()) 31 | if err != nil { 32 | return nil, err 33 | } 34 | if tcpConn, ok := conn.(*net.TCPConn); ok { 35 | // Make sure to set keep alive so that the connection doesn't die 36 | _ = tcpConn.SetKeepAlive(true) 37 | } 38 | 39 | if c.config.TLSConfig != nil { 40 | conn = tls.Client(conn, c.config.TLSConfig) 41 | } 42 | 43 | // Create the actual RPC client 44 | result, err := NewRPCClient(conn, c.config.Plugins) 45 | if err != nil { 46 | _ = conn.Close() 47 | return nil, err 48 | } 49 | 50 | // Begin the stream syncing so that stdin, out, err work properly 51 | err = result.SyncStreams( 52 | c.config.SyncStdout, 53 | c.config.SyncStderr) 54 | if err != nil { 55 | _ = result.Close() 56 | return nil, err 57 | } 58 | 59 | return result, nil 60 | } 61 | 62 | // NewRPCClient creates a client from an already-open connection-like value. 63 | // Dial is typically used instead. 64 | func NewRPCClient(conn io.ReadWriteCloser, plugins map[string]Plugin) (*RPCClient, error) { 65 | // Create the yamux client so we can multiplex 66 | mux, err := yamux.Client(conn, nil) 67 | if err != nil { 68 | _ = conn.Close() 69 | return nil, err 70 | } 71 | 72 | // Connect to the control stream. 73 | control, err := mux.Open() 74 | if err != nil { 75 | _ = mux.Close() 76 | return nil, err 77 | } 78 | 79 | // Connect stdout, stderr streams 80 | stdstream := make([]net.Conn, 2) 81 | for i := range stdstream { 82 | stdstream[i], err = mux.Open() 83 | if err != nil { 84 | _ = mux.Close() 85 | return nil, err 86 | } 87 | } 88 | 89 | // Create the broker and start it up 90 | broker := newMuxBroker(mux) 91 | go broker.Run() 92 | 93 | // Build the client using our broker and control channel. 94 | return &RPCClient{ 95 | broker: broker, 96 | control: rpc.NewClient(control), 97 | plugins: plugins, 98 | stdout: stdstream[0], 99 | stderr: stdstream[1], 100 | }, nil 101 | } 102 | 103 | // SyncStreams should be called to enable syncing of stdout, 104 | // stderr with the plugin. 105 | // 106 | // This will return immediately and the syncing will continue to happen 107 | // in the background. You do not need to launch this in a goroutine itself. 108 | // 109 | // This should never be called multiple times. 110 | func (c *RPCClient) SyncStreams(stdout io.Writer, stderr io.Writer) error { 111 | go copyStream("stdout", stdout, c.stdout) 112 | go copyStream("stderr", stderr, c.stderr) 113 | return nil 114 | } 115 | 116 | // Close closes the connection. The client is no longer usable after this 117 | // is called. 118 | func (c *RPCClient) Close() error { 119 | // Call the control channel and ask it to gracefully exit. If this 120 | // errors, then we save it so that we always return an error but we 121 | // want to try to close the other channels anyways. 122 | var empty struct{} 123 | returnErr := c.control.Call("Control.Quit", true, &empty) 124 | 125 | // Close the other streams we have 126 | if err := c.control.Close(); err != nil { 127 | return err 128 | } 129 | if err := c.stdout.Close(); err != nil { 130 | return err 131 | } 132 | if err := c.stderr.Close(); err != nil { 133 | return err 134 | } 135 | if err := c.broker.Close(); err != nil { 136 | return err 137 | } 138 | 139 | // Return back the error we got from Control.Quit. This is very important 140 | // since we MUST return non-nil error if this fails so that Client.Kill 141 | // will properly try a process.Kill. 142 | return returnErr 143 | } 144 | 145 | func (c *RPCClient) Dispense(name string) (interface{}, error) { 146 | p, ok := c.plugins[name] 147 | if !ok { 148 | return nil, fmt.Errorf("unknown plugin type: %s", name) 149 | } 150 | 151 | var id uint32 152 | if err := c.control.Call( 153 | "Dispenser.Dispense", name, &id); err != nil { 154 | return nil, err 155 | } 156 | 157 | conn, err := c.broker.Dial(id) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | return p.Client(c.broker, rpc.NewClient(conn)) 163 | } 164 | 165 | // Ping pings the connection to ensure it is still alive. 166 | // 167 | // The error from the RPC call is returned exactly if you want to inspect 168 | // it for further error analysis. Any error returned from here would indicate 169 | // that the connection to the plugin is not healthy. 170 | func (c *RPCClient) Ping() error { 171 | var empty struct{} 172 | return c.control.Call("Control.Ping", true, &empty) 173 | } 174 | -------------------------------------------------------------------------------- /rpc_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "os" 10 | "sync" 11 | "testing" 12 | "time" 13 | 14 | hclog "github.com/hashicorp/go-hclog" 15 | ) 16 | 17 | func TestClient_App(t *testing.T) { 18 | pluginLogger := hclog.New(&hclog.LoggerOptions{ 19 | Level: hclog.Trace, 20 | Output: os.Stderr, 21 | JSONFormat: true, 22 | }) 23 | 24 | testPlugin := &testInterfaceImpl{ 25 | logger: pluginLogger, 26 | } 27 | 28 | client, _ := TestPluginRPCConn(t, map[string]Plugin{ 29 | "test": &testInterfacePlugin{Impl: testPlugin}, 30 | }, nil) 31 | defer func() { _ = client.Close() }() 32 | 33 | raw, err := client.Dispense("test") 34 | if err != nil { 35 | t.Fatalf("err: %s", err) 36 | } 37 | 38 | impl, ok := raw.(testInterface) 39 | if !ok { 40 | t.Fatalf("bad: %#v", raw) 41 | } 42 | 43 | result := impl.Double(21) 44 | if result != 42 { 45 | t.Fatalf("bad: %#v", result) 46 | } 47 | } 48 | 49 | func TestClient_syncStreams(t *testing.T) { 50 | // Create streams for the server that we can talk to 51 | stdout_r, stdout_w := io.Pipe() 52 | stderr_r, stderr_w := io.Pipe() 53 | 54 | client, _ := TestPluginRPCConn(t, map[string]Plugin{}, &TestOptions{ 55 | ServerStdout: stdout_r, 56 | ServerStderr: stderr_r, 57 | }) 58 | 59 | // Start the data copying 60 | var stdout_out, stderr_out safeBuffer 61 | stdout := &safeBuffer{ 62 | b: bytes.NewBufferString("stdouttest"), 63 | } 64 | stderr := &safeBuffer{ 65 | b: bytes.NewBufferString("stderrtest"), 66 | } 67 | go func() { _ = client.SyncStreams(&stdout_out, &stderr_out) }() 68 | go func() { _, _ = io.Copy(stdout_w, stdout) }() 69 | go func() { _, _ = io.Copy(stderr_w, stderr) }() 70 | 71 | // Unfortunately I can't think of a better way to make sure all the 72 | // copies above go through so let's just exit. 73 | time.Sleep(100 * time.Millisecond) 74 | 75 | // Close everything, and lets test the result 76 | _ = client.Close() 77 | _ = stdout_w.Close() 78 | _ = stderr_w.Close() 79 | 80 | if v := stdout_out.String(); v != "stdouttest" { 81 | t.Fatalf("bad: %q", v) 82 | } 83 | if v := stderr_out.String(); v != "stderrtest" { 84 | t.Fatalf("bad: %q", v) 85 | } 86 | } 87 | 88 | type safeBuffer struct { 89 | sync.Mutex 90 | b *bytes.Buffer 91 | } 92 | 93 | func (s *safeBuffer) Write(p []byte) (n int, err error) { 94 | s.Lock() 95 | defer s.Unlock() 96 | if s.b == nil { 97 | s.b = new(bytes.Buffer) 98 | } 99 | return s.b.Write(p) 100 | } 101 | 102 | func (s *safeBuffer) Read(p []byte) (n int, err error) { 103 | s.Lock() 104 | defer s.Unlock() 105 | if s.b == nil { 106 | s.b = new(bytes.Buffer) 107 | } 108 | return s.b.Read(p) 109 | } 110 | 111 | func (s *safeBuffer) String() string { 112 | s.Lock() 113 | defer s.Unlock() 114 | if s.b == nil { 115 | s.b = new(bytes.Buffer) 116 | } 117 | return s.b.String() 118 | } 119 | -------------------------------------------------------------------------------- /rpc_server.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "net" 12 | "net/rpc" 13 | "sync" 14 | 15 | "github.com/hashicorp/yamux" 16 | ) 17 | 18 | // RPCServer listens for network connections and then dispenses interface 19 | // implementations over net/rpc. 20 | // 21 | // After setting the fields below, they shouldn't be read again directly 22 | // from the structure which may be reading/writing them concurrently. 23 | type RPCServer struct { 24 | Plugins map[string]Plugin 25 | 26 | // Stdout, Stderr are what this server will use instead of the 27 | // normal stdin/out/err. This is because due to the multi-process nature 28 | // of our plugin system, we can't use the normal process values so we 29 | // make our own custom one we pipe across. 30 | Stdout io.Reader 31 | Stderr io.Reader 32 | 33 | // DoneCh should be set to a non-nil channel that will be closed 34 | // when the control requests the RPC server to end. 35 | DoneCh chan<- struct{} 36 | 37 | lock sync.Mutex 38 | } 39 | 40 | // ServerProtocol impl. 41 | func (s *RPCServer) Init() error { return nil } 42 | 43 | // ServerProtocol impl. 44 | func (s *RPCServer) Config() string { return "" } 45 | 46 | // ServerProtocol impl. 47 | func (s *RPCServer) Serve(lis net.Listener) { 48 | defer s.done() 49 | 50 | for { 51 | conn, err := lis.Accept() 52 | if err != nil { 53 | severity := "ERR" 54 | if errors.Is(err, net.ErrClosed) { 55 | severity = "DEBUG" 56 | } 57 | log.Printf("[%s] plugin: plugin server: %s", severity, err) 58 | return 59 | } 60 | 61 | go s.ServeConn(conn) 62 | } 63 | } 64 | 65 | // ServeConn runs a single connection. 66 | // 67 | // ServeConn blocks, serving the connection until the client hangs up. 68 | func (s *RPCServer) ServeConn(conn io.ReadWriteCloser) { 69 | // First create the yamux server to wrap this connection 70 | mux, err := yamux.Server(conn, nil) 71 | if err != nil { 72 | _ = conn.Close() 73 | log.Printf("[ERR] plugin: error creating yamux server: %s", err) 74 | return 75 | } 76 | 77 | // Accept the control connection 78 | control, err := mux.Accept() 79 | if err != nil { 80 | _ = mux.Close() 81 | if err != io.EOF { 82 | log.Printf("[ERR] plugin: error accepting control connection: %s", err) 83 | } 84 | 85 | return 86 | } 87 | 88 | // Connect the stdstreams (in, out, err) 89 | stdstream := make([]net.Conn, 2) 90 | for i := range stdstream { 91 | stdstream[i], err = mux.Accept() 92 | if err != nil { 93 | _ = mux.Close() 94 | log.Printf("[ERR] plugin: accepting stream %d: %s", i, err) 95 | return 96 | } 97 | } 98 | 99 | // Copy std streams out to the proper place 100 | go copyStream("stdout", stdstream[0], s.Stdout) 101 | go copyStream("stderr", stdstream[1], s.Stderr) 102 | 103 | // Create the broker and start it up 104 | broker := newMuxBroker(mux) 105 | go broker.Run() 106 | 107 | // Use the control connection to build the dispenser and serve the 108 | // connection. 109 | server := rpc.NewServer() 110 | _ = server.RegisterName("Control", &controlServer{ 111 | server: s, 112 | }) 113 | _ = server.RegisterName("Dispenser", &dispenseServer{ 114 | broker: broker, 115 | plugins: s.Plugins, 116 | }) 117 | server.ServeConn(control) 118 | } 119 | 120 | // done is called internally by the control server to trigger the 121 | // doneCh to close which is listened to by the main process to cleanly 122 | // exit. 123 | func (s *RPCServer) done() { 124 | s.lock.Lock() 125 | defer s.lock.Unlock() 126 | 127 | if s.DoneCh != nil { 128 | close(s.DoneCh) 129 | s.DoneCh = nil 130 | } 131 | } 132 | 133 | // dispenseServer dispenses variousinterface implementations for Terraform. 134 | type controlServer struct { 135 | server *RPCServer 136 | } 137 | 138 | // Ping can be called to verify the connection (and likely the binary) 139 | // is still alive to a plugin. 140 | func (c *controlServer) Ping( 141 | null bool, response *struct{}, 142 | ) error { 143 | *response = struct{}{} 144 | return nil 145 | } 146 | 147 | func (c *controlServer) Quit( 148 | null bool, response *struct{}, 149 | ) error { 150 | // End the server 151 | c.server.done() 152 | 153 | // Always return true 154 | *response = struct{}{} 155 | 156 | return nil 157 | } 158 | 159 | // dispenseServer dispenses variousinterface implementations for Terraform. 160 | type dispenseServer struct { 161 | broker *MuxBroker 162 | plugins map[string]Plugin 163 | } 164 | 165 | func (d *dispenseServer) Dispense( 166 | name string, response *uint32, 167 | ) error { 168 | // Find the function to create this implementation 169 | p, ok := d.plugins[name] 170 | if !ok { 171 | return fmt.Errorf("unknown plugin type: %s", name) 172 | } 173 | 174 | // Create the implementation first so we know if there is an error. 175 | impl, err := p.Server(d.broker) 176 | if err != nil { 177 | // We turn the error into an errors error so that it works across RPC 178 | return errors.New(err.Error()) 179 | } 180 | 181 | // Reserve an ID for our implementation 182 | id := d.broker.NextId() 183 | *response = id 184 | 185 | // Run the rest in a goroutine since it can only happen once this RPC 186 | // call returns. We wait for a connection for the plugin implementation 187 | // and serve it. 188 | go func() { 189 | conn, err := d.broker.Accept(id) 190 | if err != nil { 191 | log.Printf("[ERR] go-plugin: plugin dispense error: %s: %s", name, err) 192 | return 193 | } 194 | 195 | serve(conn, "Plugin", impl) 196 | }() 197 | 198 | return nil 199 | } 200 | 201 | func serve(conn io.ReadWriteCloser, name string, v interface{}) { 202 | server := rpc.NewServer() 203 | if err := server.RegisterName(name, v); err != nil { 204 | log.Printf("[ERR] go-plugin: plugin dispense error: %s", err) 205 | return 206 | } 207 | 208 | server.ServeConn(conn) 209 | } 210 | -------------------------------------------------------------------------------- /runner/runner.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package runner 5 | 6 | import ( 7 | "context" 8 | "io" 9 | ) 10 | 11 | // Runner defines the interface required by go-plugin to manage the lifecycle of 12 | // of a plugin and attempt to negotiate a connection with it. Note that this 13 | // is orthogonal to the protocol and transport used, which is negotiated over stdout. 14 | type Runner interface { 15 | // Start should start the plugin and ensure any work required for servicing 16 | // other interface methods is done. If the context is cancelled, it should 17 | // only abort any attempts to _start_ the plugin. Waiting and shutdown are 18 | // handled separately. 19 | Start(ctx context.Context) error 20 | 21 | // Diagnose makes a best-effort attempt to return any debug information that 22 | // might help users understand why a plugin failed to start and negotiate a 23 | // connection. 24 | Diagnose(ctx context.Context) string 25 | 26 | // Stdout is used to negotiate the go-plugin protocol. 27 | Stdout() io.ReadCloser 28 | 29 | // Stderr is used for forwarding plugin logs to the host process logger. 30 | Stderr() io.ReadCloser 31 | 32 | // Name is a human-friendly name for the plugin, such as the path to the 33 | // executable. It does not have to be unique. 34 | Name() string 35 | 36 | AttachedRunner 37 | } 38 | 39 | // AttachedRunner defines a limited subset of Runner's interface to represent the 40 | // reduced responsibility for plugin lifecycle when attaching to an already running 41 | // plugin. 42 | type AttachedRunner interface { 43 | // Wait should wait until the plugin stops running, whether in response to 44 | // an out of band signal or in response to calling Kill(). 45 | Wait(ctx context.Context) error 46 | 47 | // Kill should stop the plugin and perform any cleanup required. 48 | Kill(ctx context.Context) error 49 | 50 | // ID is a unique identifier to represent the running plugin. e.g. pid or 51 | // container ID. 52 | ID() string 53 | 54 | AddrTranslator 55 | } 56 | 57 | // AddrTranslator translates addresses between the execution context of the host 58 | // process and the plugin. For example, if the plugin is in a container, the file 59 | // path for a Unix socket may be different between the host and the container. 60 | // 61 | // It is only intended to be used by the host process. 62 | type AddrTranslator interface { 63 | // Called before connecting on any addresses received back from the plugin. 64 | PluginToHost(pluginNet, pluginAddr string) (hostNet string, hostAddr string, err error) 65 | 66 | // Called on any host process addresses before they are sent to the plugin. 67 | HostToPlugin(hostNet, hostAddr string) (pluginNet string, pluginAddr string, err error) 68 | } 69 | 70 | // ReattachFunc can be passed to a client's reattach config to reattach to an 71 | // already running plugin instead of starting it ourselves. 72 | type ReattachFunc func() (AttachedRunner, error) 73 | -------------------------------------------------------------------------------- /server_mux.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | ) 10 | 11 | // ServeMuxMap is the type that is used to configure ServeMux 12 | type ServeMuxMap map[string]*ServeConfig 13 | 14 | // ServeMux is like Serve, but serves multiple types of plugins determined 15 | // by the argument given on the command-line. 16 | // 17 | // This command doesn't return until the plugin is done being executed. Any 18 | // errors are logged or output to stderr. 19 | func ServeMux(m ServeMuxMap) { 20 | if len(os.Args) != 2 { 21 | fmt.Fprintf(os.Stderr, 22 | "Invoked improperly. This is an internal command that shouldn't\n"+ 23 | "be manually invoked.\n") 24 | os.Exit(1) 25 | } 26 | 27 | opts, ok := m[os.Args[1]] 28 | if !ok { 29 | fmt.Fprintf(os.Stderr, "Unknown plugin: %s\n", os.Args[1]) 30 | os.Exit(1) 31 | } 32 | 33 | Serve(opts) 34 | } 35 | -------------------------------------------------------------------------------- /server_unix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | //go:build !windows 5 | // +build !windows 6 | 7 | package plugin 8 | 9 | import ( 10 | "fmt" 11 | "os" 12 | "os/user" 13 | "syscall" 14 | "testing" 15 | ) 16 | 17 | func TestUnixSocketGroupPermissions(t *testing.T) { 18 | group, err := user.LookupGroupId(fmt.Sprintf("%d", os.Getgid())) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | for name, tc := range map[string]struct { 23 | group string 24 | }{ 25 | "as integer": {fmt.Sprintf("%d", os.Getgid())}, 26 | "as name": {group.Name}, 27 | } { 28 | t.Run(name, func(t *testing.T) { 29 | ln, err := serverListener_unix(UnixSocketConfig{Group: tc.group}) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | defer func() { _ = ln.Close() }() 34 | 35 | info, err := os.Lstat(ln.Addr().String()) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | if info.Mode()&os.ModePerm != 0o660 { 40 | t.Fatal(info.Mode()) 41 | } 42 | stat, ok := info.Sys().(*syscall.Stat_t) 43 | if !ok { 44 | t.Fatal() 45 | } 46 | if stat.Gid != uint32(os.Getgid()) { 47 | t.Fatalf("Expected %d, but got %d", os.Getgid(), stat.Gid) 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "io" 8 | "log" 9 | ) 10 | 11 | func copyStream(name string, dst io.Writer, src io.Reader) { 12 | if src == nil { 13 | panic(name + ": src is nil") 14 | } 15 | if dst == nil { 16 | panic(name + ": dst is nil") 17 | } 18 | if _, err := io.Copy(dst, src); err != nil && err != io.EOF { 19 | log.Printf("[ERR] plugin: stream copy '%s' error: %s", name, err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/grpc/test.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | syntax = "proto3"; 5 | 6 | package grpctest; 7 | 8 | option go_package = "./grpctest"; 9 | 10 | import "google/protobuf/empty.proto"; 11 | 12 | message TestRequest { 13 | int32 Input = 1; 14 | } 15 | 16 | message TestResponse { 17 | int32 Output = 2; 18 | } 19 | 20 | message PrintKVRequest { 21 | string Key = 1; 22 | oneof Value { 23 | string ValueString = 2; 24 | int32 ValueInt = 3; 25 | } 26 | } 27 | 28 | message PrintKVResponse { 29 | 30 | } 31 | 32 | message BidirectionalRequest { 33 | uint32 id = 1; 34 | } 35 | 36 | message BidirectionalResponse { 37 | uint32 id = 1; 38 | } 39 | 40 | message PrintStdioRequest { 41 | bytes stdout = 1; 42 | bytes stderr = 2; 43 | } 44 | 45 | service Test { 46 | rpc Double(TestRequest) returns (TestResponse) {} 47 | rpc PrintKV(PrintKVRequest) returns (PrintKVResponse) {} 48 | rpc Bidirectional(BidirectionalRequest) returns (BidirectionalResponse) {} 49 | rpc Stream(stream TestRequest) returns (stream TestResponse) {} 50 | rpc PrintStdio(PrintStdioRequest) returns (google.protobuf.Empty) {} 51 | } 52 | 53 | message PingRequest { 54 | } 55 | 56 | message PongResponse { 57 | string msg = 1; 58 | } 59 | 60 | service PingPong { 61 | rpc Ping(PingRequest) returns (PongResponse) {} 62 | } 63 | -------------------------------------------------------------------------------- /testing.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package plugin 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "io" 10 | "net" 11 | "net/rpc" 12 | "testing" 13 | 14 | hclog "github.com/hashicorp/go-hclog" 15 | "github.com/hashicorp/go-plugin/internal/grpcmux" 16 | "google.golang.org/grpc" 17 | "google.golang.org/grpc/credentials/insecure" 18 | ) 19 | 20 | // TestOptions allows specifying options that can affect the behavior of the 21 | // test functions 22 | type TestOptions struct { 23 | //ServerStdout causes the given value to be used in place of a blank buffer 24 | //for RPCServer's Stdout 25 | ServerStdout io.ReadCloser 26 | 27 | //ServerStderr causes the given value to be used in place of a blank buffer 28 | //for RPCServer's Stderr 29 | ServerStderr io.ReadCloser 30 | } 31 | 32 | // The testing file contains test helpers that you can use outside of 33 | // this package for making it easier to test plugins themselves. 34 | 35 | // TestConn is a helper function for returning a client and server 36 | // net.Conn connected to each other. 37 | func TestConn(t testing.TB) (net.Conn, net.Conn) { 38 | // Listen to any local port. This listener will be closed 39 | // after a single connection is established. 40 | l, err := net.Listen("tcp", "127.0.0.1:0") 41 | if err != nil { 42 | t.Fatalf("err: %s", err) 43 | } 44 | 45 | // Start a goroutine to accept our client connection 46 | var serverConn net.Conn 47 | doneCh := make(chan struct{}) 48 | go func() { 49 | defer close(doneCh) 50 | defer func() { _ = l.Close() }() 51 | var err error 52 | serverConn, err = l.Accept() 53 | if err != nil { 54 | t.Fatalf("err: %s", err) 55 | } 56 | }() 57 | 58 | // Connect to the server 59 | clientConn, err := net.Dial("tcp", l.Addr().String()) 60 | if err != nil { 61 | t.Fatalf("err: %s", err) 62 | } 63 | 64 | // Wait for the server side to acknowledge it has connected 65 | <-doneCh 66 | 67 | return clientConn, serverConn 68 | } 69 | 70 | // TestRPCConn returns a rpc client and server connected to each other. 71 | func TestRPCConn(t testing.TB) (*rpc.Client, *rpc.Server) { 72 | clientConn, serverConn := TestConn(t) 73 | 74 | server := rpc.NewServer() 75 | go server.ServeConn(serverConn) 76 | 77 | client := rpc.NewClient(clientConn) 78 | return client, server 79 | } 80 | 81 | // TestPluginRPCConn returns a plugin RPC client and server that are connected 82 | // together and configured. 83 | func TestPluginRPCConn(t testing.TB, ps map[string]Plugin, opts *TestOptions) (*RPCClient, *RPCServer) { 84 | // Create two net.Conns we can use to shuttle our control connection 85 | clientConn, serverConn := TestConn(t) 86 | 87 | // Start up the server 88 | server := &RPCServer{Plugins: ps, Stdout: new(bytes.Buffer), Stderr: new(bytes.Buffer)} 89 | if opts != nil { 90 | if opts.ServerStdout != nil { 91 | server.Stdout = opts.ServerStdout 92 | } 93 | if opts.ServerStderr != nil { 94 | server.Stderr = opts.ServerStderr 95 | } 96 | } 97 | go server.ServeConn(serverConn) 98 | 99 | // Connect the client to the server 100 | client, err := NewRPCClient(clientConn, ps) 101 | if err != nil { 102 | t.Fatalf("err: %s", err) 103 | } 104 | 105 | return client, server 106 | } 107 | 108 | // TestGRPCConn returns a gRPC client conn and grpc server that are connected 109 | // together and configured. The register function is used to register services 110 | // prior to the Serve call. This is used to test gRPC connections. 111 | func TestGRPCConn(t testing.TB, register func(*grpc.Server)) (*grpc.ClientConn, *grpc.Server) { 112 | // Create a listener 113 | l, err := net.Listen("tcp", "127.0.0.1:0") 114 | if err != nil { 115 | t.Fatalf("err: %s", err) 116 | } 117 | 118 | server := grpc.NewServer() 119 | register(server) 120 | go func() { _ = server.Serve(l) }() 121 | 122 | // Connect to the server 123 | conn, err := grpc.Dial( 124 | l.Addr().String(), 125 | grpc.WithBlock(), 126 | grpc.WithTransportCredentials(insecure.NewCredentials()), 127 | ) 128 | if err != nil { 129 | t.Fatalf("err: %s", err) 130 | } 131 | 132 | // Connection successful, close the listener 133 | _ = l.Close() 134 | 135 | return conn, server 136 | } 137 | 138 | // TestPluginGRPCConn returns a plugin gRPC client and server that are connected 139 | // together and configured. This is used to test gRPC connections. 140 | func TestPluginGRPCConn(t testing.TB, multiplex bool, ps map[string]Plugin) (*GRPCClient, *GRPCServer) { 141 | // Create a listener 142 | ln, err := serverListener(UnixSocketConfig{}) 143 | if err != nil { 144 | t.Fatal(err) 145 | } 146 | 147 | logger := hclog.New(&hclog.LoggerOptions{ 148 | Level: hclog.Debug, 149 | }) 150 | 151 | // Start up the server 152 | var muxer *grpcmux.GRPCServerMuxer 153 | if multiplex { 154 | muxer = grpcmux.NewGRPCServerMuxer(logger, ln) 155 | ln = muxer 156 | } 157 | server := &GRPCServer{ 158 | Plugins: ps, 159 | DoneCh: make(chan struct{}), 160 | Server: DefaultGRPCServer, 161 | Stdout: new(bytes.Buffer), 162 | Stderr: new(bytes.Buffer), 163 | logger: logger, 164 | muxer: muxer, 165 | } 166 | if err := server.Init(); err != nil { 167 | t.Fatalf("err: %s", err) 168 | } 169 | go server.Serve(ln) 170 | 171 | client := &Client{ 172 | address: ln.Addr(), 173 | protocol: ProtocolGRPC, 174 | config: &ClientConfig{ 175 | Plugins: ps, 176 | GRPCBrokerMultiplex: multiplex, 177 | }, 178 | logger: logger, 179 | } 180 | 181 | grpcClient, err := newGRPCClient(context.Background(), client) 182 | if err != nil { 183 | t.Fatal(err) 184 | } 185 | 186 | return grpcClient, server 187 | } 188 | --------------------------------------------------------------------------------