Skip to content

Commit 41902a5

Browse files
committed
Fix sts.maxUnavailable calculation edge cases.
Fix maxSurge calculation to be capped at lws.replicas, and evaluate percentages against sts.replicas rather than lws.replicas. Also add unit tests to prevent regressions.
1 parent f483dee commit 41902a5

File tree

2 files changed

+86
-4
lines changed

2 files changed

+86
-4
lines changed

pkg/controllers/leaderworkerset_controller.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -804,14 +804,18 @@ func constructLeaderStatefulSetApplyConfiguration(lws *leaderworkerset.LeaderWor
804804

805805
podTemplateApplyConfiguration.WithAnnotations(podAnnotations)
806806

807-
lwsMaxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(&lws.Spec.RolloutStrategy.RollingUpdateConfiguration.MaxUnavailable, int(replicas), false)
807+
lwsReplicas := int(*lws.Spec.Replicas)
808+
lwsMaxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(&lws.Spec.RolloutStrategy.RollingUpdateConfiguration.MaxUnavailable, lwsReplicas, false)
808809
if err != nil {
809810
return nil, err
810811
}
811-
lwsMaxSurge, err := intstr.GetScaledValueFromIntOrPercent(&lws.Spec.RolloutStrategy.RollingUpdateConfiguration.MaxSurge, int(replicas), true)
812+
lwsMaxSurge, err := intstr.GetScaledValueFromIntOrPercent(&lws.Spec.RolloutStrategy.RollingUpdateConfiguration.MaxSurge, lwsReplicas, true)
812813
if err != nil {
813814
return nil, err
814815
}
816+
if lwsMaxSurge > lwsReplicas {
817+
lwsMaxSurge = lwsReplicas
818+
}
815819
stsMaxUnavailableInt := int32(lwsMaxUnavailable + lwsMaxSurge)
816820
// lwsMaxUnavailable=0 and lwsMaxSurge=0 together should be blocked by webhook,
817821
// but just in case, we'll make sure that stsMaxUnavailable is at least 1.

pkg/controllers/leaderworkerset_controller_test.go

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func TestLeaderStatefulSetApplyConfig(t *testing.T) {
6969
revisionKey string
7070
lws *leaderworkerset.LeaderWorkerSet
7171
wantApplyConfig *appsapplyv1.StatefulSetApplyConfiguration
72+
stsReplicas *int32
7273
}{
7374
{
7475
name: "1 replica, size 1, with empty leader template, exclusive placement disabled",
@@ -403,7 +404,9 @@ func TestLeaderStatefulSetApplyConfig(t *testing.T) {
403404
PodManagementPolicy: ptr.To[appsv1.PodManagementPolicyType](appsv1.ParallelPodManagement),
404405
UpdateStrategy: appsapplyv1.StatefulSetUpdateStrategy().
405406
WithType(appsv1.RollingUpdateStatefulSetStrategyType).
406-
WithRollingUpdate(appsapplyv1.RollingUpdateStatefulSetStrategy().WithPartition(0).WithMaxUnavailable(intstr.FromInt32(2))),
407+
// maxSurge is capped at 1 (the value of lwsReplicas),
408+
// so stsMaxUnavailableInt = 0 (lwsMaxUnavailable) + 1 (capped maxSurge) = 1.
409+
WithRollingUpdate(appsapplyv1.RollingUpdateStatefulSetStrategy().WithPartition(0).WithMaxUnavailable(intstr.FromInt32(1))),
407410
},
408411
},
409412
},
@@ -662,11 +665,86 @@ func TestLeaderStatefulSetApplyConfig(t *testing.T) {
662665
},
663666
},
664667
},
668+
{
669+
// Validates maxSurge uses lws replicas, not sts replicas.
670+
name: "1 maxUnavailable, 50% maxSurge, 2 lws replicas, 3 sts replicas currently",
671+
revisionKey: revisionKey2,
672+
stsReplicas: ptr.To[int32](3),
673+
lws: wrappers.BuildBasicLeaderWorkerSet("test-sample", "default").
674+
Replica(2).
675+
RolloutStrategy(leaderworkerset.RolloutStrategy{
676+
Type: leaderworkerset.RollingUpdateStrategyType,
677+
RollingUpdateConfiguration: &leaderworkerset.RollingUpdateConfiguration{
678+
MaxUnavailable: intstr.FromInt32(1),
679+
MaxSurge: intstr.FromString("50%"),
680+
},
681+
}).
682+
WorkerTemplateSpec(wrappers.MakeWorkerPodSpec()).
683+
Size(1).
684+
RestartPolicy(leaderworkerset.RecreateGroupOnPodRestart).Obj(),
685+
wantApplyConfig: &appsapplyv1.StatefulSetApplyConfiguration{
686+
TypeMetaApplyConfiguration: metaapplyv1.TypeMetaApplyConfiguration{
687+
Kind: ptr.To[string]("StatefulSet"),
688+
APIVersion: ptr.To[string]("apps/v1"),
689+
},
690+
ObjectMetaApplyConfiguration: &metaapplyv1.ObjectMetaApplyConfiguration{
691+
Name: ptr.To[string]("test-sample"),
692+
Namespace: ptr.To[string]("default"),
693+
Labels: map[string]string{
694+
"leaderworkerset.sigs.k8s.io/name": "test-sample",
695+
"leaderworkerset.sigs.k8s.io/template-revision-hash": revisionKey2,
696+
},
697+
Annotations: map[string]string{"leaderworkerset.sigs.k8s.io/replicas": "2"},
698+
},
699+
Spec: &appsapplyv1.StatefulSetSpecApplyConfiguration{
700+
Replicas: ptr.To[int32](3), // using stsReplicas
701+
Selector: &metaapplyv1.LabelSelectorApplyConfiguration{
702+
MatchLabels: map[string]string{
703+
"leaderworkerset.sigs.k8s.io/name": "test-sample",
704+
"leaderworkerset.sigs.k8s.io/worker-index": "0",
705+
},
706+
},
707+
Template: &coreapplyv1.PodTemplateSpecApplyConfiguration{
708+
ObjectMetaApplyConfiguration: &metaapplyv1.ObjectMetaApplyConfiguration{
709+
Labels: map[string]string{
710+
"leaderworkerset.sigs.k8s.io/name": "test-sample",
711+
"leaderworkerset.sigs.k8s.io/worker-index": "0",
712+
"leaderworkerset.sigs.k8s.io/template-revision-hash": revisionKey2,
713+
},
714+
Annotations: map[string]string{
715+
"leaderworkerset.sigs.k8s.io/size": "1",
716+
},
717+
},
718+
Spec: &coreapplyv1.PodSpecApplyConfiguration{
719+
Containers: []coreapplyv1.ContainerApplyConfiguration{
720+
{
721+
Name: ptr.To[string]("worker"),
722+
Image: ptr.To[string]("docker.io/nginxinc/nginx-unprivileged:1.27"),
723+
Ports: []coreapplyv1.ContainerPortApplyConfiguration{{ContainerPort: ptr.To[int32](8080), Protocol: ptr.To[corev1.Protocol](corev1.ProtocolTCP)}},
724+
Resources: &coreapplyv1.ResourceRequirementsApplyConfiguration{},
725+
},
726+
},
727+
},
728+
},
729+
ServiceName: ptr.To[string]("test-sample"),
730+
PodManagementPolicy: ptr.To[appsv1.PodManagementPolicyType](appsv1.ParallelPodManagement),
731+
UpdateStrategy: appsapplyv1.StatefulSetUpdateStrategy().
732+
WithType(appsv1.RollingUpdateStatefulSetStrategyType).
733+
// maxUnavailable=1, maxSurge=50% of 2 replicas (lwsReplicas) = 1.
734+
// So stsMaxUnavailableInt = 1 + 1 = 2
735+
WithRollingUpdate(appsapplyv1.RollingUpdateStatefulSetStrategy().WithPartition(0).WithMaxUnavailable(intstr.FromInt32(2))),
736+
},
737+
},
738+
},
665739
}
666740

667741
for _, tc := range tests {
668742
t.Run(tc.name, func(t *testing.T) {
669-
stsApplyConfig, err := constructLeaderStatefulSetApplyConfiguration(tc.lws, 0, *tc.lws.Spec.Replicas, tc.revisionKey)
743+
stsReplicas := *tc.lws.Spec.Replicas
744+
if tc.stsReplicas != nil {
745+
stsReplicas = *tc.stsReplicas
746+
}
747+
stsApplyConfig, err := constructLeaderStatefulSetApplyConfiguration(tc.lws, 0, stsReplicas, tc.revisionKey)
670748
if err != nil {
671749
t.Errorf("failed with error: %s", err.Error())
672750
}

0 commit comments

Comments
 (0)