blob: 248f7db564ec2c062ef4f4f60bfab9986eca9316 [file] [log] [blame]
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package upgrade
import (
"fmt"
"os"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/util/version"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/klog"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiv1beta1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta1"
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/validation"
cmdutil "k8s.io/kubernetes/cmd/kubeadm/app/cmd/util"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/features"
"k8s.io/kubernetes/cmd/kubeadm/app/phases/controlplane"
"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"
etcdutil "k8s.io/kubernetes/cmd/kubeadm/app/util/etcd"
)
const (
defaultImagePullTimeout = 15 * time.Minute
)
// applyFlags holds the information about the flags that can be passed to apply
type applyFlags struct {
*applyPlanFlags
nonInteractiveMode bool
force bool
dryRun bool
etcdUpgrade bool
criSocket string
newK8sVersionStr string
newK8sVersion *version.Version
imagePullTimeout time.Duration
}
// SessionIsInteractive returns true if the session is of an interactive type (the default, can be opted out of with -y, -f or --dry-run)
func (f *applyFlags) SessionIsInteractive() bool {
return !f.nonInteractiveMode
}
// NewCmdApply returns the cobra command for `kubeadm upgrade apply`
func NewCmdApply(apf *applyPlanFlags) *cobra.Command {
flags := &applyFlags{
applyPlanFlags: apf,
imagePullTimeout: defaultImagePullTimeout,
etcdUpgrade: true,
criSocket: kubeadmapiv1beta1.DefaultCRISocket,
}
cmd := &cobra.Command{
Use: "apply [version]",
DisableFlagsInUseLine: true,
Short: "Upgrade your Kubernetes cluster to the specified version.",
Run: func(cmd *cobra.Command, args []string) {
var err error
flags.ignorePreflightErrorsSet, err = validation.ValidateIgnorePreflightErrors(flags.ignorePreflightErrors)
kubeadmutil.CheckErr(err)
// Ensure the user is root
klog.V(1).Infof("running preflight checks")
err = runPreflightChecks(flags.ignorePreflightErrorsSet)
kubeadmutil.CheckErr(err)
// If the version is specified in config file, pick up that value.
if flags.cfgPath != "" {
klog.V(1).Infof("fetching configuration from file %s", flags.cfgPath)
// Note that cfg isn't preserved here, it's just an one-off to populate flags.newK8sVersionStr based on --config
cfg, err := configutil.ConfigFileAndDefaultsToInternalConfig(flags.cfgPath, &kubeadmapiv1beta1.InitConfiguration{})
kubeadmutil.CheckErr(err)
if cfg.KubernetesVersion != "" {
flags.newK8sVersionStr = cfg.KubernetesVersion
}
}
// If the new version is already specified in config file, version arg is optional.
if flags.newK8sVersionStr == "" {
err = cmdutil.ValidateExactArgNumber(args, []string{"version"})
kubeadmutil.CheckErr(err)
}
// If option was specified in both args and config file, args will overwrite the config file.
if len(args) == 1 {
flags.newK8sVersionStr = args[0]
}
// Default the flags dynamically, based on each others' value
err = SetImplicitFlags(flags)
kubeadmutil.CheckErr(err)
err = RunApply(flags)
kubeadmutil.CheckErr(err)
},
}
// Register the common flags for apply and plan
addApplyPlanFlags(cmd.Flags(), flags.applyPlanFlags)
// Specify the valid flags specific for apply
cmd.Flags().BoolVarP(&flags.nonInteractiveMode, "yes", "y", flags.nonInteractiveMode, "Perform the upgrade and do not prompt for confirmation (non-interactive mode).")
cmd.Flags().BoolVarP(&flags.force, "force", "f", flags.force, "Force upgrading although some requirements might not be met. This also implies non-interactive mode.")
cmd.Flags().BoolVar(&flags.dryRun, "dry-run", flags.dryRun, "Do not change any state, just output what actions would be performed.")
cmd.Flags().BoolVar(&flags.etcdUpgrade, "etcd-upgrade", flags.etcdUpgrade, "Perform the upgrade of etcd.")
cmd.Flags().DurationVar(&flags.imagePullTimeout, "image-pull-timeout", flags.imagePullTimeout, "The maximum amount of time to wait for the control plane pods to be downloaded.")
// TODO: Register this flag in a generic place
cmd.Flags().StringVar(&flags.criSocket, "cri-socket", flags.criSocket, "Specify the CRI socket to connect to.")
return cmd
}
// RunApply takes care of the actual upgrade functionality
// It does the following things:
// - Checks if the cluster is healthy
// - Gets the configuration from the kubeadm-config ConfigMap in the cluster
// - Enforces all version skew policies
// - Asks the user if they really want to upgrade
// - Makes sure the control plane images are available locally on the master(s)
// - Upgrades the control plane components
// - Applies the other resources that'd be created with kubeadm init as well, like
// - Creating the RBAC rules for the bootstrap tokens and the cluster-info ConfigMap
// - Applying new kube-dns and kube-proxy manifests
// - Uploads the newly used configuration to the cluster ConfigMap
func RunApply(flags *applyFlags) error {
// Start with the basics, verify that the cluster is healthy and get the configuration from the cluster (using the ConfigMap)
klog.V(1).Infof("[upgrade/apply] verifying health of cluster")
klog.V(1).Infof("[upgrade/apply] retrieving configuration from cluster")
upgradeVars, err := enforceRequirements(flags.applyPlanFlags, flags.dryRun, flags.newK8sVersionStr)
if err != nil {
return err
}
if len(flags.criSocket) != 0 {
fmt.Println("[upgrade/apply] Respecting the --cri-socket flag that is set with higher priority than the config file.")
upgradeVars.cfg.NodeRegistration.CRISocket = flags.criSocket
}
// Validate requested and validate actual version
klog.V(1).Infof("[upgrade/apply] validating requested and actual version")
if err := configutil.NormalizeKubernetesVersion(&upgradeVars.cfg.ClusterConfiguration); err != nil {
return err
}
// Use normalized version string in all following code.
flags.newK8sVersionStr = upgradeVars.cfg.KubernetesVersion
k8sVer, err := version.ParseSemantic(flags.newK8sVersionStr)
if err != nil {
return errors.Errorf("unable to parse normalized version %q as a semantic version", flags.newK8sVersionStr)
}
flags.newK8sVersion = k8sVer
if err := features.ValidateVersion(features.InitFeatureGates, upgradeVars.cfg.FeatureGates, upgradeVars.cfg.KubernetesVersion); err != nil {
return err
}
// Enforce the version skew policies
klog.V(1).Infof("[upgrade/version] enforcing version skew policies")
if err := EnforceVersionPolicies(flags, upgradeVars.versionGetter); err != nil {
return errors.Wrap(err, "[upgrade/version] FATAL")
}
// If the current session is interactive, ask the user whether they really want to upgrade
if flags.SessionIsInteractive() {
if err := InteractivelyConfirmUpgrade("Are you sure you want to proceed with the upgrade?"); err != nil {
return err
}
}
// Use a prepuller implementation based on creating DaemonSets
// and block until all DaemonSets are ready; then we know for sure that all control plane images are cached locally
klog.V(1).Infof("[upgrade/apply] creating prepuller")
prepuller := upgrade.NewDaemonSetPrepuller(upgradeVars.client, upgradeVars.waiter, &upgradeVars.cfg.ClusterConfiguration)
componentsToPrepull := constants.MasterComponents
if upgradeVars.cfg.Etcd.External == nil && flags.etcdUpgrade {
componentsToPrepull = append(componentsToPrepull, constants.Etcd)
}
if err := upgrade.PrepullImagesInParallel(prepuller, flags.imagePullTimeout, componentsToPrepull); err != nil {
return errors.Wrap(err, "[upgrade/prepull] Failed prepulled the images for the control plane components error")
}
// Now; perform the upgrade procedure
klog.V(1).Infof("[upgrade/apply] performing upgrade")
if err := PerformControlPlaneUpgrade(flags, upgradeVars.client, upgradeVars.waiter, upgradeVars.cfg); err != nil {
return errors.Wrap(err, "[upgrade/apply] FATAL")
}
// Upgrade RBAC rules and addons.
klog.V(1).Infof("[upgrade/postupgrade] upgrading RBAC rules and addons")
if err := upgrade.PerformPostUpgradeTasks(upgradeVars.client, upgradeVars.cfg, flags.newK8sVersion, flags.dryRun); err != nil {
return errors.Wrap(err, "[upgrade/postupgrade] FATAL post-upgrade error")
}
if flags.dryRun {
fmt.Println("[dryrun] Finished dryrunning successfully!")
return nil
}
fmt.Println("")
fmt.Printf("[upgrade/successful] SUCCESS! Your cluster was upgraded to %q. Enjoy!\n", flags.newK8sVersionStr)
fmt.Println("")
fmt.Println("[upgrade/kubelet] Now that your control plane is upgraded, please proceed with upgrading your kubelets if you haven't already done so.")
return nil
}
// SetImplicitFlags handles dynamically defaulting flags based on each other's value
func SetImplicitFlags(flags *applyFlags) error {
// If we are in dry-run or force mode; we should automatically execute this command non-interactively
if flags.dryRun || flags.force {
flags.nonInteractiveMode = true
}
if len(flags.newK8sVersionStr) == 0 {
return errors.New("version string can't be empty")
}
return nil
}
// EnforceVersionPolicies makes sure that the version the user specified is valid to upgrade to
// There are both fatal and skippable (with --force) errors
func EnforceVersionPolicies(flags *applyFlags, versionGetter upgrade.VersionGetter) error {
fmt.Printf("[upgrade/version] You have chosen to change the cluster version to %q\n", flags.newK8sVersionStr)
versionSkewErrs := upgrade.EnforceVersionPolicies(versionGetter, flags.newK8sVersionStr, flags.newK8sVersion, flags.allowExperimentalUpgrades, flags.allowRCUpgrades)
if versionSkewErrs != nil {
if len(versionSkewErrs.Mandatory) > 0 {
return errors.Errorf("the --version argument is invalid due to these fatal errors:\n\n%v\nPlease fix the misalignments highlighted above and try upgrading again",
kubeadmutil.FormatErrMsg(versionSkewErrs.Mandatory))
}
if len(versionSkewErrs.Skippable) > 0 {
// Return the error if the user hasn't specified the --force flag
if !flags.force {
return errors.Errorf("the --version argument is invalid due to these errors:\n\n%v\nCan be bypassed if you pass the --force flag",
kubeadmutil.FormatErrMsg(versionSkewErrs.Skippable))
}
// Soft errors found, but --force was specified
fmt.Printf("[upgrade/version] Found %d potential version compatibility errors but skipping since the --force flag is set: \n\n%v", len(versionSkewErrs.Skippable), kubeadmutil.FormatErrMsg(versionSkewErrs.Skippable))
}
}
return nil
}
// PerformControlPlaneUpgrade actually performs the upgrade procedure for the cluster of your type (self-hosted or static-pod-hosted)
func PerformControlPlaneUpgrade(flags *applyFlags, client clientset.Interface, waiter apiclient.Waiter, internalcfg *kubeadmapi.InitConfiguration) error {
// OK, the cluster is hosted using static pods. Upgrade a static-pod hosted cluster
fmt.Printf("[upgrade/apply] Upgrading your Static Pod-hosted control plane to version %q...\n", flags.newK8sVersionStr)
if flags.dryRun {
return DryRunStaticPodUpgrade(internalcfg)
}
// Don't save etcd backup directory if etcd is HA, as this could cause corruption
return PerformStaticPodUpgrade(client, waiter, internalcfg, flags.etcdUpgrade)
}
// GetPathManagerForUpgrade returns a path manager properly configured for the given InitConfiguration.
func GetPathManagerForUpgrade(internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade bool) (upgrade.StaticPodPathManager, error) {
isHAEtcd := etcdutil.CheckConfigurationIsHA(&internalcfg.Etcd)
return upgrade.NewKubeStaticPodPathManagerUsingTempDirs(constants.GetStaticPodDirectory(), true, etcdUpgrade && !isHAEtcd)
}
// PerformStaticPodUpgrade performs the upgrade of the control plane components for a static pod hosted cluster
func PerformStaticPodUpgrade(client clientset.Interface, waiter apiclient.Waiter, internalcfg *kubeadmapi.InitConfiguration, etcdUpgrade bool) error {
pathManager, err := GetPathManagerForUpgrade(internalcfg, etcdUpgrade)
if err != nil {
return err
}
// The arguments oldEtcdClient and newEtdClient, are uninitialized because passing in the clients allow for mocking the client during testing
return upgrade.StaticPodControlPlane(client, waiter, pathManager, internalcfg, etcdUpgrade, nil, nil)
}
// DryRunStaticPodUpgrade fakes an upgrade of the control plane
func DryRunStaticPodUpgrade(internalcfg *kubeadmapi.InitConfiguration) error {
dryRunManifestDir, err := constants.CreateTempDirForKubeadm("kubeadm-upgrade-dryrun")
if err != nil {
return err
}
defer os.RemoveAll(dryRunManifestDir)
if err := controlplane.CreateInitStaticPodManifestFiles(dryRunManifestDir, internalcfg); err != nil {
return err
}
// Print the contents of the upgraded manifests and pretend like they were in /etc/kubernetes/manifests
files := []dryrunutil.FileToPrint{}
for _, component := range constants.MasterComponents {
realPath := constants.GetStaticPodFilepath(component, dryRunManifestDir)
outputPath := constants.GetStaticPodFilepath(component, constants.GetStaticPodDirectory())
files = append(files, dryrunutil.NewFileToPrint(realPath, outputPath))
}
return dryrunutil.PrintDryRunFiles(files, os.Stdout)
}