sigs.k8s.io/cluster-api@v1.7.1/internal/controllers/machineset/machineset_preflight.go (about)

     1  /*
     2  Copyright 2023 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package machineset
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"time"
    24  
    25  	"github.com/blang/semver/v4"
    26  	"github.com/pkg/errors"
    27  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    28  	"k8s.io/apimachinery/pkg/runtime/schema"
    29  	kerrors "k8s.io/apimachinery/pkg/util/errors"
    30  	"k8s.io/apimachinery/pkg/util/sets"
    31  	"k8s.io/klog/v2"
    32  	"k8s.io/utils/ptr"
    33  	ctrl "sigs.k8s.io/controller-runtime"
    34  
    35  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    36  	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
    37  	"sigs.k8s.io/cluster-api/controllers/external"
    38  	"sigs.k8s.io/cluster-api/feature"
    39  	"sigs.k8s.io/cluster-api/internal/contract"
    40  )
    41  
    42  type preflightCheckErrorMessage *string
    43  
    44  // preflightFailedRequeueAfter is used to requeue the MachineSet to re-verify the preflight checks if
    45  // the preflight checks fail.
    46  const preflightFailedRequeueAfter = 15 * time.Second
    47  
    48  var minVerKubernetesKubeletVersionSkewThree = semver.MustParse("1.28.0")
    49  
    50  func (r *Reconciler) runPreflightChecks(ctx context.Context, cluster *clusterv1.Cluster, ms *clusterv1.MachineSet, action string) (_ ctrl.Result, message string, retErr error) {
    51  	log := ctrl.LoggerFrom(ctx)
    52  	// If the MachineSetPreflightChecks feature gate is disabled return early.
    53  	if !feature.Gates.Enabled(feature.MachineSetPreflightChecks) {
    54  		return ctrl.Result{}, "", nil
    55  	}
    56  
    57  	skipped := skippedPreflightChecks(ms)
    58  	// If all the preflight checks are skipped then return early.
    59  	if skipped.Has(clusterv1.MachineSetPreflightCheckAll) {
    60  		return ctrl.Result{}, "", nil
    61  	}
    62  
    63  	// If the cluster does not have a control plane reference then there is nothing to do. Return early.
    64  	if cluster.Spec.ControlPlaneRef == nil {
    65  		return ctrl.Result{}, "", nil
    66  	}
    67  
    68  	// Get the control plane object.
    69  	controlPlane, err := external.Get(ctx, r.UnstructuredCachingClient, cluster.Spec.ControlPlaneRef, cluster.Namespace)
    70  	if err != nil {
    71  		return ctrl.Result{}, "", errors.Wrapf(err, "failed to perform %q: failed to perform preflight checks: failed to get ControlPlane %s", action, klog.KRef(cluster.Spec.ControlPlaneRef.Namespace, cluster.Spec.ControlPlaneRef.Name))
    72  	}
    73  	cpKlogRef := klog.KRef(controlPlane.GetNamespace(), controlPlane.GetName())
    74  
    75  	// If the Control Plane version is not set then we are dealing with a control plane that does not support version
    76  	// or a control plane where the version is not set. In both cases we cannot perform any preflight checks as
    77  	// we do not have enough information. Return early.
    78  	cpVersion, err := contract.ControlPlane().Version().Get(controlPlane)
    79  	if err != nil {
    80  		if errors.Is(err, contract.ErrFieldNotFound) {
    81  			return ctrl.Result{}, "", nil
    82  		}
    83  		return ctrl.Result{}, "", errors.Wrapf(err, "failed to perform %q: failed to perform preflight checks: failed to get the version of ControlPlane %s", action, cpKlogRef)
    84  	}
    85  	cpSemver, err := semver.ParseTolerant(*cpVersion)
    86  	if err != nil {
    87  		return ctrl.Result{}, "", errors.Wrapf(err, "failed to perform %q: failed to perform preflight checks: failed to parse version %q of ControlPlane %s", action, *cpVersion, cpKlogRef)
    88  	}
    89  
    90  	errList := []error{}
    91  	preflightCheckErrs := []preflightCheckErrorMessage{}
    92  	// Run the control-plane-stable preflight check.
    93  	if !skipped.Has(clusterv1.MachineSetPreflightCheckControlPlaneIsStable) {
    94  		preflightCheckErr, err := r.controlPlaneStablePreflightCheck(controlPlane)
    95  		if err != nil {
    96  			errList = append(errList, err)
    97  		}
    98  		if preflightCheckErr != nil {
    99  			preflightCheckErrs = append(preflightCheckErrs, preflightCheckErr)
   100  		}
   101  	}
   102  
   103  	// Check the version skew policies only if version is defined in the MachineSet.
   104  	if ms.Spec.Template.Spec.Version != nil {
   105  		msVersion := *ms.Spec.Template.Spec.Version
   106  		msSemver, err := semver.ParseTolerant(msVersion)
   107  		if err != nil {
   108  			return ctrl.Result{}, "", errors.Wrapf(err, "failed to perform %q: failed to perform preflight checks: failed to parse version %q of MachineSet %s", action, msVersion, klog.KObj(ms))
   109  		}
   110  
   111  		// Run the kubernetes-version skew preflight check.
   112  		if !skipped.Has(clusterv1.MachineSetPreflightCheckKubernetesVersionSkew) {
   113  			preflightCheckErr := r.kubernetesVersionPreflightCheck(cpSemver, msSemver)
   114  			if preflightCheckErr != nil {
   115  				preflightCheckErrs = append(preflightCheckErrs, preflightCheckErr)
   116  			}
   117  		}
   118  
   119  		// Run the kubeadm-version skew preflight check.
   120  		if !skipped.Has(clusterv1.MachineSetPreflightCheckKubeadmVersionSkew) {
   121  			preflightCheckErr, err := r.kubeadmVersionPreflightCheck(cpSemver, msSemver, ms)
   122  			if err != nil {
   123  				errList = append(errList, err)
   124  			}
   125  			if preflightCheckErr != nil {
   126  				preflightCheckErrs = append(preflightCheckErrs, preflightCheckErr)
   127  			}
   128  		}
   129  	}
   130  
   131  	if len(errList) > 0 {
   132  		return ctrl.Result{}, "", errors.Wrapf(kerrors.NewAggregate(errList), "failed to perform %q: failed to perform preflight checks", action)
   133  	}
   134  	if len(preflightCheckErrs) > 0 {
   135  		preflightCheckErrStrings := []string{}
   136  		for _, v := range preflightCheckErrs {
   137  			preflightCheckErrStrings = append(preflightCheckErrStrings, *v)
   138  		}
   139  		msg := fmt.Sprintf("Performing %q on hold because %s. The operation will continue after the preflight check(s) pass", action, strings.Join(preflightCheckErrStrings, "; "))
   140  		log.Info(msg)
   141  		return ctrl.Result{RequeueAfter: preflightFailedRequeueAfter}, msg, nil
   142  	}
   143  	return ctrl.Result{}, "", nil
   144  }
   145  
   146  func (r *Reconciler) controlPlaneStablePreflightCheck(controlPlane *unstructured.Unstructured) (preflightCheckErrorMessage, error) {
   147  	cpKlogRef := klog.KRef(controlPlane.GetNamespace(), controlPlane.GetName())
   148  
   149  	// Check that the control plane is not provisioning.
   150  	isProvisioning, err := contract.ControlPlane().IsProvisioning(controlPlane)
   151  	if err != nil {
   152  		return nil, errors.Wrapf(err, "failed to perform %q preflight check: failed to check if ControlPlane %s is provisioning", clusterv1.MachineSetPreflightCheckControlPlaneIsStable, cpKlogRef)
   153  	}
   154  	if isProvisioning {
   155  		return ptr.To(fmt.Sprintf("ControlPlane %s is provisioning (%q preflight failed)", cpKlogRef, clusterv1.MachineSetPreflightCheckControlPlaneIsStable)), nil
   156  	}
   157  
   158  	// Check that the control plane is not upgrading.
   159  	isUpgrading, err := contract.ControlPlane().IsUpgrading(controlPlane)
   160  	if err != nil {
   161  		return nil, errors.Wrapf(err, "failed to perform %q preflight check: failed to check if the ControlPlane %s is upgrading", clusterv1.MachineSetPreflightCheckControlPlaneIsStable, cpKlogRef)
   162  	}
   163  	if isUpgrading {
   164  		return ptr.To(fmt.Sprintf("ControlPlane %s is upgrading (%q preflight failed)", cpKlogRef, clusterv1.MachineSetPreflightCheckControlPlaneIsStable)), nil
   165  	}
   166  
   167  	return nil, nil
   168  }
   169  
   170  func (r *Reconciler) kubernetesVersionPreflightCheck(cpSemver, msSemver semver.Version) preflightCheckErrorMessage {
   171  	// Check the Kubernetes version skew policy.
   172  	// => MS minor version cannot be greater than the Control Plane minor version.
   173  	// => MS minor version cannot be outside of the supported skew.
   174  	// Kubernetes skew policy: https://kubernetes.io/releases/version-skew-policy/#kubelet
   175  	if msSemver.Minor > cpSemver.Minor {
   176  		return ptr.To(fmt.Sprintf("MachineSet version (%s) and ControlPlane version (%s) do not conform to the kubernetes version skew policy as MachineSet version is higher than ControlPlane version (%q preflight failed)", msSemver.String(), cpSemver.String(), clusterv1.MachineSetPreflightCheckKubernetesVersionSkew))
   177  	}
   178  	minorSkew := uint64(3)
   179  	// For Control Planes running Kubernetes < v1.28, the version skew policy for kubelets is two.
   180  	if cpSemver.LT(minVerKubernetesKubeletVersionSkewThree) {
   181  		minorSkew = 2
   182  	}
   183  	if msSemver.Minor < cpSemver.Minor-minorSkew {
   184  		return ptr.To(fmt.Sprintf("MachineSet version (%s) and ControlPlane version (%s) do not conform to the kubernetes version skew policy as MachineSet version is more than %d minor versions older than the ControlPlane version (%q preflight failed)", msSemver.String(), cpSemver.String(), minorSkew, clusterv1.MachineSetPreflightCheckKubernetesVersionSkew))
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  func (r *Reconciler) kubeadmVersionPreflightCheck(cpSemver, msSemver semver.Version, ms *clusterv1.MachineSet) (preflightCheckErrorMessage, error) {
   191  	// If the bootstrap.configRef is nil return early.
   192  	if ms.Spec.Template.Spec.Bootstrap.ConfigRef == nil {
   193  		return nil, nil
   194  	}
   195  
   196  	// If using kubeadm bootstrap provider, check the kubeadm version skew policy.
   197  	// => MS version should match (major+minor) the Control Plane version.
   198  	// kubeadm skew policy: https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/#kubeadm-s-skew-against-kubeadm
   199  	bootstrapConfigRef := ms.Spec.Template.Spec.Bootstrap.ConfigRef
   200  	groupVersion, err := schema.ParseGroupVersion(bootstrapConfigRef.APIVersion)
   201  	if err != nil {
   202  		return nil, errors.Wrapf(err, "failed to perform %q preflight check: failed to parse bootstrap configRef APIVersion %s", clusterv1.MachineSetPreflightCheckKubeadmVersionSkew, bootstrapConfigRef.APIVersion)
   203  	}
   204  	kubeadmBootstrapProviderUsed := bootstrapConfigRef.Kind == "KubeadmConfigTemplate" &&
   205  		groupVersion.Group == bootstrapv1.GroupVersion.Group
   206  	if kubeadmBootstrapProviderUsed {
   207  		if cpSemver.Minor != msSemver.Minor {
   208  			return ptr.To(fmt.Sprintf("MachineSet version (%s) and ControlPlane version (%s) do not conform to kubeadm version skew policy as kubeadm only supports joining with the same major+minor version as the control plane (%q preflight failed)", msSemver.String(), cpSemver.String(), clusterv1.MachineSetPreflightCheckKubeadmVersionSkew)), nil
   209  		}
   210  	}
   211  	return nil, nil
   212  }
   213  
   214  func skippedPreflightChecks(ms *clusterv1.MachineSet) sets.Set[clusterv1.MachineSetPreflightCheck] {
   215  	skipped := sets.Set[clusterv1.MachineSetPreflightCheck]{}
   216  	if ms == nil {
   217  		return skipped
   218  	}
   219  	skip := ms.Annotations[clusterv1.MachineSetSkipPreflightChecksAnnotation]
   220  	if skip == "" {
   221  		return skipped
   222  	}
   223  	skippedList := strings.Split(skip, ",")
   224  	for i := range skippedList {
   225  		skipped.Insert(clusterv1.MachineSetPreflightCheck(strings.TrimSpace(skippedList[i])))
   226  	}
   227  	return skipped
   228  }