Skip to content

Commit

Permalink
feat(plugin): specify plugin version (#6683)
Browse files Browse the repository at this point in the history
Signed-off-by: knqyf263 <[email protected]>
Co-authored-by: DmitriyLewen <[email protected]>
  • Loading branch information
knqyf263 and DmitriyLewen authored May 17, 2024
1 parent a944f0e commit d6dc567
Show file tree
Hide file tree
Showing 26 changed files with 368 additions and 154 deletions.
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

0 comments on commit d6dc567

Please sign in to comment.