diff --git a/integrations/access/common/app.go b/integrations/access/common/app.go index 886683f3c3cb6..805c0dde6ef8a 100644 --- a/integrations/access/common/app.go +++ b/integrations/access/common/app.go @@ -97,7 +97,7 @@ func (a *BaseApp) checkTeleportVersion(ctx context.Context) (proto.PingResponse, } return pong, trace.Wrap(err, "Unable to get Teleport server version") } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) return pong, trace.Wrap(err) } diff --git a/integrations/access/email/app.go b/integrations/access/email/app.go index 67855505a032e..0f37f174c197a 100644 --- a/integrations/access/email/app.go +++ b/integrations/access/email/app.go @@ -161,7 +161,7 @@ func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, err log.Error("Unable to get Teleport server version") return pong, trace.Wrap(err) } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) return pong, trace.Wrap(err) } diff --git a/integrations/access/jira/app.go b/integrations/access/jira/app.go index cc758dbda2fcc..7eface37eea79 100644 --- a/integrations/access/jira/app.go +++ b/integrations/access/jira/app.go @@ -236,7 +236,7 @@ func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, err log.Error("Unable to get Teleport server version") return pong, trace.Wrap(err) } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) return pong, trace.Wrap(err) } diff --git a/integrations/access/msteams/app.go b/integrations/access/msteams/app.go index 866f392f90d1e..901a8f16990ab 100644 --- a/integrations/access/msteams/app.go +++ b/integrations/access/msteams/app.go @@ -228,7 +228,7 @@ func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, err return pong, trace.Wrap(err) } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) return pong, trace.Wrap(err) } diff --git a/integrations/access/opsgenie/app.go b/integrations/access/opsgenie/app.go index 8ab196ee2a254..1a12c0e6de3be 100644 --- a/integrations/access/opsgenie/app.go +++ b/integrations/access/opsgenie/app.go @@ -196,7 +196,7 @@ func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, err log.Error("Unable to get Teleport server version") return pong, trace.Wrap(err) } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) return pong, trace.Wrap(err) } diff --git a/integrations/access/pagerduty/app.go b/integrations/access/pagerduty/app.go index e773196618139..3483bda56745b 100644 --- a/integrations/access/pagerduty/app.go +++ b/integrations/access/pagerduty/app.go @@ -189,7 +189,7 @@ func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, err log.Error("Unable to get Teleport server version") return pong, trace.Wrap(err) } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) return pong, trace.Wrap(err) } diff --git a/integrations/access/servicenow/app.go b/integrations/access/servicenow/app.go index de459abe59724..e9aa4d3154f30 100644 --- a/integrations/access/servicenow/app.go +++ b/integrations/access/servicenow/app.go @@ -183,7 +183,7 @@ func (a *App) checkTeleportVersion(ctx context.Context) (proto.PingResponse, err log.Error("Unable to get Teleport server version") return pong, trace.Wrap(err) } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) return pong, trace.Wrap(err) } diff --git a/integrations/terraform/provider/provider.go b/integrations/terraform/provider/provider.go index a3e54686f5e93..37366e6e3a456 100644 --- a/integrations/terraform/provider/provider.go +++ b/integrations/terraform/provider/provider.go @@ -396,7 +396,7 @@ func (p *Provider) checkTeleportVersion(ctx context.Context, client *client.Clie resp.Diagnostics.AddError("Unable to get Teleport server version!", "Unable to get Teleport server version!") return false } - err = utils.CheckVersion(pong.ServerVersion, minServerVersion) + err = utils.CheckMinVersion(pong.ServerVersion, minServerVersion) if err != nil { log.WithError(err).Debug("Teleport version check error!") resp.Diagnostics.AddError("Teleport version check error!", err.Error()) diff --git a/lib/auth/authclient/clt.go b/lib/auth/authclient/clt.go index f1c182fc9d054..66fafb23d32c4 100644 --- a/lib/auth/authclient/clt.go +++ b/lib/auth/authclient/clt.go @@ -1768,7 +1768,7 @@ func TryCreateAppSessionForClientCertV15(ctx context.Context, client CreateAppSe // If the auth server is v16+, the client does not need to provide a pre-created app session. const minServerVersion = "16.0.0-aa" // "-aa" matches all development versions - if utils.MeetsVersion(pingResp.ServerVersion, minServerVersion) { + if utils.MeetsMinVersion(pingResp.ServerVersion, minServerVersion) { return "", nil } diff --git a/lib/client/api.go b/lib/client/api.go index 3a07c699af7ce..3e2fcb2b6e999 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -4167,7 +4167,7 @@ func (tc *TeleportClient) Ping(ctx context.Context) (*webclient.PingResponse, er // Verify server->client and client->server compatibility. if tc.CheckVersions { - if !utils.MeetsVersion(teleport.Version, pr.MinClientVersion) { + if !utils.MeetsMinVersion(teleport.Version, pr.MinClientVersion) { fmt.Fprintf(tc.Stderr, ` WARNING Detected potentially incompatible client and server versions. @@ -4179,9 +4179,25 @@ Future versions of tsh will fail when incompatible versions are detected. pr.MinClientVersion, teleport.Version, pr.MinClientVersion) } + if !utils.MeetsMaxVersion(teleport.Version, pr.ServerVersion) { + serverVersionWithWildcards, err := utils.MajorSemverWithWildcards(pr.ServerVersion) + if err != nil { + return nil, trace.Wrap(err) + } + fmt.Fprintf(tc.Stderr, ` +WARNING +Detected potentially incompatible client and server versions. +Maximum client version supported by the server is %v but you are using %v. +Please downgrade tsh to %v or use the --skip-version-check flag to bypass this check. +Future versions of tsh will fail when incompatible versions are detected. + +`, + serverVersionWithWildcards, teleport.Version, serverVersionWithWildcards) + } + // Recent `tsh mfa` changes require at least Teleport v15. const minServerVersion = "15.0.0-aa" // "-aa" matches all development versions - if !utils.MeetsVersion(pr.ServerVersion, minServerVersion) { + if !utils.MeetsMinVersion(pr.ServerVersion, minServerVersion) { fmt.Fprintf(tc.Stderr, ` WARNING Detected incompatible client and server versions. diff --git a/lib/service/awsoidc.go b/lib/service/awsoidc.go index a733c85609570..8e5b190d427eb 100644 --- a/lib/service/awsoidc.go +++ b/lib/service/awsoidc.go @@ -207,7 +207,7 @@ func (updater *AWSOIDCDeployServiceUpdater) updateAWSOIDCDeployServices(ctx cont return trace.Wrap(err) } - if !utils.MeetsVersion(updater.TeleportClusterVersion, minServerVersion) { + if !utils.MeetsMinVersion(updater.TeleportClusterVersion, minServerVersion) { updater.Log.DebugContext(ctx, "Skipping update. AWS OIDC Deploy Service will not be compatible with cluster", "cluster_version", updater.TeleportClusterVersion, "stable_version", stableVersion) return nil } diff --git a/lib/utils/utils_test.go b/lib/utils/utils_test.go index 42ca172f35b78..2073398c4c827 100644 --- a/lib/utils/utils_test.go +++ b/lib/utils/utils_test.go @@ -133,8 +133,8 @@ func TestRemoveFromSlice(t *testing.T) { } } -// TestVersions tests versions compatibility checking -func TestVersions(t *testing.T) { +// TestMinVersions tests versions compatibility checking +func TestMinVersions(t *testing.T) { t.Parallel() type tc struct { @@ -149,8 +149,8 @@ func TestVersions(t *testing.T) { } for _, testCase := range successTestCases { t.Run(testCase.info, func(t *testing.T) { - require.NoError(t, CheckVersion(testCase.client, testCase.minClient)) - assert.True(t, MeetsVersion(testCase.client, testCase.minClient), "MeetsVersion expected to succeed") + require.NoError(t, CheckMinVersion(testCase.client, testCase.minClient)) + assert.True(t, MeetsMinVersion(testCase.client, testCase.minClient), "MeetsMinVersion expected to succeed") }) } @@ -160,8 +160,41 @@ func TestVersions(t *testing.T) { } for _, testCase := range failTestCases { t.Run(testCase.info, func(t *testing.T) { - fixtures.AssertBadParameter(t, CheckVersion(testCase.client, testCase.minClient)) - assert.False(t, MeetsVersion(testCase.client, testCase.minClient), "MeetsVersion expected to fail") + fixtures.AssertBadParameter(t, CheckMinVersion(testCase.client, testCase.minClient)) + assert.False(t, MeetsMinVersion(testCase.client, testCase.minClient), "MeetsMinVersion expected to fail") + }) + } +} + +// TestMaxVersions tests versions compatibility checking +func TestMaxVersions(t *testing.T) { + t.Parallel() + + type tc struct { + info string + client string + maxClient string + } + successTestCases := []tc{ + {info: "client same as max version", client: "1.0.0", maxClient: "1.0.0"}, + {info: "client older than max version", client: "1.1.0", maxClient: "1.2.0"}, + {info: "pre-releases clients are ok", client: "1.0.0-alpha.1", maxClient: "1.0.0"}, + } + for _, testCase := range successTestCases { + t.Run(testCase.info, func(t *testing.T) { + require.NoError(t, CheckMaxVersion(testCase.client, testCase.maxClient)) + assert.True(t, MeetsMaxVersion(testCase.client, testCase.maxClient), "MeetsMinVersion expected to succeed") + }) + } + + failTestCases := []tc{ + {info: "client newer than max version", client: "1.3.0", maxClient: "1.1.0"}, + {info: "newer pre-releases are no ok", client: "1.1.0", maxClient: "1.1.0-alpha.1"}, + } + for _, testCase := range failTestCases { + t.Run(testCase.info, func(t *testing.T) { + fixtures.AssertBadParameter(t, CheckMaxVersion(testCase.client, testCase.maxClient)) + assert.False(t, MeetsMaxVersion(testCase.client, testCase.maxClient), "MeetsMinVersion expected to fail") }) } } diff --git a/lib/utils/ver.go b/lib/utils/ver.go index 02c63de84345d..f47bc8824ef3c 100644 --- a/lib/utils/ver.go +++ b/lib/utils/ver.go @@ -25,20 +25,32 @@ import ( "github.com/gravitational/trace" ) -// MeetsVersion returns true if gotVer is empty or at least minVer. -func MeetsVersion(gotVer, minVer string) bool { +// MeetsMinVersion returns true if gotVer is empty or at least minVer. +func MeetsMinVersion(gotVer, minVer string) bool { if gotVer == "" { return true // Ignore empty versions. } - err := CheckVersion(gotVer, minVer) + err := CheckMinVersion(gotVer, minVer) // Non BadParameter errors are semver parsing errors. return !trace.IsBadParameter(err) } -// CheckVersion compares a version with a minimum version supported. -func CheckVersion(currentVersion, minVersion string) error { +// MeetsMaxVersion returns true if gotVer is empty or at most maxVer. +func MeetsMaxVersion(gotVer, maxVer string) bool { + if gotVer == "" { + return true // Ignore empty versions. + } + + err := CheckMaxVersion(gotVer, maxVer) + + // Non BadParameter errors are semver parsing errors. + return !trace.IsBadParameter(err) +} + +// CheckMinVersion compares a version with a minimum version supported. +func CheckMinVersion(currentVersion, minVersion string) error { currentSemver, minSemver, err := versionStringToSemver(currentVersion, minVersion) if err != nil { return trace.Wrap(err) @@ -51,6 +63,20 @@ func CheckVersion(currentVersion, minVersion string) error { return nil } +// CheckMaxVersion compares a version with a maximum version supported. +func CheckMaxVersion(currentVersion, maxVersion string) error { + currentSemver, maxSemver, err := versionStringToSemver(currentVersion, maxVersion) + if err != nil { + return trace.Wrap(err) + } + + if maxSemver.LessThan(*currentSemver) { + return trace.BadParameter("incompatible versions: %v > %v", currentVersion, maxVersion) + } + + return nil +} + // VersionBeforeAlpha appends "-aa" to the version so that it comes before -alpha. // This ban be used to make version checks work during development. func VersionBeforeAlpha(version string) string { @@ -82,6 +108,16 @@ func MajorSemver(version string) (string, error) { return fmt.Sprintf("%d.0.0", ver.Major), nil } +// MajorSemverWithWildcards returns the major version as a semver string. +// Ex: 13.4.3 -> 13.x.x +func MajorSemverWithWildcards(version string) (string, error) { + ver, err := semver.NewVersion(version) + if err != nil { + return "", trace.Wrap(err) + } + return fmt.Sprintf("%d.x.x", ver.Major), nil +} + func versionStringToSemver(ver1, ver2 string) (*semver.Version, *semver.Version, error) { v1Semver, err := semver.NewVersion(ver1) if err != nil { diff --git a/lib/utils/ver_test.go b/lib/utils/ver_test.go index 6265151439a2b..ff3d4b3bafaa7 100644 --- a/lib/utils/ver_test.go +++ b/lib/utils/ver_test.go @@ -24,18 +24,33 @@ import ( "github.com/stretchr/testify/require" ) -func TestMeetsVersion_emptyOrInvalid(t *testing.T) { - // See TestVersions for more comprehensive tests. +func TestMeetsMinVersion_emptyOrInvalid(t *testing.T) { + // See TestMinVersions for more comprehensive tests. - if !MeetsVersion("", "v1.2.3") { - t.Error("MeetsVersion with an empty gotVer should always succeed") + if !MeetsMinVersion("", "v1.2.3") { + t.Error("MeetsMinVersion with an empty gotVer should always succeed") } - if !MeetsVersion("banana", "v1.2.3") { - t.Error("MeetsVersion with an invalid version should always succeed") + if !MeetsMinVersion("banana", "v1.2.3") { + t.Error("MeetsMinVersion with an invalid version should always succeed") } - if !MeetsVersion("v1.2.3", "banana") { - t.Error("MeetsVersion with an invalid version should always succeed") + if !MeetsMinVersion("v1.2.3", "banana") { + t.Error("MeetsMinVersion with an invalid version should always succeed") + } +} + +func TestMeetsMaxVersion_emptyOrInvalid(t *testing.T) { + // See TestMaxVersions for more comprehensive tests. + + if !MeetsMaxVersion("", "v1.2.3") { + t.Error("MeetsMaxVersion with an empty gotVer should always succeed") + } + + if !MeetsMaxVersion("banana", "v1.2.3") { + t.Error("banana with an invalid version should always succeed") + } + if !MeetsMaxVersion("v1.2.3", "banana") { + t.Error("MeetsMaxVersion with an invalid version should always succeed") } }