You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/sdk/plugins/host/load.go

214 lines
6.0 KiB

package external_host_plugins
import (
"bytes"
"compress/gzip"
"context"
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
)
// NOTE: This package could probably use some reflect based bits to allow
// loading other types of plugins later. That is an exercise left for future
// refactoring, but worth calling out now.
type pluginInfo struct {
containerFs fs.FS
filename string
creationFunc func() (pb.HostPluginServiceClient, error)
}
// CreateHostPlugin takes in a type, parses the various options to look for a
// plugin matching that name, and returns a host plugin client, a cleanup
// function to execute on shutdown of the enclosing program, and an error.
func CreateHostPlugin(
ctx context.Context,
pluginType string,
opt ...Option,
) (
hp pb.HostPluginServiceClient,
cleanup func() error,
retErr error,
) {
defer func() {
if retErr != nil && cleanup != nil {
_ = cleanup()
}
}()
pluginType = strings.ToLower(pluginType)
opts, err := getOpts(opt...)
if err != nil {
return nil, nil, fmt.Errorf("error parsing host plugin options: %w", err)
}
if len(opts.withHostPluginsSources) == 0 {
return nil, nil, fmt.Errorf("no host plugins available")
}
// First, scan available plugins, then find the right one to use
pluginMap := map[string]pluginInfo{}
var plugin pluginInfo
{
for _, sourceInfo := range opts.withHostPluginsSources {
switch {
case sourceInfo.pluginFs != nil:
dirs, err := fs.ReadDir(sourceInfo.pluginFs, ".")
if err != nil {
return nil, nil, fmt.Errorf("error scanning host plugins: %w", err)
}
// Store a match between the config type string and the expected plugin name
for _, entry := range dirs {
pluginType := strings.TrimSuffix(strings.TrimPrefix(entry.Name(), sourceInfo.pluginFsPrefix), ".gz")
if runtime.GOOS == "windows" {
pluginType = strings.TrimSuffix(pluginType, ".exe")
}
pluginMap[pluginType] = pluginInfo{
containerFs: sourceInfo.pluginFs,
filename: entry.Name(),
}
}
case sourceInfo.pluginMap != nil:
for k, creationFunc := range sourceInfo.pluginMap {
pluginMap[k] = pluginInfo{creationFunc: creationFunc, filename: k}
}
}
}
plugin = pluginMap[pluginType]
if plugin.filename == "" && plugin.creationFunc == nil {
return nil, nil, fmt.Errorf("unknown host plugin type %q", pluginType)
}
}
// If the source is just a func, execute it and skip ahead; otherwise it's a plugin, so instantiate it
{
switch {
case plugin.creationFunc != nil:
hp, err = plugin.creationFunc()
if err != nil {
return nil, nil, fmt.Errorf("error performing direct instantiation of host plugin with type %q: %w", plugin.filename, err)
}
case plugin.containerFs != nil:
hp, cleanup, err = executeHostPlugin(plugin, opt...)
if err != nil {
return nil, cleanup, err
}
}
}
return hp, cleanup, nil
}
// executeHostPlugin takes in the discovered plugin information and spins out
// the actual binary, returning a client that talks to it.
func executeHostPlugin(plugin pluginInfo, opt ...Option) (pb.HostPluginServiceClient, func() error, error) {
opts, err := getOpts(opt...)
if err != nil {
return nil, nil, fmt.Errorf("error parsing host plugin options: %w", err)
}
// Open and basic validation
file, err := plugin.containerFs.Open(plugin.filename)
if err != nil {
return nil, nil, err
}
stat, err := file.Stat()
if err != nil {
return nil, nil, fmt.Errorf("error discovering host plugin information: %w", err)
}
if stat.IsDir() {
return nil, nil, fmt.Errorf("host plugin is a directory, not a file")
}
// Read in plugin bytes
expLen := stat.Size()
buf := make([]byte, expLen)
readLen, err := file.Read(buf)
if err != nil {
file.Close()
return nil, nil, fmt.Errorf("error reading host plugin bytes: %w", err)
}
if err := file.Close(); err != nil {
return nil, nil, fmt.Errorf("error closing host plugin file: %w", err)
}
if int64(readLen) != expLen {
return nil, nil, fmt.Errorf("reading host plugin expected %d bytes, read %d", expLen, readLen)
}
executedFileName := plugin.filename
// If it's compressed, uncompress it
if strings.HasSuffix(plugin.filename, ".gz") {
executedFileName = strings.TrimSuffix(plugin.filename, ".gz")
gzipReader, err := gzip.NewReader(bytes.NewReader(buf))
if err != nil {
return nil, nil, fmt.Errorf("error creating gzip decompression reader: %w", err)
}
uncompBuf := new(bytes.Buffer)
_, err = uncompBuf.ReadFrom(gzipReader)
gzipReader.Close()
if err != nil {
return nil, nil, fmt.Errorf("error reading gzip compressed data from reader: %w", err)
}
buf = uncompBuf.Bytes()
}
cleanup := func() error {
return nil
}
// Now, create a temp dir and write out the plugin bytes
dir := opts.withHostPluginExecutionDir
if dir == "" {
tmpDir, err := ioutil.TempDir("", "*")
if err != nil {
return nil, nil, fmt.Errorf("error creating tmp dir for kms execution: %w", err)
}
cleanup = func() error {
return os.RemoveAll(tmpDir)
}
dir = tmpDir
}
pluginPath := filepath.Join(dir, executedFileName)
if err := ioutil.WriteFile(pluginPath, buf, fs.FileMode(0700)); err != nil {
return nil, cleanup, fmt.Errorf("error writing out host plugin for execution: %w", err)
}
// Execute the plugin
client, err := NewHostPluginClient(pluginPath, WithLogger(opts.withLogger))
if err != nil {
return nil, cleanup, fmt.Errorf("error fetching host plugin client: %w", err)
}
origCleanup := cleanup
cleanup = func() error {
client.Kill()
return origCleanup()
}
rpcClient, err := client.Client()
if err != nil {
return nil, cleanup, fmt.Errorf("error fetching host plugin rpc client: %w", err)
}
raw, err := rpcClient.Dispense(hostServicePluginSetName)
if err != nil {
return nil, cleanup, fmt.Errorf("error dispensing host plugin: %w", err)
}
var ok bool
hp, ok := raw.(pb.HostPluginServiceClient)
if !ok {
return nil, cleanup, fmt.Errorf("error converting rpc host plugin to host plugin interface: %w", err)
}
return hp, cleanup, nil
}