sigs.k8s.io/cluster-api@v1.6.3/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 kcpLocalClusterConfiguration := kcp.Spec.KubeadmConfigSpec.ClusterConfiguration 193 if kcpLocalClusterConfiguration == nil { 194 kcpLocalClusterConfiguration = &bootstrapv1.ClusterConfiguration{} 195 } 196 197 // Compare and return. 198 return reflect.DeepEqual(machineClusterConfig, kcpLocalClusterConfiguration) 199 } 200 201 // matchInitOrJoinConfiguration verifies if KCP and machine InitConfiguration or JoinConfiguration matches. 202 // NOTE: By extension this method takes care of detecting changes in other fields of the KubeadmConfig configuration (e.g. Files, Mounts etc.) 203 func matchInitOrJoinConfiguration(machineConfig *bootstrapv1.KubeadmConfig, kcp *controlplanev1.KubeadmControlPlane) bool { 204 if machineConfig == nil { 205 // Return true here because failing to get KubeadmConfig should not be considered as unmatching. 206 // This is a safety precaution to avoid rolling out machines if the client or the api-server is misbehaving. 207 return true 208 } 209 210 // takes the KubeadmConfigSpec from KCP and applies the transformations required 211 // to allow a comparison with the KubeadmConfig referenced from the machine. 212 kcpConfig := getAdjustedKcpConfig(kcp, machineConfig) 213 214 // Default both KubeadmConfigSpecs before comparison. 215 // *Note* This assumes that newly added default values never 216 // introduce a semantic difference to the unset value. 217 // But that is something that is ensured by our API guarantees. 218 kcpConfig.Default() 219 machineConfig.Spec.Default() 220 221 // cleanups all the fields that are not relevant for the comparison. 222 cleanupConfigFields(kcpConfig, machineConfig) 223 224 return reflect.DeepEqual(&machineConfig.Spec, kcpConfig) 225 } 226 227 // getAdjustedKcpConfig takes the KubeadmConfigSpec from KCP and applies the transformations required 228 // to allow a comparison with the KubeadmConfig referenced from the machine. 229 // NOTE: The KCP controller applies a set of transformations when creating a KubeadmConfig referenced from the machine, 230 // mostly depending on the fact that the machine was the initial control plane node or a joining control plane node. 231 // In this function we don't have such information, so we are making the KubeadmConfigSpec similar to the KubeadmConfig. 232 func getAdjustedKcpConfig(kcp *controlplanev1.KubeadmControlPlane, machineConfig *bootstrapv1.KubeadmConfig) *bootstrapv1.KubeadmConfigSpec { 233 kcpConfig := kcp.Spec.KubeadmConfigSpec.DeepCopy() 234 235 // Machine's join configuration is nil when it is the first machine in the control plane. 236 if machineConfig.Spec.JoinConfiguration == nil { 237 kcpConfig.JoinConfiguration = nil 238 } 239 240 // Machine's init configuration is nil when the control plane is already initialized. 241 if machineConfig.Spec.InitConfiguration == nil { 242 kcpConfig.InitConfiguration = nil 243 } 244 245 return kcpConfig 246 } 247 248 // cleanupConfigFields cleanups all the fields that are not relevant for the comparison. 249 func cleanupConfigFields(kcpConfig *bootstrapv1.KubeadmConfigSpec, machineConfig *bootstrapv1.KubeadmConfig) { 250 // KCP ClusterConfiguration will only be compared with a machine's ClusterConfiguration annotation, so 251 // we are cleaning up from the reflect.DeepEqual comparison. 252 kcpConfig.ClusterConfiguration = nil 253 machineConfig.Spec.ClusterConfiguration = nil 254 255 // If KCP JoinConfiguration is not present, set machine JoinConfiguration to nil (nothing can trigger rollout here). 256 // NOTE: this is required because CABPK applies an empty joinConfiguration in case no one is provided. 257 if kcpConfig.JoinConfiguration == nil { 258 machineConfig.Spec.JoinConfiguration = nil 259 } 260 261 // Cleanup JoinConfiguration.Discovery from kcpConfig and machineConfig, because those info are relevant only for 262 // the join process and not for comparing the configuration of the machine. 263 emptyDiscovery := bootstrapv1.Discovery{} 264 if kcpConfig.JoinConfiguration != nil { 265 kcpConfig.JoinConfiguration.Discovery = emptyDiscovery 266 } 267 if machineConfig.Spec.JoinConfiguration != nil { 268 machineConfig.Spec.JoinConfiguration.Discovery = emptyDiscovery 269 } 270 271 // If KCP JoinConfiguration.ControlPlane is not present, set machine join configuration to nil (nothing can trigger rollout here). 272 // NOTE: this is required because CABPK applies an empty joinConfiguration.ControlPlane in case no one is provided. 273 if kcpConfig.JoinConfiguration != nil && kcpConfig.JoinConfiguration.ControlPlane == nil && 274 machineConfig.Spec.JoinConfiguration != nil { 275 machineConfig.Spec.JoinConfiguration.ControlPlane = nil 276 } 277 278 // If KCP's join NodeRegistration is empty, set machine's node registration to empty as no changes should trigger rollout. 279 emptyNodeRegistration := bootstrapv1.NodeRegistrationOptions{} 280 if kcpConfig.JoinConfiguration != nil && reflect.DeepEqual(kcpConfig.JoinConfiguration.NodeRegistration, emptyNodeRegistration) && 281 machineConfig.Spec.JoinConfiguration != nil { 282 machineConfig.Spec.JoinConfiguration.NodeRegistration = emptyNodeRegistration 283 } 284 285 // Clear up the TypeMeta information from the comparison. 286 // NOTE: KCP types don't carry this information. 287 if machineConfig.Spec.InitConfiguration != nil && kcpConfig.InitConfiguration != nil { 288 machineConfig.Spec.InitConfiguration.TypeMeta = kcpConfig.InitConfiguration.TypeMeta 289 } 290 if machineConfig.Spec.JoinConfiguration != nil && kcpConfig.JoinConfiguration != nil { 291 machineConfig.Spec.JoinConfiguration.TypeMeta = kcpConfig.JoinConfiguration.TypeMeta 292 } 293 }