mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-22 07:10:41 -08:00
feat(plugin): specify plugin version (#6683)
Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: DmitriyLewen <dmitriy.lewen@smartforce.io>
This commit is contained in:
@@ -130,6 +130,16 @@ The following rules will apply in deciding which platform to select:
|
|||||||
After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache.
|
After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache.
|
||||||
When the plugin is called via Trivy CLI, `bin` command will be executed.
|
When the plugin is called via Trivy CLI, `bin` command will be executed.
|
||||||
|
|
||||||
|
#### Tagging plugin repositories
|
||||||
|
If you are hosting your plugin in a Git repository, it is strongly recommended to tag your releases with a version number.
|
||||||
|
By tagging your releases, Trivy can install specific versions of your plugin.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ trivy plugin install referrer@v0.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
When tagging versions, you must follow [the Semantic Versioning][semver] and prefix the tag with `v`, like `v1.2.3`.
|
||||||
|
|
||||||
#### Plugin arguments/flags
|
#### Plugin arguments/flags
|
||||||
The plugin is responsible for handling flags and arguments.
|
The plugin is responsible for handling flags and arguments.
|
||||||
Any arguments are passed to the plugin from the `trivy` command.
|
Any arguments are passed to the plugin from the `trivy` command.
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ $ trivy plugin install referrer
|
|||||||
|
|
||||||
This command will download the plugin and install it in the plugin cache.
|
This command will download the plugin and install it in the plugin cache.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set.
|
Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set.
|
||||||
Trivy will now search XDG_DATA_HOME for the location of the Trivy plugins cache.
|
Trivy will now search XDG_DATA_HOME for the location of the Trivy plugins cache.
|
||||||
The preference order is as follows:
|
The preference order is as follows:
|
||||||
@@ -56,6 +58,15 @@ $ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl
|
|||||||
$ trivy plugin install myplugin.tar.gz
|
$ trivy plugin install myplugin.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If the plugin's Git repository is [properly tagged](./developer-guide.md#tagging-plugin-repositories), you can specify the version to install like this:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ trivy plugin install referrer@v0.3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
The leading `v` in the version is required. Also, the version must follow the [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
Under the hood Trivy leverages [go-getter][go-getter] to download plugins.
|
Under the hood Trivy leverages [go-getter][go-getter] to download plugins.
|
||||||
This means the following protocols are supported for downloading plugins:
|
This means the following protocols are supported for downloading plugins:
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,19 @@ Install a plugin
|
|||||||
trivy plugin install NAME | URL | FILE_PATH
|
trivy plugin install NAME | URL | FILE_PATH
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```
|
||||||
|
# Install a plugin from the plugin index
|
||||||
|
$ trivy plugin install referrer
|
||||||
|
|
||||||
|
# Specify the version of the plugin to install
|
||||||
|
$ trivy plugin install referrer@v0.3.0
|
||||||
|
|
||||||
|
# Install a plugin from a URL
|
||||||
|
$ trivy plugin install github.com/aquasecurity/trivy-plugin-referrer
|
||||||
|
```
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -727,6 +727,14 @@ func NewPluginCommand() *cobra.Command {
|
|||||||
Use: "install NAME | URL | FILE_PATH",
|
Use: "install NAME | URL | FILE_PATH",
|
||||||
Aliases: []string{"i"},
|
Aliases: []string{"i"},
|
||||||
Short: "Install a plugin",
|
Short: "Install a plugin",
|
||||||
|
Example: ` # Install a plugin from the plugin index
|
||||||
|
$ trivy plugin install referrer
|
||||||
|
|
||||||
|
# Specify the version of the plugin to install
|
||||||
|
$ trivy plugin install referrer@v0.3.0
|
||||||
|
|
||||||
|
# Install a plugin from a URL
|
||||||
|
$ trivy plugin install github.com/aquasecurity/trivy-plugin-referrer`,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
DisableFlagsInUseLine: true,
|
DisableFlagsInUseLine: true,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/walker"
|
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -16,6 +15,7 @@ import (
|
|||||||
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret"
|
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
|
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
|
||||||
"github.com/aquasecurity/trivy/pkg/fanal/cache"
|
"github.com/aquasecurity/trivy/pkg/fanal/cache"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/fanal/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupGitServer() (*httptest.Server, error) {
|
func setupGitServer() (*httptest.Server, error) {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Index struct {
|
|||||||
Version int `yaml:"version"`
|
Version int `yaml:"version"`
|
||||||
Plugins []struct {
|
Plugins []struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
|
Version string `yaml:"version"`
|
||||||
Maintainer string `yaml:"maintainer"`
|
Maintainer string `yaml:"maintainer"`
|
||||||
Summary string `yaml:"summary"`
|
Summary string `yaml:"summary"`
|
||||||
Repository string `yaml:"repository"`
|
Repository string `yaml:"repository"`
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestManager_Search(t *testing.T) {
|
|||||||
want: `NAME DESCRIPTION MAINTAINER OUTPUT
|
want: `NAME DESCRIPTION MAINTAINER OUTPUT
|
||||||
foo A foo plugin aquasecurity ✓
|
foo A foo plugin aquasecurity ✓
|
||||||
bar A bar plugin aquasecurity
|
bar A bar plugin aquasecurity
|
||||||
test A test plugin aquasecurity
|
test_plugin A test plugin aquasecurity
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/go-version/pkg/semver"
|
||||||
"github.com/aquasecurity/trivy/pkg/downloader"
|
"github.com/aquasecurity/trivy/pkg/downloader"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/log"
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
@@ -30,14 +31,20 @@ var (
|
|||||||
type ManagerOption func(indexer *Manager)
|
type ManagerOption func(indexer *Manager)
|
||||||
|
|
||||||
func WithWriter(w io.Writer) ManagerOption {
|
func WithWriter(w io.Writer) ManagerOption {
|
||||||
return func(indexer *Manager) {
|
return func(manager *Manager) {
|
||||||
indexer.w = w
|
manager.w = w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(logger *log.Logger) ManagerOption {
|
||||||
|
return func(manager *Manager) {
|
||||||
|
manager.logger = logger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithIndexURL(indexURL string) ManagerOption {
|
func WithIndexURL(indexURL string) ManagerOption {
|
||||||
return func(indexer *Manager) {
|
return func(manager *Manager) {
|
||||||
indexer.indexURL = indexURL
|
manager.indexURL = indexURL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,17 +95,18 @@ func Update(ctx context.Context) error { return defaultManager(
|
|||||||
func Search(ctx context.Context, keyword string) error { return defaultManager().Search(ctx, keyword) }
|
func Search(ctx context.Context, keyword string) error { return defaultManager().Search(ctx, keyword) }
|
||||||
|
|
||||||
// Install installs a plugin
|
// Install installs a plugin
|
||||||
func (m *Manager) Install(ctx context.Context, name string, opts Options) (Plugin, error) {
|
func (m *Manager) Install(ctx context.Context, arg string, opts Options) (Plugin, error) {
|
||||||
src := m.tryIndex(ctx, name)
|
input := m.parseArg(ctx, arg)
|
||||||
|
input.name = m.tryIndex(ctx, input.name)
|
||||||
|
|
||||||
// If the plugin is already installed, it skips installing the plugin.
|
// If the plugin is already installed, it skips installing the plugin.
|
||||||
if p, installed := m.isInstalled(ctx, src); installed {
|
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))
|
m.logger.InfoContext(ctx, "The plugin is already installed", log.String("name", p.Name))
|
||||||
return p, nil
|
return p, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.logger.InfoContext(ctx, "Installing the plugin...", log.String("src", src))
|
m.logger.InfoContext(ctx, "Installing the plugin...", log.String("src", input.name))
|
||||||
return m.install(ctx, src, opts)
|
return m.install(ctx, input.String(), opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin, error) {
|
func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin, error) {
|
||||||
@@ -129,7 +137,8 @@ func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin
|
|||||||
return Plugin{}, xerrors.Errorf("yaml encode error: %w", err)
|
return Plugin{}, xerrors.Errorf("yaml encode error: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.logger.InfoContext(ctx, "Plugin successfully installed", log.String("name", plugin.Name))
|
m.logger.InfoContext(ctx, "Plugin successfully installed",
|
||||||
|
log.String("name", plugin.Name), log.String("version", plugin.Version))
|
||||||
|
|
||||||
return plugin, nil
|
return plugin, nil
|
||||||
}
|
}
|
||||||
@@ -340,16 +349,45 @@ func (m *Manager) loadMetadata(dir string) (Plugin, error) {
|
|||||||
return plugin, nil
|
return plugin, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) isInstalled(ctx context.Context, url string) (Plugin, bool) {
|
func (m *Manager) isInstalled(ctx context.Context, url, version string) (Plugin, bool) {
|
||||||
installedPlugins, err := m.LoadAll(ctx)
|
installedPlugins, err := m.LoadAll(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Plugin{}, false
|
return Plugin{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, plugin := range installedPlugins {
|
for _, plugin := range installedPlugins {
|
||||||
if plugin.Repository == url {
|
if plugin.Repository == url && (version == "" || plugin.Version == version) {
|
||||||
return plugin, true
|
return plugin, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Plugin{}, false
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
package plugin_test
|
package plugin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"github.com/aquasecurity/trivy/pkg/clock"
|
"github.com/aquasecurity/trivy/pkg/clock"
|
||||||
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
|
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -24,11 +23,40 @@ import (
|
|||||||
"github.com/aquasecurity/trivy/pkg/plugin"
|
"github.com/aquasecurity/trivy/pkg/plugin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func setupInstalledPlugin(t *testing.T, homeDir string, p plugin.Plugin) {
|
||||||
|
pluginDir := filepath.Join(homeDir, ".trivy", "plugins", p.Name)
|
||||||
|
|
||||||
|
// Create the test plugin directory
|
||||||
|
err := os.MkdirAll(pluginDir, os.ModePerm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// write the plugin name
|
||||||
|
pluginMetadata := fmt.Sprintf(`name: "%s"
|
||||||
|
repository: "%s"
|
||||||
|
version: "%s"
|
||||||
|
usage: test
|
||||||
|
description: test
|
||||||
|
platforms:
|
||||||
|
- selector:
|
||||||
|
os: linux
|
||||||
|
arch: amd64
|
||||||
|
uri: ./test.sh
|
||||||
|
bin: ./test.sh
|
||||||
|
installed:
|
||||||
|
platform:
|
||||||
|
os: linux
|
||||||
|
arch: amd64`, p.Name, p.Repository, p.Version)
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestManager_Run(t *testing.T) {
|
func TestManager_Run(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// the test.sh script can't be run on windows so skipping
|
// the test.sh script can't be run on windows so skipping
|
||||||
t.Skip("Test satisfied adequately by Linux tests")
|
t.Skip("Test satisfied adequately by Linux tests")
|
||||||
}
|
}
|
||||||
|
|
||||||
type fields struct {
|
type fields struct {
|
||||||
Name string
|
Name string
|
||||||
Repository string
|
Repository string
|
||||||
@@ -183,110 +211,6 @@ func TestManager_Run(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestManager_Install(t *testing.T) {
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
// the test.sh script can't be run on windows so skipping
|
|
||||||
t.Skip("Test satisfied adequately by Linux tests")
|
|
||||||
}
|
|
||||||
wantPlugin := plugin.Plugin{
|
|
||||||
Name: "test_plugin",
|
|
||||||
Repository: "github.com/aquasecurity/trivy-plugin-test",
|
|
||||||
Version: "0.1.0",
|
|
||||||
Summary: "test",
|
|
||||||
Description: "test",
|
|
||||||
Platforms: []plugin.Platform{
|
|
||||||
{
|
|
||||||
Selector: &plugin.Selector{
|
|
||||||
OS: "linux",
|
|
||||||
Arch: "amd64",
|
|
||||||
},
|
|
||||||
URI: "./test.sh",
|
|
||||||
Bin: "./test.sh",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Installed: plugin.Installed{
|
|
||||||
Platform: plugin.Selector{
|
|
||||||
OS: "linux",
|
|
||||||
Arch: "amd64",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
pluginName string
|
|
||||||
want plugin.Plugin
|
|
||||||
wantFile string
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "http",
|
|
||||||
want: wantPlugin,
|
|
||||||
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "local path",
|
|
||||||
pluginName: "testdata/test_plugin",
|
|
||||||
want: wantPlugin,
|
|
||||||
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "index",
|
|
||||||
pluginName: "test",
|
|
||||||
want: wantPlugin,
|
|
||||||
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "plugin not found",
|
|
||||||
pluginName: "testdata/not_found",
|
|
||||||
wantErr: "no such file or directory",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no plugin.yaml",
|
|
||||||
pluginName: "testdata/no_yaml",
|
|
||||||
wantErr: "file open error",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// The test plugin will be installed here
|
|
||||||
dst := t.TempDir()
|
|
||||||
t.Setenv("XDG_DATA_HOME", dst)
|
|
||||||
|
|
||||||
// For plugin index
|
|
||||||
fsutils.SetCacheDir("testdata")
|
|
||||||
|
|
||||||
if tt.pluginName == "" {
|
|
||||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
zr := zip.NewWriter(w)
|
|
||||||
require.NoError(t, zr.AddFS(os.DirFS("testdata/test_plugin")))
|
|
||||||
require.NoError(t, zr.Close())
|
|
||||||
}))
|
|
||||||
t.Cleanup(ts.Close)
|
|
||||||
tt.pluginName = ts.URL + "/test_plugin.zip"
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := plugin.NewManager().Install(context.Background(), tt.pluginName, plugin.Options{
|
|
||||||
Platform: ftypes.Platform{
|
|
||||||
Platform: &v1.Platform{
|
|
||||||
Architecture: "amd64",
|
|
||||||
OS: "linux",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if tt.wantErr != "" {
|
|
||||||
require.ErrorContains(t, err, tt.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
assert.EqualExportedValues(t, tt.want, got)
|
|
||||||
assert.FileExists(t, filepath.Join(dst, tt.wantFile))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManager_Uninstall(t *testing.T) {
|
func TestManager_Uninstall(t *testing.T) {
|
||||||
ctx := clock.With(context.Background(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
|
ctx := clock.With(context.Background(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
|
||||||
pluginName := "test_plugin"
|
pluginName := "test_plugin"
|
||||||
@@ -423,42 +347,28 @@ func TestManager_Upgrade(t *testing.T) {
|
|||||||
t.Skip("Test satisfied adequately by Linux tests")
|
t.Skip("Test satisfied adequately by Linux tests")
|
||||||
}
|
}
|
||||||
pluginName := "test_plugin"
|
pluginName := "test_plugin"
|
||||||
|
pluginVersion := "0.0.5"
|
||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
pluginDir := filepath.Join(tempDir, ".trivy", "plugins", pluginName)
|
|
||||||
|
|
||||||
t.Setenv("XDG_DATA_HOME", tempDir)
|
t.Setenv("XDG_DATA_HOME", tempDir)
|
||||||
|
setupInstalledPlugin(t, tempDir, plugin.Plugin{
|
||||||
// Create the test plugin directory
|
Name: pluginName,
|
||||||
err := os.MkdirAll(pluginDir, os.ModePerm)
|
Version: pluginVersion,
|
||||||
require.NoError(t, err)
|
Repository: "testdata/test_plugin",
|
||||||
|
})
|
||||||
// write the plugin name
|
|
||||||
pluginMetadata := `name: "test_plugin"
|
|
||||||
repository: testdata/test_plugin
|
|
||||||
version: "0.0.5"
|
|
||||||
usage: test
|
|
||||||
description: A simple test plugin
|
|
||||||
installed:
|
|
||||||
platform:
|
|
||||||
os: linux
|
|
||||||
arch: amd64`
|
|
||||||
|
|
||||||
err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
m := plugin.NewManager()
|
m := plugin.NewManager()
|
||||||
|
|
||||||
// verify initial version
|
// verify initial version
|
||||||
verifyVersion(t, ctx, m, pluginName, "0.0.5")
|
verifyVersion(t, ctx, m, pluginName, pluginVersion)
|
||||||
|
|
||||||
// Upgrade the existing plugin
|
// Upgrade the existing plugin
|
||||||
err = m.Upgrade(ctx, nil)
|
err := m.Upgrade(ctx, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// verify plugin updated
|
// verify plugin updated
|
||||||
verifyVersion(t, ctx, m, pluginName, "0.1.0")
|
verifyVersion(t, ctx, m, pluginName, "0.2.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyVersion(t *testing.T, ctx context.Context, m *plugin.Manager, pluginName, expectedVersion string) {
|
func verifyVersion(t *testing.T, ctx context.Context, m *plugin.Manager, pluginName, expectedVersion string) {
|
||||||
|
|||||||
203
pkg/plugin/manager_unix_test.go
Normal file
203
pkg/plugin/manager_unix_test.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package plugin_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/sosedoff/gitkit" // Not work on Windows
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/aquasecurity/trivy/pkg/clock"
|
||||||
|
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/log"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/plugin"
|
||||||
|
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupGitServer() (*httptest.Server, error) {
|
||||||
|
service := gitkit.New(gitkit.Config{
|
||||||
|
Dir: "./testdata",
|
||||||
|
AutoCreate: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := service.Setup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := httptest.NewServer(service)
|
||||||
|
|
||||||
|
return ts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_Install(t *testing.T) {
|
||||||
|
gs, err := setupGitServer()
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(gs.Close)
|
||||||
|
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
zr := zip.NewWriter(w)
|
||||||
|
require.NoError(t, zr.AddFS(os.DirFS("testdata/test_plugin")))
|
||||||
|
require.NoError(t, zr.Close())
|
||||||
|
}))
|
||||||
|
t.Cleanup(ts.Close)
|
||||||
|
|
||||||
|
wantPlugin := plugin.Plugin{
|
||||||
|
Name: "test_plugin",
|
||||||
|
Repository: "testdata/test_plugin",
|
||||||
|
Version: "0.2.0",
|
||||||
|
Summary: "test",
|
||||||
|
Description: "test",
|
||||||
|
Platforms: []plugin.Platform{
|
||||||
|
{
|
||||||
|
Selector: &plugin.Selector{
|
||||||
|
OS: "linux",
|
||||||
|
Arch: "amd64",
|
||||||
|
},
|
||||||
|
URI: "./test.sh",
|
||||||
|
Bin: "./test.sh",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Installed: plugin.Installed{
|
||||||
|
Platform: plugin.Selector{
|
||||||
|
OS: "linux",
|
||||||
|
Arch: "amd64",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
wantPluginWithVersion := wantPlugin
|
||||||
|
wantPluginWithVersion.Version = "0.1.0"
|
||||||
|
|
||||||
|
wantLogs := `2021-08-25T12:20:30Z INFO Installing the plugin... src="%s"
|
||||||
|
2021-08-25T12:20:30Z INFO Plugin successfully installed name="test_plugin" version="%s"
|
||||||
|
`
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pluginName string
|
||||||
|
installed *plugin.Plugin
|
||||||
|
want plugin.Plugin
|
||||||
|
wantFile string
|
||||||
|
wantLogs string
|
||||||
|
wantErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "http",
|
||||||
|
pluginName: ts.URL + "/test_plugin.zip",
|
||||||
|
want: wantPlugin,
|
||||||
|
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
||||||
|
wantLogs: fmt.Sprintf(wantLogs, ts.URL+"/test_plugin.zip", "0.2.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "local path",
|
||||||
|
pluginName: "testdata/test_plugin",
|
||||||
|
want: wantPlugin,
|
||||||
|
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
||||||
|
wantLogs: fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "git",
|
||||||
|
pluginName: "git::" + gs.URL + "/test_plugin.git",
|
||||||
|
want: wantPlugin,
|
||||||
|
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
||||||
|
wantLogs: fmt.Sprintf(wantLogs, "git::"+gs.URL+"/test_plugin.git", "0.2.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with version",
|
||||||
|
pluginName: "git::" + gs.URL + "/test_plugin.git@v0.1.0",
|
||||||
|
want: wantPluginWithVersion,
|
||||||
|
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
||||||
|
wantLogs: fmt.Sprintf(wantLogs, "git::"+gs.URL+"/test_plugin.git", "0.1.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "via index",
|
||||||
|
pluginName: "test_plugin",
|
||||||
|
want: wantPlugin,
|
||||||
|
wantFile: ".trivy/plugins/test_plugin/test.sh",
|
||||||
|
wantLogs: fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "installed",
|
||||||
|
pluginName: "test_plugin",
|
||||||
|
installed: &plugin.Plugin{
|
||||||
|
Name: "test_plugin",
|
||||||
|
Repository: "testdata/test_plugin",
|
||||||
|
Version: "0.2.0",
|
||||||
|
},
|
||||||
|
want: wantPlugin,
|
||||||
|
wantLogs: "2021-08-25T12:20:30Z INFO The plugin is already installed name=\"test_plugin\"\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different version installed",
|
||||||
|
pluginName: "test_plugin@v0.2.0",
|
||||||
|
installed: &plugin.Plugin{
|
||||||
|
Name: "test_plugin",
|
||||||
|
Repository: "testdata/test_plugin",
|
||||||
|
Version: "0.1.0",
|
||||||
|
},
|
||||||
|
want: wantPlugin,
|
||||||
|
wantLogs: fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "plugin not found",
|
||||||
|
pluginName: "testdata/not_found",
|
||||||
|
wantErr: "no such file or directory",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no plugin.yaml",
|
||||||
|
pluginName: "testdata/no_yaml",
|
||||||
|
wantErr: "file open error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// The test plugin will be installed here
|
||||||
|
dst := t.TempDir()
|
||||||
|
t.Setenv("XDG_DATA_HOME", dst)
|
||||||
|
|
||||||
|
// For plugin index
|
||||||
|
fsutils.SetCacheDir("testdata")
|
||||||
|
|
||||||
|
if tt.installed != nil {
|
||||||
|
setupInstalledPlugin(t, dst, *tt.installed)
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotLogs bytes.Buffer
|
||||||
|
logger := log.New(log.NewHandler(&gotLogs, nil))
|
||||||
|
|
||||||
|
ctx := clock.With(context.Background(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
|
||||||
|
|
||||||
|
got, err := plugin.NewManager(plugin.WithLogger(logger)).Install(ctx, tt.pluginName, plugin.Options{
|
||||||
|
Platform: ftypes.Platform{
|
||||||
|
Platform: &v1.Platform{
|
||||||
|
Architecture: "amd64",
|
||||||
|
OS: "linux",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if tt.wantErr != "" {
|
||||||
|
require.ErrorContains(t, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualExportedValues(t, tt.want, got)
|
||||||
|
if tt.wantFile != "" {
|
||||||
|
assert.FileExists(t, filepath.Join(dst, tt.wantFile))
|
||||||
|
}
|
||||||
|
assert.Equal(t, tt.wantLogs, gotLogs.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
2
pkg/plugin/testdata/plugin/index.yaml
vendored
2
pkg/plugin/testdata/plugin/index.yaml
vendored
@@ -9,7 +9,7 @@ plugins:
|
|||||||
maintainer: aquasecurity
|
maintainer: aquasecurity
|
||||||
summary: A bar plugin
|
summary: A bar plugin
|
||||||
repository: github.com/aquasecurity/trivy-plugin-bar
|
repository: github.com/aquasecurity/trivy-plugin-bar
|
||||||
- name: test
|
- name: test_plugin
|
||||||
maintainer: aquasecurity
|
maintainer: aquasecurity
|
||||||
summary: A test plugin
|
summary: A test plugin
|
||||||
repository: testdata/test_plugin
|
repository: testdata/test_plugin
|
||||||
1
pkg/plugin/testdata/test_plugin.git/HEAD
vendored
Normal file
1
pkg/plugin/testdata/test_plugin.git/HEAD
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ref: refs/heads/main
|
||||||
8
pkg/plugin/testdata/test_plugin.git/config
vendored
Normal file
8
pkg/plugin/testdata/test_plugin.git/config
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[core]
|
||||||
|
repositoryformatversion = 0
|
||||||
|
filemode = true
|
||||||
|
bare = true
|
||||||
|
ignorecase = true
|
||||||
|
precomposeunicode = true
|
||||||
|
[remote "origin"]
|
||||||
|
url = /Users/teppei/src/github.com/aquasecurity/trivy/pkg/plugin/testdata/test_plugin
|
||||||
1
pkg/plugin/testdata/test_plugin.git/description
vendored
Normal file
1
pkg/plugin/testdata/test_plugin.git/description
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Unnamed repository; edit this file 'description' to name the repository.
|
||||||
6
pkg/plugin/testdata/test_plugin.git/info/exclude
vendored
Normal file
6
pkg/plugin/testdata/test_plugin.git/info/exclude
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# git ls-files --others --exclude-from=.git/info/exclude
|
||||||
|
# Lines that start with '#' are comments.
|
||||||
|
# For a project mostly in C, the following would be a good set of
|
||||||
|
# exclude patterns (uncomment them if you want to use them):
|
||||||
|
# *.[oa]
|
||||||
|
# *~
|
||||||
BIN
pkg/plugin/testdata/test_plugin.git/objects/08/6aefb548a1150b765d1e163a5e542fc80bd660
vendored
Normal file
BIN
pkg/plugin/testdata/test_plugin.git/objects/08/6aefb548a1150b765d1e163a5e542fc80bd660
vendored
Normal file
Binary file not shown.
3
pkg/plugin/testdata/test_plugin.git/objects/0a/e1413e3807e024dbc7de4129d12bdcae7dea61
vendored
Normal file
3
pkg/plugin/testdata/test_plugin.git/objects/0a/e1413e3807e024dbc7de4129d12bdcae7dea61
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
xM<><4D>
|
||||||
|
<EFBFBD>0<0C><><EFBFBD>S<EFBFBD><53>[7a}ɺ<>
|
||||||
|
<EFBFBD>#iE<69>(<28><>s<EFBFBD><73><EFBFBD>,>-0<><30>!b C!)<29><><EFBFBD><EFBFBD><EFBFBD>A1<41>$<24>$~h<><0B><><08> <16><><EFBFBD><1B><><EFBFBD>AI
|
||||||
BIN
pkg/plugin/testdata/test_plugin.git/objects/49/0535e047e795a5c95306ce281c9f08cdb35b7c
vendored
Normal file
BIN
pkg/plugin/testdata/test_plugin.git/objects/49/0535e047e795a5c95306ce281c9f08cdb35b7c
vendored
Normal file
Binary file not shown.
BIN
pkg/plugin/testdata/test_plugin.git/objects/92/9b4718db99b64a38b4e8c3ec8e673976821c08
vendored
Normal file
BIN
pkg/plugin/testdata/test_plugin.git/objects/92/9b4718db99b64a38b4e8c3ec8e673976821c08
vendored
Normal file
Binary file not shown.
BIN
pkg/plugin/testdata/test_plugin.git/objects/a0/82cf7b16998b8f048e7d2bf8207d9525688a9f
vendored
Normal file
BIN
pkg/plugin/testdata/test_plugin.git/objects/a0/82cf7b16998b8f048e7d2bf8207d9525688a9f
vendored
Normal file
Binary file not shown.
BIN
pkg/plugin/testdata/test_plugin.git/objects/d7/8abde66b1d35bdac65402f0e2cddf3a96cd377
vendored
Normal file
BIN
pkg/plugin/testdata/test_plugin.git/objects/d7/8abde66b1d35bdac65402f0e2cddf3a96cd377
vendored
Normal file
Binary file not shown.
BIN
pkg/plugin/testdata/test_plugin.git/objects/dc/135ebfc7f680300c981029184a492bbdfa6db3
vendored
Normal file
BIN
pkg/plugin/testdata/test_plugin.git/objects/dc/135ebfc7f680300c981029184a492bbdfa6db3
vendored
Normal file
Binary file not shown.
4
pkg/plugin/testdata/test_plugin.git/packed-refs
vendored
Normal file
4
pkg/plugin/testdata/test_plugin.git/packed-refs
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# pack-refs with: peeled fully-peeled sorted
|
||||||
|
d78abde66b1d35bdac65402f0e2cddf3a96cd377 refs/heads/main
|
||||||
|
929b4718db99b64a38b4e8c3ec8e673976821c08 refs/tags/v0.1.0
|
||||||
|
d78abde66b1d35bdac65402f0e2cddf3a96cd377 refs/tags/v0.2.0
|
||||||
0
pkg/plugin/testdata/test_plugin.git/refs/heads/.gitkeep
vendored
Normal file
0
pkg/plugin/testdata/test_plugin.git/refs/heads/.gitkeep
vendored
Normal file
0
pkg/plugin/testdata/test_plugin.git/refs/tags/.gitkeep
vendored
Normal file
0
pkg/plugin/testdata/test_plugin.git/refs/tags/.gitkeep
vendored
Normal file
7
pkg/plugin/testdata/test_plugin/plugin.yaml
vendored
7
pkg/plugin/testdata/test_plugin/plugin.yaml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: "test_plugin"
|
name: "test_plugin"
|
||||||
repository: github.com/aquasecurity/trivy-plugin-test
|
repository: testdata/test_plugin
|
||||||
version: "0.1.0"
|
version: "0.2.0"
|
||||||
summary: test
|
summary: test
|
||||||
description: test
|
description: test
|
||||||
platforms:
|
platforms:
|
||||||
@@ -9,6 +9,3 @@ platforms:
|
|||||||
arch: amd64
|
arch: amd64
|
||||||
uri: ./test.sh
|
uri: ./test.sh
|
||||||
bin: ./test.sh
|
bin: ./test.sh
|
||||||
# for testing
|
|
||||||
_goos: linux
|
|
||||||
_goarch: amd64
|
|
||||||
Reference in New Issue
Block a user