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

Code before:
	isLearner, started, err := c.getMemberStatus(learnerID)
	if err != nil {
		return err
	}
	if !isLearner {
		klog.V(1).Infof("[etcd] Member %s already promoted.", strconv.FormatUint(learnerID, 16))
		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", strconv.FormatUint(learnerID, 16))
		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 = fmt.Errorf("failed to get member %s status: %v", strconv.FormatUint(learnerID, 16), err)
					return false, nil
				}
				if !started {
					klog.V(1).Infof("[etcd] Member %s is not started yet", strconv.FormatUint(learnerID, 16))
					lastError = fmt.Errorf("the etcd member %s is not started", strconv.FormatUint(learnerID, 16))
					return false, nil
				}
				return true, nil
			})
		if err != nil {
			return lastError
		}
	}

	// always reset the last error to nil
	lastError = nil

	klog.V(1).Infof("[etcd] Promoting a learner as a voting member: %s", strconv.FormatUint(learnerID, 16))
	cli, err := c.newEtcdClient(c.Endpoints)
	if err != nil {
		return err
	}
	defer func() { _ = cli.Close() }()

	// TODO: warning logs from etcd client should be removed.
	// The warning logs are printed by etcd client code for several reasons, including
	// 1. can not promote yet(no synced)
	// 2. context deadline exceeded
	// 3. peer URLs already exists
	// Once the client provides a way to check if the etcd learner is ready to promote, the retry logic can be revisited.

Review comment (suggestion):
```suggestion
					lastError = errors.Errorf("the etcd member %s is not started", strconv.FormatUint(learnerID, 16))
```

Code after:
	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
			}
			if !started {
				klog.V(1).Infof("[etcd] Member %s is not started yet. Waiting for it to be started.", learnerIDUint)
				lastError = errors.Errorf("the etcd member %s is not started", learnerIDUint)
				return false, nil
			}
			return true, nil
		})
	if err != nil {
		return lastError
	}

	klog.V(1).Infof("[etcd] Promoting a learner as a voting member: %s", learnerIDUint)

	cli, err := c.newEtcdClient(c.Endpoints)
	if err != nil {
		return err
	}

	defer func() { _ = cli.Close() }()