Skip to content

Instantly share code, notes, and snippets.

@nicolai86
Last active October 11, 2016 17:43
Show Gist options
  • Save nicolai86/0dee36c4250ca1a9430bb7ebec4cfa58 to your computer and use it in GitHub Desktop.
Save nicolai86/0dee36c4250ca1a9430bb7ebec4cfa58 to your computer and use it in GitHub Desktop.
golang plugins /w `net/rpc` & docker

golang plugins /w net/rpc and docker

After attending the dotGo 2016 talk from Brad Rydzewski I wanted to get a working example of plugins in golang via net/rpc with docker for additional isolation. A bit of googling let me to find this go playground detailing how to setup net/rpc via os.Stdin and os.Stdout. I connected the dots so that I can execute a docker image instead.

usage

$ GOOS=linux GOARCH=amd64 go build -o my-plugin plugin.go
$ docker build -t rpc-sqrt .
$ go run main.go
FROM scratch
ADD my-plugin /
ENTRYPOINT ["/my-plugin"]
package main
import (
"context"
"fmt"
"io"
"log"
"net/rpc"
"os"
"os/exec"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
)
func main() {
fmt.Println("Start master")
c, err := NewDockerClient("rpc-sqrt")
if err != nil {
panic(err)
}
var res float64
if err := c.Call("Hook.Sqrt", 3.0, &res); err != nil {
fmt.Printf("rpc error: %q\n", err)
}
fmt.Printf("√%1.0f = %2.4f\n", 3.0, res)
}
func NewDockerClient(imageName string) (*rpc.Client, error) {
cli, err := client.NewEnvClient()
if err != nil {
panic(err)
}
ctx := context.Background()
container, err := cli.ContainerCreate(ctx, &container.Config{
Image: imageName,
OpenStdin: true,
}, &container.HostConfig{
AutoRemove: true,
}, nil, "")
if err != nil {
log.Fatalf(err.Error())
}
resp, err := cli.ContainerAttach(ctx, container.ID, types.ContainerAttachOptions{
Stream: true,
Stdin: true,
Stdout: true,
Stderr: false,
})
if err != nil {
panic(err)
}
if err := cli.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}); err != nil {
log.Fatalf(err.Error())
}
return rpc.NewClient(resp.Conn), nil
}
package main
import (
"io"
"log"
"math"
"net/rpc"
"os"
"os/signal"
"time"
)
func main() {
l := log.New(os.Stderr, "", 0)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for sig := range c {
l.Printf("done: %s\n", sig)
os.Exit(1)
}
}()
NewServerServe(hookRPCWrap{pluginRPC{}})
}
// HookRPC is the RPC interface needed both for the server and the client.
type HookRPC interface {
Sqrt(a float64, b *float64) error
}
// hookRPCWrap is just for filtering out the non-HookRPC functions,
// to keep net/rpc calm (otherwise it may spurt warnings).
type hookRPCWrap struct {
HookRPC
}
type pluginRPC struct{}
func (p pluginRPC) Sqrt(a float64, b *float64) error {
*b = math.Sqrt(a)
return nil
}
// NewServer creates a new server using the process' stdin and stdout.
func NewServerServe(rcvr HookRPC) {
s := rpc.NewServer()
s.RegisterName("Hook", rcvr)
s.ServeConn(struct {
io.Reader
io.Writer
io.Closer
}{os.Stdin, os.Stdout,
multiCloser{[]io.Closer{os.Stdout, os.Stdin, procCloser{}}},
})
}
type multiCloser struct {
closers []io.Closer
}
// Close all the underlying closers, in sequence.
// Return the first error.
func (mc multiCloser) Close() error {
var err error
for _, c := range mc.closers {
if closeErr := c.Close(); closeErr != nil && err == nil {
err = closeErr
}
}
return err
}
type procCloser struct {
*os.Process
}
// Close the underlying process by killing it.
// If there's no such process, then commit suicide.
func (pc procCloser) Close() error {
if pc.Process == nil {
os.Exit(0)
return nil
}
c := make(chan error, 1)
go func() { _, err := pc.Process.Wait(); c <- err }()
if err := pc.Process.Signal(os.Interrupt); err != nil {
return err
}
select {
case err := <-c:
return err
case <-time.After(1 * time.Second):
return pc.Process.Kill()
}
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment