kubeadm: dynamically populate the current/minimum k8s versions

Kubeadm requires manual version updates of its current supported k8s
control plane version and minimally supported k8s control plane and
kubelet versions every release cycle.

To avoid that, in constants.go:
- Add the helper function getSkewedKubernetesVersion() that can be
used to retrieve a MAJOR.(MINOR+n).0 version of k8s. It currently
uses the kubeadm version populated in "component-base/version" during
the kubeadm build process.
- Use the function to set existing version constants (variables).

Update util/config/common.go#NormalizeKubernetesVersion() to
tolerate the case where a k8s version in the ClusterConfiguration
is too old for the kubeadm binary to use during code freeze.

Include unit tests for the new utilities.
This commit is contained in:
Lubomir I. Ivanov 2021-07-29 00:13:26 +03:00
parent 9ff3b7e744
commit 207ffa7bdc
4 changed files with 198 additions and 5 deletions

View File

@ -29,7 +29,9 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/util/wait"
apimachineryversion "k8s.io/apimachinery/pkg/version"
bootstrapapi "k8s.io/cluster-bootstrap/token/api"
componentversion "k8s.io/component-base/version"
utilnet "k8s.io/utils/net"
"github.com/pkg/errors"
@ -448,13 +450,13 @@ var (
ControlPlaneComponents = []string{KubeAPIServer, KubeControllerManager, KubeScheduler}
// MinimumControlPlaneVersion specifies the minimum control plane version kubeadm can deploy
MinimumControlPlaneVersion = version.MustParseSemantic("v1.21.0")
MinimumControlPlaneVersion = getSkewedKubernetesVersion(-1)
// MinimumKubeletVersion specifies the minimum version of kubelet which kubeadm supports
MinimumKubeletVersion = version.MustParseSemantic("v1.21.0")
MinimumKubeletVersion = getSkewedKubernetesVersion(-1)
// CurrentKubernetesVersion specifies current Kubernetes version supported by kubeadm
CurrentKubernetesVersion = version.MustParseSemantic("v1.22.0")
CurrentKubernetesVersion = getSkewedKubernetesVersion(0)
// SupportedEtcdVersion lists officially supported etcd versions with corresponding Kubernetes releases
SupportedEtcdVersion = map[uint8]string{
@ -483,8 +485,52 @@ var (
Factor: 1.0,
Jitter: 0.1,
}
// defaultKubernetesVersionForTests is the default version used for unit tests.
// The MINOR should be at least 3 as some tests subtract 3 from the MINOR version.
defaultKubernetesVersionForTests = version.MustParseSemantic("v1.3.0")
)
// isRunningInTest can be used to determine if the code in this file is being run in a test.
func isRunningInTest() bool {
return strings.HasSuffix(os.Args[0], ".test")
}
// getSkewedKubernetesVersion returns the current MAJOR.(MINOR+n).0 Kubernetes version with a skew of 'n'
// It uses the kubeadm version provided by the 'component-base/version' package. This version must be populated
// by passing linker flags during the kubeadm build process. If the version is empty, assume that kubeadm
// was either build incorrectly or this code is running in unit tests.
func getSkewedKubernetesVersion(n int) *version.Version {
versionInfo := componentversion.Get()
ver := getSkewedKubernetesVersionImpl(&versionInfo, n, isRunningInTest)
if ver == nil {
panic("kubeadm is not build properly using 'make ...': missing component version information")
}
return ver
}
func getSkewedKubernetesVersionImpl(versionInfo *apimachineryversion.Info, n int, isRunningInTestFunc func() bool) *version.Version {
// TODO: update if the kubeadm version gets decoupled from the Kubernetes version.
// This would require keeping track of the supported skew in a table.
// More changes would be required if the kubelet version one day decouples from that of Kubernetes.
var ver *version.Version
if len(versionInfo.Major) == 0 {
if isRunningInTestFunc() {
ver = defaultKubernetesVersionForTests // An arbitrary version for testing purposes
} else {
// If this is not running in tests assume that the kubeadm binary is not build properly
return nil
}
} else {
ver = version.MustParseSemantic(versionInfo.GitVersion)
}
// Append the MINOR version skew.
// TODO: handle the case of Kubernetes moving to v2.0 or having MAJOR version updates in the future.
// This would require keeping track (in a table) of the last MINOR for a particular MAJOR.
minor := uint(int(ver.Minor()) + n)
return version.MustParseSemantic(fmt.Sprintf("v%d.%d.0", ver.Major(), minor))
}
// EtcdSupportedVersion returns officially supported version of etcd for a specific Kubernetes release
// If passed version is not in the given list, the function returns the nearest version with a warning
func EtcdSupportedVersion(supportedEtcdVersion map[uint8]string, versionString string) (etcdVersion *version.Version, warning, err error) {

View File

@ -21,6 +21,7 @@ import (
"testing"
"k8s.io/apimachinery/pkg/util/version"
apimachineryversion "k8s.io/apimachinery/pkg/version"
)
func TestGetStaticPodDirectory(t *testing.T) {
@ -237,3 +238,61 @@ func TestGetKubernetesServiceCIDR(t *testing.T) {
})
}
}
func TestGetSkewedKubernetesVersionImpl(t *testing.T) {
tests := []struct {
name string
versionInfo *apimachineryversion.Info
n int
isRunningInTestFunc func() bool
expectedResult *version.Version
}{
{
name: "invalid versionInfo; running in test",
versionInfo: &apimachineryversion.Info{},
expectedResult: defaultKubernetesVersionForTests,
},
{
name: "invalid versionInfo; not running in test",
versionInfo: &apimachineryversion.Info{},
isRunningInTestFunc: func() bool { return false },
expectedResult: nil,
},
{
name: "valid skew of -1",
versionInfo: &apimachineryversion.Info{Major: "1", GitVersion: "v1.23.0"},
n: -1,
expectedResult: version.MustParseSemantic("v1.22.0"),
},
{
name: "valid skew of 0",
versionInfo: &apimachineryversion.Info{Major: "1", GitVersion: "v1.23.0"},
n: 0,
expectedResult: version.MustParseSemantic("v1.23.0"),
},
{
name: "valid skew of +1",
versionInfo: &apimachineryversion.Info{Major: "1", GitVersion: "v1.23.0"},
n: 1,
expectedResult: version.MustParseSemantic("v1.24.0"),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if tc.isRunningInTestFunc == nil {
tc.isRunningInTestFunc = func() bool { return true }
}
result := getSkewedKubernetesVersionImpl(tc.versionInfo, tc.n, tc.isRunningInTestFunc)
if (tc.expectedResult == nil) != (result == nil) {
t.Errorf("expected result: %v, got: %v", tc.expectedResult, result)
}
if result == nil {
return
}
if cmp, _ := result.Compare(tc.expectedResult.String()); cmp != 0 {
t.Errorf("expected result: %v, got %v", tc.expectedResult, result)
}
})
}
}

View File

@ -32,6 +32,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
netutil "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/version"
apimachineryversion "k8s.io/apimachinery/pkg/version"
componentversion "k8s.io/component-base/version"
"k8s.io/klog/v2"
"github.com/pkg/errors"
@ -102,8 +104,23 @@ func NormalizeKubernetesVersion(cfg *kubeadmapi.ClusterConfiguration) error {
if err != nil {
return errors.Wrapf(err, "couldn't parse Kubernetes version %q", cfg.KubernetesVersion)
}
if k8sVersion.LessThan(constants.MinimumControlPlaneVersion) {
return errors.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s", constants.MinimumControlPlaneVersion.String(), cfg.KubernetesVersion)
// During the k8s release process, a kubeadm version in the main branch could be 1.23.0-pre,
// while the 1.22.0 version is not released yet. The MinimumControlPlaneVersion validation
// in such a case will not pass, since the value of MinimumControlPlaneVersion would be
// calculated as kubeadm version - 1 (1.22) and k8sVersion would still be at 1.21.x
// (fetched from the 'stable' marker). Handle this case by only showing a warning.
mcpVersion := constants.MinimumControlPlaneVersion
versionInfo := componentversion.Get()
if isKubeadmPrereleaseVersion(&versionInfo, k8sVersion, mcpVersion) {
klog.V(1).Infof("WARNING: tolerating control plane version %s, assuming that k8s version %s is not released yet",
cfg.KubernetesVersion, mcpVersion)
return nil
}
// If not a pre-release version, handle the validation normally.
if k8sVersion.LessThan(mcpVersion) {
return errors.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s",
mcpVersion, cfg.KubernetesVersion)
}
return nil
}
@ -204,3 +221,20 @@ func MigrateOldConfig(oldConfig []byte) ([]byte, error) {
return bytes.Join(newConfig, []byte(constants.YAMLDocumentSeparator)), nil
}
// isKubeadmPrereleaseVersion returns true if the kubeadm version is a pre-release version and
// the minimum control plane version is N+2 MINOR version of the given k8sVersion.
func isKubeadmPrereleaseVersion(versionInfo *apimachineryversion.Info, k8sVersion, mcpVersion *version.Version) bool {
if len(versionInfo.Major) != 0 { // Make sure the component version is populated
kubeadmVersion := version.MustParseSemantic(versionInfo.String())
if len(kubeadmVersion.PreRelease()) != 0 { // Only handle this if the kubeadm binary is a pre-release
// After incrementing the k8s MINOR version by one, if this version is equal or greater than the
// MCP version, return true.
v := k8sVersion.WithMinor(k8sVersion.Minor() + 1)
if comp, _ := v.Compare(mcpVersion.String()); comp != -1 {
return true
}
}
}
return false
}

View File

@ -26,6 +26,8 @@ import (
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/version"
apimachineryversion "k8s.io/apimachinery/pkg/version"
"github.com/lithammer/dedent"
)
@ -397,3 +399,55 @@ func TestMigrateOldConfigFromFile(t *testing.T) {
})
}
}
func TestIsKubeadmPrereleaseVersion(t *testing.T) {
validVersionInfo := &apimachineryversion.Info{Major: "1", GitVersion: "v1.23.0-alpha.1"}
tests := []struct {
name string
versionInfo *apimachineryversion.Info
k8sVersion *version.Version
mcpVersion *version.Version
expectedResult bool
}{
{
name: "invalid versionInfo",
versionInfo: &apimachineryversion.Info{},
expectedResult: false,
},
{
name: "kubeadm is not a prerelease version",
versionInfo: &apimachineryversion.Info{Major: "1", GitVersion: "v1.23.0"},
expectedResult: false,
},
{
name: "mcpVersion is equal to k8sVersion",
versionInfo: validVersionInfo,
k8sVersion: version.MustParseSemantic("v1.21.0"),
mcpVersion: version.MustParseSemantic("v1.21.0"),
expectedResult: true,
},
{
name: "k8sVersion is 1 MINOR version older than mcpVersion",
versionInfo: validVersionInfo,
k8sVersion: version.MustParseSemantic("v1.21.0"),
mcpVersion: version.MustParseSemantic("v1.22.0"),
expectedResult: true,
},
{
name: "k8sVersion is 2 MINOR versions older than mcpVersion",
versionInfo: validVersionInfo,
k8sVersion: version.MustParseSemantic("v1.21.0"),
mcpVersion: version.MustParseSemantic("v1.23.0"),
expectedResult: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := isKubeadmPrereleaseVersion(tc.versionInfo, tc.k8sVersion, tc.mcpVersion)
if result != tc.expectedResult {
t.Errorf("expected result: %v, got %v", tc.expectedResult, result)
}
})
}
}