rollouts/pkg/controller/rollout/rollout_releaseManager_test.go

419 lines
14 KiB
Go

/*
Copyright 2025 The Kruise 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 rollout
import (
"context"
"testing"
"github.com/openkruise/rollouts/api/v1beta1"
"github.com/openkruise/rollouts/pkg/util"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func TestFetchBatchRelease(t *testing.T) {
testCases := []struct {
name string
existingObjects []runtime.Object
namespace string
releaseName string
expectFound bool
expectError bool
}{
{
name: "BatchRelease exists",
existingObjects: []runtime.Object{
&v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default"},
},
},
namespace: "default",
releaseName: "my-rollout",
expectFound: true,
expectError: false,
},
{
name: "BatchRelease does not exist",
existingObjects: []runtime.Object{},
namespace: "default",
releaseName: "my-rollout",
expectFound: false,
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(tc.existingObjects...).Build()
br, err := fetchBatchRelease(fakeClient, tc.namespace, tc.releaseName)
if tc.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tc.expectFound {
assert.NotNil(t, br)
assert.Equal(t, tc.releaseName, br.Name)
} else {
assert.True(t, br.Name == "" || err != nil)
}
})
}
}
func TestRemoveRolloutProgressingAnnotation(t *testing.T) {
workload := &util.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: "my-deployment",
Namespace: "default",
Annotations: map[string]string{
util.InRolloutProgressingAnnotation: "true",
},
},
}
rollout := &v1beta1.Rollout{
Spec: v1beta1.RolloutSpec{
WorkloadRef: v1beta1.ObjectRef{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "my-deployment",
},
},
}
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "my-deployment",
Namespace: "default",
Annotations: map[string]string{
util.InRolloutProgressingAnnotation: "true",
},
},
}
t.Run("should remove annotation when present", func(t *testing.T) {
c := &RolloutContext{Rollout: rollout, Workload: workload}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(deployment).Build()
err := removeRolloutProgressingAnnotation(fakeClient, c)
assert.NoError(t, err)
updatedDeployment := &appsv1.Deployment{}
err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: "my-deployment", Namespace: "default"}, updatedDeployment)
assert.NoError(t, err)
assert.NotContains(t, updatedDeployment.Annotations, util.InRolloutProgressingAnnotation)
})
t.Run("should do nothing if workload is nil", func(t *testing.T) {
c := &RolloutContext{Rollout: rollout, Workload: nil}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
err := removeRolloutProgressingAnnotation(fakeClient, c)
assert.NoError(t, err)
})
t.Run("should do nothing if annotation is not present", func(t *testing.T) {
workloadWithoutAnnotation := &util.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: "my-deployment",
Namespace: "default",
Annotations: map[string]string{}, // No annotation
},
}
deploymentWithoutAnnotation := deployment.DeepCopy()
deploymentWithoutAnnotation.Annotations = map[string]string{}
c := &RolloutContext{Rollout: rollout, Workload: workloadWithoutAnnotation}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(deploymentWithoutAnnotation).Build()
err := removeRolloutProgressingAnnotation(fakeClient, c)
assert.NoError(t, err)
})
}
func TestRemoveBatchRelease(t *testing.T) {
rollout := &v1beta1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default"},
}
c := &RolloutContext{Rollout: rollout}
t.Run("should return false, nil if BatchRelease not found", func(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
retry, err := removeBatchRelease(fakeClient, c)
assert.NoError(t, err)
assert.False(t, retry)
})
t.Run("should return true, nil if BatchRelease is being deleted", func(t *testing.T) {
now := metav1.Now()
br := &v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{
Name: "my-rollout",
Namespace: "default",
DeletionTimestamp: &now,
Finalizers: []string{"rollouts.kruise.io/batch-release-finalizer"},
},
}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(br).Build()
retry, err := removeBatchRelease(fakeClient, c)
assert.NoError(t, err)
assert.True(t, retry)
})
t.Run("should delete BatchRelease and return true, nil", func(t *testing.T) {
br := &v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default"},
}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(br).Build()
retry, err := removeBatchRelease(fakeClient, c)
assert.NoError(t, err)
assert.True(t, retry)
err = fakeClient.Get(context.TODO(), types.NamespacedName{Name: "my-rollout", Namespace: "default"}, br)
assert.True(t, client.IgnoreNotFound(err) == nil)
})
}
func TestFinalizingBatchRelease(t *testing.T) {
rollout := &v1beta1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default"},
}
baseBR := &v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default"},
Spec: v1beta1.BatchReleaseSpec{
ReleasePlan: v1beta1.ReleasePlan{
BatchPartition: int32p(1),
},
},
}
testCases := []struct {
name string
c *RolloutContext
existingBR *v1beta1.BatchRelease
expectRetry bool
expectError bool
expectedPolicy v1beta1.FinalizingPolicyType
expectPatch bool
}{
{
name: "BatchRelease not found",
c: &RolloutContext{Rollout: rollout},
existingBR: nil, // Not in the client
expectRetry: false,
expectError: false,
},
{
name: "BatchRelease already completed",
c: &RolloutContext{Rollout: rollout},
existingBR: &v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default"},
Spec: v1beta1.BatchReleaseSpec{ReleasePlan: v1beta1.ReleasePlan{BatchPartition: nil}},
Status: v1beta1.BatchReleaseStatus{Phase: v1beta1.RolloutPhaseCompleted},
},
expectRetry: false,
expectError: false,
},
{
name: "Patch to WaitResume policy",
c: &RolloutContext{Rollout: rollout, WaitReady: true},
existingBR: func() *v1beta1.BatchRelease {
br := baseBR.DeepCopy()
br.Spec.ReleasePlan.FinalizingPolicy = v1beta1.ImmediateFinalizingPolicyType
return br
}(),
expectRetry: true,
expectError: false,
expectedPolicy: v1beta1.WaitResumeFinalizingPolicyType,
expectPatch: true,
},
{
name: "Patch to Immediate policy",
c: &RolloutContext{Rollout: rollout, WaitReady: false},
existingBR: func() *v1beta1.BatchRelease {
br := baseBR.DeepCopy()
br.Spec.ReleasePlan.FinalizingPolicy = v1beta1.WaitResumeFinalizingPolicyType
return br
}(),
expectRetry: true,
expectError: false,
expectedPolicy: v1beta1.ImmediateFinalizingPolicyType,
expectPatch: true,
},
{
name: "Policy already correct (WaitResume), no patch needed",
c: &RolloutContext{Rollout: rollout, WaitReady: true},
existingBR: &v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default"},
Spec: v1beta1.BatchReleaseSpec{
ReleasePlan: v1beta1.ReleasePlan{
BatchPartition: nil,
FinalizingPolicy: v1beta1.WaitResumeFinalizingPolicyType,
},
},
},
expectRetry: true,
expectError: false,
expectPatch: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var objs []runtime.Object
if tc.existingBR != nil {
objs = append(objs, tc.existingBR)
}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
retry, err := finalizingBatchRelease(fakeClient, tc.c)
assert.Equal(t, tc.expectRetry, retry)
if tc.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tc.expectPatch {
updatedBR := &v1beta1.BatchRelease{}
getErr := fakeClient.Get(context.TODO(), types.NamespacedName{Name: rollout.Name, Namespace: rollout.Namespace}, updatedBR)
assert.NoError(t, getErr)
assert.Nil(t, updatedBR.Spec.ReleasePlan.BatchPartition)
assert.Equal(t, tc.expectedPolicy, updatedBR.Spec.ReleasePlan.FinalizingPolicy)
}
})
}
}
type mockReleaseManager struct {
client client.Client
}
func (m *mockReleaseManager) fetchClient() client.Client { return m.client }
func (m *mockReleaseManager) runCanary(c *RolloutContext) error { return nil }
func (m *mockReleaseManager) doCanaryJump(c *RolloutContext) bool { return false }
func (m *mockReleaseManager) doCanaryFinalising(c *RolloutContext) (bool, error) { return false, nil }
func (m *mockReleaseManager) createBatchRelease(rollout *v1beta1.Rollout, rolloutID string, batch int32, isRollback bool) *v1beta1.BatchRelease {
plan := rollout.Spec.Strategy.Canary.Steps[batch]
var replicasValue intstr.IntOrString
if plan.Replicas != nil {
replicasValue = *plan.Replicas
}
br := &v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{
Name: rollout.Name,
Namespace: rollout.Namespace,
Annotations: map[string]string{"rollout-id": rolloutID},
},
Spec: v1beta1.BatchReleaseSpec{
WorkloadRef: v1beta1.ObjectRef{
APIVersion: rollout.Spec.WorkloadRef.APIVersion,
Kind: rollout.Spec.WorkloadRef.Kind,
Name: rollout.Spec.WorkloadRef.Name,
},
ReleasePlan: v1beta1.ReleasePlan{
Batches: []v1beta1.ReleaseBatch{
{CanaryReplicas: replicasValue},
},
BatchPartition: int32p(0),
},
},
}
return br
}
func TestRunBatchRelease(t *testing.T) {
rollout := &v1beta1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default", UID: "rollout-uid-12345"},
Spec: v1beta1.RolloutSpec{
WorkloadRef: v1beta1.ObjectRef{APIVersion: "apps/v1", Kind: "Deployment", Name: "my-app"},
Strategy: v1beta1.RolloutStrategy{
Canary: &v1beta1.CanaryStrategy{
Steps: []v1beta1.CanaryStep{
{Replicas: &intstr.IntOrString{Type: intstr.String, StrVal: "10%"}}, // Index 0
{Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: 5}}, // Index 1
},
},
},
},
}
rolloutID := string(rollout.UID)
t.Run("should_create_new_BatchRelease_if_not_found", func(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
m := &mockReleaseManager{client: fakeClient}
done, br, err := runBatchRelease(m, rollout, rolloutID, 2, false)
assert.NoError(t, err)
assert.False(t, done)
assert.NotNil(t, br)
createdBR := &v1beta1.BatchRelease{}
getErr := fakeClient.Get(context.TODO(), types.NamespacedName{Name: rollout.Name, Namespace: rollout.Namespace}, createdBR)
assert.NoError(t, getErr)
assert.Equal(t, int32(5), createdBR.Spec.ReleasePlan.Batches[0].CanaryReplicas.IntVal)
assert.Equal(t, int32(0), *createdBR.Spec.ReleasePlan.BatchPartition)
assert.Equal(t, rolloutID, createdBR.Annotations["rollout-id"])
})
t.Run("should_update_existing_BatchRelease_if_spec_is_outdated", func(t *testing.T) {
existingBR := &v1beta1.BatchRelease{
ObjectMeta: metav1.ObjectMeta{Name: "my-rollout", Namespace: "default", Annotations: map[string]string{"rollout-id": "old-id"}},
Spec: v1beta1.BatchReleaseSpec{ReleasePlan: v1beta1.ReleasePlan{BatchPartition: int32p(99)}}, // old spec
}
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(existingBR).Build()
m := &mockReleaseManager{client: fakeClient}
done, br, err := runBatchRelease(m, rollout, rolloutID, 1, false)
assert.NoError(t, err)
assert.False(t, done)
assert.NotNil(t, br)
updatedBR := &v1beta1.BatchRelease{}
getErr := fakeClient.Get(context.TODO(), types.NamespacedName{Name: rollout.Name, Namespace: rollout.Namespace}, updatedBR)
assert.NoError(t, getErr)
assert.Equal(t, int32(0), *updatedBR.Spec.ReleasePlan.BatchPartition)
assert.Equal(t, "10%", updatedBR.Spec.ReleasePlan.Batches[0].CanaryReplicas.StrVal)
assert.Equal(t, rolloutID, updatedBR.Annotations["rollout-id"])
})
t.Run("should_return_done=true_if_BatchRelease_is_up-to_date", func(t *testing.T) {
m := &mockReleaseManager{}
// create a BR that matches what createBatchRelease would generate for batch 1 (index 0)
existingBR := m.createBatchRelease(rollout, rolloutID, 0, false)
m.client = fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(existingBR).Build()
done, br, err := runBatchRelease(m, rollout, rolloutID, 1, false)
assert.NoError(t, err)
assert.True(t, done)
assert.NotNil(t, br)
})
}
func int32p(i int32) *int32 {
return &i
}