migrate backup actions to plugins

Signed-off-by: Steve Kriss <steve@heptio.com>
This commit is contained in:
Steve Kriss
2017-11-21 10:03:03 -08:00
parent 2ce15de2f8
commit 0f2d1ab82b
27 changed files with 1817 additions and 678 deletions
+216 -69
View File
@@ -18,15 +18,17 @@ package plugin
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/heptio/ark/pkg/backup"
"github.com/heptio/ark/pkg/cloudprovider"
)
@@ -38,6 +40,13 @@ func (k PluginKind) String() string {
return string(k)
}
func baseConfig() *plugin.ClientConfig {
return &plugin.ClientConfig{
HandshakeConfig: Handshake,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
}
}
const (
// PluginKindObjectStore is the Kind string for
// an Object Store plugin.
@@ -47,12 +56,36 @@ const (
// a Block Store plugin.
PluginKindBlockStore PluginKind = "blockstore"
// PluginKindCloudProvider is the Kind string for
// a CloudProvider plugin (i.e. an Object & Block
// store).
//
// NOTE that it is highly likely that in subsequent
// versions of Ark this kind of plugin will be replaced
// with a different mechanism for providing multiple
// plugin impls within a single binary. This should
// probably not be used.
PluginKindCloudProvider PluginKind = "cloudprovider"
// PluginKindBackupItemAction is the Kind string for
// a Backup ItemAction plugin.
PluginKindBackupItemAction PluginKind = "backupitemaction"
pluginDir = "/plugins"
)
var AllPluginKinds = []PluginKind{
PluginKindObjectStore,
PluginKindBlockStore,
PluginKindCloudProvider,
PluginKindBackupItemAction,
}
type pluginInfo struct {
kind PluginKind
name string
kinds []PluginKind
name string
commandName string
commandArgs []string
}
// Manager exposes functions for getting implementations of the pluggable
@@ -65,86 +98,58 @@ type Manager interface {
// GetBlockStore returns the plugin implementation of the
// cloudprovider.BlockStore interface with the specified name.
GetBlockStore(name string) (cloudprovider.BlockStore, error)
// GetBackupItemActions returns all backup.ItemAction plugins.
// These plugin instances should ONLY be used for a single backup
// (mainly because each one outputs to a per-backup log),
// and should be terminated upon completion of the backup with
// CloseBackupItemActions().
GetBackupItemActions(backupName string, logger logrus.FieldLogger, level logrus.Level) ([]backup.ItemAction, error)
// CloseBackupItemActions terminates the plugin sub-processes that
// are hosting BackupItemAction plugins for the given backup name.
CloseBackupItemActions(backupName string) error
}
type manager struct {
logger hclog.Logger
clients map[pluginInfo]*plugin.Client
internalPlugins map[pluginInfo]interface{}
logger hclog.Logger
pluginRegistry *registry
clientStore *clientStore
}
// NewManager constructs a manager for getting plugin implementations.
func NewManager(logger logrus.FieldLogger, level logrus.Level) Manager {
return &manager{
logger: (&logrusAdapter{impl: logger, level: level}),
clients: make(map[pluginInfo]*plugin.Client),
internalPlugins: map[pluginInfo]interface{}{
{kind: PluginKindObjectStore, name: "aws"}: struct{}{},
{kind: PluginKindBlockStore, name: "aws"}: struct{}{},
func NewManager(logger logrus.FieldLogger, level logrus.Level) (Manager, error) {
m := &manager{
logger: &logrusAdapter{impl: logger, level: level},
pluginRegistry: newRegistry(),
clientStore: newClientStore(),
}
{kind: PluginKindObjectStore, name: "gcp"}: struct{}{},
{kind: PluginKindBlockStore, name: "gcp"}: struct{}{},
if err := m.registerPlugins(); err != nil {
return nil, err
}
{kind: PluginKindObjectStore, name: "azure"}: struct{}{},
{kind: PluginKindBlockStore, name: "azure"}: struct{}{},
},
return m, nil
}
func pluginForKind(kind PluginKind) plugin.Plugin {
switch kind {
case PluginKindObjectStore:
return &ObjectStorePlugin{}
case PluginKindBlockStore:
return &BlockStorePlugin{}
default:
return nil
}
}
func addPlugins(config *plugin.ClientConfig, kinds ...PluginKind) {
for _, kind := range kinds {
if kind == PluginKindObjectStore {
config.Plugins[kind.String()] = &ObjectStorePlugin{}
} else if kind == PluginKindBlockStore {
config.Plugins[kind.String()] = &BlockStorePlugin{}
}
}
}
func (m *manager) getPlugin(descriptor pluginInfo, logger hclog.Logger) (interface{}, error) {
client, found := m.clients[descriptor]
if !found {
var (
externalPath = filepath.Join(pluginDir, fmt.Sprintf("ark-%s-%s", descriptor.kind, descriptor.name))
config = &plugin.ClientConfig{
HandshakeConfig: Handshake,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Plugins: make(map[string]plugin.Plugin),
Logger: logger,
}
)
// First check to see if there's an external plugin for this kind and name. this
// is so users can override the built-in plugins if they want. If it doesn't exist,
// see if there's an internal one.
if _, err := os.Stat(externalPath); err == nil {
addPlugins(config, descriptor.kind)
config.Cmd = exec.Command(externalPath)
client = plugin.NewClient(config)
m.clients[descriptor] = client
} else if _, found := m.internalPlugins[descriptor]; found {
addPlugins(config, PluginKindObjectStore, PluginKindBlockStore)
config.Cmd = exec.Command("/ark", "plugin", "cloudprovider", descriptor.name)
client = plugin.NewClient(config)
// since a single sub-process will serve both an object and block store
// for a given cloud-provider, record this client as being valid for both
m.clients[pluginInfo{PluginKindObjectStore, descriptor.name}] = client
m.clients[pluginInfo{PluginKindBlockStore, descriptor.name}] = client
} else {
return nil, errors.Errorf("plugin not found for kind=%s, name=%s", descriptor.kind, descriptor.name)
}
}
func getPluginInstance(client *plugin.Client, kind PluginKind) (interface{}, error) {
protocolClient, err := client.Client()
if err != nil {
return nil, errors.WithStack(err)
}
plugin, err := protocolClient.Dispense(descriptor.kind.String())
plugin, err := protocolClient.Dispense(string(kind))
if err != nil {
return nil, errors.WithStack(err)
}
@@ -152,10 +157,56 @@ func (m *manager) getPlugin(descriptor pluginInfo, logger hclog.Logger) (interfa
return plugin, nil
}
func (m *manager) registerPlugins() error {
// first, register internal plugins
for _, provider := range []string{"aws", "gcp", "azure"} {
m.pluginRegistry.register(provider, "/ark", []string{"plugin", "cloudprovider", provider}, PluginKindObjectStore, PluginKindBlockStore)
}
m.pluginRegistry.register("backup_pv", "/ark", []string{"plugin", string(PluginKindBackupItemAction), "backup_pv"}, PluginKindBackupItemAction)
// second, register external plugins (these will override internal plugins, if applicable)
if _, err := os.Stat(pluginDir); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
files, err := ioutil.ReadDir(pluginDir)
if err != nil {
return err
}
for _, file := range files {
name, kind, err := parse(file.Name())
if err != nil {
continue
}
if kind == PluginKindCloudProvider {
m.pluginRegistry.register(name, filepath.Join(pluginDir, file.Name()), nil, PluginKindObjectStore, PluginKindBlockStore)
} else {
m.pluginRegistry.register(name, filepath.Join(pluginDir, file.Name()), nil, kind)
}
}
return nil
}
func parse(filename string) (string, PluginKind, error) {
for _, kind := range AllPluginKinds {
if prefix := fmt.Sprintf("ark-%s-", kind); strings.Index(filename, prefix) == 0 {
return strings.Replace(filename, prefix, "", -1), kind, nil
}
}
return "", "", errors.New("invalid file name")
}
// GetObjectStore returns the plugin implementation of the cloudprovider.ObjectStore
// interface with the specified name.
func (m *manager) GetObjectStore(name string) (cloudprovider.ObjectStore, error) {
pluginObj, err := m.getPlugin(pluginInfo{PluginKindObjectStore, name}, m.logger)
pluginObj, err := m.getCloudProviderPlugin(name, PluginKindObjectStore)
if err != nil {
return nil, err
}
@@ -171,7 +222,7 @@ func (m *manager) GetObjectStore(name string) (cloudprovider.ObjectStore, error)
// GetBlockStore returns the plugin implementation of the cloudprovider.BlockStore
// interface with the specified name.
func (m *manager) GetBlockStore(name string) (cloudprovider.BlockStore, error) {
pluginObj, err := m.getPlugin(pluginInfo{PluginKindBlockStore, name}, m.logger)
pluginObj, err := m.getCloudProviderPlugin(name, PluginKindBlockStore)
if err != nil {
return nil, err
}
@@ -183,3 +234,99 @@ func (m *manager) GetBlockStore(name string) (cloudprovider.BlockStore, error) {
return blockStore, nil
}
func (m *manager) getCloudProviderPlugin(name string, kind PluginKind) (interface{}, error) {
client, err := m.clientStore.get(kind, name, "")
if err != nil {
pluginInfo, err := m.pluginRegistry.get(kind, name)
if err != nil {
return nil, err
}
// build a plugin client that can dispense all of the PluginKinds it's registered for
clientBuilder := newClientBuilder(baseConfig()).
withCommand(pluginInfo.commandName, pluginInfo.commandArgs...)
for _, kind := range pluginInfo.kinds {
clientBuilder.withPlugin(kind, pluginForKind(kind))
}
client = clientBuilder.client()
// register the plugin client for the appropriate kinds
for _, kind := range pluginInfo.kinds {
m.clientStore.add(client, kind, name, "")
}
}
pluginObj, err := getPluginInstance(client, kind)
if err != nil {
return nil, err
}
return pluginObj, nil
}
// GetBackupActions returns all backup.BackupAction plugins.
// These plugin instances should ONLY be used for a single backup
// (mainly because each one outputs to a per-backup log),
// and should be terminated upon completion of the backup with
// CloseBackupActions().
func (m *manager) GetBackupItemActions(backupName string, logger logrus.FieldLogger, level logrus.Level) ([]backup.ItemAction, error) {
clients, err := m.clientStore.list(PluginKindBackupItemAction, backupName)
if err != nil {
pluginInfo, err := m.pluginRegistry.list(PluginKindBackupItemAction)
if err != nil {
return nil, err
}
// create clients for each, using the provided logger
log := &logrusAdapter{impl: logger, level: level}
for _, plugin := range pluginInfo {
client := newClientBuilder(baseConfig()).
withCommand(plugin.commandName, plugin.commandArgs...).
withPlugin(PluginKindBackupItemAction, &BackupItemActionPlugin{log: log}).
withLogger(log).
client()
m.clientStore.add(client, PluginKindBackupItemAction, plugin.name, backupName)
clients = append(clients, client)
}
}
var backupActions []backup.ItemAction
for _, client := range clients {
plugin, err := getPluginInstance(client, PluginKindBackupItemAction)
if err != nil {
return nil, err
}
backupAction, ok := plugin.(backup.ItemAction)
if !ok {
return nil, errors.New("could not convert gRPC client to backup.BackupAction")
}
backupActions = append(backupActions, backupAction)
}
return backupActions, nil
}
// CloseBackupItemActions terminates the plugin sub-processes that
// are hosting BackupItemAction plugins for the given backup name.
func (m *manager) CloseBackupItemActions(backupName string) error {
clients, err := m.clientStore.list(PluginKindBackupItemAction, backupName)
if err != nil {
return err
}
for _, client := range clients {
client.Kill()
}
m.clientStore.deleteAll(PluginKindBackupItemAction, backupName)
return nil
}