Repository: etcd-io/etcd
File: server/etcdserver/api/membership/cluster.go (Go)

Code before:
	serverversion.MustDetectDowngrade(c.lg, sv, c.version)
	onSet(c.lg, c.version)

	for _, m := range c.members {
		if c.localID == m.ID {
			setIsLearnerMetric(m)
		}

		c.lg.Info(
			"recovered/added member from store",
			zap.String("cluster-id", c.cid.String()),
			zap.String("local-member-id", c.localID.String()),
			zap.String("recovered-remote-peer-id", m.ID.String()),
			zap.Strings("recovered-remote-peer-urls", m.PeerURLs),
			zap.Bool("recovered-remote-peer-is-learner", m.IsLearner),
		)
	}
	if c.version != nil {
		c.lg.Info(
			"set cluster version from store",
			zap.String("cluster-version", version.Cluster(c.version.String())),
		)
	}
}

// ValidateConfigurationChange takes a proposed ConfChange and
// ensures that it is still valid.
func (c *RaftCluster) ValidateConfigurationChange(cc raftpb.ConfChange, shouldApplyV3 ShouldApplyV3) error {
	var membersMap map[types.ID]*Member
	var removedMap map[types.ID]bool

	if shouldApplyV3 {
		membersMap, removedMap = c.be.MustReadMembersFromBackend()
	} else {
		membersMap, removedMap = membersFromStore(c.lg, c.v2store)
	}

	id := types.ID(cc.NodeID)
	if removedMap[id] {
		return ErrIDRemoved
	}
	switch cc.Type {
	case raftpb.ConfChangeAddNode, raftpb.ConfChangeAddLearnerNode:
		confChangeContext := new(ConfigChangeContext)
		if err := json.Unmarshal(cc.Context, confChangeContext); err != nil {
			c.lg.Panic("failed to unmarshal confChangeContext", zap.Error(err))
		}

		if confChangeContext.IsPromote { // promoting a learner member to voting member
			if membersMap[id] == nil {
				return ErrIDNotFound

Review comment (suggestion):
This creates a hole that on bootstrap etcd may blindly apply some confChange to raft. I need to think this through in https://github.com/etcd-io/etcd/issues/20187

Code after:
		if c.localID == m.ID {
			setIsLearnerMetric(m)
		}

		c.lg.Info(
			"recovered/added member from store",
			zap.String("cluster-id", c.cid.String()),
			zap.String("local-member-id", c.localID.String()),
			zap.String("recovered-remote-peer-id", m.ID.String()),
			zap.Strings("recovered-remote-peer-urls", m.PeerURLs),
			zap.Bool("recovered-remote-peer-is-learner", m.IsLearner),
		)
	}
	if c.version != nil {
		c.lg.Info(
			"set cluster version from store",
			zap.String("cluster-version", version.Cluster(c.version.String())),
		)
	}
}

// ValidateConfigurationChange takes a proposed ConfChange and
// ensures that it is still valid.
func (c *RaftCluster) ValidateConfigurationChange(cc raftpb.ConfChange, shouldApplyV3 ShouldApplyV3) error {
	if !shouldApplyV3 {
		return nil
	}

	membersMap, removedMap := c.be.MustReadMembersFromBackend()

	id := types.ID(cc.NodeID)
	if removedMap[id] {
		return ErrIDRemoved
	}
	switch cc.Type {
	case raftpb.ConfChangeAddNode, raftpb.ConfChangeAddLearnerNode:
		confChangeContext := new(ConfigChangeContext)
		if err := json.Unmarshal(cc.Context, confChangeContext); err != nil {
			c.lg.Panic("failed to unmarshal confChangeContext", zap.Error(err))
		}

		if confChangeContext.IsPromote { // promoting a learner member to voting member
			if membersMap[id] == nil {
				return ErrIDNotFound
			}
			if !membersMap[id].IsLearner {
				return ErrMemberNotLearner
			}
		} else { // adding a new member
			if membersMap[id] != nil {
				return ErrIDExists