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  }