Allow to use version labels in kubeadm upgrade apply
kubeadm upgrade apply now is able to utilize all possible combinations of version argument, including labels (latest, stable-1.8, ci/latest-1.9) as well as specific builds (v1.8.0-rc.1, ci/v1.9.0-alpha.1.123_01234567889) As side effect, specifying exact build to deploy from CI area is now also possible in kubeadm init command. Fixes: kubernetes/kubeadm#451
This commit is contained in:
		@@ -19,6 +19,7 @@ go_library(
 | 
			
		||||
        "//cmd/kubeadm/app/preflight:go_default_library",
 | 
			
		||||
        "//cmd/kubeadm/app/util:go_default_library",
 | 
			
		||||
        "//cmd/kubeadm/app/util/apiclient:go_default_library",
 | 
			
		||||
        "//cmd/kubeadm/app/util/config:go_default_library",
 | 
			
		||||
        "//cmd/kubeadm/app/util/dryrun:go_default_library",
 | 
			
		||||
        "//cmd/kubeadm/app/util/kubeconfig:go_default_library",
 | 
			
		||||
        "//pkg/api:go_default_library",
 | 
			
		||||
@@ -41,7 +42,6 @@ go_test(
 | 
			
		||||
    deps = [
 | 
			
		||||
        "//cmd/kubeadm/app/apis/kubeadm/v1alpha1:go_default_library",
 | 
			
		||||
        "//cmd/kubeadm/app/phases/upgrade:go_default_library",
 | 
			
		||||
        "//pkg/util/version:go_default_library",
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@ package upgrade
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
@@ -32,6 +31,7 @@ import (
 | 
			
		||||
	"k8s.io/kubernetes/cmd/kubeadm/app/phases/upgrade"
 | 
			
		||||
	kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
 | 
			
		||||
	"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
 | 
			
		||||
	configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
 | 
			
		||||
	dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/api"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/util/version"
 | 
			
		||||
@@ -123,6 +123,19 @@ func RunApply(flags *applyFlags) error {
 | 
			
		||||
	internalcfg := &kubeadmapi.MasterConfiguration{}
 | 
			
		||||
	api.Scheme.Convert(upgradeVars.cfg, internalcfg, nil)
 | 
			
		||||
 | 
			
		||||
	// Validate requested and validate actual version
 | 
			
		||||
	if err := configutil.NormalizeKubernetesVersion(internalcfg); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Use normalized version string in all following code.
 | 
			
		||||
	flags.newK8sVersionStr = internalcfg.KubernetesVersion
 | 
			
		||||
	k8sVer, err := version.ParseSemantic(flags.newK8sVersionStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to parse normalized version %q as a semantic version", flags.newK8sVersionStr)
 | 
			
		||||
	}
 | 
			
		||||
	flags.newK8sVersion = k8sVer
 | 
			
		||||
 | 
			
		||||
	// Enforce the version skew policies
 | 
			
		||||
	if err := EnforceVersionPolicies(flags, upgradeVars.versionGetter); err != nil {
 | 
			
		||||
		return fmt.Errorf("[upgrade/version] FATAL: %v", err)
 | 
			
		||||
@@ -170,15 +183,8 @@ func SetImplicitFlags(flags *applyFlags) error {
 | 
			
		||||
		flags.nonInteractiveMode = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	k8sVer, err := version.ParseSemantic(flags.newK8sVersionStr)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("couldn't parse version %q as a semantic version", flags.newK8sVersionStr)
 | 
			
		||||
	}
 | 
			
		||||
	flags.newK8sVersion = k8sVer
 | 
			
		||||
 | 
			
		||||
	// Automatically add the "v" prefix to the string representation in case it doesn't exist
 | 
			
		||||
	if !strings.HasPrefix(flags.newK8sVersionStr, "v") {
 | 
			
		||||
		flags.newK8sVersionStr = fmt.Sprintf("v%s", flags.newK8sVersionStr)
 | 
			
		||||
	if len(flags.newK8sVersionStr) == 0 {
 | 
			
		||||
		return fmt.Errorf("version string can't be empty")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -19,8 +19,6 @@ package upgrade
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"k8s.io/kubernetes/pkg/util/version"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
@@ -38,7 +36,6 @@ func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr:   "v1.8.0",
 | 
			
		||||
				newK8sVersion:      version.MustParseSemantic("v1.8.0"),
 | 
			
		||||
				dryRun:             false,
 | 
			
		||||
				force:              false,
 | 
			
		||||
				nonInteractiveMode: false,
 | 
			
		||||
@@ -53,7 +50,6 @@ func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr:   "v1.8.0",
 | 
			
		||||
				newK8sVersion:      version.MustParseSemantic("v1.8.0"),
 | 
			
		||||
				dryRun:             false,
 | 
			
		||||
				force:              false,
 | 
			
		||||
				nonInteractiveMode: true,
 | 
			
		||||
@@ -68,7 +64,6 @@ func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr:   "v1.8.0",
 | 
			
		||||
				newK8sVersion:      version.MustParseSemantic("v1.8.0"),
 | 
			
		||||
				dryRun:             true,
 | 
			
		||||
				force:              false,
 | 
			
		||||
				nonInteractiveMode: true,
 | 
			
		||||
@@ -83,7 +78,6 @@ func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr:   "v1.8.0",
 | 
			
		||||
				newK8sVersion:      version.MustParseSemantic("v1.8.0"),
 | 
			
		||||
				dryRun:             false,
 | 
			
		||||
				force:              true,
 | 
			
		||||
				nonInteractiveMode: true,
 | 
			
		||||
@@ -98,7 +92,6 @@ func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr:   "v1.8.0",
 | 
			
		||||
				newK8sVersion:      version.MustParseSemantic("v1.8.0"),
 | 
			
		||||
				dryRun:             true,
 | 
			
		||||
				force:              true,
 | 
			
		||||
				nonInteractiveMode: true,
 | 
			
		||||
@@ -113,7 +106,6 @@ func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr:   "v1.8.0",
 | 
			
		||||
				newK8sVersion:      version.MustParseSemantic("v1.8.0"),
 | 
			
		||||
				dryRun:             true,
 | 
			
		||||
				force:              true,
 | 
			
		||||
				nonInteractiveMode: true,
 | 
			
		||||
@@ -128,45 +120,6 @@ func TestSetImplicitFlags(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			errExpected: true,
 | 
			
		||||
		},
 | 
			
		||||
		{ // if the new version is invalid; it should error out
 | 
			
		||||
			flags: &applyFlags{
 | 
			
		||||
				newK8sVersionStr: "foo",
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr: "foo",
 | 
			
		||||
			},
 | 
			
		||||
			errExpected: true,
 | 
			
		||||
		},
 | 
			
		||||
		{ // if the new version is valid but without the "v" prefix; it parse and prepend v
 | 
			
		||||
			flags: &applyFlags{
 | 
			
		||||
				newK8sVersionStr: "1.8.0",
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr: "v1.8.0",
 | 
			
		||||
				newK8sVersion:    version.MustParseSemantic("v1.8.0"),
 | 
			
		||||
			},
 | 
			
		||||
			errExpected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{ // valid version should succeed
 | 
			
		||||
			flags: &applyFlags{
 | 
			
		||||
				newK8sVersionStr: "v1.8.1",
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr: "v1.8.1",
 | 
			
		||||
				newK8sVersion:    version.MustParseSemantic("v1.8.1"),
 | 
			
		||||
			},
 | 
			
		||||
			errExpected: false,
 | 
			
		||||
		},
 | 
			
		||||
		{ // valid version should succeed
 | 
			
		||||
			flags: &applyFlags{
 | 
			
		||||
				newK8sVersionStr: "1.8.0-alpha.3",
 | 
			
		||||
			},
 | 
			
		||||
			expectedFlags: applyFlags{
 | 
			
		||||
				newK8sVersionStr: "v1.8.0-alpha.3",
 | 
			
		||||
				newK8sVersion:    version.MustParseSemantic("v1.8.0-alpha.3"),
 | 
			
		||||
			},
 | 
			
		||||
			errExpected: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, rt := range tests {
 | 
			
		||||
		actualErr := SetImplicitFlags(rt.flags)
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ func NewDaemonSetPrepuller(client clientset.Interface, waiter apiclient.Waiter,
 | 
			
		||||
 | 
			
		||||
// CreateFunc creates a DaemonSet for making the image available on every relevant node
 | 
			
		||||
func (d *DaemonSetPrepuller) CreateFunc(component string) error {
 | 
			
		||||
	image := images.GetCoreImage(component, d.cfg.ImageRepository, d.cfg.KubernetesVersion, d.cfg.UnifiedControlPlaneImage)
 | 
			
		||||
	image := images.GetCoreImage(component, d.cfg.GetControlPlaneImageRepository(), d.cfg.KubernetesVersion, d.cfg.UnifiedControlPlaneImage)
 | 
			
		||||
	ds := buildPrePullDaemonSet(component, image)
 | 
			
		||||
 | 
			
		||||
	// Create the DaemonSet in the API Server
 | 
			
		||||
 
 | 
			
		||||
@@ -45,25 +45,11 @@ func SetInitDynamicDefaults(cfg *kubeadmapi.MasterConfiguration) error {
 | 
			
		||||
	}
 | 
			
		||||
	cfg.API.AdvertiseAddress = ip.String()
 | 
			
		||||
 | 
			
		||||
	// Requested version is automatic CI build, thus use KubernetesCI Image Repository for core images
 | 
			
		||||
	if kubeadmutil.KubernetesIsCIVersion(cfg.KubernetesVersion) {
 | 
			
		||||
		cfg.CIImageRepository = kubeadmconstants.DefaultCIImageRepository
 | 
			
		||||
	}
 | 
			
		||||
	// Validate version argument
 | 
			
		||||
	ver, err := kubeadmutil.KubernetesReleaseVersion(cfg.KubernetesVersion)
 | 
			
		||||
	// Resolve possible version labels and validate version string
 | 
			
		||||
	err = NormalizeKubernetesVersion(cfg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	cfg.KubernetesVersion = ver
 | 
			
		||||
 | 
			
		||||
	// Parse the given kubernetes version and make sure it's higher than the lowest supported
 | 
			
		||||
	k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("couldn't parse kubernetes version %q: %v", cfg.KubernetesVersion, err)
 | 
			
		||||
	}
 | 
			
		||||
	if k8sVersion.LessThan(kubeadmconstants.MinimumControlPlaneVersion) {
 | 
			
		||||
		return fmt.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s", kubeadmconstants.MinimumControlPlaneVersion.String(), cfg.KubernetesVersion)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cfg.Token == "" {
 | 
			
		||||
		var err error
 | 
			
		||||
@@ -123,3 +109,30 @@ func ConfigFileAndDefaultsToInternalConfig(cfgPath string, defaultversionedcfg *
 | 
			
		||||
	}
 | 
			
		||||
	return internalcfg, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NormalizeKubernetesVersion resolves version labels, sets alternative
 | 
			
		||||
// image registry if requested for CI builds, and validates minimal
 | 
			
		||||
// version that kubeadm supports.
 | 
			
		||||
func NormalizeKubernetesVersion(cfg *kubeadmapi.MasterConfiguration) error {
 | 
			
		||||
	// Requested version is automatic CI build, thus use KubernetesCI Image Repository for core images
 | 
			
		||||
	if kubeadmutil.KubernetesIsCIVersion(cfg.KubernetesVersion) {
 | 
			
		||||
		cfg.CIImageRepository = kubeadmconstants.DefaultCIImageRepository
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Parse and validate the version argument and resolve possible CI version labels
 | 
			
		||||
	ver, err := kubeadmutil.KubernetesReleaseVersion(cfg.KubernetesVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	cfg.KubernetesVersion = ver
 | 
			
		||||
 | 
			
		||||
	// Parse the given kubernetes version and make sure it's higher than the lowest supported
 | 
			
		||||
	k8sVersion, err := version.ParseSemantic(cfg.KubernetesVersion)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("couldn't parse kubernetes version %q: %v", cfg.KubernetesVersion, err)
 | 
			
		||||
	}
 | 
			
		||||
	if k8sVersion.LessThan(kubeadmconstants.MinimumControlPlaneVersion) {
 | 
			
		||||
		return fmt.Errorf("this version of kubeadm only supports deploying clusters with the control plane version >= %s. Current version: %s", kubeadmconstants.MinimumControlPlaneVersion.String(), cfg.KubernetesVersion)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -49,17 +49,22 @@ var (
 | 
			
		||||
//  latest-1    (latest release in 1.x, including alpha/beta)
 | 
			
		||||
//  latest-1.0  (and similarly 1.1, 1.2, 1.3, ...)
 | 
			
		||||
func KubernetesReleaseVersion(version string) (string, error) {
 | 
			
		||||
	if kubeReleaseRegex.MatchString(version) {
 | 
			
		||||
		if strings.HasPrefix(version, "v") {
 | 
			
		||||
			return version, nil
 | 
			
		||||
		}
 | 
			
		||||
		return "v" + version, nil
 | 
			
		||||
	ver := normalizedBuildVersion(version)
 | 
			
		||||
	if len(ver) != 0 {
 | 
			
		||||
		return ver, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	bucketURL, versionLabel, err := splitVersion(version)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// revalidate, if exact build from e.g. CI bucket requested.
 | 
			
		||||
	ver = normalizedBuildVersion(versionLabel)
 | 
			
		||||
	if len(ver) != 0 {
 | 
			
		||||
		return ver, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if kubeReleaseLabelRegex.MatchString(versionLabel) {
 | 
			
		||||
		url := fmt.Sprintf("%s/%s.txt", bucketURL, versionLabel)
 | 
			
		||||
		body, err := fetchFromURL(url)
 | 
			
		||||
@@ -92,6 +97,18 @@ func KubernetesIsCIVersion(version string) bool {
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Internal helper: returns normalized build version (with "v" prefix if needed)
 | 
			
		||||
// If input doesn't match known version pattern, returns empty string.
 | 
			
		||||
func normalizedBuildVersion(version string) string {
 | 
			
		||||
	if kubeReleaseRegex.MatchString(version) {
 | 
			
		||||
		if strings.HasPrefix(version, "v") {
 | 
			
		||||
			return version
 | 
			
		||||
		}
 | 
			
		||||
		return "v" + version
 | 
			
		||||
	}
 | 
			
		||||
	return ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Internal helper: split version parts,
 | 
			
		||||
// Return base URL and cleaned-up version
 | 
			
		||||
func splitVersion(version string) (string, string, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -220,6 +220,8 @@ func TestKubernetesIsCIVersion(t *testing.T) {
 | 
			
		||||
		// CI builds
 | 
			
		||||
		{"ci/latest-1", true},
 | 
			
		||||
		{"ci-cross/latest", true},
 | 
			
		||||
		{"ci/v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
 | 
			
		||||
		{"ci-cross/v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
@@ -231,3 +233,58 @@ func TestKubernetesIsCIVersion(t *testing.T) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate KubernetesReleaseVersion but with bucket prefixes
 | 
			
		||||
func TestCIBuildVersion(t *testing.T) {
 | 
			
		||||
	type T struct {
 | 
			
		||||
		input    string
 | 
			
		||||
		expected string
 | 
			
		||||
		valid    bool
 | 
			
		||||
	}
 | 
			
		||||
	cases := []T{
 | 
			
		||||
		// Official releases
 | 
			
		||||
		{"v1.7.0", "v1.7.0", true},
 | 
			
		||||
		{"release/v1.8.0", "v1.8.0", true},
 | 
			
		||||
		{"1.4.0-beta.0", "v1.4.0-beta.0", true},
 | 
			
		||||
		{"release/0invalid", "", false},
 | 
			
		||||
		// CI or custom builds
 | 
			
		||||
		{"ci/v1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
 | 
			
		||||
		{"ci-cross/v1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
 | 
			
		||||
		{"ci/1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
 | 
			
		||||
		{"ci-cross/1.9.0-alpha.1.123+acbcbfd53bfa0a", "v1.9.0-alpha.1.123+acbcbfd53bfa0a", true},
 | 
			
		||||
		{"ci/0invalid", "", false},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		ver, err := KubernetesReleaseVersion(tc.input)
 | 
			
		||||
		t.Logf("Input: %q. Result: %q, Error: %v", tc.input, ver, err)
 | 
			
		||||
		switch {
 | 
			
		||||
		case err != nil && tc.valid:
 | 
			
		||||
			t.Errorf("KubernetesReleaseVersion: unexpected error for input %q. Error: %v", tc.input, err)
 | 
			
		||||
		case err == nil && !tc.valid:
 | 
			
		||||
			t.Errorf("KubernetesReleaseVersion: error expected for input %q, but result is %q", tc.input, ver)
 | 
			
		||||
		case ver != tc.expected:
 | 
			
		||||
			t.Errorf("KubernetesReleaseVersion: unexpected result for input %q. Expected: %q Actual: %q", tc.input, tc.expected, ver)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNormalizedBuildVersionVersion(t *testing.T) {
 | 
			
		||||
	type T struct {
 | 
			
		||||
		input    string
 | 
			
		||||
		expected string
 | 
			
		||||
	}
 | 
			
		||||
	cases := []T{
 | 
			
		||||
		{"v1.7.0", "v1.7.0"},
 | 
			
		||||
		{"v1.8.0-alpha.2.1231+afabd012389d53a", "v1.8.0-alpha.2.1231+afabd012389d53a"},
 | 
			
		||||
		{"1.7.0", "v1.7.0"},
 | 
			
		||||
		{"unknown-1", ""},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tc := range cases {
 | 
			
		||||
		output := normalizedBuildVersion(tc.input)
 | 
			
		||||
		if output != tc.expected {
 | 
			
		||||
			t.Errorf("normalizedBuildVersion: unexpected output %q for input %q. Expected: %q", output, tc.input, tc.expected)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user