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.