diff --git a/go.mod b/go.mod index cb83d8c..20edf26 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 7f2daaf..91759a1 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/managedplugin/plugin.go b/managedplugin/plugin.go index fc042d8..a9d7338 100644 --- a/managedplugin/plugin.go +++ b/managedplugin/plugin.go @@ -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" @@ -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{}) diff --git a/managedplugin/version_checker.go b/managedplugin/version_checker.go new file mode 100644 index 0000000..c822101 --- /dev/null +++ b/managedplugin/version_checker.go @@ -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 +} diff --git a/managedplugin/version_checker_test.go b/managedplugin/version_checker_test.go new file mode 100644 index 0000000..3ed101a --- /dev/null +++ b/managedplugin/version_checker_test.go @@ -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) +}