Skip to content

Commit dc7f2bd

Browse files
authored
feat: Implement PluginVersionWarner to warn on outdated plugins. (#419)
* 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.
1 parent a5f68a1 commit dc7f2bd

File tree

5 files changed

+126
-50
lines changed

5 files changed

+126
-50
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.22.0
55
toolchain go1.23.1
66

77
require (
8+
github.com/Masterminds/semver v1.5.0
89
github.com/apache/arrow/go/v17 v17.0.0
910
github.com/avast/retry-go/v4 v4.6.0
1011
github.com/cloudquery/cloudquery-api-go v1.13.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
22
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
3+
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
4+
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
35
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
46
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
57
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=

managedplugin/plugin.go

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"time"
1818

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

690-
func (c *Client) FindLatestPluginVersion(ctx context.Context, typ PluginType) (string, error) {
691-
if c.config.Registry != RegistryCloudQuery {
692-
return "", fmt.Errorf("plugin registry is not cloudquery; cannot find latest plugin version")
693-
}
694-
695-
if c.teamName == "" {
696-
return "", fmt.Errorf("team name is required to find the latest plugin version")
697-
}
698-
699-
pathSplit := strings.Split(c.config.Path, "/")
700-
if len(pathSplit) != 2 {
701-
return "", fmt.Errorf("invalid cloudquery plugin path: %s. format should be team/name", c.config.Path)
702-
}
703-
org, name := pathSplit[0], pathSplit[1]
704-
705-
if org != "cloudquery" {
706-
return "", fmt.Errorf("plugin org is not cloudquery; cannot find latest plugin version")
707-
}
708-
709-
ops := HubDownloadOptions{
710-
AuthToken: c.authToken,
711-
TeamName: c.teamName,
712-
LocalPath: c.LocalPath,
713-
PluginTeam: org,
714-
PluginKind: typ.String(),
715-
PluginName: name,
716-
PluginVersion: c.config.Version,
717-
}
718-
hubClient, err := getHubClient(c.logger, ops)
719-
if err != nil {
720-
return "", fmt.Errorf("failed to get hub client: %w", err)
721-
}
722-
723-
resp, err := hubClient.GetPluginWithResponse(ctx, ops.PluginTeam, cloudquery_api.PluginKind(ops.PluginKind), ops.PluginName)
724-
if err != nil {
725-
return "", fmt.Errorf("failed to get plugin: %w", err)
726-
}
727-
728-
if resp.JSON200 == nil {
729-
return "", fmt.Errorf("failed to get latest plugin version: %w", err)
730-
}
731-
732-
if resp.JSON200.LatestVersion == nil {
733-
return "", nil // It's possible to have no latest version (unpublished plugins)
734-
}
735-
736-
return *resp.JSON200.LatestVersion, nil
737-
}
738-
739689
func (c *Client) MaxVersion(ctx context.Context) (int, error) {
740690
discoveryClient := pbDiscovery.NewDiscoveryClient(c.Conn)
741691
versionsRes, err := discoveryClient.GetVersions(ctx, &pbDiscovery.GetVersions_Request{})

managedplugin/version_checker.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package managedplugin
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/Masterminds/semver"
8+
cloudquery_api "github.com/cloudquery/cloudquery-api-go"
9+
"github.com/rs/zerolog"
10+
)
11+
12+
type PluginVersionWarner struct {
13+
hubClient *cloudquery_api.ClientWithResponses
14+
logger zerolog.Logger
15+
}
16+
17+
func NewPluginVersionWarner(logger zerolog.Logger, optionalAuthToken string) (*PluginVersionWarner, error) {
18+
hubClient, err := getHubClient(logger, HubDownloadOptions{AuthToken: optionalAuthToken})
19+
if err != nil {
20+
return nil, err
21+
}
22+
return &PluginVersionWarner{hubClient: hubClient, logger: logger}, nil
23+
}
24+
25+
func (p *PluginVersionWarner) getLatestVersion(ctx context.Context, org string, name string, kind string) (*semver.Version, error) {
26+
if p == nil {
27+
return nil, fmt.Errorf("plugin version warner is not initialized")
28+
}
29+
if kind != PluginSource.String() && kind != PluginDestination.String() && kind != PluginTransformer.String() {
30+
p.logger.Debug().Str("plugin", name).Str("kind", kind).Msg("invalid kind")
31+
return nil, fmt.Errorf("invalid kind: %s", kind)
32+
}
33+
resp, err := p.hubClient.GetPluginWithResponse(ctx, org, cloudquery_api.PluginKind(kind), name)
34+
if err != nil {
35+
p.logger.Debug().Str("plugin", name).Err(err).Msg("failed to get plugin info from hub")
36+
return nil, err
37+
}
38+
if resp.JSON200 == nil {
39+
p.logger.Debug().Str("plugin", name).Msg("failed to get plugin info from hub, request didn't error but 200 response is nil")
40+
return nil, fmt.Errorf("failed to get plugin info from hub, request didn't error but 200 response is nil")
41+
}
42+
if resp.JSON200.LatestVersion == nil {
43+
p.logger.Debug().Str("plugin", name).Msg("cannot check if plugin is outdated, latest version is nil")
44+
return nil, fmt.Errorf("cannot check if plugin is outdated, latest version is nil")
45+
}
46+
latestVersion := *resp.JSON200.LatestVersion
47+
latestSemver, err := semver.NewVersion(latestVersion)
48+
if err != nil {
49+
p.logger.Debug().Str("plugin", name).Str("version", latestVersion).Err(err).Msg("failed to parse latest version")
50+
return nil, err
51+
}
52+
return latestSemver, nil
53+
}
54+
55+
// WarnIfOutdated requests the latest version of a plugin from the hub and warns if the client's supplied version is outdated.
56+
// It returns true if nothing went wrong comparing the versions, and the client's version is outdated; false otherwise.
57+
func (p *PluginVersionWarner) WarnIfOutdated(ctx context.Context, org string, name string, kind string, actualVersion string) (bool, error) {
58+
if p == nil {
59+
return false, fmt.Errorf("plugin version warner is not initialized")
60+
}
61+
if actualVersion == "" {
62+
return false, nil
63+
}
64+
actualVersionSemver, err := semver.NewVersion(actualVersion)
65+
if err != nil {
66+
p.logger.Debug().Str("plugin", name).Str("version", actualVersion).Err(err).Msg("failed to parse actual version")
67+
return false, err
68+
}
69+
latestVersionSemver, err := p.getLatestVersion(ctx, org, name, kind)
70+
if err != nil {
71+
return false, err
72+
}
73+
if actualVersionSemver.LessThan(latestVersionSemver) {
74+
p.logger.Warn().
75+
Str("plugin", name).
76+
Str("using_version", actualVersionSemver.String()).
77+
Str("latest_version", latestVersionSemver.String()).
78+
Str("url", fmt.Sprintf("https://hub.cloudquery.io/plugins/%s/%s/%s", kind, org, name)).
79+
Msg("Plugin is outdated, consider upgrading to the latest version.")
80+
return true, nil
81+
}
82+
83+
return false, nil
84+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package managedplugin
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/rs/zerolog"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestPluginVersionWarnerUnknownPluginFails(t *testing.T) {
13+
versionWarner, err := NewPluginVersionWarner(zerolog.Nop(), "")
14+
require.NoError(t, err)
15+
warned, err := versionWarner.WarnIfOutdated(context.Background(), "unknown", "unknown", "source", "1.0.0")
16+
assert.Error(t, err)
17+
assert.False(t, warned)
18+
}
19+
20+
// Note: this is an integration test that requires Internet access and the hub to be running
21+
func TestPluginLatestVersionDoesNotWarn(t *testing.T) {
22+
versionWarner, err := NewPluginVersionWarner(zerolog.Nop(), "")
23+
require.NoError(t, err)
24+
latestVersion, err := versionWarner.getLatestVersion(context.Background(), "cloudquery", "aws", "source")
25+
assert.NoError(t, err)
26+
hasWarned, err := versionWarner.WarnIfOutdated(context.Background(), "cloudquery", "aws", "source", latestVersion.String())
27+
assert.NoError(t, err)
28+
assert.False(t, hasWarned)
29+
}
30+
31+
// Note: this is an integration test that requires Internet access and the hub to be running
32+
// CloudQuery's aws source plugin must exist in the hub, and be over version v1.0.0
33+
func TestPluginLatestVersionWarns(t *testing.T) {
34+
versionWarner, err := NewPluginVersionWarner(zerolog.Nop(), "")
35+
require.NoError(t, err)
36+
hasWarned, err := versionWarner.WarnIfOutdated(context.Background(), "cloudquery", "aws", "source", "v1.0.0")
37+
assert.NoError(t, err)
38+
assert.True(t, hasWarned)
39+
}

0 commit comments

Comments
 (0)