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 all 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
13 changes: 13 additions & 0 deletions docs/docs/references/configuration/cli/trivy_plugin_install.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ Install a plugin
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 [email protected]

# Install a plugin from a URL
$ trivy plugin install github.com/aquasecurity/trivy-plugin-referrer
```

### Options

```
Expand Down
14 changes: 11 additions & 3 deletions pkg/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,9 +724,17 @@ func NewPluginCommand() *cobra.Command {
}
cmd.AddCommand(
&cobra.Command{
Use: "install NAME | URL | FILE_PATH",
Aliases: []string{"i"},
Short: "Install a plugin",
Use: "install NAME | URL | FILE_PATH",
Aliases: []string{"i"},
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 [email protected]
# Install a plugin from a URL
$ trivy plugin install github.com/aquasecurity/trivy-plugin-referrer`,
SilenceErrors: true,
SilenceUsage: true,
DisableFlagsInUseLine: true,
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
1 change: 1 addition & 0 deletions pkg/plugin/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Index struct {
Version int `yaml:"version"`
Plugins []struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Maintainer string `yaml:"maintainer"`
Summary string `yaml:"summary"`
Repository string `yaml:"repository"`
Expand Down
2 changes: 1 addition & 1 deletion pkg/plugin/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func TestManager_Search(t *testing.T) {
want: `NAME DESCRIPTION MAINTAINER OUTPUT
foo A foo plugin aquasecurity ✓
bar A bar plugin aquasecurity
test A test plugin aquasecurity
test_plugin A test plugin aquasecurity
`,
},
{
Expand Down
62 changes: 50 additions & 12 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 All @@ -30,14 +31,20 @@ var (
type ManagerOption func(indexer *Manager)

func WithWriter(w io.Writer) ManagerOption {
return func(indexer *Manager) {
indexer.w = w
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(indexer *Manager) {
indexer.indexURL = indexURL
return func(manager *Manager) {
manager.indexURL = indexURL
}
}

Expand Down Expand Up @@ -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) }

// 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(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, 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))
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 +137,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 @@ -340,16 +349,45 @@ func (m *Manager) loadMetadata(dir string) (Plugin, error) {
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)
if err != nil {
return Plugin{}, false
}

for _, plugin := range installedPlugins {
if plugin.Repository == url {
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,
}
}
Loading