github.com/lfch/etcd-io/tests/v3@v3.0.0-20221004140520-eac99acd3e9d/common/member_test.go (about) 1 // Copyright 2022 The etcd Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package common 16 17 import ( 18 "context" 19 "testing" 20 "time" 21 22 clientv3 "github.com/lfch/etcd-io/client/v3" 23 "github.com/lfch/etcd-io/server/v3/etcdserver" 24 "github.com/lfch/etcd-io/tests/v3/framework" 25 "github.com/lfch/etcd-io/tests/v3/framework/testutils" 26 "github.com/stretchr/testify/require" 27 ) 28 29 func TestMemberList(t *testing.T) { 30 testRunner.BeforeTest(t) 31 32 for _, tc := range clusterTestCases { 33 t.Run(tc.name, func(t *testing.T) { 34 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 35 defer cancel() 36 clus := testRunner.NewCluster(ctx, t, tc.config) 37 defer clus.Close() 38 cc := framework.MustClient(clus.Client(clientv3.AuthConfig{})) 39 40 testutils.ExecuteUntil(ctx, t, func() { 41 resp, err := cc.MemberList(ctx) 42 if err != nil { 43 t.Fatalf("could not get member list, err: %s", err) 44 } 45 expectNum := len(clus.Members()) 46 gotNum := len(resp.Members) 47 if expectNum != gotNum { 48 t.Fatalf("number of members not equal, expect: %d, got: %d", expectNum, gotNum) 49 } 50 for _, m := range resp.Members { 51 if len(m.ClientURLs) == 0 { 52 t.Fatalf("member is not started, memberId:%d, memberName:%s", m.ID, m.Name) 53 } 54 } 55 }) 56 }) 57 } 58 } 59 60 func TestMemberAdd(t *testing.T) { 61 testRunner.BeforeTest(t) 62 63 learnerTcs := []struct { 64 name string 65 learner bool 66 }{ 67 { 68 name: "NotLearner", 69 learner: false, 70 }, 71 { 72 name: "Learner", 73 learner: true, 74 }, 75 } 76 77 quorumTcs := []struct { 78 name string 79 strictReconfigCheck bool 80 waitForQuorum bool 81 expectError bool 82 }{ 83 { 84 name: "StrictReconfigCheck/WaitForQuorum", 85 strictReconfigCheck: true, 86 waitForQuorum: true, 87 }, 88 { 89 name: "StrictReconfigCheck/NoWaitForQuorum", 90 strictReconfigCheck: true, 91 expectError: true, 92 }, 93 { 94 name: "DisableStrictReconfigCheck/WaitForQuorum", 95 waitForQuorum: true, 96 }, 97 { 98 name: "DisableStrictReconfigCheck/NoWaitForQuorum", 99 }, 100 } 101 102 for _, learnerTc := range learnerTcs { 103 for _, quorumTc := range quorumTcs { 104 for _, clusterTc := range clusterTestCases { 105 t.Run(learnerTc.name+"/"+quorumTc.name+"/"+clusterTc.name, func(t *testing.T) { 106 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 107 defer cancel() 108 c := clusterTc.config 109 c.DisableStrictReconfigCheck = !quorumTc.strictReconfigCheck 110 clus := testRunner.NewCluster(ctx, t, c) 111 defer clus.Close() 112 cc := framework.MustClient(clus.Client(clientv3.AuthConfig{})) 113 114 testutils.ExecuteUntil(ctx, t, func() { 115 var addResp *clientv3.MemberAddResponse 116 var err error 117 if quorumTc.waitForQuorum { 118 time.Sleep(etcdserver.HealthInterval) 119 } 120 if learnerTc.learner { 121 addResp, err = cc.MemberAddAsLearner(ctx, "newmember", []string{"http://localhost:123"}) 122 } else { 123 addResp, err = cc.MemberAdd(ctx, "newmember", []string{"http://localhost:123"}) 124 } 125 if quorumTc.expectError && c.ClusterSize > 1 { 126 // calling MemberAdd/MemberAddAsLearner on a single node will not fail, 127 // whether strictReconfigCheck or whether waitForQuorum 128 require.ErrorContains(t, err, "etcdserver: unhealthy cluster") 129 } else { 130 require.NoError(t, err, "MemberAdd failed") 131 if addResp.Member == nil { 132 t.Fatalf("MemberAdd failed, expected: member != nil, got: member == nil") 133 } 134 if addResp.Member.ID == 0 { 135 t.Fatalf("MemberAdd failed, expected: ID != 0, got: ID == 0") 136 } 137 if len(addResp.Member.PeerURLs) == 0 { 138 t.Fatalf("MemberAdd failed, expected: non-empty PeerURLs, got: empty PeerURLs") 139 } 140 } 141 }) 142 }) 143 } 144 } 145 } 146 } 147 148 func TestMemberRemove(t *testing.T) { 149 testRunner.BeforeTest(t) 150 151 tcs := []struct { 152 name string 153 strictReconfigCheck bool 154 waitForQuorum bool 155 expectSingleNodeError bool 156 expectClusterError bool 157 }{ 158 { 159 name: "StrictReconfigCheck/WaitForQuorum", 160 strictReconfigCheck: true, 161 waitForQuorum: true, 162 expectSingleNodeError: true, 163 }, 164 { 165 name: "StrictReconfigCheck/NoWaitForQuorum", 166 strictReconfigCheck: true, 167 expectSingleNodeError: true, 168 expectClusterError: true, 169 }, 170 { 171 name: "DisableStrictReconfigCheck/WaitForQuorum", 172 waitForQuorum: true, 173 }, 174 { 175 name: "DisableStrictReconfigCheck/NoWaitForQuorum", 176 }, 177 } 178 179 for _, quorumTc := range tcs { 180 for _, clusterTc := range clusterTestCases { 181 if !quorumTc.strictReconfigCheck && clusterTc.config.ClusterSize == 1 { 182 // skip these test cases 183 // when strictReconfigCheck is disabled, calling MemberRemove will cause the single node to panic 184 continue 185 } 186 t.Run(quorumTc.name+"/"+clusterTc.name, func(t *testing.T) { 187 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 188 defer cancel() 189 c := clusterTc.config 190 c.DisableStrictReconfigCheck = !quorumTc.strictReconfigCheck 191 clus := testRunner.NewCluster(ctx, t, c) 192 defer clus.Close() 193 // client connects to a specific member which won't be removed from cluster 194 cc := clus.Members()[0].Client() 195 196 testutils.ExecuteUntil(ctx, t, func() { 197 if quorumTc.waitForQuorum { 198 time.Sleep(etcdserver.HealthInterval) 199 } 200 201 memberId, clusterId := memberToRemove(ctx, t, cc, c.ClusterSize) 202 removeResp, err := cc.MemberRemove(ctx, memberId) 203 204 if c.ClusterSize == 1 && quorumTc.expectSingleNodeError { 205 require.ErrorContains(t, err, "etcdserver: re-configuration failed due to not enough started members") 206 return 207 } 208 209 if c.ClusterSize > 1 && quorumTc.expectClusterError { 210 require.ErrorContains(t, err, "etcdserver: unhealthy cluster") 211 return 212 } 213 214 require.NoError(t, err, "MemberRemove failed") 215 t.Logf("removeResp.Members:%v", removeResp.Members) 216 if removeResp.Header.ClusterId != clusterId { 217 t.Fatalf("MemberRemove failed, expected ClusterId: %d, got: %d", clusterId, removeResp.Header.ClusterId) 218 } 219 if len(removeResp.Members) != c.ClusterSize-1 { 220 t.Fatalf("MemberRemove failed, expected length of members: %d, got: %d", c.ClusterSize-1, len(removeResp.Members)) 221 } 222 for _, m := range removeResp.Members { 223 if m.ID == memberId { 224 t.Fatalf("MemberRemove failed, member(id=%d) is still in cluster", memberId) 225 } 226 } 227 }) 228 }) 229 } 230 } 231 } 232 233 // memberToRemove chooses a member to remove. 234 // If clusterSize == 1, return the only member. 235 // Otherwise, return a member that client has not connected to. 236 // It ensures that `MemberRemove` function does not return an "etcdserver: server stopped" error. 237 func memberToRemove(ctx context.Context, t *testing.T, client framework.Client, clusterSize int) (memberId uint64, clusterId uint64) { 238 listResp, err := client.MemberList(ctx) 239 if err != nil { 240 t.Fatal(err) 241 } 242 243 clusterId = listResp.Header.ClusterId 244 if clusterSize == 1 { 245 memberId = listResp.Members[0].ID 246 } else { 247 // get status of the specific member that client has connected to 248 statusResp, err := client.Status(ctx) 249 if err != nil { 250 t.Fatal(err) 251 } 252 253 // choose a member that client has not connected to 254 for _, m := range listResp.Members { 255 if m.ID != statusResp[0].Header.MemberId { 256 memberId = m.ID 257 break 258 } 259 } 260 if memberId == 0 { 261 t.Fatalf("memberToRemove failed. listResp:%v, statusResp:%v", listResp, statusResp) 262 } 263 } 264 return memberId, clusterId 265 }