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 }