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 }