Skip to content

Commit

Permalink
feat: Implement PluginVersionWarner to warn on outdated plugins. (#419)
Browse files Browse the repository at this point in the history
* Implement PluginVersionWarner for unauthenticated version checking.

* Fix rebase conflicts and previously merged version strategy.

* Make sure it doesn't panic if called after bad init.

* Improve safeguard for nil struct.

* Make kind a string, because there's no PluginKind.FromString.

* Update tests.

* Use constants.

* Optionally accept an AuthToken.
  • Loading branch information
marianogappa authored Oct 16, 2024
1 parent a5f68a1 commit dc7f2bd
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 50 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.22.0
toolchain go1.23.1

require (
github.com/Masterminds/semver v1.5.0
github.com/apache/arrow/go/v17 v17.0.0
github.com/avast/retry-go/v4 v4.6.0
github.com/cloudquery/cloudquery-api-go v1.13.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
Expand Down
50 changes: 0 additions & 50 deletions managedplugin/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"time"

"github.com/avast/retry-go/v4"
cloudquery_api "github.com/cloudquery/cloudquery-api-go"
pbBase "github.com/cloudquery/plugin-pb-go/pb/base/v0"
pbDiscovery "github.com/cloudquery/plugin-pb-go/pb/discovery/v0"
pbDiscoveryV1 "github.com/cloudquery/plugin-pb-go/pb/discovery/v1"
Expand Down Expand Up @@ -687,55 +686,6 @@ func (c *Client) Versions(ctx context.Context) ([]int, error) {
return res, nil
}

func (c *Client) FindLatestPluginVersion(ctx context.Context, typ PluginType) (string, error) {
if c.config.Registry != RegistryCloudQuery {
return "", fmt.Errorf("plugin registry is not cloudquery; cannot find latest plugin version")
}

if c.teamName == "" {
return "", fmt.Errorf("team name is required to find the latest plugin version")
}

pathSplit := strings.Split(c.config.Path, "/")
if len(pathSplit) != 2 {
return "", fmt.Errorf("invalid cloudquery plugin path: %s. format should be team/name", c.config.Path)
}
org, name := pathSplit[0], pathSplit[1]

if org != "cloudquery" {
return "", fmt.Errorf("plugin org is not cloudquery; cannot find latest plugin version")
}

ops := HubDownloadOptions{
AuthToken: c.authToken,
TeamName: c.teamName,
LocalPath: c.LocalPath,
PluginTeam: org,
PluginKind: typ.String(),
PluginName: name,
PluginVersion: c.config.Version,
}
hubClient, err := getHubClient(c.logger, ops)
if err != nil {
return "", fmt.Errorf("failed to get hub client: %w", err)
}

resp, err := hubClient.GetPluginWithResponse(ctx, ops.PluginTeam, cloudquery_api.PluginKind(ops.PluginKind), ops.PluginName)
if err != nil {
return "", fmt.Errorf("failed to get plugin: %w", err)
}

if resp.JSON200 == nil {
return "", fmt.Errorf("failed to get latest plugin version: %w", err)
}

if resp.JSON200.LatestVersion == nil {
return "", nil // It's possible to have no latest version (unpublished plugins)
}

return *resp.JSON200.LatestVersion, nil
}

func (c *Client) MaxVersion(ctx context.Context) (int, error) {
discoveryClient := pbDiscovery.NewDiscoveryClient(c.Conn)
versionsRes, err := discoveryClient.GetVersions(ctx, &pbDiscovery.GetVersions_Request{})
Expand Down
84 changes: 84 additions & 0 deletions managedplugin/version_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package managedplugin

import (
"context"
"fmt"

"github.com/Masterminds/semver"
cloudquery_api "github.com/cloudquery/cloudquery-api-go"
"github.com/rs/zerolog"
)

type PluginVersionWarner struct {
hubClient *cloudquery_api.ClientWithResponses
logger zerolog.Logger
}

func NewPluginVersionWarner(logger zerolog.Logger, optionalAuthToken string) (*PluginVersionWarner, error) {
hubClient, err := getHubClient(logger, HubDownloadOptions{AuthToken: optionalAuthToken})
if err != nil {
return nil, err
}
return &PluginVersionWarner{hubClient: hubClient, logger: logger}, nil
}

func (p *PluginVersionWarner) getLatestVersion(ctx context.Context, org string, name string, kind string) (*semver.Version, error) {
if p == nil {
return nil, fmt.Errorf("plugin version warner is not initialized")
}
if kind != PluginSource.String() && kind != PluginDestination.String() && kind != PluginTransformer.String() {
p.logger.Debug().Str("plugin", name).Str("kind", kind).Msg("invalid kind")
return nil, fmt.Errorf("invalid kind: %s", kind)
}
resp, err := p.hubClient.GetPluginWithResponse(ctx, org, cloudquery_api.PluginKind(kind), name)
if err != nil {
p.logger.Debug().Str("plugin", name).Err(err).Msg("failed to get plugin info from hub")
return nil, err
}
if resp.JSON200 == nil {
p.logger.Debug().Str("plugin", name).Msg("failed to get plugin info from hub, request didn't error but 200 response is nil")
return nil, fmt.Errorf("failed to get plugin info from hub, request didn't error but 200 response is nil")
}
if resp.JSON200.LatestVersion == nil {
p.logger.Debug().Str("plugin", name).Msg("cannot check if plugin is outdated, latest version is nil")
return nil, fmt.Errorf("cannot check if plugin is outdated, latest version is nil")
}
latestVersion := *resp.JSON200.LatestVersion
latestSemver, err := semver.NewVersion(latestVersion)
if err != nil {
p.logger.Debug().Str("plugin", name).Str("version", latestVersion).Err(err).Msg("failed to parse latest version")
return nil, err
}
return latestSemver, nil
}

// WarnIfOutdated requests the latest version of a plugin from the hub and warns if the client's supplied version is outdated.
// It returns true if nothing went wrong comparing the versions, and the client's version is outdated; false otherwise.
func (p *PluginVersionWarner) WarnIfOutdated(ctx context.Context, org string, name string, kind string, actualVersion string) (bool, error) {
if p == nil {
return false, fmt.Errorf("plugin version warner is not initialized")
}
if actualVersion == "" {
return false, nil
}
actualVersionSemver, err := semver.NewVersion(actualVersion)
if err != nil {
p.logger.Debug().Str("plugin", name).Str("version", actualVersion).Err(err).Msg("failed to parse actual version")
return false, err
}
latestVersionSemver, err := p.getLatestVersion(ctx, org, name, kind)
if err != nil {
return false, err
}
if actualVersionSemver.LessThan(latestVersionSemver) {
p.logger.Warn().
Str("plugin", name).
Str("using_version", actualVersionSemver.String()).
Str("latest_version", latestVersionSemver.String()).
Str("url", fmt.Sprintf("https://hub.cloudquery.io/plugins/%s/%s/%s", kind, org, name)).
Msg("Plugin is outdated, consider upgrading to the latest version.")
return true, nil
}

return false, nil
}
39 changes: 39 additions & 0 deletions managedplugin/version_checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package managedplugin

import (
"context"
"testing"

"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPluginVersionWarnerUnknownPluginFails(t *testing.T) {
versionWarner, err := NewPluginVersionWarner(zerolog.Nop(), "")
require.NoError(t, err)
warned, err := versionWarner.WarnIfOutdated(context.Background(), "unknown", "unknown", "source", "1.0.0")
assert.Error(t, err)
assert.False(t, warned)
}

// Note: this is an integration test that requires Internet access and the hub to be running
func TestPluginLatestVersionDoesNotWarn(t *testing.T) {
versionWarner, err := NewPluginVersionWarner(zerolog.Nop(), "")
require.NoError(t, err)
latestVersion, err := versionWarner.getLatestVersion(context.Background(), "cloudquery", "aws", "source")
assert.NoError(t, err)
hasWarned, err := versionWarner.WarnIfOutdated(context.Background(), "cloudquery", "aws", "source", latestVersion.String())
assert.NoError(t, err)
assert.False(t, hasWarned)
}

// Note: this is an integration test that requires Internet access and the hub to be running
// CloudQuery's aws source plugin must exist in the hub, and be over version v1.0.0
func TestPluginLatestVersionWarns(t *testing.T) {
versionWarner, err := NewPluginVersionWarner(zerolog.Nop(), "")
require.NoError(t, err)
hasWarned, err := versionWarner.WarnIfOutdated(context.Background(), "cloudquery", "aws", "source", "v1.0.0")
assert.NoError(t, err)
assert.True(t, hasWarned)
}

0 comments on commit dc7f2bd

Please sign in to comment.