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  }