sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/filters.go (about) 1 /* 2 Copyright 2021 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 internal 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "reflect" 23 "strings" 24 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 28 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 29 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 30 controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1" 31 "sigs.k8s.io/cluster-api/util/collections" 32 ) 33 34 // matchesMachineSpec checks if a Machine matches any of a set of KubeadmConfigs and a set of infra machine configs. 35 // If it doesn't, it returns the reasons why. 36 // Kubernetes version, infrastructure template, and KubeadmConfig field need to be equivalent. 37 // Note: We don't need to compare the entire MachineSpec to determine if a Machine needs to be rolled out, 38 // because all the fields in the MachineSpec, except for version, the infrastructureRef and bootstrap.ConfigRef, are either: 39 // - mutated in-place (ex: NodeDrainTimeout) 40 // - are not dictated by KCP (ex: ProviderID) 41 // - are not relevant for the rollout decision (ex: failureDomain). 42 func matchesMachineSpec(infraConfigs map[string]*unstructured.Unstructured, machineConfigs map[string]*bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane, machine *clusterv1.Machine) (string, bool) { 43 mismatchReasons := []string{} 44 45 if !collections.MatchesKubernetesVersion(kcp.Spec.Version)(machine) { 46 machineVersion := "" 47 if machine != nil && machine.Spec.Version != nil { 48 machineVersion = *machine.Spec.Version 49 } 50 mismatchReasons = append(mismatchReasons, fmt.Sprintf("Machine version %q is not equal to KCP version %q", machineVersion, kcp.Spec.Version)) 51 } 52 53 if reason, matches := matchesKubeadmBootstrapConfig(machineConfigs, kcp, machine); !matches { 54 mismatchReasons = append(mismatchReasons, reason) 55 } 56 57 if reason, matches := matchesTemplateClonedFrom(infraConfigs, kcp, machine); !matches { 58 mismatchReasons = append(mismatchReasons, reason) 59 } 60 61 if len(mismatchReasons) > 0 { 62 return strings.Join(mismatchReasons, ","), false 63 } 64 65 return "", true 66 } 67 68 // NeedsRollout checks if a Machine needs to be rolled out and returns the reason why. 69 func NeedsRollout(reconciliationTime, rolloutAfter *metav1.Time, rolloutBefore *controlplanev1.RolloutBefore, infraConfigs map[string]*unstructured.Unstructured, machineConfigs map[string]*bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane, machine *clusterv1.Machine) (string, bool) { 70 rolloutReasons := []string{} 71 72 // Machines whose certificates are about to expire. 73 if collections.ShouldRolloutBefore(reconciliationTime, rolloutBefore)(machine) { 74 rolloutReasons = append(rolloutReasons, "certificates will expire soon, rolloutBefore expired") 75 } 76 77 // Machines that are scheduled for rollout (KCP.Spec.RolloutAfter set, 78 // the RolloutAfter deadline is expired, and the machine was created before the deadline). 79 if collections.ShouldRolloutAfter(reconciliationTime, rolloutAfter)(machine) { 80 rolloutReasons = append(rolloutReasons, "rolloutAfter expired") 81 } 82 83 // Machines that do not match with KCP config. 84 if mismatchReason, matches := matchesMachineSpec(infraConfigs, machineConfigs, kcp, machine); !matches { 85 rolloutReasons = append(rolloutReasons, mismatchReason) 86 } 87 88 if len(rolloutReasons) > 0 { 89 return fmt.Sprintf("Machine %s needs rollout: %s", machine.Name, strings.Join(rolloutReasons, ",")), true 90 } 91 92 return "", false 93 } 94 95 // matchesTemplateClonedFrom checks if a Machine has a corresponding infrastructure machine that 96 // matches a given KCP infra template and if it doesn't match returns the reason why. 97 // Note: Differences to the labels and annotations on the infrastructure machine are not considered for matching 98 // criteria, because changes to labels and annotations are propagated in-place to the infrastructure machines. 99 // TODO: This function will be renamed in a follow-up PR to something better. (ex: MatchesInfraMachine). 100 func matchesTemplateClonedFrom(infraConfigs map[string]*unstructured.Unstructured, kcp *controlplanev1.KubeadmControlPlane, machine *clusterv1.Machine) (string, bool) { 101 if machine == nil { 102 return "Machine cannot be compared with KCP.spec.machineTemplate.infrastructureRef: Machine is nil", false 103 } 104 infraObj, found := infraConfigs[machine.Name] 105 if !found { 106 // Return true here because failing to get infrastructure machine should not be considered as unmatching. 107 return "", true 108 } 109 110 clonedFromName, ok1 := infraObj.GetAnnotations()[clusterv1.TemplateClonedFromNameAnnotation] 111 clonedFromGroupKind, ok2 := infraObj.GetAnnotations()[clusterv1.TemplateClonedFromGroupKindAnnotation] 112 if !ok1 || !ok2 { 113 // All kcp cloned infra machines should have this annotation. 114 // Missing the annotation may be due to older version machines or adopted machines. 115 // Should not be considered as mismatch. 116 return "", true 117 } 118 119 // Check if the machine's infrastructure reference has been created from the current KCP infrastructure template. 120 if clonedFromName != kcp.Spec.MachineTemplate.InfrastructureRef.Name || 121 clonedFromGroupKind != kcp.Spec.MachineTemplate.InfrastructureRef.GroupVersionKind().GroupKind().String() { 122 return fmt.Sprintf("Infrastructure template on KCP rotated from %s %s to %s %s", 123 clonedFromGroupKind, clonedFromName, 124 kcp.Spec.MachineTemplate.InfrastructureRef.GroupVersionKind().GroupKind().String(), kcp.Spec.MachineTemplate.InfrastructureRef.Name), false 125 } 126 127 return "", true 128 } 129 130 // matchesKubeadmBootstrapConfig checks if machine's KubeadmConfigSpec is equivalent with KCP's KubeadmConfigSpec. 131 // Note: Differences to the labels and annotations on the KubeadmConfig are not considered for matching 132 // criteria, because changes to labels and annotations are propagated in-place to KubeadmConfig. 133 func matchesKubeadmBootstrapConfig(machineConfigs map[string]*bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane, machine *clusterv1.Machine) (string, bool) { 134 if machine == nil { 135 return "Machine KubeadmConfig cannot be compared: Machine is nil", false 136 } 137 138 // Check if KCP and machine ClusterConfiguration matches, if not return 139 if !matchClusterConfiguration(kcp, machine) { 140 return "Machine ClusterConfiguration is outdated", false 141 } 142 143 bootstrapRef := machine.Spec.Bootstrap.ConfigRef 144 if bootstrapRef == nil { 145 // Missing bootstrap reference should not be considered as unmatching. 146 // This is a safety precaution to avoid selecting machines that are broken, which in the future should be remediated separately. 147 return "", true 148 } 149 150 machineConfig, found := machineConfigs[machine.Name] 151 if !found { 152 // Return true here because failing to get KubeadmConfig should not be considered as unmatching. 153 // This is a safety precaution to avoid rolling out machines if the client or the api-server is misbehaving. 154 return "", true 155 } 156 157 // Check if KCP and machine InitConfiguration or JoinConfiguration matches 158 // NOTE: only one between init configuration and join configuration is set on a machine, depending 159 // on the fact that the machine was the initial control plane node or a joining control plane node. 160 if !matchInitOrJoinConfiguration(machineConfig, kcp) { 161 return "Machine InitConfiguration or JoinConfiguration are outdated", false 162 } 163 164 return "", true 165 } 166 167 // matchClusterConfiguration verifies if KCP and machine ClusterConfiguration matches. 168 // NOTE: Machines that have KubeadmClusterConfigurationAnnotation will have to match with KCP ClusterConfiguration. 169 // If the annotation is not present (machine is either old or adopted), we won't roll out on any possible changes 170 // made in KCP's ClusterConfiguration given that we don't have enough information to make a decision. 171 // Users should use KCP.Spec.RolloutAfter field to force a rollout in this case. 172 func matchClusterConfiguration(kcp *controlplanev1.KubeadmControlPlane, machine *clusterv1.Machine) bool { 173 machineClusterConfigStr, ok := machine.GetAnnotations()[controlplanev1.KubeadmClusterConfigurationAnnotation] 174 if !ok { 175 // We don't have enough information to make a decision; don't' trigger a roll out. 176 return true 177 } 178 179 machineClusterConfig := &bootstrapv1.ClusterConfiguration{} 180 // ClusterConfiguration annotation is not correct, only solution is to rollout. 181 // The call to json.Unmarshal has to take a pointer to the pointer struct defined above, 182 // otherwise we won't be able to handle a nil ClusterConfiguration (that is serialized into "null"). 183 // See https://github.com/kubernetes-sigs/cluster-api/issues/3353. 184 if err := json.Unmarshal([]byte(machineClusterConfigStr), &machineClusterConfig); err != nil { 185 return false 186 } 187 188 // If any of the compared values are nil, treat them the same as an empty ClusterConfiguration. 189 if machineClusterConfig == nil { 190 machineClusterConfig = &bootstrapv1.ClusterConfiguration{} 191 } 192 193 kcpLocalClusterConfiguration := kcp.Spec.KubeadmConfigSpec.ClusterConfiguration 194 if kcpLocalClusterConfiguration == nil { 195 kcpLocalClusterConfiguration = &bootstrapv1.ClusterConfiguration{} 196 } 197 198 // Skip checking DNS fields because we can update the configuration of the working cluster in place. 199 machineClusterConfig.DNS = kcpLocalClusterConfiguration.DNS 200 201 // Compare and return. 202 return reflect.DeepEqual(machineClusterConfig, kcpLocalClusterConfiguration) 203 } 204 205 // matchInitOrJoinConfiguration verifies if KCP and machine InitConfiguration or JoinConfiguration matches. 206 // NOTE: By extension this method takes care of detecting changes in other fields of the KubeadmConfig configuration (e.g. Files, Mounts etc.) 207 func matchInitOrJoinConfiguration(machineConfig *bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane) bool { 208 if machineConfig == nil { 209 // Return true here because failing to get KubeadmConfig should not be considered as unmatching. 210 // This is a safety precaution to avoid rolling out machines if the client or the api-server is misbehaving. 211 return true 212 } 213 214 // takes the KubeadmConfigSpec from KCP and applies the transformations required 215 // to allow a comparison with the KubeadmConfig referenced from the machine. 216 kcpConfig := getAdjustedKcpConfig(kcp, machineConfig) 217 218 // Default both KubeadmConfigSpecs before comparison. 219 // *Note* This assumes that newly added default values never 220 // introduce a semantic difference to the unset value. 221 // But that is something that is ensured by our API guarantees. 222 kcpConfig.Default() 223 machineConfig.Spec.Default() 224 225 // cleanups all the fields that are not relevant for the comparison. 226 cleanupConfigFields(kcpConfig, machineConfig) 227 228 return reflect.DeepEqual(&machineConfig.Spec, kcpConfig) 229 } 230 231 // getAdjustedKcpConfig takes the KubeadmConfigSpec from KCP and applies the transformations required 232 // to allow a comparison with the KubeadmConfig referenced from the machine. 233 // NOTE: The KCP controller applies a set of transformations when creating a KubeadmConfig referenced from the machine, 234 // mostly depending on the fact that the machine was the initial control plane node or a joining control plane node. 235 // In this function we don't have such information, so we are making the KubeadmConfigSpec similar to the KubeadmConfig. 236 func getAdjustedKcpConfig(kcp *controlplanev1.KubeadmControlPlane, machineConfig *bootstrapv1.KubeadmConfig) *bootstrapv1.KubeadmConfigSpec { 237 kcpConfig := kcp.Spec.KubeadmConfigSpec.DeepCopy() 238 239 // Machine's join configuration is nil when it is the first machine in the control plane. 240 if machineConfig.Spec.JoinConfiguration == nil { 241 kcpConfig.JoinConfiguration = nil 242 } 243 244 // Machine's init configuration is nil when the control plane is already initialized. 245 if machineConfig.Spec.InitConfiguration == nil { 246 kcpConfig.InitConfiguration = nil 247 } 248 249 return kcpConfig 250 } 251 252 // cleanupConfigFields cleanups all the fields that are not relevant for the comparison. 253 func cleanupConfigFields(kcpConfig *bootstrapv1.KubeadmConfigSpec, machineConfig *bootstrapv1.KubeadmConfig) { 254 // KCP ClusterConfiguration will only be compared with a machine's ClusterConfiguration annotation, so 255 // we are cleaning up from the reflect.DeepEqual comparison. 256 kcpConfig.ClusterConfiguration = nil 257 machineConfig.Spec.ClusterConfiguration = nil 258 259 // If KCP JoinConfiguration is not present, set machine JoinConfiguration to nil (nothing can trigger rollout here). 260 // NOTE: this is required because CABPK applies an empty joinConfiguration in case no one is provided. 261 if kcpConfig.JoinConfiguration == nil { 262 machineConfig.Spec.JoinConfiguration = nil 263 } 264 265 // Cleanup JoinConfiguration.Discovery from kcpConfig and machineConfig, because those info are relevant only for 266 // the join process and not for comparing the configuration of the machine. 267 emptyDiscovery := bootstrapv1.Discovery{} 268 if kcpConfig.JoinConfiguration != nil { 269 kcpConfig.JoinConfiguration.Discovery = emptyDiscovery 270 } 271 if machineConfig.Spec.JoinConfiguration != nil { 272 machineConfig.Spec.JoinConfiguration.Discovery = emptyDiscovery 273 } 274 275 // If KCP JoinConfiguration.ControlPlane is not present, set machine join configuration to nil (nothing can trigger rollout here). 276 // NOTE: this is required because CABPK applies an empty joinConfiguration.ControlPlane in case no one is provided. 277 if kcpConfig.JoinConfiguration != nil && kcpConfig.JoinConfiguration.ControlPlane == nil && 278 machineConfig.Spec.JoinConfiguration != nil { 279 machineConfig.Spec.JoinConfiguration.ControlPlane = nil 280 } 281 282 // If KCP's join NodeRegistration is empty, set machine's node registration to empty as no changes should trigger rollout. 283 emptyNodeRegistration := bootstrapv1.NodeRegistrationOptions{} 284 if kcpConfig.JoinConfiguration != nil && reflect.DeepEqual(kcpConfig.JoinConfiguration.NodeRegistration, emptyNodeRegistration) && 285 machineConfig.Spec.JoinConfiguration != nil { 286 machineConfig.Spec.JoinConfiguration.NodeRegistration = emptyNodeRegistration 287 } 288 289 // Clear up the TypeMeta information from the comparison. 290 // NOTE: KCP types don't carry this information. 291 if machineConfig.Spec.InitConfiguration != nil && kcpConfig.InitConfiguration != nil { 292 machineConfig.Spec.InitConfiguration.TypeMeta = kcpConfig.InitConfiguration.TypeMeta 293 } 294 if machineConfig.Spec.JoinConfiguration != nil && kcpConfig.JoinConfiguration != nil { 295 machineConfig.Spec.JoinConfiguration.TypeMeta = kcpConfig.JoinConfiguration.TypeMeta 296 } 297 }