Merge pull request #63201 from chuckha/offline-plan

Automatic merge from submit-queue (batch tested with PRs 63138, 63091, 63201, 63341). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Enable bypassing online checks in kubeadm upgrade plan

Signed-off-by: Chuck Ha <ha.chuck@gmail.com>

**What this PR does / why we need it**:

This PR makes `kubeadm upgrade plan` a little nicer to use in an air gapped environment. `kubeadm upgrade plan` now accepts a version and returns that instead of checking the internet.

**Which issue(s) this PR fixes**:

Fixes kubernetes/kubeadm#698

**Special notes for your reviewer**:

I also cleaned up the tests for this section of code by adding formal names for table tests and using `t.Run`.

**Release note**:

```release-note
`kubeadm upgrade plan` now accepts a version which improves the UX nicer in air-gapped environments.
```
This commit is contained in:
Kubernetes Submit Queue 2018-05-01 16:00:15 -07:00 committed by GitHub
commit 18b545f67c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 153 additions and 27 deletions

View File

@ -87,7 +87,7 @@ func enforceRequirements(flags *cmdUpgradeFlags, dryRun bool, newK8sVersion stri
client: client,
cfg: cfg,
// Use a real version getter interface that queries the API server, the kubeadm client and the Kubernetes CI system for latest versions
versionGetter: upgrade.NewKubeVersionGetter(client, os.Stdout),
versionGetter: upgrade.NewOfflineVersionGetter(upgrade.NewKubeVersionGetter(client, os.Stdout), newK8sVersion),
// Use the waiter conditionally based on the dryrunning variable
waiter: getWaiter(dryRun, client),
}, nil

View File

@ -34,12 +34,21 @@ import (
etcdutil "k8s.io/kubernetes/cmd/kubeadm/app/util/etcd"
)
type planFlags struct {
newK8sVersionStr string
parent *cmdUpgradeFlags
}
// NewCmdPlan returns the cobra command for `kubeadm upgrade plan`
func NewCmdPlan(parentFlags *cmdUpgradeFlags) *cobra.Command {
flags := &planFlags{
parent: parentFlags,
}
cmd := &cobra.Command{
Use: "plan",
Short: "Check which versions are available to upgrade to and validate whether your current cluster is upgradeable.",
Run: func(_ *cobra.Command, _ []string) {
Use: "plan [version] [flags]",
Short: "Check which versions are available to upgrade to and validate whether your current cluster is upgradeable. To skip the internet check, pass in the optional [version] parameter.",
Run: func(_ *cobra.Command, args []string) {
var err error
parentFlags.ignorePreflightErrorsSet, err = validation.ValidateIgnorePreflightErrors(parentFlags.ignorePreflightErrors, parentFlags.skipPreFlight)
kubeadmutil.CheckErr(err)
@ -47,7 +56,22 @@ func NewCmdPlan(parentFlags *cmdUpgradeFlags) *cobra.Command {
err = runPreflightChecks(parentFlags.ignorePreflightErrorsSet)
kubeadmutil.CheckErr(err)
err = RunPlan(parentFlags)
// If the version is specified in config file, pick up that value.
if parentFlags.cfgPath != "" {
glog.V(1).Infof("fetching configuration from file", parentFlags.cfgPath)
cfg, err := upgrade.FetchConfigurationFromFile(parentFlags.cfgPath)
kubeadmutil.CheckErr(err)
if cfg.KubernetesVersion != "" {
flags.newK8sVersionStr = cfg.KubernetesVersion
}
}
// If option was specified in both args and config file, args will overwrite the config file.
if len(args) == 1 {
flags.newK8sVersionStr = args[0]
}
err = RunPlan(flags)
kubeadmutil.CheckErr(err)
},
}
@ -56,11 +80,11 @@ func NewCmdPlan(parentFlags *cmdUpgradeFlags) *cobra.Command {
}
// RunPlan takes care of outputting available versions to upgrade to for the user
func RunPlan(parentFlags *cmdUpgradeFlags) error {
func RunPlan(flags *planFlags) error {
// Start with the basics, verify that the cluster is healthy, build a client and a versionGetter. Never dry-run when planning.
glog.V(1).Infof("[upgrade/plan] verifying health of cluster")
glog.V(1).Infof("[upgrade/plan] retrieving configuration from cluster")
upgradeVars, err := enforceRequirements(parentFlags, false, "")
upgradeVars, err := enforceRequirements(flags.parent, false, flags.newK8sVersionStr)
if err != nil {
return err
}
@ -77,7 +101,7 @@ func RunPlan(parentFlags *cmdUpgradeFlags) error {
// Compute which upgrade possibilities there are
glog.V(1).Infof("[upgrade/plan] computing upgrade possibilities")
availUpgrades, err := upgrade.GetAvailableUpgrades(upgradeVars.versionGetter, parentFlags.allowExperimentalUpgrades, parentFlags.allowRCUpgrades, etcdClient, upgradeVars.cfg.FeatureGates)
availUpgrades, err := upgrade.GetAvailableUpgrades(upgradeVars.versionGetter, flags.parent.allowExperimentalUpgrades, flags.parent.allowRCUpgrades, etcdClient, upgradeVars.cfg.FeatureGates)
if err != nil {
return fmt.Errorf("[upgrade/versions] FATAL: %v", err)
}

View File

@ -79,12 +79,14 @@ func (f fakeEtcdCluster) WaitForStatus(delay time.Duration, retries int, retryIn
func TestGetAvailableUpgrades(t *testing.T) {
featureGates := make(map[string]bool)
tests := []struct {
vg *fakeVersionGetter
name string
vg VersionGetter
expectedUpgrades []Upgrade
allowExperimental, allowRCs bool
errExpected bool
}{
{ // no action needed, already up-to-date
{
name: "no action needed, already up-to-date",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.3",
kubeletVersion: "v1.9.3",
@ -97,7 +99,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: false,
errExpected: false,
},
{ // simple patch version upgrade
{
name: "simple patch version upgrade",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.1",
kubeletVersion: "v1.9.1", // the kubelet are on the same version as the control plane
@ -129,7 +132,41 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: false,
errExpected: false,
},
{ // minor version upgrade only
{
name: "no version provided to offline version getter does not change behavior",
vg: NewOfflineVersionGetter(&fakeVersionGetter{
clusterVersion: "v1.9.1",
kubeletVersion: "v1.9.1", // the kubelet are on the same version as the control plane
kubeadmVersion: "v1.9.2",
stablePatchVersion: "v1.9.3",
stableVersion: "v1.9.3",
}, ""),
expectedUpgrades: []Upgrade{
{
Description: "version in the v1.9 series",
Before: ClusterState{
KubeVersion: "v1.9.1",
KubeletVersions: map[string]uint16{
"v1.9.1": 1,
},
KubeadmVersion: "v1.9.2",
DNSVersion: "1.14.10",
EtcdVersion: "3.1.12",
},
After: ClusterState{
KubeVersion: "v1.9.3",
KubeadmVersion: "v1.9.3",
DNSVersion: "1.14.10",
EtcdVersion: "3.1.12",
},
},
},
allowExperimental: false,
errExpected: false,
},
{
name: "minor version upgrade only",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.1",
kubeletVersion: "v1.9.1", // the kubelet are on the same version as the control plane
@ -161,7 +198,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: false,
errExpected: false,
},
{ // both minor version upgrade and patch version upgrade available
{
name: "both minor version upgrade and patch version upgrade available",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.3",
kubeletVersion: "v1.9.3", // the kubelet are on the same version as the control plane
@ -211,7 +249,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: false,
errExpected: false,
},
{ // allow experimental upgrades, but no upgrade available
{
name: "allow experimental upgrades, but no upgrade available",
vg: &fakeVersionGetter{
clusterVersion: "v1.10.0-alpha.2",
kubeletVersion: "v1.9.5",
@ -225,7 +264,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: true,
errExpected: false,
},
{ // upgrade to an unstable version should be supported
{
name: "upgrade to an unstable version should be supported",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.5",
kubeletVersion: "v1.9.5",
@ -258,7 +298,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: true,
errExpected: false,
},
{ // upgrade from an unstable version to an unstable version should be supported
{
name: "upgrade from an unstable version to an unstable version should be supported",
vg: &fakeVersionGetter{
clusterVersion: "v1.10.0-alpha.1",
kubeletVersion: "v1.9.5",
@ -291,7 +332,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: true,
errExpected: false,
},
{ // v1.X.0-alpha.0 should be ignored
{
name: "v1.X.0-alpha.0 should be ignored",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.5",
kubeletVersion: "v1.9.5",
@ -325,7 +367,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: true,
errExpected: false,
},
{ // upgrade to an RC version should be supported
{
name: "upgrade to an RC version should be supported",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.5",
kubeletVersion: "v1.9.5",
@ -359,7 +402,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowRCs: true,
errExpected: false,
},
{ // it is possible (but very uncommon) that the latest version from the previous branch is an rc and the current latest version is alpha.0. In that case, show the RC
{
name: "it is possible (but very uncommon) that the latest version from the previous branch is an rc and the current latest version is alpha.0. In that case, show the RC",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.5",
kubeletVersion: "v1.9.5",
@ -393,7 +437,8 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: true,
errExpected: false,
},
{ // upgrade to an RC version should be supported. There may also be an even newer unstable version.
{
name: "upgrade to an RC version should be supported. There may also be an even newer unstable version.",
vg: &fakeVersionGetter{
clusterVersion: "v1.9.5",
kubeletVersion: "v1.9.5",
@ -446,19 +491,49 @@ func TestGetAvailableUpgrades(t *testing.T) {
allowExperimental: true,
errExpected: false,
},
{
name: "offline version getter",
vg: NewOfflineVersionGetter(&fakeVersionGetter{
clusterVersion: "v1.10.1",
kubeletVersion: "v1.10.0",
kubeadmVersion: "v1.10.1",
}, "v1.11.1"),
expectedUpgrades: []Upgrade{
{
Description: "version in the v1.1 series",
Before: ClusterState{
KubeVersion: "v1.10.1",
KubeletVersions: map[string]uint16{
"v1.10.0": 1,
},
KubeadmVersion: "v1.10.1",
DNSVersion: "1.14.10",
EtcdVersion: "3.1.12",
},
After: ClusterState{
KubeVersion: "v1.11.1",
KubeadmVersion: "v1.11.1",
DNSVersion: "1.14.10",
EtcdVersion: "3.2.18",
},
},
},
},
}
// Instantiating a fake etcd cluster for being able to get etcd version for a corresponding
// kubernetes release.
testCluster := fakeEtcdCluster{}
for _, rt := range tests {
actualUpgrades, actualErr := GetAvailableUpgrades(rt.vg, rt.allowExperimental, rt.allowRCs, testCluster, featureGates)
if !reflect.DeepEqual(actualUpgrades, rt.expectedUpgrades) {
t.Errorf("failed TestGetAvailableUpgrades\n\texpected upgrades: %v\n\tgot: %v", rt.expectedUpgrades, actualUpgrades)
}
if (actualErr != nil) != rt.errExpected {
t.Errorf("failed TestGetAvailableUpgrades\n\texpected error: %t\n\tgot error: %t", rt.errExpected, (actualErr != nil))
}
t.Run(rt.name, func(t *testing.T) {
actualUpgrades, actualErr := GetAvailableUpgrades(rt.vg, rt.allowExperimental, rt.allowRCs, testCluster, featureGates)
if !reflect.DeepEqual(actualUpgrades, rt.expectedUpgrades) {
t.Errorf("failed TestGetAvailableUpgrades\n\texpected upgrades: %v\n\tgot: %v", rt.expectedUpgrades, actualUpgrades)
}
if (actualErr != nil) != rt.errExpected {
t.Errorf("failed TestGetAvailableUpgrades\n\texpected error: %t\n\tgot error: %t", rt.errExpected, (actualErr != nil))
}
})
}
}

View File

@ -122,3 +122,30 @@ func computeKubeletVersions(nodes []v1.Node) map[string]uint16 {
}
return kubeletVersions
}
// OfflineVersionGetter will use the version provided or
type OfflineVersionGetter struct {
VersionGetter
version string
}
// NewOfflineVersionGetter wraps a VersionGetter and skips online communication if default information is supplied.
// Version can be "" and the behavior will be identical to the versionGetter passed in.
func NewOfflineVersionGetter(versionGetter VersionGetter, version string) VersionGetter {
return &OfflineVersionGetter{
VersionGetter: versionGetter,
version: version,
}
}
// VersionFromCILabel will return the version that was passed into the struct
func (o *OfflineVersionGetter) VersionFromCILabel(ciVersionLabel, description string) (string, *versionutil.Version, error) {
if o.version == "" {
return o.VersionGetter.VersionFromCILabel(ciVersionLabel, description)
}
ver, err := versionutil.ParseSemantic(o.version)
if err != nil {
return "", nil, fmt.Errorf("Couldn't parse version %s: %v", description, err)
}
return o.version, ver, nil
}