k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/phases/upgrade/postupgrade.go (about)

     1  /*
     2  Copyright 2017 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 upgrade
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os"
    24  	"path/filepath"
    25  
    26  	"github.com/pkg/errors"
    27  
    28  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	errorsutil "k8s.io/apimachinery/pkg/util/errors"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	clientset "k8s.io/client-go/kubernetes"
    34  	"k8s.io/klog/v2"
    35  
    36  	kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
    37  	kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
    38  	"k8s.io/kubernetes/cmd/kubeadm/app/phases/addons/dns"
    39  	"k8s.io/kubernetes/cmd/kubeadm/app/phases/addons/proxy"
    40  	"k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/clusterinfo"
    41  	nodebootstraptoken "k8s.io/kubernetes/cmd/kubeadm/app/phases/bootstraptoken/node"
    42  	kubeletphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubelet"
    43  	patchnodephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/patchnode"
    44  	"k8s.io/kubernetes/cmd/kubeadm/app/phases/uploadconfig"
    45  	kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
    46  	dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
    47  )
    48  
    49  // PerformPostUpgradeTasks runs nearly the same functions as 'kubeadm init' would do
    50  // Note that the mark-control-plane phase is left out, not needed, and no token is created as that doesn't belong to the upgrade
    51  func PerformPostUpgradeTasks(client clientset.Interface, cfg *kubeadmapi.InitConfiguration, patchesDir string, dryRun bool, out io.Writer) error {
    52  	var errs []error
    53  
    54  	// Upload currently used configuration to the cluster
    55  	// Note: This is done right in the beginning of cluster initialization; as we might want to make other phases
    56  	// depend on centralized information from this source in the future
    57  	if err := uploadconfig.UploadConfiguration(cfg, client); err != nil {
    58  		errs = append(errs, err)
    59  	}
    60  
    61  	// Create the new, version-branched kubelet ComponentConfig ConfigMap
    62  	if err := kubeletphase.CreateConfigMap(&cfg.ClusterConfiguration, client); err != nil {
    63  		errs = append(errs, errors.Wrap(err, "error creating kubelet configuration ConfigMap"))
    64  	}
    65  
    66  	// Write the new kubelet config down to disk and the env file if needed
    67  	if err := WriteKubeletConfigFiles(cfg, patchesDir, dryRun, out); err != nil {
    68  		errs = append(errs, err)
    69  	}
    70  
    71  	// Annotate the node with the crisocket information, sourced either from the InitConfiguration struct or
    72  	// --cri-socket.
    73  	// TODO: In the future we want to use something more official like NodeStatus or similar for detecting this properly
    74  	if err := patchnodephase.AnnotateCRISocket(client, cfg.NodeRegistration.Name, cfg.NodeRegistration.CRISocket); err != nil {
    75  		errs = append(errs, errors.Wrap(err, "error uploading crisocket"))
    76  	}
    77  
    78  	// Create RBAC rules that makes the bootstrap tokens able to get nodes
    79  	if err := nodebootstraptoken.AllowBoostrapTokensToGetNodes(client); err != nil {
    80  		errs = append(errs, err)
    81  	}
    82  
    83  	// Create/update RBAC rules that makes the bootstrap tokens able to post CSRs
    84  	if err := nodebootstraptoken.AllowBootstrapTokensToPostCSRs(client); err != nil {
    85  		errs = append(errs, err)
    86  	}
    87  
    88  	// Create/update RBAC rules that makes the bootstrap tokens able to get their CSRs approved automatically
    89  	if err := nodebootstraptoken.AutoApproveNodeBootstrapTokens(client); err != nil {
    90  		errs = append(errs, err)
    91  	}
    92  
    93  	// Create/update RBAC rules that makes the nodes to rotate certificates and get their CSRs approved automatically
    94  	if err := nodebootstraptoken.AutoApproveNodeCertificateRotation(client); err != nil {
    95  		errs = append(errs, err)
    96  	}
    97  
    98  	// TODO: Is this needed to do here? I think that updating cluster info should probably be separate from a normal upgrade
    99  	// Create the cluster-info ConfigMap with the associated RBAC rules
   100  	// if err := clusterinfo.CreateBootstrapConfigMapIfNotExists(client, kubeadmconstants.GetAdminKubeConfigPath()); err != nil {
   101  	// 	return err
   102  	//}
   103  	// Create/update RBAC rules that makes the cluster-info ConfigMap reachable
   104  	if err := clusterinfo.CreateClusterInfoRBACRules(client); err != nil {
   105  		errs = append(errs, err)
   106  	}
   107  
   108  	if err := PerformAddonsUpgrade(client, cfg, patchesDir, out); err != nil {
   109  		errs = append(errs, err)
   110  	}
   111  
   112  	return errorsutil.NewAggregate(errs)
   113  }
   114  
   115  // PerformAddonsUpgrade performs the upgrade of the coredns and kube-proxy addons.
   116  func PerformAddonsUpgrade(client clientset.Interface, cfg *kubeadmapi.InitConfiguration, patchesDir string, out io.Writer) error {
   117  	unupgradedControlPlanes, err := unupgradedControlPlaneInstances(client, cfg.NodeRegistration.Name)
   118  	if err != nil {
   119  		return errors.Wrapf(err, "failed to determine whether all the control plane instances have been upgraded")
   120  	}
   121  	if len(unupgradedControlPlanes) > 0 {
   122  		fmt.Fprintf(out, "[upgrade/addons] skip upgrade addons because control plane instances %v have not been upgraded\n", unupgradedControlPlanes)
   123  		return nil
   124  	}
   125  
   126  	var errs []error
   127  
   128  	// If the coredns ConfigMap is missing, show a warning and assume that the
   129  	// DNS addon was skipped during "kubeadm init", and that its redeployment on upgrade is not desired.
   130  	//
   131  	// TODO: remove this once "kubeadm upgrade apply" phases are supported:
   132  	//   https://github.com/kubernetes/kubeadm/issues/1318
   133  	var missingCoreDNSConfigMap bool
   134  	if _, err := client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(
   135  		context.TODO(),
   136  		kubeadmconstants.CoreDNSConfigMap,
   137  		metav1.GetOptions{},
   138  	); err != nil && apierrors.IsNotFound(err) {
   139  		missingCoreDNSConfigMap = true
   140  	}
   141  	if missingCoreDNSConfigMap {
   142  		klog.Warningf("the ConfigMaps %q in the namespace %q were not found. "+
   143  			"Assuming that a DNS server was not deployed for this cluster. "+
   144  			"Note that once 'kubeadm upgrade apply' supports phases you "+
   145  			"will have to skip the DNS upgrade manually",
   146  			kubeadmconstants.CoreDNSConfigMap,
   147  			metav1.NamespaceSystem)
   148  	} else {
   149  		// Upgrade CoreDNS
   150  		if err := dns.EnsureDNSAddon(&cfg.ClusterConfiguration, client, patchesDir, out, false); err != nil {
   151  			errs = append(errs, err)
   152  		}
   153  	}
   154  
   155  	// If the kube-proxy ConfigMap is missing, show a warning and assume that kube-proxy
   156  	// was skipped during "kubeadm init", and that its redeployment on upgrade is not desired.
   157  	//
   158  	// TODO: remove this once "kubeadm upgrade apply" phases are supported:
   159  	//   https://github.com/kubernetes/kubeadm/issues/1318
   160  	if _, err := client.CoreV1().ConfigMaps(metav1.NamespaceSystem).Get(
   161  		context.TODO(),
   162  		kubeadmconstants.KubeProxyConfigMap,
   163  		metav1.GetOptions{},
   164  	); err != nil && apierrors.IsNotFound(err) {
   165  		klog.Warningf("the ConfigMap %q in the namespace %q was not found. "+
   166  			"Assuming that kube-proxy was not deployed for this cluster. "+
   167  			"Note that once 'kubeadm upgrade apply' supports phases you "+
   168  			"will have to skip the kube-proxy upgrade manually",
   169  			kubeadmconstants.KubeProxyConfigMap,
   170  			metav1.NamespaceSystem)
   171  	} else {
   172  		// Upgrade kube-proxy
   173  		if err := proxy.EnsureProxyAddon(&cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, client, out, false); err != nil {
   174  			errs = append(errs, err)
   175  		}
   176  	}
   177  
   178  	return errorsutil.NewAggregate(errs)
   179  }
   180  
   181  // unupgradedControlPlaneInstances returns a list of control palne instances that have not yet been upgraded.
   182  //
   183  // NB. This function can only be called after the current control plane instance has been upgraded already.
   184  // Because it determines whether the other control plane instances have been upgraded by checking whether
   185  // the kube-apiserver image of other control plane instance is the same as that of this instance.
   186  func unupgradedControlPlaneInstances(client clientset.Interface, nodeName string) ([]string, error) {
   187  	selector := labels.SelectorFromSet(labels.Set(map[string]string{
   188  		"component": kubeadmconstants.KubeAPIServer,
   189  	}))
   190  	pods, err := client.CoreV1().Pods(metav1.NamespaceSystem).List(context.TODO(), metav1.ListOptions{
   191  		LabelSelector: selector.String(),
   192  	})
   193  	if err != nil {
   194  		return nil, errors.Wrap(err, "failed to list kube-apiserver Pod from cluster")
   195  	}
   196  	if len(pods.Items) == 0 {
   197  		return nil, errors.Errorf("cannot find kube-apiserver Pod by label selector: %v", selector.String())
   198  	}
   199  
   200  	nodeImageMap := map[string]string{}
   201  
   202  	for _, pod := range pods.Items {
   203  		found := false
   204  		for _, c := range pod.Spec.Containers {
   205  			if c.Name == kubeadmconstants.KubeAPIServer {
   206  				nodeImageMap[pod.Spec.NodeName] = c.Image
   207  				found = true
   208  				break
   209  			}
   210  		}
   211  		if !found {
   212  			return nil, errors.Errorf("cannot find container by name %q for Pod %v", kubeadmconstants.KubeAPIServer, klog.KObj(&pod))
   213  		}
   214  	}
   215  
   216  	upgradedImage, ok := nodeImageMap[nodeName]
   217  	if !ok {
   218  		return nil, errors.Errorf("cannot find kube-apiserver image for current control plane instance %v", nodeName)
   219  	}
   220  
   221  	unupgradedNodes := sets.New[string]()
   222  	for node, image := range nodeImageMap {
   223  		if image != upgradedImage {
   224  			unupgradedNodes.Insert(node)
   225  		}
   226  	}
   227  
   228  	if len(unupgradedNodes) > 0 {
   229  		return sets.List(unupgradedNodes), nil
   230  	}
   231  
   232  	return nil, nil
   233  }
   234  
   235  // WriteKubeletConfigFiles writes the kubelet config file to disk, but first creates a backup of any existing one.
   236  func WriteKubeletConfigFiles(cfg *kubeadmapi.InitConfiguration, patchesDir string, dryRun bool, out io.Writer) error {
   237  	// Set up the kubelet directory to use. If dry-running, this will return a fake directory
   238  	kubeletDir, err := GetKubeletDir(dryRun)
   239  	if err != nil {
   240  		// The error here should never occur in reality, would only be thrown if /tmp doesn't exist on the machine.
   241  		return err
   242  	}
   243  
   244  	// Create a copy of the kubelet config file in the /etc/kubernetes/tmp/ folder.
   245  	backupDir, err := kubeadmconstants.CreateTempDirForKubeadm(kubeadmconstants.KubernetesDir, "kubeadm-kubelet-config")
   246  	if err != nil {
   247  		return err
   248  	}
   249  	src := filepath.Join(kubeletDir, kubeadmconstants.KubeletConfigurationFileName)
   250  	dest := filepath.Join(backupDir, kubeadmconstants.KubeletConfigurationFileName)
   251  
   252  	if !dryRun {
   253  		fmt.Printf("[upgrade] Backing up kubelet config file to %s\n", dest)
   254  		err := kubeadmutil.CopyFile(src, dest)
   255  		if err != nil {
   256  			return errors.Wrap(err, "error backing up the kubelet config file")
   257  		}
   258  	} else {
   259  		fmt.Printf("[dryrun] Would back up kubelet config file to %s\n", dest)
   260  	}
   261  
   262  	errs := []error{}
   263  	// Write the configuration for the kubelet down to disk so the upgraded kubelet can start with fresh config
   264  	if err := kubeletphase.WriteConfigToDisk(&cfg.ClusterConfiguration, kubeletDir, patchesDir, out); err != nil {
   265  		errs = append(errs, errors.Wrap(err, "error writing kubelet configuration to file"))
   266  	}
   267  
   268  	if dryRun { // Print what contents would be written
   269  		err := dryrunutil.PrintDryRunFile(kubeadmconstants.KubeletConfigurationFileName, kubeletDir, kubeadmconstants.KubeletRunDirectory, os.Stdout)
   270  		if err != nil {
   271  			errs = append(errs, errors.Wrap(err, "error printing files on dryrun"))
   272  		}
   273  	}
   274  	return errorsutil.NewAggregate(errs)
   275  }
   276  
   277  // GetKubeletDir gets the kubelet directory based on whether the user is dry-running this command or not.
   278  func GetKubeletDir(dryRun bool) (string, error) {
   279  	if dryRun {
   280  		return kubeadmconstants.CreateTempDirForKubeadm("", "kubeadm-upgrade-dryrun")
   281  	}
   282  	return kubeadmconstants.KubeletRunDirectory, nil
   283  }