pluginrpc

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 28, 2024 License: Apache-2.0 Imports: 22 Imported by: 5

README

pluginrpc-go

Build Report Card GoDoc Slack

The Golang library for PluginRPC.

The pluginrpc.com/pluginrpc library provides all the primitives necessary to operate with the PluginRPC ecosystem. The protoc-gen-pluginrpc-go plugin generates stubs for Protobuf services to work with PluginRPC. It makes authoring and consuming plugins based on Protobuf services incredibly simple.

For more on the motivation behind PluginRPC, see the github.com/pluginrpc/pluginrpc documentation.

For a full example, see the internal/example directory. This contains:

  • proto/pluginrpc/example/v1: An Protobuf package that contains an example Protobuf service EchoService.
  • gen/pluginrpc/example/v1: The generated code from protoc-gen-go and protoc-gen-pluginrpc-go for the example Protobuf Package.
  • echo-plugin: An implementation of a PluginRPC plugin for EchoService.
  • echo-request-client: A simple client that calls the EchoRequest RPC via invoking echo-plugin.
  • echo-list-client: A simple client that calls the EchoList RPC via invoking echo-plugin.
  • echo-error-client: A simple client that calls the EchoError RPC via invoking echo-plugin.

Usage

Install the protoc-gen-go and protoc-gen-pluginrpc-go plugins:

$ go install \
    google.golang.org/protobuf/cmd/protoc-gen-go@latest \
    pluginrpc.com/pluginrpc/cmd/protoc-gen-pluginrpc-go@latest

Generate stubs. The easiest way to do so is by using buf. See Buf's [generation tutorial] for more details on setting up generation. You'll likely need a buf.gen.yaml file that looks approximately like the following:

version: v2
inputs:
  # Or wherever your .proto files live
  - directory: proto
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      # Replace github.com/acme/foo with the name of your Golang module
      value: github.com/acme/foo/gen
plugins:
  - local: protoc-gen-go
    out: gen
    opt: paths=source_relative
  - local: protoc-gen-pluginrpc-go
    out: gen
    opt: paths=source_relative

Build your plugin. See echo-plugin for a full example. Assuming you intend to expose the EchoService as a plugin, your code will look something like this:

func main() {
	pluginrpc.Main(newServer)
}

func newServer() (pluginrpc.Server, error) {
	spec, err := examplev1pluginrpc.EchoServiceSpecBuilder{
		// Note that EchoList does not have optional args and will default to path being the only arg.
		//
		// This means that the following commands will invoke their respective procedures:
		//
		//   echo-plugin echo request
		//   echo-plugin /pluginrpc.example.v1.EchoService/EchoList
		//   echo-plugin echo error
		EchoRequest: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "request")},
		EchoError:   []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "error")},
	}.Build()
	if err != nil {
		return nil, err
	}
	serverRegistrar := pluginrpc.NewServerRegistrar()
	echoServiceServer := examplev1pluginrpc.NewEchoServiceServer(pluginrpc.NewHandler(spec), echoServiceHandler{})
	examplev1pluginrpc.RegisterEchoServiceServer(serverRegistrar, echoServiceServer)
	return pluginrpc.NewServer(spec, serverRegistrar)
}

type echoServiceHandler struct{}

func (echoServiceHandler) EchoRequest(_ context.Context, request *examplev1.EchoRequestRequest) (*examplev1.EchoRequestResponse, error) {
    ...
}

func (echoServiceHandler) EchoList(context.Context, *examplev1.EchoListRequest) (*examplev1.EchoListResponse, error) {
    ...
}

func (echoServiceHandler) EchoError(_ context.Context, request *examplev1.EchoErrorRequest) (*examplev1.EchoErrorResponse, error) {
    ...
}

Invoke your plugin. You'll create a client that points to your plugin. See echo-request-client for a full example. Invocation will look something like this:

client := pluginrpc.NewClient(pluginrpc.NewExecRunner("echo-plugin"))
echoServiceClient, err := examplev1pluginrpc.NewEchoServiceClient(client)
if err != nil {
    return err
}
response, err := echoServiceClient.EchoRequest(
    context.Background(),
    &examplev1.EchoRequestRequest{
        ...
    },
)

See pluginrpc_test.go for an example of how to test plugins.

Plugin Options

The protoc-gen-pluginrpc-go has an option streaming that specifies how to handle streaming RPCs. PluginRPC does not support streaming methods. There are three valid values for streaming: error, warn, ignore. The default is warn:

  • streaming=error: The plugin will error if a streaming method is encountered.
  • streaming=warn: The plugin will produce a warning to stderr if a streaming method is encountered.
  • streaming=ignore: The plugin will ignore streaming methods and not produce a warning.

In the case of warn or ignore, streaming RPCs will be skipped and no functions will be generated for them. If a service only has streaming RPCs, no interfaces will be generated for this service. If a file only has services with only streaming RPCs, no file will be generated.

Additionally, `protoc-gen-pluginrpc-go has all the standard Go plugin options:

  • module=<module>
  • paths={import,source_relative}
  • annotate_code={true,false}
  • M<file>=<package>

Status: Beta

This framework is in active development, and should not be considered stable.

Offered under the Apache 2 license.

Documentation

Overview

Package pluginrpc implements an RPC framework for plugins.

Index

Constants

View Source
const (
	// ProtocolFlagName is the name of the protocol bool flag.
	ProtocolFlagName = "protocol"
	// SpecFlagName is the name of the spec bool flag.
	SpecFlagName = "spec"
	// FormatFlagName is the name of the format string flag.
	FormatFlagName = "format"
)
View Source
const (
	// Version is the semantic version of the pluginrpc module.
	Version = "0.5.0"

	// IsAtLeastVersion0_1_0 is used in compile-time handshake's with pluginrpc's generated code.
	IsAtLeastVersion0_1_0 = true
	// IsAtLeastVersion0_4_0 is used in compile-time handshake's with pluginrpc's generated code.
	IsAtLeastVersion0_4_0 = true
)

Variables

View Source
var (
	// AllFormats are all Formsts.
	AllFormats = []Format{
		FormatJSON,
		FormatBinary,
	}
)
View Source
var OSEnv = Env{
	Args:   os.Args[1:],
	Stdin:  os.Stdin,
	Stdout: os.Stdout,
	Stderr: os.Stderr,
}

OSEnv is an Env using os.Args, os.Stdin, os.Stdout, and os.Stderr.

Functions

func Main

func Main(newServer func() (Server, error), _ ...MainOption)

Main is a convenience function that will run the server within a main function with the proper semantics.

All registration should already be complete before passing the Server to this function.

func main() {
	pluginrpc.Main(newServer)
}

func newServer() (pluginrpc.Server, error) {
	spec, err := examplev1pluginrpc.EchoServiceSpecBuilder{
		EchoRequest: []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "request")},
		EchoError:   []pluginrpc.ProcedureOption{pluginrpc.ProcedureWithArgs("echo", "error")},
	}.Build()
	if err != nil {
		return nil, err
	}
	serverRegistrar := pluginrpc.NewServerRegistrar()
	echoServiceServer := examplev1pluginrpc.NewEchoServiceServer(pluginrpc.NewHandler(spec), echoServiceHandler{})
	examplev1pluginrpc.RegisterEchoServiceServer(serverRegistrar, echoServiceServer)
	return pluginrpc.NewServer(spec, serverRegistrar)
}

func NewProtoProcedure

func NewProtoProcedure(procedure Procedure) *pluginrpcv1.Procedure

NewProtoProcedure returns a new pluginrpcv1.Procedure for the given Procedure.

func NewProtoSpec

func NewProtoSpec(spec Spec) *pluginrpcv1.Spec

NewProtoSpec returns a new pluginrpcv1.Spec for the given Spec.

Types

type CallOption

type CallOption func(*callOptions)

CallOption is an option for an individual client call.

type Client

type Client interface {
	// Spec returns the Spec that the client receives.
	//
	// Clients will cache retrieved protocols and Specs. If it is possible that a plugin will
	// change during the lifetime of a Client, it is the responsibility of the caller to
	// create a new Client. We may change this requirement in the future.
	Spec(ctx context.Context) (Spec, error)
	// Call calls the given Procedure.
	//
	// The request will be sent over stdin, with a response being sent on stdout.
	// The response given will then be populated.
	Call(
		ctx context.Context,
		procedurePath string,
		request any,
		response any,
		options ...CallOption,
	) error
	// contains filtered or unexported methods
}

Client is a client that calls plugins.

Typically, Clients are not directly invoked. Instead, the generated code for a given service will use a Client to call the Procedures that the service specifies.

func NewClient

func NewClient(runner Runner, options ...ClientOption) Client

NewClient returns a new Client for the given Runner.

type ClientOption

type ClientOption func(*clientOptions)

ClientOption is an option for a new Client.

func ClientWithFormat

func ClientWithFormat(format Format) ClientOption

ClientWithFormat will result in the given Format being used for requests and responses.

The default is FormatBinary.

func ClientWithStderr

func ClientWithStderr(stderr io.Writer) ClientOption

ClientWithStderr will result in the stderr of the plugin being propagated to the given writer.

The default is to drop stderr.

type Code

type Code uint32

Code is an error code. There are no user-defined codes, so only the codes enumerated below are valid. In both name and semantics, these codes match the gRPC status codes.

const (

	// CodeCanceled indicates that the operation was canceled, typically by the
	// caller.
	CodeCanceled Code = 1

	// CodeUnknown indicates that the operation failed for an unknown reason.
	CodeUnknown Code = 2

	// CodeInvalidArgument indicates that client supplied an invalid argument.
	CodeInvalidArgument Code = 3

	// CodeDeadlineExceeded indicates that deadline expired before the operation
	// could complete.
	CodeDeadlineExceeded Code = 4

	// CodeNotFound indicates that some requested entity (for example, a file or
	// directory) was not found.
	CodeNotFound Code = 5

	// CodeAlreadyExists indicates that client attempted to create an entity (for
	// example, a file or directory) that already exists.
	CodeAlreadyExists Code = 6

	// CodePermissionDenied indicates that the caller doesn't have permission to
	// execute the specified operation.
	CodePermissionDenied Code = 7

	// CodeResourceExhausted indicates that some resource has been exhausted. For
	// example, a per-user quota may be exhausted or the entire file system may
	// be full.
	CodeResourceExhausted Code = 8

	// CodeFailedPrecondition indicates that the system is not in a state
	// required for the operation's execution.
	CodeFailedPrecondition Code = 9

	// CodeAborted indicates that operation was aborted by the system, usually
	// because of a concurrency issue such as a sequencer check failure or
	// transaction abort.
	CodeAborted Code = 10

	// CodeOutOfRange indicates that the operation was attempted past the valid
	// range (for example, seeking past end-of-file).
	CodeOutOfRange Code = 11

	// CodeUnimplemented indicates that the operation isn't implemented,
	// supported, or enabled in this service.
	CodeUnimplemented Code = 12

	// CodeInternal indicates that some invariants expected by the underlying
	// system have been broken. This code is reserved for serious errors.
	CodeInternal Code = 13

	// CodeUnavailable indicates that the service is currently unavailable. This
	// is usually temporary, so clients can back off and retry idempotent
	// operations.
	CodeUnavailable Code = 14

	// CodeDataLoss indicates that the operation has resulted in unrecoverable
	// data loss or corruption.
	CodeDataLoss Code = 15

	// CodeUnauthenticated indicates that the request does not have valid
	// authentication credentials for the operation.
	CodeUnauthenticated Code = 16
)

func CodeForProto

func CodeForProto(protoCode pluginrpcv1.Code) (Code, error)

CodeForProto returns the Code for the pluginrpcv1.Code.

Returns error the pluginrpcv1.Code is not valid.

func (Code) String

func (c Code) String() string

String implements fmt.Stringer.

func (Code) ToProto

func (c Code) ToProto() (pluginrpcv1.Code, error)

ToProto returns the pluginrpcv1.Code for the given Code.

Returns error if the Code is not valid.

type Env

type Env struct {
	Args   []string
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer
}

Env specifies an environment used to invoke a plugin.

This abstracts away args, stdin, stdout, and stderr. Envs are used by Runners and Servers.

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error is an error with a Code.

func NewError

func NewError(code Code, underlying error) *Error

NewError returns a new Error.

Code and underlying with a non-empty message are required.

An Error will never have an invalid Code or nil underlying error when returned from this function.

func NewErrorForProto

func NewErrorForProto(protoError *pluginrpcv1.Error) *Error

NewErrorForProto returns a new Error for the given pluginrpcv1.Error.

If protoError is nil, this returns nil.

func NewErrorf

func NewErrorf(code Code, format string, args ...any) *Error

Code and a non-empty message are required.

An Error will never have an invalid Code or nil underlying error when returned from this function.

func WrapError

func WrapError(err error) *Error

WrapError wraps the given error as a Error.

If the given error is nil, this returns nil. If the given error is already a Error, this is returned. Otherwise, an error with code CodeUnknown is returned.

An Error will never have an invalid Code when returned from this function.

func (*Error) Code

func (e *Error) Code() Code

Code returns the error code.

If e is nil, this returns 0.

func (*Error) Error

func (e *Error) Error() string

Error implements error.

If e is nil, this returns the empty string.

func (*Error) ToProto

func (e *Error) ToProto() *pluginrpcv1.Error

ToProto converts the Error to a pluginrpcv1.Error.

If e is nil, this returns nil.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap implements error.

If e is nil, this returns nil.

type ExecRunnerOption

type ExecRunnerOption func(*execRunnerOptions)

ExecRunnerOption is an option for a new os/exec Runner.

func ExecRunnerWithArgs

func ExecRunnerWithArgs(args ...string) ExecRunnerOption

ExecRunnerWithArgs returns a new ExecRunnerOption that specifies a sub-command to invoke on the program.

For example, if the plugin is implemented under the sub-command `foo bar` on the program `plug`, specifying ExecRunnerWithArgs("foo", "bar") will result in the command `plug foo bar` being invoked as the plugin. In this scenario, all procedures and flag will be implemented under this sub-command. In this example, `plug foo bar --plugin-spec` should produce the spec.

type ExitError

type ExitError struct {
	// contains filtered or unexported fields
}

ExitError is an process exit error with an exit code.

Runners return ExitErrors to indicate the exit code of the process.

func NewExitError

func NewExitError(exitCode int, underlying error) *ExitError

NewExitError returns a new ExitError.

An ExitError will never have an exit code of 0 when returned from this function.

func WrapExitError

func WrapExitError(err error) *ExitError

WrapExitError wraps the given error as a *ExitError.

If the given error is nil, this returns nil. If the given error is already a *ExitError, this is returned.

An ExitError will never have a exit code of 0 when returned from this function.

func (*ExitError) Error

func (e *ExitError) Error() string

Error implements error.

If e is nil, this returns the empty string.

func (*ExitError) ExitCode

func (e *ExitError) ExitCode() int

ExitCode returns the exit code.

If e is nil, this returns 0.

func (*ExitError) Unwrap

func (e *ExitError) Unwrap() error

Unwrap implements error.

If e is nil, this returns nil.

type Format

type Format uint32

Format is the serialization mechanism of the body of Requests, Responses and Specs.

const (
	// FormatBinary is the binary format.
	FormatBinary Format = 1
	// FormatJSON is the JSON format.
	FormatJSON Format = 2
)

func FormatForString

func FormatForString(s string) Format

FormatForString returns the Format for the given string.

Returns 0 if the Format is unknown or s is empty.

func (Format) String

func (f Format) String() string

String implements fmt.Stringer.

type HandleEnv

type HandleEnv struct {
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer
}

HandleEnv is the part of the environment that Handlers can have access to.

type HandleOption

type HandleOption func(*handleOptions)

HandleOption is an option for handler.Handle.

func HandleWithFormat

func HandleWithFormat(format Format) HandleOption

HandleWithFormat returns a new HandleOption that says to marshal and unmarshal requests, responses, and errors in the given format.

The default is FormatBinary.

type Handler

type Handler interface {
	Handle(
		ctx context.Context,
		handleEnv HandleEnv,
		request any,
		handle func(context.Context, any) (any, error),
		options ...HandleOption,
	) error
	// contains filtered or unexported methods
}

Handler handles requests on the server side.

This is used within generated code when registering an implementation of a service.

Currently, Handlers do not have any customization, however this type is exposes so that customization can be provided in the future.

func NewHandler

func NewHandler(spec Spec, _ ...HandlerOption) Handler

NewHandler returns a new Handler.

type HandlerOption

type HandlerOption func(*handlerOptions)

HandlerOption is an option for a new Handler.

type MainOption

type MainOption func(*mainOptions)

MainOption is an option for Main.

type Procedure

type Procedure interface {
	// Path returns the path of the Procedure.
	//
	// Paths are always valid URIs.
	Path() string
	// Args returns optional custom args which can be used to invoke the Procedure.
	//
	// If there are no args, the Procedure can be invoked with the single arg equal to the path.
	// Arg values may only use the characters [a-zA-Z0-9-_], and never start or end with a dash
	// or underscore.
	Args() []string
	// contains filtered or unexported methods
}

Procedure defines a single procedure that a plugin exposes.

func NewProcedure

func NewProcedure(path string, options ...ProcedureOption) (Procedure, error)

NewProcedure returns a new validated Procedure for the given path.

func NewProcedureForProto

func NewProcedureForProto(protoProcedure *pluginrpcv1.Procedure) (Procedure, error)

NewProcedureForProto returns a new validated Procedure for the given pluginrpcv1.Procedure.

type ProcedureOption

type ProcedureOption func(*procedureOptions)

ProcedureOption is an option for a new Procedure.

func ProcedureWithArgs

func ProcedureWithArgs(args ...string) ProcedureOption

ProcedureWithArgs specifies optional custom args which can be used to invoke the Procedure.

If there are no args, the Procedure can be invoked with the single arg equal to the path. Arg values may only use the characters [a-zA-Z0-9-_], and never start with a dash or underscore.

type Runner

type Runner interface {
	// Run runs the external command with the given environment.
	//
	// The environment variables are always cleared before running the command.
	// If no stdin, stdout, or stderr are provided, the equivalent of /dev/null are given to the command.
	// The command is run in the context of the current working directory.
	//
	// If there is an exit error, it is returned as a *ExitError.
	Run(ctx context.Context, env Env) error
}

Runner runs external commands.

Runners should not proxy any environment variables to the commands they run.

func NewExecRunner

func NewExecRunner(programName string, options ...ExecRunnerOption) Runner

NewExecRunner returns a new Runner that uses os/exec to call the given external command given by the program name.

func NewServerRunner

func NewServerRunner(server Server, _ ...ServerRunnerOption) Runner

NewServerRunner returns a new Runner that directly calls the server.

This is primarily used for testing.

type Server

type Server interface {
	// Serve serves the plugin.
	Serve(ctx context.Context, env Env) error
	// contains filtered or unexported methods
}

Server is the server for plugin implementations.

The easiest way to run a server for a plugin is to call ServerMain.

func NewServer

func NewServer(spec Spec, serverRegistrar ServerRegistrar, options ...ServerOption) (Server, error)

NewServer returns a new Server for a given Spec and ServerRegistrar.

The Spec will be validated against the ServerRegistar to make sure there is a 1-1 mapping between Procedures and registered paths.

Once passed to this constructor, the ServerRegistrar can no longer have new paths registered to it.

type ServerOption

type ServerOption func(*serverOptions)

ServerOption is an option for a new Server.

func ServerWithDoc added in v0.5.0

func ServerWithDoc(doc string) ServerOption

ServerWithDoc will attach the given documentation to the server.

This will add ths given docs as a prefix when the flag -h/--help is used.

type ServerRegistrar

type ServerRegistrar interface {
	// Register registers the given handle function for the given path.
	//
	// Paths must be unique.
	Register(path string, handleFunc func(context.Context, HandleEnv, ...HandleOption) error)
	// contains filtered or unexported methods
}

ServerRegistrar is used to registered paths when constructing a server.

By splitting out registration from the Server interface, we allow the Server to be immutable.

Generally, ServerRegistrars are called by `Register.*Server` functions from generated code.

func NewServerRegistrar

func NewServerRegistrar() ServerRegistrar

NewServerRegistrar returns a new ServerRegistrar.

type ServerRunnerOption

type ServerRunnerOption func(*serverRunnerOptions)

ServerRunnerOption is an option for a new ServerRunner.

type Spec

type Spec interface {
	// ProcedureForPath returns the Procedure for the given path.
	//
	// If no such procedure exists, this returns nil.
	ProcedureForPath(path string) Procedure
	// Procedures returns all Procedures.
	Procedures() []Procedure
	// contains filtered or unexported methods
}

Spec specifies a set of Procedures that a plugin implements. This describes the shape of the plugin to clients.

Specs are returned on stdout when `--spec` is called.

A given Spec will have no duplicate Procedures either by path or args.

func MergeSpecs added in v0.4.0

func MergeSpecs(specs ...Spec) (Spec, error)

MergeSpecs merges the given Specs.

Returns error if any Procedures overlap by Path or Args.

func NewSpec

func NewSpec(procedures ...Procedure) (Spec, error)

NewSpec returns a new validated Spec for the given Procedures.

func NewSpecForProto

func NewSpecForProto(protoSpec *pluginrpcv1.Spec) (Spec, error)

NewSpecForProto returns a new validated Spec for the given pluginrpcv1.Spec.

Directories

Path Synopsis
cmd
internal
example/cmd/echo-error-client
Package main implements a client that calls the EchoError RPC on the echo-plugin plugin.
Package main implements a client that calls the EchoError RPC on the echo-plugin plugin.
example/cmd/echo-list-client
Package main implements a client that calls the EchoList RPC on the echo-plugin plugin.
Package main implements a client that calls the EchoList RPC on the echo-plugin plugin.
example/cmd/echo-plugin
Package main implements an example plugin.
Package main implements an example plugin.
example/cmd/echo-request-client
Package main implements a client that calls the EchoRequest RPC on the echo-plugin plugin.
Package main implements a client that calls the EchoRequest RPC on the echo-plugin plugin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL