github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/controllers/apps/configuration/rolling_upgrade_policy.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package configuration 21 22 import ( 23 "context" 24 "fmt" 25 "os" 26 27 corev1 "k8s.io/api/core/v1" 28 "sigs.k8s.io/controller-runtime/pkg/client" 29 30 appsv1alpha1 "github.com/1aal/kubeblocks/apis/apps/v1alpha1" 31 cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core" 32 "github.com/1aal/kubeblocks/pkg/configuration/util" 33 "github.com/1aal/kubeblocks/pkg/constant" 34 podutil "github.com/1aal/kubeblocks/pkg/controllerutil" 35 viper "github.com/1aal/kubeblocks/pkg/viperx" 36 ) 37 38 const ( 39 // StatefulSetSpec.Spec.MinReadySeconds 40 // units: s 41 defaultMinReadySeconds = 10 42 ) 43 44 type rollingUpgradePolicy struct { 45 } 46 47 func init() { 48 RegisterPolicy(appsv1alpha1.RollingPolicy, &rollingUpgradePolicy{}) 49 if err := viper.BindEnv(constant.PodMinReadySecondsEnv); err != nil { 50 os.Exit(-1) 51 } 52 viper.SetDefault(constant.PodMinReadySecondsEnv, defaultMinReadySeconds) 53 } 54 55 func (r *rollingUpgradePolicy) Upgrade(params reconfigureParams) (ReturnedStatus, error) { 56 var funcs RollingUpgradeFuncs 57 58 switch params.WorkloadType() { 59 case appsv1alpha1.Consensus: 60 funcs = GetConsensusRollingUpgradeFuncs() 61 case appsv1alpha1.Replication: 62 funcs = GetReplicationRollingUpgradeFuncs() 63 case appsv1alpha1.Stateful: 64 funcs = GetStatefulSetRollingUpgradeFuncs() 65 default: 66 return makeReturnedStatus(ESNotSupport), cfgcore.MakeError("not supported component workload type[%s]", params.WorkloadType()) 67 } 68 return performRollingUpgrade(params, funcs) 69 } 70 71 func (r *rollingUpgradePolicy) GetPolicyName() string { 72 return string(appsv1alpha1.RollingPolicy) 73 } 74 75 func canPerformUpgrade(pods []corev1.Pod, params reconfigureParams) bool { 76 target := params.getTargetReplicas() 77 if len(pods) == target { 78 return true 79 } 80 if params.WorkloadType() == appsv1alpha1.Consensus { 81 params.Ctx.Log.Info(fmt.Sprintf("wait for consensus component is ready, %d pods are ready, and the expected replicas is %d.", len(pods), target)) 82 return false 83 } 84 if len(pods) < target { 85 params.Ctx.Log.Info(fmt.Sprintf("component pods are not all ready, %d pods are ready, which is less than the expected replicas(%d).", len(pods), target)) 86 return false 87 } 88 return true 89 } 90 91 func performRollingUpgrade(params reconfigureParams, funcs RollingUpgradeFuncs) (ReturnedStatus, error) { 92 pods, err := funcs.GetPodsFunc(params) 93 if err != nil { 94 return makeReturnedStatus(ESFailedAndRetry), err 95 } 96 97 var ( 98 rollingReplicas = params.maxRollingReplicas() 99 configKey = params.getConfigKey() 100 configVersion = params.getTargetVersionHash() 101 ) 102 103 if !canPerformUpgrade(pods, params) { 104 return makeReturnedStatus(ESRetry), nil 105 } 106 107 podStats := staticPodStats(pods, params.getTargetReplicas(), params.podMinReadySeconds()) 108 podWins := markDynamicCursor(pods, podStats, configKey, configVersion, rollingReplicas) 109 if !validPodState(podWins) { 110 params.Ctx.Log.Info("wait for pod stat ready.") 111 return makeReturnedStatus(ESRetry), nil 112 } 113 114 waitRollingPods := podWins.getWaitRollingPods() 115 if len(waitRollingPods) == 0 { 116 return makeReturnedStatus(ESNone, withSucceed(int32(podStats.targetReplica)), withExpected(int32(podStats.targetReplica))), nil 117 } 118 119 for _, pod := range waitRollingPods { 120 if podStats.isUpdating(&pod) { 121 params.Ctx.Log.Info("pod is in rolling update.", "pod name", pod.Name) 122 continue 123 } 124 if err := funcs.RestartContainerFunc(&pod, params.Ctx.Ctx, params.ContainerNames, params.ReconfigureClientFactory); err != nil { 125 return makeReturnedStatus(ESFailedAndRetry), err 126 } 127 if err := updatePodLabelsWithConfigVersion(&pod, configKey, configVersion, params.Client, params.Ctx.Ctx); err != nil { 128 return makeReturnedStatus(ESFailedAndRetry), err 129 } 130 } 131 132 return makeReturnedStatus(ESRetry, 133 withExpected(int32(podStats.targetReplica)), 134 withSucceed(int32(len(podStats.updated)+len(podStats.updating)))), nil 135 } 136 137 func validPodState(wind switchWindow) bool { 138 for i := 0; i < wind.begin; i++ { 139 pod := &wind.pods[i] 140 if !wind.isReady(pod) { 141 return false 142 } 143 } 144 return true 145 } 146 147 func markDynamicCursor(pods []corev1.Pod, podsStats *componentPodStats, configKey, currentVersion string, rollingReplicas int32) switchWindow { 148 podWindows := switchWindow{ 149 end: 0, 150 begin: len(pods), 151 pods: pods, 152 componentPodStats: podsStats, 153 } 154 155 // find update last 156 for i := podsStats.targetReplica - 1; i >= 0; i-- { 157 pod := &pods[i] 158 if !podutil.IsMatchConfigVersion(pod, configKey, currentVersion) { 159 podWindows.end = i + 1 160 break 161 } 162 if !podsStats.isAvailable(pod) { 163 podsStats.updating[pod.Name] = pod 164 podWindows.end = i + 1 165 break 166 } 167 podsStats.updated[pod.Name] = pod 168 } 169 170 podWindows.begin = util.Max[int](podWindows.end-int(rollingReplicas), 0) 171 for i := podWindows.begin; i < podWindows.end; i++ { 172 pod := &pods[i] 173 if podutil.IsMatchConfigVersion(pod, configKey, currentVersion) { 174 podsStats.updating[pod.Name] = pod 175 } 176 } 177 return podWindows 178 } 179 180 func staticPodStats(pods []corev1.Pod, targetReplicas int, minReadySeconds int32) *componentPodStats { 181 podsStats := &componentPodStats{ 182 updated: make(map[string]*corev1.Pod), 183 updating: make(map[string]*corev1.Pod), 184 available: make(map[string]*corev1.Pod), 185 ready: make(map[string]*corev1.Pod), 186 targetReplica: targetReplicas, 187 } 188 189 for i := 0; i < len(pods); i++ { 190 pod := &pods[i] 191 switch { 192 case podutil.IsAvailable(pod, minReadySeconds): 193 podsStats.available[pod.Name] = pod 194 case podutil.PodIsReady(pod): 195 podsStats.ready[pod.Name] = pod 196 default: 197 } 198 } 199 return podsStats 200 } 201 202 type componentPodStats struct { 203 // failed to start pod 204 ready map[string]*corev1.Pod 205 available map[string]*corev1.Pod 206 207 // updated pod count 208 updated map[string]*corev1.Pod 209 updating map[string]*corev1.Pod 210 211 // expected pod 212 targetReplica int 213 } 214 215 func (s *componentPodStats) isAvailable(pod *corev1.Pod) bool { 216 _, ok := s.available[pod.Name] 217 return ok 218 } 219 220 func (s *componentPodStats) isReady(pod *corev1.Pod) bool { 221 _, ok := s.ready[pod.Name] 222 return ok || s.isAvailable(pod) 223 } 224 225 func (s *componentPodStats) isUpdating(pod *corev1.Pod) bool { 226 _, ok := s.updating[pod.Name] 227 return ok 228 } 229 230 type switchWindow struct { 231 begin int 232 end int 233 234 pods []corev1.Pod 235 *componentPodStats 236 } 237 238 func (w *switchWindow) getWaitRollingPods() []corev1.Pod { 239 return w.pods[w.begin:w.end] 240 } 241 242 func updatePodLabelsWithConfigVersion(pod *corev1.Pod, labelKey, configVersion string, cli client.Client, ctx context.Context) error { 243 patch := client.MergeFrom(pod.DeepCopy()) 244 if pod.Labels == nil { 245 pod.Labels = make(map[string]string, 1) 246 } 247 pod.Labels[labelKey] = configVersion 248 return cli.Patch(ctx, pod, patch) 249 }