Repository: etcd-io/etcd
File: tests/e2e/ctl_v3_member_test.go (Go)

Code before:
			// Add a learner once the cluster is healthy
			learnerID, addErr = cx.epc.StartNewProc(ctx, nil, cx.t, true, e2e.WithAuth("root", "root"))
			if addErr != nil {
				if strings.Contains(addErr.Error(), "etcdserver: unhealthy cluster") {
					time.Sleep(1 * time.Second)
					continue
				}
			}
			break
		}
		require.NoError(cx.t, addErr)

		var leaderIdx int
		var followerIdx int

		// Determine the index of the leader and the follower
		for idx, proc := range cx.epc.Procs[:2] {
			status, err := proc.Etcdctl(e2e.WithAuth("root", "root")).Status(ctx)
			require.NoError(cx.t, err)

			if status[0].Header.MemberId == status[0].Leader {
				leaderIdx = idx
			} else {
				followerIdx = idx
			}
		}

		if fromFollower {
			_, err = cx.epc.Procs[followerIdx].
				Etcdctl(e2e.WithAuth("root", "root")).
				MemberPromote(ctx, learnerID)
		} else {
			_, err = cx.epc.Procs[leaderIdx].
				Etcdctl(e2e.WithAuth("root", "root")).
				MemberPromote(ctx, learnerID)
		}

		require.NoError(cx.t, err)
	}
}

func ctlV3MemberAdd(cx ctlCtx, peerURL string, isLearner bool) error {
	cmdArgs := append(cx.PrefixArgs(), "member", "add", "newmember", fmt.Sprintf("--peer-urls=%s", peerURL))
	asLearner := " "
	if isLearner {
		cmdArgs = append(cmdArgs, "--learner")
		asLearner = " as learner "
	}
	return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf(" added%sto cluster ", asLearner)})
}


Review comment (suggestion):
pls use `WaitLeader` below,

https://github.com/etcd-io/etcd/blob/48f735d13e83dea5b1901ea627eddd75d05ee8d5/tests/framework/e2e/cluster.go#L1110

Code after:
				if strings.Contains(addErr.Error(), "etcdserver: unhealthy cluster") {
					time.Sleep(1 * time.Second)
					continue
				}
			}
			break
		}
		require.NoError(cx.t, addErr)

		leaderIdx := cx.epc.WaitLeader(cx.t)
		followerIdx := (leaderIdx + 1) % len(cx.epc.Procs)

		require.NoError(cx.t, authEnable(cx))
		cx.user, cx.pass = "root", "root"

		if fromFollower {
			_, err = cx.epc.Procs[followerIdx].
				Etcdctl(e2e.WithAuth("root", "root")).
				MemberPromote(ctx, learnerID)
		} else {
			_, err = cx.epc.Procs[leaderIdx].
				Etcdctl(e2e.WithAuth("root", "root")).
				MemberPromote(ctx, learnerID)
		}

		require.NoError(cx.t, err)
	}
}

func ctlV3MemberAdd(cx ctlCtx, peerURL string, isLearner bool) error {
	cmdArgs := append(cx.PrefixArgs(), "member", "add", "newmember", fmt.Sprintf("--peer-urls=%s", peerURL))
	asLearner := " "
	if isLearner {
		cmdArgs = append(cmdArgs, "--learner")
		asLearner = " as learner "
	}
	return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: fmt.Sprintf(" added%sto cluster ", asLearner)})
}

func memberUpdateTest(cx ctlCtx) {
	mr, err := getMemberList(cx, false)
	require.NoError(cx.t, err)

	peerURL := fmt.Sprintf("http://localhost:%d", e2e.EtcdProcessBasePort+11)
	memberID := fmt.Sprintf("%x", mr.Members[0].ID)
	require.NoError(cx.t, ctlV3MemberUpdate(cx, memberID, peerURL))
}

func ctlV3MemberUpdate(cx ctlCtx, memberID, peerURL string) error {
	cmdArgs := append(cx.PrefixArgs(), "member", "update", memberID, fmt.Sprintf("--peer-urls=%s", peerURL))
	return e2e.SpawnWithExpectWithEnv(cmdArgs, cx.envMap, expect.ExpectedResponse{Value: " updated in cluster "})