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  }