@@ -23,18 +23,25 @@ import (
23
23
"testing"
24
24
"time"
25
25
26
+ "github.com/coreos/go-semver/semver"
26
27
"github.com/stretchr/testify/require"
27
28
"go.uber.org/zap"
28
29
30
+ pb "go.etcd.io/etcd/api/v3/etcdserverpb"
31
+ "go.etcd.io/etcd/client/pkg/v3/fileutil"
29
32
clientv3 "go.etcd.io/etcd/client/v3"
33
+ "go.etcd.io/etcd/pkg/v3/expect"
30
34
"go.etcd.io/etcd/server/v3/etcdserver"
31
35
"go.etcd.io/etcd/tests/v3/framework/e2e"
32
36
"go.etcd.io/etcd/tests/v3/robustness/identity"
33
37
"go.etcd.io/etcd/tests/v3/robustness/report"
34
38
"go.etcd.io/etcd/tests/v3/robustness/traffic"
35
39
)
36
40
37
- var MemberReplace Failpoint = memberReplace {}
41
+ var (
42
+ MemberReplace Failpoint = memberReplace {}
43
+ MemberDowngrade Failpoint = memberDowngrade {}
44
+ )
38
45
39
46
type memberReplace struct {}
40
47
@@ -138,6 +145,76 @@ func (f memberReplace) Available(config e2e.EtcdProcessClusterConfig, member e2e
138
145
return config .ClusterSize > 1 && (config .Version == e2e .QuorumLastVersion || member .Config ().ExecPath == e2e .BinPath .Etcd )
139
146
}
140
147
148
+ type memberDowngrade struct {}
149
+
150
+ func (f memberDowngrade ) Inject (ctx context.Context , t * testing.T , lg * zap.Logger , clus * e2e.EtcdProcessCluster , baseTime time.Time , ids identity.Provider ) ([]report.ClientReport , error ) {
151
+ v , err := e2e .GetVersionFromBinary (e2e .BinPath .Etcd )
152
+ if err != nil {
153
+ return nil , err
154
+ }
155
+ targetVersion := semver.Version {Major : v .Major , Minor : v .Minor - 1 }
156
+ numberOfMembersToDowngrade := rand .Int ()% len (clus .Procs ) + 1
157
+ membersToDowngrade := rand .Perm (len (clus .Procs ))[:numberOfMembersToDowngrade ]
158
+ lg .Info ("Test downgrading members" , zap .Any ("members" , membersToDowngrade ))
159
+
160
+ member := clus .Procs [0 ]
161
+ endpoints := []string {member .EndpointsGRPC ()[0 ]}
162
+ cc , err := clientv3 .New (clientv3.Config {
163
+ Endpoints : endpoints ,
164
+ Logger : zap .NewNop (),
165
+ DialKeepAliveTime : 10 * time .Second ,
166
+ DialKeepAliveTimeout : 100 * time .Millisecond ,
167
+ })
168
+ if err != nil {
169
+ return nil , err
170
+ }
171
+ defer cc .Close ()
172
+
173
+ // Need to wait health interval for cluster to accept changes
174
+ time .Sleep (etcdserver .HealthInterval )
175
+ lg .Info ("Enable downgrade" )
176
+ err = enableDowngrade (ctx , cc , & targetVersion )
177
+ if err != nil {
178
+ return nil , err
179
+ }
180
+ // Need to wait health interval for cluster to prepare for downgrade
181
+ time .Sleep (etcdserver .HealthInterval )
182
+
183
+ for _ , memberID := range membersToDowngrade {
184
+ member = clus .Procs [memberID ]
185
+ lg .Info ("Downgrading member" , zap .String ("member" , member .Config ().Name ))
186
+ if err = member .Stop (); err != nil {
187
+ return nil , err
188
+ }
189
+ member .Config ().ExecPath = e2e .BinPath .EtcdLastRelease
190
+ lg .Info ("Restarting member" , zap .String ("member" , member .Config ().Name ))
191
+ err = member .Start (ctx )
192
+ if err != nil {
193
+ return nil , err
194
+ }
195
+ err = verifyVersion (t , clus , member , targetVersion )
196
+ }
197
+ time .Sleep (etcdserver .HealthInterval )
198
+ return nil , err
199
+ }
200
+
201
+ func (f memberDowngrade ) Name () string {
202
+ return "MemberDowngrade"
203
+ }
204
+
205
+ func (f memberDowngrade ) Available (config e2e.EtcdProcessClusterConfig , member e2e.EtcdProcess , profile traffic.Profile ) bool {
206
+ if ! fileutil .Exist (e2e .BinPath .EtcdLastRelease ) {
207
+ return false
208
+ }
209
+ v , err := e2e .GetVersionFromBinary (e2e .BinPath .Etcd )
210
+ if err != nil {
211
+ panic ("Failed checking etcd version binary" )
212
+ }
213
+ v3_6 := semver.Version {Major : 3 , Minor : 6 }
214
+ // only current version cluster can be downgraded.
215
+ return v .Compare (v3_6 ) >= 0 && (config .Version == e2e .CurrentVersion && member .Config ().ExecPath == e2e .BinPath .Etcd )
216
+ }
217
+
141
218
func getID (ctx context.Context , cc * clientv3.Client , name string ) (id uint64 , found bool , err error ) {
142
219
// Ensure linearized MemberList by first making a linearized Get request from the same member.
143
220
// This is required for v3.4 support as it doesn't support linearized MemberList https://github.com/etcd-io/etcd/issues/18929
@@ -170,3 +247,29 @@ func patchArgs(args []string, flag, newValue string) error {
170
247
}
171
248
return fmt .Errorf ("--%s flag not found" , flag )
172
249
}
250
+
251
+ func enableDowngrade (ctx context.Context , cc * clientv3.Client , targetVersion * semver.Version ) error {
252
+ _ , err := cc .Maintenance .Downgrade (ctx , clientv3 .DowngradeAction (pb .DowngradeRequest_VALIDATE ), targetVersion .String ())
253
+ if err != nil {
254
+ return err
255
+ }
256
+ _ , err = cc .Maintenance .Downgrade (ctx , clientv3 .DowngradeAction (pb .DowngradeRequest_ENABLE ), targetVersion .String ())
257
+ return err
258
+ }
259
+
260
+ func verifyVersion (t * testing.T , clus * e2e.EtcdProcessCluster , member e2e.EtcdProcess , expectedVersion semver.Version ) error {
261
+ var err error
262
+ expected := fmt .Sprintf (`"etcdserver":"%d.%d\..*"etcdcluster":"%d\.%d\.` , expectedVersion .Major , expectedVersion .Minor , expectedVersion .Major , expectedVersion .Minor )
263
+ for i := 0 ; i < 35 ; i ++ {
264
+ if err = e2e .CURLGetFromMember (clus , member , e2e.CURLReq {Endpoint : "/version" , Expected : expect.ExpectedResponse {Value : expected , IsRegularExpr : true }}); err != nil {
265
+ t .Logf ("#%d: v3 is not ready yet (%v)" , i , err )
266
+ time .Sleep (200 * time .Millisecond )
267
+ continue
268
+ }
269
+ break
270
+ }
271
+ if err != nil {
272
+ return fmt .Errorf ("failed to verify version, expected %v got (%w)" , expected , err )
273
+ }
274
+ return nil
275
+ }
0 commit comments