diff --git a/Makefile b/Makefile index 079dc1d..19e0ba2 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ fmt: golangci $(GOLANGCI) fmt .PHONY: lint -lint: golangci +lint: fmt $(GOLANGCI) run .PHONY: test diff --git a/apis/rollout/v1alpha1/rolloutrun_types.go b/apis/rollout/v1alpha1/rolloutrun_types.go index 58f08a9..444623e 100644 --- a/apis/rollout/v1alpha1/rolloutrun_types.go +++ b/apis/rollout/v1alpha1/rolloutrun_types.go @@ -120,6 +120,11 @@ type RolloutRunStepTarget struct { // Replicas is the replicas of the rollout task, which represents the number of pods to be upgraded Replicas intstr.IntOrString `json:"replicas"` + + // ReplicaSlidingWindow used to control the number of pods that are allowed to be upgraded in + // a sliding window for progressive rollout smoothly. + // +optional + ReplicaSlidingWindow *intstr.IntOrString `json:"replicaSlidingWindow,omitempty"` } type RolloutRunStatus struct { diff --git a/apis/rollout/v1alpha1/rolloutstrategy_types.go b/apis/rollout/v1alpha1/rolloutstrategy_types.go index 083aacc..c4c7091 100644 --- a/apis/rollout/v1alpha1/rolloutstrategy_types.go +++ b/apis/rollout/v1alpha1/rolloutstrategy_types.go @@ -90,6 +90,11 @@ type RolloutStep struct { // Replicas is the replicas of the rollout task, which represents the number of pods to be upgraded Replicas intstr.IntOrString `json:"replicas"` + // ReplicaSlidingWindow used to control the number of pods that are allowed to be upgraded in + // a sliding window for progressive rollout smoothly. + // +optional + ReplicaSlidingWindow *intstr.IntOrString `json:"replicaSlidingWindow,omitempty"` + // traffic strategy // +optional Traffic *TrafficStrategy `json:"traffic,omitempty"` diff --git a/apis/rollout/v1alpha1/zz_generated.deepcopy.go b/apis/rollout/v1alpha1/zz_generated.deepcopy.go index 7795a2a..3890c5d 100644 --- a/apis/rollout/v1alpha1/zz_generated.deepcopy.go +++ b/apis/rollout/v1alpha1/zz_generated.deepcopy.go @@ -795,7 +795,9 @@ func (in *RolloutRunCanaryStrategy) DeepCopyInto(out *RolloutRunCanaryStrategy) if in.Targets != nil { in, out := &in.Targets, &out.Targets *out = make([]RolloutRunStepTarget, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Traffic != nil { in, out := &in.Traffic, &out.Traffic @@ -952,7 +954,9 @@ func (in *RolloutRunStep) DeepCopyInto(out *RolloutRunStep) { if in.Targets != nil { in, out := &in.Targets, &out.Targets *out = make([]RolloutRunStepTarget, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Traffic != nil { in, out := &in.Traffic, &out.Traffic @@ -1023,6 +1027,11 @@ func (in *RolloutRunStepTarget) DeepCopyInto(out *RolloutRunStepTarget) { *out = *in out.CrossClusterObjectNameReference = in.CrossClusterObjectNameReference out.Replicas = in.Replicas + if in.ReplicaSlidingWindow != nil { + in, out := &in.ReplicaSlidingWindow, &out.ReplicaSlidingWindow + *out = new(intstr.IntOrString) + **out = **in + } return } @@ -1094,6 +1103,11 @@ func (in *RolloutStatus) DeepCopy() *RolloutStatus { func (in *RolloutStep) DeepCopyInto(out *RolloutStep) { *out = *in out.Replicas = in.Replicas + if in.ReplicaSlidingWindow != nil { + in, out := &in.ReplicaSlidingWindow, &out.ReplicaSlidingWindow + *out = new(intstr.IntOrString) + **out = **in + } if in.Traffic != nil { in, out := &in.Traffic, &out.Traffic *out = new(TrafficStrategy) @@ -1268,7 +1282,9 @@ func (in *RolloutWebhookReviewBatch) DeepCopyInto(out *RolloutWebhookReviewBatch if in.Targets != nil { in, out := &in.Targets, &out.Targets *out = make([]RolloutRunStepTarget, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Properties != nil { in, out := &in.Properties, &out.Properties @@ -1296,7 +1312,9 @@ func (in *RolloutWebhookReviewCanary) DeepCopyInto(out *RolloutWebhookReviewCana if in.Targets != nil { in, out := &in.Targets, &out.Targets *out = make([]RolloutRunStepTarget, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.Properties != nil { in, out := &in.Properties, &out.Properties diff --git a/config/crd/bases/rollout.kusionstack.io_rolloutruns.yaml b/config/crd/bases/rollout.kusionstack.io_rolloutruns.yaml index 3ed0ac6..0f09e3b 100644 --- a/config/crd/bases/rollout.kusionstack.io_rolloutruns.yaml +++ b/config/crd/bases/rollout.kusionstack.io_rolloutruns.yaml @@ -90,6 +90,14 @@ spec: name: description: Name is the resource name type: string + replicaSlidingWindow: + anyOf: + - type: integer + - type: string + description: |- + ReplicaSlidingWindow used to control the number of pods that are allowed to be upgraded in + a sliding window for progressive rollout smoothly. + x-kubernetes-int-or-string: true replicas: anyOf: - type: integer @@ -485,6 +493,14 @@ spec: name: description: Name is the resource name type: string + replicaSlidingWindow: + anyOf: + - type: integer + - type: string + description: |- + ReplicaSlidingWindow used to control the number of pods that are allowed to be upgraded in + a sliding window for progressive rollout smoothly. + x-kubernetes-int-or-string: true replicas: anyOf: - type: integer diff --git a/config/crd/bases/rollout.kusionstack.io_rolloutstrategies.yaml b/config/crd/bases/rollout.kusionstack.io_rolloutstrategies.yaml index 2aa5a6b..45b6238 100644 --- a/config/crd/bases/rollout.kusionstack.io_rolloutstrategies.yaml +++ b/config/crd/bases/rollout.kusionstack.io_rolloutstrategies.yaml @@ -113,6 +113,14 @@ spec: description: Properties contains additional information for step type: object + replicaSlidingWindow: + anyOf: + - type: integer + - type: string + description: |- + ReplicaSlidingWindow used to control the number of pods that are allowed to be upgraded in + a sliding window for progressive rollout smoothly. + x-kubernetes-int-or-string: true replicas: anyOf: - type: integer diff --git a/config/kind/workload/bases/rollout.yaml b/config/kind/workload/bases/rollout.yaml index 184577b..9d5829d 100644 --- a/config/kind/workload/bases/rollout.yaml +++ b/config/kind/workload/bases/rollout.yaml @@ -44,5 +44,6 @@ batch: batches: - replicas: 1 - replicas: 3 + replicaSlidingWindow: 1 breakpoint: true - replicas: 100% diff --git a/pkg/controllers/rollout/utils.go b/pkg/controllers/rollout/utils.go index 477c15d..19aae2a 100644 --- a/pkg/controllers/rollout/utils.go +++ b/pkg/controllers/rollout/utils.go @@ -155,7 +155,8 @@ func constructRolloutRunBatches(strategy *rolloutv1alpha1.BatchStrategy, workloa Cluster: info.ClusterName, Name: info.Name, }, - Replicas: b.Replicas, + Replicas: b.Replicas, + ReplicaSlidingWindow: b.ReplicaSlidingWindow, } targets = append(targets, target) } diff --git a/pkg/controllers/rollout/utils_test.go b/pkg/controllers/rollout/utils_test.go index 9a753dc..0884fd4 100644 --- a/pkg/controllers/rollout/utils_test.go +++ b/pkg/controllers/rollout/utils_test.go @@ -21,6 +21,7 @@ import ( "github.com/davecgh/go-spew/spew" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" rolloutv1alpha1 "kusionstack.io/rollout/apis/rollout/v1alpha1" "kusionstack.io/rollout/pkg/workload" @@ -61,8 +62,9 @@ func Test_constructRolloutRunBatches(t *testing.T) { }, }, { - Breakpoint: true, - Replicas: intstr.FromString("50%"), + Breakpoint: true, + Replicas: intstr.FromString("50%"), + ReplicaSlidingWindow: ptr.To(intstr.FromString("10%")), Match: &rolloutv1alpha1.ResourceMatch{ Names: []rolloutv1alpha1.CrossClusterObjectNameReference{ { @@ -128,14 +130,16 @@ func Test_constructRolloutRunBatches(t *testing.T) { Cluster: "cluster-a", Name: "test-1", }, - Replicas: intstr.FromString("50%"), + Replicas: intstr.FromString("50%"), + ReplicaSlidingWindow: ptr.To(intstr.FromString("10%")), }, { CrossClusterObjectNameReference: rolloutv1alpha1.CrossClusterObjectNameReference{ Cluster: "cluster-b", Name: "test-1", }, - Replicas: intstr.FromString("50%"), + Replicas: intstr.FromString("50%"), + ReplicaSlidingWindow: ptr.To(intstr.FromString("10%")), }, }, }, diff --git a/pkg/controllers/rolloutrun/control/control.go b/pkg/controllers/rolloutrun/control/control.go index a39669f..cd2f633 100644 --- a/pkg/controllers/rolloutrun/control/control.go +++ b/pkg/controllers/rolloutrun/control/control.go @@ -78,7 +78,7 @@ func (c *BatchReleaseControl) Initialize(info *workload.Info, ownerKind, ownerNa return err } -func (c *BatchReleaseControl) UpdatePartition(info *workload.Info, expectedUpdated intstr.IntOrString) (bool, error) { +func (c *BatchReleaseControl) UpdatePartition(info *workload.Info, expectedUpdated int32) (bool, error) { ctx := clusterinfo.WithCluster(context.Background(), info.ClusterName) obj := info.Object return utils.UpdateOnConflict(ctx, c.client, c.client, obj, func() error { diff --git a/pkg/controllers/rolloutrun/executor/batch.go b/pkg/controllers/rolloutrun/executor/batch.go index 9e9ef4f..e1e2e3e 100644 --- a/pkg/controllers/rolloutrun/executor/batch.go +++ b/pkg/controllers/rolloutrun/executor/batch.go @@ -21,6 +21,7 @@ import ( "time" utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/intstr" ctrl "sigs.k8s.io/controller-runtime" rolloutv1alpha1 "kusionstack.io/rollout/apis/rollout/v1alpha1" @@ -210,9 +211,9 @@ func (e *batchExecutor) doBatchUpgrading(ctx *ExecutorContext) (bool, time.Durat status := info.APIStatus() batchTargetStatuses = append(batchTargetStatuses, info.APIStatus()) - expectedUpdatedReplicas, _ := workload.CalculateUpdatedReplicas(&status.Replicas, item.Replicas) + currentBatchExpectedReplicas, _ := workload.CalculateUpdatedReplicas(&status.Replicas, item.Replicas) - if info.CheckUpdatedReady(expectedUpdatedReplicas) { + if info.CheckUpdatedReady(currentBatchExpectedReplicas) { // if the target is ready, we will not change partition continue } @@ -220,13 +221,18 @@ func (e *batchExecutor) doBatchUpgrading(ctx *ExecutorContext) (bool, time.Durat allWorkloadReady = false logger.V(3).Info("still waiting for target to be ready", "target", item.CrossClusterObjectNameReference) + expectedReplicas, err := e.calculateExpectedReplicasBySlidingWindow(status, currentBatchExpectedReplicas, item.ReplicaSlidingWindow) + if err != nil { + return false, retryStop, err + } + // ensure partition: upgradePartition is an idempotent function - changed, err := batchControl.UpdatePartition(info, item.Replicas) + changed, err := batchControl.UpdatePartition(info, expectedReplicas) if err != nil { return false, retryStop, err } if changed { - logger.V(2).Info("upgrade target partition", "target", item.CrossClusterObjectNameReference, "partition", expectedUpdatedReplicas) + logger.V(2).Info("upgrade target partition", "target", item.CrossClusterObjectNameReference, "partition", expectedReplicas) } } @@ -240,3 +246,20 @@ func (e *batchExecutor) doBatchUpgrading(ctx *ExecutorContext) (bool, time.Durat // wait for next reconcile return false, retryDefault, nil } + +// calculateExpectedReplicasBySlidingWindow calculate expected replicas by sliding window +// if window is nil, return currentBatchExpectedReplicas +// if window is not nil, return min(currentBatchExpectedReplicas, updatedAvailableReplicas + increment) +func (e *batchExecutor) calculateExpectedReplicasBySlidingWindow(status rolloutv1alpha1.RolloutWorkloadStatus, currentBatchExpectedReplicas int32, window *intstr.IntOrString) (int32, error) { + if window == nil { + return currentBatchExpectedReplicas, nil + } + increment, err := workload.CalculateUpdatedReplicas(&status.Replicas, *window) + if err != nil { + return currentBatchExpectedReplicas, err + } + expected := status.UpdatedAvailableReplicas + increment + // limit expected replicas to currentBatchExpectedReplicas + expected = min(currentBatchExpectedReplicas, expected) + return expected, nil +} diff --git a/pkg/controllers/rolloutrun/executor/batch_test.go b/pkg/controllers/rolloutrun/executor/batch_test.go index 97ce0cb..1844ba2 100644 --- a/pkg/controllers/rolloutrun/executor/batch_test.go +++ b/pkg/controllers/rolloutrun/executor/batch_test.go @@ -315,12 +315,17 @@ func (s *batchExecutorTestSuite) Test_BatchExecutor_Do() { } func newRunStepTarget(cluster, name string, replicas intstr.IntOrString) rolloutv1alpha1.RolloutRunStepTarget { + return newRunStepTargetWithSlidingWindow(cluster, name, replicas, nil) +} + +func newRunStepTargetWithSlidingWindow(cluster, name string, replicas intstr.IntOrString, window *intstr.IntOrString) rolloutv1alpha1.RolloutRunStepTarget { return rolloutv1alpha1.RolloutRunStepTarget{ CrossClusterObjectNameReference: rolloutv1alpha1.CrossClusterObjectNameReference{ Cluster: cluster, Name: name, }, - Replicas: replicas, + Replicas: replicas, + ReplicaSlidingWindow: window, } } @@ -482,7 +487,7 @@ func (s *batchExecutorTestSuite) Test_BatchExecutor_Do_Running() { }, }, { - name: "workflow instance not found", + name: "workload instance not found", getObjects: func() (*rolloutv1alpha1.Rollout, *rolloutv1alpha1.RolloutRun) { rollout := s.rollout.DeepCopy() rolloutRun := s.rolloutRun.DeepCopy() @@ -569,13 +574,90 @@ func (s *batchExecutorTestSuite) Test_BatchExecutor_Do_Running() { for _, obj := range objs { if s.IsType(&appsv1.StatefulSet{}, obj) { sts := obj.(*appsv1.StatefulSet) - s.NotNil(sts.Spec.UpdateStrategy.RollingUpdate) - s.NotNil(sts.Spec.UpdateStrategy.RollingUpdate.Partition) + s.Require().NotNil(sts.Spec.UpdateStrategy.RollingUpdate) + s.Require().NotNil(sts.Spec.UpdateStrategy.RollingUpdate.Partition) s.EqualValues(90, *sts.Spec.UpdateStrategy.RollingUpdate.Partition) } } }, }, + { + name: "upgrade workload partition by sliding window", + getObjects: func() (*rolloutv1alpha1.Rollout, *rolloutv1alpha1.RolloutRun) { + rollout := s.rollout.DeepCopy() + rolloutRun := s.rolloutRun.DeepCopy() + + // setup rolloutRun + rolloutRun.Spec.Batch.Batches = []rolloutv1alpha1.RolloutRunStep{{ + Targets: []rolloutv1alpha1.RolloutRunStepTarget{ + // test-a with normal sliding window + newRunStepTargetWithSlidingWindow("cluster-a", "test-a", intstr.FromInt(50), ptr.To(intstr.FromInt(10))), + // test-b with a too big sliding window + newRunStepTargetWithSlidingWindow("cluster-a", "test-b", intstr.FromInt(10), ptr.To(intstr.FromInt(50))), + }, + }} + rolloutRun.Status.Phase = rolloutv1alpha1.RolloutRunPhaseProgressing + rolloutRun.Status.BatchStatus = &rolloutv1alpha1.RolloutRunBatchStatus{ + RolloutBatchStatus: rolloutv1alpha1.RolloutBatchStatus{ + CurrentBatchIndex: 0, + CurrentBatchState: StepRunning, + }, + Records: []rolloutv1alpha1.RolloutRunStepStatus{ + { + Index: ptr.To[int32](0), + State: StepRunning, + StartTime: ptr.To(metav1.Now()), + }, + }, + } + return rollout, rolloutRun + }, + getWorkloads: func() []client.Object { + return []client.Object{ + newFakeObject("cluster-a", "default", "test-a", 100, 20, 15), + newFakeObject("cluster-a", "default", "test-b", 100, 0, 0), + } + }, + assertResult: func(done bool, result reconcile.Result, err error) { + s.Require().NoError(err) + s.Equal(reconcile.Result{RequeueAfter: retryDefault}, result) + s.False(done) + }, + assertStatus: func(status *rolloutv1alpha1.RolloutRunStatus) { + s.Len(status.BatchStatus.Records, 1) + s.Len(status.BatchStatus.Records[0].Targets, 2) + + for _, target := range status.BatchStatus.Records[0].Targets { + s.EqualValues(100, target.Replicas) + switch target.Name { + case "test-a": + s.EqualValues(15, target.UpdatedReplicas) + s.EqualValues(15, target.UpdatedReadyReplicas) + s.EqualValues(15, target.UpdatedAvailableReplicas) + case "test-b": + s.EqualValues(0, target.UpdatedReplicas) + s.EqualValues(0, target.UpdatedReadyReplicas) + s.EqualValues(0, target.UpdatedAvailableReplicas) + } + } + }, + assertWorkloads: func(objs []client.Object) { + for _, obj := range objs { + if s.IsType(&appsv1.StatefulSet{}, obj) { + sts := obj.(*appsv1.StatefulSet) + s.Require().NotNil(sts.Spec.UpdateStrategy.RollingUpdate) + s.Require().NotNil(sts.Spec.UpdateStrategy.RollingUpdate.Partition) + switch sts.Name { + case "test-a": + // partition = total(100) - (updatedReplicas(15) + slidingWindow(10) ) = 75 + s.EqualValues(75, *sts.Spec.UpdateStrategy.RollingUpdate.Partition) + case "test-b": + s.EqualValues(90, *sts.Spec.UpdateStrategy.RollingUpdate.Partition) + } + } + } + }, + }, { name: "waiting for workload ready", getObjects: func() (*rolloutv1alpha1.Rollout, *rolloutv1alpha1.RolloutRun) { diff --git a/pkg/controllers/rolloutrun/executor/default_test.go b/pkg/controllers/rolloutrun/executor/default_test.go index eb9c664..c512c44 100644 --- a/pkg/controllers/rolloutrun/executor/default_test.go +++ b/pkg/controllers/rolloutrun/executor/default_test.go @@ -147,13 +147,28 @@ func newFakeObject(cluster, namespace, name string, replicas, partition, updated }, }, Status: appsv1.StatefulSetStatus{ - Replicas: replicas, - UpdatedReplicas: updated, + Replicas: replicas, + UpdatedReplicas: updated, + ReadyReplicas: replicas, + AvailableReplicas: replicas, }, } if realPartition <= 0 { sts.Spec.UpdateStrategy.RollingUpdate = nil } + + if partition == 0 { + // partition == 0 means all replicas are updated + sts.Status.CurrentRevision = "v1" + sts.Status.UpdateRevision = "v1" + sts.Status.CurrentReplicas = updated + sts.Status.UpdatedReplicas = updated + } else { + sts.Status.CurrentRevision = "v1" + sts.Status.UpdateRevision = "v2" + sts.Status.CurrentReplicas = replicas - updated + sts.Status.UpdatedReplicas = updated + } return sts } diff --git a/pkg/workload/collaset/release.go b/pkg/workload/collaset/release.go index 5f618fc..39f01e9 100644 --- a/pkg/workload/collaset/release.go +++ b/pkg/workload/collaset/release.go @@ -18,8 +18,8 @@ package collaset import ( "fmt" + "maps" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" operatingv1alpha1 "kusionstack.io/kube-api/apps/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,7 +45,7 @@ func (c *accessorImpl) BatchPreCheck(object client.Object) error { return nil } -func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated intstr.IntOrString) error { +func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated int32) error { obj, err := checkObj(object) if err != nil { return err @@ -56,10 +56,7 @@ func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated ints specPartition = ptr.Deref(obj.Spec.UpdateStrategy.RollingUpdate.ByPartition.Partition, 0) } - expectedPartition, err := workload.CalculateExpectedPartition(obj.Spec.Replicas, expectedUpdated, specPartition) - if err != nil { - return err - } + expectedPartition := workload.CalculateExpectedPartition(obj.Spec.Replicas, expectedUpdated, specPartition) if expectedPartition > 0 { obj.Spec.UpdateStrategy.RollingUpdate = &operatingv1alpha1.RollingUpdateCollaSetStrategy{ @@ -106,23 +103,17 @@ func applyPodTemplateMetadataPatch(obj *operatingv1alpha1.CollaSet, patch *rollo if obj.Spec.Selector.MatchLabels == nil { obj.Spec.Selector.MatchLabels = make(map[string]string) } - for k, v := range patch.Labels { - obj.Spec.Selector.MatchLabels[k] = v - } + maps.Copy(obj.Spec.Selector.MatchLabels, patch.Labels) if obj.Spec.Template.Labels == nil { obj.Spec.Template.Labels = make(map[string]string) } - for k, v := range patch.Labels { - obj.Spec.Template.Labels[k] = v - } + maps.Copy(obj.Spec.Template.Labels, patch.Labels) } if len(patch.Annotations) > 0 { if obj.Spec.Template.Annotations == nil { obj.Spec.Template.Annotations = make(map[string]string) } - for k, v := range patch.Annotations { - obj.Spec.Template.Annotations[k] = v - } + maps.Copy(obj.Spec.Template.Annotations, patch.Annotations) } } diff --git a/pkg/workload/collaset/release_test.go b/pkg/workload/collaset/release_test.go index e510abc..86aa8f2 100644 --- a/pkg/workload/collaset/release_test.go +++ b/pkg/workload/collaset/release_test.go @@ -18,7 +18,6 @@ package collaset import ( "github.com/stretchr/testify/suite" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" operatingv1alpha1 "kusionstack.io/kube-api/apps/v1alpha1" ) @@ -46,13 +45,13 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { tests := []struct { name string object *operatingv1alpha1.CollaSet - input intstr.IntOrString + input int32 checkResult func(object *operatingv1alpha1.CollaSet, err error) }{ { name: "total 10, want to update 1", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromInt(1), + input: 1, checkResult: func(object *operatingv1alpha1.CollaSet, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -63,9 +62,9 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, { - name: "total 10, want to update 60%", + name: "total 10, want to update 6", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromString("60%"), + input: 6, checkResult: func(object *operatingv1alpha1.CollaSet, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -76,9 +75,9 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, { - name: "total 10, updated 9, want to update 50%", + name: "total 10, updated 9, want to update 5", object: newTestApplyPartitionObject(10, 9), - input: intstr.FromString("50%"), + input: 5, checkResult: func(object *operatingv1alpha1.CollaSet, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -89,9 +88,9 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, { - name: "total 10, want to update 100%", + name: "total 10, want to update 10", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromString("100%"), + input: 10, checkResult: func(object *operatingv1alpha1.CollaSet, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate.ByPartition.Partition) @@ -100,7 +99,7 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { { name: "total 10, want to update 11", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromInt(11), + input: 11, checkResult: func(object *operatingv1alpha1.CollaSet, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate.ByPartition.Partition) @@ -116,7 +115,7 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, }, - input: intstr.FromInt(10), + input: 10, checkResult: func(object *operatingv1alpha1.CollaSet, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate) @@ -134,7 +133,7 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, }, - input: intstr.FromInt(10), + input: 10, checkResult: func(object *operatingv1alpha1.CollaSet, err error) { s.Require().NoError(err) if s.NotNil(object.Spec.UpdateStrategy.RollingUpdate) { diff --git a/pkg/workload/collaset/suit_test.go b/pkg/workload/collaset/suit_test.go index 345d0b5..b0813ef 100644 --- a/pkg/workload/collaset/suit_test.go +++ b/pkg/workload/collaset/suit_test.go @@ -8,6 +8,6 @@ import ( // In order for 'go test' to run this suite, we need to create // a normal test function and pass our suite to suite.Run -func TestTestSuite(t *testing.T) { +func TestReleaseControlTestSuite(t *testing.T) { suite.Run(t, new(releaseControlTestSuite)) } diff --git a/pkg/workload/interface.go b/pkg/workload/interface.go index 38f8eba..3630912 100644 --- a/pkg/workload/interface.go +++ b/pkg/workload/interface.go @@ -18,7 +18,6 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" "kusionstack.io/rollout/apis/rollout/v1alpha1" @@ -49,7 +48,7 @@ type BatchReleaseControl interface { // BatchPreCheck checks object before batch release. BatchPreCheck(obj client.Object) error // ApplyPartition use expectedUpdated replicas to calculate partition and apply it to the workload. - ApplyPartition(obj client.Object, expectedUpdated intstr.IntOrString) error + ApplyPartition(obj client.Object, expectedUpdatedReplicas int32) error } // CanaryReleaseControl defines the control functions for workload canary release diff --git a/pkg/workload/poddecoration/release.go b/pkg/workload/poddecoration/release.go index e87c5d4..a574859 100644 --- a/pkg/workload/poddecoration/release.go +++ b/pkg/workload/poddecoration/release.go @@ -19,7 +19,6 @@ package poddecoration import ( "fmt" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" operatingv1alpha1 "kusionstack.io/kube-api/apps/v1alpha1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -41,7 +40,7 @@ func (c *accessorImpl) BatchPreCheck(object client.Object) error { return nil } -func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated intstr.IntOrString) error { +func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated int32) error { // object must be *operatingv1alpha1.PodDecoration obj, err := checkObj(object) if err != nil { @@ -53,10 +52,7 @@ func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated ints specPartition = ptr.Deref(obj.Spec.UpdateStrategy.RollingUpdate.Partition, 0) } - expectedPartition, err := workload.CalculateExpectedPartition(&obj.Status.MatchedPods, expectedUpdated, specPartition) - if err != nil { - return err - } + expectedPartition := workload.CalculateExpectedPartition(&obj.Status.MatchedPods, expectedUpdated, specPartition) if expectedPartition == 0 { obj.Spec.UpdateStrategy.RollingUpdate = nil @@ -65,13 +61,5 @@ func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated ints Partition: ptr.To(expectedPartition), } } - - if expectedPartition > 0 { - obj.Spec.UpdateStrategy.RollingUpdate = &operatingv1alpha1.PodDecorationRollingUpdate{ - Partition: ptr.To(expectedPartition), - } - } else if obj.Spec.UpdateStrategy.RollingUpdate != nil { - obj.Spec.UpdateStrategy.RollingUpdate.Partition = nil - } return nil } diff --git a/pkg/workload/poddecoration/release_test.go b/pkg/workload/poddecoration/release_test.go index 84ab866..8cbe0b8 100644 --- a/pkg/workload/poddecoration/release_test.go +++ b/pkg/workload/poddecoration/release_test.go @@ -18,7 +18,6 @@ package poddecoration import ( "github.com/stretchr/testify/suite" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" operatingv1alpha1 "kusionstack.io/kube-api/apps/v1alpha1" ) @@ -46,13 +45,13 @@ func (s *releaseControlTestSuite) TestApplyPartition() { tests := []struct { name string object *operatingv1alpha1.PodDecoration - input intstr.IntOrString + input int32 checkResult func(object *operatingv1alpha1.PodDecoration, err error) }{ { name: "total 10, want to update 1", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromInt(1), + input: 1, checkResult: func(object *operatingv1alpha1.PodDecoration, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -62,9 +61,9 @@ func (s *releaseControlTestSuite) TestApplyPartition() { }, }, { - name: "total 10, want to update 60%", + name: "total 10, want to update 6", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromString("60%"), + input: 6, checkResult: func(object *operatingv1alpha1.PodDecoration, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -74,9 +73,9 @@ func (s *releaseControlTestSuite) TestApplyPartition() { }, }, { - name: "total 10, updated 9, want to update 50%", + name: "total 10, updated 9, want to update 5", object: newTestApplyPartitionObject(10, 9), - input: intstr.FromString("50%"), + input: 5, checkResult: func(object *operatingv1alpha1.PodDecoration, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -86,9 +85,9 @@ func (s *releaseControlTestSuite) TestApplyPartition() { }, }, { - name: "total 10, want to update 100%", + name: "total 10, want to update 10", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromString("100%"), + input: 10, checkResult: func(object *operatingv1alpha1.PodDecoration, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate) @@ -97,7 +96,7 @@ func (s *releaseControlTestSuite) TestApplyPartition() { { name: "total 10, want to update 11", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromInt(11), + input: 11, checkResult: func(object *operatingv1alpha1.PodDecoration, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate) @@ -115,7 +114,7 @@ func (s *releaseControlTestSuite) TestApplyPartition() { MatchedPods: 10, }, }, - input: intstr.FromInt(10), + input: 10, checkResult: func(object *operatingv1alpha1.PodDecoration, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate) diff --git a/pkg/workload/poddecoration/suit_test.go b/pkg/workload/poddecoration/suit_test.go index d546ae0..ed59ebe 100644 --- a/pkg/workload/poddecoration/suit_test.go +++ b/pkg/workload/poddecoration/suit_test.go @@ -8,6 +8,6 @@ import ( // In order for 'go test' to run this suite, we need to create // a normal test function and pass our suite to suite.Run -func TestTestSuite(t *testing.T) { +func TestReleaseControlTestSuite(t *testing.T) { suite.Run(t, new(releaseControlTestSuite)) } diff --git a/pkg/workload/statefulset/accessor.go b/pkg/workload/statefulset/accessor.go index e842314..f0b5fd5 100644 --- a/pkg/workload/statefulset/accessor.go +++ b/pkg/workload/statefulset/accessor.go @@ -65,15 +65,27 @@ func (s *accessorImpl) GetInfo(cluster string, object client.Object) (*workload. } func (p *accessorImpl) getStatus(obj *appsv1.StatefulSet) workload.InfoStatus { - return workload.InfoStatus{ - ObservedGeneration: obj.Status.ObservedGeneration, - StableRevision: obj.Status.CurrentRevision, - UpdatedRevision: obj.Status.UpdateRevision, - Replicas: ptr.Deref(obj.Spec.Replicas, 0), - UpdatedReplicas: obj.Status.UpdatedReplicas, - UpdatedReadyReplicas: obj.Status.UpdatedReplicas, - UpdatedAvailableReplicas: obj.Status.UpdatedReplicas, + info := workload.InfoStatus{ + ObservedGeneration: obj.Status.ObservedGeneration, + StableRevision: obj.Status.CurrentRevision, + UpdatedRevision: obj.Status.UpdateRevision, + Replicas: ptr.Deref(obj.Spec.Replicas, 0), + UpdatedReplicas: obj.Status.UpdatedReplicas, } + + if obj.Status.CurrentRevision == obj.Status.UpdateRevision { + // if someone delete pod directly, the status.updatedReplicas will be reduced firstly + // then Status.ReadyReplicas and Status.AvailableReplicas will be reduced. + // So we neet to respect the min value. + info.UpdatedReadyReplicas = min(obj.Status.ReadyReplicas, obj.Status.UpdatedReplicas) + info.UpdatedAvailableReplicas = min(obj.Status.AvailableReplicas, obj.Status.UpdatedReplicas) + } else { + // status.ReadyReplicas contains all current and updated replicas + info.UpdatedReadyReplicas = min(max(obj.Status.ReadyReplicas-obj.Status.CurrentReplicas, 0), obj.Status.UpdatedReplicas) + info.UpdatedAvailableReplicas = min(max(obj.Status.AvailableReplicas-obj.Status.CurrentReplicas, 0), obj.Status.UpdatedReplicas) + } + + return info } func checkObj(object client.Object) (*appsv1.StatefulSet, error) { diff --git a/pkg/workload/statefulset/accessor_test.go b/pkg/workload/statefulset/accessor_test.go new file mode 100644 index 0000000..2cf2bd4 --- /dev/null +++ b/pkg/workload/statefulset/accessor_test.go @@ -0,0 +1,126 @@ +// Copyright 2025 The KusionStack Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package statefulset + +import ( + "github.com/stretchr/testify/suite" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/utils/ptr" + + "kusionstack.io/rollout/pkg/workload" +) + +type accessorTestSuite struct { + suite.Suite +} + +func (s *accessorTestSuite) Test_getStatus() { + tests := []struct { + name string + object *appsv1.StatefulSet + want workload.InfoStatus + }{ + { + name: "normal", + object: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To[int32](10), + }, + Status: appsv1.StatefulSetStatus{ + ObservedGeneration: 1, + CurrentRevision: "v1", + UpdateRevision: "v1", + Replicas: 10, + CurrentReplicas: 4, + UpdatedReplicas: 4, + ReadyReplicas: 9, + AvailableReplicas: 9, + }, + }, + want: workload.InfoStatus{ + Replicas: 10, + ObservedGeneration: 1, + StableRevision: "v1", + UpdatedRevision: "v1", + UpdatedReplicas: 4, + UpdatedReadyReplicas: 4, + UpdatedAvailableReplicas: 4, + }, + }, + { + name: "upgrading", + object: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To[int32](10), + }, + Status: appsv1.StatefulSetStatus{ + ObservedGeneration: 1, + CurrentRevision: "v1", + UpdateRevision: "v2", + Replicas: 10, + CurrentReplicas: 4, + UpdatedReplicas: 3, + ReadyReplicas: 9, + AvailableReplicas: 9, + }, + }, + want: workload.InfoStatus{ + Replicas: 10, + ObservedGeneration: 1, + StableRevision: "v1", + UpdatedRevision: "v2", + UpdatedReplicas: 3, + UpdatedReadyReplicas: 3, + UpdatedAvailableReplicas: 3, + }, + }, + + { + name: "upgrading 2", + object: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Replicas: ptr.To[int32](10), + }, + Status: appsv1.StatefulSetStatus{ + ObservedGeneration: 1, + CurrentRevision: "v1", + UpdateRevision: "v2", + Replicas: 10, + CurrentReplicas: 4, + UpdatedReplicas: 6, + ReadyReplicas: 9, + AvailableReplicas: 8, + }, + }, + want: workload.InfoStatus{ + Replicas: 10, + ObservedGeneration: 1, + StableRevision: "v1", + UpdatedRevision: "v2", + UpdatedReplicas: 6, + UpdatedReadyReplicas: 5, + UpdatedAvailableReplicas: 4, + }, + }, + } + for i := range tests { + tt := tests[i] + s.Run(tt.name, func() { + c := &accessorImpl{} + got := c.getStatus(tt.object) + s.Equal(tt.want, got) + }) + } +} diff --git a/pkg/workload/statefulset/release.go b/pkg/workload/statefulset/release.go index 721c486..869060c 100644 --- a/pkg/workload/statefulset/release.go +++ b/pkg/workload/statefulset/release.go @@ -18,9 +18,9 @@ package statefulset import ( "fmt" + "maps" appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -44,7 +44,7 @@ func (c *accessorImpl) BatchPreCheck(object client.Object) error { return nil } -func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated intstr.IntOrString) error { +func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated int32) error { obj, err := checkObj(object) if err != nil { return err @@ -55,10 +55,7 @@ func (c *accessorImpl) ApplyPartition(object client.Object, expectedUpdated ints specPartition = ptr.Deref(obj.Spec.UpdateStrategy.RollingUpdate.Partition, 0) } - expectedPartition, err := workload.CalculateExpectedPartition(obj.Spec.Replicas, expectedUpdated, specPartition) - if err != nil { - return err - } + expectedPartition := workload.CalculateExpectedPartition(obj.Spec.Replicas, expectedUpdated, specPartition) if expectedPartition > 0 { obj.Spec.UpdateStrategy = appsv1.StatefulSetUpdateStrategy{ @@ -105,23 +102,17 @@ func applyPodTemplateMetadataPatch(obj *appsv1.StatefulSet, patch *rolloutv1alph if obj.Spec.Selector.MatchLabels == nil { obj.Spec.Selector.MatchLabels = make(map[string]string) } - for k, v := range patch.Labels { - obj.Spec.Selector.MatchLabels[k] = v - } + maps.Copy(obj.Spec.Selector.MatchLabels, patch.Labels) if obj.Spec.Template.Labels == nil { obj.Spec.Template.Labels = make(map[string]string) } - for k, v := range patch.Labels { - obj.Spec.Template.Labels[k] = v - } + maps.Copy(obj.Spec.Template.Labels, patch.Labels) } if len(patch.Annotations) > 0 { if obj.Spec.Template.Annotations == nil { obj.Spec.Template.Annotations = make(map[string]string) } - for k, v := range patch.Annotations { - obj.Spec.Template.Annotations[k] = v - } + maps.Copy(obj.Spec.Template.Annotations, patch.Annotations) } } diff --git a/pkg/workload/statefulset/release_test.go b/pkg/workload/statefulset/release_test.go index 91caecb..0c7daea 100644 --- a/pkg/workload/statefulset/release_test.go +++ b/pkg/workload/statefulset/release_test.go @@ -19,7 +19,6 @@ package statefulset import ( "github.com/stretchr/testify/suite" appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/utils/ptr" ) @@ -45,13 +44,13 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { tests := []struct { name string object *appsv1.StatefulSet - input intstr.IntOrString + input int32 checkResult func(object *appsv1.StatefulSet, err error) }{ { name: "total 10, want to update 1", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromInt(1), + input: 1, checkResult: func(object *appsv1.StatefulSet, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -61,9 +60,9 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, { - name: "total 10, want to update 60%", + name: "total 10, want to update 6", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromString("60%"), + input: 6, checkResult: func(object *appsv1.StatefulSet, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -73,9 +72,9 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, { - name: "total 10, updated 9, want to update 50%", + name: "total 10, updated 9, want to update 5", object: newTestApplyPartitionObject(10, 9), - input: intstr.FromString("50%"), + input: 5, checkResult: func(object *appsv1.StatefulSet, err error) { s.Require().NoError(err) s.Require().NotNil(object.Spec.UpdateStrategy.RollingUpdate) @@ -85,9 +84,9 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, { - name: "total 10, want to update 100%", + name: "total 10, want to update 10", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromString("100%"), + input: 10, checkResult: func(object *appsv1.StatefulSet, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate) @@ -96,7 +95,7 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { { name: "total 10, want to update 11", object: newTestApplyPartitionObject(10, 0), - input: intstr.FromInt(11), + input: 11, checkResult: func(object *appsv1.StatefulSet, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate) @@ -112,7 +111,7 @@ func (s *releaseControlTestSuite) Test_ApplyPartition() { }, }, }, - input: intstr.FromInt(10), + input: 10, checkResult: func(object *appsv1.StatefulSet, err error) { s.Require().NoError(err) s.Nil(object.Spec.UpdateStrategy.RollingUpdate) diff --git a/pkg/workload/statefulset/suit_test.go b/pkg/workload/statefulset/suit_test.go index ec70a6f..95754d2 100644 --- a/pkg/workload/statefulset/suit_test.go +++ b/pkg/workload/statefulset/suit_test.go @@ -8,6 +8,10 @@ import ( // In order for 'go test' to run this suite, we need to create // a normal test function and pass our suite to suite.Run -func TestTestSuite(t *testing.T) { +func TestReleaseControlTestSuite(t *testing.T) { suite.Run(t, new(releaseControlTestSuite)) } + +func TestAccessorTestSuite(t *testing.T) { + suite.Run(t, new(accessorTestSuite)) +} diff --git a/pkg/workload/util.go b/pkg/workload/util.go index 5d158b9..f19d1d7 100644 --- a/pkg/workload/util.go +++ b/pkg/workload/util.go @@ -15,6 +15,8 @@ package workload import ( + "maps" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" @@ -51,24 +53,16 @@ func CalculateUpdatedReplicas(totalReplicas *int32, expectedReplicas intstr.IntO // CalculateExpectedPartition calculates the expected partition based on the total replicas, expected replicas, and the partition in the spec. // In this function, partition means how many replicas are not updated. -func CalculateExpectedPartition(total *int32, expectedReplicas intstr.IntOrString, partitionInSpec int32) (int32, error) { - expectedUpdatedReplicas, err := CalculateUpdatedReplicas(total, expectedReplicas) - if err != nil { - return 0, err - } - +func CalculateExpectedPartition(total *int32, expectedUpdatedReplicas, partitionInSpec int32) int32 { totalReplicas := ptr.Deref(total, 0) - currentUpdatedReplicas := totalReplicas - partitionInSpec - if currentUpdatedReplicas < 0 { - currentUpdatedReplicas = 0 - } + currentUpdatedReplicas := max(totalReplicas-partitionInSpec, 0) if currentUpdatedReplicas >= expectedUpdatedReplicas { // already updated if the current updated partition is greater than or equal to the expected updated partition - return partitionInSpec, nil + return partitionInSpec } - return totalReplicas - expectedUpdatedReplicas, nil + return max(totalReplicas-expectedUpdatedReplicas, 0) } // PatchMetadata patches metadata with the given patch @@ -77,17 +71,13 @@ func PatchMetadata(meta *metav1.ObjectMeta, patch rolloutv1alpha1.MetadataPatch) if meta.Labels == nil { meta.Labels = make(map[string]string) } - for k, v := range patch.Labels { - meta.Labels[k] = v - } + maps.Copy(meta.Labels, patch.Labels) } if len(patch.Annotations) > 0 { if meta.Annotations == nil { meta.Annotations = make(map[string]string) } - for k, v := range patch.Annotations { - meta.Annotations[k] = v - } + maps.Copy(meta.Annotations, patch.Annotations) } } diff --git a/pkg/workload/util_test.go b/pkg/workload/util_test.go index 06eaa71..91ae3a7 100644 --- a/pkg/workload/util_test.go +++ b/pkg/workload/util_test.go @@ -16,15 +16,13 @@ package workload import ( "testing" - - "k8s.io/apimachinery/pkg/util/intstr" ) func TestCalculateExpectedPartition(t *testing.T) { tests := []struct { name string total int32 - expectedReplicas intstr.IntOrString + expectedReplicas int32 partitionInSpec int32 want int32 wantErr bool @@ -32,7 +30,7 @@ func TestCalculateExpectedPartition(t *testing.T) { { name: "total 10, current partition 10, want to update 1", total: 10, - expectedReplicas: intstr.FromInt(1), + expectedReplicas: 1, partitionInSpec: 10, want: 9, wantErr: false, @@ -40,7 +38,7 @@ func TestCalculateExpectedPartition(t *testing.T) { { name: "total 10, current partition 5, want to update 1", total: 10, - expectedReplicas: intstr.FromInt(1), + expectedReplicas: 1, partitionInSpec: 5, want: 5, wantErr: false, @@ -48,7 +46,7 @@ func TestCalculateExpectedPartition(t *testing.T) { { name: "total 10, current partition 0, want to update 1", total: 10, - expectedReplicas: intstr.FromInt(1), + expectedReplicas: 1, partitionInSpec: 0, want: 0, wantErr: false, @@ -56,7 +54,7 @@ func TestCalculateExpectedPartition(t *testing.T) { { name: "total 10, current partition 15, want to update 0", total: 10, - expectedReplicas: intstr.FromInt(0), + expectedReplicas: 0, partitionInSpec: 15, want: 15, wantErr: false, @@ -64,20 +62,24 @@ func TestCalculateExpectedPartition(t *testing.T) { { name: "total 10, current partition 15, want to update 1", total: 10, - expectedReplicas: intstr.FromInt(1), + expectedReplicas: 1, partitionInSpec: 15, want: 9, wantErr: false, }, + { + name: "total 10, current partition 0, want to update 15", + total: 10, + expectedReplicas: 15, + partitionInSpec: 0, + want: 0, + wantErr: false, + }, } for i := range tests { tt := tests[i] t.Run(tt.name, func(t *testing.T) { - got, err := CalculateExpectedPartition(&tt.total, tt.expectedReplicas, tt.partitionInSpec) - if (err != nil) != tt.wantErr { - t.Errorf("CalculateExpectedPartition() error = %v, wantErr %v", err, tt.wantErr) - return - } + got := CalculateExpectedPartition(&tt.total, tt.expectedReplicas, tt.partitionInSpec) if got != tt.want { t.Errorf("CalculateExpectedPartition() = %v, want %v", got, tt.want) }