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  }