Skip to content

Commit e263b87

Browse files
committed
Deployment controller should count terminating pods in the status
1 parent dc1914c commit e263b87

File tree

5 files changed

+206
-28
lines changed

5 files changed

+206
-28
lines changed

pkg/controller/deployment/sync.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@ import (
2727
v1 "k8s.io/api/core/v1"
2828
"k8s.io/apimachinery/pkg/api/errors"
2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3031
"k8s.io/klog/v2"
3132
"k8s.io/kubernetes/pkg/controller"
3233
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
34+
"k8s.io/kubernetes/pkg/features"
3335
labelsutil "k8s.io/kubernetes/pkg/util/labels"
3436
)
3537

@@ -504,6 +506,9 @@ func calculateStatus(allRSs []*apps.ReplicaSet, newRS *apps.ReplicaSet, deployme
504506
UnavailableReplicas: unavailableReplicas,
505507
CollisionCount: deployment.Status.CollisionCount,
506508
}
509+
if utilfeature.DefaultFeatureGate.Enabled(features.DeploymentPodReplacementPolicy) {
510+
status.TerminatingReplicas = deploymentutil.GetTerminatingReplicaCountForReplicaSets(allRSs)
511+
}
507512

508513
// Copy conditions one by one so we won't mutate the original object.
509514
conditions := deployment.Status.Conditions

pkg/controller/deployment/util/deployment_util.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,26 @@ func GetAvailableReplicaCountForReplicaSets(replicaSets []*apps.ReplicaSet) int3
714714
return totalAvailableReplicas
715715
}
716716

717+
// GetTerminatingReplicaCountForReplicaSets returns the number of terminating pods for all replica sets
718+
// or returns an error if any replica sets have been synced by the controller but do not report their terminating count.
719+
func GetTerminatingReplicaCountForReplicaSets(replicaSets []*apps.ReplicaSet) *int32 {
720+
terminatingReplicas := int32(0)
721+
for _, rs := range replicaSets {
722+
switch {
723+
case rs == nil:
724+
// No-op
725+
case rs.Status.ObservedGeneration == 0 && rs.Status.TerminatingReplicas == nil:
726+
// Replicasets that have never been synced by the controller don't contribute to TerminatingReplicas
727+
case rs.Status.TerminatingReplicas == nil:
728+
// If any replicaset synced by the controller hasn't reported TerminatingReplicas, we cannot calculate a sum
729+
return nil
730+
default:
731+
terminatingReplicas += *rs.Status.TerminatingReplicas
732+
}
733+
}
734+
return &terminatingReplicas
735+
}
736+
717737
// IsRollingUpdate returns true if the strategy type is a rolling update.
718738
func IsRollingUpdate(deployment *apps.Deployment) bool {
719739
return deployment.Spec.Strategy.Type == apps.RollingUpdateDeploymentStrategyType

pkg/controller/deployment/util/deployment_util_test.go

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func generateRS(deployment apps.Deployment) apps.ReplicaSet {
5757
return apps.ReplicaSet{
5858
ObjectMeta: metav1.ObjectMeta{
5959
UID: randomUID(),
60-
Name: names.SimpleNameGenerator.GenerateName("replicaset"),
60+
Name: names.SimpleNameGenerator.GenerateName(deployment.Name),
6161
Labels: template.Labels,
6262
OwnerReferences: []metav1.OwnerReference{*newDControllerRef(&deployment)},
6363
},
@@ -344,42 +344,93 @@ func TestFindOldReplicaSets(t *testing.T) {
344344
}
345345

346346
func TestGetReplicaCountForReplicaSets(t *testing.T) {
347-
rs1 := generateRS(generateDeployment("foo"))
347+
rs1 := generateRS(generateDeployment("foo-rs"))
348+
rs1.Status.ObservedGeneration = 1
348349
*(rs1.Spec.Replicas) = 1
349350
rs1.Status.Replicas = 2
350-
rs2 := generateRS(generateDeployment("bar"))
351+
rs1.Status.TerminatingReplicas = ptr.To[int32](3)
352+
353+
rs2 := generateRS(generateDeployment("bar-rs"))
354+
rs1.Status.ObservedGeneration = 1
351355
*(rs2.Spec.Replicas) = 2
352356
rs2.Status.Replicas = 3
357+
rs2.Status.TerminatingReplicas = ptr.To[int32](1)
358+
359+
rs3 := generateRS(generateDeployment("unsynced-rs"))
360+
*(rs3.Spec.Replicas) = 3
361+
rs3.Status.Replicas = 0
362+
rs3.Status.TerminatingReplicas = nil
363+
364+
rs4 := generateRS(generateDeployment("dropped-rs"))
365+
rs4.Status.ObservedGeneration = 1
366+
*(rs4.Spec.Replicas) = 1
367+
rs4.Status.Replicas = 1
368+
rs4.Status.TerminatingReplicas = nil
353369

354370
tests := []struct {
355-
Name string
356-
sets []*apps.ReplicaSet
357-
expectedCount int32
358-
expectedActual int32
371+
name string
372+
sets []*apps.ReplicaSet
373+
expectedCount int32
374+
expectedActual int32
375+
expectedTerminating *int32
359376
}{
360377
{
361-
"1:2 Replicas",
362-
[]*apps.ReplicaSet{&rs1},
363-
1,
364-
2,
378+
name: "scaling down rs1",
379+
sets: []*apps.ReplicaSet{&rs1},
380+
expectedCount: 1,
381+
expectedActual: 2,
382+
expectedTerminating: ptr.To[int32](3),
383+
},
384+
{
385+
name: "scaling down rs1 and rs2",
386+
sets: []*apps.ReplicaSet{&rs1, &rs2},
387+
expectedCount: 3,
388+
expectedActual: 5,
389+
expectedTerminating: ptr.To[int32](4),
390+
},
391+
{
392+
name: "scaling up rs3",
393+
sets: []*apps.ReplicaSet{&rs3},
394+
expectedCount: 3,
395+
expectedActual: 0,
396+
expectedTerminating: ptr.To[int32](0),
365397
},
366398
{
367-
"3:5 Replicas",
368-
[]*apps.ReplicaSet{&rs1, &rs2},
369-
3,
370-
5,
399+
name: "scaling down rs1 and rs2 and scaling up rs3",
400+
sets: []*apps.ReplicaSet{&rs1, &rs2, &rs3},
401+
expectedCount: 6,
402+
expectedActual: 5,
403+
expectedTerminating: ptr.To[int32](4),
404+
},
405+
{
406+
name: "invalid/unknown terminating status for rs4",
407+
sets: []*apps.ReplicaSet{&rs4},
408+
expectedCount: 1,
409+
expectedActual: 1,
410+
expectedTerminating: nil,
411+
},
412+
{
413+
name: "invalid/unknown terminating status for rs4 with rs1, rs2 and rs3",
414+
sets: []*apps.ReplicaSet{&rs1, &rs2, &rs3, &rs4},
415+
expectedCount: 7,
416+
expectedActual: 6,
417+
expectedTerminating: nil,
371418
},
372419
}
373420

374421
for _, test := range tests {
375-
t.Run(test.Name, func(t *testing.T) {
376-
rs := GetReplicaCountForReplicaSets(test.sets)
377-
if rs != test.expectedCount {
378-
t.Errorf("In test case %s, expectedCount %+v, got %+v", test.Name, test.expectedCount, rs)
422+
t.Run(test.name, func(t *testing.T) {
423+
replicasCount := GetReplicaCountForReplicaSets(test.sets)
424+
if replicasCount != test.expectedCount {
425+
t.Errorf("expectedCount %d, got %d", test.expectedCount, replicasCount)
426+
}
427+
actualReplicasCount := GetActualReplicaCountForReplicaSets(test.sets)
428+
if actualReplicasCount != test.expectedActual {
429+
t.Errorf("expectedActual %d, got %d", test.expectedActual, actualReplicasCount)
379430
}
380-
rs = GetActualReplicaCountForReplicaSets(test.sets)
381-
if rs != test.expectedActual {
382-
t.Errorf("In test case %s, expectedActual %+v, got %+v", test.Name, test.expectedActual, rs)
431+
terminatingReplicasCount := GetTerminatingReplicaCountForReplicaSets(test.sets)
432+
if !ptr.Equal(terminatingReplicasCount, test.expectedTerminating) {
433+
t.Errorf("expectedTerminating %d, got %d", ptr.Deref(test.expectedTerminating, -1), ptr.Deref(terminatingReplicasCount, -1))
383434
}
384435
})
385436
}

test/integration/deployment/deployment_test.go

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ import (
2828
"k8s.io/apimachinery/pkg/util/intstr"
2929
"k8s.io/apimachinery/pkg/util/uuid"
3030
"k8s.io/apimachinery/pkg/util/wait"
31+
utilfeature "k8s.io/apiserver/pkg/util/feature"
3132
"k8s.io/client-go/util/retry"
33+
featuregatetesting "k8s.io/component-base/featuregate/testing"
3234
"k8s.io/klog/v2/ktesting"
3335
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
36+
"k8s.io/kubernetes/pkg/features"
3437
"k8s.io/kubernetes/test/integration/framework"
3538
testutil "k8s.io/kubernetes/test/utils"
3639
"k8s.io/utils/ptr"
@@ -965,7 +968,7 @@ func TestDeploymentAvailableCondition(t *testing.T) {
965968
}
966969

967970
// Verify all replicas fields of DeploymentStatus have desired counts
968-
if err = tester.checkDeploymentStatusReplicasFields(10, 10, 0, 0, 10); err != nil {
971+
if err = tester.checkDeploymentStatusReplicasFields(10, 10, 0, 0, 10, nil); err != nil {
969972
t.Fatal(err)
970973
}
971974

@@ -985,7 +988,7 @@ func TestDeploymentAvailableCondition(t *testing.T) {
985988
}
986989

987990
// Verify all replicas fields of DeploymentStatus have desired counts
988-
if err = tester.checkDeploymentStatusReplicasFields(10, 10, 10, 0, 10); err != nil {
991+
if err = tester.checkDeploymentStatusReplicasFields(10, 10, 10, 0, 10, nil); err != nil {
989992
t.Fatal(err)
990993
}
991994

@@ -1008,7 +1011,7 @@ func TestDeploymentAvailableCondition(t *testing.T) {
10081011
}
10091012

10101013
// Verify all replicas fields of DeploymentStatus have desired counts
1011-
if err = tester.checkDeploymentStatusReplicasFields(10, 10, 10, 10, 0); err != nil {
1014+
if err = tester.checkDeploymentStatusReplicasFields(10, 10, 10, 10, 0, nil); err != nil {
10121015
t.Fatal(err)
10131016
}
10141017
}
@@ -1303,3 +1306,98 @@ func TestReplicaSetOrphaningAndAdoptionWhenLabelsChange(t *testing.T) {
13031306
t.Fatalf("failed waiting for replicaset adoption by deployment %q to complete: %v", deploymentName, err)
13041307
}
13051308
}
1309+
1310+
func TestTerminatingReplicasDeploymentStatus(t *testing.T) {
1311+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeploymentPodReplacementPolicy, false)
1312+
1313+
_, ctx := ktesting.NewTestContext(t)
1314+
ctx, cancel := context.WithCancel(ctx)
1315+
defer cancel()
1316+
1317+
closeFn, rm, dc, informers, c := dcSetup(ctx, t)
1318+
defer closeFn()
1319+
1320+
name := "test-terminating-replica-status"
1321+
ns := framework.CreateNamespaceOrDie(c, name, t)
1322+
defer framework.DeleteNamespaceOrDie(c, ns, t)
1323+
1324+
deploymentName := "deployment"
1325+
replicas := int32(6)
1326+
tester := &deploymentTester{t: t, c: c, deployment: newDeployment(deploymentName, ns.Name, replicas)}
1327+
tester.deployment.Spec.Strategy.Type = apps.RecreateDeploymentStrategyType
1328+
tester.deployment.Spec.Strategy.RollingUpdate = nil
1329+
tester.deployment.Spec.Template.Spec.NodeName = "fake-node"
1330+
tester.deployment.Spec.Template.Spec.TerminationGracePeriodSeconds = ptr.To(int64(300))
1331+
1332+
var err error
1333+
tester.deployment, err = c.AppsV1().Deployments(ns.Name).Create(context.TODO(), tester.deployment, metav1.CreateOptions{})
1334+
if err != nil {
1335+
t.Fatalf("failed to create deployment %q: %v", deploymentName, err)
1336+
}
1337+
1338+
// Start informer and controllers
1339+
stopControllers := runControllersAndInformers(t, rm, dc, informers)
1340+
defer stopControllers()
1341+
1342+
// Ensure the deployment completes while marking its pods as ready simultaneously
1343+
if err := tester.waitForDeploymentCompleteAndMarkPodsReady(); err != nil {
1344+
t.Fatal(err)
1345+
}
1346+
// Should not update terminating replicas when feature gate is disabled
1347+
// Verify all replicas fields of DeploymentStatus have desired counts
1348+
if err = tester.checkDeploymentStatusReplicasFields(6, 6, 6, 6, 0, nil); err != nil {
1349+
t.Fatal(err)
1350+
}
1351+
1352+
// Scale down the deployment
1353+
tester.deployment, err = tester.updateDeployment(func(update *apps.Deployment) {
1354+
update.Spec.Replicas = ptr.To(int32(4))
1355+
})
1356+
if err != nil {
1357+
t.Fatalf("failed updating deployment %q: %v", deploymentName, err)
1358+
}
1359+
// Wait for number of ready replicas to equal number of replicas.
1360+
if err = tester.waitForReadyReplicas(); err != nil {
1361+
t.Fatal(err)
1362+
}
1363+
// Verify all replicas fields of DeploymentStatus have desired counts
1364+
if err = tester.checkDeploymentStatusReplicasFields(4, 4, 4, 4, 0, nil); err != nil {
1365+
t.Fatal(err)
1366+
}
1367+
1368+
// should update terminating replicas when feature gate is enabled
1369+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeploymentPodReplacementPolicy, true)
1370+
// Scale down the deployment
1371+
tester.deployment, err = tester.updateDeployment(func(update *apps.Deployment) {
1372+
update.Spec.Replicas = ptr.To(int32(3))
1373+
})
1374+
if err != nil {
1375+
t.Fatalf("failed updating deployment %q: %v", deploymentName, err)
1376+
}
1377+
// Wait for number of ready replicas to equal number of replicas.
1378+
if err = tester.waitForReadyReplicas(); err != nil {
1379+
t.Fatal(err)
1380+
}
1381+
// Verify all replicas fields of DeploymentStatus have desired counts
1382+
if err = tester.checkDeploymentStatusReplicasFields(3, 3, 3, 3, 0, ptr.To[int32](3)); err != nil {
1383+
t.Fatal(err)
1384+
}
1385+
1386+
// should not update terminating replicas when feature gate is disabled
1387+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeploymentPodReplacementPolicy, false)
1388+
// Scale down the deployment
1389+
tester.deployment, err = tester.updateDeployment(func(update *apps.Deployment) {
1390+
update.Spec.Replicas = ptr.To(int32(2))
1391+
})
1392+
if err != nil {
1393+
t.Fatalf("failed updating deployment %q: %v", deploymentName, err)
1394+
}
1395+
// Wait for number of ready replicas to equal number of replicas.
1396+
if err = tester.waitForReadyReplicas(); err != nil {
1397+
t.Fatal(err)
1398+
}
1399+
// Verify all replicas fields of DeploymentStatus have desired counts
1400+
if err = tester.checkDeploymentStatusReplicasFields(2, 2, 2, 2, 0, nil); err != nil {
1401+
t.Fatal(err)
1402+
}
1403+
}

test/integration/deployment/util.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"k8s.io/kubernetes/pkg/controller/replicaset"
3838
"k8s.io/kubernetes/test/integration/framework"
3939
testutil "k8s.io/kubernetes/test/utils"
40+
"k8s.io/utils/ptr"
4041
)
4142

4243
const (
@@ -433,7 +434,7 @@ func (d *deploymentTester) markUpdatedPodsReadyWithoutComplete() error {
433434

434435
// Verify all replicas fields of DeploymentStatus have desired count.
435436
// Immediately return an error when found a non-matching replicas field.
436-
func (d *deploymentTester) checkDeploymentStatusReplicasFields(replicas, updatedReplicas, readyReplicas, availableReplicas, unavailableReplicas int32) error {
437+
func (d *deploymentTester) checkDeploymentStatusReplicasFields(replicas, updatedReplicas, readyReplicas, availableReplicas, unavailableReplicas int32, terminatingReplicas *int32) error {
437438
deployment, err := d.c.AppsV1().Deployments(d.deployment.Namespace).Get(context.TODO(), d.deployment.Name, metav1.GetOptions{})
438439
if err != nil {
439440
return fmt.Errorf("failed to get deployment %q: %v", d.deployment.Name, err)
@@ -448,10 +449,13 @@ func (d *deploymentTester) checkDeploymentStatusReplicasFields(replicas, updated
448449
return fmt.Errorf("unexpected .readyReplicas: expect %d, got %d", readyReplicas, deployment.Status.ReadyReplicas)
449450
}
450451
if deployment.Status.AvailableReplicas != availableReplicas {
451-
return fmt.Errorf("unexpected .replicas: expect %d, got %d", availableReplicas, deployment.Status.AvailableReplicas)
452+
return fmt.Errorf("unexpected .availableReplicas: expect %d, got %d", availableReplicas, deployment.Status.AvailableReplicas)
452453
}
453454
if deployment.Status.UnavailableReplicas != unavailableReplicas {
454-
return fmt.Errorf("unexpected .replicas: expect %d, got %d", unavailableReplicas, deployment.Status.UnavailableReplicas)
455+
return fmt.Errorf("unexpected .unavailableReplicas: expect %d, got %d", unavailableReplicas, deployment.Status.UnavailableReplicas)
456+
}
457+
if !ptr.Equal(deployment.Status.TerminatingReplicas, terminatingReplicas) {
458+
return fmt.Errorf("unexpected .terminatingReplicas: expect %v, got %v", ptr.Deref(terminatingReplicas, -1), ptr.Deref(deployment.Status.TerminatingReplicas, -1))
455459
}
456460
return nil
457461
}

0 commit comments

Comments
 (0)