sigs.k8s.io/cluster-api@v1.7.1/controlplane/kubeadm/internal/workload_cluster.go (about)

     1  /*
     2  Copyright 2020 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  	"context"
    21  	"crypto"
    22  	"crypto/rand"
    23  	"crypto/rsa"
    24  	"crypto/tls"
    25  	"crypto/x509"
    26  	"crypto/x509/pkix"
    27  	"fmt"
    28  	"math/big"
    29  	"reflect"
    30  	"time"
    31  
    32  	"github.com/blang/semver/v4"
    33  	"github.com/pkg/errors"
    34  	appsv1 "k8s.io/api/apps/v1"
    35  	corev1 "k8s.io/api/core/v1"
    36  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    37  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    38  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    39  	"k8s.io/apimachinery/pkg/util/sets"
    40  	"k8s.io/client-go/rest"
    41  	"k8s.io/client-go/util/retry"
    42  	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
    43  	"sigs.k8s.io/yaml"
    44  
    45  	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
    46  	bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1"
    47  	kubeadmtypes "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types"
    48  	controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1beta1"
    49  	"sigs.k8s.io/cluster-api/controlplane/kubeadm/internal/proxy"
    50  	"sigs.k8s.io/cluster-api/internal/util/kubeadm"
    51  	"sigs.k8s.io/cluster-api/util"
    52  	"sigs.k8s.io/cluster-api/util/certs"
    53  	containerutil "sigs.k8s.io/cluster-api/util/container"
    54  	"sigs.k8s.io/cluster-api/util/patch"
    55  	"sigs.k8s.io/cluster-api/util/version"
    56  )
    57  
    58  const (
    59  	kubeProxyKey                   = "kube-proxy"
    60  	kubeadmConfigKey               = "kubeadm-config"
    61  	kubeadmAPIServerCertCommonName = "kube-apiserver"
    62  	kubeletConfigKey               = "kubelet"
    63  	cgroupDriverKey                = "cgroupDriver"
    64  	labelNodeRoleOldControlPlane   = "node-role.kubernetes.io/master" // Deprecated: https://github.com/kubernetes/kubeadm/issues/2200
    65  	labelNodeRoleControlPlane      = "node-role.kubernetes.io/control-plane"
    66  	clusterStatusKey               = "ClusterStatus"
    67  	clusterConfigurationKey        = "ClusterConfiguration"
    68  )
    69  
    70  var (
    71  	// Starting from v1.22.0 kubeadm dropped the usage of the ClusterStatus entry from the kubeadm-config ConfigMap
    72  	// so we're not anymore required to remove API endpoints for control plane nodes after deletion.
    73  	//
    74  	// NOTE: The following assumes that kubeadm version equals to Kubernetes version.
    75  	minKubernetesVersionWithoutClusterStatus = semver.MustParse("1.22.0")
    76  
    77  	// Starting from v1.21.0 kubeadm defaults to systemdCGroup driver, as well as images built with ImageBuilder,
    78  	// so it is necessary to mutate the kubelet-config-xx ConfigMap.
    79  	//
    80  	// NOTE: The following assumes that kubeadm version equals to Kubernetes version.
    81  	minVerKubeletSystemdDriver = semver.MustParse("1.21.0")
    82  
    83  	// Starting from v1.24.0 kubeadm uses "kubelet-config" a ConfigMap name for KubeletConfiguration,
    84  	// Dropping the X-Y suffix.
    85  	//
    86  	// NOTE: The following assumes that kubeadm version equals to Kubernetes version.
    87  	minVerUnversionedKubeletConfig = semver.MustParse("1.24.0")
    88  
    89  	// ErrControlPlaneMinNodes signals that a cluster doesn't meet the minimum required nodes
    90  	// to remove an etcd member.
    91  	ErrControlPlaneMinNodes = errors.New("cluster has fewer than 2 control plane nodes; removing an etcd member is not supported")
    92  )
    93  
    94  // WorkloadCluster defines all behaviors necessary to upgrade kubernetes on a workload cluster
    95  //
    96  // TODO: Add a detailed description to each of these method definitions.
    97  type WorkloadCluster interface {
    98  	// Basic health and status checks.
    99  	ClusterStatus(ctx context.Context) (ClusterStatus, error)
   100  	UpdateStaticPodConditions(ctx context.Context, controlPlane *ControlPlane)
   101  	UpdateEtcdConditions(ctx context.Context, controlPlane *ControlPlane)
   102  	EtcdMembers(ctx context.Context) ([]string, error)
   103  	GetAPIServerCertificateExpiry(ctx context.Context, kubeadmConfig *bootstrapv1.KubeadmConfig, nodeName string) (*time.Time, error)
   104  
   105  	// Upgrade related tasks.
   106  	ReconcileKubeletRBACBinding(ctx context.Context, version semver.Version) error
   107  	ReconcileKubeletRBACRole(ctx context.Context, version semver.Version) error
   108  	UpdateKubernetesVersionInKubeadmConfigMap(version semver.Version) func(*bootstrapv1.ClusterConfiguration)
   109  	UpdateImageRepositoryInKubeadmConfigMap(imageRepository string) func(*bootstrapv1.ClusterConfiguration)
   110  	UpdateFeatureGatesInKubeadmConfigMap(featureGates map[string]bool) func(*bootstrapv1.ClusterConfiguration)
   111  	UpdateEtcdLocalInKubeadmConfigMap(localEtcd *bootstrapv1.LocalEtcd) func(*bootstrapv1.ClusterConfiguration)
   112  	UpdateEtcdExternalInKubeadmConfigMap(externalEtcd *bootstrapv1.ExternalEtcd) func(*bootstrapv1.ClusterConfiguration)
   113  	UpdateAPIServerInKubeadmConfigMap(apiServer bootstrapv1.APIServer) func(*bootstrapv1.ClusterConfiguration)
   114  	UpdateControllerManagerInKubeadmConfigMap(controllerManager bootstrapv1.ControlPlaneComponent) func(*bootstrapv1.ClusterConfiguration)
   115  	UpdateSchedulerInKubeadmConfigMap(scheduler bootstrapv1.ControlPlaneComponent) func(*bootstrapv1.ClusterConfiguration)
   116  	UpdateKubeletConfigMap(ctx context.Context, version semver.Version) error
   117  	UpdateKubeProxyImageInfo(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane, version semver.Version) error
   118  	UpdateCoreDNS(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane, version semver.Version) error
   119  	RemoveEtcdMemberForMachine(ctx context.Context, machine *clusterv1.Machine) error
   120  	RemoveMachineFromKubeadmConfigMap(ctx context.Context, machine *clusterv1.Machine, version semver.Version) error
   121  	RemoveNodeFromKubeadmConfigMap(ctx context.Context, nodeName string, version semver.Version) error
   122  	ForwardEtcdLeadership(ctx context.Context, machine *clusterv1.Machine, leaderCandidate *clusterv1.Machine) error
   123  	AllowBootstrapTokensToGetNodes(ctx context.Context) error
   124  	AllowClusterAdminPermissions(ctx context.Context, version semver.Version) error
   125  	UpdateClusterConfiguration(ctx context.Context, version semver.Version, mutators ...func(*bootstrapv1.ClusterConfiguration)) error
   126  
   127  	// State recovery tasks.
   128  	ReconcileEtcdMembers(ctx context.Context, nodeNames []string, version semver.Version) ([]string, error)
   129  }
   130  
   131  // Workload defines operations on workload clusters.
   132  type Workload struct {
   133  	Client              ctrlclient.Client
   134  	CoreDNSMigrator     coreDNSMigrator
   135  	etcdClientGenerator etcdClientFor
   136  	restConfig          *rest.Config
   137  }
   138  
   139  var _ WorkloadCluster = &Workload{}
   140  
   141  func (w *Workload) getControlPlaneNodes(ctx context.Context) (*corev1.NodeList, error) {
   142  	controlPlaneNodes := &corev1.NodeList{}
   143  	controlPlaneNodeNames := sets.Set[string]{}
   144  
   145  	for _, label := range []string{labelNodeRoleOldControlPlane, labelNodeRoleControlPlane} {
   146  		nodes := &corev1.NodeList{}
   147  		if err := w.Client.List(ctx, nodes, ctrlclient.MatchingLabels(map[string]string{
   148  			label: "",
   149  		})); err != nil {
   150  			return nil, err
   151  		}
   152  
   153  		for i := range nodes.Items {
   154  			node := nodes.Items[i]
   155  
   156  			// Continue if we already added that node.
   157  			if controlPlaneNodeNames.Has(node.Name) {
   158  				continue
   159  			}
   160  
   161  			controlPlaneNodeNames.Insert(node.Name)
   162  			controlPlaneNodes.Items = append(controlPlaneNodes.Items, node)
   163  		}
   164  	}
   165  
   166  	return controlPlaneNodes, nil
   167  }
   168  
   169  func (w *Workload) getConfigMap(ctx context.Context, configMap ctrlclient.ObjectKey) (*corev1.ConfigMap, error) {
   170  	original := &corev1.ConfigMap{}
   171  	if err := w.Client.Get(ctx, configMap, original); err != nil {
   172  		return nil, errors.Wrapf(err, "error getting %s/%s configmap from target cluster", configMap.Namespace, configMap.Name)
   173  	}
   174  	return original.DeepCopy(), nil
   175  }
   176  
   177  // UpdateImageRepositoryInKubeadmConfigMap updates the image repository in the kubeadm config map.
   178  func (w *Workload) UpdateImageRepositoryInKubeadmConfigMap(imageRepository string) func(*bootstrapv1.ClusterConfiguration) {
   179  	return func(c *bootstrapv1.ClusterConfiguration) {
   180  		if imageRepository == "" {
   181  			return
   182  		}
   183  
   184  		c.ImageRepository = imageRepository
   185  	}
   186  }
   187  
   188  // UpdateFeatureGatesInKubeadmConfigMap updates the feature gates in the kubeadm config map.
   189  func (w *Workload) UpdateFeatureGatesInKubeadmConfigMap(featureGates map[string]bool) func(*bootstrapv1.ClusterConfiguration) {
   190  	return func(c *bootstrapv1.ClusterConfiguration) {
   191  		// Even if featureGates is nil, reset it to ClusterConfiguration
   192  		// to override any previously set feature gates.
   193  		c.FeatureGates = featureGates
   194  	}
   195  }
   196  
   197  // UpdateKubernetesVersionInKubeadmConfigMap updates the kubernetes version in the kubeadm config map.
   198  func (w *Workload) UpdateKubernetesVersionInKubeadmConfigMap(version semver.Version) func(*bootstrapv1.ClusterConfiguration) {
   199  	return func(c *bootstrapv1.ClusterConfiguration) {
   200  		c.KubernetesVersion = fmt.Sprintf("v%s", version.String())
   201  	}
   202  }
   203  
   204  // UpdateKubeletConfigMap will create a new kubelet-config-1.x config map for a new version of the kubelet.
   205  // This is a necessary process for upgrades.
   206  func (w *Workload) UpdateKubeletConfigMap(ctx context.Context, version semver.Version) error {
   207  	// Check if the desired configmap already exists
   208  	desiredKubeletConfigMapName := generateKubeletConfigName(version)
   209  	configMapKey := ctrlclient.ObjectKey{Name: desiredKubeletConfigMapName, Namespace: metav1.NamespaceSystem}
   210  	_, err := w.getConfigMap(ctx, configMapKey)
   211  	if err == nil {
   212  		// Nothing to do, the configmap already exists
   213  		return nil
   214  	}
   215  	if !apierrors.IsNotFound(errors.Cause(err)) {
   216  		return errors.Wrapf(err, "error determining if kubelet configmap %s exists", desiredKubeletConfigMapName)
   217  	}
   218  
   219  	previousMinorVersionKubeletConfigMapName := generateKubeletConfigName(semver.Version{Major: version.Major, Minor: version.Minor - 1})
   220  
   221  	// If desired and previous ConfigMap name are the same it means we already completed the transition
   222  	// to the unified KubeletConfigMap name in the previous upgrade; no additional operations are required.
   223  	if desiredKubeletConfigMapName == previousMinorVersionKubeletConfigMapName {
   224  		return nil
   225  	}
   226  
   227  	configMapKey = ctrlclient.ObjectKey{Name: previousMinorVersionKubeletConfigMapName, Namespace: metav1.NamespaceSystem}
   228  	// Returns a copy
   229  	cm, err := w.getConfigMap(ctx, configMapKey)
   230  	if apierrors.IsNotFound(errors.Cause(err)) {
   231  		return errors.Errorf("unable to find kubelet configmap %s", previousMinorVersionKubeletConfigMapName)
   232  	}
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	// In order to avoid using two cgroup drivers on the same machine,
   238  	// (cgroupfs and systemd cgroup drivers), starting from
   239  	// 1.21 image builder is going to configure containerd for using the
   240  	// systemd driver, and the Kubelet configuration must be aligned to this change.
   241  	// NOTE: It is considered safe to update the kubelet-config-1.21 ConfigMap
   242  	// because only new nodes using v1.21 images will pick up the change during
   243  	// kubeadm join.
   244  	if version.GE(minVerKubeletSystemdDriver) {
   245  		data, ok := cm.Data[kubeletConfigKey]
   246  		if !ok {
   247  			return errors.Errorf("unable to find %q key in %s", kubeletConfigKey, cm.Name)
   248  		}
   249  		kubeletConfig, err := yamlToUnstructured([]byte(data))
   250  		if err != nil {
   251  			return errors.Wrapf(err, "unable to decode kubelet ConfigMap's %q content to Unstructured object", kubeletConfigKey)
   252  		}
   253  		cgroupDriver, _, err := unstructured.NestedString(kubeletConfig.UnstructuredContent(), cgroupDriverKey)
   254  		if err != nil {
   255  			return errors.Wrapf(err, "unable to extract %q from Kubelet ConfigMap's %q", cgroupDriverKey, cm.Name)
   256  		}
   257  
   258  		// If the value is not already explicitly set by the user, change according to kubeadm/image builder new requirements.
   259  		if cgroupDriver == "" {
   260  			cgroupDriver = "systemd"
   261  
   262  			if err := unstructured.SetNestedField(kubeletConfig.UnstructuredContent(), cgroupDriver, cgroupDriverKey); err != nil {
   263  				return errors.Wrapf(err, "unable to update %q on Kubelet ConfigMap's %q", cgroupDriverKey, cm.Name)
   264  			}
   265  			updated, err := yaml.Marshal(kubeletConfig)
   266  			if err != nil {
   267  				return errors.Wrapf(err, "unable to encode Kubelet ConfigMap's %q to YAML", cm.Name)
   268  			}
   269  			cm.Data[kubeletConfigKey] = string(updated)
   270  		}
   271  	}
   272  
   273  	// Update the name to the new name
   274  	cm.Name = desiredKubeletConfigMapName
   275  	// Clear the resource version. Is this necessary since this cm is actually a DeepCopy()?
   276  	cm.ResourceVersion = ""
   277  
   278  	if err := w.Client.Create(ctx, cm); err != nil && !apierrors.IsAlreadyExists(err) {
   279  		return errors.Wrapf(err, "error creating configmap %s", desiredKubeletConfigMapName)
   280  	}
   281  	return nil
   282  }
   283  
   284  // UpdateAPIServerInKubeadmConfigMap updates api server configuration in kubeadm config map.
   285  func (w *Workload) UpdateAPIServerInKubeadmConfigMap(apiServer bootstrapv1.APIServer) func(*bootstrapv1.ClusterConfiguration) {
   286  	return func(c *bootstrapv1.ClusterConfiguration) {
   287  		c.APIServer = apiServer
   288  	}
   289  }
   290  
   291  // UpdateControllerManagerInKubeadmConfigMap updates controller manager configuration in kubeadm config map.
   292  func (w *Workload) UpdateControllerManagerInKubeadmConfigMap(controllerManager bootstrapv1.ControlPlaneComponent) func(*bootstrapv1.ClusterConfiguration) {
   293  	return func(c *bootstrapv1.ClusterConfiguration) {
   294  		c.ControllerManager = controllerManager
   295  	}
   296  }
   297  
   298  // UpdateSchedulerInKubeadmConfigMap updates scheduler configuration in kubeadm config map.
   299  func (w *Workload) UpdateSchedulerInKubeadmConfigMap(scheduler bootstrapv1.ControlPlaneComponent) func(*bootstrapv1.ClusterConfiguration) {
   300  	return func(c *bootstrapv1.ClusterConfiguration) {
   301  		c.Scheduler = scheduler
   302  	}
   303  }
   304  
   305  // RemoveMachineFromKubeadmConfigMap removes the entry for the machine from the kubeadm configmap.
   306  func (w *Workload) RemoveMachineFromKubeadmConfigMap(ctx context.Context, machine *clusterv1.Machine, version semver.Version) error {
   307  	if machine == nil || machine.Status.NodeRef == nil {
   308  		// Nothing to do, no node for Machine
   309  		return nil
   310  	}
   311  
   312  	return w.RemoveNodeFromKubeadmConfigMap(ctx, machine.Status.NodeRef.Name, version)
   313  }
   314  
   315  // RemoveNodeFromKubeadmConfigMap removes the entry for the node from the kubeadm configmap.
   316  func (w *Workload) RemoveNodeFromKubeadmConfigMap(ctx context.Context, name string, v semver.Version) error {
   317  	if version.Compare(v, minKubernetesVersionWithoutClusterStatus, version.WithoutPreReleases()) >= 0 {
   318  		return nil
   319  	}
   320  
   321  	return w.updateClusterStatus(ctx, func(s *bootstrapv1.ClusterStatus) {
   322  		delete(s.APIEndpoints, name)
   323  	}, v)
   324  }
   325  
   326  // updateClusterStatus gets the ClusterStatus kubeadm-config ConfigMap, converts it to the
   327  // Cluster API representation, and then applies a mutation func; if changes are detected, the
   328  // data are converted back into the Kubeadm API version in use for the target Kubernetes version and the
   329  // kubeadm-config ConfigMap updated.
   330  func (w *Workload) updateClusterStatus(ctx context.Context, mutator func(status *bootstrapv1.ClusterStatus), version semver.Version) error {
   331  	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   332  		key := ctrlclient.ObjectKey{Name: kubeadmConfigKey, Namespace: metav1.NamespaceSystem}
   333  		configMap, err := w.getConfigMap(ctx, key)
   334  		if err != nil {
   335  			return errors.Wrap(err, "failed to get kubeadmConfigMap")
   336  		}
   337  
   338  		currentData, ok := configMap.Data[clusterStatusKey]
   339  		if !ok {
   340  			return errors.Errorf("unable to find %q in the kubeadm-config ConfigMap", clusterStatusKey)
   341  		}
   342  
   343  		currentClusterStatus, err := kubeadmtypes.UnmarshalClusterStatus(currentData)
   344  		if err != nil {
   345  			return errors.Wrapf(err, "unable to decode %q in the kubeadm-config ConfigMap's from YAML", clusterStatusKey)
   346  		}
   347  
   348  		updatedClusterStatus := currentClusterStatus.DeepCopy()
   349  		mutator(updatedClusterStatus)
   350  
   351  		if !reflect.DeepEqual(currentClusterStatus, updatedClusterStatus) {
   352  			updatedData, err := kubeadmtypes.MarshalClusterStatusForVersion(updatedClusterStatus, version)
   353  			if err != nil {
   354  				return errors.Wrapf(err, "unable to encode %q kubeadm-config ConfigMap's to YAML", clusterStatusKey)
   355  			}
   356  			configMap.Data[clusterStatusKey] = updatedData
   357  			if err := w.Client.Update(ctx, configMap); err != nil {
   358  				return errors.Wrap(err, "failed to upgrade the kubeadmConfigMap")
   359  			}
   360  		}
   361  		return nil
   362  	})
   363  }
   364  
   365  // UpdateClusterConfiguration gets the ClusterConfiguration kubeadm-config ConfigMap, converts it to the
   366  // Cluster API representation, and then applies a mutation func; if changes are detected, the
   367  // data are converted back into the Kubeadm API version in use for the target Kubernetes version and the
   368  // kubeadm-config ConfigMap updated.
   369  func (w *Workload) UpdateClusterConfiguration(ctx context.Context, version semver.Version, mutators ...func(*bootstrapv1.ClusterConfiguration)) error {
   370  	return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
   371  		key := ctrlclient.ObjectKey{Name: kubeadmConfigKey, Namespace: metav1.NamespaceSystem}
   372  		configMap, err := w.getConfigMap(ctx, key)
   373  		if err != nil {
   374  			return errors.Wrap(err, "failed to get kubeadmConfigMap")
   375  		}
   376  
   377  		currentData, ok := configMap.Data[clusterConfigurationKey]
   378  		if !ok {
   379  			return errors.Errorf("unable to find %q in the kubeadm-config ConfigMap", clusterConfigurationKey)
   380  		}
   381  
   382  		currentObj, err := kubeadmtypes.UnmarshalClusterConfiguration(currentData)
   383  		if err != nil {
   384  			return errors.Wrapf(err, "unable to decode %q in the kubeadm-config ConfigMap's from YAML", clusterConfigurationKey)
   385  		}
   386  
   387  		updatedObj := currentObj.DeepCopy()
   388  		for i := range mutators {
   389  			mutators[i](updatedObj)
   390  		}
   391  
   392  		if !reflect.DeepEqual(currentObj, updatedObj) {
   393  			updatedData, err := kubeadmtypes.MarshalClusterConfigurationForVersion(updatedObj, version)
   394  			if err != nil {
   395  				return errors.Wrapf(err, "unable to encode %q kubeadm-config ConfigMap's to YAML", clusterConfigurationKey)
   396  			}
   397  			configMap.Data[clusterConfigurationKey] = updatedData
   398  			if err := w.Client.Update(ctx, configMap); err != nil {
   399  				return errors.Wrap(err, "failed to upgrade cluster configuration in the kubeadmConfigMap")
   400  			}
   401  		}
   402  		return nil
   403  	})
   404  }
   405  
   406  // ClusterStatus holds stats information about the cluster.
   407  type ClusterStatus struct {
   408  	// Nodes are a total count of nodes
   409  	Nodes int32
   410  	// ReadyNodes are the count of nodes that are reporting ready
   411  	ReadyNodes int32
   412  	// HasKubeadmConfig will be true if the kubeadm config map has been uploaded, false otherwise.
   413  	HasKubeadmConfig bool
   414  }
   415  
   416  // ClusterStatus returns the status of the cluster.
   417  func (w *Workload) ClusterStatus(ctx context.Context) (ClusterStatus, error) {
   418  	status := ClusterStatus{}
   419  
   420  	// count the control plane nodes
   421  	nodes, err := w.getControlPlaneNodes(ctx)
   422  	if err != nil {
   423  		return status, err
   424  	}
   425  
   426  	for _, node := range nodes.Items {
   427  		nodeCopy := node
   428  		status.Nodes++
   429  		if util.IsNodeReady(&nodeCopy) {
   430  			status.ReadyNodes++
   431  		}
   432  	}
   433  
   434  	// find the kubeadm conifg
   435  	key := ctrlclient.ObjectKey{
   436  		Name:      kubeadmConfigKey,
   437  		Namespace: metav1.NamespaceSystem,
   438  	}
   439  	err = w.Client.Get(ctx, key, &corev1.ConfigMap{})
   440  	// TODO: Consider if this should only return false if the error is IsNotFound.
   441  	// TODO: Consider adding a third state of 'unknown' when there is an error retrieving the config map.
   442  	status.HasKubeadmConfig = err == nil
   443  	return status, nil
   444  }
   445  
   446  // GetAPIServerCertificateExpiry returns the certificate expiry of the apiserver on the given node.
   447  func (w *Workload) GetAPIServerCertificateExpiry(ctx context.Context, kubeadmConfig *bootstrapv1.KubeadmConfig, nodeName string) (*time.Time, error) {
   448  	// Create a proxy.
   449  	p := proxy.Proxy{
   450  		Kind:       "pods",
   451  		Namespace:  metav1.NamespaceSystem,
   452  		KubeConfig: w.restConfig,
   453  		Port:       int(calculateAPIServerPort(kubeadmConfig)),
   454  	}
   455  
   456  	// Create a dialer.
   457  	dialer, err := proxy.NewDialer(p)
   458  	if err != nil {
   459  		return nil, errors.Wrapf(err, "unable to get certificate expiry for kube-apiserver on Node/%s: failed to create dialer", nodeName)
   460  	}
   461  
   462  	// Dial to the kube-apiserver.
   463  	rawConn, err := dialer.DialContextWithAddr(ctx, staticPodName("kube-apiserver", nodeName))
   464  	if err != nil {
   465  		return nil, errors.Wrapf(err, "unable to get certificate expiry for kube-apiserver on Node/%s: unable to dial to kube-apiserver", nodeName)
   466  	}
   467  
   468  	// Execute a TLS handshake over the connection to the kube-apiserver.
   469  	// xref: roughly same code as in tls.DialWithDialer.
   470  	conn := tls.Client(rawConn, &tls.Config{InsecureSkipVerify: true}) //nolint:gosec // Intentionally not verifying the server cert here.
   471  	if err := conn.HandshakeContext(ctx); err != nil {
   472  		_ = rawConn.Close()
   473  		return nil, errors.Wrapf(err, "unable to get certificate expiry for kube-apiserver on Node/%s: TLS handshake with the kube-apiserver failed", nodeName)
   474  	}
   475  	defer conn.Close()
   476  
   477  	// Return the expiry of the peer certificate with cn=kube-apiserver (which is the one generated by kubeadm).
   478  	var kubeAPIServerCert *x509.Certificate
   479  	for _, cert := range conn.ConnectionState().PeerCertificates {
   480  		if cert.Subject.CommonName == kubeadmAPIServerCertCommonName {
   481  			kubeAPIServerCert = cert
   482  		}
   483  	}
   484  	if kubeAPIServerCert == nil {
   485  		return nil, errors.Wrapf(err, "unable to get certificate expiry for kube-apiserver on Node/%s: couldn't get peer certificate with cn=%q", nodeName, kubeadmAPIServerCertCommonName)
   486  	}
   487  	return &kubeAPIServerCert.NotAfter, nil
   488  }
   489  
   490  // calculateAPIServerPort calculates the kube-apiserver bind port based
   491  // on a KubeadmConfig.
   492  func calculateAPIServerPort(config *bootstrapv1.KubeadmConfig) int32 {
   493  	if config.Spec.InitConfiguration != nil &&
   494  		config.Spec.InitConfiguration.LocalAPIEndpoint.BindPort != 0 {
   495  		return config.Spec.InitConfiguration.LocalAPIEndpoint.BindPort
   496  	}
   497  
   498  	if config.Spec.JoinConfiguration != nil &&
   499  		config.Spec.JoinConfiguration.ControlPlane != nil &&
   500  		config.Spec.JoinConfiguration.ControlPlane.LocalAPIEndpoint.BindPort != 0 {
   501  		return config.Spec.JoinConfiguration.ControlPlane.LocalAPIEndpoint.BindPort
   502  	}
   503  
   504  	return 6443
   505  }
   506  
   507  func generateClientCert(caCertEncoded, caKeyEncoded []byte, clientKey *rsa.PrivateKey) (tls.Certificate, error) {
   508  	caCert, err := certs.DecodeCertPEM(caCertEncoded)
   509  	if err != nil {
   510  		return tls.Certificate{}, err
   511  	}
   512  	caKey, err := certs.DecodePrivateKeyPEM(caKeyEncoded)
   513  	if err != nil {
   514  		return tls.Certificate{}, err
   515  	}
   516  	x509Cert, err := newClientCert(caCert, clientKey, caKey)
   517  	if err != nil {
   518  		return tls.Certificate{}, err
   519  	}
   520  	return tls.X509KeyPair(certs.EncodeCertPEM(x509Cert), certs.EncodePrivateKeyPEM(clientKey))
   521  }
   522  
   523  func newClientCert(caCert *x509.Certificate, key *rsa.PrivateKey, caKey crypto.Signer) (*x509.Certificate, error) {
   524  	cfg := certs.Config{
   525  		CommonName: "cluster-api.x-k8s.io",
   526  	}
   527  
   528  	now := time.Now().UTC()
   529  
   530  	tmpl := x509.Certificate{
   531  		SerialNumber: new(big.Int).SetInt64(0),
   532  		Subject: pkix.Name{
   533  			CommonName:   cfg.CommonName,
   534  			Organization: cfg.Organization,
   535  		},
   536  		NotBefore:   now.Add(time.Minute * -5),
   537  		NotAfter:    now.Add(time.Hour * 24 * 365 * 10), // 10 years
   538  		KeyUsage:    x509.KeyUsageDigitalSignature,
   539  		ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
   540  	}
   541  
   542  	b, err := x509.CreateCertificate(rand.Reader, &tmpl, caCert, key.Public(), caKey)
   543  	if err != nil {
   544  		return nil, errors.Wrapf(err, "failed to create signed client certificate: %+v", tmpl)
   545  	}
   546  
   547  	c, err := x509.ParseCertificate(b)
   548  	return c, errors.WithStack(err)
   549  }
   550  
   551  func staticPodName(component, nodeName string) string {
   552  	return fmt.Sprintf("%s-%s", component, nodeName)
   553  }
   554  
   555  // UpdateKubeProxyImageInfo updates kube-proxy image in the kube-proxy DaemonSet.
   556  func (w *Workload) UpdateKubeProxyImageInfo(ctx context.Context, kcp *controlplanev1.KubeadmControlPlane, version semver.Version) error {
   557  	// Return early if we've been asked to skip kube-proxy upgrades entirely.
   558  	if _, ok := kcp.Annotations[controlplanev1.SkipKubeProxyAnnotation]; ok {
   559  		return nil
   560  	}
   561  
   562  	ds := &appsv1.DaemonSet{}
   563  
   564  	if err := w.Client.Get(ctx, ctrlclient.ObjectKey{Name: kubeProxyKey, Namespace: metav1.NamespaceSystem}, ds); err != nil {
   565  		if apierrors.IsNotFound(err) {
   566  			// if kube-proxy is missing, return without errors
   567  			return nil
   568  		}
   569  		return errors.Wrapf(err, "failed to determine if %s daemonset already exists", kubeProxyKey)
   570  	}
   571  
   572  	container := findKubeProxyContainer(ds)
   573  	if container == nil {
   574  		return nil
   575  	}
   576  
   577  	newImageName, err := containerutil.ModifyImageTag(container.Image, kcp.Spec.Version)
   578  	if err != nil {
   579  		return err
   580  	}
   581  
   582  	// Modify the image repository if a value was explicitly set or an upgrade is required.
   583  	imageRepository := ImageRepositoryFromClusterConfig(kcp.Spec.KubeadmConfigSpec.ClusterConfiguration, version)
   584  	if imageRepository != "" {
   585  		newImageName, err = containerutil.ModifyImageRepository(newImageName, imageRepository)
   586  		if err != nil {
   587  			return err
   588  		}
   589  	}
   590  
   591  	if container.Image != newImageName {
   592  		helper, err := patch.NewHelper(ds, w.Client)
   593  		if err != nil {
   594  			return err
   595  		}
   596  		patchKubeProxyImage(ds, newImageName)
   597  		return helper.Patch(ctx, ds)
   598  	}
   599  	return nil
   600  }
   601  
   602  func findKubeProxyContainer(ds *appsv1.DaemonSet) *corev1.Container {
   603  	containers := ds.Spec.Template.Spec.Containers
   604  	for idx := range containers {
   605  		if containers[idx].Name == kubeProxyKey {
   606  			return &containers[idx]
   607  		}
   608  	}
   609  	return nil
   610  }
   611  
   612  func patchKubeProxyImage(ds *appsv1.DaemonSet, image string) {
   613  	containers := ds.Spec.Template.Spec.Containers
   614  	for idx := range containers {
   615  		if containers[idx].Name == kubeProxyKey {
   616  			containers[idx].Image = image
   617  		}
   618  	}
   619  }
   620  
   621  // yamlToUnstructured looks inside a config map for a specific key and extracts the embedded YAML into an
   622  // *unstructured.Unstructured.
   623  func yamlToUnstructured(rawYAML []byte) (*unstructured.Unstructured, error) {
   624  	unst := &unstructured.Unstructured{}
   625  	err := yaml.Unmarshal(rawYAML, unst)
   626  	return unst, err
   627  }
   628  
   629  // ImageRepositoryFromClusterConfig returns the image repository to use. It returns:
   630  //   - clusterConfig.ImageRepository if set.
   631  //   - else either k8s.gcr.io or registry.k8s.io depending on the default registry of the kubeadm
   632  //     binary of the given kubernetes version. This is only done for Kubernetes versions >= v1.22.0
   633  //     and < v1.26.0 because in this version range the default registry was changed.
   634  //
   635  // Note: Please see the following issue for more context: https://github.com/kubernetes-sigs/cluster-api/issues/7833
   636  // tl;dr is that the imageRepository must be in sync with the default registry of kubeadm.
   637  // Otherwise kubeadm preflight checks will fail because kubeadm is trying to pull the CoreDNS image
   638  // from the wrong repository (<registry>/coredns instead of <registry>/coredns/coredns).
   639  func ImageRepositoryFromClusterConfig(clusterConfig *bootstrapv1.ClusterConfiguration, kubernetesVersion semver.Version) string {
   640  	// If ImageRepository is explicitly specified, return early.
   641  	if clusterConfig != nil &&
   642  		clusterConfig.ImageRepository != "" {
   643  		return clusterConfig.ImageRepository
   644  	}
   645  
   646  	// If v1.22.0 <= version < v1.26.0 return the default registry of the
   647  	// corresponding kubeadm binary.
   648  	if kubernetesVersion.GTE(kubeadm.MinKubernetesVersionImageRegistryMigration) &&
   649  		kubernetesVersion.LT(kubeadm.NextKubernetesVersionImageRegistryMigration) {
   650  		return kubeadm.GetDefaultRegistry(kubernetesVersion)
   651  	}
   652  
   653  	// Use defaulting or current values otherwise.
   654  	return ""
   655  }