blob: 46271cae8339ad3203041091b713bc28e0d77400 [file] [log] [blame]
/*
Copyright © 2021 Aspect Build Systems Inc
Not licensed for re-use.
*/
package system
import (
"fmt"
"os/exec"
hclog "github.com/hashicorp/go-hclog"
goplugin "github.com/hashicorp/go-plugin"
"aspect.build/cli/pkg/ioutils"
"aspect.build/cli/pkg/plugin/sdk/v1alpha1/config"
"aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin"
)
// PluginSystem is the interface that defines all the methods for the aspect CLI
// plugin system intended to be used by the Core.
type PluginSystem interface {
Configure(streams ioutils.Streams) error
PluginList() *PluginList
TearDown()
}
type pluginSystem struct {
finder Finder
parser Parser
clientFactory ClientFactory
clients []ClientProvider
plugins *PluginList
}
// NewPluginSystem instantiates a default internal implementation of the
// PluginSystem interface.
func NewPluginSystem() PluginSystem {
return &pluginSystem{
finder: NewFinder(),
parser: NewParser(),
clientFactory: &clientFactory{},
plugins: &PluginList{},
}
}
// Configure configures the plugin system.
func (ps *pluginSystem) Configure(streams ioutils.Streams) error {
aspectpluginsPath, err := ps.finder.Find()
if err != nil {
return fmt.Errorf("failed to configure plugin system: %w", err)
}
aspectplugins, err := ps.parser.Parse(aspectpluginsPath)
if err != nil {
return fmt.Errorf("failed to configure plugin system: %w", err)
}
ps.clients = make([]ClientProvider, 0, len(aspectplugins))
for _, aspectplugin := range aspectplugins {
logLevel := hclog.LevelFromString(aspectplugin.LogLevel)
if logLevel == hclog.NoLevel {
logLevel = hclog.Error
}
pluginLogger := hclog.New(&hclog.LoggerOptions{
Name: aspectplugin.Name,
Level: logLevel,
})
// TODO(f0rmiga): make this loop concurrent so that all plugins are
// configured faster.
clientConfig := &goplugin.ClientConfig{
HandshakeConfig: config.Handshake,
Plugins: config.PluginMap,
Cmd: exec.Command(aspectplugin.From),
AllowedProtocols: []goplugin.Protocol{goplugin.ProtocolGRPC},
SyncStdout: streams.Stdout,
SyncStderr: streams.Stderr,
Logger: pluginLogger,
}
client := ps.clientFactory.New(clientConfig)
ps.clients = append(ps.clients, client)
rpcClient, err := client.Client()
if err != nil {
return fmt.Errorf("failed to configure plugin system: %w", err)
}
rawplugin, err := rpcClient.Dispense(config.DefaultPluginName)
if err != nil {
return fmt.Errorf("failed to configure plugin system: %w", err)
}
aspectplugin := rawplugin.(plugin.Plugin)
ps.plugins.insert(aspectplugin)
}
return nil
}
// TearDown tears down the plugin system, making all the necessary actions to
// clean up the system.
func (ps *pluginSystem) TearDown() {
for _, client := range ps.clients {
client.Kill()
}
}
// PluginList returns the list of configures plugins.
func (ps *pluginSystem) PluginList() *PluginList {
return ps.plugins
}
// ClientFactory hides the call to goplugin.NewClient.
type ClientFactory interface {
New(*goplugin.ClientConfig) ClientProvider
}
type clientFactory struct{}
// New calls the goplugin.NewClient with the given config.
func (*clientFactory) New(config *goplugin.ClientConfig) ClientProvider {
return goplugin.NewClient(config)
}
// ClientProvider is an interface for goplugin.Client returned by
// goplugin.NewClient.
type ClientProvider interface {
Client() (goplugin.ClientProtocol, error)
Kill()
}
// PluginList implements a simple linked list for the parsed plugins from the
// plugins file.
type PluginList struct {
Head *PluginNode
tail *PluginNode
}
func (l *PluginList) insert(p plugin.Plugin) {
node := &PluginNode{Plugin: p}
if l.Head == nil {
l.Head = node
} else {
l.tail.Next = node
}
l.tail = node
}
// PluginNode is a node in the PluginList linked list.
type PluginNode struct {
Next *PluginNode
Plugin plugin.Plugin
}