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

Code before:
		return nil
	}
	lv := semver.Must(semver.NewVersion(version.Version))
	// only keep major.minor version for comparison against cluster version
	lv = &semver.Version{Major: lv.Major, Minor: lv.Minor}
	if !lv.LessThan(*cv) {
		return nil
	}
	// allow 3.4 server to join 3.5 cluster. Note the local data schema will
	// be automatically migrated to 3.4 if `--next-cluster-version-compatible`
	// is enabled (true). Users can also execute `etcdutl migrate` to migrate
	// the data before starting the server.
	oneMinorVersionDown := &semver.Version{Major: cv.Major, Minor: cv.Minor - 1}
	if !nextClusterVersionCompatible || !lv.Equal(*oneMinorVersionDown) {
		return fmt.Errorf("invalid downgrade; (current version: %s is lower than determined cluster version: %s).", version.Version, version.Cluster(cv.String()))
	}
	return nil
}

// IsValidVersionChange checks the two scenario when version is valid to change:
// 1. Downgrade: cluster version is 1 minor version higher than local version,
// cluster version should change.
// 2. Cluster start: when not all members version are available, cluster version
// is set to MinVersion(3.0), when all members are at higher version, cluster version
// is lower than local version, cluster version should change
func IsValidVersionChange(cv *semver.Version, lv *semver.Version, nextClusterVersionCompatible bool) bool {
	cv = &semver.Version{Major: cv.Major, Minor: cv.Minor}
	lv = &semver.Version{Major: lv.Major, Minor: lv.Minor}

	if (nextClusterVersionCompatible && isValidDowngrade(cv, lv)) || (cv.Major == lv.Major && cv.LessThan(*lv)) {
		return true
	}
	return false
}

// IsLocalMemberLearner returns if the local member is raft learner
func (c *RaftCluster) IsLocalMemberLearner() bool {
	c.Lock()
	defer c.Unlock()
	localMember, ok := c.members[c.localID]
	if !ok {
		if c.lg != nil {
			c.lg.Panic(
				"failed to find local ID in cluster members",
				zap.String("cluster-id", c.cid.String()),
				zap.String("local-member-id", c.localID.String()),
			)
		} else {
			plog.Panicf("failed to find local ID %s in cluster %s", c.localID.String(), c.cid.String())
		}
	}

Review comment (suggestion):
Note both versions are cluster versions. The second parameter name `lv` is misleading, although it's the same value as server local binary version in single node cluster.
```suggestion
func IsValidClusterVersionChange(verFrom *semver.Version, verTo *semver.Version, nextClusterVersionCompatible bool) bool {
```

Code after:
		return nil
	}
	lv := semver.Must(semver.NewVersion(version.Version))
	// only keep major.minor version for comparison against cluster version
	lv = &semver.Version{Major: lv.Major, Minor: lv.Minor}
	if !lv.LessThan(*cv) {
		return nil
	}
	// allow 3.4 server to join 3.5 cluster. Note the local data schema will
	// be automatically migrated to 3.4 if `--next-cluster-version-compatible`
	// is enabled (true). Users can also execute `etcdutl migrate` to migrate
	// the data before starting the server.
	oneMinorVersionDown := &semver.Version{Major: cv.Major, Minor: cv.Minor - 1}
	if !nextClusterVersionCompatible || !lv.Equal(*oneMinorVersionDown) {
		return fmt.Errorf("invalid downgrade; (current version: %s is lower than determined cluster version: %s).", version.Version, version.Cluster(cv.String()))
	}
	return nil
}

// IsValidClusterVersionChange checks the two scenario when version is valid to change:
// 1. Downgrade: cluster version is 1 minor version higher than local version,
// cluster version should change.
// 2. Cluster start: when not all members version are available, cluster version
// is set to MinVersion(3.0), when all members are at higher version, cluster version
// is lower than minimal server version, cluster version should change
func IsValidClusterVersionChange(verFrom *semver.Version, verTo *semver.Version, nextClusterVersionCompatible bool) bool {
	verFrom = &semver.Version{Major: verFrom.Major, Minor: verFrom.Minor}
	verTo = &semver.Version{Major: verTo.Major, Minor: verTo.Minor}

	if (nextClusterVersionCompatible && isValidDowngrade(verFrom, verTo)) || (verFrom.Major == verTo.Major && verFrom.LessThan(*verTo)) {
		return true
	}
	return false
}

// IsLocalMemberLearner returns if the local member is raft learner
func (c *RaftCluster) IsLocalMemberLearner() bool {
	c.Lock()
	defer c.Unlock()
	localMember, ok := c.members[c.localID]
	if !ok {
		if c.lg != nil {
			c.lg.Panic(
				"failed to find local ID in cluster members",
				zap.String("cluster-id", c.cid.String()),
				zap.String("local-member-id", c.localID.String()),
			)
		} else {
			plog.Panicf("failed to find local ID %s in cluster %s", c.localID.String(), c.cid.String())
		}
	}