Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# AGENTS.md - Coding Guidelines for go-plugin

## Project Overview

`github.com/hashicorp/go-plugin` is a Go library for building plugin systems using
process isolation with net/rpc or gRPC transport. Plugins are standalone binaries
that communicate with a host process. Go 1.24, MPL-2.0 license.

## Build / Lint / Test Commands

```bash
# Build all packages
go build ./...

# Run all tests with race detection
go test -race ./...

# Run all tests with verbose output and coverage
go test -race ./... -v -coverprofile=coverage.out

# Run a single test
go test -race -run TestClient -v .
go test -race -run TestServer_testMode -v ./...

# Run tests in a specific package
go test -race -v ./internal/cmdrunner

# Lint
golangci-lint run

# Format check (CI fails if files are changed)
go fmt ./...

# Regenerate protobuf code (requires buf)
buf generate --path test/grpc/test.proto
```

## Code Style

### File Headers

Every `.go` file must start with the copyright header:

```go
// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: MPL-2.0
```

### Package Declarations

- Root package is `plugin`.
- Internal packages live under `internal/` (e.g., `internal/cmdrunner`,
`internal/grpcmux`, `internal/plugin`).
- Public sub-packages: `runner/`, `test/grpc/`.

### Imports

Group imports in three blocks separated by blank lines:
1. Standard library
2. Third-party packages
3. Internal packages (`github.com/hashicorp/go-plugin/...`)

Use named imports sparingly; only when disambiguation is needed:

```go
import (
"context"
"fmt"

hclog "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin/internal/grpcmux"
"google.golang.org/grpc"
)
```

### Naming Conventions

- Exported types and functions use PascalCase: `Client`, `ServeConfig`, `HandshakeConfig`.
- Unexported uses camelCase: `managedClients`, `defaultPluginLogBufferSize`.
- Interfaces define behavior; use `-er` suffix where appropriate (`Runner`).
- Test helpers accept `testing.TB` (not `*testing.T`) to support both tests and benchmarks.
- Prefix test functions with `Test`: `TestClient`, `TestServer_testMode`.
- Use underscores in test names for readability: `TestClient_killStart`.

### Error Handling

- Return errors; do not panic in library code.
- Define sentinel errors as package-level `var` using `errors.New`:
```go
var ErrProcessNotFound = cmdrunner.ErrProcessNotFound
var ErrChecksumsDoNotMatch = errors.New("checksums did not match")
```
- Wrap errors with `fmt.Errorf("context: %w", err)` when adding context.
- Discard cleanup errors with `_ =`: `_ = l.Close()`.

### Types and Interfaces

- Struct fields are documented with comments above each field.
- Use interface compliance checks in test files:
```go
var _ Plugin = (*testInterfacePlugin)(nil)
var _ Plugin = new(NetRPCUnsupportedPlugin)
```
- Prefer embedding for composition: `NetRPCUnsupportedPlugin` is designed for embedding.

### Testing Patterns

- Tests are in `*_test.go` files in the same package (not `_test` package).
- Use `t.Fatal` / `t.Fatalf` for unexpected errors, not `t.Error`.
- Use `t.TempDir()` for temporary directories; clean up with `defer`.
- Use `context.WithCancel` with `defer cancel()` for context-based tests.
- Helper processes are spawned via `helperProcess()` in `plugin_test.go`.

### Logging

- Use `github.com/hashicorp/go-hclog` for all logging.
- Prefer `hclog.New()` with explicit `LoggerOptions` in non-test code.
- Logger is passed via `ClientConfig.Logger` or created with sensible defaults.

### gRPC / Protobuf

- Proto definitions in `test/grpc/test.proto`.
- Generated code uses `buf` with settings in `buf.yaml` / `buf.gen.yaml`.
- Do not edit generated `*.pb.go` files directly.
76 changes: 73 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ created for [Packer](https://www.packer.io), it is additionally in use by
[Boundary](https://www.boundaryproject.io),
and [Waypoint](https://www.waypointproject.io).

While the plugin system is over RPC, it is currently only designed to work
over a local [reliable] network. Plugins over a real network are not supported
and will lead to unexpected behavior.
## Remote Plugin Support

**New!** The plugin system now supports reliable gRPC communication over the
network. Plugins can run on different machines and communicate over TCP/IP
with built-in keepalive for reliable connections.

For local plugins (subprocess-based), see the traditional usage below.
For remote plugins (network-based), see the [Remote Plugin Support](#remote-plugin-support-1)
section.

This plugin system has been used on millions of machines across many different
projects and has proven to be battle hardened and ready for production use.
Expand Down Expand Up @@ -75,6 +81,70 @@ reattach.
checksum and RPC communications can be configured to use TLS. The host process
must be properly secured to protect this configuration.

## Remote Plugin Support

The plugin system now supports reliable gRPC communication over the network.
Unlike the traditional local subprocess-based approach, remote plugins can
run on different machines and communicate over TCP/IP.

### Key Features

- **Network Transport**: Plugins communicate over TCP/IP instead of local sockets
- **Keepalive**: Automatic keepalive pings maintain reliable connections over
potentially unreliable networks
- **Health Checks**: Clients can verify server availability before connecting
- **TLS Support**: Can use TLS for secure connections over untrusted networks
- **Graceful Shutdown**: Server handles signals for clean shutdown

### Server Usage

```go
import (
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"github.com/hashicorp/go-plugin"
)

// Create a TCP listener
lis, _ := net.Listen("tcp", ":50051")

// Create server with keepalive enforcement
grpcServer := grpc.NewServer(
grpc.KeepaliveEnforcementPolicy(
plugin.DefaultGRPCServerKeepaliveEnforcementPolicy(),
),
)

// Register health check
healthCheck := health.NewServer()
healthCheck.SetServingStatus(plugin.GRPCServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(grpcServer, healthCheck)

// Register plugins and serve
grpcServer.Serve(lis)
```

### Client Usage

```go
import "github.com/hashicorp/go-plugin"

// Connect to remote server with keepalive
client, err := plugin.NewGRPCRemoteClient(&plugin.GRPCRemoteClientConfig{
Addr: "localhost:50051",
Plugins: pluginMap,
})
defer client.Close()

// Dispense and use the plugin
raw, _ := client.Dispense("kv")
kv := raw.(MyPlugin)
kv.Put("key", []byte("value"))
```

See the `examples/remote` directory for a complete working example.

## Architecture

The HashiCorp plugin system works by launching subprocesses and communicating
Expand Down
98 changes: 98 additions & 0 deletions examples/remote/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Remote Plugin Example

This example demonstrates how to use go-plugin for remote gRPC plugin
communication over the network. Unlike the local subprocess-based examples,
this shows how to connect to a plugin server that may be running on a
different machine.

## Features

- **Network Transport**: Plugins communicate over TCP/IP instead of local sockets
- **Keepalive**: Automatic keepalive pings maintain reliable connections
- **Health Checks**: Clients can verify server availability before connecting
- **Graceful Shutdown**: Server handles SIGTERM for clean shutdown

## Running the Example

### 1. Start the Server

```bash
cd server
go run . -addr :50051
```

The server will listen on port 50051 and accept connections from anywhere.

### 2. Run the Client

In another terminal:

```bash
cd client

# Put a value
go run . -addr localhost:50051 put mykey "hello world"

# Get the value
go run . -addr localhost:50051 get mykey

# Health check
go run . -addr localhost:50051 -health
```

## Code Structure

- `shared/` - Shared interface and gRPC implementation
- `server/` - Remote plugin server
- `client/` - Remote plugin client

## Key Differences from Local Plugins

1. **No Subprocess Management**: The server runs as a standalone process
2. **Network Addresses**: Uses `host:port` addresses instead of local sockets
3. **Keepalive**: Built-in keepalive for reliable network transport
4. **TLS Support**: Can use TLS for secure connections over untrusted networks

## API Usage

### Server Side

```go
import (
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"github.com/hashicorp/go-plugin"
)

// Create server with keepalive
grpcServer := grpc.NewServer(
grpc.KeepaliveEnforcementPolicy(
plugin.DefaultGRPCServerKeepaliveEnforcementPolicy(),
),
)

// Register health check
healthCheck := health.NewServer()
healthCheck.SetServingStatus(plugin.GRPCServiceName, grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(grpcServer, healthCheck)

// Register plugins and serve
grpcServer.Serve(listener)
```

### Client Side

```go
import "github.com/hashicorp/go-plugin"

// Connect to remote server with keepalive
client, err := plugin.NewGRPCRemoteClient(&plugin.GRPCRemoteClientConfig{
Addr: "localhost:50051",
Plugins: pluginMap,
})
defer client.Close()

// Use the plugin
raw, err := client.Dispense("kv")
kv := raw.(KV)
```
Loading