added ut for v1alpha1 Rollout validation (#295)

Signed-off-by: GautamBytes <manchandanigautam@gmail.com>
This commit is contained in:
Gautam Manchandani 2025-07-18 16:45:49 +05:30 committed by GitHub
parent deaa5f38b5
commit c19c76203e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 386 additions and 0 deletions

View File

@ -0,0 +1,386 @@
package validating
import (
"strings"
"testing"
appsv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
func int32Ptr(i int32) *int32 {
return &i
}
// Helper function to check if an error list contains a specific field path
func containsField(errList field.ErrorList, fieldPath string) bool {
for _, err := range errList {
if strings.Contains(err.Field, fieldPath) {
return true
}
}
return false
}
func TestValidateV1alpha1Rollout(t *testing.T) {
scheme := runtime.NewScheme()
_ = appsv1alpha1.AddToScheme(scheme)
tests := []struct {
name string
rollout *appsv1alpha1.Rollout
wantErr bool
errMsg string
}{
{
name: "valid rollout with replicas",
rollout: &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "valid-rollout", Namespace: "default"},
Spec: appsv1alpha1.RolloutSpec{
ObjectRef: appsv1alpha1.ObjectRef{
WorkloadRef: &appsv1alpha1.WorkloadRef{
APIVersion: "apps/v1", Kind: "Deployment", Name: "test",
},
},
Strategy: appsv1alpha1.RolloutStrategy{
Canary: &appsv1alpha1.CanaryStrategy{
Steps: []appsv1alpha1.CanaryStep{
{Replicas: &intstr.IntOrString{Type: intstr.String, StrVal: "10%"}},
},
},
},
},
},
wantErr: false,
},
{
name: "missing workloadRef",
rollout: &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "rollout-no-ref"},
Spec: appsv1alpha1.RolloutSpec{
ObjectRef: appsv1alpha1.ObjectRef{}, // WorkloadRef is nil
Strategy: appsv1alpha1.RolloutStrategy{
Canary: nil,
},
},
},
wantErr: true,
errMsg: "WorkloadRef is required",
},
{
name: "invalid rolling style",
rollout: &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{
Name: "rollout-invalid-style",
Annotations: map[string]string{appsv1alpha1.RolloutStyleAnnotation: "invalid-style"},
},
Spec: appsv1alpha1.RolloutSpec{
ObjectRef: appsv1alpha1.ObjectRef{
WorkloadRef: &appsv1alpha1.WorkloadRef{APIVersion: "apps/v1", Kind: "Deployment", Name: "test"},
},
Strategy: appsv1alpha1.RolloutStrategy{
Canary: &appsv1alpha1.CanaryStrategy{
Steps: []appsv1alpha1.CanaryStep{{Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}}},
},
},
},
},
wantErr: true,
errMsg: "Rolling style must be 'Canary', 'Partition' or empty",
},
{
name: "empty steps",
rollout: &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "rollout-empty-steps"},
Spec: appsv1alpha1.RolloutSpec{
ObjectRef: appsv1alpha1.ObjectRef{
WorkloadRef: &appsv1alpha1.WorkloadRef{APIVersion: "apps/v1", Kind: "Deployment", Name: "test"},
},
Strategy: appsv1alpha1.RolloutStrategy{
Canary: &appsv1alpha1.CanaryStrategy{Steps: []appsv1alpha1.CanaryStep{}},
},
},
},
wantErr: true,
errMsg: "The number of Canary.Steps cannot be empty",
},
{
name: "step with no replicas defined",
rollout: &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "rollout-invalid-step"},
Spec: appsv1alpha1.RolloutSpec{
ObjectRef: appsv1alpha1.ObjectRef{
WorkloadRef: &appsv1alpha1.WorkloadRef{APIVersion: "apps/v1", Kind: "Deployment", Name: "test"},
},
Strategy: appsv1alpha1.RolloutStrategy{
Canary: &appsv1alpha1.CanaryStrategy{
Steps: []appsv1alpha1.CanaryStep{{}}, // Invalid step
},
},
},
},
wantErr: true,
errMsg: "weight and replicas cannot be empty at the same time",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).Build()
handler := &RolloutCreateUpdateHandler{Client: fakeClient}
errList := handler.validateV1alpha1Rollout(tt.rollout)
if tt.wantErr {
assert.NotEmpty(t, errList)
assert.Contains(t, errList.ToAggregate().Error(), tt.errMsg)
} else {
assert.Empty(t, errList)
}
})
}
}
func TestValidateV1alpha1RolloutUpdate(t *testing.T) {
scheme := runtime.NewScheme()
_ = appsv1alpha1.AddToScheme(scheme)
baseRollout := &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"},
Spec: appsv1alpha1.RolloutSpec{
ObjectRef: appsv1alpha1.ObjectRef{
WorkloadRef: &appsv1alpha1.WorkloadRef{
APIVersion: "apps/v1", Kind: "Deployment", Name: "test-workload",
},
},
Strategy: appsv1alpha1.RolloutStrategy{
Canary: &appsv1alpha1.CanaryStrategy{
Steps: []appsv1alpha1.CanaryStep{
{Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: 1}},
},
TrafficRoutings: []appsv1alpha1.TrafficRoutingRef{
{Service: "test-service", Ingress: &appsv1alpha1.IngressTrafficRouting{Name: "test-ingress"}},
},
},
},
},
Status: appsv1alpha1.RolloutStatus{Phase: appsv1alpha1.RolloutPhaseInitial},
}
tests := []struct {
name string
oldRollout *appsv1alpha1.Rollout
newRollout *appsv1alpha1.Rollout
wantErr bool
errMsg string
}{
{
name: "allow update in initial phase",
oldRollout: baseRollout,
newRollout: func() *appsv1alpha1.Rollout {
r := baseRollout.DeepCopy()
r.Spec.Strategy.Canary.Steps[0].Replicas = &intstr.IntOrString{Type: intstr.Int, IntVal: 2}
return r
}(),
wantErr: false,
},
{
name: "forbid workloadRef change in progressing phase",
oldRollout: func() *appsv1alpha1.Rollout {
r := baseRollout.DeepCopy()
r.Status.Phase = appsv1alpha1.RolloutPhaseProgressing
return r
}(),
newRollout: func() *appsv1alpha1.Rollout {
r := baseRollout.DeepCopy()
r.Status.Phase = appsv1alpha1.RolloutPhaseProgressing
r.Spec.ObjectRef.WorkloadRef.Name = "new-workload"
return r
}(),
wantErr: true,
errMsg: "'ObjectRef' field is immutable",
},
{
name: "forbid traffic routing change in terminating phase",
oldRollout: func() *appsv1alpha1.Rollout {
r := baseRollout.DeepCopy()
r.Status.Phase = appsv1alpha1.RolloutPhaseTerminating
return r
}(),
newRollout: func() *appsv1alpha1.Rollout {
r := baseRollout.DeepCopy()
r.Status.Phase = appsv1alpha1.RolloutPhaseTerminating
r.Spec.Strategy.Canary.TrafficRoutings = []appsv1alpha1.TrafficRoutingRef{
{Service: "another-service", Ingress: &appsv1alpha1.IngressTrafficRouting{Name: "test-ingress"}},
}
return r
}(),
wantErr: true,
errMsg: "'Strategy.Canary.TrafficRoutings' field is immutable",
},
{
name: "forbid rolling style change in progressing phase",
oldRollout: func() *appsv1alpha1.Rollout {
r := baseRollout.DeepCopy()
r.Status.Phase = appsv1alpha1.RolloutPhaseProgressing
return r
}(),
newRollout: func() *appsv1alpha1.Rollout {
r := baseRollout.DeepCopy()
r.Status.Phase = appsv1alpha1.RolloutPhaseProgressing
r.Annotations = map[string]string{appsv1alpha1.RolloutStyleAnnotation: "Partition"}
return r
}(),
wantErr: true,
errMsg: "'Rolling-Style' annotation is immutable",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(tt.oldRollout).
Build()
handler := &RolloutCreateUpdateHandler{Client: fakeClient}
errList := handler.validateV1alpha1RolloutUpdate(tt.oldRollout, tt.newRollout)
if tt.wantErr {
assert.NotEmpty(t, errList)
assert.Contains(t, errList.ToAggregate().Error(), tt.errMsg)
} else {
assert.Empty(t, errList)
}
})
}
}
func TestValidateV1alpha1RolloutSpecCanarySteps(t *testing.T) {
ctxCanary := &validateContext{style: string(appsv1alpha1.CanaryRollingStyle)}
tests := []struct {
name string
ctx *validateContext
steps []appsv1alpha1.CanaryStep
traffic bool
wantErr bool
errField string
}{
{
name: "valid steps with non-decreasing replicas",
ctx: ctxCanary,
traffic: false,
steps: []appsv1alpha1.CanaryStep{
{Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: 2}},
{Replicas: &intstr.IntOrString{Type: intstr.String, StrVal: "50%"}},
},
wantErr: false,
},
{
name: "decreasing replicas",
ctx: ctxCanary,
traffic: false,
steps: []appsv1alpha1.CanaryStep{
{Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: 5}},
{Replicas: &intstr.IntOrString{Type: intstr.Int, IntVal: 3}},
},
wantErr: true,
errField: "CanaryReplicas",
},
{
name: "invalid replica percentage > 100%",
ctx: ctxCanary,
traffic: false,
steps: []appsv1alpha1.CanaryStep{
{Replicas: &intstr.IntOrString{Type: intstr.String, StrVal: "120%"}},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errList := validateV1alpha1RolloutSpecCanarySteps(tt.ctx, tt.steps, field.NewPath("steps"), tt.traffic)
if tt.wantErr {
assert.NotEmpty(t, errList)
if tt.errField != "" {
assert.True(t, containsField(errList, tt.errField), "expected error on field %s", tt.errField)
}
} else {
assert.Empty(t, errList, "expected no errors but got: %v", errList)
}
})
}
}
func TestValidateV1alpha1RolloutConflict(t *testing.T) {
scheme := runtime.NewScheme()
_ = appsv1alpha1.AddToScheme(scheme)
workloadRef := &appsv1alpha1.WorkloadRef{
APIVersion: "apps/v1", Kind: "Deployment", Name: "test",
}
existingRollout := &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "existing-rollout", Namespace: "default"},
Spec: appsv1alpha1.RolloutSpec{ObjectRef: appsv1alpha1.ObjectRef{WorkloadRef: workloadRef}},
}
newRollout := &appsv1alpha1.Rollout{
ObjectMeta: metav1.ObjectMeta{Name: "new-rollout", Namespace: "default"},
Spec: appsv1alpha1.RolloutSpec{ObjectRef: appsv1alpha1.ObjectRef{WorkloadRef: workloadRef}},
}
tests := []struct {
name string
existing []client.Object
rollout *appsv1alpha1.Rollout
wantErr bool
}{
{
name: "no conflict",
existing: []client.Object{},
rollout: newRollout,
wantErr: false,
},
{
name: "conflict with existing rollout",
existing: []client.Object{existingRollout},
rollout: newRollout,
wantErr: true,
},
{
name: "no conflict if workload is different",
existing: []client.Object{existingRollout},
rollout: func() *appsv1alpha1.Rollout {
r := newRollout.DeepCopy()
r.Spec.ObjectRef.WorkloadRef.Name = "different-workload"
return r
}(),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(tt.existing...).
Build()
handler := &RolloutCreateUpdateHandler{Client: fakeClient}
errList := handler.validateV1alpha1RolloutConflict(tt.rollout, field.NewPath("spec"))
if tt.wantErr {
assert.NotEmpty(t, errList)
assert.Contains(t, errList.ToAggregate().Error(), "conflict with Rollout")
} else {
assert.Empty(t, errList)
}
})
}
}