github.com/hashicorp/go-plugin@v1.6.0/docs/extensive-go-plugin-tutorial.md (about) 1 # Intro 2 3 If you don't know what go-plugin is, don't worry, here is a small introduction on the subject matter: 4 5 Back in the old days when Go didn't have the `plugin` package, HashiCorp was looking for a way to use plugins. 6 7 Mitchell had this brilliant idea of using RPC over the local network to serve a local interface as something that could easily be implemented with any other language that supported RPC. This sounds convoluted but has many benefits! For example, your code will never crash because of a plugin and the ability to use any language to implement a plugin. Not just Go. 8 9 It has been a battle-hardened solution for years now and is being actively used by Terraform, Vault, Consul, and especially Packer. All using go-plugin in order to provide a much needed flexibility. Writing a plugin is easy. Or so they say. 10 11 It can get complicated quickly, for example, if you are trying to use GRPC. You can lose sight of what exactly you'll need to implement, where and why; or utilizing various languages; or using go-plugins in your own project and extending your CLI with pluggable components. 12 13 These are all nothing to sneeze at. Suddenly you'll find yourself with hundreds of lines of code pasted from various examples and yet nothing works. Or worse, it DOES work but you have no idea how. Then you find yourself needing to extend it with a new capability, or you find an elusive bug and can't trace its origins. 14 15 Let's try to demystify things and draw a clearer picture about how it works and how the pieces fit together. 16 17 # Basic plugin 18 19 Start by writing a simple Go GRPC plugin. In fact, we can go through the basic example in the go-plugin’s repository. We'll go step-by-step, and the switch to GRPC will be much easier! 20 21 ## Basic concepts 22 23 ### Server 24 25 In the case of plugins, the Server is the one serving the plugin's implementation. This means the server will have to provide the implementation to an interface. 26 27 ### Client 28 29 The Client calls the server in order to execute the desired behaviour. The underlying logic will connect to the server running on localhost on a random higher port, call the wanted function’s implementation and wait for a response. Once the response is received provide that back to the calling Client. 30 31 ## Implementation 32 33 ### The main function 34 35 #### Logger 36 37 The plugins defined here use stdout in a special way. If you aren't writing a Go based plugin, you will have to do that yourself by outputting something like this: 38 39 ~~~ 40 1|1|tcp|127.0.0.1:1234|grpc 41 ~~~ 42 43 We'll come back to this later. Suffice to say the framework will pick this up and will connect to the plugin based on the output. In order to get some output back, we must define a special logger: 44 45 ~~~go 46 // Create an hclog.Logger 47 logger := hclog.New(&hclog.LoggerOptions{ 48 Name: "plugin", 49 Output: os.Stdout, 50 Level: hclog.Debug, 51 }) 52 ~~~ 53 54 #### NewClient 55 56 ~~~go 57 // We're a host! Start by launching the plugin process. 58 client := plugin.NewClient(&plugin.ClientConfig{ 59 HandshakeConfig: handshakeConfig, 60 Plugins: pluginMap, 61 Cmd: exec.Command("./plugin/greeter"), 62 Logger: logger, 63 }) 64 defer client.Kill() 65 ~~~ 66 67 What is happening here? Let's see one by one: 68 69 `HandshakeConfig: handshakeConfig,`: This part is the handshake configuration of the plugin. It has a nice comment as well. 70 71 ~~~go 72 // handshakeConfigs are used to just do a basic handshake between 73 // a plugin and host. If the handshake fails, a user friendly error is shown. 74 // This prevents users from executing bad plugins or executing a plugin 75 // directory. It is a UX feature, not a security feature. 76 var handshakeConfig = plugin.HandshakeConfig{ 77 ProtocolVersion: 1, 78 MagicCookieKey: "BASIC_PLUGIN", 79 MagicCookieValue: "hello", 80 } 81 ~~~ 82 83 The `ProtocolVersion` here is used in order to maintain compatibility with your current plugin versions. It's basically like an API version. If you increase this, you will have two options. Don't accept lower protocol versions or switch to the version number and use a different client implementation for a lower version than for a higher version. This way you will maintain backwards compatibility. 84 85 The `MagicCookieKey` and `MagicCookieValue` are used for a basic handshake which the comment is talking about. You have to set this **ONCE** for your application. Never change it again, for if you do, your plugins will no longer work. For uniqueness sake, I suggest using UUID. 86 87 `Cmd` is one of the most important parts about a plugin. Basically how plugins work is that they boil down to a compiled binary which starts an RPC server. This is where you will have to define the binary which will be executed and does all this. Since this is all happening locally, (please keep in mind that Go-plugins only support localhost, and for a good reason), these binaries will most likely sit next to your application's binary or in a pre-configured global location. Something like: `~/.config/my-app/plugins`. This is individual for each plugin of course. The plugins can be autoloaded via a discovery function given a path and a glob. 88 89 And last but not least is the `Plugins` map. This map is used in order to identify a plugin called `Dispense`. This map is globally available and must stay consistent in order for all the plugins to work: 90 91 ~~~go 92 // pluginMap is the map of plugins we can dispense. 93 var pluginMap = map[string]plugin.Plugin{"greeter": &shared.GreeterPlugin{}} 94 ~~~ 95 96 You can see that the key is the name of the plugin and the value is the plugin. 97 98 We then proceed to create an RPC client: 99 100 ~~~go 101 // Connect via RPC 102 rpcClient, err := client.Client() 103 if err != nil { 104 log.Fatal(err) 105 } 106 ~~~ 107 108 Nothing fancy about this one... 109 110 The interesting part is this: 111 112 ~~~go 113 // Request the plugin 114 raw, err := rpcClient.Dispense("greeter") 115 if err != nil { 116 log.Fatal(err) 117 } 118 ~~~ 119 120 Dispense will look in the above created map and search for the plugin. If it cannot find it, it will throw an error. If it does find it, it will cast this plugin to an RPC or a GRPC type plugin. Then proceed to create an RPC or a GRPC client out of it. 121 122 There is no call yet. This is just creating a client and parsing it to a respective representation. 123 124 Now, the magic: 125 126 ~~~go 127 // We should have a Greeter now! This feels like a normal interface 128 // implementation but is in fact over an RPC connection. 129 greeter := raw.(shared.Greeter) 130 fmt.Println(greeter.Greet()) 131 ~~~ 132 133 Here, we are type asserting our raw GRPC client into our own plugin type. This is so we can call the respective function on the plugin! Once that's done we will have a {client,struct,implementation} that can be called like a simple function. 134 135 The implementation right now comes from greeter_impl.go, but that will change once protoc makes an appearance. 136 137 Behind the scenes, go-plugin will do a bunch of hat tricks with multiplexing TCP connections as well as a remote procedure call to our plugin. Our plugin then will run the function, generate some kind of output, and will then send that back for the waiting client. 138 139 The client will then proceed to parse the message into a given response type and will then return it back to the client’s callee. 140 141 This concludes main.go for now. 142 143 ### The Interface 144 145 Now let’s investigate the Interface. The interface is used to provide calling details. This interface will be what defines our plugins’ capabilities. How does our `Greeter` look like? 146 147 ~~~go 148 // Greeter is the interface that we're exposing as a plugin. 149 type Greeter interface { 150 Greet() string 151 } 152 ~~~ 153 154 This defines a function which will return a string typed value. 155 156 Now, we will need a couple of things for this to work. Firstly we need something which defines the RPC workings. go-plugin is working with `net/http` inside. It also uses something called Yamux for connection multiplexing, but we needn’t worry about this detail. 157 158 Implementing the RPC details looks like this: 159 160 ~~~go 161 // Here is an implementation that talks over RPC 162 type GreeterRPC struct { 163 client *rpc.Client 164 } 165 166 func (g *GreeterRPC) Greet() string { 167 var resp string 168 err := g.client.Call("Plugin.Greet", new(interface{}), &resp) 169 if err != nil { 170 // You usually want your interfaces to return errors. If they don't, 171 // there isn't much other choice here. 172 panic(err) 173 } 174 175 return resp 176 } 177 ~~~ 178 179 Here the GreeterRPC struct is an RPC specific implementation that will handle communication over RPC. This is the Client in this setup. 180 181 In case of gRPC, it would look like this: 182 183 ~~~go 184 // GRPCClient is an implementation of KV that talks over RPC. 185 type GreeterGRPC struct{ client proto.GreeterClient } 186 187 func (m *GreeterGRPC) Greet() (string, error) { 188 s, err := m.client.Greet(context.Background(), &proto.Empty{}) 189 return s, err 190 } 191 ~~~ 192 193 What's Proto and what is GreeterClient? GRPC uses Google's protoc library to serialize and unserialize data. `proto.GreeterClient` is generated Go code by protoc. This code is a skeleton for which implementation detail will be replaced on run time. Well, the actual result will be used and not replaced as such. 194 195 Back to our previous example. The RPC client calls a specific Plugin function called Greet for which the implementation will be provided by a Server that will be streamed back over the RPC protocol. 196 197 The server is pretty easy to follow: 198 199 ~~~go 200 // Here is the RPC server that GreeterRPC talks to, conforming to 201 // the requirements of net/rpc 202 type GreeterRPCServer struct { 203 // This is the real implementation 204 Impl Greeter 205 } 206 ~~~ 207 208 Impl is the concrete implementation that will be called in the Server's implementation of the Greet plugin. Now we must define Greet on the RPCServer in order for it to be able to call the remote code. This looks like as follows: 209 210 ~~~go 211 func (s *GreeterRPCServer) Greet(args interface{}, resp *string) error { 212 *resp = s.Impl.Greet() 213 return nil 214 } 215 ~~~ 216 217 This is all still boilerplate for the RPC works. Now comes the plugin. For this, the comment is actually quite good: 218 219 ~~~go 220 // This is the implementation of plugin.Plugin so we can serve/consume this 221 // 222 // This has two methods: Server must return an RPC server for this plugin 223 // type. We construct a GreeterRPCServer for this. 224 // 225 // Client must return an implementation of our interface that communicates 226 // over an RPC client. We return GreeterRPC for this. 227 // 228 // Ignore MuxBroker. That is used to create more multiplexed streams on our 229 // plugin connection and is a more advanced use case. 230 type GreeterPlugin struct { 231 // Impl Injection 232 Impl Greeter 233 } 234 235 func (p *GreeterPlugin) Server(*plugin.MuxBroker) (interface{}, error) { 236 return &GreeterRPCServer{Impl: p.Impl}, nil 237 } 238 239 func (GreeterPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { 240 return &GreeterRPC{client: c}, nil 241 } 242 ~~~ 243 244 So, remember: `GreeterRPCServer` is the one calling the actual implementation while Client is receiving the result of that call. The `GreeterPlugin` has the `Greeter` interface embedded just like the `RPCServer`. We will use the `GreeterPlugin` as a struct in the plugin map. This is the plugin that we will actually use. 245 246 This is all still common stuff. These things will need to be visible for both. The plugin's implementation will use the interface to see what it needs to implement. The Client will use it to see what to call and what APIs are available. Like, `Greet`. 247 248 ### The Implementation 249 250 In a completely separate package, but which still has access to the interface definition, this plugin could be like this: 251 252 ~~~go 253 // Here is a real implementation of Greeter 254 type GreeterHello struct { 255 logger hclog.Logger 256 } 257 258 func (g *GreeterHello) Greet() string { 259 g.logger.Debug("message from GreeterHello.Greet") 260 return "Hello!" 261 } 262 ~~~ 263 264 We create a struct and then add the function to it which is defined by the plugin's interface. This interface, since it's required by both parties, could well sit in a common package outside of both programs. Something like a SDK. Both code could import it and use it as a common dependency. This way we have separated the interface from the plugin **and** the calling client. 265 266 The `main` function could look something like this: 267 268 ~~~go 269 logger := hclog.New(&hclog.LoggerOptions{ 270 Level: hclog.Trace, 271 Output: os.Stderr, 272 JSONFormat: true, 273 }) 274 275 greeter := &GreeterHello{ 276 logger: logger, 277 } 278 // pluginMap is the map of plugins we can dispense. 279 var pluginMap = map[string]plugin.Plugin{ 280 "greeter": &shared.GreeterPlugin{Impl: greeter}, 281 } 282 283 logger.Debug("message from plugin", "foo", "bar") 284 285 plugin.Serve(&plugin.ServeConfig{ 286 HandshakeConfig: handshakeConfig, 287 Plugins: pluginMap, 288 }) 289 ~~~ 290 291 Notice two things that we need. One is the `handshakeConfig`. You can either define it here, with the same cookie details as you defined in the client code, or you can extract the handshake information into the SDK. This is up to you. 292 293 Then the next interesting thing is the `plugin.Serve` method. The plugins open up a RPC communication socket and over a hijacked `stdout`, broadcasts its availability to the calling Client in this format: 294 295 ~~~bash 296 CORE-PROTOCOL-VERSION | APP-PROTOCOL-VERSION | NETWORK-TYPE | NETWORK-ADDR | PROTOCOL 297 ~~~ 298 299 For Go plugins, you don't have to concern yourself with this. `go-plugin` takes care of all this for you. For non-Go versions, we must take this into account. And before calling serve, we need to output this information to `stdout`. 300 301 For example, a Python plugin must deal with this himself. Like this: 302 303 ~~~python 304 # Output information 305 print("1|1|tcp|127.0.0.1:1234|grpc") 306 sys.stdout.flush() 307 ~~~ 308 309 For GRPC plugins, it's also mandatory to implement a HealthChecker. 310 311 How would all this look like with GRPC? 312 313 It gets slightly more complicated but not too much. We need to use `protoc` to create a protocol description for our implementation, and then we will call that. Let's look at this now by converting the basic greeter example into GRPC. 314 315 # GRPC Basic plugin 316 317 The example that's under GRPC is quite elaborate and perhaps you don't need the Python part. I will focus on the basic RPC example into a GRPC example. That should not be a problem. 318 319 ## The API 320 321 First and foremost, you will need to define an API to implement with `protoc`. For our basic example, the protoc file could look like this: 322 323 ~~~proto3 324 syntax = "proto3"; 325 package proto; 326 327 message GreetResponse { 328 string message = 1; 329 } 330 331 message Empty {} 332 333 service GreeterService { 334 rpc Greet(Empty) returns (GreetResponse); 335 } 336 ~~~ 337 338 The syntax is quite simple and readable. What this defines is a message, which is a response, that will contain a `message` with the type `string`. The `service` defines a service which has a method called `Greet`. The service definition is basically an interface for which we will be providing the concrete implementation through the plugin. 339 340 To read more about protoc, visit this page: [Google Protocol Buffer](https://developers.google.com/protocol-buffers/). 341 342 ## Generate the code 343 344 Now, with the protoc definition in hand, we need to generate the stubs that the local client implementation can call. That client call will then, through the remote procedure call, call the right function on the server which will have the concrete implementation at the ready. Run it and return the result in the specified format. Because the stub needs to be available by both parties, (the client AND the server), this needs to live in a shared location. 345 346 The client is calling the stub and the server is implementing the stub. Both need it in order to know what to call/implement. 347 348 To generate the code, run the following command: 349 350 ~~~bash 351 protoc -I proto/ proto/greeter.proto --go_out=plugins=grpc:proto 352 ~~~ 353 354 I encourage you to read the generated code. Much will make little sense at first. It will have a bunch of structs and defined things that the GRPC package will use in order to server the function. The interesting bits and pieces are: 355 356 ~~~go 357 func (m *GreetResponse) GetMessage() string { 358 if m != nil { 359 return m.Message 360 } 361 return "" 362 } 363 ~~~ 364 365 Which will get use the message inside the response. 366 367 ~~~go 368 type GreeterServiceClient interface { 369 Greet(ctx context.Context, in *Empty, opts ...grpc.CallOption) (*GreetResponse, error) 370 } 371 ~~~ 372 373 374 This is our ServiceClient interface which defines the Greet function’s topology. 375 376 And lastly, this guy: 377 378 ~~~go 379 func RegisterGreeterServiceServer(s *grpc.Server, srv GreeterServiceServer) { 380 s.RegisterService(&_GreeterService_serviceDesc, srv) 381 } 382 ~~~ 383 384 Which we will need in order to register our implementation for the server. We can ignore the rest. 385 386 ## The interface 387 388 Much like the RPC, we need to define an interface for the client and server to use. This must be in a shared place as both the server and the client need to know about it. You could put this into an SDK and your peers could just get the SDK and implement some function for define and done. The interface definition in the GRPC land could look something like this: 389 390 ~~~go 391 // Greeter is the interface that we're exposing as a plugin. 392 type Greeter interface { 393 Greet() string 394 } 395 396 // This is the implementation of plugin.GRPCPlugin so we can serve/consume this. 397 type GreeterGRPCPlugin struct { 398 // GRPCPlugin must still implement the Plugin interface 399 plugin.Plugin 400 // Concrete implementation, written in Go. This is only used for plugins 401 // that are written in Go. 402 Impl Greeter 403 } 404 405 func (p *GreeterGRPCPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { 406 proto.RegisterGreeterServer(s, &GRPCServer{Impl: p.Impl}) 407 return nil 408 } 409 410 func (p *GreeterGRPCPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { 411 return &GRPCClient{client: proto.NewGreeterClient(c)}, nil 412 } 413 ~~~ 414 415 With this we have the Plugin's implementation for hashicorp what needed to be done. The plugin will call the underlying implementation and serve/consume the plugin. We can now write the GRPC part of it. 416 417 Please note that `proto` is a shared library too where the protocol stubs reside. That needs to be somewhere on the path or in a separate SDK of some sort, but it must be visible. 418 419 ## Writing the GRPC Client 420 421 Firstly we define the grpc client struct: 422 423 ~~~go 424 // GRPCClient is an implementation of Greeter that talks over RPC. 425 type GRPCClient struct{ client proto.GreeterClient } 426 ~~~ 427 428 Then we define how the client will call the remote function: 429 430 ~~~go 431 func (m *GRPCClient) Greet() string { 432 ret, _ := m.client.Greet(context.Background(), &proto.Empty{}) 433 return ret.Message 434 } 435 ~~~ 436 437 This will take the `client` in the `GRPCClient` and will call the method on it. Once that's done we will return to the result `Message` property which will be `Hello!`. `proto.Empty` is an empty struct; we use this if there is no parameter for a defined method or no return value. We can't just leave it blank. `protoc` needs to be told explicitly that there is no parameter or return value. 438 439 ## Writing the GRPC Server 440 441 The server implementation will also be similar. We call `Impl` here which will have our concrete plugin implementation. 442 443 ~~~go 444 // Here is the gRPC server that GRPCClient talks to. 445 type GRPCServer struct { 446 // This is the real implementation 447 Impl Greeter 448 } 449 450 func (m *GRPCServer) Greet( 451 ctx context.Context, 452 req *proto.Empty) *proto.GreeterResponse { 453 v := m.Impl.Greet() 454 return &proto.GreeterResponse{Message: v} 455 } 456 ~~~ 457 458 And we will use the `protoc` defined message response. `v` will have the response from `Greet` which will be `Hello!` provided by the concrete plugin's implementation. We then transform that into a protoc type by setting the `Message` property on the `GreeterResponse` struct provided by the automatically generated protoc stub code. 459 460 Easy, right? 461 462 ## Writing the plugin itself 463 464 The whole thing looks much like the RPC implementation with just a few small modifications and changes. This can sit completely outside of everything, or can even be provided by a third party implementor. 465 466 ~~~go 467 // Here is a real implementation of KV that writes to a local file with 468 // the key name and the contents are the value of the key. 469 type Greeter struct{} 470 471 func (Greeter) Greet() error { 472 return "Hello!" 473 } 474 475 func main() { 476 plugin.Serve(&plugin.ServeConfig{ 477 HandshakeConfig: shared.Handshake, 478 Plugins: map[string]plugin.Plugin{ 479 "greeter": &shared.GreeterGRPCPlugin{Impl: &Greeter{}}, 480 }, 481 482 // A non-nil value here enables gRPC serving for this plugin... 483 GRPCServer: plugin.DefaultGRPCServer, 484 }) 485 } 486 ~~~ 487 488 ## Calling it all in the main 489 490 Once all that is done, the `main` function looks the same as RPC's main but with some small modifications. 491 492 ~~~go 493 // We're a host. Start by launching the plugin process. 494 client := plugin.NewClient(&plugin.ClientConfig{ 495 HandshakeConfig: shared.Handshake, 496 Plugins: shared.PluginMap, 497 Cmd: exec.Command("./plugin/greeter"), 498 AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 499 }) 500 ~~~ 501 502 The `NewClient` now defines `AllowedProtocols` to be `ProtocolGRPC`. The rest is the same as before calling `Dispense` and value hinting the plugin to the correct type then calling `Greet()`. 503 504 # Conclusion 505 506 This is it. We made it! Now our plugin works over GRPC with a defined API by protoc. The plugin's implementation can live where ever we want it, but it needs some shared data. These are: 507 508 * The generated code by `protoc` 509 * The defined plugin interface 510 * The GRPC Server and Client 511 512 These need to be visible by both the Client and the Server. The Server here is the plugin. If you are planning on making people be able to extend your application with go-plugin, you should make these available as a separate SDK. So people won't have to include your whole project just to implement an interface and use protoc. In fact, you could also extract the `protoc` definition into a separate repository so that your SDK can also pull it in. 513 514 You will have three repositories: 515 * Your application 516 * The SDK providing the interface and the GRPC Server and Client implementation 517 * The protoc definition file and generated skeleton ( for Go based plugins ). 518 519 Other languages will have to generate their own protoc code, and includ it into the plugin; like the Python implementation example located here: [Go-plugin Python Example](https://github.com/hashicorp/go-plugin/tree/master/examples/grpc/plugin-python). Also, read this documentation carefully: [non-go go-plugin](https://github.com/hashicorp/go-plugin/blob/master/docs/guide-plugin-write-non-go.md). This document will also clarify what `1|1|tcp|127.0.0.1:1234|grpc` means and will dissipate the confusion around how plugins work. 520 521 Lastly, if you would like to have an in-depth explanation about how go-plugin came to be, watch this video by Mitchell: 522 523 [go-plugin explanation video](https://www.youtube.com/watch?v=SRvm3zQQc1Q). 524 525 That's it. I hope this has helped to clear the confusion around how to use go-plugin.