/* Copyright 2022 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 deployment import ( "context" "fmt" "strconv" "strings" "testing" apps "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" intstrutil "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" appslisters "k8s.io/client-go/listers/apps/v1" "k8s.io/client-go/tools/record" "k8s.io/utils/pointer" rolloutsv1alpha1 "github.com/openkruise/rollouts/api/v1alpha1" "github.com/openkruise/rollouts/pkg/controller/deployment/util" ) func TestSyncDeployment(t *testing.T) { tests := map[string]struct { oldRSsReplicas []int32 newRSReplicas int32 dReplicas int32 oldRSsAvailable []int32 newRSAvailable int32 dAvailable int32 partition intstrutil.IntOrString maxSurge intstrutil.IntOrString maxUnavailable intstrutil.IntOrString expectOldReplicas int32 expectNewReplicas int32 }{ "rolling new: surge new first, limited by maxSurge": { []int32{6, 4}, 0, 10, []int32{0, 0}, 0, 0, intstrutil.FromInt(4), intstrutil.FromInt(2), intstrutil.FromString("50%"), 10, 2, }, "rolling new: surge new first, limited by partition": { []int32{6, 4}, 0, 10, []int32{0, 0}, 0, 0, intstrutil.FromInt(4), intstrutil.FromInt(10), intstrutil.FromString("50%"), 10, 4, }, "rolling old: limited by maxUnavailable": { []int32{6, 4}, 2, 10, []int32{6, 4}, 0, 10, intstrutil.FromInt(4), intstrutil.FromInt(2), intstrutil.FromString("20%"), 8, 2, }, "rolling new: limited by partition": { []int32{6, 2}, 2, 10, []int32{6, 2}, 0, 8, intstrutil.FromInt(3), intstrutil.FromInt(5), intstrutil.FromString("40%"), 8, 3, }, "rolling new: scaling down old first, limited by partition": { []int32{6, 4}, 0, 10, []int32{6, 4}, 0, 10, intstrutil.FromInt(3), intstrutil.FromInt(0), intstrutil.FromString("40%"), 7, 0, }, "rolling new: scaling down old first, limited by maxUnavailable": { []int32{6, 4}, 0, 10, []int32{6, 4}, 0, 10, intstrutil.FromInt(5), intstrutil.FromInt(0), intstrutil.FromString("20%"), 8, 0, }, "no op: partition satisfied, maxSurge>0, new pod unavailable": { []int32{3, 4}, 3, 10, []int32{3, 4}, 0, 7, intstrutil.FromInt(3), intstrutil.FromInt(2), intstrutil.FromString("30%"), 7, 3, }, "no op: partition satisfied, maxSurge>0, new pod available": { []int32{3, 4}, 3, 10, []int32{3, 4}, 3, 10, intstrutil.FromInt(3), intstrutil.FromInt(2), intstrutil.FromString("30%"), 7, 3, }, "rolling old: scale down old to satisfied replicas": { []int32{3}, 3, 5, []int32{3}, 3, 6, intstrutil.FromInt(3), intstrutil.FromInt(2), intstrutil.FromString("25%"), 2, 3, }, "scale up: scale down old first": { []int32{4}, 0, 5, []int32{4}, 0, 4, intstrutil.FromInt(3), intstrutil.FromInt(0), intstrutil.FromInt(1), 5, 0, }, "scale up": { []int32{5}, 5, 20, []int32{5}, 5, 10, intstrutil.FromInt(3), intstrutil.FromInt(0), intstrutil.FromString("30%"), 10, 10, }, "scale down": { []int32{12}, 8, 10, []int32{12}, 8, 20, intstrutil.FromInt(5), intstrutil.FromInt(0), intstrutil.FromString("30%"), 6, 4, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { fakeClient := fake.NewSimpleClientset() fakeRecord := record.NewFakeRecorder(10) informers := informers.NewSharedInformerFactory(fakeClient, 0) rsInformer := informers.Apps().V1().ReplicaSets().Informer() dInformer := informers.Apps().V1().Deployments().Informer() var deployment apps.Deployment var newRS apps.ReplicaSet { deployment = generateDeployment("busybox") deployment.Spec.Replicas = pointer.Int32(test.dReplicas) deployment.Status.ReadyReplicas = test.newRSReplicas availableReplicas := test.newRSAvailable for _, available := range test.oldRSsAvailable { availableReplicas += available } deployment.Status.UpdatedReplicas = test.newRSReplicas deployment.Status.Replicas = availableReplicas deployment.Status.AvailableReplicas = availableReplicas dInformer.GetIndexer().Add(&deployment) _, err := fakeClient.AppsV1().Deployments(deployment.Namespace).Create(context.TODO(), &deployment, metav1.CreateOptions{}) if err != nil { t.Fatalf("got unexpected error: %v", err) } } { for index, replicas := range test.oldRSsReplicas { rs := generateRS(deployment) rs.SetName(fmt.Sprintf("rs-%d", index)) rs.Spec.Replicas = pointer.Int32(replicas) rs.Status.Replicas = replicas if strings.HasPrefix(name, "scale") { rs.Annotations = map[string]string{ util.DesiredReplicasAnnotation: strconv.Itoa(-1), util.MaxReplicasAnnotation: strconv.Itoa(int(test.dAvailable + test.maxSurge.IntVal)), } } rs.Spec.Template.Spec.Containers[0].Image = fmt.Sprintf("old-version-%d", index) rs.Status.ReadyReplicas = test.oldRSsAvailable[index] rs.Status.AvailableReplicas = test.oldRSsAvailable[index] rsInformer.GetIndexer().Add(&rs) _, err := fakeClient.AppsV1().ReplicaSets(rs.Namespace).Create(context.TODO(), &rs, metav1.CreateOptions{}) if err != nil { t.Fatalf("got unexpected error: %v", err) } } } { newRS = generateRS(deployment) newRS.SetName("rs-new") newRS.Spec.Replicas = pointer.Int32(test.newRSReplicas) if strings.HasPrefix(name, "scale") { newRS.Annotations = map[string]string{ util.DesiredReplicasAnnotation: strconv.Itoa(-1), util.MaxReplicasAnnotation: strconv.Itoa(int(test.dAvailable + test.maxSurge.IntVal)), } } newRS.Status.Replicas = test.newRSReplicas newRS.Status.ReadyReplicas = test.newRSAvailable newRS.Status.AvailableReplicas = test.newRSAvailable rsInformer.GetIndexer().Add(&newRS) _, err := fakeClient.AppsV1().ReplicaSets(newRS.Namespace).Create(context.TODO(), &newRS, metav1.CreateOptions{}) if err != nil { t.Fatalf("got unexpected error: %v", err) } } dc := &DeploymentController{ client: fakeClient, eventRecorder: fakeRecord, dLister: appslisters.NewDeploymentLister(dInformer.GetIndexer()), rsLister: appslisters.NewReplicaSetLister(rsInformer.GetIndexer()), strategy: rolloutsv1alpha1.DeploymentStrategy{ RollingUpdate: &apps.RollingUpdateDeployment{ MaxSurge: &test.maxSurge, MaxUnavailable: &test.maxUnavailable, }, Partition: test.partition, }, } err := dc.syncDeployment(context.TODO(), &deployment) if err != nil { t.Fatalf("got unexpected error: %v", err) } rss, err := dc.client.AppsV1().ReplicaSets(deployment.Namespace).List(context.TODO(), metav1.ListOptions{}) if err != nil { t.Fatalf("got unexpected error: %v", err) } resultOld := int32(0) resultNew := int32(0) for _, rs := range rss.Items { if rs.GetName() != "rs-new" { resultOld += *rs.Spec.Replicas } else { resultNew = *rs.Spec.Replicas } } if resultOld != test.expectOldReplicas || resultNew != test.expectNewReplicas { t.Fatalf("expect new %d, but got new %d; expect old %d, but got old %d ", test.expectNewReplicas, resultNew, test.expectOldReplicas, resultOld) } }) } }