hashicorp/go-plugin

repository·main·Indexed 27 days ago

https://github.com/hashicorp/go-plugin

A Go-based plugin system that uses RPC (net/rpc or gRPC) to allow host processes to communicate with plugins running as separate subprocesses. Used by HashiCorp products like Terraform, Vault, and Nomad, it provides isolation, cross-language support via gRPC, bidirectional communication, and security features including cryptographic checksum verification and mTLS. It supports complex arguments via MuxBroker, protocol versioning, and host process upgrades through plugin reattachment.

Tokens
11K
Snippets
34
Records
83
Agent score
91%

What's inside go-plugin

  1. Core features of go-plugin

    main

    The go-plugin system provides several production-ready features for building robust plugin architectures:

    • Go Interface Implementation: Plugins feel like native Go code; authors implement interfaces and users call them as if they were in the same process.
    • Cross-language Support: Supports serving plugins via gRPC, allowing plugins to be written in any major language.
    • Complex Arguments: Supports io.Reader, io.Writer, and other complex types via the MuxBroker library.
    • Bidirectional Communication: Allows the host to pass interface implementations to the plugin, enabling callbacks into the host.
    • Built-in Logging: Automatically mirrors plugin log output to the host, prefixed with the plugin path. Supports structured logging if hclog is used by both parties.
    • Protocol Versioning: Supports a basic protocol version to invalidate incompatible plugins when interface signatures change.
    • Stdout/Stderr Syncing: Mirrors plugin stdout/stderr to the host process.
    • TTY Preservation: Connects plugin stdin to the host's stdin, allowing interactive tools (like ssh) to work within a plugin.
    • Host Upgrades: Supports reattaching to running plugins via ReattachConfig in NewClient to allow host process upgrades.
    • Security: Supports cryptographic verification via checksums and encrypted RPC communication using TLS.
  2. Understand the go-plugin architecture

    main

    The go-plugin system uses RPC (Remote Procedure Call) over the local network to facilitate communication between a host application and a plugin. This architecture provides two primary benefits:

    1. Isolation: Because plugins run as separate processes, a plugin crash will not crash the host application.
    2. Language Agnostic: Plugins can be implemented in any language that supports the chosen RPC protocol (e.g., gRPC), not just Go.

    The system consists of two main components:

    • Server: The plugin process itself. It implements a specific interface and serves that implementation via RPC.
    • Client: The host application. It connects to the server running on localhost (typically on a random high-numbered port), invokes methods on the interface, and receives the responses.
  3. Learn how to use go-plugin via the extensive tutorial

    main
    For a deep dive into the usage and structure of go-plugin, refer to the extensive tutorial. This guide provides a detailed walkthrough of setting up plugins, specifically using the implementations found in the examples folder of the repository.
  4. Register the gRPC Health Checking Service

    main

    go-plugin requires the gRPC Health Checking Service to be registered on your server. You must explicitly set the status of the component named "plugin" to SERVING. Failure to do this may cause the host to abruptly restart your plugin process.

    health = HealthServicer()
    health.set("plugin", health_pb2.HealthCheckResponse.ServingStatus.Value('SERVING'))
    health_pb2_grpc.add_HealthServicer_to_server(health, server)
  5. Write plugins in languages other than Go

    main
    If you need to implement a plugin using a programming language other than Go, follow the guide for writing non-Go plugins. This allows you to leverage different ecosystems while still integrating with a Go-based host application via the go-plugin RPC mechanism.
  6. Serve a gRPC plugin service

    main

    Create and start a gRPC server to host your service implementation. You can listen on a TCP address or a Unix domain socket. Note that go-plugin assumes connections are local and reliable; do not serve plugins across a network.

    Example using Python:

    # Make the server
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    
    # Add our service
    kv_pb2_grpc.add_KVServicer_to_server(KVServicer(), server)
    
    # Listen on a port
    server.add_insecure_port(':1234')
    
    # Start
    server.start()
  7. Define a gRPC API for plugins using protoc

    main

    To implement a plugin using gRPC, you must first define your service and message structures in a .proto file. This file serves as the contract between the host (client) and the plugin (server).

    syntax = "proto3";
    package proto;
    
    message GreetResponse {
        string message = 1;
    }
    
    message Empty {}
    
    service GreeterService {
        rpc Greet(Empty) returns (GreetResponse);
    }
  8. Implement the HashiCorp plugin system

    main

    To use the go-plugin system, follow these five high-level steps:

    1. Define Interfaces: Choose the Go interfaces you want to expose for plugins.
    2. Implement Client and Server: For each interface, create both a client and a server implementation that communicates via net/rpc or gRPC (or both).
    3. Create a Plugin Implementation: Implement the plugin.Plugin interface, which defines how to create the RPC client/server for your specific plugin type.
    4. Serve the Plugin (Plugin Author): In the plugin's main function, call plugin.Serve to start the RPC server.
    5. Consume the Plugin (Plugin User): Use plugin.Client to launch the plugin subprocess and request the interface implementation over RPC.

    Detailed examples are available in the examples/ directory of the repository.

  9. Implement a handshake for non-Go plugins

    main

    If you are building a plugin in a language other than Go, you must implement a handshake so the go-plugin host can connect to your process. The handshake must be sent as a single line of data to stdout followed by a newline character (\n).

    The handshake format is:

    CORE-PROTOCOL-VERSION|APP-PROTOCOL-VERSION|NETWORK-TYPE|NETWORK-ADDR|PROTOCOL

    Fields:

    • CORE-PROTOCOL-VERSION: Must be 1.
    • APP-PROTOCOL-VERSION: The version of the application data protocol (defined by your specific application).
    • NETWORK-TYPE: Either unix or tcp.
    • NETWORK-ADDR: The path to the Unix socket (for unix) or an IP address (for tcp).
    • PROTOCOL: The communication protocol, such as grpc or netrpc (default for older versions).
    1|3|unix|/path/to/socket|grpc
  10. Dispense and Use a Plugin via RPC

    main

    Once a client is initialized, follow these steps to access the plugin's functionality:

    1. Create an RPC client using client.Client().
    2. Use rpcClient.Dispense(name) to retrieve the plugin instance from the plugin map. This returns a raw interface.
    3. Type-assert the raw interface into your local interface type to call its methods.
    // Connect via RPC
    rpcClient, err := client.Client()
    if err != nil {
    	log.Fatal(err)
    }
    
    // Request the plugin
    raw, err := rpcClient.Dispense("greeter")
    if err != nil {
    	log.Fatal(err)
    }
    
    // Type assert the raw client into your interface
    greeter := raw.(shared.Greeter)
    fmt.Println(greeter.Greet())
  11. Implement a gRPC service for a non-Go plugin

    main

    To write a plugin in a language other than Go, you must implement a standard gRPC server for the protocol buffers service defined by the host application. For example, if the host defines a KV service, your implementation must provide the Get and Put RPC methods as defined in the .proto file.

    class KVServicer(kv_pb2_grpc.KVServicer):
        """Implementation of KV service."""
    
        def Get(self, request, context):
            filename = "kv_" + request.key
            with open(filename, 'r') as f:
                result = kv_pb2.GetResponse()
                result.value = f.read()
                return result
    
        def Put(self, request, context):
            filename = "kv_" + request.key
            value = "{0}\n\nWritten from plugin-python".format(request.value)
            with open(filename, 'w') as f:
                f.write(value)
    
            return kv_pb2.Empty()