diff --git a/api/version/version.go b/api/version/version.go index bd39791a9..ef3c09e7c 100644 --- a/api/version/version.go +++ b/api/version/version.go @@ -44,6 +44,9 @@ var ( V3_6 = semver.Version{Major: 3, Minor: 6} V3_7 = semver.Version{Major: 3, Minor: 7} V4_0 = semver.Version{Major: 4, Minor: 0} + + // AllVersions keeps all the versions in ascending order. + AllVersions = []semver.Version{V3_0, V3_1, V3_2, V3_3, V3_4, V3_5, V3_6, V3_7, V4_0} ) func init() { diff --git a/client/v3/client.go b/client/v3/client.go index 0c91889fa..10c00f612 100644 --- a/client/v3/client.go +++ b/client/v3/client.go @@ -18,11 +18,11 @@ import ( "context" "errors" "fmt" - "strconv" "strings" "sync" "time" + "github.com/coreos/go-semver/semver" "go.uber.org/zap" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -32,6 +32,7 @@ import ( "google.golang.org/grpc/status" "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" + "go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/client/pkg/v3/logutil" "go.etcd.io/etcd/client/v3/credentials" "go.etcd.io/etcd/client/v3/internal/endpoint" @@ -475,6 +476,22 @@ func (c *Client) roundRobinQuorumBackoff(waitBetween time.Duration, jitterFracti } } +// minSupportedVersion returns the minimum version supported, which is the previous minor release. +func minSupportedVersion() *semver.Version { + ver := semver.Must(semver.NewVersion(version.Version)) + // consider only major and minor version + ver = &semver.Version{Major: ver.Major, Minor: ver.Minor} + for i := range version.AllVersions { + if version.AllVersions[i].Equal(*ver) { + if i == 0 { + return ver + } + return &version.AllVersions[i-1] + } + } + panic("current version is not in the version list") +} + func (c *Client) checkVersion() (err error) { var wg sync.WaitGroup @@ -496,20 +513,13 @@ func (c *Client) checkVersion() (err error) { errc <- rerr return } - vs := strings.Split(resp.Version, ".") - maj, min := 0, 0 - if len(vs) >= 2 { - var serr error - if maj, serr = strconv.Atoi(vs[0]); serr != nil { - errc <- serr - return - } - if min, serr = strconv.Atoi(vs[1]); serr != nil { - errc <- serr - return - } + vs, serr := semver.NewVersion(resp.Version) + if serr != nil { + errc <- serr + return } - if maj < 3 || (maj == 3 && min < 4) { + + if vs.LessThan(*minSupportedVersion()) { rerr = ErrOldCluster } errc <- rerr diff --git a/client/v3/client_test.go b/client/v3/client_test.go index c569d7a3b..f81a1c102 100644 --- a/client/v3/client_test.go +++ b/client/v3/client_test.go @@ -24,13 +24,16 @@ import ( "testing" "time" + "github.com/coreos/go-semver/semver" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" "go.etcd.io/etcd/api/v3/etcdserverpb" "go.etcd.io/etcd/api/v3/v3rpc/rpctypes" + "go.etcd.io/etcd/api/v3/version" "go.etcd.io/etcd/client/pkg/v3/testutil" "google.golang.org/grpc" @@ -312,6 +315,47 @@ func TestSyncFiltersMembers(t *testing.T) { } } +func TestMinSupportedVersion(t *testing.T) { + testutil.BeforeTest(t) + var tests = []struct { + name string + currentVersion semver.Version + minSupportedVersion semver.Version + }{ + { + name: "v3.6 client should accept v3.5", + currentVersion: version.V3_6, + minSupportedVersion: version.V3_5, + }, + { + name: "v3.7 client should accept v3.6", + currentVersion: version.V3_7, + minSupportedVersion: version.V3_6, + }, + { + name: "first minor version should accept its previous version", + currentVersion: version.V4_0, + minSupportedVersion: version.V3_7, + }, + { + name: "first version in list should not accept previous versions", + currentVersion: version.V3_0, + minSupportedVersion: version.V3_0, + }, + } + + versionBackup := version.Version + t.Cleanup(func() { + version.Version = versionBackup + }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + version.Version = tt.currentVersion.String() + require.True(t, minSupportedVersion().Equal(tt.minSupportedVersion)) + }) + } +} + func TestClientRejectOldCluster(t *testing.T) { testutil.BeforeTest(t) var tests = []struct { @@ -323,13 +367,13 @@ func TestClientRejectOldCluster(t *testing.T) { { name: "all new versions with the same value", endpoints: []string{"192.168.3.41:22379", "192.168.3.41:22479", "192.168.3.41:22579"}, - versions: []string{"3.5.4", "3.5.4", "3.5.4"}, + versions: []string{version.Version, version.Version, version.Version}, expectedError: nil, }, { name: "all new versions with different values", endpoints: []string{"192.168.3.41:22379", "192.168.3.41:22479", "192.168.3.41:22579"}, - versions: []string{"3.5.4", "3.5.4", "3.4.0"}, + versions: []string{version.Version, minSupportedVersion().String(), minSupportedVersion().String()}, expectedError: nil, }, { diff --git a/client/v3/go.mod b/client/v3/go.mod index 518b9de76..45779695a 100644 --- a/client/v3/go.mod +++ b/client/v3/go.mod @@ -3,6 +3,7 @@ module go.etcd.io/etcd/client/v3 go 1.19 require ( + github.com/coreos/go-semver v0.3.1 github.com/dustin/go-humanize v1.0.1 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/prometheus/client_golang v1.16.0 @@ -18,7 +19,6 @@ require ( github.com/benbjohnson/clock v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect