diff --git a/tests/common/member_test.go b/tests/common/member_test.go index a9b2ba428a6..b1f84491c74 100644 --- a/tests/common/member_test.go +++ b/tests/common/member_test.go @@ -19,6 +19,9 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/server/v3/etcdserver" "go.etcd.io/etcd/tests/v3/framework/testutils" ) @@ -52,3 +55,100 @@ func TestMemberList(t *testing.T) { }) } } + +func TestMemberAdd(t *testing.T) { + testRunner.BeforeTest(t) + + learnerTcs := []struct { + name string + learner bool + }{ + { + name: "NotLearner", + learner: false, + }, + { + name: "Learner", + learner: true, + }, + } + + quorumTcs := []struct { + name string + strictReconfigCheck bool + waitForQuorum bool + expectError bool + }{ + { + name: "StrictReconfigCheck/WaitForQuorum", + strictReconfigCheck: true, + waitForQuorum: true, + expectError: false, + }, + { + name: "StrictReconfigCheck/NoWaitForQuorum", + strictReconfigCheck: true, + waitForQuorum: false, + expectError: true, + }, + { + name: "DisableStrictReconfigCheck/WaitForQuorum", + strictReconfigCheck: false, + waitForQuorum: true, + expectError: false, + }, + { + name: "DisableStrictReconfigCheck/NoWaitForQuorum", + strictReconfigCheck: false, + waitForQuorum: false, + expectError: false, + }, + } + + for _, learnerTc := range learnerTcs { + for _, quorumTc := range quorumTcs { + for _, clusterTc := range clusterTestCases { + t.Run(learnerTc.name+"/"+quorumTc.name+"/"+clusterTc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + c := clusterTc.config + if !quorumTc.strictReconfigCheck { + c.DisableStrictReconfigCheck = true + } + clus := testRunner.NewCluster(ctx, t, c) + defer clus.Close() + cc := clus.Client() + + testutils.ExecuteUntil(ctx, t, func() { + var addResp *clientv3.MemberAddResponse + var err error + if quorumTc.waitForQuorum { + time.Sleep(etcdserver.HealthInterval) + } + if learnerTc.learner { + addResp, err = cc.MemberAddAsLearner(ctx, "newmember", []string{"http://localhost:123"}) + } else { + addResp, err = cc.MemberAdd(ctx, "newmember", []string{"http://localhost:123"}) + } + if quorumTc.expectError && c.ClusterSize > 1 { + // calling MemberAdd/MemberAddAsLearner on a single node will not fail, + // whether strictReconfigCheck or whether waitForQuorum + require.ErrorContains(t, err, "etcdserver: unhealthy cluster") + } else { + require.NoError(t, err, "MemberAdd failed") + if addResp.Member == nil { + t.Fatalf("MemberAdd failed, expected: member != nil, got: member == nil") + } + if addResp.Member.ID == 0 { + t.Fatalf("MemberAdd failed, expected: ID != 0, got: ID == 0") + } + if len(addResp.Member.PeerURLs) == 0 { + t.Fatalf("MemberAdd failed, expected: non-empty PeerURLs, got: empty PeerURLs") + } + } + }) + }) + } + } + } +} diff --git a/tests/e2e/ctl_v3_member_test.go b/tests/e2e/ctl_v3_member_test.go index 61e7957eb66..5679be600c8 100644 --- a/tests/e2e/ctl_v3_member_test.go +++ b/tests/e2e/ctl_v3_member_test.go @@ -50,19 +50,7 @@ func TestCtlV3MemberRemoveClientAutoTLS(t *testing.T) { func TestCtlV3MemberRemovePeerTLS(t *testing.T) { testCtl(t, memberRemoveTest, withQuorum(), withDisableStrictReconfig(), withCfg(*e2e.NewConfigPeerTLS())) } -func TestCtlV3MemberAdd(t *testing.T) { testCtl(t, memberAddTest) } -func TestCtlV3MemberAddNoTLS(t *testing.T) { testCtl(t, memberAddTest, withCfg(*e2e.NewConfigNoTLS())) } -func TestCtlV3MemberAddClientTLS(t *testing.T) { - testCtl(t, memberAddTest, withCfg(*e2e.NewConfigClientTLS())) -} -func TestCtlV3MemberAddClientAutoTLS(t *testing.T) { - testCtl(t, memberAddTest, withCfg(*e2e.NewConfigClientAutoTLS())) -} -func TestCtlV3MemberAddPeerTLS(t *testing.T) { - testCtl(t, memberAddTest, withCfg(*e2e.NewConfigPeerTLS())) -} -func TestCtlV3MemberAddForLearner(t *testing.T) { testCtl(t, memberAddForLearnerTest) } -func TestCtlV3MemberUpdate(t *testing.T) { testCtl(t, memberUpdateTest) } +func TestCtlV3MemberUpdate(t *testing.T) { testCtl(t, memberUpdateTest) } func TestCtlV3MemberUpdateNoTLS(t *testing.T) { testCtl(t, memberUpdateTest, withCfg(*e2e.NewConfigNoTLS())) } @@ -178,18 +166,6 @@ func ctlV3MemberRemove(cx ctlCtx, ep, memberID, clusterID string) error { return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, fmt.Sprintf("%s removed from cluster %s", memberID, clusterID)) } -func memberAddTest(cx ctlCtx) { - if err := ctlV3MemberAdd(cx, fmt.Sprintf("http://localhost:%d", e2e.EtcdProcessBasePort+11), false); err != nil { - cx.t.Fatal(err) - } -} - -func memberAddForLearnerTest(cx ctlCtx) { - if err := ctlV3MemberAdd(cx, fmt.Sprintf("http://localhost:%d", e2e.EtcdProcessBasePort+11), true); err != nil { - cx.t.Fatal(err) - } -} - func ctlV3MemberAdd(cx ctlCtx, peerURL string, isLearner bool) error { cmdArgs := append(cx.PrefixArgs(), "member", "add", "newmember", fmt.Sprintf("--peer-urls=%s", peerURL)) if isLearner { diff --git a/tests/framework/e2e/cluster.go b/tests/framework/e2e/cluster.go index 61706136c5c..99786d64736 100644 --- a/tests/framework/e2e/cluster.go +++ b/tests/framework/e2e/cluster.go @@ -76,14 +76,6 @@ func NewConfigClientTLS() *EtcdProcessClusterConfig { } } -func NewConfigClientBoth() *EtcdProcessClusterConfig { - return &EtcdProcessClusterConfig{ - ClusterSize: 1, - ClientTLS: ClientTLSAndNonTLS, - InitialToken: "new", - } -} - func NewConfigClientAutoTLS() *EtcdProcessClusterConfig { return &EtcdProcessClusterConfig{ ClusterSize: 1, diff --git a/tests/framework/integration.go b/tests/framework/integration.go index 05b3bceb9d6..a17d7592036 100644 --- a/tests/framework/integration.go +++ b/tests/framework/integration.go @@ -327,3 +327,11 @@ func (c integrationClient) Watch(ctx context.Context, key string, opts config.Wa return c.Client.Watch(ctx, key, opOpts...) } + +func (c integrationClient) MemberAdd(ctx context.Context, _ string, peerAddrs []string) (*clientv3.MemberAddResponse, error) { + return c.Client.MemberAdd(ctx, peerAddrs) +} + +func (c integrationClient) MemberAddAsLearner(ctx context.Context, _ string, peerAddrs []string) (*clientv3.MemberAddResponse, error) { + return c.Client.MemberAddAsLearner(ctx, peerAddrs) +} diff --git a/tests/framework/interface.go b/tests/framework/interface.go index 583dd117d95..9c5338b9088 100644 --- a/tests/framework/interface.go +++ b/tests/framework/interface.go @@ -72,6 +72,8 @@ type Client interface { Txn(context context.Context, compares, ifSucess, ifFail []string, o config.TxnOptions) (*clientv3.TxnResponse, error) MemberList(context context.Context) (*clientv3.MemberListResponse, error) + MemberAdd(context context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error) + MemberAddAsLearner(context context.Context, name string, peerAddrs []string) (*clientv3.MemberAddResponse, error) Watch(ctx context.Context, key string, opts config.WatchOptions) clientv3.WatchChan }