mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-27 16:17:34 +00:00
256 lines
9.0 KiB
Go
256 lines
9.0 KiB
Go
package rollout
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/crossplane/crossplane-runtime/pkg/event"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/apimachinery/pkg/util/intstr"
|
|
"k8s.io/klog/v2"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
|
|
"github.com/oam-dev/kubevela/apis/standard.oam.dev/v1alpha1"
|
|
"github.com/oam-dev/kubevela/pkg/controller/common"
|
|
"github.com/oam-dev/kubevela/pkg/controller/common/rollout/workloads"
|
|
"github.com/oam-dev/kubevela/pkg/oam"
|
|
)
|
|
|
|
// the default time to check back if we still have work to do
|
|
const rolloutReconcileRequeueTime = 5 * time.Second
|
|
|
|
// Controller is the controller that controls the rollout plan resource
|
|
type Controller struct {
|
|
client client.Client
|
|
recorder event.Recorder
|
|
parentController oam.Object
|
|
|
|
rolloutSpec *v1alpha1.RolloutPlan
|
|
rolloutStatus v1alpha1.RolloutStatus
|
|
|
|
targetWorkload *unstructured.Unstructured
|
|
sourceWorkload *unstructured.Unstructured
|
|
}
|
|
|
|
// NewRolloutPlanController creates a RolloutPlanController
|
|
func NewRolloutPlanController(client client.Client, parentController oam.Object, recorder event.Recorder,
|
|
rolloutSpec *v1alpha1.RolloutPlan,
|
|
rolloutStatus v1alpha1.RolloutStatus, targetWorkload,
|
|
sourceWorkload *unstructured.Unstructured) *Controller {
|
|
return &Controller{
|
|
client: client,
|
|
parentController: parentController,
|
|
recorder: recorder,
|
|
rolloutSpec: rolloutSpec,
|
|
rolloutStatus: rolloutStatus,
|
|
targetWorkload: targetWorkload,
|
|
sourceWorkload: sourceWorkload,
|
|
}
|
|
}
|
|
|
|
// Reconcile reconciles a rollout plan
|
|
func (r *Controller) Reconcile(ctx context.Context) (res reconcile.Result, status v1alpha1.RolloutStatus) {
|
|
klog.InfoS("Reconcile the rollout plan", "rollout Spec", r.rolloutSpec,
|
|
"target workload", klog.KObj(r.targetWorkload))
|
|
if r.sourceWorkload != nil {
|
|
klog.InfoS("we will do rolling upgrades", "source workload", klog.KObj(r.sourceWorkload))
|
|
}
|
|
klog.InfoS("rollout spec ", "rollout state", r.rolloutStatus.RollingState, "batch rolling state",
|
|
r.rolloutStatus.BatchRollingState, "current batch", r.rolloutStatus.CurrentBatch, "upgraded Replicas",
|
|
r.rolloutStatus.UpgradedReplicas)
|
|
|
|
defer klog.InfoS("Finished reconciling rollout plan", "rollout state", status.RollingState,
|
|
"batch rolling state", status.BatchRollingState, "current batch", status.CurrentBatch,
|
|
"upgraded Replicas", status.UpgradedReplicas, "reconcile result ", res)
|
|
|
|
status = r.rolloutStatus
|
|
|
|
defer func() {
|
|
if status.RollingState == v1alpha1.RolloutFailedState ||
|
|
status.RollingState == v1alpha1.RolloutSucceedState {
|
|
// no need to requeue if we reach the terminal states
|
|
res = reconcile.Result{}
|
|
} else {
|
|
res = reconcile.Result{
|
|
RequeueAfter: rolloutReconcileRequeueTime,
|
|
}
|
|
}
|
|
}()
|
|
|
|
workloadController, err := r.GetWorkloadController()
|
|
if err != nil {
|
|
r.rolloutStatus.RolloutFailed(err.Error())
|
|
r.recorder.Event(r.parentController, event.Warning("Unsupported workload", err))
|
|
return
|
|
}
|
|
|
|
switch r.rolloutStatus.RollingState {
|
|
case v1alpha1.VerifyingState:
|
|
status = *workloadController.Verify(ctx)
|
|
|
|
case v1alpha1.InitializingState:
|
|
// TODO: call the pre-rollout webhooks
|
|
status = *workloadController.Initialize(ctx)
|
|
|
|
case v1alpha1.RollingInBatchesState:
|
|
status = r.reconcileBatchInRolling(ctx, workloadController)
|
|
|
|
case v1alpha1.FinalisingState:
|
|
// TODO: call the post-rollout webhooks
|
|
status = *workloadController.Finalize(ctx)
|
|
|
|
case v1alpha1.RolloutSucceedState:
|
|
// Nothing to do
|
|
|
|
case v1alpha1.RolloutFailedState:
|
|
// Nothing to do
|
|
|
|
default:
|
|
panic(fmt.Sprintf("illegal rollout status %+v", r.rolloutStatus))
|
|
}
|
|
|
|
return res, status
|
|
}
|
|
|
|
// reconcile logic when we are in the middle of rollout
|
|
func (r *Controller) reconcileBatchInRolling(ctx context.Context, workloadController workloads.WorkloadController) (
|
|
status v1alpha1.RolloutStatus) {
|
|
|
|
if r.rolloutSpec.Paused {
|
|
r.recorder.Event(r.parentController, event.Normal("Rollout paused", "Rollout paused"))
|
|
r.rolloutStatus.SetConditions(v1alpha1.NewPositiveCondition("Paused"))
|
|
return r.rolloutStatus
|
|
}
|
|
|
|
// makes sure that the current batch and replica count in the status are validate
|
|
replicas, err := workloadController.Size(ctx)
|
|
if err != nil {
|
|
r.rolloutStatus.RolloutRetry(err.Error())
|
|
return r.rolloutStatus
|
|
}
|
|
r.validateRollingBatchStatus(int(replicas))
|
|
|
|
switch r.rolloutStatus.BatchRollingState {
|
|
case v1alpha1.BatchInitializingState:
|
|
// TODO: call the pre-batch webhook
|
|
|
|
case v1alpha1.BatchInRollingState:
|
|
// still rolling the batch, the batch rolling is not completed yet
|
|
status = *workloadController.RolloutOneBatchPods(ctx)
|
|
|
|
case v1alpha1.BatchVerifyingState:
|
|
// verifying if the application is ready to roll
|
|
// need to check if they meet the availability requirements in the rollout spec.
|
|
// TODO: evaluate any metrics/analysis
|
|
status = *workloadController.CheckOneBatchPods(ctx)
|
|
|
|
case v1alpha1.BatchFinalizingState:
|
|
// all the pods in the are available
|
|
r.finalizeOneBatch()
|
|
|
|
case v1alpha1.BatchReadyState:
|
|
// all the pods in the are upgraded and their state are ready
|
|
// wait to move to the next batch if there are any
|
|
r.tryMovingToNextBatch()
|
|
|
|
default:
|
|
panic(fmt.Sprintf("illegal status %+v", r.rolloutStatus))
|
|
}
|
|
|
|
return status
|
|
}
|
|
|
|
// check if we can move to the next batch
|
|
func (r *Controller) tryMovingToNextBatch() {
|
|
if r.rolloutSpec.BatchPartition == nil || *r.rolloutSpec.BatchPartition > r.rolloutStatus.CurrentBatch {
|
|
klog.InfoS("ready to rollout the next batch", "current batch", r.rolloutStatus.CurrentBatch)
|
|
r.rolloutStatus.CurrentBatch++
|
|
r.rolloutStatus.StateTransition(v1alpha1.BatchRolloutApprovedEvent)
|
|
} else {
|
|
klog.V(common.LogDebug).InfoS("the current batch is waiting to move on", "current batch",
|
|
r.rolloutStatus.CurrentBatch)
|
|
}
|
|
}
|
|
|
|
func (r *Controller) finalizeOneBatch() {
|
|
// TODO: call the post-batch webhooks if there are any
|
|
currentBatch := int(r.rolloutStatus.CurrentBatch)
|
|
if currentBatch == len(r.rolloutSpec.RolloutBatches)-1 {
|
|
// this is the last batch, mark the rollout finalized
|
|
r.rolloutStatus.StateTransition(v1alpha1.AllBatchFinishedEvent)
|
|
r.recorder.Event(r.parentController, event.Normal("all batches rolled out",
|
|
fmt.Sprintf("upgrade pod = %d, total ready pod = %d", r.rolloutStatus.UpgradedReplicas,
|
|
r.rolloutStatus.UpgradedReadyReplicas)))
|
|
} else {
|
|
klog.InfoS("finished one batch rollout", "current batch", r.rolloutStatus.CurrentBatch)
|
|
// th
|
|
r.recorder.Event(r.parentController, event.Normal("Batch finalized",
|
|
fmt.Sprintf("the batch num = %d is ready", r.rolloutStatus.CurrentBatch)))
|
|
r.rolloutStatus.StateTransition(v1alpha1.FinishedOneBatchEvent)
|
|
}
|
|
}
|
|
|
|
// verify that the upgradedReplicas and current batch in the status are valid according to the spec
|
|
func (r *Controller) validateRollingBatchStatus(totalSize int) bool {
|
|
status := r.rolloutStatus
|
|
spec := r.rolloutSpec
|
|
podCount := 0
|
|
if spec.BatchPartition != nil && *spec.BatchPartition < status.CurrentBatch {
|
|
klog.ErrorS(fmt.Errorf("the current batch value in the status is greater than the batch partition"),
|
|
"batch partition", *spec.BatchPartition, "current batch status", status.CurrentBatch)
|
|
return false
|
|
}
|
|
upgradedReplicas := int(status.UpgradedReplicas)
|
|
currentBatch := int(status.CurrentBatch)
|
|
// calculate the lower bound of the possible pod count just before the current batch
|
|
for i, r := range spec.RolloutBatches {
|
|
if i < currentBatch {
|
|
batchSize, _ := intstr.GetValueFromIntOrPercent(&r.Replicas, totalSize, true)
|
|
podCount += batchSize
|
|
}
|
|
}
|
|
// the recorded number should be at least as much as the all the pods before the current batch
|
|
if podCount > upgradedReplicas {
|
|
klog.ErrorS(fmt.Errorf("the upgraded replica in the status is too small"), "upgraded num status",
|
|
upgradedReplicas, "pods in all the previous batches", podCount)
|
|
return false
|
|
}
|
|
// calculate the upper bound with the current batch
|
|
if currentBatch == len(spec.RolloutBatches)-1 {
|
|
// avoid round up problems
|
|
podCount = totalSize
|
|
} else {
|
|
batchSize, _ := intstr.GetValueFromIntOrPercent(&spec.RolloutBatches[currentBatch].Replicas,
|
|
totalSize, true)
|
|
podCount += batchSize
|
|
}
|
|
// the recorded number should be not as much as the all the pods including the active batch
|
|
if podCount < upgradedReplicas {
|
|
klog.ErrorS(fmt.Errorf("the upgraded replica in the status is too large"), "upgraded num status",
|
|
upgradedReplicas, "pods in the batches including the current batch", podCount)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// GetWorkloadController pick the right workload controller to work on the workload
|
|
func (r *Controller) GetWorkloadController() (workloads.WorkloadController, error) {
|
|
kind := r.targetWorkload.GetObjectKind().GroupVersionKind().Kind
|
|
target := types.NamespacedName{
|
|
Namespace: r.targetWorkload.GetNamespace(),
|
|
Name: r.targetWorkload.GetName(),
|
|
}
|
|
|
|
switch kind {
|
|
case "CloneSet":
|
|
return workloads.NewCloneSetController(r.client, r.recorder, r.parentController,
|
|
r.rolloutSpec, &r.rolloutStatus, target), nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("the workload kind `%s` is not supported", kind)
|
|
}
|
|
}
|