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 }