Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin): specify plugin version #6683

Merged
merged 11 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/docs/plugin/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 [email protected]
```

When tagging versions, you must follow [the Semantic Versioning][semver] and prefix the tag with `v`, like `v1.2.3`.

#### Plugin arguments/flags
The plugin is responsible for handling flags and arguments.
Any arguments are passed to the plugin from the `trivy` command.
Expand Down
11 changes: 11 additions & 0 deletions docs/docs/plugin/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ $ trivy plugin install referrer

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 will now search XDG_DATA_HOME for the location of the Trivy plugins cache.
The preference order is as follows:
Expand All @@ -56,6 +58,15 @@ $ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl
$ 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 [email protected]
```

!!! 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.
This means the following protocols are supported for downloading plugins:

Expand Down
2 changes: 1 addition & 1 deletion pkg/fanal/artifact/repo/git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package repo

import (
"context"
"github.com/aquasecurity/trivy/pkg/fanal/walker"
"net/http/httptest"
"testing"

Expand All @@ -16,6 +15,7 @@ import (
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret"
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
"github.com/aquasecurity/trivy/pkg/fanal/cache"
"github.com/aquasecurity/trivy/pkg/fanal/walker"
)

func setupGitServer() (*httptest.Server, error) {
Expand Down
44 changes: 38 additions & 6 deletions pkg/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"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"
Expand Down Expand Up @@ -88,17 +89,18 @@ func Update(ctx context.Context) error { return defaultManager(
func Search(ctx context.Context, keyword string) error { return defaultManager().Search(ctx, keyword) }

// Install installs a plugin
func (m *Manager) Install(ctx context.Context, name string, opts Options) (Plugin, error) {
src := m.tryIndex(ctx, name)
func (m *Manager) Install(ctx context.Context, arg string, opts Options) (Plugin, error) {
input := m.parseArg(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, src); installed {
if p, installed := m.isInstalled(ctx, input.name); installed {
DmitriyLewen marked this conversation as resolved.
Show resolved Hide resolved
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", src))
return m.install(ctx, src, opts)
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) {
Expand Down Expand Up @@ -129,7 +131,8 @@ func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin
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
}
Expand Down Expand Up @@ -353,3 +356,32 @@ func (m *Manager) isInstalled(ctx context.Context, url string) (Plugin, bool) {
}
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(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.Debug("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,
}
}
111 changes: 3 additions & 108 deletions pkg/plugin/manager_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
//go:build unix

package plugin_test

import (
"archive/zip"
"bytes"
"context"
"github.com/aquasecurity/trivy/pkg/clock"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
v1 "github.com/google/go-containerregistry/pkg/v1"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
Expand All @@ -29,6 +27,7 @@ func TestManager_Run(t *testing.T) {
// the test.sh script can't be run on windows so skipping
t.Skip("Test satisfied adequately by Linux tests")
}

type fields struct {
Name string
Repository string
Expand Down Expand Up @@ -183,110 +182,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) {
ctx := clock.With(context.Background(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC))
pluginName := "test_plugin"
Expand Down
Loading