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 }