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