Repository: kubernetes/kubernetes
File: cmd/kubeadm/app/util/etcd/etcd.go (Go)

Code before:
	for _, member := range resp.Members {
		if member.ID == memberID {
			m = member
			break
		}
	}
	if m == nil {
		return false, false, fmt.Errorf("member %s not found", strconv.FormatUint(memberID, 16))
	}

	started = true
	if len(m.Name) == 0 {
		started = false
	}

	return m.IsLearner, started, nil
}

// MemberPromote promotes a member as a voting member. If the given member ID is already a voting member this method
// will return early and do nothing. It waits for the member to be started before attempting to promote.
func (c *Client) MemberPromote(learnerID uint64) error {
	isLearner, started, err := c.getMemberStatus(learnerID)
	if err != nil {
		return err
	}

	learnerIDUint := strconv.FormatUint(learnerID, 16)

	if !isLearner {
		klog.V(1).Infof("[etcd] Member %s already promoted.", learnerIDUint)
		return nil
	}

	// Wait for the member to be started before promoting
	var (
		lastError error
	)
	if !started {
		klog.V(1).Infof("[etcd] Waiting for member %s to be started before promoting", learnerIDUint)
		err = wait.PollUntilContextTimeout(context.Background(), constants.EtcdAPICallRetryInterval, kubeadmapi.GetActiveTimeouts().EtcdAPICall.Duration,
			true, func(_ context.Context) (bool, error) {
				var err error
				_, started, err = c.getMemberStatus(learnerID)
				if err != nil {
					lastError = errors.WithMessagef(err, "failed to get member %s status", learnerIDUint)
					return false, nil
				}
				if !started {
					klog.V(1).Infof("[etcd] Member %s is not started yet", learnerIDUint)
					lastError = errors.Errorf("the etcd member %s is not started", learnerIDUint)
					return false, nil

Review comment (suggestion):
Worth a code comment, I think.

```suggestion
	// There is no field for "started." If the member is not started, the Name and ClientURLs fields are set to their respective zero values.
	if len(m.Name) == 0 {
		started = false
```

Code after:
}

// getMemberStatus returns the status of the given member ID.
// It returns whether the member is a learner and whether it is started.
func (c *Client) getMemberStatus(memberID uint64) (isLearner bool, started bool, err error) {
	resp, err := c.listMembersOnce()
	if err != nil {
		return false, false, err
	}

	var m *etcdserverpb.Member
	for _, member := range resp.Members {
		if member.ID == memberID {
			m = member
			break
		}
	}
	if m == nil {
		return false, false, fmt.Errorf("member %s not found", strconv.FormatUint(memberID, 16))
	}

	started = true
	// There is no field for "started".
	// If the member is not started, the Name and ClientURLs fields are set to their respective zero values.
	if len(m.Name) == 0 {
		started = false
	}

	return m.IsLearner, started, nil
}

// MemberPromote promotes a member as a voting member. If the given member ID is already a voting member this method
// will return early and do nothing. It waits for the member to be started before attempting to promote.
func (c *Client) MemberPromote(learnerID uint64) error {
	var (
		lastError     error
		learnerIDUint = strconv.FormatUint(learnerID, 16)
	)

	klog.V(1).Infof("[etcd] Waiting for a learner to start: %s", learnerIDUint)

	err := wait.PollUntilContextTimeout(context.Background(), constants.EtcdAPICallRetryInterval, kubeadmapi.GetActiveTimeouts().EtcdAPICall.Duration,
		true, func(_ context.Context) (bool, error) {
			isLearner, started, err := c.getMemberStatus(learnerID)
			if err != nil {
				lastError = errors.WithMessagef(err, "failed to get member %s status", learnerIDUint)
				return false, nil
			}
			if !isLearner {
				klog.V(1).Infof("[etcd] Member %s was already promoted.", learnerIDUint)
				return true, nil