github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/internal/operator/orbiter/kinds/clusters/kubernetes/upgrade.go (about)

     1  package kubernetes
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/caos/orbos/internal/operator/common"
     7  	"github.com/caos/orbos/mntr"
     8  	"github.com/caos/orbos/pkg/kubernetes"
     9  )
    10  
    11  type initializedMachines []*initializedMachine
    12  
    13  func (c initializedMachines) Len() int           { return len(c) }
    14  func (c initializedMachines) Swap(i, j int)      { c[i], c[j] = c[j], c[i] }
    15  func (c initializedMachines) Less(i, j int) bool { return c[i].infra.ID() < c[j].infra.ID() }
    16  
    17  func ensureSoftware(
    18  	monitor mntr.Monitor,
    19  	target KubernetesVersion,
    20  	k8sClient *kubernetes.Client,
    21  	controlplane []*initializedMachine,
    22  	workers []*initializedMachine) (bool, error) {
    23  
    24  	sortedMachines := append(controlplane, workers...)
    25  	from, to, err := findPath(monitor, sortedMachines, target)
    26  	if err != nil {
    27  		return false, err
    28  	}
    29  
    30  	monitor.WithFields(map[string]interface{}{
    31  		"currentSoftware":   from,
    32  		"currentKubernetes": from.Kubelet,
    33  		"desiredSofware":    to,
    34  		"desiredKubernetes": to.Kubelet,
    35  	}).Debug("Ensuring kubernetes version")
    36  
    37  	return step(k8sClient, monitor, sortedMachines, from, to)
    38  }
    39  
    40  func findPath(
    41  	monitor mntr.Monitor,
    42  	machines []*initializedMachine,
    43  	target KubernetesVersion,
    44  ) (common.Software, common.Software, error) {
    45  
    46  	var overallLowKubelet KubernetesVersion
    47  	var overallLowKubeletMinor int
    48  	zeroSW := common.Software{}
    49  
    50  	for _, machine := range machines {
    51  		id := machine.infra.ID()
    52  		if machine.node == nil {
    53  			continue
    54  		}
    55  
    56  		nodeinfoKubelet := machine.node.Status.NodeInfo.KubeletVersion
    57  
    58  		monitor.WithFields(map[string]interface{}{
    59  			"machine": id,
    60  			"kubelet": nodeinfoKubelet,
    61  		}).Debug("Found kubelet version from node info")
    62  		kubelet := ParseString(nodeinfoKubelet)
    63  		if kubelet == Unknown {
    64  			return zeroSW, zeroSW, fmt.Errorf("parsing version %s from nodes %s info failed", nodeinfoKubelet, id)
    65  		}
    66  
    67  		kubeletMinor, err := kubelet.ExtractMinor(monitor)
    68  		if err != nil {
    69  			return zeroSW, zeroSW, fmt.Errorf("extracting minor from kubelet version %s from nodes %s info failed: %w", nodeinfoKubelet, id, err)
    70  		}
    71  
    72  		if overallLowKubelet == Unknown {
    73  			overallLowKubelet = kubelet
    74  			overallLowKubeletMinor = kubeletMinor
    75  			continue
    76  		}
    77  
    78  		kubeletPatch, err := kubelet.ExtractPatch(monitor)
    79  		if err != nil {
    80  			return zeroSW, zeroSW, fmt.Errorf("extracting patch from kubelet version %s from nodes %s info failed: %w", nodeinfoKubelet, id, err)
    81  		}
    82  		tmpOverallLowKubeletMinor, err := overallLowKubelet.ExtractMinor(monitor)
    83  		if err != nil {
    84  			return zeroSW, zeroSW, fmt.Errorf("extracting minor from overall kubelet version %s failed: %w", overallLowKubelet, err)
    85  		}
    86  		tmpOverallLowKubeletPatch, err := overallLowKubelet.ExtractPatch(monitor)
    87  		if err != nil {
    88  			return zeroSW, zeroSW, fmt.Errorf("extracting patch from overall kubelet version %s failed: %w", overallLowKubelet, err)
    89  		}
    90  
    91  		if kubeletMinor < tmpOverallLowKubeletMinor ||
    92  			kubeletMinor == tmpOverallLowKubeletMinor && kubeletPatch < tmpOverallLowKubeletPatch {
    93  			overallLowKubelet = kubelet
    94  			overallLowKubeletMinor = kubeletMinor
    95  		}
    96  	}
    97  
    98  	if overallLowKubelet == target || overallLowKubelet == Unknown {
    99  		target := target.DefineSoftware()
   100  		monitor.WithFields(map[string]interface{}{
   101  			"from": overallLowKubelet,
   102  			"to":   target,
   103  		}).Debug("Cluster is up to date")
   104  		return target, target, nil
   105  	}
   106  
   107  	targetMinor, err := target.ExtractMinor(monitor)
   108  	if err != nil {
   109  		return zeroSW, zeroSW, fmt.Errorf("extracting minor from target version %s failed: %w", target, err)
   110  	}
   111  
   112  	if targetMinor < overallLowKubeletMinor {
   113  		return zeroSW, zeroSW, fmt.Errorf("downgrading from %s to %s is not possible as they are on different minors", overallLowKubelet, target)
   114  	}
   115  
   116  	overallLowKubeletSoftware := overallLowKubelet.DefineSoftware()
   117  	if (targetMinor - overallLowKubeletMinor) < 2 {
   118  		monitor.WithFields(map[string]interface{}{
   119  			"from":                   overallLowKubelet,
   120  			"to":                     target,
   121  			"targetMinor":            targetMinor,
   122  			"overallLowKubeletMinor": overallLowKubeletMinor,
   123  		}).Debug("Desired version can be reached directly")
   124  		return overallLowKubeletSoftware, target.DefineSoftware(), nil
   125  	}
   126  
   127  	nextHighestMinor := overallLowKubelet.NextHighestMinor()
   128  	monitor.WithFields(map[string]interface{}{
   129  		"from":         overallLowKubelet,
   130  		"fromMinor":    overallLowKubeletMinor,
   131  		"intermediate": nextHighestMinor,
   132  		"to":           target,
   133  		"toMinor":      targetMinor,
   134  	}).Debug("Desired version can be reached via an intermediate version")
   135  	return overallLowKubeletSoftware, nextHighestMinor.DefineSoftware(), nil
   136  }
   137  
   138  func step(
   139  	k8sClient *kubernetes.Client,
   140  	monitor mntr.Monitor,
   141  	sortedMachines initializedMachines,
   142  	from common.Software,
   143  	to common.Software,
   144  ) (bool, error) {
   145  
   146  	for _, machine := range sortedMachines {
   147  		if machine.node != nil && machine.node.Labels["orbos.ch/updating"] == machine.node.Status.NodeInfo.KubeletVersion {
   148  			delete(machine.node.Labels, "orbos.ch/updating")
   149  			if k8sClient.Tainted(machine.node, kubernetes.Updating) {
   150  				machine.node.Spec.Taints = k8sClient.RemoveFromTaints(machine.node.Spec.Taints, kubernetes.Updating)
   151  			}
   152  			if err := k8sClient.UpdateNode(machine.node); err != nil {
   153  				return false, err
   154  			}
   155  		}
   156  	}
   157  
   158  	for idx, machine := range sortedMachines {
   159  
   160  		next, err := plan(k8sClient, monitor, machine, idx == 0, from, to)
   161  		if err != nil {
   162  			return false, fmt.Errorf("planning machine %s failed: %w", machine.infra.ID(), err)
   163  		}
   164  
   165  		if next == nil {
   166  			continue
   167  		}
   168  		return false, next()
   169  	}
   170  	return true, nil
   171  }
   172  
   173  func plan(
   174  	k8sClient *kubernetes.Client,
   175  	monitor mntr.Monitor,
   176  	machine *initializedMachine,
   177  	isFirstControlplane bool,
   178  	from common.Software,
   179  	to common.Software,
   180  ) (func() error, error) {
   181  	from.Kubeadm = common.Package{}
   182  
   183  	isControlplane := machine.pool.tier == Controlplane
   184  
   185  	id := machine.infra.ID()
   186  	machinemonitor := monitor.WithField("machine", id)
   187  
   188  	awaitNodeAgent := func() error {
   189  		machinemonitor.Info("Awaiting node agent")
   190  		return nil
   191  	}
   192  
   193  	drain := func() error {
   194  		if isControlplane || machine.node == nil || machine.node.Spec.Unschedulable {
   195  			return nil
   196  		}
   197  		machine.node.Labels["orbos.ch/updating"] = to.Kubelet.Version
   198  		return k8sClient.Drain(machine.currentMachine, machine.node, kubernetes.Updating, false)
   199  	}
   200  
   201  	ensureSoftware := func(packages common.Software, phase string) func() error {
   202  		swmonitor := machinemonitor.WithField("phase", phase)
   203  		zeroPkg := common.Package{}
   204  		return func() error {
   205  			if !packages.Kubelet.Equals(zeroPkg) &&
   206  				!machine.currentNodeagent.Software.Kubelet.Equals(packages.Kubelet) ||
   207  				!packages.Containerruntime.Equals(zeroPkg) &&
   208  					!machine.currentNodeagent.Software.Containerruntime.Equals(packages.Containerruntime) ||
   209  				!packages.Kernel.Equals(zeroPkg) &&
   210  					!machine.currentNodeagent.Software.Kernel.Equals(packages.Kernel) {
   211  				if err := drain(); err != nil {
   212  					return err
   213  				}
   214  			}
   215  			if !softwareContains(*machine.desiredNodeagent.Software, packages) {
   216  				swmonitor.Changed("Kubernetes software desired")
   217  			} else {
   218  				swmonitor.Info("Awaiting kubernetes software")
   219  			}
   220  			machine.desiredNodeagent.Software.Merge(packages, true)
   221  			return nil
   222  		}
   223  	}
   224  
   225  	labelUpgradeState := func() error {
   226  		machine.node.Labels["orbos.ch/kubeadm-upgraded"] = to.Kubelet.Version
   227  		return k8sClient.UpdateNode(machine.node)
   228  	}
   229  
   230  	migrate := func() (err error) {
   231  
   232  		defer func() {
   233  			if err != nil {
   234  				err = fmt.Errorf("migrating node %s failed: %w", machine.infra.ID(), err)
   235  			}
   236  		}()
   237  
   238  		if err := drain(); err != nil {
   239  			return err
   240  		}
   241  
   242  		upgradeAction := "node"
   243  		if isFirstControlplane {
   244  			machinemonitor.Info("Migrating first controlplane node")
   245  			upgradeAction = fmt.Sprintf("apply %s --yes", to.Kubelet.Version)
   246  		} else {
   247  			machinemonitor.Info("Migrating node")
   248  		}
   249  
   250  		if _, err := machine.infra.Execute(nil, fmt.Sprintf("sudo kubeadm upgrade %s", upgradeAction)); err != nil {
   251  			return err
   252  		}
   253  
   254  		return labelUpgradeState()
   255  	}
   256  
   257  	nodeIsReady := machine.currentNodeagent.NodeIsReady
   258  
   259  	if !machine.currentMachine.Joined {
   260  		if softwareContains(machine.currentNodeagent.Software, to) {
   261  			if !nodeIsReady {
   262  				return awaitNodeAgent, nil
   263  			}
   264  
   265  			// This node needs to be joined first
   266  			return nil, nil
   267  		}
   268  		return ensureSoftware(to, "Prepare for joining"), nil
   269  	}
   270  
   271  	if !machine.currentNodeagent.Software.Kernel.Equals(to.Kernel) {
   272  		return ensureSoftware(common.Software{Kernel: to.Kernel}, "Update kernel"), nil
   273  	} else {
   274  		machine.desiredNodeagent.Software.Merge(common.Software{Kernel: to.Kernel}, true)
   275  	}
   276  
   277  	if !machine.currentNodeagent.Software.Kubeadm.Equals(to.Kubeadm) || !machine.desiredNodeagent.Software.Kubeadm.Equals(to.Kubeadm) {
   278  		if !softwareContains(machine.currentNodeagent.Software, from) || !softwareContains(*machine.desiredNodeagent.Software, from) {
   279  			return ensureSoftware(from, "Reconcile lower kubernetes software"), nil
   280  		}
   281  
   282  		return ensureSoftware(common.Software{Kubeadm: to.Kubeadm}, "Update kubeadm"), nil
   283  	}
   284  
   285  	kubadmUpgraded := machine.node.Labels["orbos.ch/kubeadm-upgraded"]
   286  	if kubadmUpgraded != to.Kubelet.Version {
   287  
   288  		if kubadmUpgraded == "" {
   289  			return labelUpgradeState, nil
   290  		}
   291  
   292  		return migrate, nil
   293  	}
   294  
   295  	if !softwareContains(machine.currentNodeagent.Software, to) || !softwareContains(*machine.desiredNodeagent.Software, to) {
   296  		return ensureSoftware(to, "Reconcile kubernetes software"), nil
   297  	}
   298  
   299  	if !nodeIsReady {
   300  		return awaitNodeAgent, nil
   301  	}
   302  
   303  	return nil, nil
   304  }