Files
trivy/pkg/plugin/manager.go
Teppei Fukuda d6dc56732b feat(plugin): specify plugin version (#6683)
Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
2024-05-17 09:29:19 +00:00

394 lines
11 KiB
Go

package plugin
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/samber/lo"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"
"github.com/aquasecurity/go-version/pkg/semver"
"github.com/aquasecurity/trivy/pkg/downloader"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)
const configFile = "plugin.yaml"
var (
pluginsRelativeDir = filepath.Join(".trivy", "plugins")
_defaultManager *Manager
)
type ManagerOption func(indexer *Manager)
func WithWriter(w io.Writer) ManagerOption {
return func(manager *Manager) {
manager.w = w
}
}
func WithLogger(logger *log.Logger) ManagerOption {
return func(manager *Manager) {
manager.logger = logger
}
}
func WithIndexURL(indexURL string) ManagerOption {
return func(manager *Manager) {
manager.indexURL = indexURL
}
}
// Manager manages the plugins
type Manager struct {
w io.Writer
indexURL string
logger *log.Logger
pluginRoot string
indexPath string
}
func NewManager(opts ...ManagerOption) *Manager {
m := &Manager{
w: os.Stdout,
indexURL: indexURL,
logger: log.WithPrefix("plugin"),
pluginRoot: filepath.Join(fsutils.HomeDir(), pluginsRelativeDir),
indexPath: filepath.Join(fsutils.CacheDir(), "plugin", "index.yaml"),
}
for _, opt := range opts {
opt(m)
}
return m
}
func defaultManager() *Manager {
if _defaultManager == nil {
_defaultManager = NewManager()
}
return _defaultManager
}
func Install(ctx context.Context, name string, opts Options) (Plugin, error) {
return defaultManager().Install(ctx, name, opts)
}
func Start(ctx context.Context, name string, opts Options) (Wait, error) {
return defaultManager().Start(ctx, name, opts)
}
func RunWithURL(ctx context.Context, name string, opts Options) error {
return defaultManager().RunWithURL(ctx, name, opts)
}
func Upgrade(ctx context.Context, names []string) error { return defaultManager().Upgrade(ctx, names) }
func Uninstall(ctx context.Context, name string) error { return defaultManager().Uninstall(ctx, name) }
func Information(name string) error { return defaultManager().Information(name) }
func List(ctx context.Context) error { return defaultManager().List(ctx) }
func Update(ctx context.Context) error { return defaultManager().Update(ctx) }
func Search(ctx context.Context, keyword string) error { return defaultManager().Search(ctx, keyword) }
// Install installs a plugin
func (m *Manager) Install(ctx context.Context, arg string, opts Options) (Plugin, error) {
input := m.parseArg(ctx, arg)
input.name = m.tryIndex(ctx, input.name)
// If the plugin is already installed, it skips installing the plugin.
if p, installed := m.isInstalled(ctx, input.name, input.version); installed {
m.logger.InfoContext(ctx, "The plugin is already installed", log.String("name", p.Name))
return p, nil
}
m.logger.InfoContext(ctx, "Installing the plugin...", log.String("src", input.name))
return m.install(ctx, input.String(), opts)
}
func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin, error) {
tempDir, err := downloader.DownloadToTempDir(ctx, src)
if err != nil {
return Plugin{}, xerrors.Errorf("download failed: %w", err)
}
defer os.RemoveAll(tempDir)
m.logger.DebugContext(ctx, "Loading the plugin metadata...")
plugin, err := m.loadMetadata(tempDir)
if err != nil {
return Plugin{}, xerrors.Errorf("failed to load the plugin metadata: %w", err)
}
if err = plugin.install(ctx, plugin.Dir(), tempDir, opts); err != nil {
return Plugin{}, xerrors.Errorf("failed to install the plugin: %w", err)
}
// Copy plugin.yaml into the plugin dir
f, err := os.Create(filepath.Join(plugin.Dir(), configFile))
if err != nil {
return Plugin{}, xerrors.Errorf("failed to create plugin.yaml: %w", err)
}
defer f.Close()
if err = yaml.NewEncoder(f).Encode(plugin); err != nil {
return Plugin{}, xerrors.Errorf("yaml encode error: %w", err)
}
m.logger.InfoContext(ctx, "Plugin successfully installed",
log.String("name", plugin.Name), log.String("version", plugin.Version))
return plugin, nil
}
// Uninstall installs the plugin
func (m *Manager) Uninstall(ctx context.Context, name string) error {
pluginDir := filepath.Join(m.pluginRoot, name)
if !fsutils.DirExists(pluginDir) {
m.logger.ErrorContext(ctx, "No such plugin")
return nil
}
if err := os.RemoveAll(pluginDir); err != nil {
return xerrors.Errorf("failed to uninstall the plugin: %w", err)
}
m.logger.InfoContext(ctx, "Plugin successfully uninstalled", log.String("name", name))
return nil
}
// Information gets the information about an installed plugin
func (m *Manager) Information(name string) error {
plugin, err := m.load(name)
if err != nil {
return xerrors.Errorf("plugin load error: %w", err)
}
_, err = fmt.Fprintf(m.w, `
Plugin: %s
Version: %s
Summary: %s
Description: %s
`, plugin.Name, plugin.Version, plugin.Summary, plugin.Description)
return err
}
// List gets a list of all installed plugins
func (m *Manager) List(ctx context.Context) error {
s, err := m.list(ctx)
if err != nil {
return xerrors.Errorf("unable to list plugins: %w", err)
}
_, err = fmt.Fprintf(m.w, "%s\n", s)
return err
}
func (m *Manager) list(ctx context.Context) (string, error) {
if _, err := os.Stat(m.pluginRoot); err != nil {
if os.IsNotExist(err) {
return "No Installed Plugins", nil
}
return "", xerrors.Errorf("stat error: %w", err)
}
plugins, err := m.LoadAll(ctx)
if err != nil {
return "", xerrors.Errorf("unable to load plugins: %w", err)
} else if len(plugins) == 0 {
return "No Installed Plugins", nil
}
pluginList := []string{"Installed Plugins:"}
for _, plugin := range plugins {
pluginList = append(pluginList, fmt.Sprintf(" Name: %s\n Version: %s\n", plugin.Name, plugin.Version))
}
return strings.Join(pluginList, "\n"), nil
}
// Upgrade upgrades an existing plugins
func (m *Manager) Upgrade(ctx context.Context, names []string) error {
if len(names) == 0 {
plugins, err := m.LoadAll(ctx)
if err != nil {
return xerrors.Errorf("unable to load plugins: %w", err)
} else if len(plugins) == 0 {
m.logger.InfoContext(ctx, "No installed plugins")
return nil
}
names = lo.Map(plugins, func(p Plugin, _ int) string { return p.Name })
}
for _, name := range names {
if err := m.upgrade(ctx, name); err != nil {
return xerrors.Errorf("unable to upgrade '%s' plugin: %w", name, err)
}
}
return nil
}
func (m *Manager) upgrade(ctx context.Context, name string) error {
plugin, err := m.load(name)
if err != nil {
return xerrors.Errorf("plugin load error: %w", err)
}
logger := m.logger.With("name", name)
logger.InfoContext(ctx, "Upgrading plugin...")
updated, err := m.install(ctx, plugin.Repository, Options{
// Use the current installed platform
Platform: ftypes.Platform{
Platform: &v1.Platform{
OS: plugin.Installed.Platform.OS,
Architecture: plugin.Installed.Platform.Arch,
},
},
})
if err != nil {
return xerrors.Errorf("unable to perform an upgrade installation: %w", err)
}
if plugin.Version == updated.Version {
logger.InfoContext(ctx, "The plugin is up-to-date", log.String("version", plugin.Version))
} else {
logger.InfoContext(ctx, "Plugin upgraded",
log.String("from", plugin.Version), log.String("to", updated.Version))
}
return nil
}
// LoadAll loads all plugins
func (m *Manager) LoadAll(ctx context.Context) ([]Plugin, error) {
dirs, err := os.ReadDir(m.pluginRoot)
if err != nil {
return nil, xerrors.Errorf("failed to read %s: %w", m.pluginRoot, err)
}
var plugins []Plugin
for _, d := range dirs {
if !d.IsDir() {
continue
}
plugin, err := m.loadMetadata(filepath.Join(m.pluginRoot, d.Name()))
if err != nil {
m.logger.WarnContext(ctx, "Plugin load error", log.Err(err))
continue
}
plugins = append(plugins, plugin)
}
return plugins, nil
}
// Start starts the plugin
func (m *Manager) Start(ctx context.Context, name string, opts Options) (Wait, error) {
plugin, err := m.load(name)
if err != nil {
return nil, xerrors.Errorf("plugin load error: %w", err)
}
wait, err := plugin.Start(ctx, opts)
if err != nil {
return nil, xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
}
return wait, nil
}
// RunWithURL runs the plugin
func (m *Manager) RunWithURL(ctx context.Context, name string, opts Options) error {
plugin, err := m.Install(ctx, name, opts)
if err != nil {
return xerrors.Errorf("plugin install error: %w", err)
}
if err = plugin.Run(ctx, opts); err != nil {
return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err)
}
return nil
}
func (m *Manager) load(name string) (Plugin, error) {
pluginDir := filepath.Join(m.pluginRoot, name)
if _, err := os.Stat(pluginDir); err != nil {
if os.IsNotExist(err) {
return Plugin{}, xerrors.Errorf("could not find a plugin called '%s', did you install it?", name)
}
return Plugin{}, xerrors.Errorf("plugin stat error: %w", err)
}
plugin, err := m.loadMetadata(pluginDir)
if err != nil {
return Plugin{}, xerrors.Errorf("unable to load plugin metadata: %w", err)
}
return plugin, nil
}
func (m *Manager) loadMetadata(dir string) (Plugin, error) {
filePath := filepath.Join(dir, configFile)
f, err := os.Open(filePath)
if err != nil {
return Plugin{}, xerrors.Errorf("file open error: %w", err)
}
defer f.Close()
var plugin Plugin
if err = yaml.NewDecoder(f).Decode(&plugin); err != nil {
return Plugin{}, xerrors.Errorf("yaml decode error: %w", err)
}
if plugin.Name == "" {
return Plugin{}, xerrors.Errorf("'name' is empty")
}
// e.g. ~/.trivy/plugins/kubectl
plugin.dir = filepath.Join(m.pluginRoot, plugin.Name)
if plugin.Summary == "" && plugin.Usage != "" {
plugin.Summary = plugin.Usage // For backward compatibility
plugin.Usage = ""
}
return plugin, nil
}
func (m *Manager) isInstalled(ctx context.Context, url, version string) (Plugin, bool) {
installedPlugins, err := m.LoadAll(ctx)
if err != nil {
return Plugin{}, false
}
for _, plugin := range installedPlugins {
if plugin.Repository == url && (version == "" || plugin.Version == version) {
return plugin, true
}
}
return Plugin{}, false
}
// Input represents the user-specified Input.
type Input struct {
name string
version string
}
func (i *Input) String() string {
if i.version != "" {
// cf. https://github.com/hashicorp/go-getter/blob/268c11cae8cf0d9374783e06572679796abe9ce9/README.md#git-git
return i.name + "?ref=v" + i.version
}
return i.name
}
func (m *Manager) parseArg(ctx context.Context, arg string) Input {
before, after, found := strings.Cut(arg, "@v")
if !found {
return Input{name: arg}
} else if _, err := semver.Parse(after); err != nil {
m.logger.DebugContext(ctx, "Unable to identify the plugin version", log.String("name", arg), log.Err(err))
return Input{name: arg}
}
// cf. https://github.com/hashicorp/go-getter/blob/268c11cae8cf0d9374783e06572679796abe9ce9/README.md#git-git
return Input{
name: before,
version: after,
}
}